def test_sign_input(self): private_key = PrivateKey(secret=8675309) tx_ins = [] prev_tx = bytes.fromhex( "0025bc3c0fa8b7eb55b9437fdbd016870d18e0df0ace7bc9864efc38414147c8" ) tx_ins.append(TxIn(prev_tx, 0)) tx_outs = [ TxOut.to_address("mzx5YhAH9kNHtcN481u6WkjeHjYtVeKVh2", 99000000), TxOut.to_address("mnrVtF8DWjMu839VW3rBfgYaAfKk8983Xf", 10000000), ] tx = Tx(1, tx_ins, tx_outs, 0, network="testnet") self.assertTrue(tx.sign_input(0, private_key))
def test_sign_p2wpkh(self): private_key = PrivateKey(secret=8675309) prev_tx = bytes.fromhex( "6bfa079532dd9fad6cfbf218edc294fdfa7dd0cb3956375bc864577fb36fad97" ) prev_index = 0 fee = 500 tx_in = TxIn(prev_tx, prev_index) amount = tx_in.value(network="testnet") - fee tx_out = TxOut.to_address("mqYz6JpuKukHzPg94y4XNDdPCEJrNkLQcv", amount) t = Tx(1, [tx_in], [tx_out], 0, network="testnet", segwit=True) self.assertTrue(t.sign_input(0, private_key)) want = "0100000000010197ad6fb37f5764c85b375639cbd07dfafd94c2ed18f2fb6cad9fdd329507fa6b0000000000ffffffff014c400f00000000001976a9146e13971913b9aa89659a9f53d327baa8826f2d7588ac02483045022100feab5b8feefd5e774bdfdc1dc23525b40f1ffaa25a376f8453158614f00fa6cb02204456493d0bc606ebeb3fa008e056bbc96a67cb0c11abcc871bfc2bec60206bf0012103935581e52c354cd2f484fe8ed83af7a3097005b2f9c60bff71d35bd795f54b6700000000" self.assertEqual(t.serialize().hex(), want)
def test_sign_p2sh_p2wpkh(self): private_key = PrivateKey(secret=8675309) redeem_script = private_key.point.p2sh_p2wpkh_redeem_script() prev_tx = bytes.fromhex( "2e19b463bd5c8a3e0f10ae827f5a670f6794fca96394ecf8488321291d1c2ee9" ) prev_index = 1 fee = 500 tx_in = TxIn(prev_tx, prev_index) amount = tx_in.value(network="testnet") - fee tx_out = TxOut.to_address("mqYz6JpuKukHzPg94y4XNDdPCEJrNkLQcv", amount) t = Tx(1, [tx_in], [tx_out], 0, network="testnet", segwit=True) self.assertTrue(t.sign_input(0, private_key, redeem_script=redeem_script)) want = "01000000000101e92e1c1d29218348f8ec9463a9fc94670f675a7f82ae100f3e8a5cbd63b4192e0100000017160014d52ad7ca9b3d096a38e752c2018e6fbc40cdf26fffffffff014c400f00000000001976a9146e13971913b9aa89659a9f53d327baa8826f2d7588ac0247304402205e3ae5ac9a0e0a16ae04b0678c5732973ce31051ba9f42193e69843e600d84f2022060a91cbd48899b1bf5d1ffb7532f69ab74bc1701a253a415196b38feb599163b012103935581e52c354cd2f484fe8ed83af7a3097005b2f9c60bff71d35bd795f54b6700000000" self.assertEqual(t.serialize().hex(), want)
def test_op_cltv(self): locktime_0 = Locktime(1234) locktime_1 = Locktime(2345) sequence = Sequence() tx_in = TxIn(b"\x00" * 32, 0, sequence=sequence) tx_out = TxOut(1, Script()) tx_obj = Tx(1, [tx_in], [tx_out], locktime_1) stack = [] self.assertFalse(op_checklocktimeverify(stack, tx_obj, 0)) tx_in.sequence = Sequence(0xFFFFFFFE) self.assertFalse(op_checklocktimeverify(stack, tx_obj, 0)) stack = [encode_num(-5)] self.assertFalse(op_checklocktimeverify(stack, tx_obj, 0)) stack = [encode_num(locktime_0)] self.assertTrue(op_checklocktimeverify(stack, tx_obj, 0)) tx_obj.locktime = Locktime(1582820194) self.assertFalse(op_checklocktimeverify(stack, tx_obj, 0)) tx_obj.locktime = Locktime(500) self.assertFalse(op_checklocktimeverify(stack, tx_obj, 0))
def test_sign_p2sh_multisig(self): private_key1 = PrivateKey(secret=8675309) private_key2 = PrivateKey(secret=8675310) redeem_script = RedeemScript.create_p2sh_multisig( quorum_m=2, pubkey_hexes=[ private_key1.point.sec().hex(), private_key2.point.sec().hex(), ], sort_keys=False, ) prev_tx = bytes.fromhex( "ded9b3c8b71032d42ea3b2fd5211d75b39a90637f967e637b64dfdb887dd11d7" ) prev_index = 1 fee_sats = 500 tx_in = TxIn(prev_tx, prev_index) tx_in_sats = 1000000 amount = tx_in_sats - fee_sats tx_out = TxOut.to_address("mqYz6JpuKukHzPg94y4XNDdPCEJrNkLQcv", amount) t = Tx(1, [tx_in], [tx_out], 0, network="testnet", segwit=True) sig1 = t.get_sig_legacy(0, private_key1, redeem_script=redeem_script) sig2 = t.get_sig_legacy(0, private_key2, redeem_script=redeem_script) self.assertTrue( t.check_sig_legacy( 0, private_key1.point, Signature.parse(sig1[:-1]), redeem_script=redeem_script, ) ) self.assertTrue( t.check_sig_legacy( 0, private_key2.point, Signature.parse(sig2[:-1]), redeem_script=redeem_script, ) ) tx_in.finalize_p2sh_multisig([sig1, sig2], redeem_script) want = "01000000000101d711dd87b8fd4db637e667f93706a9395bd71152fdb2a32ed43210b7c8b3d9de01000000da00483045022100c457fa45f63636eb2552cef642116a8363469d60b99dcda19686d30ed2a539bb0220222c7617e3dd9aef37095df52047e9a6bf11254a88eab521aec1b8b4e7913b3401473044022003d3d6a1b232b42d9fb961b42ab6854077a1e195473d952d54e6dcf22ef6dede02206f62a44b65e1dbccbdd54a3fd6f87c05a8d8da39c70e06f5ee07d469e1155e020147522103935581e52c354cd2f484fe8ed83af7a3097005b2f9c60bff71d35bd795f54b672103674944c63d8dc3373a88cd1f8403b39b48be07bdb83d51dbbaa34be070c72e1452aeffffffff014c400f00000000001976a9146e13971913b9aa89659a9f53d327baa8826f2d7588ac0000000000" self.assertEqual(t.serialize().hex(), want)
def test_op_csv(self): sequence_0 = Sequence() sequence_1 = Sequence(2345) tx_in = TxIn(b"\x00" * 32, 0, sequence=sequence_0) tx_out = TxOut(1, Script()) tx_obj = Tx(1, [tx_in], [tx_out]) stack = [] self.assertFalse(op_checksequenceverify(stack, tx_obj, 0)) tx_in.sequence = sequence_1 self.assertFalse(op_checksequenceverify(stack, tx_obj, 0)) stack = [encode_num(-5)] self.assertFalse(op_checksequenceverify(stack, tx_obj, 0)) tx_obj.version = 2 self.assertFalse(op_checksequenceverify(stack, tx_obj, 0)) stack = [encode_num(1234 | (1 << 22))] self.assertFalse(op_checksequenceverify(stack, tx_obj, 0)) stack = [encode_num(9999)] self.assertFalse(op_checksequenceverify(stack, tx_obj, 0)) stack = [encode_num(1234)] self.assertTrue(op_checksequenceverify(stack, tx_obj, 0))
def test_sign_p2pkh(self): private_key = PrivateKey(secret=8675309) tx_ins = [] prev_tx = bytes.fromhex( "448c1cf931cb8a35d648b75a63c7dbdc6d81a8b82be07c055d599a4ce810a20a" ) tx_ins.append(TxIn(prev_tx, 0)) tx_outs = [ TxOut.to_address("mzx5YhAH9kNHtcN481u6WkjeHjYtVeKVh2", 5999000), TxOut.to_address("tb1qjfavna0z7r484w674f723w7g4jpeaplttt464w", 1000000), TxOut.to_address( "tb1qdhd06yyf7pazh2vx3hm37c3gq8lpra2993hlr784z4e3xwpgksmsceq9wc", 1000000, ), TxOut.to_address("2MyJsxLnxj7DsNch4xE7B3nMpB94kDPoE2s", 1000000), TxOut.to_address( "tb1p9gpzhc5fhlwlf49ze00fgjszxh5pl2p7az76758xwarweq08gcas8qa0r7", 1000000, ), ] tx_obj = Tx(1, tx_ins, tx_outs, 0, network="signet") self.assertTrue(tx_obj.sign_p2pkh(0, private_key))
def test_sign_p2sh_p2wsh_multisig(self): private_key1 = PrivateKey(secret=8675309) private_key2 = PrivateKey(secret=8675310) witness_script = WitnessScript( [0x52, private_key1.point.sec(), private_key2.point.sec(), 0x52, 0xAE] ) prev_tx = bytes.fromhex( "f92c8c8e40296c6a94539b6d22d8994a56dd8ff2d6018d07a8371fef1f66efee" ) prev_index = 0 fee = 500 tx_in = TxIn(prev_tx, prev_index) amount = tx_in.value(network="testnet") - fee tx_out = TxOut.to_address("mqYz6JpuKukHzPg94y4XNDdPCEJrNkLQcv", amount) t = Tx(1, [tx_in], [tx_out], 0, network="testnet", segwit=True) sig1 = t.get_sig_segwit(0, private_key1, witness_script=witness_script) sig2 = t.get_sig_segwit(0, private_key2, witness_script=witness_script) self.assertTrue( t.check_sig_segwit( 0, private_key1.point, Signature.parse(sig1[:-1]), witness_script=witness_script, ) ) self.assertTrue( t.check_sig_segwit( 0, private_key2.point, Signature.parse(sig2[:-1]), witness_script=witness_script, ) ) tx_in.finalize_p2sh_p2wsh_multisig([sig1, sig2], witness_script) want = "01000000000101eeef661fef1f37a8078d01d6f28fdd564a99d8226d9b53946a6c29408e8c2cf900000000232200206ddafd1089f07a2ba9868df71f622801fe11f5452c6ff1f8f51573133828b437ffffffff014c400f00000000001976a9146e13971913b9aa89659a9f53d327baa8826f2d7588ac0400483045022100d31433973b7f8014a4e17d46c4720c6c9bed1ee720dc1f0839dd847fa6972553022039278e98a3c18f4748a2727b99acd41eb1534dcf041a3abefd0c7546c868f55801473044022027be7d616b0930c1edf7ed39cc99edf5975e7b859d3224fe340d55c595c2798f02206c05662d39e5b05cc13f936360d62a482b122ad9791074bbdafec3ddc221b8c00147522103935581e52c354cd2f484fe8ed83af7a3097005b2f9c60bff71d35bd795f54b672103674944c63d8dc3373a88cd1f8403b39b48be07bdb83d51dbbaa34be070c72e1452ae00000000" self.assertEqual(t.serialize().hex(), want)
def test_sign_p2wsh_multisig(self): private_key1 = PrivateKey(secret=8675309) private_key2 = PrivateKey(secret=8675310) witness_script = WitnessScript( [0x52, private_key1.point.sec(), private_key2.point.sec(), 0x52, 0xAE] ) prev_tx = bytes.fromhex( "61cd20e3ffdf9216cee9cd607e1a65d3096513c4df3a63d410c047379b54a94a" ) prev_index = 1 fee = 500 tx_in = TxIn(prev_tx, prev_index) amount = tx_in.value(network="testnet") - fee tx_out = TxOut.to_address("mqYz6JpuKukHzPg94y4XNDdPCEJrNkLQcv", amount) t = Tx(1, [tx_in], [tx_out], 0, network="testnet", segwit=True) sig1 = t.get_sig_segwit(0, private_key1, witness_script=witness_script) sig2 = t.get_sig_segwit(0, private_key2, witness_script=witness_script) self.assertTrue( t.check_sig_segwit( 0, private_key1.point, Signature.parse(sig1[:-1]), witness_script=witness_script, ) ) self.assertTrue( t.check_sig_segwit( 0, private_key2.point, Signature.parse(sig2[:-1]), witness_script=witness_script, ) ) tx_in.finalize_p2wsh_multisig([sig1, sig2], witness_script) want = "010000000001014aa9549b3747c010d4633adfc4136509d3651a7e60cde9ce1692dfffe320cd610100000000ffffffff014c400f00000000001976a9146e13971913b9aa89659a9f53d327baa8826f2d7588ac04004730440220325e9f389c4835dab74d644e8c8e295535d9b082d28aefc3fa127e23538051bd022050d68dcecda660d4c01a8443c2b30bd0b3e4b1a405b0f352dcb068210862f6810147304402201abceabfc94903644cf7be836876eaa418cb226e03554c17a71c65b232f4507302202105a8344abae9632d1bc8249a52cf651c4ea02ca5259e20b50d8169c949f5a20147522103935581e52c354cd2f484fe8ed83af7a3097005b2f9c60bff71d35bd795f54b672103674944c63d8dc3373a88cd1f8403b39b48be07bdb83d51dbbaa34be070c72e1452ae00000000" self.assertEqual(t.serialize().hex(), want)
def create_multisig_psbt( public_key_records, input_dicts, output_dicts, fee_sats, script_type="p2sh", ): """ Helper method to create a multisig PSBT whose change can be validated. network (testnet/mainnet/signet) will be inferred from xpubs/tpubs. public_key_records are a list of entries that loom like this: [xfp_hex, xpub_b58, base_path] # TODO: turn this into a new object? """ if script_type != "p2sh": raise NotImplementedError(f"script_type {script_type} not yet implemented") # initialize variables network = None tx_lookup, pubkey_lookup, redeem_lookup, hd_pubs = {}, {}, {}, {} # Use a nested default dict for increased readability # It's possible (though nonstandard) for one xfp to have multiple public_key_records in a multisig wallet # https://stackoverflow.com/a/19189356 recursive_defaultdict = lambda: defaultdict(recursive_defaultdict) # noqa: E731 xfp_dict = recursive_defaultdict() # This at the child pubkey lookup that each input will traverse off of for xfp_hex, xpub_b58, base_path in public_key_records: hd_pubkey_obj = HDPublicKey.parse(xpub_b58) # We will use this dict/list structure for each input/ouput in the for-loops below xfp_dict[xfp_hex][base_path] = hd_pubkey_obj named_global_hd_pubkey_obj = NamedHDPublicKey.from_hd_pub( child_hd_pub=hd_pubkey_obj, xfp_hex=xfp_hex, # we're only going to base path level path=base_path, ) hd_pubs[named_global_hd_pubkey_obj.serialize()] = named_global_hd_pubkey_obj if network is None: # Set the initial value network = hd_pubkey_obj.network else: # Confirm it hasn't changed if network != hd_pubkey_obj.network: raise MixedNetwork( f"Mixed networks in public key records: {public_key_records}" ) tx_ins, total_input_sats = [], 0 for cnt, input_dict in enumerate(input_dicts): # Prev tx stuff prev_tx_dict = input_dict["prev_tx_dict"] prev_tx_obj = Tx.parse_hex(prev_tx_dict["hex"], network=network) tx_lookup[prev_tx_obj.hash()] = prev_tx_obj if prev_tx_dict["hash_hex"] != prev_tx_obj.hash().hex(): raise ValueError( f"Hash digest mismatch for input #{cnt}: {prev_tx_dict['hash_hex']} != {prev_tx_obj.hash().hex()}" ) if "path_dict" in input_dict: # Standard BIP67 unordered list of pubkeys (will be sorted lexicographically) iterator = input_dict["path_dict"].items() sort_keys = True elif "path_list" in input_dict: # Caller supplied ordering of pubkeys (will not be sorted) iterator = input_dict["path_list"] sort_keys = False else: raise RuntimeError( f"input_dict has no `path_dict` nor a `path_list`: {input_dict}" ) input_pubkey_hexes = [] for xfp_hex, root_path in iterator: # Get the correct xpub/path child_hd_pubkey = _safe_get_child_hdpubkey( xfp_dict=xfp_dict, xfp_hex=xfp_hex, root_path=root_path, cnt=cnt, ) input_pubkey_hexes.append(child_hd_pubkey.sec().hex()) # Enhance the PSBT named_hd_pubkey_obj = NamedHDPublicKey.from_hd_pub( child_hd_pub=child_hd_pubkey, xfp_hex=xfp_hex, path=root_path, ) # pubkey lookups needed for validation pubkey_lookup[named_hd_pubkey_obj.sec()] = named_hd_pubkey_obj utxo = prev_tx_obj.tx_outs[prev_tx_dict["output_idx"]] # Grab amount as developer safety check if prev_tx_dict["output_sats"] != utxo.amount: raise ValueError( f"Wrong number of sats for input #{cnt}! Expecting {prev_tx_dict['output_sats']} but got {utxo.amount}" ) total_input_sats += utxo.amount redeem_script = RedeemScript.create_p2sh_multisig( quorum_m=input_dict["quorum_m"], pubkey_hexes=input_pubkey_hexes, sort_keys=sort_keys, expected_addr=utxo.script_pubkey.address(network=network), expected_addr_network=network, ) # Confirm address matches previous ouput if redeem_script.address(network=network) != utxo.script_pubkey.address( network=network ): raise ValueError( f"Invalid redeem script for input #{cnt}. Expecting {redeem_script.address(network=network)} but got {utxo.script_pubkey.address(network=network)}" ) tx_in = TxIn(prev_tx=prev_tx_obj.hash(), prev_index=prev_tx_dict["output_idx"]) tx_ins.append(tx_in) # For enhancing the PSBT for HWWs: redeem_lookup[redeem_script.hash160()] = redeem_script tx_outs = [] for cnt, output_dict in enumerate(output_dicts): tx_out = TxOut( amount=output_dict["sats"], script_pubkey=address_to_script_pubkey(output_dict["address"]), ) tx_outs.append(tx_out) if output_dict.get("path_dict"): # This output claims to be change, so we must validate it here output_pubkey_hexes = [] for xfp_hex, root_path in output_dict["path_dict"].items(): child_hd_pubkey = _safe_get_child_hdpubkey( xfp_dict=xfp_dict, xfp_hex=xfp_hex, root_path=root_path, cnt=cnt, ) output_pubkey_hexes.append(child_hd_pubkey.sec().hex()) # Enhance the PSBT named_hd_pubkey_obj = NamedHDPublicKey.from_hd_pub( child_hd_pub=child_hd_pubkey, xfp_hex=xfp_hex, path=root_path, ) pubkey_lookup[named_hd_pubkey_obj.sec()] = named_hd_pubkey_obj redeem_script = RedeemScript.create_p2sh_multisig( quorum_m=output_dict["quorum_m"], pubkey_hexes=output_pubkey_hexes, # We intentionally only allow change addresses to be lexicographically sorted sort_keys=True, ) # Confirm address matches previous ouput if redeem_script.address(network=network) != output_dict["address"]: raise ValueError( f"Invalid redeem script for output #{cnt}. Expecting {redeem_script.address(network=network)} but got {output_dict['address']}" ) # For enhancing the PSBT for HWWs: redeem_lookup[redeem_script.hash160()] = redeem_script tx_obj = Tx( version=1, tx_ins=tx_ins, tx_outs=tx_outs, locktime=0, network=network, segwit=False, ) # Safety check to try and prevent footgun calculated_fee_sats = total_input_sats - sum([tx_out.amount for tx_out in tx_outs]) if fee_sats != calculated_fee_sats: raise ValueError( f"TX fee of {fee_sats} sats supplied != {calculated_fee_sats} sats calculated" ) return PSBT.create( tx_obj=tx_obj, validate=True, tx_lookup=tx_lookup, pubkey_lookup=pubkey_lookup, redeem_lookup=redeem_lookup, witness_lookup={}, hd_pubs=hd_pubs, )