def sign_tx(self, tx): 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 = proto.TxInputType() # Set the input stuff txinputtype.prev_hash = ser_uint256(txin.prevout.hash)[::-1] txinputtype.prev_index = txin.prevout.n txinputtype.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 = proto.InputScriptType.SPENDP2SHWITNESS else: txinputtype.script_type = proto.InputScriptType.SPENDWITNESS else: txinputtype.script_type = proto.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(): txinputtype.address_n = [ 0x80000000 | 84, 0x80000000 | (1 if self.is_testnet else 0), ] txinputtype.multisig = None txinputtype.script_type = proto.InputScriptType.SPENDWITNESS inputs.append(txinputtype) to_ignore.append(input_num) # Check for multisig is_ms, multisig = parse_multisig(scriptcode) if is_ms: txinputtype.multisig = parse_multisig_xpubs( tx, psbt_in, multisig) if not is_wit: if utxo.is_p2sh: txinputtype.script_type = ( proto.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[0] == 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[1:] 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.is_testnet: 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 = proto.TxOutputType() txoutput.amount = out.nValue txoutput.script_type = proto.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) 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[0] == master_fp: wit, ver, prog = out.is_witness() if out.is_p2pkh(): txoutput.address_n = keypath[1:] txoutput.address = None elif wit: txoutput.script_type = proto.OutputScriptType.PAYTOWITNESS txoutput.address_n = keypath[1:] 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) == 20: txoutput.script_type = ( proto.OutputScriptType.PAYTOP2SHWITNESS) txoutput.address_n = keypath[1:] txoutput.address = None is_ms, multisig = parse_multisig( psbt_out.witness_script if wit else psbt_out. redeem_script) if is_ms: txoutput.multisig = parse_multisig_xpubs( tx, psbt_out, 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 = proto.TransactionType() t.version = prev.nVersion t.lock_time = prev.nLockTime for vin in prev.vin: i = proto.TxInputType() i.prev_hash = ser_uint256(vin.prevout.hash)[::-1] i.prev_index = vin.prevout.n i.script_sig = vin.scriptSig i.sequence = vin.nSequence t.inputs.append(i) for vout in prev.vout: o = proto.TxOutputBinType() o.amount = vout.nValue o.script_pubkey = vout.scriptPubKey t.bin_outputs.append(o) logging.debug(psbt_in.non_witness_utxo.hash) prevtxs[ser_uint256( psbt_in.non_witness_utxo.sha256)[::-1]] = t # Sign the transaction tx_details = proto.SignTx() tx_details.version = tx.tx.nVersion tx_details.lock_time = tx.tx.nLockTime signed_tx = btc.sign_tx(self.client, self.coin_name, inputs, outputs, tx_details, prevtxs) # 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][0] if fp == master_fp and pubkey not in psbt_in.partial_sigs: psbt_in.partial_sigs[pubkey] = sig + b"\x01" break p += 1 return {"psbt": tx.serialize()}
def sign_tx(self, tx): c_tx = CTransaction(tx.tx) tx_bytes = c_tx.serialize_with_witness() # Master key fingerprint master_fpr = hash160( compress_public_key( self.app.getWalletPublicKey("")["publicKey"]))[:4] # An entry per input, each with 0 to many keys to sign with all_signature_attempts = [[]] * len(c_tx.vin) # Get the app version to determine whether to use Trusted Input for segwit version = self.app.getFirmwareVersion() use_trusted_segwit = ( version["major_version"] == 1 and version["minor_version"] >= 4) or version["major_version"] > 1 # NOTE: We only support signing Segwit inputs, where we can skip over non-segwit # inputs, or non-segwit inputs, where *all* inputs are non-segwit. This is due # to Ledger's mutually exclusive signing steps for each type. segwit_inputs = [] # Legacy style inputs legacy_inputs = [] has_segwit = False has_legacy = False script_codes = [[]] * len(c_tx.vin) # Detect changepath, (p2sh-)p2(w)pkh only change_path = "" for txout, i_num in zip(c_tx.vout, range(len(c_tx.vout))): # Find which wallet key could be change based on hdsplit: m/.../1/k # Wallets shouldn't be sending to change address as user action # otherwise this will get confused for pubkey, path in tx.outputs[i_num].hd_keypaths.items(): if (struct.pack("<I", path[0]) == master_fpr and len(path) > 2 and path[-2] == 1): # For possible matches, check if pubkey matches possible template if (hash160(pubkey) in txout.scriptPubKey or hash160( bytearray.fromhex("0014") + hash160(pubkey)) in txout.scriptPubKey): change_path = "" for index in path[1:]: change_path += str(index) + "/" change_path = change_path[:-1] for txin, psbt_in, i_num in zip(c_tx.vin, tx.inputs, range(len(c_tx.vin))): seq = format(txin.nSequence, "x") seq = seq.zfill(8) seq = bytearray.fromhex(seq) seq.reverse() seq_hex = "".join("{:02x}".format(x) for x in seq) 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(i_num)) utxo = psbt_in.non_witness_utxo.vout[txin.prevout.n] if utxo is None: raise Exception( "PSBT is missing input utxo information, cannot sign") scriptcode = utxo.scriptPubKey if is_p2sh(scriptcode): if len(psbt_in.redeem_script) == 0: continue scriptcode = psbt_in.redeem_script is_wit, _, _ = is_witness(scriptcode) segwit_inputs.append({ "value": txin.prevout.serialize() + struct.pack("<Q", utxo.nValue), "witness": True, "sequence": seq_hex, }) if is_wit: if is_p2wsh(scriptcode): if len(psbt_in.witness_script) == 0: continue scriptcode = psbt_in.witness_script elif is_p2wpkh(scriptcode): _, _, wit_prog = is_witness(scriptcode) scriptcode = b"\x76\xa9\x14" + wit_prog + b"\x88\xac" else: continue has_segwit = True else: # We only need legacy inputs in the case where all inputs are legacy, we check # later ledger_prevtx = bitcoinTransaction( psbt_in.non_witness_utxo.serialize()) legacy_inputs.append( self.app.getTrustedInput(ledger_prevtx, txin.prevout.n)) legacy_inputs[-1]["sequence"] = seq_hex has_legacy = True if psbt_in.non_witness_utxo and use_trusted_segwit: ledger_prevtx = bitcoinTransaction( psbt_in.non_witness_utxo.serialize()) segwit_inputs[-1].update( self.app.getTrustedInput(ledger_prevtx, txin.prevout.n)) pubkeys = [] signature_attempts = [] # Save scriptcode for later signing script_codes[i_num] = scriptcode # Find which pubkeys could sign this input (should be all?) for pubkey in psbt_in.hd_keypaths.keys(): if hash160(pubkey) in scriptcode or pubkey in scriptcode: pubkeys.append(pubkey) # Figure out which keys in inputs are from our wallet for pubkey in pubkeys: keypath = psbt_in.hd_keypaths[pubkey] if master_fpr == struct.pack("<I", keypath[0]): # Add the keypath strings keypath_str = "" for index in keypath[1:]: keypath_str += str(index) + "/" keypath_str = keypath_str[:-1] signature_attempts.append([keypath_str, pubkey]) all_signature_attempts[i_num] = signature_attempts # Sign any segwit inputs if has_segwit: # Process them up front with all scriptcodes blank blank_script_code = bytearray() for i in range(len(segwit_inputs)): self.app.startUntrustedTransaction( i == 0, i, segwit_inputs, script_codes[i] if use_trusted_segwit else blank_script_code, c_tx.nVersion, ) # Number of unused fields for Nano S, only changepath and transaction in bytes req self.app.finalizeInput(b"DUMMY", -1, -1, change_path, tx_bytes) # For each input we control do segwit signature for i in range(len(segwit_inputs)): for signature_attempt in all_signature_attempts[i]: self.app.startUntrustedTransaction(False, 0, [segwit_inputs[i]], script_codes[i], c_tx.nVersion) tx.inputs[i].partial_sigs[ signature_attempt[1]] = self.app.untrustedHashSign( signature_attempt[0], "", c_tx.nLockTime, 0x01) elif has_legacy: first_input = True # Legacy signing if all inputs are legacy for i in range(len(legacy_inputs)): for signature_attempt in all_signature_attempts[i]: assert tx.inputs[i].non_witness_utxo is not None self.app.startUntrustedTransaction(first_input, i, legacy_inputs, script_codes[i], c_tx.nVersion) self.app.finalizeInput(b"DUMMY", -1, -1, change_path, tx_bytes) tx.inputs[i].partial_sigs[ signature_attempt[1]] = self.app.untrustedHashSign( signature_attempt[0], "", c_tx.nLockTime, 0x01) first_input = False # Send PSBT back return {"psbt": tx.serialize()}