Merge bitcoin-core/HWI#819: Drop Python 3.9, use 3.10 for dist, bump Ledger
What changed, and why it matters
This is a routine maintenance merge for the Bitcoin Core Hardware Wallet Interface (HWI). It drops support for the end-of-life Python 3.9, switches deterministic builds and CI to Python 3.10, updates Ledger test firmware/simulator versions, re-enables some Ledger tests, and makes small build/test cleanups. There is no direct evidence in the commit of a security vulnerability being fixed or introduced.
No immediate security action required. Reviewers should verify that the Python 3.10 toolchain and updated Ledger app/simulator versions do not break reproducible builds or device compatibility, and confirm the new setup.py stability check passes in CI.
Security signals we found
Dependency/toolchain version bump (Python 3.9 EOL removal, Python 3.10 adoption)
Ledger firmware/app and Speculos simulator version bump in CI/tests
Test automation rules updated for new Ledger UI prompts
Build reproducibility timestamps refreshed to 2026
No changes to hardware wallet protocol logic, key handling, or transaction parsing
Evidence from the diff
The merge commit updates HWI’s Python baseline from 3.9 to 3.10 across pyproject.toml, setup.py, CI matrices, Dockerfiles, and build scripts. It bumps the Ledger Bitcoin app test target from 2.4.1 to 2.5.0 and Speculos from 0.25.10 to 0.26.9, adjusts test automation JSON for new Ledger UI prompts, removes NanoX test skips, and adds a build-step check that setup.py regeneration is stable. Build determinism timestamps are moved from 2019 to 2026. These are infrastructure/test-maintenance changes; no runtime cryptographic or hardware-communication code is modified.
Changed components
HWI build system (contrib/build*.sh, contrib/build*.Dockerfile)HWI Python packaging metadata (pyproject.toml, setup.py, poetry.lock)GitHub Actions CI workflows (.github/workflows/*.yml)Ledger device test emulator configuration (test/test_ledger.py, test/data/speculos-automation.json)Shared device test framework (test/test_device.py)Inspect captured patch +124 / −156
### .containerignore
@@ -0,0 +1,11 @@
+# Exclude local build artifacts and any symlinks outside the context
+build*
+
+# Local documentation files (can be symlinks outside the context)
+*.md
+
+# Common Python / tooling artifacts
+__pycache__/
+*.pyc
+.venv/
+dist/
### .dockerignore
@@ -0,0 +1,11 @@
+# Exclude local build artifacts and any symlinks outside the context
+build*
+
+# Local documentation files (can be symlinks outside the context)
+*.md
+
+# Common Python / tooling artifacts
+__pycache__/
+*.pyc
+.venv/
+dist/
### .github/workflows/ci.yml
@@ -95,6 +95,9 @@ jobs:
git config --global --add safe.directory ${{ github.workspace }}
contrib/build_bin.sh
contrib/build_dist.sh
+ # Verify setup.py regeneration is stable after generating GUI artifacts.
+ contrib/generate_setup.sh
+ git diff --exit-code -- setup.py
find dist -type f -exec sha256sum {} \;
"
@@ -242,4 +245,3 @@ jobs:
with:
device: keepkey
runs-on: ubuntu-latest
-
### .github/workflows/device-test.yml
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ['3.9', '3.10', '3.11', '3.12']
+ python-version: ['3.10', '3.11', '3.12']
device:
- ${{ inputs.device }}
test:
### .github/workflows/ledger-app-builder.yml
@@ -11,14 +11,14 @@ jobs:
build:
name: Build Bitcoin 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
+ # app-bitcoin v2.5.0 targets SDK v26.5.0, provided by builder 5.3.17.
+ container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:5.3.17
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.git
+ # Pin to v2.5.0
+ git clone --branch 2.5.0 --depth 1 https://github.com/LedgerHQ/app-bitcoin.git
cd app-bitcoin
- make DEBUG=1 BOLOS_SDK=$NANOX_SDK
+ make DEBUG=1 COIN=bitcoin_testnet BOLOS_SDK=$NANOX_SDK
- uses: actions/upload-artifact@v4
with:
name: ledger_app
### .github/workflows/ledger-legacy-app-builder.yml
@@ -11,7 +11,7 @@ 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
+ # Pin to 4.23.0 for SDK v25.9.0 compatibility with Speculos v0.26.9
container: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder:4.23.0
steps:
- run: |
### .python-version
@@ -1 +1 @@
-3.9
+3.10.20
### ci/py39.Dockerfile
@@ -1,23 +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 \
- 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
### contrib/build-wine.Dockerfile
@@ -1,7 +1,5 @@
FROM debian:bookworm-slim
-SHELL ["/bin/bash", "-c"]
-
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get install -y \
### contrib/build.Dockerfile
@@ -1,7 +1,5 @@
FROM debian:bookworm-slim
-SHELL ["/bin/bash", "-c"]
-
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get install -y \
@@ -36,9 +34,9 @@ ENV PATH="$PYENV_ROOT/bin:$PATH"
COPY contrib/reproducible-python.diff /opt/reproducible-python.diff
ENV PYTHON_CONFIGURE_OPTS="--enable-shared"
-ENV BUILD_DATE="Jan 1 2019"
+ENV BUILD_DATE="Jan 1 2026"
ENV BUILD_TIME="00:00:00"
-RUN eval "$(pyenv init --path)" && eval "$(pyenv virtualenv-init -)" && cat /opt/reproducible-python.diff | pyenv install -kp 3.9.19
+RUN /bin/bash -c 'eval "$(pyenv init --path)" && eval "$(pyenv virtualenv-init -)" && cat /opt/reproducible-python.diff | pyenv install -kp 3.10.20'
ENV LC_ALL=C.UTF-8
ENV LANG=C.UTF-8
### contrib/build_bin.sh
@@ -6,8 +6,11 @@ set -ex
ARCH=$(uname -m | tr '[:upper:]' '[:lower:]')
+PYTHON_VERSION=3.10.20
+
eval "$(pyenv init --path)"
eval "$(pyenv virtualenv-init -)"
+export PYENV_VERSION="$PYTHON_VERSION"
pip install -U pip
pip install poetry
@@ -21,8 +24,8 @@ else
fi
# We also need to change the timestamps of all of the base library files
-lib_dir=$(pyenv prefix)/lib/python3.9
-TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" '{}' \;
+lib_dir=$(pyenv prefix)/lib/python3.10
+TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "202601010000.00" '{}' \;
# Make the standalone binary
export PYTHONHASHSEED=42
@@ -45,7 +48,7 @@ fi
target_tarfile="hwi-${VERSION}-${OS}-${ARCH}.tar.gz"
-if [[ $gui_support == "--with-gui" ]]; then
+if [[ $gui_support == "--with-gui" && $ARCH == "x86_64" ]]; then
tar -czf $target_tarfile hwi hwi-qt
else
tar -czf $target_tarfile hwi
### contrib/build_dist.sh
@@ -4,8 +4,11 @@
set -ex
+PYTHON_VERSION=3.10.20
+
eval "$(pyenv init --path)"
eval "$(pyenv virtualenv-init -)"
+export PYENV_VERSION="$PYTHON_VERSION"
pip install -U pip
pip install poetry
@@ -14,11 +17,12 @@ gui_support="${1:---with-gui}";
# Setup poetry and install the dependencies
if [[ $gui_support == "--with-gui" ]]; then
poetry install -E qt
+ poetry run contrib/generate-ui.sh
else
poetry install
fi
# Make the distribution archives for pypi
poetry build -f wheel
-# faketime is needed to make sdist detereministic
-TZ=UTC faketime -f "2019-01-01 00:00:00" poetry build -f sdist
+# faketime is needed to make sdist deterministic
+TZ=UTC faketime -f "2026-01-01 00:00:00" poetry build -f sdist
### contrib/build_wine.sh
@@ -3,7 +3,11 @@
set -ex
-PYTHON_VERSION=3.9.13
+# Note: Python MSIs/EXEs are no longer hosted for 3.10.x on python.org.
+# The NuGet python package currently only goes up to 3.10.11, so Windows builds use that.
+PYTHON_VERSION=3.10.11
+PYTHON_NUGET_URL="https://api.nuget.org/v3-flatcontainer/python/${PYTHON_VERSION}/python.${PYTHON_VERSION}.nupkg"
+PYTHON_NUGET_HASH="7c6f99b160a36a7e09492dfcff2b0a3a60bb5229ca44cdcc3ecb32871a6144d0"
PYTHON_FOLDER="python3"
PYHOME="c:/$PYTHON_FOLDER"
@@ -19,28 +23,23 @@ WINDOWS_SDK_VERSION=10.0.17763.0
wine 'wineboot'
-# Install Python
-# Get the PGP keys
-wget -O pubkeys.txt -N -c "https://keybase.io/stevedower/pgp_keys.asc?fingerprint=7ed10b6531d7c8e1bc296021fc624643487034e5"
-gpg --import pubkeys.txt
-rm pubkeys.txt
-
-# Install python components
-for msifile in core dev exe lib pip tools; do
- wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/amd64/${msifile}.msi"
- wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/amd64/${msifile}.msi.asc"
- gpg --verify "${msifile}.msi.asc" "${msifile}.msi"
- wine msiexec /i "${msifile}.msi" /qb TARGETDIR=$PYHOME
- rm $msifile.msi*
-done
+# Install Python from NuGet package
+wget -O python.nupkg -N -c "$PYTHON_NUGET_URL"
+echo "$PYTHON_NUGET_HASH python.nupkg" | sha256sum -c
+rm -rf python-nupkg
+7z x python.nupkg -opython-nupkg >/dev/null
+rm -rf ~/.wine/drive_c/python3
+mkdir -p ~/.wine/drive_c/python3
+cp -a python-nupkg/tools/* ~/.wine/drive_c/python3/
+rm -rf python.nupkg python-nupkg
# Get and build libusb
wget -N -c -O libusb.tar.bz2 "$LIBUSB_URL"
echo "$LIBUSB_HASH libusb.tar.bz2" | sha256sum -c
tar -xf libusb.tar.bz2
pushd "libusb-$LIBUSB_VERSION"
./configure --host=x86_64-w64-mingw32
-faketime -f "2019-01-01 00:00:00" make
+faketime -f "2026-01-01 00:00:00" make
cp libusb/.libs/libusb-1.0.dll ~/.wine/drive_c/python3/
popd
rm -r libusb*
@@ -62,7 +61,7 @@ $PYTHON -m pip install poetry
# We also need to change the timestamps of all of the base library files
lib_dir=~/.wine/drive_c/python3/Lib
-TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "201901010000.00" '{}' \;
+TZ=UTC find ${lib_dir} -name '*.py' -type f -execdir touch -t "202601010000.00" '{}' \;
# Install python dependencies
POETRY="wine $PYHOME/Scripts/poetry.exe"
### contrib/generate-ui.sh
@@ -3,6 +3,7 @@
set -ex
pushd hwilib/ui
+rm -f ui_*.py
for file in *.ui
do
gen_file=ui_`echo $file| cut -d. -f1`.py
### contrib/generate_setup.sh
@@ -5,6 +5,8 @@ set -ex
# Setup poetry and install the dependencies
poetry install -E qt
+# Generate the Python modules so Poetry consistently detects hwilib.ui as a package.
+poetry run contrib/generate-ui.sh
# Build the source distribution
poetry build -f sdist
### docs/development/release-process.rst
@@ -4,7 +4,7 @@ Release Process
1. Bump version number in ``pyproject.toml`` and ``hwilib/__init__.py``, generate the setup.py file, and git tag release
2. Build distribution archives for PyPi with ``contrib/build_dist.sh``
3. For MacOS and Linux, use ``contrib/build_bin.sh``. This needs to be run on a macOS machine for the macOS binary and on a Linux machine for the linux one.
-4. For Windows, use ``contrib/build_wine.sh`` to build the Windows binary using wine
+4. For Windows, use ``contrib/build_wine.sh`` to build the Windows binary using wine. Note that this uses Python 3.10.11 via the NuGet package because python.org no longer hosts the MSIs for newer 3.10.x releases.
5. Make ``SHA256SUMS.txt`` using ``contrib/make_shasums.sh``.
6. Make ``SHA256SUMS.txt.asc`` using ``gpg --clearsign SHA256SUMS.txt``
7. Upload distribution archives to PyPi
@@ -26,7 +26,7 @@ Build everything::
docker run -it --name hwi-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-builder /bin/bash -c "contrib/build_bin.sh && contrib/build_dist.sh"
docker run -it --name hwi-wine-builder -v $PWD:/opt/hwi --rm --workdir /opt/hwi hwi-wine-builder /bin/bash -c "contrib/build_wine.sh"
- docker run --platform linux/arm64 -it --rm --name hwi-builder-arm64 -v $PWD:/opt/hwi --workdir /opt/hwi hwi-builder-arm64 /bin/bash -c "contrib/build_bin.sh --without-gui && contrib/build_dist.sh --without-gui"
+ docker run --platform linux/arm64 -it --rm --name hwi-builder-arm64 -v $PWD:/opt/hwi --workdir /opt/hwi hwi-builder-arm64 /bin/bash -c "contrib/build_bin.sh --without-gui && contrib/build_dist.sh --without-gui"
Building macOS binary
=====================
@@ -35,14 +35,14 @@ Note that the macOS build is non-deterministic.
First install `pyenv <https://github.com/pyenv/pyenv>`_ using whichever method you prefer.
-Then a deterministic build of Python 3.9.19 needs to be installed. This can be done with the patch in ``contrib/reproducible-python.diff``. First ``cd`` into HWI's source tree. Then use::
+Then a deterministic build of Python 3.10.20 needs to be installed. This can be done with the patch in ``contrib/reproducible-python.diff``. First ``cd`` into HWI's source tree. Then use::
- cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2019" BUILD_TIME="00:00:00" pyenv install -kp 3.9.19
+ cat contrib/reproducible-python.diff | PYTHON_CONFIGURE_OPTS="--enable-framework" BUILD_DATE="Jan 1 2026" BUILD_TIME="00:00:00" pyenv install -kp 3.10.20
-Make sure that python 3.9.19 is active::
+Make sure that python 3.10.20 is active::
$ python --version
- Python 3.9.19
+ Python 3.10.20
Now install `Poetry <https://github.com/sdispater/poetry>`_ with ``pip install poetry``
### poetry.lock
@@ -507,27 +507,6 @@ files = [
{file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"},
]
-[[package]]
-name = "importlib-metadata"
-version = "7.0.1"
-description = "Read metadata from Python packages"
-optional = false
-python-versions = ">=3.8"
-groups = ["dev"]
-markers = "python_version == \"3.9\""
-files = [
- {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"},
- {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"},
-]
-
-[package.dependencies]
-zipp = ">=0.5"
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
-perf = ["ipython"]
-testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"]
-
[[package]]
name = "jinja2"
version = "3.1.3"
@@ -882,7 +861,6 @@ files = [
[package.dependencies]
altgraph = "*"
-importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""}
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
packaging = ">=22.0"
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
@@ -907,7 +885,6 @@ files = [
]
[package.dependencies]
-importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""}
packaging = ">=22.0"
setuptools = ">=42.0.0"
@@ -933,7 +910,7 @@ description = "Python bindings for the Qt cross-platform application and UI fram
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11"
groups = ["main"]
-markers = "python_version == \"3.9\" and extra == \"qt\""
+markers = "python_version == \"3.10\" and extra == \"qt\""
files = [
{file = "PySide2-5.15.2.1-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:b5e1d92f26b0bbaefff67727ccbb2e1b577f2c0164b349b3d6e80febb4c5bde2"},
{file = "PySide2-5.15.2.1-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:235240b6ec8206d9fdf0232472c6ef3241783d480425e5b54796f06e39ed23da"},
@@ -1017,7 +994,7 @@ description = "Python / C++ bindings helper module"
optional = true
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11"
groups = ["main"]
-markers = "python_version == \"3.9\" and extra == \"qt\""
+markers = "python_version == \"3.10\" and extra == \"qt\""
files = [
{file = "shiboken2-5.15.2.1-5.15.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:f890f5611ab8f48b88cfecb716da2ac55aef99e2923198cefcf781842888ea65"},
{file = "shiboken2-5.15.2.1-5.15.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87079c07587859a525b9800d60b1be971338ce9b371d6ead81f15ee5a46d448b"},
@@ -1069,7 +1046,6 @@ babel = ">=2.9"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
docutils = ">=0.18.1,<0.21"
imagesize = ">=1.3"
-importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""}
Jinja2 = ">=3.0"
packaging = ">=21.0"
Pygments = ">=2.13"
@@ -1252,7 +1228,7 @@ description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
-markers = "python_version < \"3.11\""
+markers = "python_version == \"3.10\""
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
@@ -1288,27 +1264,10 @@ h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
-[[package]]
-name = "zipp"
-version = "3.17.0"
-description = "Backport of pathlib-compatible object wrapper for zip files"
-optional = false
-python-versions = ">=3.8"
-groups = ["dev"]
-markers = "python_version == \"3.9\""
-files = [
- {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
- {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
-]
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-ruff"]
-
[extras]
qt = ["pyside2"]
[metadata]
lock-version = "2.1"
-python-versions = "^3.9,<3.13"
-content-hash = "cfdbc60bf068f88587fe801325c19dfada1e868dbb0cdd65c5b7a7353dc0ca6a"
+python-versions = "^3.10,<3.13"
+content-hash = "32c83e332e591f7a95b48ef02297798399287c93bc25be7730a25f84d20b4f2b"
### pyproject.toml
@@ -16,14 +16,14 @@ packages = [
]
[tool.poetry.dependencies]
-python = "^3.9,<3.13"
+python = "^3.10,<3.13"
hidapi = ">=0.14.0"
ecdsa = "~0"
pyaes = "^1.6"
mnemonic = "~0"
typing-extensions = "^4.4"
libusb1 = ">=1.7,<4"
-pyside2 = { version = "^5.14.0", optional = true, python = "<3.10" }
+pyside2 = { version = "^5.15.2.1", optional = true, python = "<3.11" }
cbor2 = ">=5.4.6,<5.8"
pyserial = "^3.5"
semver = "^3.0.1"
### setup.py
@@ -39,7 +39,7 @@
'typing-extensions>=4.4,<5.0']
extras_require = \
-{'qt:python_version < "3.10"': ['pyside2>=5.14.0,<6.0.0']}
+{'qt:python_version < "3.11"': ['pyside2>=5.15.2.1,<6.0.0.0']}
entry_points = \
{'console_scripts': ['hwi = hwilib._cli:main', 'hwi-qt = hwilib._gui:main']}
@@ -60,7 +60,7 @@
'install_requires': install_requires,
'extras_require': extras_require,
'entry_points': entry_points,
- 'python_requires': '>=3.9,<3.13',
+ 'python_requires': '>=3.10,<3.13',
}
### test/data/speculos-automation.json
@@ -1,6 +1,14 @@
{
"version": 1,
"rules": [
+ {
+ "regexp": "^(Security risk|High fees warning).*",
+ "y": 34,
+ "actions": [
+ [ "button", 2, true ],
+ [ "button", 2, false ]
+ ]
+ },
{
"text": "Confirm",
"x": 43, "y": 37,
@@ -11,6 +19,16 @@
[ "button", 2, false ]
]
},
+ {
+ "text": "Confirm",
+ "x": 41, "y": 34,
+ "actions": [
+ [ "button", 1, true ],
+ [ "button", 2, true ],
+ [ "button", 1, false ],
+ [ "button", 2, false ]
+ ]
+ },
{
"text": "Confirm account ",
"actions": [
@@ -28,7 +46,7 @@
]
},
{
- "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).*",
+ "regexp": "^(Address|Review|Amount|External amounts|You spend|You receive|Fee|Confirm|The derivation|Derivation path|Reject if|The change path|Change path|Register wallet|Policy map|Key|Path|Public key|Our key|Their key|Unspendable key|Spend from|Spending policy|Primary spending path|Spending path|Transaction output|Wallet name|Wallet policy|Descriptor template|Verify [Bb]itcoin|Output|Warning).*",
"actions": [
[ "button", 2, true ],
[ "button", 2, false ]
@@ -97,13 +115,6 @@
[ "button", 2, false ],
[ "setbool", "seen_msg_hash", false ]
]
- },
- {
- "regexp": "^(Cancel|Reject).*",
- "actions": [
- [ "button", 1, true ],
- [ "button", 1, false ]
- ]
}
]
}
### test/setup_environment.sh
@@ -69,7 +69,7 @@ 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)
+SPECULOS_VERSION="v0.26.9" # Requires Python >=3.10 (v0.25.11+)
JADE_VERSION="1.0.36"
# Keep COLDCARD_VERSION in sync with .github/actions/install-sim/action.yml
### test/test_bitbox02.py
@@ -43,6 +43,7 @@ def __init__(self, simulator):
self.include_xpubs = True
self.supports_device_multiple_multisig = True
self.supports_legacy = False
+ self.supports_arbitrary_keypool_paths = True
def start(self):
super().start()
### test/test_coldcard.py
@@ -45,6 +45,7 @@ def __init__(self, simulator):
self.include_xpubs = False
self.supports_device_multiple_multisig = True
self.supports_legacy = True
+ self.supports_arbitrary_keypool_paths = True
def start(self):
super().start()
### test/test_device.py
@@ -36,6 +36,7 @@ def __init__(self):
self.include_xpubs = None
self.supports_device_multiple_multisig = None
self.supports_legacy = None
+ self.supports_arbitrary_keypool_paths = True
def start(self):
assert self.type is not None
@@ -174,7 +175,7 @@ def do_command(self, args):
args = [f'"{arg}"' for arg in args]
input_str = '\n'.join(args) + '\n'
proc = subprocess.Popen(['hwi', '--stdin'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
- result = proc.communicate(input_str.encode(), timeout=60)
+ result = proc.communicate(input_str.encode(), timeout=120)
return json.loads(result[0].decode())
else:
return process_commands(args)
@@ -313,13 +314,14 @@ def test_getkeypool(self):
addr_info = self.wrpc.getaddressinfo(self.wrpc.getrawchangeaddress('bech32'))
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):
- addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'bech32'))
- self.assertTrue(addr_info['hdkeypath'].startswith("m/0h/0h/4h/"))
+ if self.emulator.supports_arbitrary_keypool_paths:
+ 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):
+ addr_info = self.wrpc.getaddressinfo(self.wrpc.getnewaddress('', 'bech32'))
+ self.assertTrue(addr_info['hdkeypath'].startswith("m/0h/0h/4h/"))
keypool_desc = self.do_command(self.dev_args + ['getkeypool', '--path', '/0h/0h/4h/*', '0', '20'])
self.assertEqual(keypool_desc['error'], 'Path must start with m/')
@@ -498,10 +500,12 @@ def _test_signtx(self, input_types, multisig_types, external, op_return: bool):
in_amt = 1
number_inputs = 0
# Single-sig
- if "segwit" in input_types:
+ if "segwit" in input_types or "sh_wit" in input_types:
self.wpk_rpc.sendtoaddress(sh_wpkh_addr, in_amt)
+ number_inputs += 1
+ if "segwit" in input_types or "wit" in input_types:
self.wpk_rpc.sendtoaddress(wpkh_addr, in_amt)
- number_inputs += 2
+ number_inputs += 1
if "legacy" in input_types:
self.wpk_rpc.sendtoaddress(pkh_addr, in_amt)
number_inputs += 1
@@ -591,20 +595,12 @@ def test_signtx(self):
# https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927
raise unittest.SkipTest("Coldcard sign test temporarily disabled")
- if self.emulator.type == "ledger" and not self.emulator.legacy:
- # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927
- raise unittest.SkipTest("Test temporarily disabled for NanoX")
-
for addrtypes, multisig_types, external, op_return in self.signtx_cases:
with self.subTest(addrtypes=addrtypes, multisig_types=multisig_types, external=external, op_return=op_return):
self._test_signtx(addrtypes, multisig_types, external, op_return)
# Make a huge transaction which might cause some problems with different interfaces
def test_big_tx(self):
- if self.emulator.type == "ledger" and not self.emulator.legacy:
- # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927
- raise unittest.SkipTest("Test temporarily disabled for NanoX")
-
# 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}")
@@ -636,10 +632,6 @@ def test_big_tx(self):
class TestDisplayAddress(DeviceTestCase):
def test_display_address_path(self):
- if self.emulator.type == "ledger" and not self.emulator.legacy:
- # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927
- raise unittest.SkipTest("Test temporarily disabled for NanoX")
-
result = self.do_command(self.dev_args + ['displayaddress', "--addr-type", "legacy", '--path', 'm/44h/1h/0h/0/0'])
if self.emulator.supports_legacy:
self.assertNotIn('error', result)
@@ -665,10 +657,6 @@ def test_display_address_bad_path(self):
self.assertEqual(result['code'], -7)
def test_display_address_descriptor(self):
- if self.emulator.type == "ledger" and not self.emulator.legacy:
- # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927
- raise unittest.SkipTest("Test temporarily disabled for NanoX")
-
account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/84h/1h/0h'])['xpub']
p2sh_segwit_account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/49h/1h/0h'])['xpub']
legacy_account_xpub = self.do_command(self.dev_args + ['getxpub', 'm/44h/1h/0h'])['xpub']
@@ -790,10 +778,6 @@ def _check_sign_msg(self, msg):
self.assertTrue(self.rpc.verifymessage(addr, sig, msg))
def test_sign_msg(self):
- if self.emulator.type == "ledger" and not self.emulator.legacy:
- # https://github.com/bitcoin-core/HWI/pull/795#issuecomment-3112271927
- raise unittest.SkipTest("Test temporarily disabled for NanoX")
-
self._check_sign_msg("Message signing test")
self._check_sign_msg("285") # Specific test case for Ledger shorter S
### test/test_digitalbitbox.py
@@ -43,6 +43,7 @@ def __init__(self, simulator):
self.include_xpubs = False
self.supports_device_multiple_multisig = True
self.supports_legacy = True
+ self.supports_arbitrary_keypool_paths = True
def start(self):
super().start()
### test/test_jade.py
@@ -53,6 +53,7 @@ def __init__(self, jade_qemu_emulator_path):
self.include_xpubs = False
self.supports_device_multiple_multisig = True
self.supports_legacy = True
+ self.supports_arbitrary_keypool_paths = True
def start(self):
super().start()
### test/test_keepkey.py
@@ -65,6 +65,7 @@ def __init__(self, path):
self.include_xpubs = False
self.supports_device_multiple_multisig = True
self.supports_legacy = True
+ self.supports_arbitrary_keypool_paths = True
def start(self):
super().start()
### test/test_ledger.py
@@ -47,12 +47,14 @@ def __init__(self, path, legacy=False):
self.include_xpubs = True
self.supports_device_multiple_multisig = True
self.supports_legacy = True
+ # Bitcoin app 2.5.0 enforces standard Ledger paths for xpub derivation.
+ self.supports_arbitrary_keypool_paths = legacy
def start(self):
super().start()
automation_path = os.path.abspath("data/speculos-automation.json")
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"
+ os.environ["SPECULOS_APPNAME"] = "Bitcoin Test:1.6.6" if self.legacy else "Bitcoin Test:2.5.0"
self.emulator_stderr = open('ledger-emulator.stderr', 'a')
# Start the emulator
### test/test_trezor.py
@@ -64,6 +64,7 @@ def __init__(self, path, model):
self.include_xpubs = False
self.supports_device_multiple_multisig = True
self.supports_legacy = True
+ self.supports_arbitrary_keypool_paths = True
def start(self):
super().start()Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.