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 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 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 _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 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 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 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 test_invalid_psbt(self): for invalid in self.data['invalid']: with self.subTest(invalid=invalid): with self.assertRaises(PSBTSerializationError) as cm: psbt = PSBT() psbt.deserialize(invalid)
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")