Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jade native psbt #753

Merged
merged 1 commit into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions hwilib/devices/jade.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
parse_multisig
)

import base64
import logging
import semver
import os
Expand Down Expand Up @@ -90,6 +91,7 @@ def func(*args: Any, **kwargs: Any) -> Any:
# This class extends the HardwareWalletClient for Blockstream Jade specific things
class JadeClient(HardwareWalletClient):
MIN_SUPPORTED_FW_VERSION = semver.VersionInfo(0, 1, 32)
PSBT_SUPPORTED_FW_VERSION = semver.VersionInfo(0, 1, 47)

NETWORKS = {Chain.MAIN: 'mainnet',
Chain.TEST: 'testnet',
Expand Down Expand Up @@ -131,12 +133,12 @@ def __init__(self, path: str, password: Optional[str] = None, expert: bool = Fal
self.jade.connect()

verinfo = self.jade.get_version_info()
self.fw_version = semver.parse_version_info(verinfo['JADE_VERSION'])
uninitialized = verinfo['JADE_STATE'] not in ['READY', 'TEMP']

# Check minimum supported firmware version (ignore candidate/build parts)
fw_version = semver.parse_version_info(verinfo['JADE_VERSION'])
if self.MIN_SUPPORTED_FW_VERSION > fw_version.finalize_version():
raise DeviceNotReadyError(f'Jade fw version: {fw_version} - minimum required version: {self.MIN_SUPPORTED_FW_VERSION}. '
if self.MIN_SUPPORTED_FW_VERSION > self.fw_version.finalize_version():
raise DeviceNotReadyError(f'Jade fw version: {self.fw_version} - minimum required version: {self.MIN_SUPPORTED_FW_VERSION}. '
'Please update using a Blockstream Green companion app')
if path == SIMULATOR_PATH:
if uninitialized:
Expand Down Expand Up @@ -165,10 +167,9 @@ def get_pubkey_at_path(self, bip32_path: str) -> ExtendedKey:
ext_key = ExtendedKey.deserialize(xpub)
return ext_key

# Walk the PSBT looking for inputs we can sign. Push any signatures into the
# 'partial_sigs' map in the input, and return the updated PSBT.
@jade_exception
def sign_tx(self, tx: PSBT) -> PSBT:
# Old firmware does not have native PSBT handling - walk the PSBT looking for inputs we can sign.
# Push any signatures into the 'partial_sigs' map in the input, and return the updated PSBT.
def legacy_sign_tx(self, tx: PSBT) -> PSBT:
"""
Sign a transaction with the Blockstream Jade.
"""
Expand Down Expand Up @@ -366,6 +367,29 @@ def _split_at_last_hardened_element(path: Sequence[int]) -> Tuple[Sequence[int],
# Return the updated psbt
return tx

# Sign tx PSBT - newer Jade firmware supports native PSBT signing, but old firmwares require
# mapping to the legacy 'sign_tx' structures.
@jade_exception
def sign_tx(self, tx: PSBT) -> PSBT:
"""
Sign a transaction with the Blockstream Jade.
"""
# Old firmware does not have native PSBT handling - use legacy method
if self.PSBT_SUPPORTED_FW_VERSION > self.fw_version.finalize_version():
return self.legacy_sign_tx(tx)

# Firmware 0.1.47 (March 2023) and later support native PSBT signing
psbt_b64 = tx.serialize()
psbt_bytes = base64.b64decode(psbt_b64.strip())

# NOTE: sign_psbt() does not use AE signatures, so sticks with default (rfc6979)
psbt_bytes = self.jade.sign_psbt(self._network(), psbt_bytes)
psbt_b64 = base64.b64encode(psbt_bytes).decode()

psbt_signed = PSBT()
psbt_signed.deserialize(psbt_b64)
return psbt_signed

# Sign message, confirmed on device
@jade_exception
def sign_message(self, message: Union[str, bytes], bip32_path: str) -> str:
Expand Down
7 changes: 6 additions & 1 deletion test/test_jade.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ def test_get_signing_p2shwsh(self):
result = self.do_command(self.dev_args + ['displayaddress', descriptor_param])
self.assertEqual(result['address'], '2NAXBEePa5ebo1zTDrtQ9C21QDkkamwczfQ', result)

class TestJadeSignTx(TestSignTx):
# disable big psbt as jade simulator can't handle it
def test_big_tx(self):
pass

def jade_test_suite(emulator, bitcoind, interface):
dev_emulator = JadeEmulator(emulator)

Expand All @@ -234,7 +239,7 @@ def jade_test_suite(emulator, bitcoind, interface):
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, bitcoind, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestJadeGetMultisigAddresses, bitcoind, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, bitcoind, emulator=dev_emulator, interface=interface))
suite.addTest(DeviceTestCase.parameterize(TestSignTx, bitcoind, emulator=dev_emulator, interface=interface, signtx_cases=signtx_cases))
suite.addTest(DeviceTestCase.parameterize(TestJadeSignTx, bitcoind, emulator=dev_emulator, interface=interface, signtx_cases=signtx_cases))

result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)
return result.wasSuccessful()
Expand Down
Loading