From eb36bd6980f094ffcf3f0b3d1a1decd1656a7d60 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 3 Jul 2026 17:38:43 +0200 Subject: [PATCH 1/6] test: pin Trezor T Rust nightly to 2025-04-15 The rolling nightly toolchain has drifted past what trezor-firmware core/v2.9.6 supports: recent nightlies reject its reexport_test_harness_main attribute with error E0658. Pin the nightly to 2025-04-15, matching the firmware's own shell.nix. Co-authored-by: Claude (Fable 5) --- test/setup_environment.sh | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/setup_environment.sh b/test/setup_environment.sh index 7d602a5f1..36788bfdb 100755 --- a/test/setup_environment.sh +++ b/test/setup_environment.sh @@ -115,12 +115,15 @@ if [[ -n ${build_trezor_1} || -n ${build_trezor_t} ]]; then fi if [[ -n ${build_trezor_t} ]]; then - rustup update - rustup toolchain uninstall nightly - rustup toolchain install nightly - rustup default nightly - rustup component add rustfmt - rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu + # Pin the Rust nightly to the one trezor-firmware ${TREZOR_VERSION} expects + # (see its shell.nix: rust-bin.nightly."2025-04-15"). The rolling "nightly" + # channel has drifted and newer nightlies reject the firmware's + # reexport_test_harness_main attribute with error E0658. + TREZOR_RUST_NIGHTLY="nightly-2025-04-15" + rustup toolchain install ${TREZOR_RUST_NIGHTLY} + rustup default ${TREZOR_RUST_NIGHTLY} + rustup component add rustfmt --toolchain ${TREZOR_RUST_NIGHTLY} + rustup component add rust-src --toolchain ${TREZOR_RUST_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 From 9ded73e055c653cfbc60fdafc18f102a5c623681 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 3 Feb 2026 10:25:33 +0100 Subject: [PATCH 2/6] ci: drop cirrus leftovers --- README.md | 2 +- ci/cirrus.Dockerfile | 99 -------------------------------------------- ci/py310.Dockerfile | 2 +- ci/py311.Dockerfile | 2 +- setup.py | 2 +- 5 files changed, 4 insertions(+), 103 deletions(-) delete mode 100644 ci/cirrus.Dockerfile diff --git a/README.md b/README.md index c80374b18..ec1f8fab2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Bitcoin Hardware Wallet Interface -[![Build Status](https://api.cirrus-ci.com/github/bitcoin-core/HWI.svg)](https://cirrus-ci.com/github/bitcoin-core/HWI) +[![CI](https://github.com/bitcoin-core/HWI/actions/workflows/ci.yml/badge.svg)](https://github.com/bitcoin-core/HWI/actions/workflows/ci.yml) [![Documentation Status](https://readthedocs.org/projects/hwi/badge/?version=latest)](https://hwi.readthedocs.io/en/latest/?badge=latest) The Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets. diff --git a/ci/cirrus.Dockerfile b/ci/cirrus.Dockerfile deleted file mode 100644 index 07c201c6e..000000000 --- a/ci/cirrus.Dockerfile +++ /dev/null @@ -1,99 +0,0 @@ -# Cache break (modify this line to break cirrus' dockerfile build cache) 1 - -FROM python:3.9 - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update -RUN apt-get install -y \ - autotools-dev \ - automake \ - bsdmainutils \ - build-essential \ - ccache \ - clang \ - cmake \ - curl \ - cython3 \ - gcc-arm-none-eabi \ - gcc-arm-linux-gnueabihf \ - git \ - libboost-system-dev \ - libboost-filesystem-dev \ - libboost-chrono-dev \ - libboost-test-dev \ - libboost-thread-dev \ - libc6-dev-armhf-cross \ - libdb-dev \ - libdb++-dev \ - libevent-dev \ - libgcrypt20-dev \ - libnewlib-arm-none-eabi \ - libpcsclite-dev \ - libsdl2-dev \ - libsdl2-image-dev \ - libssl-dev \ - libslirp-dev \ - libtool \ - libudev-dev \ - libusb-1.0-0-dev \ - ninja-build \ - pkg-config \ - qemu-user-static \ - swig - -RUN pip install poetry flake8 -RUN wget https://static.rust-lang.org/rustup/dist/x86_64-unknown-linux-gnu/rustup-init -RUN chmod +x rustup-init && ./rustup-init -y -ENV PATH="/root/.cargo/bin:$PATH" -RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v22.0/protoc-22.0-linux-x86_64.zip -RUN unzip protoc-22.0-linux-x86_64.zip -d /usr/local -RUN protoc --version - -#################### -# Local build/test steps -# ----------------- -# To install all simulators/tests locally, uncomment the block below, -# then build the docker image and interactively run the tests -# as needed. -# e.g., -# docker build -f ci/cirrus.Dockerfile -t hwi_test . -# docker run -it --entrypoint /bin/bash hwi_test -# cd test; poetry run ./run_tests.py --ledger --coldcard --interface=cli --device-only -# For BitBox02: -# docker build -f ci/cirrus.Dockerfile -t hwi_test . -# ./ci/build_bitbox02.sh -# docker run -it -v bitbox02_volume:/test/work/bitbox02-firmware --name hwi --entrypoint /bin/bash hwi_test -# cd test; poetry run ./run_tests.py --bitbox02 --interface=cli --device-only -#################### - -#################### -#ENV EMAIL=email -#COPY pyproject.toml pyproject.toml -#RUN poetry run pip install construct pyelftools mnemonic jsonschema -# -## Set up environments first to take advantage of layer caching -#RUN mkdir test -#COPY test/setup_environment.sh test/setup_environment.sh -#COPY test/data/coldcard-multisig.patch test/data/coldcard-multisig.patch -## One by one to allow for intermediate caching of successful builds -#RUN cd test; ./setup_environment.sh --trezor-1 -#RUN cd test; ./setup_environment.sh --trezor-t -#RUN cd test; ./setup_environment.sh --coldcard -#RUN cd test; ./setup_environment.sh --bitbox01 -#RUN cd test; ./setup_environment.sh --ledger -#RUN cd test; ./setup_environment.sh --keepkey -#RUN cd test; ./setup_environment.sh --jade -#RUN cd test; ./setup_environment.sh --bitcoind -# -## Once everything has been built, put rest of files in place -## which have higher turn-over. -#COPY test/ test/ -#COPY hwi.py hwi-qt.py README.md / -#COPY hwilib/ /hwilib/ -#RUN poetry install -# -#################### - -ENV LC_ALL=C.UTF-8 -ENV LANG=C.UTF-8 -ENV LANGUAGE=C.UTF-8 diff --git a/ci/py310.Dockerfile b/ci/py310.Dockerfile index 299b44333..20106aa24 100644 --- a/ci/py310.Dockerfile +++ b/ci/py310.Dockerfile @@ -1,4 +1,4 @@ -# Cache break (modify this line to break cirrus' dockerfile build cache) 1 +# Cache break (modify this line to break the dockerfile build cache) 1 FROM python:3.10 diff --git a/ci/py311.Dockerfile b/ci/py311.Dockerfile index ab80e889a..cf97b42cf 100644 --- a/ci/py311.Dockerfile +++ b/ci/py311.Dockerfile @@ -1,4 +1,4 @@ -# Cache break (modify this line to break cirrus' dockerfile build cache) +# Cache break (modify this line to break the dockerfile build cache) FROM python:3.11 diff --git a/setup.py b/setup.py index e821a4ca2..7589240d6 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ 'name': 'hwi', 'version': '3.2.0', 'description': 'A library for working with Bitcoin hardware wallets', - 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![Build Status](https://api.cirrus-ci.com/github/bitcoin-core/HWI.svg)](https://cirrus-ci.com/github/bitcoin-core/HWI)\n[![Documentation Status](https://readthedocs.org/projects/hwi/badge/?version=latest)](https://hwi.readthedocs.io/en/latest/?badge=latest)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\nCaveat emptor: Inclusion of a specific hardware wallet vendor does not imply any endorsement of quality or security.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependencies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path are known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Documentation\n\nDocumentation for HWI can be found on [readthedocs.io](https://hwi.readthedocs.io/).\n\n### Device Support\n\nFor documentation on devices supported and how they are supported, please check the [device support page](https://hwi.readthedocs.io/en/latest/devices/index.html#support-matrix)\n\n### Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](https://hwi.readthedocs.io/en/latest/examples/bitcoin-core-usage.html).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", + 'long_description': "# Bitcoin Hardware Wallet Interface\n\n[![CI](https://github.com/bitcoin-core/HWI/actions/workflows/ci.yml/badge.svg)](https://github.com/bitcoin-core/HWI/actions/workflows/ci.yml)\n[![Documentation Status](https://readthedocs.org/projects/hwi/badge/?version=latest)](https://hwi.readthedocs.io/en/latest/?badge=latest)\n\nThe Bitcoin Hardware Wallet Interface is a Python library and command line tool for interacting with hardware wallets.\nIt provides a standard way for software to work with hardware wallets without needing to implement device specific drivers.\nPython software can use the provided library (`hwilib`). Software in other languages can execute the `hwi` tool.\n\nCaveat emptor: Inclusion of a specific hardware wallet vendor does not imply any endorsement of quality or security.\n\n## Prerequisites\n\nPython 3 is required. The libraries and [udev rules](hwilib/udev/README.md) for each device must also be installed. Some libraries will need to be installed\n\nFor Ubuntu/Debian:\n```\nsudo apt install libusb-1.0-0-dev libudev-dev python3-dev\n```\n\nFor Centos:\n```\nsudo yum -y install python3-devel libusbx-devel systemd-devel\n```\n\nFor macOS:\n```\nbrew install libusb\n```\n\n## Install\n\n```\ngit clone https://github.com/bitcoin-core/HWI.git\ncd HWI\npoetry install # or 'pip3 install .' or 'python3 setup.py install'\n```\n\nThis project uses the [Poetry](https://github.com/sdispater/poetry) dependency manager. HWI and its dependencies can be installed via poetry by executing the following in the root source directory:\n\n```\npoetry install\n```\n\nPip can also be used to automatically install HWI and its dependencies using the `setup.py` file (which is usually in sync with `pyproject.toml`):\n\n```\npip3 install .\n```\n\nThe `setup.py` file can be used to install HWI and its dependencies so long as `setuptools` is also installed:\n\n```\npip3 install -U setuptools\npython3 setup.py install\n```\n\n## Dependencies\n\nSee `pyproject.toml` for all dependencies. Dependencies under `[tool.poetry.dependencies]` are user dependencies, and `[tool.poetry.dev-dependencies]` for development based dependencies. These dependencies will be installed with any of the three above installation methods.\n\n## Usage\n\nTo use, first enumerate all devices and find the one that you want to use with\n\n```\n./hwi.py enumerate\n```\n\nOnce the device type and device path are known, issue commands to it like so:\n\n```\n./hwi.py -t -d \n```\n\nAll output will be in JSON form and sent to `stdout`.\nAdditional information or prompts will be sent to `stderr` and will not necessarily be in JSON.\nThis additional information is for debugging purposes.\n\nTo see a complete list of available commands and global parameters, run\n`./hwi.py --help`. To see options specific to a particular command,\npass the `--help` parameter after the command name; for example:\n\n```\n./hwi.py getdescriptors --help\n```\n\n## Documentation\n\nDocumentation for HWI can be found on [readthedocs.io](https://hwi.readthedocs.io/).\n\n### Device Support\n\nFor documentation on devices supported and how they are supported, please check the [device support page](https://hwi.readthedocs.io/en/latest/devices/index.html#support-matrix)\n\n### Using with Bitcoin Core\n\nSee [Using Bitcoin Core with Hardware Wallets](https://hwi.readthedocs.io/en/latest/examples/bitcoin-core-usage.html).\n\n## License\n\nThis project is available under the MIT License, Copyright Andrew Chow.\n", 'author': 'Ava Chow', 'author_email': 'me@achow101.com', 'maintainer': 'None', From 7ac09cf8803749c182b3579d0d75c41afa5edf2a Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 3 Feb 2026 10:28:26 +0100 Subject: [PATCH 3/6] ci: drop unused Python 3.7 Docker file Also drops Python 3.6 dataclasses leftover. --- ci/py37.Dockerfile | 23 ----------------------- poetry.lock | 2 +- pyproject.toml | 1 - setup.py | 3 +-- 4 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 ci/py37.Dockerfile diff --git a/ci/py37.Dockerfile b/ci/py37.Dockerfile deleted file mode 100644 index fc8b67781..000000000 --- a/ci/py37.Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# Cache break (modify this line to break cirrus' dockerfile build cache) 1 - -FROM python:3.7 - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update -RUN apt-get install -y \ - cython3 \ - git \ - libpcsclite-dev \ - libsdl2-dev \ - libsdl2-image-dev \ - libslirp-dev \ - libudev-dev \ - libusb-1.0-0-dev \ - qemu-user-static \ - swig - -RUN pip install poetry flake8 - -ENV LC_ALL=C.UTF-8 -ENV LANG=C.UTF-8 -ENV LANGUAGE=C.UTF-8 diff --git a/poetry.lock b/poetry.lock index 88af47e0e..a11e72cbb 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 = "ffa2aebd594ec1a5db15d7325c7bb26036b593faccf363163379e276bff1c191" +content-hash = "278dc524a94cc0c503225e4a20f353bd09fa3deaab956e9d559c3795a304ad65" diff --git a/pyproject.toml b/pyproject.toml index ab98299ba..034ae7935 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,6 @@ libusb1 = ">=1.7,<4" pyside2 = { version = "^5.14.0", optional = true, python = "<3.10" } cbor2 = ">=5.4.6,<5.8" pyserial = "^3.5" -dataclasses = {version = "^0.8", python = ">=3.6,<3.7"} semver = "^3.0.1" noiseprotocol = "^0.3.1" protobuf = "^4.23.3" diff --git a/setup.py b/setup.py index 7589240d6..d6255f68e 100644 --- a/setup.py +++ b/setup.py @@ -39,8 +39,7 @@ 'typing-extensions>=4.4,<5.0'] extras_require = \ -{':python_version == "3.6"': ['dataclasses>=0.8,<0.9'], - 'qt:python_version < "3.10"': ['pyside2>=5.14.0,<6.0.0']} +{'qt:python_version < "3.10"': ['pyside2>=5.14.0,<6.0.0']} entry_points = \ {'console_scripts': ['hwi = hwilib._cli:main', 'hwi-qt = hwilib._gui:main']} From f15d6d9a748147169e7517467d92c90acff1fc10 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:21:14 +0200 Subject: [PATCH 4/6] Fix repo renamings for Ledger apps The Ledger bitcoin app repos have moved: - new: from LedgerHQ/app-bitcoin-new to LedgerHQ/app-bitcoin - legacy: from LedgerHQ/app-bitcoin to LedgerHQ/app-bitcoin-legacy --- .github/workflows/ledger-app-builder.yml | 6 +++--- .github/workflows/ledger-legacy-app-builder.yml | 6 +++--- hwilib/devices/ledger_bitcoin/README.md | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ledger-app-builder.yml b/.github/workflows/ledger-app-builder.yml index d873a2411..e97f883b6 100644 --- a/.github/workflows/ledger-app-builder.yml +++ b/.github/workflows/ledger-app-builder.yml @@ -16,10 +16,10 @@ jobs: steps: - run: | # 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 + git clone --branch 2.4.1 --depth 1 https://github.com/LedgerHQ/app-bitcoin.git + cd app-bitcoin make DEBUG=1 BOLOS_SDK=$NANOX_SDK - uses: actions/upload-artifact@v4 with: name: ledger_app - path: app-bitcoin-new/bin/app.elf + path: app-bitcoin/bin/app.elf diff --git a/.github/workflows/ledger-legacy-app-builder.yml b/.github/workflows/ledger-legacy-app-builder.yml index aac5afb1a..d9c6179bc 100644 --- a/.github/workflows/ledger-legacy-app-builder.yml +++ b/.github/workflows/ledger-legacy-app-builder.yml @@ -16,10 +16,10 @@ jobs: 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 + git clone --depth 1 https://github.com/LedgerHQ/app-bitcoin-legacy.git -b legacy-1.6.6 + cd app-bitcoin-legacy make DEBUG=1 BOLOS_SDK=$NANOSP_SDK - uses: actions/upload-artifact@v4 with: name: ledger_app_legacy - path: app-bitcoin/bin/app.elf + path: app-bitcoin-legacy/bin/app.elf diff --git a/hwilib/devices/ledger_bitcoin/README.md b/hwilib/devices/ledger_bitcoin/README.md index e3cc0ee60..f91bdd64a 100644 --- a/hwilib/devices/ledger_bitcoin/README.md +++ b/hwilib/devices/ledger_bitcoin/README.md @@ -1,8 +1,8 @@ # Ledger Bitcoin application client -This is a stripped down version of the client provided at https://github.com/LedgerHQ/app-bitcoin-new/tree/master/bitcoin_client. +This is a stripped down version of the client provided at https://github.com/LedgerHQ/app-bitcoin/tree/master/bitcoin_client. -This stripped down version was made at commit [4e82e44ecfe4ba358da9848087e7e597309abc53](https://github.com/LedgerHQ/app-bitcoin-new/commit/4e82e44ecfe4ba358da9848087e7e597309abc53) +This stripped down version was made at commit [4e82e44ecfe4ba358da9848087e7e597309abc53](https://github.com/LedgerHQ/app-bitcoin/commit/4e82e44ecfe4ba358da9848087e7e597309abc53) ## Changes From ca2caf044e4a5c60b5b5db6aa5bee43f60c93610 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Fri, 10 Jul 2026 09:56:54 +0200 Subject: [PATCH 5/6] psbt: don't overwrite PSBTv2 tx version and fallback locktime self.tx is never None, for PSBTv2 it's an empty placeholder. Its defaults would overwrite the deserialized tx version and fallback locktime, invalidating any existing signature. Check the PSBT version instead, here and in get_unsigned_tx(). Without this fix all BIP 370 valid test vectors, now added, fail the serialization round-trip. --- hwilib/psbt.py | 7 ++++--- test/data/test_psbt.json | 16 +++++++++++++++- test/test_psbt.py | 12 ++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/hwilib/psbt.py b/hwilib/psbt.py index 28b5e1888..5100fdec9 100644 --- a/hwilib/psbt.py +++ b/hwilib/psbt.py @@ -1047,7 +1047,7 @@ def cache_unsigned_tx_pieces(self) -> None: """ # To make things easier, we split up the global transaction # and use the PSBTv2 fields for PSBTv0 - if self.tx is not None: + if self.version == 0: self.setup_from_tx(self.tx) def setup_from_tx(self, tx: CTransaction): @@ -1110,7 +1110,7 @@ def get_unsigned_tx(self) -> CTransaction: :return: A CTransaction """ - if not self.tx.is_null(): + if self.version == 0: return self.tx assert self.tx_version is not None @@ -1153,6 +1153,7 @@ def convert_to_v0(self) -> None: """ Sets this PSBT to version 0 """ + tx = self.get_unsigned_tx() self._convert_version(0) - self.tx = self.get_unsigned_tx() + self.tx = tx self.explicit_version = False diff --git a/test/data/test_psbt.json b/test/data/test_psbt.json index 1a1886233..5aba48193 100644 --- a/test/data/test_psbt.json +++ b/test/data/test_psbt.json @@ -70,7 +70,21 @@ "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" ], "creator" : [ { diff --git a/test/test_psbt.py b/test/test_psbt.py index 3a585b060..63ba11703 100755 --- a/test/test_psbt.py +++ b/test/test_psbt.py @@ -28,5 +28,17 @@ def test_valid_psbt(self): serd = psbt.serialize() self.assertEqual(valid, serd) + def test_convert_to_v0(self): + # BIP 370 vector "1 input, 2 output updated PSBTv2, with PSBT_IN_SEQUENCE" + psbt = PSBT() + psbt.deserialize("cHNidP8BAgQCAAAAAQQBAQEFAQIB+wQCAAAAAAEAUgIAAAABwaolbiFLlqGCL5PeQr/ztfP/jQUZMG41FddRWl6AWxIAAAAAAP////8BGMaaOwAAAAAWABSwo68UQghBJpPKfRZoUrUtsK7wbgAAAAABAR8Yxpo7AAAAABYAFLCjrxRCCEEmk8p9FmhStS2wrvBuAQ4gCwrZIUGcHIcZc11y3HOfnqngY40f5MHu8PmUQISBX8gBDwQAAAAAARAE/v///wAiAgLWAfhIRqZ1X3dr4A49nej7EKzJNfuDxF+wFi1MrVq3khj2nYc+VAAAgAEAAIAAAACAAAAAACoAAAABAwgACK8vAAAAAAEEFgAUxDD2TEdW2jENvRoIVXLvKZkmJywAIgIC42+/9T3VNAcM+P05ZhRoDzV6m4Xbc0C/HPp0XSrXs0AY9p2HPlQAAIABAACAAAAAgAEAAABkAAAAAQMIi73rCwAAAAABBBYAFE3Rk6yWSlasG54cyoRU/i9HT4UTAA==") + psbt.convert_to_v0() + + self.assertEqual(psbt.version, 0) + self.assertEqual(psbt.tx.nVersion, 2) + self.assertEqual(len(psbt.tx.vin), 1) + self.assertEqual(psbt.tx.vin[0].nSequence, 0xfffffffe) + self.assertEqual(len(psbt.tx.vout), 2) + if __name__ == "__main__": unittest.main() From d3e4ce7c9a4ebac1f182aae31a60a3b546b4c7fe Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Sat, 4 Jul 2026 12:10:41 +0200 Subject: [PATCH 6/6] test: only match Ledger "To" screen on the title row The Speculos automation file advances through screens by matching text fragments. The rule for the "To" screen matched any fragment starting with "To", including parts of the destination address shown below that title. This is what failed in CI run 28680592019 (job 85066632783). The address mzmauywUy3WF1TX3YxzQMA5PR4zXqJVLTo was split on the device screen into "mzmauywUy3WF1TX", "3YxzQMA5PR4zXqJVL" and "To". The latter confused the automation rule for "To", which pressed an extra right button: automation: getting actions for "To" (57, 3) automation: getting actions for "mzmauywUy3WF1TX" (9, 19) automation: getting actions for "3YxzQMA5PR4zXqJVL" (8, 33) automation: getting actions for "To" (57, 47) seproxyhal: applying automation ['button', 2, True] seproxyhal: applying automation ['button', 2, False] From there every press landed one screen late; the approval hit "Reject transaction" and the app returned 0x6985, so signtx reported a canceled error. "T" and "o" are both valid base58 characters, and bitcoind generates fresh addresses on every run, which makes this a rare and random failure. Bech32 addresses cannot trigger it ("o" is not in the bech32 character set). Limit the "To" rule to the title row (y=3), where address text never appears. The rule file format does not allow comments, so a warning about short words in automation rules goes in the README. Co-authored-by: Claude (Fable 5) --- test/README.md | 12 ++++++++++++ test/data/speculos-automation.json | 10 +++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/test/README.md b/test/README.md index 1e1264ec7..099f9d027 100644 --- a/test/README.md +++ b/test/README.md @@ -311,6 +311,18 @@ $ cmake -Bbuild -H. $ make -C build/ ``` +### Automation rules + +The Ledger tests drive the emulator's buttons with +`data/speculos-automation.json`. Speculos matches each rule against every +text fragment drawn on the screen, so beware of this pitfall when editing +it: screens can contain randomly generated base58 addresses, wrapped over +several lines, and a rule matching a short word can accidentally match a +wrapped address line. The resulting extra button press derails the rest of +the flow. This happened with a rule for the "To" screen title when an +address ended in "To". Keep rules multi-word where possible, or pin them to +the title row with `"y": 3` like the "To" rule. + ## Coldcard emulator Clone the repository: diff --git a/test/data/speculos-automation.json b/test/data/speculos-automation.json index f17a9c668..31f69f72a 100644 --- a/test/data/speculos-automation.json +++ b/test/data/speculos-automation.json @@ -28,7 +28,15 @@ ] }, { - "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).*", + "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|Output|Warning).*", + "actions": [ + [ "button", 2, true ], + [ "button", 2, false ] + ] + }, + { + "regexp": "^To.*", + "y": 3, "actions": [ [ "button", 2, true ], [ "button", 2, false ]