Skip to content

Commit c66dd1a

Browse files
committed
jade: use Jade's native PSBT signing if the firmware version supports it
If Jade is running firmware 0.1.47 or later use native PSBT signing, otherwise continue to use the existing legacy-format tx signing.
1 parent 3174515 commit c66dd1a

File tree

2 files changed

+37
-8
lines changed

2 files changed

+37
-8
lines changed

hwilib/devices/jade.py

+31-7
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
parse_multisig
5353
)
5454

55+
import base64
5556
import logging
5657
import semver
5758
import os
@@ -90,6 +91,7 @@ def func(*args: Any, **kwargs: Any) -> Any:
9091
# This class extends the HardwareWalletClient for Blockstream Jade specific things
9192
class JadeClient(HardwareWalletClient):
9293
MIN_SUPPORTED_FW_VERSION = semver.VersionInfo(0, 1, 32)
94+
PSBT_SUPPORTED_FW_VERSION = semver.VersionInfo(0, 1, 47)
9395

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

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

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

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

370+
# Sign tx PSBT - newer Jade firmware supports native PSBT signing, but old firmwares require
371+
# mapping to the legacy 'sign_tx' structures.
372+
@jade_exception
373+
def sign_tx(self, tx: PSBT) -> PSBT:
374+
"""
375+
Sign a transaction with the Blockstream Jade.
376+
"""
377+
# Old firmware does not have native PSBT handling - use legacy method
378+
if self.PSBT_SUPPORTED_FW_VERSION > self.fw_version.finalize_version():
379+
return self.legacy_sign_tx(tx)
380+
381+
# Firmware 0.1.47 (March 2023) and later support native PSBT signing
382+
psbt_b64 = tx.serialize()
383+
psbt_bytes = base64.b64decode(psbt_b64.strip())
384+
385+
# NOTE: sign_psbt() does not use AE signatures, so sticks with default (rfc6979)
386+
psbt_bytes = self.jade.sign_psbt(self._network(), psbt_bytes)
387+
psbt_b64 = base64.b64encode(psbt_bytes).decode()
388+
389+
psbt_signed = PSBT()
390+
psbt_signed.deserialize(psbt_b64)
391+
return psbt_signed
392+
369393
# Sign message, confirmed on device
370394
@jade_exception
371395
def sign_message(self, message: Union[str, bytes], bip32_path: str) -> str:

test/test_jade.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ def test_get_signing_p2shwsh(self):
215215
result = self.do_command(self.dev_args + ['displayaddress', descriptor_param])
216216
self.assertEqual(result['address'], '2NAXBEePa5ebo1zTDrtQ9C21QDkkamwczfQ', result)
217217

218+
class TestJadeSignTx(TestSignTx):
219+
# disable big psbt as jade simulator can't handle it
220+
def test_big_tx(self):
221+
pass
222+
218223
def jade_test_suite(emulator, bitcoind, interface):
219224
dev_emulator = JadeEmulator(emulator)
220225

@@ -234,7 +239,7 @@ def jade_test_suite(emulator, bitcoind, interface):
234239
suite.addTest(DeviceTestCase.parameterize(TestDisplayAddress, bitcoind, emulator=dev_emulator, interface=interface))
235240
suite.addTest(DeviceTestCase.parameterize(TestJadeGetMultisigAddresses, bitcoind, emulator=dev_emulator, interface=interface))
236241
suite.addTest(DeviceTestCase.parameterize(TestSignMessage, bitcoind, emulator=dev_emulator, interface=interface))
237-
suite.addTest(DeviceTestCase.parameterize(TestSignTx, bitcoind, emulator=dev_emulator, interface=interface, signtx_cases=signtx_cases))
242+
suite.addTest(DeviceTestCase.parameterize(TestJadeSignTx, bitcoind, emulator=dev_emulator, interface=interface, signtx_cases=signtx_cases))
238243

239244
result = unittest.TextTestRunner(stream=sys.stdout, verbosity=2).run(suite)
240245
return result.wasSuccessful()

0 commit comments

Comments
 (0)