def test_coinbase_height(self): raw_tx = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff5e03d71b07254d696e656420627920416e74506f6f6c20626a31312f4542312f4144362f43205914293101fabe6d6d678e2c8c34afc36896e7d9402824ed38e856676ee94bfdb0c6c4bcd8b2e5666a0400000000000000c7270000a5e00e00ffffffff01faf20b58000000001976a914338c84849423992471bffb1a54a8d9b1d69dc28a88ac00000000" tx = Tx.parse_hex(raw_tx) self.assertEqual(tx.coinbase_height(), 465879) raw_tx = "0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600" tx = Tx.parse_hex(raw_tx) self.assertIsNone(tx.coinbase_height())
def test_fee(self): raw_tx = "0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600" tx = Tx.parse_hex(raw_tx) self.assertEqual(tx.fee(), 40000) raw_tx = "010000000456919960ac691763688d3d3bcea9ad6ecaf875df5339e148a1fc61c6ed7a069e010000006a47304402204585bcdef85e6b1c6af5c2669d4830ff86e42dd205c0e089bc2a821657e951c002201024a10366077f87d6bce1f7100ad8cfa8a064b39d4e8fe4ea13a7b71aa8180f012102f0da57e85eec2934a82a585ea337ce2f4998b50ae699dd79f5880e253dafafb7feffffffeb8f51f4038dc17e6313cf831d4f02281c2a468bde0fafd37f1bf882729e7fd3000000006a47304402207899531a52d59a6de200179928ca900254a36b8dff8bb75f5f5d71b1cdc26125022008b422690b8461cb52c3cc30330b23d574351872b7c361e9aae3649071c1a7160121035d5c93d9ac96881f19ba1f686f15f009ded7c62efe85a872e6a19b43c15a2937feffffff567bf40595119d1bb8a3037c356efd56170b64cbcc160fb028fa10704b45d775000000006a47304402204c7c7818424c7f7911da6cddc59655a70af1cb5eaf17c69dadbfc74ffa0b662f02207599e08bc8023693ad4e9527dc42c34210f7a7d1d1ddfc8492b654a11e7620a0012102158b46fbdff65d0172b7989aec8850aa0dae49abfb84c81ae6e5b251a58ace5cfeffffffd63a5e6c16e620f86f375925b21cabaf736c779f88fd04dcad51d26690f7f345010000006a47304402200633ea0d3314bea0d95b3cd8dadb2ef79ea8331ffe1e61f762c0f6daea0fabde022029f23b3e9c30f080446150b23852028751635dcee2be669c2a1686a4b5edf304012103ffd6f4a67e94aba353a00882e563ff2722eb4cff0ad6006e86ee20dfe7520d55feffffff0251430f00000000001976a914ab0c0b2e98b1ab6dbf67d4750b0a56244948a87988ac005a6202000000001976a9143c82d7df364eb6c75be8c80df2b3eda8db57397088ac46430600" tx = Tx.parse_hex(raw_tx) self.assertEqual(tx.fee(), 140500)
def test_sig_hash_bip143(self): raw_tx = "0100000000010115e180dc28a2327e687facc33f10f2a20da717e5548406f7ae8b4c811072f8560100000000ffffffff0100b4f505000000001976a9141d7cd6c75c2e86f4cbf98eaed221b30bd9a0b92888ac02483045022100df7b7e5cda14ddf91290e02ea10786e03eb11ee36ec02dd862fe9a326bbcb7fd02203f5b4496b667e6e281cc654a2da9e4f08660c620a1051337fa8965f727eb19190121038262a6c6cec93c2d3ecd6c6072efea86d02ff8e3328bbd0242b20af3425990ac00000000" tx = Tx.parse_hex(raw_tx, network="testnet") want = int( "12bb9e0988736b8d1c3a180acd828b8a7eddae923a6a4bf0b4c14c40cd7327d1", 16 ) self.assertEqual(tx.sig_hash_legacy(0), want) tx = Tx.parse_hex(raw_tx, network="signet") self.assertEqual(tx.sig_hash_legacy(0), want)
def test_sig_hash(self): raw_tx = "0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600" tx = Tx.parse_hex(raw_tx) want = int( "27e0c5994dec7824e56dec6b2fcb342eb7cdb0d0957c2fce9882f715e85d81a6", 16 ) self.assertEqual(tx.sig_hash_legacy(0), want)
def test_serialize(self): raw_tx = "0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600" tx = Tx.parse_hex(raw_tx) self.assertEqual(tx.serialize().hex(), raw_tx) # simple test to show repr works (otherwise this would throw an error) str(tx)
def test_parse_segwit(self): raw_tx = "01000000000101c70c4ede5731f1b47a89d133be9244927fa12e15778ec78a7e071273c0c58a870400000000ffffffff02809698000000000017a9144f34d55c56f827169921df008e8dfdc23678fc1787d464da1f00000000220020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d0400473044022050a5a50e78e6f9c65b5d94c78f8e4b339848456ff7c2231702b4a37439e2a3bd02201569cbf1c672bbb1608d6e9feea28705d8d6e54aa51d9fa396469be6ffc83c2d0147304402200b69a83cc3e3e1694037ef639049b0ece00f15718a03e9038aa42ac9d1bd0ea50220780c510821cd5205e5d178e6277005f4dd61a7fcccd4f8fae9e2d2adc355e728016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae00000000" tx = Tx.parse_hex(raw_tx) self.assertTrue(tx.segwit) self.assertEqual(tx.version, 1) self.assertEqual(tx.tx_ins[0].prev_index, 4) self.assertEqual(tx.tx_outs[0].amount, 10000000) self.assertEqual(tx.locktime, 0)
def test_parse_outputs(self): raw_tx = "0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600" tx = Tx.parse_hex(raw_tx) self.assertEqual(len(tx.tx_outs), 2) want = 32454049 self.assertEqual(tx.tx_outs[0].amount, want) want = bytes.fromhex("1976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac") self.assertEqual(tx.tx_outs[0].script_pubkey.serialize(), want) want = 10011545 self.assertEqual(tx.tx_outs[1].amount, want) want = bytes.fromhex("1976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac") self.assertEqual(tx.tx_outs[1].script_pubkey.serialize(), want)
def test_parse_inputs(self): raw_tx = "0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600" tx = Tx.parse_hex(raw_tx) self.assertEqual(len(tx.tx_ins), 1) want = bytes.fromhex( "d1c789a9c60383bf715f3f6ad9d14b91fe55f3deb369fe5d9280cb1a01793f81" ) self.assertEqual(tx.tx_ins[0].prev_tx, want) self.assertEqual(tx.tx_ins[0].prev_index, 0) want = bytes.fromhex( "6b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278a" ) self.assertEqual(tx.tx_ins[0].script_sig.serialize(), want) self.assertEqual(tx.tx_ins[0].sequence, 0xFFFFFFFE)
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, )
def test_is_coinbase(self): raw_tx = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff5e03d71b07254d696e656420627920416e74506f6f6c20626a31312f4542312f4144362f43205914293101fabe6d6d678e2c8c34afc36896e7d9402824ed38e856676ee94bfdb0c6c4bcd8b2e5666a0400000000000000c7270000a5e00e00ffffffff01faf20b58000000001976a914338c84849423992471bffb1a54a8d9b1d69dc28a88ac00000000" tx = Tx.parse_hex(raw_tx) self.assertTrue(tx.is_coinbase())
def test_parse_tricky(self): raw_tx = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff2f0315230e0004ae03ca57043e3d1e1d0c8796bf579aef0c0000000000122f4e696e6a61506f6f6c2f5345475749542fffffffff038427a112000000001976a914876fbb82ec05caa6af7a3b5e5a983aae6c6cc6d688ac0000000000000000266a24aa21a9ed5c748e121c0fe146d973a4ac26fa4a68b0549d46ee22d25f50a5e46fe1b377ee00000000000000002952534b424c4f434b3acd16772ad61a3c5f00287480b720f6035d5e54c9efc71be94bb5e3727f10909000000000" tx_obj = Tx.parse_hex(raw_tx) self.assertEqual(tx_obj.serialize().hex(), raw_tx)