deps: bump hwi 2.4.0 -> 3.1.0 (drops Python 3.7/3.8) (#2616)

This commit is contained in:
k9ert 2026-05-22 09:08:28 +02:00 committed by GitHub
parent a1a82082dd
commit 314a8579a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 211 additions and 7 deletions

View file

@ -14,6 +14,7 @@
- [Set up virtualenv](#set-up-virtualenv)
- [If `pip install` fails on `cryptography==3.4.x`](#if-pip-install-fails-on-cryptography34x)
- [How to run the tests](#how-to-run-the-tests)
- [Hardware-attended Jade tests](#hardware-attended-jade-tests)
- [Code-Style](#code-style)
- [Developing on tests](#developing-on-tests)
- [bitcoin-specific stuff](#bitcoin-specific-stuff)
@ -209,6 +210,36 @@ Print the logging output live to the terminal:
pytest --capture=no --log-cli-level=DEBUG
```
### Hardware-attended Jade tests
`tests/test_jade_hardware.py` exercises Specter's HWI integration end-to-end against a physical Blockstream Jade. It is gated by `--run-jade-hardware` and skipped by default, so GitHub Actions ignore it without any workflow change.
Run with `-s` so operator prompts reach the terminal:
```
pytest --run-jade-hardware tests/test_jade_hardware.py -s
```
Three tests, increasing operator effort:
| Test | What it does | Operator action |
|---|---|---|
| `test_jade_enumerate_via_specter` | `HWIBridge.enumerate()` finds the Jade and returns a fingerprint | Connect Jade, unlock |
| `test_jade_extract_xpub_via_specter` | Pulls xpub at `m/84h/0h/0h` (mainnet) | Confirm xpub export on device |
| `test_jade_sign_psbt_via_specter` | Signs a canned testnet PSBT through Specter's sign path | Boot Jade in Temporary Signer mode, scan SeedQR, confirm tx |
The signing test uses the public **BIP-39 abandon vector** (`abandon abandon ... about`) so the PSBT fixture matches anyone's Jade once they load that seed. Setup procedure:
1. Power-cycle the Jade so it shows the boot menu.
2. Choose **Temporary Signer** -> **Scan SeedQR**.
3. Display `tests/fixtures/jade_seedqr_abandon.png` (or `cat tests/fixtures/jade_seedqr_abandon.txt` for the ASCII version) and scan it with the Jade camera.
4. When Jade asks for the network, select **TESTNET**.
5. Press Enter at the test prompt.
6. Confirm the transaction on the Jade screen when it pops up (~99,500 sats to a testnet bech32 output, ~99,500 change auto-validated, 1,000 fee).
Temporary Signer state is held in RAM only and wiped on power-cycle/USB-unplug — your real seed is not affected. Expected master fingerprint for the abandon vector is `73c5da0a`; the test fails fast with a clear hint if the loaded seed is wrong.
The fixture PSBT (`tests/fixtures/jade_hardware.psbt`) was generated with embit against m/84'/1'/0'/0/0 of the abandon vector, including a synthetic `non_witness_utxo` so Jade can verify the input amount per the SegWit fee-spoof mitigation.
Get the log-output of bitcoind side by side with the test-output. For sure you will only see the logs if the test fails.
```
pytest --bitcoind-log-stdout

View file

@ -26,7 +26,7 @@ readme = "README.md"
license = {file = "LICENSE"}
requires-python = ">=3.7,<4.0"
requires-python = ">=3.9,<3.13"
dynamic = ["dependencies", "version"]
@ -52,7 +52,8 @@ markers = [
"slow: mark test as slow.",
"elm: mark test as elementsd dependent",
"bottleneck: mark a test as so ressource intensive that it can create a bottleneck where the test just fails due to a lack of ressources",
"threading: test needs threading to work"
"threading: test needs threading to work",
"jade_hardware: requires a real Jade attached and an operator; opt-in only via --run-jade-hardware"
]
filterwarnings = [

View file

@ -6,7 +6,7 @@ Flask-Cors==6.0.0
Flask-Login==0.6.3
Flask-RESTful==0.3.10
Flask-HTTPAuth==4.8.0
hwi==2.4.0
hwi==3.1.0
python-dotenv==0.21.1
requests==2.31.0
pysocks==1.7.1

View file

@ -432,9 +432,9 @@ hidapi==0.14.0 \
--hash=sha256:fb4e94e45f6dddb20d59501187721e5d3b02e6cc8a59d261dd5cac739008582a \
--hash=sha256:fc9ec2321bf3b0b4953910aa87c0c8ab5f93b1f113a9d3d4f18845ce54708d13
# via hwi
hwi==2.4.0 \
--hash=sha256:3eaa7593f1ab360569eacdd9507dab75532bb58e8cd991d8ad72f5c4fcb67997 \
--hash=sha256:7cb7ef2a4db4bc434815374d9bad43c6425491f77828314a2d2898d3e86d3f04
hwi==3.1.0 \
--hash=sha256:21ba92bb06e2f805e2806c686f2c50d02db6826a363b01e44052415755504d6f \
--hash=sha256:42e875cbb616a91638fb90679cad93edb5075bf375e92fc1709be9b2a3dfd59c
# via -r requirements.in
idna==3.7 \
--hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
@ -921,5 +921,6 @@ wtforms==3.1.2 \
# via flask-wtf
# WARNING: The following packages were not pinned, but pip requires them to be
# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag.
# pinned when the requirements file includes hashes and the requirement is not
# satisfied by a package already installed. Consider using the --allow-unsafe flag.
# setuptools

View file

@ -89,9 +89,24 @@ def pytest_addoption(parser):
default="master",
help="Version of elementsd (something which works with git checkout ...)",
)
parser.addoption(
"--run-jade-hardware",
action="store_true",
default=False,
help="Run tests marked jade_hardware (real Jade attached + operator).",
)
listen()
def pytest_collection_modifyitems(config, items):
if config.getoption("--run-jade-hardware"):
return
skip = pytest.mark.skip(reason="opt-in via --run-jade-hardware")
for item in items:
if item.get_closest_marker("jade_hardware") is not None:
item.add_marker(skip)
def pytest_generate_tests(metafunc):
# ToDo: use custom compiled version of bitcoind
# E.g. test again bitcoind version [currentRelease] + master-branch

1
tests/fixtures/jade_hardware.psbt vendored Normal file
View file

@ -0,0 +1 @@
cHNidP8BAHECAAAAAYz9aGwGnHSzsZXBuDlKg9hNl6KeLvo8czy6WHaPTGwGAAAAAAD/////AqyEAQAAAAAAFgAUb6AWUAo8anN+uyYOLdyni6kjRVishAEAAAAAABYAFC80qhzwClOwVaKRoDp9RfCmmItSAAAAAAABAFMCAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wFR/////wFADQMAAAAAABYAFNDEo+8J6Ze26Z45flGP4+QaEYyhAAAAAAEBH0ANAwAAAAAAFgAU0MSj7wnpl7bpnjl+UY/j5BoRjKEiBgLnqyU3tdSelwMJquBunknzbOHJ/rvUTsjg0cygtPnDGRhzxdoKVAAAgAEAAIAAAACAAAAAAAAAAAAAACICA11J7M1U0AmeQ2did8em1GJdYR2oil30m/lReneRp3elGHPF2gpUAACAAQAAgAAAAIABAAAAAAAAAAA=

BIN
tests/fixtures/jade_seedqr_abandon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

15
tests/fixtures/jade_seedqr_abandon.txt vendored Normal file
View file

@ -0,0 +1,15 @@
█████████████████████████████
██ ▄▄▄▄▄ ██▄▀▀ ▀▄▀▄█ ▄▄▄▄▄ ██
██ █   █ █▄  ▀▄▀▄▀▄█ █   █ ██
██ █▄▄▄█ ██▀▄██▀▄▀▄█ █▄▄▄█ ██
██▄▄▄▄▄▄▄█ ▀ █ ▀▄▀▄█▄▄▄▄▄▄▄██
██  ▄▀ ▄▄▄▄█  █▀▄▀▄ ▄▀█▀▄▀███
██ ▀██▄▀▄▀▀▄█▀██▄▀▄▀▄▀▄▀▄▀▄██
████▀▄▄▄▄▀▀██▀ ▀█▀▄▀▄▀▄▀▄▀▄██
██▄▀█ ██▄▀██ ▄ ▀▄▀▄▀▄▀▄▀▄▀▄██
██▄██▄▄▄▄▄▀▀▀ ██▄▀ ▄▄▄ ▀▄▀▄██
██ ▄▄▄▄▄ █ ▄ ▀█▄█▀ █▄█ ▀▄▀▄██
██ █   █ █▄▄ ▀ ▀█▀▄ ▄ ▄▀▄ ███
██ █▄▄▄█ █ ▀█▄ ▀ ▀▄▀▄▀▄▀▄█▄██
██▄▄▄▄▄▄▄█▄█▄▄██▄█▄█▄█▄█▄█▄██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

140
tests/test_jade_hardware.py Normal file
View file

@ -0,0 +1,140 @@
"""Opt-in tests requiring a physical Blockstream Jade attached + operator.
Skipped by default. Enable with::
pytest --run-jade-hardware tests/test_jade_hardware.py -s
The ``-s`` is required so operator prompts reach your terminal.
Operator setup
--------------
For ``test_jade_enumerate_via_specter`` and
``test_jade_extract_xpub_via_specter``: any Jade with any seed (your
real one is fine those tests only read public material).
For ``test_jade_sign_psbt_via_specter``: the Jade must be in
**Temporary Signer mode** with the public **BIP-39 abandon vector** seed
(``abandon abandon ... about``) loaded. The simplest path:
1. Power-cycle the Jade so it's at the boot menu.
2. Choose ``Temporary Signer`` -> ``Scan SeedQR``.
3. Scan ``tests/fixtures/jade_seedqr_abandon.png`` (or display
``tests/fixtures/jade_seedqr_abandon.txt`` and scan from screen).
4. Confirm the **testnet** network on Jade.
5. The PSBT at ``tests/fixtures/jade_hardware.psbt`` is fabricated by
Coldcard's psbt_faker against this exact seed.
Temporary Signer state lives in RAM only; it's wiped on power-cycle. Your
real seed is not affected.
"""
from pathlib import Path
import pytest
from cryptoadvance.specter.hwi_rpc import HWIBridge
FIXTURE_DIR = Path(__file__).parent / "fixtures"
PSBT_FIXTURE = FIXTURE_DIR / "jade_hardware.psbt"
SEEDQR_PNG = FIXTURE_DIR / "jade_seedqr_abandon.png"
SEEDQR_TXT = FIXTURE_DIR / "jade_seedqr_abandon.txt"
ABANDON_FINGERPRINT = "73c5da0a"
def _enumerate_jade(bridge: HWIBridge, chain: str = "main"):
# HWIBridge.enumerate defaults chain="" which Chain.argparse passes
# through unchanged; Jade's enumerate then fails with
# "Unhandled network: ". Pass an explicit chain.
devs = bridge.enumerate(chain=chain)
return [d for d in devs if d.get("type") == "jade"]
def _prompt(msg: str) -> None:
print(f"\n>>> {msg}")
try:
input(">>> Press Enter when ready... ")
except EOFError:
pass
@pytest.mark.jade_hardware
def test_jade_enumerate_via_specter():
"""Jade is detected by Specter's HWIBridge and reports a fingerprint."""
_prompt("Connect and unlock the Jade.")
bridge = HWIBridge(skip_hwi_initialisation=True)
jades = _enumerate_jade(bridge)
assert jades, "no Jade detected — connect, unlock, and rerun"
jade = jades[0]
assert jade.get("fingerprint"), f"Jade enumerated without fingerprint: {jade}"
assert jade.get("path"), f"Jade enumerated without path: {jade}"
@pytest.mark.jade_hardware
def test_jade_extract_xpub_via_specter():
"""Specter can pull an xpub at a known derivation from Jade."""
_prompt("Unlock the Jade. You may be asked to confirm the xpub export.")
bridge = HWIBridge(skip_hwi_initialisation=True)
jades = _enumerate_jade(bridge)
assert jades, "no Jade detected"
fingerprint = jades[0].get("fingerprint")
assert fingerprint, f"Jade enumerated without fingerprint: {jades[0]}"
# chain must be passed explicitly: HWIBridge.extract_xpub default is
# chain="" which Specter's JadeClient.__init__ rejects via _network()
# before extract_xpub's post-init override can apply.
xpub_line = bridge.extract_xpub(
derivation="m/84h/0h/0h",
device_type="jade",
fingerprint=fingerprint,
chain="main",
)
assert xpub_line, "extract_xpub returned empty"
assert xpub_line.startswith("["), f"unexpected format: {xpub_line!r}"
assert "]" in xpub_line, f"unexpected format: {xpub_line!r}"
body = xpub_line.split("]", 1)[1].strip()
assert body.startswith(("xpub", "zpub", "ypub")), f"unexpected xpub: {body[:8]}"
@pytest.mark.jade_hardware
def test_jade_sign_psbt_via_specter():
"""End-to-end: Specter signs the canned abandon-vector PSBT through Jade.
Requires Jade in Temporary Signer mode with the abandon-vector seed
(see module docstring). The fixture PSBT was generated by Coldcard's
psbt_faker against m/84'/1'/0' on testnet; xfp is 73c5da0a.
"""
assert PSBT_FIXTURE.exists(), f"missing fixture: {PSBT_FIXTURE}"
seedqr_hint = (
f"\n PNG: {SEEDQR_PNG}\n"
f" ASCII: cat {SEEDQR_TXT}"
)
_prompt(
"Put Jade in Temporary Signer mode -> Scan SeedQR -> select TESTNET."
f"\n SeedQR for the BIP-39 abandon-vector lives at:{seedqr_hint}\n"
" Then confirm the transaction on device when prompted."
)
psbt_b64 = PSBT_FIXTURE.read_text().strip()
bridge = HWIBridge(skip_hwi_initialisation=True)
jades = _enumerate_jade(bridge, chain="test")
assert jades, "no Jade detected"
fingerprint = jades[0].get("fingerprint")
assert fingerprint, f"Jade enumerated without fingerprint: {jades[0]}"
assert fingerprint.lower() == ABANDON_FINGERPRINT, (
f"connected Jade fingerprint is {fingerprint}; "
f"expected {ABANDON_FINGERPRINT} (abandon-vector). "
"Are you in Temporary Signer mode with the right SeedQR?"
)
signed = bridge.sign_tx(
psbt=psbt_b64,
device_type="jade",
fingerprint=fingerprint,
chain="test",
)
assert signed, "sign_tx returned empty"
assert signed != psbt_b64, "PSBT was returned unsigned"