Skip to content

Commit

Permalink
Merge #753: Jade native psbt
Browse files Browse the repository at this point in the history
c66dd1a jade: use Jade's native PSBT signing if the firmware version supports it (Jamie C. Driver)

Pull request description:

  1. latest jade api to recognise more recently added hw (now available as its own PR, #768)
  2. remove code which walked psbt and mapped it into Jade's legacy 'sign_tx' json format - instead pass PSBT directly to Jade for signing, since this has been supported in fw now for well over a year and seems to be working well.

ACKs for top commit:
  achow101:
    ACK c66dd1a

Tree-SHA512: e6c80f7309995ba4bf7cc5f9185ad89d45e84226d05cc0ec8741b3a9eead3bda7f4d99e94a16041c6acb2a103719f64f4c2879ed79be6d901cbac4c13ffb689b
  • Loading branch information
achow101 committed Jan 3, 2025
2 parents f7cf6cf + c66dd1a commit 5f533aa
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 8 deletions.
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

0 comments on commit 5f533aa

Please sign in to comment.