def sign_tx(self, tx: PSBT) -> PSBT: """ Sign a transaction with the Blockstream Jade. """ # Helper to get multisig record for change output def _parse_signers( hd_keypath_origins: List[KeyOriginInfo], ) -> Tuple[List[Tuple[bytes, Sequence[int]]], List[Sequence[int]]]: # Split the path at the last hardened path element def _split_at_last_hardened_element( path: Sequence[int], ) -> Tuple[Sequence[int], Sequence[int]]: for i in range(len(path), 0, -1): if is_hardened(path[i - 1]): return (path[:i], path[i:]) return ([], path) signers = [] paths = [] for origin in hd_keypath_origins: prefix, suffix = _split_at_last_hardened_element(origin.path) signers.append((origin.fingerprint, prefix)) paths.append(suffix) # sort signers and paths like in multisig registration signers, paths = [list(a) for a in zip(*sorted(zip(signers, paths)))] return signers, paths c_txn = CTransaction(tx.tx) master_fp = self.get_master_fingerprint() signing_singlesigs = False signing_multisigs = {} need_to_sign = True while need_to_sign: signing_pubkeys: List[Optional[bytes]] = [None] * len(tx.inputs) need_to_sign = False # Signing input details jade_inputs = [] for n_vin, (txin, psbtin) in py_enumerate(zip(c_txn.vin, tx.inputs)): # Get bip32 path to use to sign, if required for this input path = None multisig_input = len(psbtin.hd_keypaths) > 1 for pubkey, origin in psbtin.hd_keypaths.items(): if origin.fingerprint == master_fp and len(origin.path) > 0: if not multisig_input: signing_singlesigs = True if psbtin.partial_sigs.get(pubkey, None) is None: # hw to sign this input - it is not already signed if signing_pubkeys[n_vin] is None: signing_pubkeys[n_vin] = pubkey path = origin.path else: # Additional signature needed for this input - ie. a multisig where this wallet is # multiple signers? Clumsy, but just loop and go through the signing procedure again. need_to_sign = True # Get the tx and prevout/scriptcode utxo = None p2sh = False input_txn_bytes = None if psbtin.witness_utxo: utxo = psbtin.witness_utxo if psbtin.non_witness_utxo: utxo = psbtin.non_witness_utxo.vout[txin.prevout.n] input_txn_bytes = ( psbtin.non_witness_utxo.serialize_without_witness() ) if utxo is None: raise Exception( "PSBT is missing input utxo information, cannot sign" ) scriptcode = utxo.scriptPubKey if is_p2sh(scriptcode): scriptcode = psbtin.redeem_script p2sh = True witness_input, witness_version, witness_program = is_witness(scriptcode) if witness_input: if is_p2wsh(scriptcode): scriptcode = psbtin.witness_script elif is_p2wpkh(scriptcode): scriptcode = b"\x76\xa9\x14" + witness_program + b"\x88\xac" else: continue # If we are signing a multisig input, deduce the potential # registration details and cache as a potential change wallet if multisig_input and path and scriptcode and (p2sh or witness_input): parsed = parse_multisig(scriptcode) if parsed: addr_type = ( AddressType.LEGACY if not witness_input else AddressType.WIT if not p2sh else AddressType.SH_WIT ) script_variant = self._convertAddrType(addr_type, multisig=True) threshold = parsed[0] pubkeys = parsed[1] hd_keypath_origins = [ psbtin.hd_keypaths[pubkey] for pubkey in pubkeys ] signers, paths = _parse_signers(hd_keypath_origins) multisig_name = self._get_multisig_name( script_variant, threshold, signers ) signing_multisigs[multisig_name] = ( script_variant, threshold, signers, ) # Build the input and add to the list - include some host entropy for AE sigs (although we won't verify) jade_inputs.append( { "is_witness": witness_input, "input_tx": input_txn_bytes, "script": scriptcode, "path": path, "ae_host_entropy": os.urandom(32), "ae_host_commitment": os.urandom(32), } ) # Change output details # This is optional, in that if we send it Jade validates the change output script # and the user need not confirm that ouptut. If not passed the change output must # be confirmed by the user on the hwwallet screen, like any other spend output. change: List[Optional[Dict[str, Any]]] = [None] * len(tx.outputs) # If signing multisig inputs, get registered multisigs details in case we # see any multisig outputs which may be change which we can auto-validate. # ie. filter speculative 'signing multisigs' to ones actually registered on the hw candidate_multisigs = {} if signing_multisigs: # register multisig if xpubs are known if tx.xpub and len(signing_multisigs) == 1: msigname = list(signing_multisigs.keys())[0] signers = [] origins = [] for xpub in tx.xpub: hd = bip32.HDKey.parse(xpub) origin = tx.xpub[xpub] origins.append((origin.fingerprint, origin.path)) signers.append( { "fingerprint": origin.fingerprint, "derivation": origin.path, "xpub": str(hd), "path": [], } ) # sort origins and signers together origins, signers = [ list(a) for a in zip(*sorted(zip(origins, signers))) ] # Get a deterministic name for this multisig wallet script_variant = signing_multisigs[msigname][0] thresh = signing_multisigs[msigname][1] num_signers = signing_multisigs[msigname][2] multisig_name = self._get_multisig_name( script_variant, thresh, origins ) # stupid sanity check of the fingerprints and origins if multisig_name == msigname: # Need to ensure this multisig wallet is registered first # (Note: 're-registering' is a no-op) self.jade.register_multisig( self._network(), multisig_name, script_variant, True, # always use sorted thresh, signers, ) # registered_multisigs = self.jade.get_registered_multisigs() signing_multisigs = { k: v for k, v in signing_multisigs.items() if k in registered_multisigs and registered_multisigs[k]["variant"] == v[0] and registered_multisigs[k]["threshold"] == v[1] and registered_multisigs[k]["num_signers"] == len(v[2]) } # Look at every output... for n_vout, (txout, psbtout) in py_enumerate(zip(c_txn.vout, tx.outputs)): num_signers = len(psbtout.hd_keypaths) if num_signers == 1 and signing_singlesigs: # Single-sig output - since we signed singlesig inputs this could be our change for pubkey, origin in psbtout.hd_keypaths.items(): # Considers 'our' outputs as potential change as far as Jade is concerned # ie. can be verified and auto-confirmed. # Is this ok, or should check path also, assuming bip44-like ? if origin.fingerprint == master_fp and len(origin.path) > 0: change_addr_type = None if txout.is_p2pkh(): change_addr_type = AddressType.LEGACY elif txout.is_witness()[0] and not txout.is_p2wsh(): change_addr_type = AddressType.WIT # ie. p2wpkh elif ( txout.is_p2sh() and is_witness(psbtout.redeem_script)[0] ): change_addr_type = AddressType.SH_WIT else: continue script_variant = self._convertAddrType( change_addr_type, multisig=False ) change[n_vout] = { "path": origin.path, "variant": script_variant, } elif num_signers > 1 and signing_multisigs: # Multisig output - since we signed multisig inputs this could be our change candidate_multisigs = { k: v for k, v in signing_multisigs.items() if len(v[2]) == num_signers } if not candidate_multisigs: continue for pubkey, origin in psbtout.hd_keypaths.items(): if origin.fingerprint == master_fp and len(origin.path) > 0: change_addr_type = None if ( txout.is_p2sh() and not is_witness(psbtout.redeem_script)[0] ): change_addr_type = AddressType.LEGACY scriptcode = psbtout.redeem_script elif txout.is_p2wsh() and not txout.is_p2sh(): change_addr_type = AddressType.WIT scriptcode = psbtout.witness_script elif ( txout.is_p2sh() and is_witness(psbtout.redeem_script)[0] ): change_addr_type = AddressType.SH_WIT scriptcode = psbtout.witness_script else: continue parsed = parse_multisig(scriptcode) if parsed: script_variant = self._convertAddrType( change_addr_type, multisig=True ) threshold = parsed[0] pubkeys = parsed[1] hd_keypath_origins = [ psbtout.hd_keypaths[pubkey] for pubkey in pubkeys ] signers, paths = _parse_signers(hd_keypath_origins) multisig_name = self._get_multisig_name( script_variant, threshold, signers ) matched_multisig = candidate_multisigs.get( multisig_name ) == (script_variant, threshold, signers) if matched_multisig: change[n_vout] = { "paths": paths, "multisig_name": multisig_name, } # The txn itself txn_bytes = c_txn.serialize_without_witness() # Request Jade generate the signatures for our inputs. # Change details are passed to be validated on the hw (user does not confirm) signatures = self.jade.sign_tx( self._network(), txn_bytes, jade_inputs, change, True ) # Push sigs into PSBT structure as appropriate for psbtin, signer_pubkey, sigdata in zip( tx.inputs, signing_pubkeys, signatures ): signer_commitment, sig = sigdata if signer_pubkey and sig: psbtin.partial_sigs[signer_pubkey] = sig # Return the updated psbt return tx
def sign_tx(self, tx: PSBT) -> PSBT: """ Sign a transaction with the Trezor. There are some limitations to what transactions can be signed. - Multisig inputs are limited to at most n-of-15 multisigs. This is a firmware limitation. - Transactions with arbitrary input scripts (scriptPubKey, redeemScript, or witnessScript) and arbitrary output scripts cannot be signed. This is a firmware limitation. - Send-to-self transactions will result in no prompt for outputs as all outputs will be detected as change. """ self._check_unlocked() # Get this devices master key fingerprint master_key = btc.get_public_node(self.client, [0x80000000], coin_name="Bitcoin") master_fp = get_xpub_fingerprint(master_key.xpub) # Do multiple passes for multisig passes = 1 p = 0 while p < passes: # Prepare inputs inputs = [] to_ignore = ( [] ) # Note down which inputs whose signatures we're going to ignore for input_num, (psbt_in, txin) in py_enumerate( list(zip(tx.inputs, tx.tx.vin)) ): txinputtype = messages.TxInputType( prev_hash=ser_uint256(txin.prevout.hash)[::-1], prev_index=txin.prevout.n, sequence=txin.nSequence, ) # Detrermine spend type scriptcode = b"" utxo = None if psbt_in.witness_utxo: utxo = psbt_in.witness_utxo if psbt_in.non_witness_utxo: if txin.prevout.hash != psbt_in.non_witness_utxo.sha256: raise BadArgumentError( "Input {} has a non_witness_utxo with the wrong hash".format( input_num ) ) utxo = psbt_in.non_witness_utxo.vout[txin.prevout.n] if utxo is None: continue scriptcode = utxo.scriptPubKey # Check if P2SH p2sh = False if is_p2sh(scriptcode): # Look up redeemscript if len(psbt_in.redeem_script) == 0: continue scriptcode = psbt_in.redeem_script p2sh = True # Check segwit is_wit, _, _ = is_witness(scriptcode) if is_wit: if p2sh: txinputtype.script_type = ( messages.InputScriptType.SPENDP2SHWITNESS ) else: txinputtype.script_type = messages.InputScriptType.SPENDWITNESS else: txinputtype.script_type = messages.InputScriptType.SPENDADDRESS txinputtype.amount = utxo.nValue # Check if P2WSH p2wsh = False if is_p2wsh(scriptcode): # Look up witnessscript if len(psbt_in.witness_script) == 0: continue scriptcode = psbt_in.witness_script p2wsh = True def ignore_input() -> None: txinputtype.address_n = [ 0x80000000 | 84, 0x80000000 | (0 if self.chain == Chain.MAIN else 1), 0x80000000, 0, 0, ] txinputtype.multisig = None txinputtype.script_type = messages.InputScriptType.SPENDWITNESS inputs.append(txinputtype) to_ignore.append(input_num) # Check for multisig is_ms, multisig = parse_multisig(scriptcode, tx.xpub, psbt_in) if is_ms: # Add to txinputtype txinputtype.multisig = multisig if not is_wit: if utxo.is_p2sh: txinputtype.script_type = ( messages.InputScriptType.SPENDMULTISIG ) else: # Cannot sign bare multisig, ignore it ignore_input() continue elif not is_ms and not is_wit and not is_p2pkh(scriptcode): # Cannot sign unknown spk, ignore it ignore_input() continue elif not is_ms and is_wit and p2wsh: # Cannot sign unknown witness script, ignore it ignore_input() continue # Find key to sign with found = False # Whether we have found a key to sign with found_in_sigs = ( False # Whether we have found one of our keys in the signatures ) our_keys = 0 for key in psbt_in.hd_keypaths.keys(): keypath = psbt_in.hd_keypaths[key] if keypath.fingerprint == master_fp: if ( key in psbt_in.partial_sigs ): # This key already has a signature found_in_sigs = True continue if ( not found ): # This key does not have a signature and we don't have a key to sign with yet txinputtype.address_n = keypath.path found = True our_keys += 1 # Determine if we need to do more passes to sign everything if our_keys > passes: passes = our_keys if ( not found and not found_in_sigs ): # None of our keys were in hd_keypaths or in partial_sigs # This input is not one of ours ignore_input() continue elif ( not found and found_in_sigs ): # All of our keys are in partial_sigs, ignore whatever signature is produced for this input ignore_input() continue # append to inputs inputs.append(txinputtype) # address version byte if self.chain != Chain.MAIN: p2pkh_version = b"\x6f" p2sh_version = b"\xc4" bech32_hrp = "tb" else: p2pkh_version = b"\x00" p2sh_version = b"\x05" bech32_hrp = "bc" # prepare outputs outputs = [] for i, out in py_enumerate(tx.tx.vout): txoutput = messages.TxOutputType(amount=out.nValue) txoutput.script_type = messages.OutputScriptType.PAYTOADDRESS if out.is_p2pkh(): txoutput.address = to_address(out.scriptPubKey[3:23], p2pkh_version) elif out.is_p2sh(): txoutput.address = to_address(out.scriptPubKey[2:22], p2sh_version) elif out.is_opreturn(): txoutput.script_type = messages.OutputScriptType.PAYTOOPRETURN txoutput.op_return_data = out.scriptPubKey[2:] else: wit, ver, prog = out.is_witness() if wit: txoutput.address = bech32.encode(bech32_hrp, ver, prog) else: raise BadArgumentError("Output is not an address") # Add the derivation path for change psbt_out = tx.outputs[i] for _, keypath in psbt_out.hd_keypaths.items(): if keypath.fingerprint != master_fp: continue wit, ver, prog = out.is_witness() if out.is_p2pkh(): txoutput.address_n = keypath.path txoutput.address = None elif wit: txoutput.script_type = messages.OutputScriptType.PAYTOWITNESS txoutput.address_n = keypath.path txoutput.address = None elif out.is_p2sh() and psbt_out.redeem_script: wit, ver, prog = CTxOut(0, psbt_out.redeem_script).is_witness() if wit and len(prog) in [20, 32]: txoutput.script_type = ( messages.OutputScriptType.PAYTOP2SHWITNESS ) txoutput.address_n = keypath.path txoutput.address = None # add multisig info is_ms, multisig = parse_multisig( psbt_out.witness_script or psbt_out.redeem_script, tx.xpub, psbt_out ) if is_ms: txoutput.multisig = multisig # append to outputs outputs.append(txoutput) # Prepare prev txs prevtxs = {} for psbt_in in tx.inputs: if psbt_in.non_witness_utxo: prev = psbt_in.non_witness_utxo t = messages.TransactionType() t.version = prev.nVersion t.lock_time = prev.nLockTime for vin in prev.vin: i = messages.TxInputType( prev_hash=ser_uint256(vin.prevout.hash)[::-1], prev_index=vin.prevout.n, script_sig=vin.scriptSig, sequence=vin.nSequence, ) t.inputs.append(i) for vout in prev.vout: o = messages.TxOutputBinType( amount=vout.nValue, script_pubkey=vout.scriptPubKey, ) t.bin_outputs.append(o) logging.debug(psbt_in.non_witness_utxo.hash) assert psbt_in.non_witness_utxo.sha256 is not None prevtxs[ser_uint256(psbt_in.non_witness_utxo.sha256)[::-1]] = t # Sign the transaction signed_tx = btc.sign_tx( client=self.client, coin_name=self.coin_name, inputs=inputs, outputs=outputs, prev_txes=prevtxs, version=tx.tx.nVersion, lock_time=tx.tx.nLockTime, ) # Each input has one signature for input_num, (psbt_in, sig) in py_enumerate( list(zip(tx.inputs, signed_tx[0])) ): if input_num in to_ignore: continue for pubkey in psbt_in.hd_keypaths.keys(): fp = psbt_in.hd_keypaths[pubkey].fingerprint if fp == master_fp and pubkey not in psbt_in.partial_sigs: psbt_in.partial_sigs[pubkey] = sig + b"\x01" break p += 1 return tx
def sign_tx(self, tx: PSBT) -> PSBT: """ Sign a transaction with the Blockstream Jade. - Jade can only be used to sign single-key inputs at this time. It cannot sign multisig or arbitrary scripts. """ c_txn = CTransaction(tx.tx) master_fp = self.get_master_fingerprint() signing_pubkeys = [None] * len(tx.inputs) # Signing input details jade_inputs = [] for n_vin, (txin, psbtin) in py_enumerate(zip(c_txn.vin, tx.inputs)): # Get bip32 path to use to sign, if required for this input path = None for pubkey, origin in psbtin.hd_keypaths.items(): if origin.fingerprint == master_fp and len(origin.path) > 0: # Our input if (pubkey not in psbtin.partial_sigs or not psbtin.partial_sigs[pubkey]): # hw to sign this input - it is not already signed signing_pubkeys[n_vin] = pubkey path = origin.path # Get the tx and prevout/scriptcode utxo = None input_txn_bytes = None if psbtin.witness_utxo: utxo = psbtin.witness_utxo if psbtin.non_witness_utxo: if txin.prevout.hash != psbtin.non_witness_utxo.sha256: raise BadArgumentError( "Input {} has a non_witness_utxo with the wrong hash". format(n_vin)) utxo = psbtin.non_witness_utxo.vout[txin.prevout.n] input_txn_bytes = psbtin.non_witness_utxo.serialize_without_witness( ) scriptcode = utxo.scriptPubKey if is_p2sh(scriptcode): scriptcode = psbtin.redeem_script witness_input, witness_version, witness_program = is_witness( scriptcode) if witness_input: if is_p2wsh(scriptcode): scriptcode = psbtin.witness_script elif is_p2wpkh(scriptcode): scriptcode = b"\x76\xa9\x14" + witness_program + b"\x88\xac" else: scriptcode = None # Build the input and add to the list jade_inputs.append({ "is_witness": witness_input, "input_tx": input_txn_bytes, "script": scriptcode, "path": path, }) # Change output details # This is optional, in that if we send it Jade validates the change output script # and the user need not confirm that ouptut. If not passed the change output must # be confirmed by the user on the hwwallet screen, like any other spend output. change = [None] * len(tx.outputs) for n_vout, (txout, psbtout) in py_enumerate(zip(c_txn.vout, tx.outputs)): for pubkey, origin in psbtout.hd_keypaths.items(): # Considers 'our' outputs as change as far as Jade is concerned # ie. can be auto-confirmed. # Is this ok, or should check path also, assuming bip44-like ? if origin.fingerprint == master_fp and len(origin.path) > 0: addr_type = None if txout.is_p2pkh(): addr_type = AddressType.LEGACY elif txout.is_witness()[0] and not txout.is_p2wsh(): addr_type = AddressType.WIT elif txout.is_p2sh(): addr_type = AddressType.SH_WIT # is it really though ? if addr_type: addr_type = self._convertAddrType(addr_type) change[n_vout] = { "path": origin.path, "variant": addr_type } # The txn itself txn_bytes = c_txn.serialize_without_witness() # Request Jade generate the signatures for our inputs. # Change details are passed to be validated on the hw (user does not confirm) signatures = self.jade.sign_tx(self._network(), txn_bytes, jade_inputs, change) # Push sigs into PSBT structure as appropriate for psbtin, pubkey, sig in zip(tx.inputs, signing_pubkeys, signatures): if pubkey and sig: psbtin.partial_sigs[pubkey] = sig # Return the updated psbt return tx