diff --git a/.github/actions/build-sim/action.yml b/.github/actions/build-sim/action.yml index e342ad845..44aa589a2 100644 --- a/.github/actions/build-sim/action.yml +++ b/.github/actions/build-sim/action.yml @@ -17,8 +17,8 @@ runs: shell: bash run: | sudo apt-get update - sudo apt-get install -y gcc-arm-linux-gnueabihf libsdl2-image-dev libslirp-dev libpcsclite-dev ninja-build - pip install poetry + sudo apt-get install -y gcc-arm-linux-gnueabihf libsdl2-image-dev libslirp-dev libpcsclite-dev ninja-build libltdl-dev + pip install poetry uv wget https://github.com/protocolbuffers/protobuf/releases/download/v22.0/protoc-22.0-linux-x86_64.zip sudo unzip protoc-22.0-linux-x86_64.zip -d /usr/local protoc --version @@ -30,6 +30,9 @@ runs: git config --global user.email 'ci@ci.com' git config --global user.name 'ci' git config --global --add safe.directory "$GITHUB_WORKSPACE" + # Rewrite lwip URLs to github mirror to avoid stalling issue + git config --global --add url."https://github.com/lwip-tcpip/lwip.git".insteadOf "https://git.savannah.gnu.org/r/lwip.git" + git config --global --add url."https://github.com/lwip-tcpip/lwip.git".insteadOf "https://git.savannah.nongnu.org/git/lwip.git" cd test ./setup_environment.sh --"${{ inputs.name }}" cd .. diff --git a/.github/actions/install-sim/action.yml b/.github/actions/install-sim/action.yml index 3cc846811..9e9e14e4a 100644 --- a/.github/actions/install-sim/action.yml +++ b/.github/actions/install-sim/action.yml @@ -20,12 +20,16 @@ runs: - if: inputs.device == 'coldcard' shell: bash + env: + # Keep in sync with test/setup_environment.sh + COLDCARD_VERSION: "2025-09-30T1238-v5.4.4" run: | apt-get update apt-get install -y libpcsclite-dev libusb-1.0-0 swig git config --global user.email "ci@ci.com" git config --global user.name "ci" - pushd test/work; git clone --recursive https://github.com/Coldcard/firmware.git; popd + # Note: cannot use --shallow-submodules because lwip submodule on git.savannah.gnu.org doesn't support it + pushd test/work; git clone --recursive --depth 1 --branch ${COLDCARD_VERSION} https://github.com/Coldcard/firmware.git; popd tar -xvf coldcard-mpy.tar.gz pushd test/work/firmware; git am ../../data/coldcard-multisig.patch; popd poetry run pip install -r test/work/firmware/requirements.txt @@ -60,13 +64,13 @@ runs: apt-get update apt-get install -y libusb-1.0-0 qemu-user-static tar -xvf speculos.tar.gz - poetry run pip install construct flask-cors flask-restful jsonschema ledgered mnemonic pyelftools pillow requests pytesseract - pip install construct flask-cors flask-restful jsonschema ledgered mnemonic pyelftools pillow requests pytesseract + poetry run pip install -e test/work/speculos + pip install -e test/work/speculos - - if: inputs.device == 'ledger' + - if: startsWith(inputs.device, 'ledger') uses: actions/download-artifact@v4 with: - name: ${{ inputs.device == 'ledger-legacy' && 'ledger_app_nano_s' || 'ledger_app_nano_x' }} + name: ${{ inputs.device == 'ledger-legacy' && 'ledger_app_legacy' || 'ledger_app' }} - if: inputs.device == 'ledger' @@ -74,6 +78,11 @@ runs: run: | mv app.elf test/work/speculos/apps/btc-test.elf + - if: inputs.device == 'ledger-legacy' + shell: bash + run: | + mv app.elf test/work/speculos/apps/btc-test-legacy.elf + - if: inputs.device == 'keepkey' shell: bash run: | diff --git a/.github/actions/test-dist/action.yml b/.github/actions/test-dist/action.yml index 2330d8cfb..6675fe34e 100644 --- a/.github/actions/test-dist/action.yml +++ b/.github/actions/test-dist/action.yml @@ -46,19 +46,19 @@ runs: if: matrix.test.script == 'Wheel' shell: bash run: | - cd test; ./run_tests.py $DEVICE --interface=cli --device-only; cd .. + cd test; ./run_tests.py --${{ matrix.device }} --interface=cli --device-only; cd .. - name: Run tests (Sdist) if: matrix.test.script == 'Sdist' shell: bash run: | - cd test; ./run_tests.py $DEVICE --interface=cli --device-only; cd .. + cd test; ./run_tests.py --${{ matrix.device }} --interface=cli --device-only; cd .. - name: Run tests (Bindist) if: matrix.test.script == 'Bindist' shell: bash run: | - cd test; poetry run ./run_tests.py $DEVICE --interface=bindist --device-only; cd .. + cd test; poetry run ./run_tests.py --${{ matrix.device }} --interface=bindist --device-only; cd .. - if: failure() shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6cc8fb07..10e2c5153 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,10 +114,7 @@ jobs: with: sim: trezor include: ${{ needs.prepare-sim-matrices.outputs.trezor }} - # Ubuntu 22.04 ships with glibc 2.35, which is needed to keep Trezor 1 - # binaries compatible with Debian Bookworm (glibc 2.36) Python containers. - # Trezor T binaries don't need this. - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest sim-builder-coldcard: name: Coldcard sim builder @@ -126,7 +123,7 @@ jobs: with: sim: coldcard include: ${{ needs.prepare-sim-matrices.outputs.coldcard }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest sim-builder-bitbox: name: Bitbox sim builder @@ -162,27 +159,23 @@ jobs: with: sim: keepkey include: ${{ needs.prepare-sim-matrices.outputs.keepkey }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest - ledger-s-app-builder: - name: Ledger Nano S Bitcoin App builder - uses: ./.github/workflows/ledger-app-builder.yml + ledger-legacy-app-builder: + name: Ledger Bitcoin Legacy App builder + uses: ./.github/workflows/ledger-legacy-app-builder.yml with: - app: nano_s runs-on: ubuntu-latest - ledger-x-app-builder: - name: Ledger Nano X Bitcoin App builder + ledger-app-builder: + name: Ledger Bitcoin App builder uses: ./.github/workflows/ledger-app-builder.yml with: - app: nano_x runs-on: ubuntu-latest bitcoind-builder: name: bitcoind builder - # Ubuntu 22.04 ships with glibc 2.35, which is needed to keep binaries - # compatible with Debian Bookworm (glibc 2.36) Python containers. - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/build-bitcoind @@ -201,16 +194,16 @@ jobs: device: trezor-t runs-on: ubuntu-latest - test-ledger-s: + test-ledger-legacy: uses: ./.github/workflows/device-test.yml - needs: [sim-builder-ledger, ledger-s-app-builder, bitcoind-builder, dist-builder] + needs: [sim-builder-ledger, ledger-legacy-app-builder, bitcoind-builder, dist-builder] with: device: ledger-legacy runs-on: ubuntu-latest - test-ledger-x: + test-ledger: uses: ./.github/workflows/device-test.yml - needs: [sim-builder-ledger, ledger-x-app-builder, bitcoind-builder, dist-builder] + needs: [sim-builder-ledger, ledger-app-builder, bitcoind-builder, dist-builder] with: device: ledger runs-on: ubuntu-latest @@ -220,7 +213,7 @@ jobs: needs: [sim-builder-coldcard, bitcoind-builder, dist-builder] with: device: coldcard - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest test-bitbox01: uses: ./.github/workflows/device-test.yml diff --git a/.github/workflows/ledger-app-builder.yml b/.github/workflows/ledger-app-builder.yml index b64a33e3e..d873a2411 100644 --- a/.github/workflows/ledger-app-builder.yml +++ b/.github/workflows/ledger-app-builder.yml @@ -1,10 +1,7 @@ -name: Ledger App Builder +name: Ledger Nano X App Builder on: workflow_call: inputs: - app: - required: true # 'nano_s' or 'nano_x' - type: string runs-on: required: false type: string @@ -12,15 +9,17 @@ on: jobs: build: - name: Build ${{ inputs.app }} + name: Build Bitcoin App runs-on: ${{ inputs.runs-on }} - container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + # Pin to 4.23.0 for SDK v25.9.0 compatibility with Speculos v0.25.10 + container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:4.23.0 steps: - run: | - git clone https://github.com/LedgerHQ/app-bitcoin-new.git + # Pin to v2.4.1 - last version that worked with HWI CI (PR #795 merged Sept 2025) + git clone --branch 2.4.1 --depth 1 https://github.com/LedgerHQ/app-bitcoin-new.git cd app-bitcoin-new - make DEBUG=1 ${{ inputs.app == 'nano_x' && 'BOLOS_SDK=$NANOX_SDK' || '' }} + make DEBUG=1 BOLOS_SDK=$NANOX_SDK - uses: actions/upload-artifact@v4 with: - name: ${{ inputs.app == 'nano_x' && 'ledger_app_nano_x' || 'ledger_app_nano_s' }} + name: ledger_app path: app-bitcoin-new/bin/app.elf diff --git a/.github/workflows/ledger-legacy-app-builder.yml b/.github/workflows/ledger-legacy-app-builder.yml new file mode 100644 index 000000000..aac5afb1a --- /dev/null +++ b/.github/workflows/ledger-legacy-app-builder.yml @@ -0,0 +1,25 @@ +name: Ledger App Builder +on: + workflow_call: + inputs: + runs-on: + required: false + type: string + default: ubuntu-latest + +jobs: + build: + name: Build Bitcoin Legacy App + runs-on: ${{ inputs.runs-on }} + # Pin to 4.23.0 for SDK v25.9.0 compatibility with Speculos v0.25.10 + container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:4.23.0 + steps: + - run: | + # Pin to legacy-1.6.6 HEAD commit for reproducibility + git clone --depth 1 https://github.com/LedgerHQ/app-bitcoin.git -b legacy-1.6.6 + cd app-bitcoin + make DEBUG=1 BOLOS_SDK=$NANOSP_SDK + - uses: actions/upload-artifact@v4 + with: + name: ledger_app_legacy + path: app-bitcoin/bin/app.elf diff --git a/poetry.lock b/poetry.lock index 8f1173a36..88af47e0e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1311,4 +1311,4 @@ qt = ["pyside2"] [metadata] lock-version = "2.1" python-versions = "^3.9,<3.13" -content-hash = "536fcc537f47e6fd969f84474533853a87cfc8b60613b7dea5f88ecbea8365d0" +content-hash = "ffa2aebd594ec1a5db15d7325c7bb26036b593faccf363163379e276bff1c191" diff --git a/pyproject.toml b/pyproject.toml index 76d8f09ce..a615cb17d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ mnemonic = "~0" typing-extensions = "^4.4" libusb1 = ">=1.7,<4" pyside2 = { version = "^5.14.0", optional = true, python = "<3.10" } -cbor2 = "^5.4.6" +cbor2 = ">=5.4.6,<5.8" pyserial = "^3.5" dataclasses = {version = "^0.8", python = ">=3.6,<3.7"} semver = "^3.0.1" diff --git a/setup.py b/setup.py index 47bef782d..6d301e0f6 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ modules = \ ['hwi', 'hwi-qt'] install_requires = \ -['cbor2>=5.4.6,<6.0.0', +['cbor2>=5.4.6,<5.8', 'ecdsa>=0,<1', 'hidapi>=0.14.0', 'libusb1>=1.7,<4', diff --git a/test/data/coldcard-multisig.patch b/test/data/coldcard-multisig.patch index b9ad6477c..05216356f 100644 --- a/test/data/coldcard-multisig.patch +++ b/test/data/coldcard-multisig.patch @@ -1,14 +1,14 @@ -From fd51e85693e0d66129133b1f195134aead1cf7d0 Mon Sep 17 00:00:00 2001 -From: Andrew Chow -Date: Tue, 17 Dec 2019 17:56:05 -0500 -Subject: [PATCH 2/3] Change default simulator multisig +From 038e4b4f5e8128f3910745324332770b0973408c Mon Sep 17 00:00:00 2001 +From: Ava Chow +Date: Wed, 1 Oct 2025 13:30:01 -0700 +Subject: [PATCH 1/2] Change default simulator multisig --- unix/variant/sim_settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/unix/variant/sim_settings.py b/unix/variant/sim_settings.py -index 2706fb4..f9b533d 100644 +index 01392d8f..527d0d9b 100644 --- a/unix/variant/sim_settings.py +++ b/unix/variant/sim_settings.py @@ -71,7 +71,11 @@ if '--ms' in sys.argv: @@ -18,27 +18,28 @@ index 2706fb4..f9b533d 100644 - sim_defaults['multisig'] = [['MeMyself', [2, 4], [[3503269483, 'tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9'], [2389277556, 'tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc'], [3190206587, 'tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa'], [1130956047, 'tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n']], {'ch': 'XTN', 'pp': "45'"}]] + sim_defaults['multisig'] = [ + ['mstest', [2, 3], [[1130956047, 0, 'tpubDF2rnouQaaYrR9x68P5Jm8WjhCE4atyGiPviFA9ve5iMnYbkTjof2HjzejcQcD7getPusDLPsWJLN2UttzK3pyVgBkRs52MiRZM7ZJ8TrEq'], [1130956047, 1, 'tpubDETRnZNJAqXiVeiL8UMDzCTBAoh3JvZkgXLdb1K2xzpJLepuJ6ka8jnVyRSkVh8Nbbo8u8dobZCsNENmRKipLzHNsS5mccjKSpXgSgavTQe'], [1130956047, 2, 'tpubDF3hdPQ5oDhtYjjaC596pboPii7UZmjqZcBPBRAbb6Bgn9hKoFxb8zWsBfdiCnTq3htUs2Yi2reeG3kMqHzZGZykJQAB5aKrJ8UfiXjmaLD']], {'ft': 8, 'ch': 'XTN', "d": ["48'/1'/0'/0'", "48'/1'/1'/0'", "48'/1'/2'/0'"]}], -+ ['mstest1', [2, 3], [[1130956047, 0, 'tpubDF2rnouQaaYrUEy2JM1YD3RFzew4onawGM4X2Re67gguTf5CbHonBRiFGe3Xjz7DK88dxBFGf2i7K1hef3PM4cFKyUjcbJXddaY9F5tJBoP'], [1130956047, 1, 'tpubDETRnZNJAqXiVeiL8UMDzCTBAoh3JvZkgXLdb1K2xzpJLepuJ6ka8jnVyRSkVh8Nbbo8u8dobZCsNENmRKipLzHNsS5mccjKSpXgSgavTQe'], [1130956047, 2, 'tpubDF3hdPQ5oDhtYjjaC596pboPii7UZmjqZcBPBRAbb6Bgn9hKoFxb8zWsBfdiCnTq3htUs2Yi2reeG3kMqHzZGZykJQAB5aKrJ8UfiXjmaLD']], {'ft': 14, 'ch': 'XTN', "d": ["48'/1'/0'/0'", "48'/1'/1'/0'", "48'/1'/2'/0'"]}], -+ ['mstest2', [2, 3], [[1130956047, 0, 'tpubDF2rnouQaaYrXF4noGTv6rQYmx87cQ4GrUdhpvXkhtChwQPbdGTi8GA88NUaSrwZBwNsTkC9bFkkC8vDyGBVVAQTZ2AS6gs68RQXtXcCvkP'], [1130956047, 1, 'tpubDETRnZNJAqXiVeiL8UMDzCTBAoh3JvZkgXLdb1K2xzpJLepuJ6ka8jnVyRSkVh8Nbbo8u8dobZCsNENmRKipLzHNsS5mccjKSpXgSgavTQe'], [1130956047, 2, 'tpubDF3hdPQ5oDhtYjjaC596pboPii7UZmjqZcBPBRAbb6Bgn9hKoFxb8zWsBfdiCnTq3htUs2Yi2reeG3kMqHzZGZykJQAB5aKrJ8UfiXjmaLD']], {'ft': 26, 'ch': 'XTN', "d": ["48'/1'/0'/0'", "48'/1'/1'/0'", "48'/1'/2'/0'"]}], ++ ['mstest1', [2, 3], [[1130956047, 0, 'tpubDF2rnouQaaYrR9x68P5Jm8WjhCE4atyGiPviFA9ve5iMnYbkTjof2HjzejcQcD7getPusDLPsWJLN2UttzK3pyVgBkRs52MiRZM7ZJ8TrEq'], [1130956047, 1, 'tpubDETRnZNJAqXiVeiL8UMDzCTBAoh3JvZkgXLdb1K2xzpJLepuJ6ka8jnVyRSkVh8Nbbo8u8dobZCsNENmRKipLzHNsS5mccjKSpXgSgavTQe'], [1130956047, 2, 'tpubDF3hdPQ5oDhtYjjaC596pboPii7UZmjqZcBPBRAbb6Bgn9hKoFxb8zWsBfdiCnTq3htUs2Yi2reeG3kMqHzZGZykJQAB5aKrJ8UfiXjmaLD']], {'ft': 14, 'ch': 'XTN', "d": ["48'/1'/0'/0'", "48'/1'/1'/0'", "48'/1'/2'/0'"]}], ++ ['mstest2', [2, 3], [[1130956047, 0, 'tpubDF2rnouQaaYrR9x68P5Jm8WjhCE4atyGiPviFA9ve5iMnYbkTjof2HjzejcQcD7getPusDLPsWJLN2UttzK3pyVgBkRs52MiRZM7ZJ8TrEq'], [1130956047, 1, 'tpubDETRnZNJAqXiVeiL8UMDzCTBAoh3JvZkgXLdb1K2xzpJLepuJ6ka8jnVyRSkVh8Nbbo8u8dobZCsNENmRKipLzHNsS5mccjKSpXgSgavTQe'], [1130956047, 2, 'tpubDF3hdPQ5oDhtYjjaC596pboPii7UZmjqZcBPBRAbb6Bgn9hKoFxb8zWsBfdiCnTq3htUs2Yi2reeG3kMqHzZGZykJQAB5aKrJ8UfiXjmaLD']], {'ft': 26, 'ch': 'XTN', "d": ["48'/1'/0'/0'", "48'/1'/1'/0'", "48'/1'/2'/0'"]}], + ] sim_defaults['fee_limit'] = -1 if '--xfp' in sys.argv: -- -2.38.1 +2.51.0 -From 8b4323c1e393d79d46248dd822ca9aaaeb2b2bc3 Mon Sep 17 00:00:00 2001 + +From 3f7b6dfcf72788a4a0784eb871462c289e8a747b Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 23 Jul 2025 10:16:22 +0200 -Subject: [PATCH] Allow multisigs to share master fingerprint +Subject: [PATCH 2/2] Allow multisigs to share master fingerprint Co-Authored-By: Ava Chow --- - shared/multisig.py | 37 ++++++++++++++++++++++++------------- - 1 file changed, 24 insertions(+), 13 deletions(-) + shared/multisig.py | 39 ++++++++++++++++++++++++--------------- + 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/shared/multisig.py b/shared/multisig.py -index 446998a2..cabb2003 100644 +index 1e692d41..6294e9a3 100644 --- a/shared/multisig.py +++ b/shared/multisig.py @@ -144,9 +144,9 @@ class MultisigWallet(WalletABC): @@ -53,7 +54,7 @@ index 446998a2..cabb2003 100644 @classmethod def render_addr_fmt(cls, addr_fmt): -@@ -270,7 +270,11 @@ class MultisigWallet(WalletABC): +@@ -275,7 +275,11 @@ class MultisigWallet(WalletABC): def get_xfp_paths(self): # return list of lists [xfp, *deriv] @@ -65,8 +66,8 @@ index 446998a2..cabb2003 100644 + return ret @classmethod - def find_match(cls, M, N, xfp_paths, addr_fmt=None): -@@ -305,24 +309,31 @@ class MultisigWallet(WalletABC): + def find_match(cls, M, N, xfp_paths, addr_fmts=None): +@@ -305,24 +309,29 @@ class MultisigWallet(WalletABC): # the same prefix path per-each xfp, as indicated # xfp_paths (unordered)? # - could also check non-prefix part is all non-hardened @@ -79,20 +80,25 @@ index 446998a2..cabb2003 100644 if x[0] not in self.xfp_paths: return False - prefix = self.xfp_paths[x[0]] +- +- if len(x) < len(prefix): +- # PSBT specs a path shorter than wallet's xpub +- #print('path len: %d vs %d' % (len(prefix), len(x))) +- return False +- +- comm = len(prefix) +- if tuple(prefix[:comm]) != tuple(x[:comm]): +- # xfp => maps to wrong path +- #print('path mismatch:\n%r\n%r\ncomm=%d' % (prefix[:comm], x[:comm], comm)) + for prefix in self.xfp_paths[x[0]]: + if len(x) < len(prefix): + # PSBT specs a path shorter than wallet's xpub + #print('path len: %d vs %d' % (len(prefix), len(x))) + return False - -- if len(x) < len(prefix): -- # PSBT specs a path shorter than wallet's xpub -- #print('path len: %d vs %d' % (len(prefix), len(x))) -- return False ++ + comm = len(prefix) + if tuple(prefix[:comm]) != tuple(x[:comm]): + # xfp => maps to wrong path -+ # But maybe there is another path that does match, so keep going + #print('path mismatch:\n%r\n%r\ncomm=%d' % (prefix[:comm], x[:comm], comm)) + continue + else: @@ -100,14 +106,9 @@ index 446998a2..cabb2003 100644 + break + else: + # No match was found - -- comm = len(prefix) -- if tuple(prefix[:comm]) != tuple(x[:comm]): -- # xfp => maps to wrong path -- #print('path mismatch:\n%r\n%r\ncomm=%d' % (prefix[:comm], x[:comm], comm)) return False return True -- -2.39.5 (Apple Git-154) +2.51.0 diff --git a/test/data/nanopb-deprecated-mode.patch b/test/data/nanopb-deprecated-mode.patch new file mode 100644 index 000000000..89b319016 --- /dev/null +++ b/test/data/nanopb-deprecated-mode.patch @@ -0,0 +1,25 @@ +From 4f69dc003f6f1092cbcdb0152fdee0e303583c8b Mon Sep 17 00:00:00 2001 +From: Ava Chow +Date: Thu, 2 Oct 2025 12:42:50 -0700 +Subject: [PATCH] Remove deprecated file mode + +--- + generator/nanopb_generator.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/generator/nanopb_generator.py b/generator/nanopb_generator.py +index 12af67e..7107198 100755 +--- a/generator/nanopb_generator.py ++++ b/generator/nanopb_generator.py +@@ -1625,7 +1625,7 @@ def parse_file(filename, fdesc, options): + optfilename = os.path.join(p, optfilename) + if options.verbose: + sys.stderr.write('Reading options from ' + optfilename + '\n') +- Globals.separate_options = read_options_file(open(optfilename, "rU")) ++ Globals.separate_options = read_options_file(open(optfilename, "r")) + break + else: + # If we are given a full filename and it does not exist, give an error. +-- +2.51.0 + diff --git a/test/data/speculos-automation.json b/test/data/speculos-automation.json index 0e53a29d4..f17a9c668 100644 --- a/test/data/speculos-automation.json +++ b/test/data/speculos-automation.json @@ -2,14 +2,47 @@ "version": 1, "rules": [ { - "regexp": "^(Address|Review|Amount|Fee|Confirm|The derivation|Derivation path|Reject if you're|The change path|Change path|external inputs|Register wallet|Register account|Policy map|Key|Path|Public key|Spend from|Account name|Wallet name|Wallet policy|Descriptor template).*", + "text": "Confirm", + "x": 43, "y": 37, + "actions": [ + [ "button", 1, true ], + [ "button", 2, true ], + [ "button", 1, false ], + [ "button", 2, false ] + ] + }, + { + "text": "Confirm account ", + "actions": [ + [ "button", 1, true ], + [ "button", 2, true ], + [ "button", 1, false ], + [ "button", 2, false ] + ] + }, + { + "regexp": "^. of . Multisig$", + "actions": [ + [ "button", 2, true ], + [ "button", 2, false ] + ] + }, + { + "regexp": "^(Address|Review|Amount|Fee|Confirm|The derivation|Derivation path|Reject if|The change path|Change path|Register wallet|Policy map|Key|Path|Public key|Spend from|Wallet name|Wallet policy|Descriptor template|Verify Bitcoin|To|Output|Warning).*", + "actions": [ + [ "button", 2, true ], + [ "button", 2, false ] + ] + }, + { + "regexp": "^(Message)$", "actions": [ [ "button", 2, true ], [ "button", 2, false ] ] }, { - "regexp": "^(Accept|Approve|Continue).*", + "regexp": "^(Accept|Approve|Continue|Sign message|Sign transaction|Register account).*", "actions": [ [ "button", 1, true ], [ "button", 2, true ], diff --git a/test/data/trezor-t-build.patch b/test/data/trezor-t-build.patch new file mode 100644 index 000000000..52121d626 --- /dev/null +++ b/test/data/trezor-t-build.patch @@ -0,0 +1,24 @@ +From 09276b3ab9be5e37cf70ca4adf7e0c1ebf27e9d3 Mon Sep 17 00:00:00 2001 +From: Ava Chow +Date: Wed, 1 Oct 2025 15:50:09 -0700 +Subject: [PATCH] Remove rust panic_immediate_abort + +--- + core/site_scons/tools.py | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/core/site_scons/tools.py b/core/site_scons/tools.py +index 95461bcbd..5ec681d30 100644 +--- a/core/site_scons/tools.py ++++ b/core/site_scons/tools.py +@@ -169,7 +169,6 @@ def add_rust_lib(*, env, build, profile, features, all_paths, build_dir): + "--no-default-features", + "--features " + ",".join(lib_features), + "-Z build-std=core", +- "-Z build-std-features=panic_immediate_abort", + ] + build_cmd = f"cargo build {profile} " + " ".join(cargo_opts) + +-- +2.51.0 + diff --git a/test/setup_environment.sh b/test/setup_environment.sh index b35bb24c8..7d602a5f1 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -64,10 +64,21 @@ set -ex mkdir -p work cd work +# Pinned firmware versions +TREZOR_VERSION="core/v2.9.6" +BITBOX01_VERSION="v7.1.0" +BITBOX02_VERSION="firmware/v9.24.0" +KEEPKEY_VERSION="v7.10.0" +SPECULOS_VERSION="v0.25.10" # Last version supporting Python 3.9 (v0.25.11+ requires >=3.10) +JADE_VERSION="1.0.36" + +# Keep COLDCARD_VERSION in sync with .github/actions/install-sim/action.yml +COLDCARD_VERSION="2025-09-30T1238-v5.4.4" + if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then # Clone trezor-firmware if it doesn't exist, or update it if it does if [ ! -d "trezor-firmware" ]; then - git clone --recursive https://github.com/trezor/trezor-firmware.git + git clone --recursive --depth 1 --shallow-submodules --branch ${TREZOR_VERSION} https://github.com/trezor/trezor-firmware.git cd trezor-firmware else cd trezor-firmware @@ -92,13 +103,12 @@ if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then if [[ -n ${build_trezor_1} ]]; then # Build trezor one emulator. This is pretty fast, so rebuilding every time is ok # But there should be some caching that makes this faster - poetry install + uv sync cd legacy export EMULATOR=1 TREZOR_TRANSPORT_V1=1 DEBUG_LINK=1 HEADLESS=1 export CC=gcc-12 - poetry run pip install protobuf==3.20.0 - poetry run script/setup - poetry run script/cibuild + uv run script/setup + uv run script/cibuild # Delete any emulator.img file find . -name "emulator.img" -exec rm {} \; cd .. @@ -113,10 +123,10 @@ if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu # Build trezor t emulator. This is pretty fast, so rebuilding every time is ok # But there should be some caching that makes this faster - poetry install + git am ../../data/trezor-t-build.patch + uv sync cd core - export CC=gcc-12 - poetry run make build_unix + uv run make build_unix # Delete any emulator.img file find . -name "trezor.flash" -exec rm {} \; cd .. @@ -128,7 +138,8 @@ if [[ -n ${build_coldcard} ]]; then # Clone coldcard firmware if it doesn't exist, or update it if it does coldcard_setup_needed=false if [ ! -d "firmware" ]; then - git clone --recursive https://github.com/Coldcard/firmware.git + # Note: cannot use --shallow-submodules because lwip submodule on git.savannah.gnu.org doesn't support it + git clone --recursive --depth 1 --branch ${COLDCARD_VERSION} https://github.com/Coldcard/firmware.git cd firmware coldcard_setup_needed=true else @@ -157,6 +168,10 @@ if [[ -n ${build_coldcard} ]]; then pip install -r requirements.txt cd unix if [ "$coldcard_setup_needed" == true ] ; then + pushd ../external/micropython + # Apply Ubuntu 24.04 compiler warning fixes (included in ColdCard firmware v5.4.4+) + git apply ../../ubuntu24_mpy.patch + popd pushd ../external/micropython/mpy-cross/ make popd @@ -170,7 +185,7 @@ fi if [[ -n ${build_bitbox01} ]]; then # Clone digital bitbox firmware if it doesn't exist, or update it if it does if [ ! -d "mcu" ]; then - git clone --recursive https://github.com/digitalbitbox/mcu.git + git clone --recursive --branch ${BITBOX01_VERSION} https://github.com/digitalbitbox/mcu.git cd mcu else cd mcu @@ -202,7 +217,7 @@ if [[ -n ${build_keepkey} ]]; then # Clone keepkey firmware if it doesn't exist, or update it if it does keepkey_setup_needed=false if [ ! -d "keepkey-firmware" ]; then - git clone --recursive https://github.com/keepkey/keepkey-firmware.git + git clone --recursive --depth 1 --shallow-submodules --branch ${KEEPKEY_VERSION} https://github.com/keepkey/keepkey-firmware.git cd keepkey-firmware keepkey_setup_needed=true else @@ -237,7 +252,9 @@ if [[ -n ${build_keepkey} ]]; then git clean -ffdx git clone https://github.com/nanopb/nanopb.git -b nanopb-0.3.9.4 fi - cd nanopb/generator/proto + cd nanopb + git am ../../../data/nanopb-deprecated-mode.patch + cd generator/proto make cd ../../../ export PATH=$PATH:`pwd`/nanopb/generator @@ -249,15 +266,11 @@ if [[ -n ${build_keepkey} ]]; then fi if [[ -n ${build_ledger} ]]; then - speculos_packages="construct flask-cors flask-restful jsonschema mnemonic pyelftools pillow requests pytesseract" - poetry run pip install ${speculos_packages} - pip install ${speculos_packages} # Clone ledger simulator Speculos if it doesn't exist, or update it if it does if [ ! -d "speculos" ]; then - git clone --recursive https://github.com/LedgerHQ/speculos.git - cd speculos + git clone --recursive --depth 1 --shallow-submodules --branch ${SPECULOS_VERSION} https://github.com/LedgerHQ/speculos.git else - cd speculos + pushd speculos git fetch # Determine if we need to pull. From https://stackoverflow.com/a/3278427 @@ -271,12 +284,19 @@ if [[ -n ${build_ledger} ]]; then elif [ $LOCAL = $BASE ]; then git pull fi + popd fi + poetry run pip install -e ./speculos + pip install -e ./speculos + + cd speculos + # Build the simulator. This is cached, but it is also fast mkdir -p build cmake -Bbuild -S . make -C build/ + cd .. fi @@ -286,7 +306,7 @@ if [[ -n ${build_jade} ]]; then # Clone Blockstream Jade firmware if it doesn't exist, or update it if it does if [ ! -d "jade" ]; then - git clone --recursive --branch master https://github.com/Blockstream/Jade.git ./jade + git clone --recursive --branch ${JADE_VERSION} https://github.com/Blockstream/Jade.git ./jade cd jade else cd jade @@ -388,7 +408,7 @@ fi if [[ -n ${build_bitbox02} ]]; then # Clone digital bitbox02 firmware if it doesn't exist, or update it if it does if [ ! -d "bitbox02-firmware" ]; then - git clone --recursive https://github.com/BitBoxSwiss/bitbox02-firmware.git + git clone --recursive --branch ${BITBOX02_VERSION} https://github.com/BitBoxSwiss/bitbox02-firmware.git cd bitbox02-firmware else cd bitbox02-firmware diff --git a/test/test_bitbox02.py b/test/test_bitbox02.py index c1cd7f63f..a9d64b999 100644 --- a/test/test_bitbox02.py +++ b/test/test_bitbox02.py @@ -4,6 +4,7 @@ import os import subprocess import time +import threading import unittest import sys import argparse @@ -22,6 +23,11 @@ # Class for emulator control class BitBox02Emulator(DeviceEmulator): + # Maximum time to wait for simulator startup (5 minutes) + MAX_STARTUP_TIME = 300 + # Maximum time to wait for simulator shutdown + MAX_SHUTDOWN_TIME = 30 + def __init__(self, simulator): self.simulator = simulator self.path = "127.0.0.1:15423" @@ -55,16 +61,38 @@ def start(self): if self.simulator_proc.poll() is not None: raise RuntimeError(f"BitBox02 simulator failed with exit code {self.simulator_proc.poll()}") - self.setup_client = Bitbox02Client(self.path) - self.setup_bb02 = self.setup_client.restore_device() - self.setup_client.close() + # Run restore_device in a thread with timeout to prevent CI from hanging + setup_error = None + + def do_restore(): + nonlocal setup_error + try: + self.setup_client = Bitbox02Client(self.path) + self.setup_bb02 = self.setup_client.restore_device() + self.setup_client.close() + except Exception as e: + setup_error = e + + restore_thread = threading.Thread(target=do_restore) + restore_thread.start() + restore_thread.join(timeout=self.MAX_STARTUP_TIME) + + if restore_thread.is_alive(): + self.simulator_proc.terminate() + raise RuntimeError("BitBox02 simulator startup timed out after 5 minutes") + if setup_error: + raise setup_error atexit.register(self.stop) def stop(self): super().stop() self.simulator_proc.terminate() - self.simulator_proc.wait() + try: + self.simulator_proc.wait(timeout=self.MAX_SHUTDOWN_TIME) + except subprocess.TimeoutExpired: + self.simulator_proc.kill() + self.simulator_proc.wait(timeout=5) self.log.close() atexit.unregister(self.stop) diff --git a/test/test_coldcard.py b/test/test_coldcard.py index 0369497e5..c30742b3d 100755 --- a/test/test_coldcard.py +++ b/test/test_coldcard.py @@ -145,9 +145,9 @@ def coldcard_test_suite(simulator, bitcoind, interface): dev_emulator = ColdcardSimulator(simulator) signtx_cases = [ - (["legacy"], ["legacy"], True, False), - (["segwit"], ["segwit"], True, False), - (["legacy", "segwit"], ["legacy", "segwit"], True, False), + (["legacy"], ["legacy"], False, False), + (["segwit"], ["segwit"], False, False), + (["legacy", "segwit"], ["legacy", "segwit"], False, False), ] # Generic device tests diff --git a/test/test_device.py b/test/test_device.py index 3e5add321..d22fdc386 100644 --- a/test/test_device.py +++ b/test/test_device.py @@ -280,6 +280,7 @@ def test_getkeypool(self): for arg in getkeypool_args: with self.subTest(addrtype=arg[0]): desc = self.do_command(self.dev_args + ["getkeypool", "--addr-type", arg[0], "0", "20"]) + self.assertIsInstance(desc, list, f"getkeypool returned error: {desc}") import_result = self.wrpc.importdescriptors(desc) self.assertTrue(import_result[0]["success"]) for _ in range(0, 21): @@ -294,6 +295,7 @@ def test_getkeypool(self): self.assertEqual(all_keypool_desc, descs) keypool_desc = self.do_command(self.dev_args + ['getkeypool', "--addr-type", "sh_wit", '--account', '3', '0', '20']) + self.assertIsInstance(keypool_desc, list, f"getkeypool returned error: {keypool_desc}") import_result = self.wrpc.importdescriptors(keypool_desc) self.assertTrue(import_result[0]['success']) for _ in range(0, 21): @@ -302,6 +304,7 @@ def test_getkeypool(self): addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('p2sh-segwit')) self.assertTrue(addr_info['hdkeypath'].startswith("m/49h/1h/3h/1/")) keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--account', '3', '0', '20']) + self.assertIsInstance(keypool_desc, list, f"getkeypool returned error: {keypool_desc}") import_result = self.wrpc.importdescriptors(keypool_desc) self.assertTrue(import_result[0]['success']) for _ in range(0, 21): @@ -311,6 +314,7 @@ def test_getkeypool(self): self.assertTrue(addr_info['hdkeypath'].startswith("m/84h/1h/3h/1/")) keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--path', 'm/0h/0h/4h/*', '0', '20']) + self.assertIsInstance(keypool_desc, list, f"getkeypool returned error: {keypool_desc}") import_result = self.wrpc.importdescriptors(keypool_desc) self.assertTrue(import_result[0]['success']) for _ in range(0, 21): @@ -481,6 +485,7 @@ def _make_multisig(self, addrtype): def _test_signtx(self, input_types, multisig_types, external, op_return: bool): # Import some keys to the watch only wallet and send coins to them keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--all', '30', '50']) + self.assertIsInstance(keypool_desc, list, f"getkeypool returned error: {keypool_desc}") import_result = self.wrpc.importdescriptors(keypool_desc) self.assertTrue(import_result[0]['success']) sh_wpkh_addr = self.wrpc.getnewaddress('', 'p2sh-segwit') @@ -602,6 +607,7 @@ def test_big_tx(self): # make a huge transaction keypool_desc = self.do_command(self.dev_args + ["getkeypool", "--account", "10", "--addr-type", "sh_wit", "0", "100"]) + self.assertIsInstance(keypool_desc, list, f"getkeypool returned error: {keypool_desc}") import_result = self.wrpc.importdescriptors(keypool_desc) self.assertTrue(import_result[0]['success']) outputs = [] diff --git a/test/test_jade.py b/test/test_jade.py index 71422eab7..238c88f37 100755 --- a/test/test_jade.py +++ b/test/test_jade.py @@ -80,9 +80,13 @@ def start(self): ) time.sleep(5) - # Wait for emulator to be up + # Wait for emulator to be up (max 5 minutes) + MAX_STARTUP_TIME = 300 + start_time = time.time() while True: # Prevent CI from lingering until timeout: + if time.time() - start_time > MAX_STARTUP_TIME: + raise RuntimeError("Jade simulator startup timed out after 5 minutes") if self.emulator_proc.poll() is not None: raise RuntimeError(f"Jade simulator failed with exit code {self.emulator_proc.poll()}") diff --git a/test/test_ledger.py b/test/test_ledger.py index c63bb5853..6ab162e74 100755 --- a/test/test_ledger.py +++ b/test/test_ledger.py @@ -51,8 +51,8 @@ def __init__(self, path, legacy=False): def start(self): super().start() automation_path = os.path.abspath("data/speculos-automation.json") - app_path = "./apps/nanos#btc#2.0#ce796c1b.elf" if self.legacy else "./apps/btc-test.elf" - os.environ["SPECULOS_APPNAME"] = "Bitcoin Test:1.6.0" if self.legacy else "Bitcoin Test:2.4.1" + app_path = f"./apps/btc-test{'-legacy' if self.legacy else ''}.elf" + os.environ["SPECULOS_APPNAME"] = "Bitcoin Test:1.6.6" if self.legacy else "Bitcoin Test:2.4.1" self.emulator_stderr = open('ledger-emulator.stderr', 'a') # Start the emulator @@ -70,7 +70,7 @@ def start(self): 'seproxyhal:DEBUG', '--api-port', '0', - '--model', 'nanos' if self.legacy else 'nanox', + '--model', 'nanosp' if self.legacy else 'nanox', app_path ], cwd=os.path.dirname(self.emulator_path),