mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
deps: bump hwi 2.4.0 -> 3.1.0 (drops Python 3.7/3.8) (#2616)
This commit is contained in:
parent
a1a82082dd
commit
314a8579a0
9 changed files with 211 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
1
tests/fixtures/jade_hardware.psbt
vendored
Normal 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
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
15
tests/fixtures/jade_seedqr_abandon.txt
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
█████████████████████████████
|
||||
██ ▄▄▄▄▄ ██▄▀▀ ▀▄▀▄█ ▄▄▄▄▄ ██
|
||||
██ █ █ █▄ ▀▄▀▄▀▄█ █ █ ██
|
||||
██ █▄▄▄█ ██▀▄██▀▄▀▄█ █▄▄▄█ ██
|
||||
██▄▄▄▄▄▄▄█ ▀ █ ▀▄▀▄█▄▄▄▄▄▄▄██
|
||||
██ ▄▀ ▄▄▄▄█ █▀▄▀▄ ▄▀█▀▄▀███
|
||||
██ ▀██▄▀▄▀▀▄█▀██▄▀▄▀▄▀▄▀▄▀▄██
|
||||
████▀▄▄▄▄▀▀██▀ ▀█▀▄▀▄▀▄▀▄▀▄██
|
||||
██▄▀█ ██▄▀██ ▄ ▀▄▀▄▀▄▀▄▀▄▀▄██
|
||||
██▄██▄▄▄▄▄▀▀▀ ██▄▀ ▄▄▄ ▀▄▀▄██
|
||||
██ ▄▄▄▄▄ █ ▄ ▀█▄█▀ █▄█ ▀▄▀▄██
|
||||
██ █ █ █▄▄ ▀ ▀█▀▄ ▄ ▄▀▄ ███
|
||||
██ █▄▄▄█ █ ▀█▄ ▀ ▀▄▀▄▀▄▀▄█▄██
|
||||
██▄▄▄▄▄▄▄█▄█▄▄██▄█▄█▄█▄█▄█▄██
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
140
tests/test_jade_hardware.py
Normal file
140
tests/test_jade_hardware.py
Normal 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue