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, psbt: PSBT) -> Dict[str, str]: def find_our_key( keypaths: Dict[bytes, Sequence[int]] ) -> Tuple[Optional[bytes], Optional[Sequence[int]]]: """ Keypaths is a map of pubkey to hd keypath, where the first element in the keypath is the master fingerprint. We attempt to find the key which belongs to the BitBox02 by matching the fingerprint, and then matching the pubkey. Returns the pubkey and the keypath, without the fingerprint. """ for pubkey, keypath_with_fingerprint in keypaths.items(): fp, keypath = keypath_with_fingerprint[0], keypath_with_fingerprint[1:] # Cheap check if the key is ours. if fp != master_fp: continue # Expensive check if the key is ours. # TODO: check for fingerprint collision # keypath_account = keypath[:-2] return pubkey, keypath return None, None def get_simple_type( output: CTxOut, redeem_script: bytes ) -> bitbox02.btc.BTCScriptConfig.SimpleType: if is_p2pkh(output.scriptPubKey): raise BadArgumentError( "The BitBox02 does not support legacy p2pkh scripts" ) if is_p2wpkh(output.scriptPubKey): return bitbox02.btc.BTCScriptConfig.P2WPKH if output.is_p2sh() and is_p2wpkh(redeem_script): return bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH raise BadArgumentError( "Input script type not recognized of input {}.".format(input_index) ) master_fp = struct.unpack("<I", unhexlify(self.get_master_fingerprint_hex()))[0] inputs: List[bitbox02.BTCInputType] = [] bip44_account = None # One pubkey per input. The pubkey identifies the key per input with which we sign. There # must be exactly one pubkey per input that belongs to the BitBox02. found_pubkeys: List[bytes] = [] for input_index, (psbt_in, tx_in) in builtins.enumerate( zip(psbt.inputs, psbt.tx.vin) ): if psbt_in.sighash and psbt_in.sighash != 1: raise BadArgumentError( "The BitBox02 only supports SIGHASH_ALL. Found sighash: {}".format( psbt_in.sighash ) ) utxo = None prevtx = None # psbt_in.witness_utxo was originally used for segwit utxo's, but since it was # discovered that the amounts are not correctly committed to in the segwit sighash, the # full prevtx (non_witness_utxo) is supplied for both segwit and non-segwit inputs. # See # - https://medium.com/shiftcrypto/bitbox-app-firmware-update-6-2020-c70f733a5330 # - https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd. # - https://github.com/zkSNACKs/WalletWasabi/pull/3822 # The BitBox02 for now requires the prevtx, at least until Taproot activates. if psbt_in.non_witness_utxo: if tx_in.prevout.hash != psbt_in.non_witness_utxo.sha256: raise BadArgumentError( "Input {} has a non_witness_utxo with the wrong hash".format( input_index ) ) utxo = psbt_in.non_witness_utxo.vout[tx_in.prevout.n] prevtx = psbt_in.non_witness_utxo elif psbt_in.witness_utxo: utxo = psbt_in.witness_utxo if utxo is None: raise BadArgumentError("No utxo found for input {}".format(input_index)) if prevtx is None: raise BadArgumentError( "Previous transaction missing for input {}".format(input_index) ) found_pubkey, keypath = find_our_key(psbt_in.hd_keypaths) if not found_pubkey: raise BadArgumentError("No key found for input {}".format(input_index)) assert keypath is not None found_pubkeys.append(found_pubkey) # TOOD: validate keypath if bip44_account is None: bip44_account = keypath[2] elif bip44_account != keypath[2]: raise BadArgumentError( "The bip44 account index must be the same for all inputs and changes" ) simple_type = get_simple_type(utxo, psbt_in.redeem_script) script_config_index_map = { bitbox02.btc.BTCScriptConfig.P2WPKH: 0, bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH: 1, } inputs.append( { "prev_out_hash": ser_uint256(tx_in.prevout.hash), "prev_out_index": tx_in.prevout.n, "prev_out_value": utxo.nValue, "sequence": tx_in.nSequence, "keypath": keypath, "script_config_index": script_config_index_map[simple_type], "prev_tx": { "version": prevtx.nVersion, "locktime": prevtx.nLockTime, "inputs": [ { "prev_out_hash": ser_uint256(prev_in.prevout.hash), "prev_out_index": prev_in.prevout.n, "signature_script": prev_in.scriptSig, "sequence": prev_in.nSequence, } for prev_in in prevtx.vin ], "outputs": [ { "value": prev_out.nValue, "pubkey_script": prev_out.scriptPubKey, } for prev_out in prevtx.vout ], }, } ) outputs: List[bitbox02.BTCOutputType] = [] for output_index, (psbt_out, tx_out) in builtins.enumerate( zip(psbt.outputs, psbt.tx.vout) ): _, keypath = find_our_key(psbt_out.hd_keypaths) is_change = keypath and keypath[-2] == 1 if is_change: assert keypath is not None simple_type = get_simple_type(tx_out, psbt_out.redeem_script) outputs.append( bitbox02.BTCOutputInternal( keypath=keypath, value=tx_out.nValue, script_config_index=script_config_index_map[simple_type], ) ) else: if tx_out.is_p2pkh(): output_type = bitbox02.btc.P2PKH output_hash = tx_out.scriptPubKey[3:23] elif is_p2wpkh(tx_out.scriptPubKey): output_type = bitbox02.btc.P2WPKH output_hash = tx_out.scriptPubKey[2:] elif tx_out.is_p2sh(): output_type = bitbox02.btc.P2SH output_hash = tx_out.scriptPubKey[2:22] elif is_p2wsh(tx_out.scriptPubKey): output_type = bitbox02.btc.P2WSH output_hash = tx_out.scriptPubKey[2:] else: raise BadArgumentError( "Output type not recognized of output {}".format(output_index) ) outputs.append( bitbox02.BTCOutputExternal( output_type=output_type, output_hash=output_hash, value=tx_out.nValue, ) ) assert bip44_account is not None bip44_network = 1 + HARDENED if self.is_testnet else 0 + HARDENED sigs = self.init().btc_sign( bitbox02.btc.TBTC if self.is_testnet else bitbox02.btc.BTC, [ bitbox02.btc.BTCScriptConfigWithKeypath( script_config=bitbox02.btc.BTCScriptConfig( simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH ), keypath=[84 + HARDENED, bip44_network, bip44_account], ), bitbox02.btc.BTCScriptConfigWithKeypath( script_config=bitbox02.btc.BTCScriptConfig( simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH ), keypath=[49 + HARDENED, bip44_network, bip44_account], ), ], inputs=inputs, outputs=outputs, locktime=psbt.tx.nLockTime, version=psbt.tx.nVersion, ) for (_, sig), pubkey, psbt_in in zip(sigs, found_pubkeys, psbt.inputs): r, s = sig[:32], sig[32:64] # ser_sig_der() adds SIGHASH_ALL psbt_in.partial_sigs[pubkey] = ser_sig_der(r, s) return {"psbt": psbt.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()}