From 17508086aaaa81d9906dd2e51733d2da404dedda Mon Sep 17 00:00:00 2001 From: "Jamie C. Driver" Date: Mon, 15 Apr 2024 12:09:56 +0100 Subject: [PATCH] jade: update Jade api to 1.0.29 Updates cbor dependency to cbor2, required for modern linux/python versions. --- .../deterministic-build/requirements-hw.txt | 6 +- contrib/requirements/requirements-hw.txt | 2 +- electrum/plugins/jade/jadepy/README.md | 2 +- electrum/plugins/jade/jadepy/jade.py | 504 +++++++++++++++--- electrum/plugins/jade/jadepy/jade_serial.py | 22 +- electrum/plugins/jade/jadepy/jade_tcp.py | 11 +- 6 files changed, 458 insertions(+), 89 deletions(-) diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index 6bc3f120e..5249a254b 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -6,8 +6,8 @@ bitbox02==6.2.0 \ --hash=sha256:cede06e399c98ed536fed6d8a421208daa00f97b697bd8363a941ac5f33309bf btchip-python==0.1.32 \ --hash=sha256:34f5e0c161c08f65dc0d070ba2ff4c315ed21c4b7e0faa32a46862d0dc1b8f55 -cbor==1.0.0 \ - --hash=sha256:13225a262ddf5615cbd9fd55a76a0d53069d18b07d2e9f19c39e6acb8609bbb6 +cbor2==5.4.6 \ + --hash=sha256:b893500db0fe033e570c3adc956af6eefc57e280026bd2d86fd53da9f1e594d7 certifi==2024.2.2 \ --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 @@ -363,4 +363,4 @@ urllib3==1.26.18 \ --hash=sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0 wheel==0.38.4 \ --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \ - --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8 \ No newline at end of file + --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8 diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index a77e273fe..6e5206738 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -24,7 +24,7 @@ ckcc-protocol>=0.7.7 bitbox02>=6.2.0 # device plugin: jade -cbor>=1.0.0,<2.0.0 +cbor2>=5.4.6,<6.0.0 pyserial>=3.5.0,<4.0.0 # prefer older protobuf (see #7922) diff --git a/electrum/plugins/jade/jadepy/README.md b/electrum/plugins/jade/jadepy/README.md index 7848c18b9..125ccb13a 100644 --- a/electrum/plugins/jade/jadepy/README.md +++ b/electrum/plugins/jade/jadepy/README.md @@ -2,7 +2,7 @@ This is a slightly modified version of the official [Jade](https://github.com/Blockstream/Jade) python library. -This modified version was made from tag [0.1.37](https://github.com/Blockstream/Jade/releases/tag/0.1.37). +This modified version was made from tag [1.0.29](https://github.com/Blockstream/Jade/releases/tag/1.0.29). Intention is to fold these modifications back into Jade repo, for future api release. diff --git a/electrum/plugins/jade/jadepy/jade.py b/electrum/plugins/jade/jadepy/jade.py index 423ea444e..9f4d08251 100644 --- a/electrum/plugins/jade/jadepy/jade.py +++ b/electrum/plugins/jade/jadepy/jade.py @@ -1,4 +1,4 @@ -import cbor +import cbor2 as cbor import hashlib import json import time @@ -17,20 +17,19 @@ from .jade_serial import JadeSerialImpl from .jade_tcp import JadeTCPImpl # 'jade' logger -logger = logging.getLogger('jade') -device_logger = logging.getLogger('jade-device') +logger = logging.getLogger(__name__) +device_logger = logging.getLogger(f'{__name__}-device') # BLE comms backend is optional # It relies on the BLE dependencies being available try: from .jade_ble import JadeBleImpl except ImportError as e: - logger.warn(e) - logger.warn('BLE scanning/connectivity will not be available') + logger.warning(e) + logger.warning('BLE scanning/connectivity will not be available') # Default serial connection -DEFAULT_SERIAL_DEVICE = '/dev/ttyUSB0' DEFAULT_BAUD_RATE = 115200 DEFAULT_SERIAL_TIMEOUT = 120 @@ -77,7 +76,7 @@ def _hexlify(data): # The default implementation used in JadeAPI._jadeRpc() below. # NOTE: Only available if the 'requests' dependency is available. # -# Callers can supply their own implementation of this call where it is required. +# Callers can supply their own implmentation of this call where it is required. # # Parameters # ---------- @@ -94,28 +93,35 @@ def _hexlify(data): # # # Use the first non-onion url # url = [url for url in params['urls'] if not url.endswith('.onion')][0] +# # if params['method'] == 'GET': # assert 'data' not in params, 'Cannot pass body to requests.get' -# f = requests.get(url) +# def http_call_fn(): return requests.get(url) # elif params['method'] == 'POST': # data = json.dumps(params['data']) -# f = requests.post(url, data) +# def http_call_fn(): return requests.post(url, data) +# else: +# raise JadeError(1, "Only GET and POST methods supported", params['method']) # -# logger.debug("http_request received reply: {}".format(f.text)) +# try: +# f = http_call_fn() +# logger.debug("http_request received reply: {}".format(f.text)) # -# if f.status_code != 200: -# logger.error("http error {} : {}".format(f.status_code, f.text)) -# raise ValueError(f.status_code) +# if f.status_code != 200: +# logger.error("http error {} : {}".format(f.status_code, f.text)) +# raise ValueError(f.status_code) # -# assert params['accept'] == 'json' -# f = f.json() +# assert params['accept'] == 'json' +# f = f.json() +# except Exception as e: +# logging.error(e) +# f = None # # return {'body': f} # # except ImportError as e: -# logger.warn(e) -# logger.warn('Default _http_requests() function will not be available') -# +# logger.info(e) +# logger.info('Default _http_requests() function will not be available') class JadeAPI: """ @@ -144,9 +150,9 @@ class JadeAPI: def __exit__(self, exc_type, exc, tb): if (exc_type): - logger.error("Exception causing JadeAPI context exit.") - logger.error(exc_type) - logger.error(exc) + logger.info("Exception causing JadeAPI context exit.") + logger.info(exc_type) + logger.info(exc) traceback.print_tb(tb) self.disconnect(exc_type is not None) @@ -340,16 +346,41 @@ class JadeAPI: return result - def get_version_info(self): + def ping(self): + """ + RPC call to test the connection to Jade and that Jade is powered on and receiving data, and + return whether the main task is currently handling a message, handling user menu navigation + or is idle. + + NOTE: unlike all other calls this is not queued and handled in fifo order - this message is + handled immediately and the response sent as quickly as possible. This call does not block. + If this call is made in parallel with Jade processing other messages, the replies may be + out of order (although the message 'id' should still be correct). Use with caution. + + Returns + ------- + 0 if the main task is currently idle + 1 if the main task is handling a client message + 2 if the main task is handling user ui menu navigation + """ + return self._jadeRpc('ping') + + def get_version_info(self, nonblocking=False): """ RPC call to fetch summary details pertaining to the hardware unit and running firmware. + Parameters + ---------- + nonblocking : bool + If True message will be handled immediately (see also ping()) *experimental feature* + Returns ------- dict Contains keys for various info describing the hw and running fw """ - return self._jadeRpc('get_version_info') + params = {'nonblocking': True} if nonblocking else None + return self._jadeRpc('get_version_info', params) def add_entropy(self, entropy): """ @@ -387,7 +418,20 @@ class JadeAPI: params = {'epoch': epoch if epoch is not None else int(time.time())} return self._jadeRpc('set_epoch', params) - def ota_update(self, fwcmp, fwlen, chunksize, patchlen=None, cb=None): + def logout(self): + """ + RPC call to logout of any wallet loaded on the Jade unit. + Any key material is freed and zero'd. + Call always returns true. + + Returns + ------- + bool + True + """ + return self._jadeRpc('logout') + + def ota_update(self, fwcmp, fwlen, chunksize, fwhash=None, patchlen=None, cb=None): """ RPC call to attempt to update the unit's firmware. @@ -403,6 +447,12 @@ class JadeAPI: and ack'd by the hw unit. The maximum supported chunk size is given in the version info data, under the key 'JADE_OTA_MAX_CHUNK'. + fwhash: 32-bytes, optional + The sha256 hash of the full uncompressed final firmware image. In the case of a full + firmware upload this should be the hash of the uncompressed file. In the case of a + delta update this is the hash of the expected final image - ie. the existing firmware + with the uploaded delta applied. ie. it is a verification of the fw image Jade will try + to boot. Optional for backward-compatibility - may become mandatory in a future release. patchlen: int, optional If the compressed firmware bytes are an incremental diff to be applied to the running firmware image, this is the size of that patch when uncompressed. @@ -434,6 +484,9 @@ class JadeAPI: 'cmpsize': cmplen, 'cmphash': cmphash} + if fwhash is not None: + params['fwhash'] = fwhash + if patchlen is not None: ota_method = 'ota_delta' params['patchsize'] = patchlen @@ -464,11 +517,53 @@ class JadeAPI: Returns ------- - bool - True on success. + int + Time in ms for the internal tests to run, as measured on the hw. + ie. excluding any messaging overhead """ return self._jadeRpc('debug_selfcheck', long_timeout=True) + def capture_image_data(self, check_qr=False): + """ + RPC call to capture raw image data from the camera. + See also scan_qr() below. + NOTE: Only available in a DEBUG build of the firmware. + + Parameters + ---------- + check_qr : bool, optional + If True only images which contain a valid qr code are captured and returned. + If False, any image is considered valid and is returned. + Defaults to False + + Returns + ------- + bytes + Raw image data from the camera framebuffer + """ + params = {'check_qr': check_qr} + return self._jadeRpc('debug_capture_image_data', params) + + def scan_qr(self, image): + """ + RPC call to scan a passed image and return any data extracted from any qr image. + Exercises the camera image capture, but ignores result and uses passed image instead. + See also capture_image_data() above. + NOTE: Only available in a DEBUG build of the firmware. + + Parameters + ---------- + image : bytes + The image data (as obtained from capture_image_data() above). + + Returns + ------- + bytes + String or byte data obtained from the image (via qr code) + """ + params = {'image': image} + return self._jadeRpc('debug_scan_qr', params) + def clean_reset(self): """ RPC call to clean/reset memory and storage, as much as is practical. @@ -529,6 +624,38 @@ class JadeAPI: params = {'seed': seed} return self._jadeRpc('debug_set_mnemonic', params) + def get_bip85_bip39_entropy(self, num_words, index, pubkey): + """ + RPC call to fetch encrypted bip85-bip39 entropy. + NOTE: Only available in a DEBUG build of the firmware. + + Parameters + ---------- + num_words : int + The number of words the entropy is required to produce. + + index : int + The index to use in the bip32 path to calcuate the entropy. + + pubkey: 33-bytes + The host ephemeral pubkey to use to generate a shared ecdh secret to use as an AES key + to encrypt the returned entropy. + + Returns + ------- + dict + pubkey - 33-bytes, Jade's ephemeral pubkey used to generate a shared ecdh secret used as + an AES key to encrypt the returned entropy + encrypted - bytes, the requested bip85 bip39 entropy, AES encrypted with the first key + derived from the ecdh shared secret, prefixed with the iv + hmac - 32-bytes, the hmac of the encrypted buffer, using the second key derived from the + ecdh shared secret + """ + params = {'num_words': num_words, + 'index': index, + 'pubkey': pubkey} + return self._jadeRpc('get_bip85_bip39_entropy', params) + def set_pinserver(self, urlA=None, urlB=None, pubkey=None, cert=None): """ RPC call to explicitly set (override) the details of the blind pinserver used to @@ -698,6 +825,55 @@ class JadeAPI: """ return self._jadeRpc('get_registered_multisigs') + def get_registered_multisig(self, multisig_name, as_file=False): + """ + RPC call to fetch details of a named multisig wallet registered to this signer. + NOTE: the multisig wallet must have been registered with firmware v1.0.23 or later + for the full signer details to be persisted and available. + + Parameters + ---------- + multisig_name : string + Name of multsig registration record to return. + + as_file : string, optional + If true the flat file format is returned, otherwise structured json is returned. + Defaults to false. + + Returns + ------- + dict + Description of registered multisig wallet identified by registration name. + Contains keys: + is_file is true: + multisig_file - str, the multisig file as produced by several wallet apps. + eg: + Name: MainWallet + Policy: 2 of 3 + Format: P2WSH + Derivation: m/48'/0'/0'/2' + + B237FE9D: xpub6E8C7BX4c7qfTsX7urnXggcAyFuhDmYLQhwRwZGLD9maUGWPinuc9k96ej... + 249192D2: xpub6EbXynW6xjYR3crcztum6KzSWqDJoAJQoovwamwVnLaCSHA6syXKPnJo6U... + 67F90FFC: xpub6EHuWWrYd8bp5FS1XAZsMPkmCqLSjpULmygWqAqWRCCjSWQwz6ntq5KnuQ... + + is_file is false: + multisig_name - str, name of multisig registration + variant - str, script type, eg. 'sh(wsh(multi(k)))' + sorted - boolean, whether bip67 key sorting is applied + threshold - int, number of signers required,N + master_blinding_key - 32-bytes, any liquid master blinding key for this wallet + signers - dict containing keys: + fingerprint - 4 bytes, origin fingerprint + derivation - [int], bip32 path from origin to signer xpub provided + xpub - str, base58 xpub of signer + path - [int], any fixed path to always apply after the xpub - usually empty. + + """ + params = {'multisig_name': multisig_name, + 'as_file': as_file} + return self._jadeRpc('get_registered_multisig', params) + def register_multisig(self, network, multisig_name, variant, sorted_keys, threshold, signers, master_blinding_key=None): """ @@ -748,8 +924,59 @@ class JadeAPI: 'master_blinding_key': master_blinding_key}} return self._jadeRpc('register_multisig', params) + def register_multisig_file(self, multisig_file): + """ + RPC call to register a new multisig wallet, which must contain the hw signer. + A registration file is provided - as produced my several wallet apps. + + Parameters + ---------- + multisig_file : string + The multisig file as produced by several wallet apps. + eg: + Name: MainWallet + Policy: 2 of 3 + Format: P2WSH + Derivation: m/48'/0'/0'/2' + + B237FE9D: xpub6E8C7BX4c7qfTsX7urnXggcAyFuhDmYLQhwRwZGLD9maUGWPinuc9k96ejhEQ1DCk... + 249192D2: xpub6EbXynW6xjYR3crcztum6KzSWqDJoAJQoovwamwVnLaCSHA6syXKPnJo6U3bVeGde... + 67F90FFC: xpub6EHuWWrYd8bp5FS1XAZsMPkmCqLSjpULmygWqAqWRCCjSWQwz6ntq5KnuQnL23No2... + + Returns + ------- + bool + True on success, implying the mutisig wallet can now be used. + """ + params = {'multisig_file': multisig_file} + return self._jadeRpc('register_multisig', params) + + def register_descriptor(self, network, descriptor_name, descriptor_script, datavalues=None): + """ + RPC call to register a new descriptor wallet, which must contain the hw signer. + A registration name is provided - if it already exists that record is overwritten. + + Parameters + ---------- + network : string + Network to which the multisig should apply - eg. 'mainnet', 'liquid', 'testnet', etc. + + descriptor_name : string + Name to use to identify this descriptor wallet registration record. + If a registration record exists with the name given, that record is overwritten. + + Returns + ------- + bool + True on success, implying the descriptor wallet can now be used. + """ + params = {'network': network, 'descriptor_name': descriptor_name, + 'descriptor': descriptor_script, 'datavalues': datavalues} + return self._jadeRpc('register_descriptor', params) + def get_receive_address(self, *args, recovery_xpub=None, csv_blocks=0, - variant=None, multisig_name=None, confidential=None): + variant=None, multisig_name=None, descriptor_name=None, + confidential=None): """ RPC call to generate, show, and return an address for the given path. The call has three forms. @@ -795,6 +1022,16 @@ class JadeAPI: multisig_name : str The name of the registered multisig wallet record used to generate the address. + 4. Descriptor wallet addresses + branch : int + Multi-path derivation branch, usually 0. + + pointer : int + Path index to descriptor + + descriptor_name : str + The name of the registered descriptor wallet record used to generate the address. + Returns ------- str @@ -805,6 +1042,10 @@ class JadeAPI: assert len(args) == 2 keys = ['network', 'paths', 'multisig_name'] args += (multisig_name,) + elif descriptor_name is not None: + assert len(args) == 3 + keys = ['network', 'branch', 'pointer', 'descriptor_name'] + args += (descriptor_name,) elif variant is not None: assert len(args) == 2 keys = ['network', 'path', 'variant'] @@ -865,6 +1106,26 @@ class JadeAPI: params = {'path': path, 'message': message} return self._jadeRpc('sign_message', params) + def sign_message_file(self, message_file): + """ + RPC call to format and sign the given message, using the given bip32 path. + A message file is provided - as produced by eg. Specter wallet. + Supports RFC6979 only. + + Parameters + ---------- + message_file : str + Message file to parse and produce signature for. + eg: 'signmessage m/84h/0h/0h/0/0 ascii:this is a test message' + + Returns + ------- + str + base64-encoded RFC6979 signature + """ + params = {'message_file': message_file} + return self._jadeRpc('sign_message', params) + def get_identity_pubkey(self, identity, curve, key_type, index=0): """ RPC call to fetch a pubkey for the given identity (slip13/slip17). @@ -960,18 +1221,28 @@ class JadeAPI: params = {'identity': identity, 'curve': curve, 'index': index, 'challenge': challenge} return self._jadeRpc('sign_identity', params) - def get_master_blinding_key(self): + def get_master_blinding_key(self, only_if_silent=False): """ RPC call to fetch the master (SLIP-077) blinding key for the hw signer. + May block temporarily to request the user's permission to export. Passing 'only_if_silent' + causes the call to return the 'denied' error if it would normally ask the user. NOTE: the master blinding key of any registered multisig wallets can be obtained from the result of `get_registered_multisigs()`. + Parameters + ---------- + only_if_silent : boolean, optional + If True Jade will return the denied error if it would normally ask the user's permission + to export the master blinding key. Passing False (or letting default) may block while + asking the user to confirm the export on Jade. + Returns ------- 32-bytes SLIP-077 master blinding key """ - return self._jadeRpc('get_master_blinding_key') + params = {'only_if_silent': only_if_silent} + return self._jadeRpc('get_master_blinding_key', params) def get_blinding_key(self, script, multisig_name=None): """ @@ -1034,26 +1305,22 @@ class JadeAPI: def get_blinding_factor(self, hash_prevouts, output_index, bftype, multisig_name=None): """ - RPC call to get a deterministic "trusted" blinding factor to blind an output. - Normally the blinding factors are generated and returned in the `get_commitments` call, - but for the last output the vbf must be generated on the host, so this call allows the - host to get a valid abf to compute the generator and then the "final" vbf. - Nonetheless, this call is kept generic, and can also generate vbfs, hence the "bftype" - parameter. + RPC call to get deterministic blinding factors to blind an output. + Predicated on the host calculating the 'hash_prevouts' value correctly. + Can fetch abf, vbf, or both together. Parameters ---------- hash_prevouts : 32-bytes - This value is computed as specified in bip143. - It is verified immediately since at this point Jade doesn't have the tx in question. - It will be checked later during `sign_liquid_tx()`. + This value should be computed by the host as specified in bip143. + It is not verified by Jade, since at this point Jade does not have the tx in question. output_index : int The index of the output we are trying to blind bftype : str - Can be eitehr "ASSET" or "VALUE", to generate abfs or vbfs. + Can be "ASSET", "VALUE", or "ASSET_AND_VALUE", to generate abf, vbf, or both. multisig_name : str, optional The name of any registered multisig wallet for which to fetch the blinding factor. @@ -1061,8 +1328,9 @@ class JadeAPI: Returns ------- - 32-bytes - The requested blinding factor + 32-bytes or 64-bytes + The blinding factor for "ASSET" and "VALUE" requests, or both concatenated abf|vbf + ie. the first 32 bytes being abf, the second 32 bytes being vbf. """ params = {'hash_prevouts': hash_prevouts, 'output_index': output_index, @@ -1109,7 +1377,7 @@ class JadeAPI: Returns ------- dict - Containing the following the blinding factors and output commitments. + Containing the blinding factors and output commitments. """ params = {'asset_id': asset_id, 'value': value, @@ -1161,7 +1429,7 @@ class JadeAPI: host_ae_entropy_values = [] for txinput in inputs: # ae-protocol - do not send the host entropy immediately - txinput = txinput.copy() # shallow copy + txinput = txinput.copy() if txinput else {} # shallow copy host_ae_entropy_values.append(txinput.pop('ae_host_entropy', None)) base_id += 1 @@ -1193,6 +1461,9 @@ class JadeAPI: # Send all n inputs requests = [] for txinput in inputs: + if txinput is None: + txinput = {} + base_id += 1 msg_id = str(base_id) request = self.jade.build_request(msg_id, 'tx_input', txinput) @@ -1212,31 +1483,44 @@ class JadeAPI: return signatures def sign_liquid_tx(self, network, txn, inputs, commitments, change, use_ae_signatures=False, - asset_info=None): + asset_info=None, additional_info=None): """ RPC call to sign a liquid transaction. Parameters ---------- network : str - Network to which the address should apply - eg. 'liquid', 'liquid-testnet', etc. + Network to which the txn should apply - eg. 'liquid', 'liquid-testnet', etc. txn : bytes The transaction to sign inputs : [dict] - The tx inputs. Should contain keys: + The tx inputs. + If signing this input, should contain keys: is_witness, bool - whether this is a segwit input - value_commitment, 33-bytes - The value commitment of ths input - - These are only required if signing this input: script, bytes- the redeem script path, [int] - the bip32 path to sign with + value_commitment, 33-bytes - The value commitment of ths input + + This is optional if signing this input: + sighash, int - The sighash to use, defaults to 0x01 (SIGHASH_ALL) These are only required for Anti-Exfil signatures: ae_host_commitment, 32-bytes - The host-commitment for Anti-Exfil signatures ae_host_entropy, 32-bytes - The host-entropy for Anti-Exfil signatures + These are only required for advanced transactions, eg. swaps, and only when the + inputs need unblinding. + Not needed for vanilla send-payment/redeposit etc: + abf, 32-bytes - asset blinding factor + asset_id, 32-bytes - the unblinded asset-id + asset_generator, 33-bytes - the (blinded) asset-generator + vbf, 32-bytes - the value blinding factor + value, int - the unblinded sats value of the input + + If not signing this input a null or an empty dict can be passed. + commitments : [dict] An array sized for the number of outputs. Unblinded outputs should have a 'null' placeholder element. @@ -1246,16 +1530,18 @@ class JadeAPI: change : [dict] An array sized for the number of outputs. - Outputs which are not change should have a 'null' placeholder element. - Change elements with data will be automatically verified by Jade, and not by the user. - Populated elements should contain sufficient data to generate the change address. + Outputs which are not to this wallet should have a 'null' placeholder element. + The output scripts for the elements with data will be verified by Jade. + Unless the element also contains 'is_change': False, these outputs will automatically + be approved and not be verified by the user. + Populated elements should contain sufficient data to generate the wallet address. See `get_receive_address()` use_ae_signatures : bool, optional Whether to use the anti-exfil protocol to generate the signatures. Defaults to False. - asset_info : [dict] + asset_info : [dict], optional Any asset-registry data relevant to the assets being transacted, such that Jade can display a meaningful name, issuer, ticker etc. rather than just asset-id. At the very least must contain 'asset_id', 'contract' and 'issuance_prevout' items, @@ -1263,6 +1549,17 @@ class JadeAPI: not required. Defaults to None. + additional_info: dict, optional + Extra data about the transaction. Only required for advanced transactions, eg. swaps. + Not needed for vanilla send-payment/redeposit etc: + tx_type, str: 'swap' indicates the tx represents an asset-swap proposal or transaction. + wallet_input_summary, dict: a list of entries containing 'asset_id' (32-bytes) and + 'satoshi' (int) showing net movement of assets out of the wallet (ie. sum of wallet + inputs per asset, minus any change outputs). + wallet_output_summary, dict: a list of entries containing 'asset_id' (32-bytes) and + 'satoshi' (int) showing net movement of assets into the wallet (ie. sum of wallet + outputs per asset, excluding any change outputs). + Returns ------- 1. if use_ae_signatures is False @@ -1286,7 +1583,8 @@ class JadeAPI: 'trusted_commitments': commitments, 'use_ae_signatures': use_ae_signatures, 'change': change, - 'asset_info': asset_info} + 'asset_info': asset_info, + 'additional_info': additional_info} reply = self._jadeRpc('sign_liquid_tx', params, str(base_id)) assert reply @@ -1301,23 +1599,25 @@ class JadeAPI: Parameters ---------- network : str - Network to which the address should apply - eg. 'mainnet', 'testnet', etc. + Network to which the txn should apply - eg. 'mainnet', 'testnet', etc. txn : bytes The transaction to sign inputs : [dict] The tx inputs. Should contain keys: - is_witness, bool - whether this is a segwit input + One of these is required: + input_tx, bytes - The prior transaction which created the utxo of this input + satoshi, int - The satoshi amount of this input - can be used in place of + 'input_tx' for a tx with a single segwit input These are only required if signing this input: + is_witness, bool - whether this is a segwit input script, bytes- the redeem script path, [int] - the bip32 path to sign with - One of these is required: - input_tx, bytes - The prior transaction which created the utxo of this input - satoshi, int - The satoshi amount of this input - can be used in place of - 'input_tx' for a tx with a single segwit input + This is optional if signing this input: + sighash, int - The sighash to use, defaults to 0x01 (SIGHASH_ALL) These are only required for Anti-Exfil signatures: ae_host_commitment, 32-bytes - The host-commitment for Anti-Exfil signatures @@ -1325,9 +1625,11 @@ class JadeAPI: change : [dict] An array sized for the number of outputs. - Outputs which are not change should have a 'null' placeholder element. - Change elements with data will be automatically verified by Jade, and not by the user. - Populated elements should contain sufficient data to generate the change address. + Outputs which are not to this wallet should have a 'null' placeholder element. + The output scripts for the elements with data will be verified by Jade. + Unless the element also contains 'is_change': False, these outputs will automatically + be approved and not be verified by the user. + Populated elements should contain sufficient data to generate the wallet address. See `get_receive_address()` use_ae_signatures : bool @@ -1362,6 +1664,50 @@ class JadeAPI: # Send inputs and receive signatures return self._send_tx_inputs(base_id, inputs, use_ae_signatures) + def sign_psbt(self, network, psbt): + """ + RPC call to sign a passed psbt as required + + Parameters + ---------- + network : str + Network to which the txn should apply - eg. 'mainnet', 'testnet', etc. + + psbt : bytes + The psbt formatted as bytes + + Returns + ------- + bytes + The psbt, updated with any signatures required from the hw signer + """ + # Send PSBT message + params = {'network': network, 'psbt': psbt} + msgid = str(random.randint(100000, 999999)) + request = self.jade.build_request(msgid, 'sign_psbt', params) + self.jade.write_request(request) + + # Read replies until we have them all, collate data and return. + # NOTE: we send 'get_extended_data' messages to request more 'chunks' of the reply data. + psbt_out = bytearray() + while True: + reply = self.jade.read_response() + self.jade.validate_reply(request, reply) + psbt_out.extend(self._get_result_or_raise_error(reply)) + + if 'seqnum' not in reply or reply['seqnum'] == reply['seqlen']: + break + + newid = str(random.randint(100000, 999999)) + params = {'origid': msgid, + 'orig': 'sign_psbt', + 'seqnum': reply['seqnum'] + 1, + 'seqlen': reply['seqlen']} + request = self.jade.build_request(newid, 'get_extended_data', params) + self.jade.write_request(request) + + return psbt_out + class JadeInterface: """ @@ -1394,9 +1740,9 @@ class JadeInterface: def __exit__(self, exc_type, exc, tb): if (exc_type): - logger.error("Exception causing JadeInterface context exit.") - logger.error(exc_type) - logger.error(exc) + logger.info("Exception causing JadeInterface context exit.") + logger.info(exc_type) + logger.info(exc) traceback.print_tb(tb) self.disconnect(exc_type is not None) @@ -1422,14 +1768,14 @@ class JadeInterface: Returns ------- JadeInterface - Interface object configured to use given serial parameters. + Inerface object configured to use given serial parameters. NOTE: the instance has not yet tried to contact the hw - caller must call 'connect()' before trying to use the Jade. """ if device and JadeTCPImpl.isSupportedDevice(device): - impl = JadeTCPImpl(device) + impl = JadeTCPImpl(device, timeout or DEFAULT_SERIAL_TIMEOUT) else: - impl = JadeSerialImpl(device or DEFAULT_SERIAL_DEVICE, + impl = JadeSerialImpl(device, baud or DEFAULT_BAUD_RATE, timeout or DEFAULT_SERIAL_TIMEOUT) return JadeInterface(impl) @@ -1464,7 +1810,7 @@ class JadeInterface: Returns ------- JadeInterface - Interface object configured to use given BLE parameters. + Inerface object configured to use given BLE parameters. NOTE: the instance has not yet tried to contact the hw - caller must call 'connect()' before trying to use the Jade. @@ -1510,7 +1856,7 @@ class JadeInterface: Log any/all outstanding messages/data. NOTE: can run indefinitely if data is arriving constantly. """ - logger.warn("Draining interface...") + logger.warning("Draining interface...") drained = bytearray() finished = False @@ -1521,14 +1867,14 @@ class JadeInterface: if finished or byte_ == b'\n' or len(drained) > 256: try: - device_logger.warn(drained.decode('utf-8')) + device_logger.warning(drained.decode('utf-8')) except Exception as e: # Dump the bytes raw and as hex if decoding as utf-8 failed - device_logger.warn("Raw:") - device_logger.warn(drained) - device_logger.warn("----") - device_logger.warn("Hex dump:") - device_logger.warn(drained.hex()) + device_logger.warning("Raw:") + device_logger.warning(drained) + device_logger.warning("----") + device_logger.warning("Hex dump:") + device_logger.warning(drained.hex()) # Clear and loop to continue collecting drained.clear() @@ -1662,7 +2008,7 @@ class JadeInterface: response = message['log'].decode("utf-8") log_methods = { 'E': device_logger.error, - 'W': device_logger.warn, + 'W': device_logger.warning, 'I': device_logger.info, 'D': device_logger.debug, 'V': device_logger.debug, @@ -1718,7 +2064,7 @@ class JadeInterface: def make_rpc_call(self, request, long_timeout=False): """ Method to send a request over the underlying interface, and await a response. - The request is minimally validated before it is sent, and the response is similarly + The request is minimally validated before it is sent, and the response is simialrly validated before being returned. Any read-timeout is respected unless 'long_timeout' is passed, in which case the call blocks indefinitely awaiting a response. diff --git a/electrum/plugins/jade/jadepy/jade_serial.py b/electrum/plugins/jade/jadepy/jade_serial.py index 0a6fc2be5..37706fe67 100644 --- a/electrum/plugins/jade/jadepy/jade_serial.py +++ b/electrum/plugins/jade/jadepy/jade_serial.py @@ -1,8 +1,9 @@ import serial import logging +from serial.tools import list_ports -logger = logging.getLogger('jade.serial') +logger = logging.getLogger(__name__) # @@ -19,8 +20,25 @@ logger = logging.getLogger('jade.serial') # (caveat cranium) # class JadeSerialImpl: + # Used when searching for devices that might be a Jade/compatible hw + JADE_DEVICE_IDS = [ + (0x10c4, 0xea60), (0x1a86, 0x55d4), (0x0403, 0x6001), + (0x1a86, 0x7523), (0x303a, 0x4001), (0x303a, 0x1001)] + + @classmethod + def _get_first_compatible_device(cls): + jades = [] + for devinfo in list_ports.comports(): + if (devinfo.vid, devinfo.pid) in cls.JADE_DEVICE_IDS: + jades.append(devinfo.device) + + if len(jades) > 1: + logger.warning(f'Multiple potential jade devices detected: {jades}') + + return jades[0] if jades else None + def __init__(self, device, baud, timeout): - self.device = device + self.device = device or self._get_first_compatible_device() self.baud = baud self.timeout = timeout self.ser = None diff --git a/electrum/plugins/jade/jadepy/jade_tcp.py b/electrum/plugins/jade/jadepy/jade_tcp.py index 0909b8bfa..6d7799ad6 100644 --- a/electrum/plugins/jade/jadepy/jade_tcp.py +++ b/electrum/plugins/jade/jadepy/jade_tcp.py @@ -2,7 +2,7 @@ import socket import logging -logger = logging.getLogger('jade.tcp') +logger = logging.getLogger(__name__) # @@ -25,9 +25,10 @@ class JadeTCPImpl: def isSupportedDevice(cls, device): return device is not None and device.startswith(cls.PROTOCOL_PREFIX) - def __init__(self, device): + def __init__(self, device, timeout): assert self.isSupportedDevice(device) self.device = device + self.timeout = timeout self.tcp_sock = None def connect(self): @@ -36,6 +37,7 @@ class JadeTCPImpl: logger.info('Connecting to {}'.format(self.device)) self.tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.tcp_sock.settimeout(self.timeout) url = self.device[len(self.PROTOCOL_PREFIX):].split(':') self.tcp_sock.connect((url[0], int(url[1]))) @@ -57,4 +59,7 @@ class JadeTCPImpl: def read(self, n): assert self.tcp_sock is not None - return self.tcp_sock.recv(n) + buf = self.tcp_sock.recv(n) + while len(buf) < n: + buf += self.tcp_sock.recv(n - len(buf)) + return buf