Skip to content

Commit

Permalink
jade: use Jade's native PSBT signing if the firmware version supports it
Browse files Browse the repository at this point in the history
If Jade is running firmware 0.1.47 or later use native PSBT signing,
otherwise continue to use the existing legacy-format tx signing.
  • Loading branch information
JamieDriver committed Dec 6, 2024
1 parent f4f7fe5 commit 0de7a50
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 7 deletions.
37 changes: 31 additions & 6 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,10 @@ 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.
# 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.
@jade_exception
def sign_tx(self, tx: PSBT) -> PSBT:
def legacy_sign_tx(self, tx: PSBT) -> PSBT:
"""
Sign a transaction with the Blockstream Jade.
"""
Expand Down Expand Up @@ -366,6 +368,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 0de7a50

Please sign in to comment.