From c07064399dcfd98b9ec90e534203b35c31a4eb92 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 1 Oct 2025 13:32:40 -0700 Subject: [PATCH 01/23] test, coldcard: Update multisig patch --- test/data/coldcard-multisig.patch | 57 ++++++++++++++++--------------- 1 file changed, 29 insertions(+), 28 deletions(-) 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 From 11ade3adc932a297efd5635421f46dc65f036a69 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 1 Oct 2025 13:52:32 -0700 Subject: [PATCH 02/23] test, trezor: Change to uv Trezor switched to uv for dependency management --- .github/actions/build-sim/action.yml | 2 +- test/setup_environment.sh | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/actions/build-sim/action.yml b/.github/actions/build-sim/action.yml index e342ad845..ab90a5358 100644 --- a/.github/actions/build-sim/action.yml +++ b/.github/actions/build-sim/action.yml @@ -18,7 +18,7 @@ runs: 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 + 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 diff --git a/test/setup_environment.sh b/test/setup_environment.sh index b35bb24c8..d227568bf 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -92,13 +92,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 +112,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 + 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 .. From 09080f28c4b12dec65851633864778e2ddd6d022 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 1 Oct 2025 15:51:38 -0700 Subject: [PATCH 03/23] test, trezor: Patch Trezor T to build --- test/data/trezor-t-build.patch | 24 ++++++++++++++++++++++++ test/setup_environment.sh | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 test/data/trezor-t-build.patch 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 d227568bf..084649ae6 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -112,9 +112,9 @@ 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 + git am ../../data/trezor-t-build.patch uv sync cd core - export CC=gcc-12 uv run make build_unix # Delete any emulator.img file find . -name "trezor.flash" -exec rm {} \; From 8e63078e3bda422bfa5e3376af5490bca2cbab6b Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 1 Oct 2025 16:09:56 -0700 Subject: [PATCH 04/23] ci: Stop using ubuntu-22.04 --- .github/workflows/ci.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6cc8fb07..920834526 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,7 +159,7 @@ 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 @@ -180,9 +177,7 @@ jobs: 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 @@ -220,7 +215,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 From 18b747e32586d684600a10e3e1c180bf61758855 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Thu, 2 Oct 2025 09:21:30 -0700 Subject: [PATCH 05/23] ci: Fix dist tests --- .github/actions/test-dist/action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From 54b88698d3a5ed031654285099f91d90c5c82832 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 1 Oct 2025 14:52:27 -0700 Subject: [PATCH 06/23] ci, ledger: Build Bitcoin Legacy app and use nanosp for testing it Recent versions of Speculos no longer support the Nano S. Additionally, we build the legacy app for the Nano S separately now. --- .github/actions/install-sim/action.yml | 9 ++++++-- .github/workflows/ci.yml | 20 ++++++++-------- .github/workflows/ledger-app-builder.yml | 11 ++++----- .../workflows/ledger-legacy-app-builder.yml | 23 +++++++++++++++++++ test/test_ledger.py | 6 ++--- 5 files changed, 46 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/ledger-legacy-app-builder.yml diff --git a/.github/actions/install-sim/action.yml b/.github/actions/install-sim/action.yml index 3cc846811..53ae7f462 100644 --- a/.github/actions/install-sim/action.yml +++ b/.github/actions/install-sim/action.yml @@ -63,10 +63,10 @@ runs: 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 - - 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 +74,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/workflows/ci.yml b/.github/workflows/ci.yml index 920834526..10e2c5153 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,18 +161,16 @@ jobs: include: ${{ needs.prepare-sim-matrices.outputs.keepkey }} 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: @@ -196,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 diff --git a/.github/workflows/ledger-app-builder.yml b/.github/workflows/ledger-app-builder.yml index b64a33e3e..f21dbabed 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,15 @@ 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 steps: - run: | git clone 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..449628b4b --- /dev/null +++ b/.github/workflows/ledger-legacy-app-builder.yml @@ -0,0 +1,23 @@ +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 }} + container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:latest + steps: + - run: | + git clone 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/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), From d4f1873c32cb96a4da3633f0ea3ea60c794dfb4a Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Thu, 2 Oct 2025 11:46:05 -0700 Subject: [PATCH 07/23] ci: Use github mirror of lwip lwip is a submodule of many device firmware repos, but the default URLs often fail to be cloned. Rewrite those URLs with git config to use the Github mirror to avoid that issue. --- .github/actions/build-sim/action.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/actions/build-sim/action.yml b/.github/actions/build-sim/action.yml index ab90a5358..1512eb3f0 100644 --- a/.github/actions/build-sim/action.yml +++ b/.github/actions/build-sim/action.yml @@ -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 .. From c6c5559798df5386000b2a1b7020a9246a188b40 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Thu, 2 Oct 2025 11:47:12 -0700 Subject: [PATCH 08/23] ci, test: Use shallow clones for device firmware repos --- test/setup_environment.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 084649ae6..0970b9e03 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -67,7 +67,7 @@ cd work 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 https://github.com/trezor/trezor-firmware.git cd trezor-firmware else cd trezor-firmware @@ -127,7 +127,7 @@ 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 + git clone --recursive --depth 1 --shallow-submodules https://github.com/Coldcard/firmware.git cd firmware coldcard_setup_needed=true else @@ -201,7 +201,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 https://github.com/keepkey/keepkey-firmware.git cd keepkey-firmware keepkey_setup_needed=true else @@ -253,7 +253,7 @@ if [[ -n ${build_ledger} ]]; then 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 + git clone --recursive --depth 1 --shallow-submodules https://github.com/LedgerHQ/speculos.git cd speculos else cd speculos From 5331398c2b969098e79e18aeb797824183586743 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Thu, 2 Oct 2025 12:45:13 -0700 Subject: [PATCH 09/23] test, keepkey: Patch nanopb deprecated file mode --- test/data/nanopb-deprecated-mode.patch | 25 +++++++++++++++++++++++++ test/setup_environment.sh | 4 +++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 test/data/nanopb-deprecated-mode.patch 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/setup_environment.sh b/test/setup_environment.sh index 0970b9e03..889338cf9 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -236,7 +236,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 From 1d09f4056e08581a9d1894cd4c79802f416fb7e1 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Thu, 2 Oct 2025 13:19:42 -0700 Subject: [PATCH 10/23] test, ledger: Install speculos dependencies from speculos Instead of listing out the dependencies required to install manually, just install from speculos itself. --- .github/actions/install-sim/action.yml | 4 ++-- test/setup_environment.sh | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/actions/install-sim/action.yml b/.github/actions/install-sim/action.yml index 53ae7f462..f61d5af8d 100644 --- a/.github/actions/install-sim/action.yml +++ b/.github/actions/install-sim/action.yml @@ -60,8 +60,8 @@ 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: startsWith(inputs.device, 'ledger') uses: actions/download-artifact@v4 diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 889338cf9..e332acb80 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -250,15 +250,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 --depth 1 --shallow-submodules https://github.com/LedgerHQ/speculos.git - cd speculos else - cd speculos + pushd speculos git fetch # Determine if we need to pull. From https://stackoverflow.com/a/3278427 @@ -272,12 +268,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 From 9ba9a17315b0dab56cce78f23f57d6444d63920d Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Thu, 2 Oct 2025 14:30:31 -0700 Subject: [PATCH 11/23] test, ledger: Update automation --- test/data/speculos-automation.json | 37 ++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) 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 ], From 2491a382b78580e29972b6031747f7d5e2c1f524 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Thu, 2 Oct 2025 16:48:03 -0700 Subject: [PATCH 12/23] test, coldcard: Apply mpy patch before building --- test/setup_environment.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index e332acb80..13f2a0317 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -156,6 +156,9 @@ if [[ -n ${build_coldcard} ]]; then pip install -r requirements.txt cd unix if [ "$coldcard_setup_needed" == true ] ; then + pushd ../external/micropython + git apply ../../ubuntu24_mpy.patch + popd pushd ../external/micropython/mpy-cross/ make popd From 613fe846a5fbef2ac11af74c4efeea7878de13fb Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Thu, 2 Oct 2025 17:45:49 -0700 Subject: [PATCH 13/23] ci: install some more dependencies --- .github/actions/build-sim/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-sim/action.yml b/.github/actions/build-sim/action.yml index 1512eb3f0..44aa589a2 100644 --- a/.github/actions/build-sim/action.yml +++ b/.github/actions/build-sim/action.yml @@ -17,7 +17,7 @@ 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 + 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 From 3d140ec98c9e6bc95b4c6c83c4eb0201640e48d5 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Fri, 3 Oct 2025 17:06:35 -0700 Subject: [PATCH 14/23] test, coldcard: Disable external input tests These don't seem to work anymore. --- test/test_coldcard.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From d25cc7d029b0d3a252f8059eb19a20f842917476 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 19 Jan 2026 13:29:40 +0100 Subject: [PATCH 15/23] test: assert getkeypool returns list before passing to importdescriptors The getkeypool command can return an error dict instead of a list when the underlying operation fails. Add assertions to catch this early with a clear error message, rather than having the test fail with a confusing JSONRPCException about wrong type passed to importdescriptors. --- test/test_device.py | 6 ++++++ 1 file changed, 6 insertions(+) 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 = [] From cab1a9a7e45c4d8067ccbb0d7fc96c9936d21aee Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 20 Jan 2026 12:24:29 +0100 Subject: [PATCH 16/23] ci: Pin device firmware versions in setup_environment.sh This pins all device firmware versions to ensure reproducible CI builds. --- test/setup_environment.sh | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 13f2a0317..e61eeb955 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -64,10 +64,17 @@ 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) + 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 --depth 1 --shallow-submodules 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 @@ -172,7 +179,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 @@ -204,7 +211,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 --depth 1 --shallow-submodules 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 @@ -255,7 +262,7 @@ fi if [[ -n ${build_ledger} ]]; then # Clone ledger simulator Speculos if it doesn't exist, or update it if it does if [ ! -d "speculos" ]; then - git clone --recursive --depth 1 --shallow-submodules https://github.com/LedgerHQ/speculos.git + git clone --recursive --depth 1 --shallow-submodules --branch ${SPECULOS_VERSION} https://github.com/LedgerHQ/speculos.git else pushd speculos git fetch @@ -395,7 +402,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 From 511e019121581be7dde141b0456aedfe73969c18 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Mon, 19 Jan 2026 18:03:40 +0100 Subject: [PATCH 17/23] ci, ledger: Pin Ledger app versions for compatibility Pin app-bitcoin-new to v2.4.4 (tag stax_1.9.0_2.4.4_sdk_...) since newer versions may be incompatible with Speculos v0.25.10 which is the last version supporting Python 3.9. Use shallow clone (--depth 1) for both Ledger apps to speed up builds. --- .github/workflows/ledger-app-builder.yml | 6 ++++-- .github/workflows/ledger-legacy-app-builder.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ledger-app-builder.yml b/.github/workflows/ledger-app-builder.yml index f21dbabed..d873a2411 100644 --- a/.github/workflows/ledger-app-builder.yml +++ b/.github/workflows/ledger-app-builder.yml @@ -11,10 +11,12 @@ jobs: build: 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 BOLOS_SDK=$NANOX_SDK - uses: actions/upload-artifact@v4 diff --git a/.github/workflows/ledger-legacy-app-builder.yml b/.github/workflows/ledger-legacy-app-builder.yml index 449628b4b..aac5afb1a 100644 --- a/.github/workflows/ledger-legacy-app-builder.yml +++ b/.github/workflows/ledger-legacy-app-builder.yml @@ -11,10 +11,12 @@ jobs: build: name: Build Bitcoin Legacy 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.git -b legacy-1.6.6 + # 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 From ba35c938264ad42b348dabe230ed4ff698d3a137 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 20 Jan 2026 12:23:55 +0100 Subject: [PATCH 18/23] ci, coldcard: pin to v5.4.4 v5.4.3 causes test_backup to hang because it lacks the 'multiprocess simulator' fix merged in June 2025. --- .github/actions/install-sim/action.yml | 6 +++++- test/setup_environment.sh | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/actions/install-sim/action.yml b/.github/actions/install-sim/action.yml index f61d5af8d..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 diff --git a/test/setup_environment.sh b/test/setup_environment.sh index e61eeb955..40ad2c32e 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -71,6 +71,9 @@ 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) +# 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 @@ -134,7 +137,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 --depth 1 --shallow-submodules 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 @@ -164,6 +168,7 @@ if [[ -n ${build_coldcard} ]]; then 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/ From 27f59da9387e1547d362d8f3dc31c5c3c60c059e Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 20 Jan 2026 12:39:53 +0100 Subject: [PATCH 19/23] ci, jade: pin to v1.0.36 --- test/setup_environment.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 40ad2c32e..7d602a5f1 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -70,6 +70,7 @@ 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" @@ -305,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 From 85d98b29fc792be2b9200e0587da6e4879a46379 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 20 Jan 2026 16:11:04 +0100 Subject: [PATCH 20/23] test, jade: add 5 minute startup timeout If the Jade emulator doesn't respond within 5 minutes, fail fast with an error message instead of hanging until the job timeout. --- test/test_jade.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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()}") From 51e55a16459dc380b64810f39b8462a85849e89f Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 20 Jan 2026 16:16:50 +0100 Subject: [PATCH 21/23] test, bitbox02: add 5 minute startup timeout Wrap the Bitbox02Client initialization and restore_device() call in a thread with a 5 minute timeout. This prevents CI from hanging for 45 minutes when the simulator startup blocks indefinitely. --- test/test_bitbox02.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/test/test_bitbox02.py b/test/test_bitbox02.py index c1cd7f63f..bec9fc01b 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,9 @@ # Class for emulator control class BitBox02Emulator(DeviceEmulator): + # Maximum time to wait for simulator startup (5 minutes) + MAX_STARTUP_TIME = 300 + def __init__(self, simulator): self.simulator = simulator self.path = "127.0.0.1:15423" @@ -55,9 +59,27 @@ 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) From 38fae57660ba55dc4113cc0da4a04ac5f0a068f7 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 21 Jan 2026 16:11:55 +0100 Subject: [PATCH 22/23] Constrain cbor2 to <5.8 In the Sdist and Wheel test environments, the Jade tests all timed out. The other tests used a fixed version 5.6 of cbor2 which is why they were not affected. Fixes #817 --- poetry.lock | 2 +- pyproject.toml | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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', From cafc8b04349028036e56f88eba9143b4b1f362ae Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 23 Jan 2026 09:04:46 +0100 Subject: [PATCH 23/23] ci, bitbox02: stop simulator after timeout --- test/test_bitbox02.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/test_bitbox02.py b/test/test_bitbox02.py index bec9fc01b..a9d64b999 100644 --- a/test/test_bitbox02.py +++ b/test/test_bitbox02.py @@ -25,6 +25,8 @@ 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 @@ -86,7 +88,11 @@ def do_restore(): 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)