def fill_psbt(self, b64psbt, non_witness: bool = True, xpubs: bool = True): psbt = PSBT() psbt.deserialize(b64psbt) if non_witness: for i, inp in enumerate(psbt.tx.vin): txid = inp.prevout.hash.to_bytes(32, 'big').hex() try: res = self.cli.gettransaction(txid) except: raise SpecterError( "Can't find previous transaction in the wallet.") stream = BytesIO(bytes.fromhex(res["hex"])) prevtx = CTransaction() prevtx.deserialize(stream) psbt.inputs[i].non_witness_utxo = prevtx if xpubs: # for multisig add xpub fields if len(self.keys) > 1: for k in self.keys: key = b'\x01' + decode_base58(k.xpub) if k.fingerprint != '': fingerprint = bytes.fromhex(k.fingerprint) else: fingerprint = get_xpub_fingerprint(k.xpub) if k.derivation != '': der = der_to_bytes(k.derivation) else: der = b'' value = fingerprint + der psbt.unknown[key] = value return psbt.serialize()
def fill_psbt(self, b64psbt): psbt = PSBT() psbt.deserialize(b64psbt) for i, inp in enumerate(psbt.tx.vin): txid = inp.prevout.hash.to_bytes(32,'big').hex() try: res = self.cli.gettransaction(txid) except: raise SpecterError("Can't find previous transaction in the wallet.") stream = BytesIO(bytes.fromhex(res["hex"])) prevtx = CTransaction() prevtx.deserialize(stream) psbt.inputs[i].non_witness_utxo = prevtx return psbt.serialize()
def get_txid(tx): b = BytesIO(bytes.fromhex(tx)) t = CTransaction() t.deserialize(b) for inp in t.vin: inp.scriptSig = b"" t.rehash() return t.hash
def fill_psbt(self, b64psbt, non_witness: bool = True, xpubs: bool = True): psbt = PSBT() psbt.deserialize(b64psbt) if non_witness: for i, inp in enumerate(psbt.tx.vin): txid = inp.prevout.hash.to_bytes(32, "big").hex() try: res = self.gettransaction(txid) stream = BytesIO(bytes.fromhex(res["hex"])) prevtx = CTransaction() prevtx.deserialize(stream) psbt.inputs[i].non_witness_utxo = prevtx except: logger.error( "Can't find previous transaction in the wallet. Signing might not be possible for certain devices..." ) else: # remove non_witness_utxo if we don't want them for inp in psbt.inputs: if inp.witness_utxo is not None: inp.non_witness_utxo = None if xpubs: # for multisig add xpub fields if len(self.keys) > 1: for k in self.keys: key = b"\x01" + decode_base58(k.xpub) if k.fingerprint != "": fingerprint = bytes.fromhex(k.fingerprint) else: fingerprint = get_xpub_fingerprint(k.xpub) if k.derivation != "": der = der_to_bytes(k.derivation) else: der = b"" value = fingerprint + der psbt.unknown[key] = value return 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()}