def _generate_and_finalize(self, unknown_inputs, psbt): if not unknown_inputs: # Just do the normal signing process to test "all inputs" case sign_res = self.do_command(self.dev_args + ['signtx', psbt['psbt']]) finalize_res = self.wrpc.finalizepsbt(sign_res['psbt']) else: # Sign only input one on first pass # then rest on second pass to test ability to successfully # ignore inputs that are not its own. Then combine both # signing passes to ensure they are actually properly being # partially signed at each step. first_psbt = PSBT() first_psbt.deserialize(psbt['psbt']) second_psbt = PSBT() second_psbt.deserialize(psbt['psbt']) # Blank master fingerprint to make hww fail to sign # Single input PSBTs will be fully signed by first signer for psbt_input in first_psbt.inputs[1:]: for pubkey, path in psbt_input.hd_keypaths.items(): psbt_input.hd_keypaths[pubkey] = KeyOriginInfo( b"\x00\x00\x00\x00", path.path) for pubkey, path in second_psbt.inputs[0].hd_keypaths.items(): second_psbt.inputs[0].hd_keypaths[pubkey] = KeyOriginInfo( b"\x00\x00\x00\x00", path.path) single_input = len(first_psbt.inputs) == 1 # Process the psbts first_psbt = first_psbt.serialize() second_psbt = second_psbt.serialize() # First will always have something to sign first_sign_res = self.do_command(self.dev_args + ['signtx', first_psbt]) self.assertTrue(single_input == self.wrpc.finalizepsbt( first_sign_res['psbt'])['complete']) # Second may have nothing to sign (1 input case) # and also may throw an error(e.g., ColdCard) second_sign_res = self.do_command(self.dev_args + ['signtx', second_psbt]) if 'psbt' in second_sign_res: self.assertTrue(not self.wrpc.finalizepsbt( second_sign_res['psbt'])['complete']) combined_psbt = self.wrpc.combinepsbt( [first_sign_res['psbt'], second_sign_res['psbt']]) else: self.assertTrue('error' in second_sign_res) combined_psbt = first_sign_res['psbt'] finalize_res = self.wrpc.finalizepsbt(combined_psbt) self.assertTrue(finalize_res['complete']) self.assertTrue( self.wrpc.testmempoolaccept([finalize_res['hex'] ])[0]["allowed"]) return finalize_res['hex']
def sign_tx(self, psbt: PSBT) -> Dict[str, str]: """Sign a partially signed bitcoin transaction (PSBT). Return {"psbt": <base64 psbt string>}. """ # this one can hang for quite some time response = self.query("sign %s" % psbt.serialize()) signed_psbt = PSBT() signed_psbt.deserialize(response) # adding partial sigs to initial tx for i in range(len(psbt.inputs)): for k in signed_psbt.inputs[i].partial_sigs: psbt.inputs[i].partial_sigs[k] = signed_psbt.inputs[ i].partial_sigs[k] return {'psbt': psbt.serialize()}
def clean_psbt(b64psbt): psbt = PSBT() psbt.deserialize(b64psbt) for inp in psbt.inputs: if inp.witness_utxo is not None and inp.non_witness_utxo is not None: inp.non_witness_utxo = None return psbt.serialize()
def test_valid_psbt(self): for valid in self.data['valid']: with self.subTest(valid=valid): psbt = PSBT() psbt.deserialize(valid) serd = psbt.serialize() self.assertEqual(valid, serd)
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 sign_tx(self, psbt: PSBT) -> Dict[str, str]: """Sign a partially signed bitcoin transaction (PSBT). Return {"psbt": <base64 psbt string>}. """ # this one can hang for quite some time signed_tx = self.query("sign %s" % psbt.serialize()) return {"psbt": signed_tx}
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 create_psbts(self, base64_psbt, wallet): psbts = HWIDevice.create_psbts(self, base64_psbt, wallet) sdcard_psbt = PSBT() sdcard_psbt.deserialize(base64_psbt) if len(wallet.keys) > 1: for k in wallet.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 sdcard_psbt.unknown[key] = value psbts['sdcard'] = sdcard_psbt.serialize() return psbts
def create_psbts(self, base64_psbt, wallet): psbts = super().create_psbts(base64_psbt, wallet) qr_psbt = PSBT() qr_psbt.deserialize(base64_psbt) for inp in qr_psbt.inputs + qr_psbt.outputs: inp.witness_script = b"" inp.redeem_script = b"" if len(inp.hd_keypaths) > 0: k = list(inp.hd_keypaths.keys())[0] # proprietary field - wallet derivation path # only contains two last derivation indexes - change and index inp.unknown[b"\xfc\xca\x01" + get_wallet_fingerprint(wallet)] = b"".join([ i.to_bytes(4, "little") for i in inp.hd_keypaths[k][-2:] ]) inp.hd_keypaths = {} psbts['qrcode'] = qr_psbt.serialize() return psbts
def create_psbts(self, base64_psbt, wallet): psbts = super().create_psbts(base64_psbt, wallet) qr_psbt = PSBT() # remove non-witness utxo if they are there to reduce QR code size updated_psbt = wallet.fill_psbt(base64_psbt, non_witness=False, xpubs=False) qr_psbt.deserialize(updated_psbt) # replace with compressed wallet information for inp in qr_psbt.inputs + qr_psbt.outputs: inp.witness_script = b"" inp.redeem_script = b"" if len(inp.hd_keypaths) > 0: k = list(inp.hd_keypaths.keys())[0] # proprietary field - wallet derivation path # only contains two last derivation indexes - change and index wallet_key = b"\xfc\xca\x01" + get_wallet_fingerprint(wallet) inp.unknown[wallet_key] = b"".join( [i.to_bytes(4, "little") for i in inp.hd_keypaths[k][-2:]]) inp.hd_keypaths = {} psbts['qrcode'] = qr_psbt.serialize() return psbts
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, 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 send(args): # Load the wallet file wallet = load_wallet_file(args.wallet) # Load the watch only wallet rpc = LoadWalletAndGetRPC(args.wallet, wallet['rpcurl']) # Get a change address from the internal keypool change_addr = wallet['internal_keypool'][wallet['internal_next']] wallet['internal_next'] += 1 # Write to the wallet write_wallet_to_file(args.wallet, wallet) # Create the transaction outputs = json.loads(args.recipients) locktime = rpc.getblockcount() psbtx = rpc.walletcreatefundedpsbt([], outputs, locktime, {'changeAddress' : change_addr, 'replaceable' : True, 'includeWatching' : True}, True) psbt = psbtx['psbt'] # HACK: Hack to prevent change detection because hardware wallets are apparently really bad at change detection for multisig change tx = PSBT() tx.deserialize(psbt) for output in tx.outputs: output.set_null() psbt = tx.serialize() # Send psbt to devices to sign out = {} out['devices'] = [] psbts = [] for d in wallet['devices']: if 'core_wallet_name' in d: wrpc = LoadWalletAndGetRPC(d['core_wallet_name'], d['rpcurl']) result = wrpc.walletprocesspsbt(psbt) core_out = {'success' : True} out['core'] = core_out psbts.append(result['psbt']) else: d_out = {} hwi_args = [] if args.testnet or args.regtest: hwi_args.append('--testnet') if 'password' in d: hwi_args.append('-p') hwi_args.append(d['password']) hwi_args.append('-f') hwi_args.append(d['fingerprint']) hwi_args.append('signtx') hwi_args.append(psbt) result = hwi_command(hwi_args) psbts.append(result['psbt']) d_out['fingerprint'] = d['fingerprint'] d_out['success'] = True out['devices'].append(d_out) # Combine, finalize, and send psbts combined = rpc.combinepsbt(psbts) finalized = rpc.finalizepsbt(combined) if not finalized['complete']: out['success'] = False out['psbt'] = finalized['psbt'] return out out['success'] = True if args.inspecttx: out['tx'] = finalized['hex'] else: out['txid'] = rpc.sendrawtransaction(finalized['hex']) return out
# Open the data file with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data/test_psbt.json'), encoding='utf-8') as f: d = json.load(f) invalids = d['invalid'] valids = d['valid'] creators = d['creator'] signers = d['signer'] combiners = d['combiner'] finalizers = d['finalizer'] extractors = d['extractor'] print("Testing invalid PSBTs") for invalid in invalids: try: psbt = PSBT() psbt.deserialize(invalid) assert False except: pass print("Testing valid PSBTs") for valid in valids: psbt = PSBT() psbt.deserialize(valid) serd = psbt.serialize() assert (valid == serd) print("PSBT Serialization tests pass")