def test_receive_1of2(self): # This test is not strictly neccesary, it just proves/shows how I generated the testnet address that received these coins # In the next test, I'll show to spend them # For full details, see test_create_p2sh_multisig in test_script.py # Insecure testing BIP39 mnemonics root_path = "m/45'/0/0/0" hdprivs = [ HDPrivateKey.from_mnemonic(seed_word * 12, network="testnet") for seed_word in ("action ", "agent ") ] # Validate the 0th receive address for m/45'/0 expected_spending_addr = "2ND4qfpdHyeXJboAUkKZqJsyiKyXvHRKhbi" pubkey_hexes = [ hdpriv.traverse(root_path).pub.sec().hex() for hdpriv in hdprivs ] redeem_script_to_use = RedeemScript.create_p2sh_multisig( quorum_m=1, pubkey_hexes=pubkey_hexes, sort_keys=True, ) self.assertEqual(expected_spending_addr, redeem_script_to_use.address(network="testnet"))
def test_nonstandard_redeemscript(self): hex_redeem_script = "4752210223136797cb0d7596cb5bd476102fe3aface2a06338e1afabffacf8c3cab4883c210385c865e61e275ba6fda4a3167180fc5a6b607150ff18797ee44737cd0d34507b52ae" stream = BytesIO(bytes.fromhex(hex_redeem_script)) redeem_script = RedeemScript.parse(stream) want = "36b865d5b9664193ea1db43d159edf9edf943802" self.assertEqual(redeem_script.hash160().hex(), want) want = "17a91436b865d5b9664193ea1db43d159edf9edf94380287" self.assertEqual(redeem_script.script_pubkey().serialize().hex(), want) want = "2MxEZNps15dAnGX5XaVwZWgoDvjvsDE5XSx" self.assertEqual(redeem_script.address(network="testnet"), want) self.assertEqual(redeem_script.address(network="signet"), want)
def test_standard_redeemscript(self): # This was tested against bitcoin core v0.22 # https://github.com/buidl-bitcoin/buidl-python/issues/123 redeem_script_hex = "522102be8d4de672b4d6149616962a7d193b702e608a1ed65aaa44f432ff9dd902252f21036ec4565fb304b0fc8e2cdd56e477816ab703e06d52f87279bf0cbdb9fa4941b221038fc14c8dd5a15828bd4fb0e206e443e3ac6e3e3782fbf6f0a1ff969a9ec8f28f53ae" rs_obj = RedeemScript.parse_hex(redeem_script_hex) self.assertEqual( "2NAaGqgaZicBUXuA2iZc6ssxLEsS4sZwdwz", rs_obj.address("testnet") ) self.assertEqual((2, 3), rs_obj.get_quorum()) self.assertTrue(rs_obj.is_p2sh_multisig()) pubkeys_wanted_hex = ( "02be8d4de672b4d6149616962a7d193b702e608a1ed65aaa44f432ff9dd902252f", "036ec4565fb304b0fc8e2cdd56e477816ab703e06d52f87279bf0cbdb9fa4941b2", "038fc14c8dd5a15828bd4fb0e206e443e3ac6e3e3782fbf6f0a1ff969a9ec8f28f", ) for cnt, pubkey_bytes in enumerate(rs_obj.signing_pubkeys()): self.assertEqual(pubkey_bytes.hex(), pubkeys_wanted_hex[cnt])
def sig_hash(self, input_index, hash_type): # get the relevant input tx_in = self.tx_ins[input_index] # get the script_pubkey of the input script_pubkey = tx_in.script_pubkey(network=self.network) # grab the RedeemScript if we have a p2sh if script_pubkey.is_p2sh(): # the last command of the ScriptSig is the raw RedeemScript raw_redeem_script = tx_in.script_sig.commands[-1] # convert to RedeemScript redeem_script = RedeemScript.convert(raw_redeem_script) else: redeem_script = None # grab the WitnessScript if we have a p2wsh if script_pubkey.is_p2wsh() or (redeem_script and redeem_script.is_p2wsh()): # the last item of the Witness is the raw WitnessScript raw_witness_script = tx_in.witness.items[-1] # convert to WitnessScript witness_script = WitnessScript.convert(raw_witness_script) else: witness_script = None # check to see if the ScriptPubKey or the RedeemScript is p2wpkh or p2wsh if (script_pubkey.is_p2wpkh() or (redeem_script and redeem_script.is_p2wpkh()) or script_pubkey.is_p2wsh() or (redeem_script and redeem_script.is_p2wsh())): return self.sig_hash_bip143( input_index, redeem_script=redeem_script, witness_script=witness_script, hash_type=hash_type, ) elif script_pubkey.is_p2tr(): if len(tx_in.witness) > 1: ext_flag = 1 else: ext_flag = 0 return self.sig_hash_bip341(input_index, ext_flag=ext_flag, hash_type=hash_type) else: return self.sig_hash_legacy(input_index, redeem_script, hash_type=hash_type)
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_create_p2sh_multisig(self): BASE_PATH = "m/45h/0" # Insecure testing BIP39 mnemonics hdpriv_root_1 = HDPrivateKey.from_mnemonic("action " * 12, network="testnet") hdpriv_root_2 = HDPrivateKey.from_mnemonic("agent " * 12, network="testnet") child_xpriv_1 = hdpriv_root_1.traverse(BASE_PATH) child_xpriv_2 = hdpriv_root_2.traverse(BASE_PATH) # xpubs from electrum: self.assertEqual( child_xpriv_1.xpub(), "tpubDBnspiLZfrq1V7j1iuMxGiPsuHyy6e4QBnADwRrbH89AcnsUEMfWiAYXmSbMuNFsrMdnbQRDGGSM1AFGL6zUWNVSmwRavoJzdQBbZKLgLgd", ) self.assertEqual( child_xpriv_2.xpub(), "tpubDAKJicb9Tkw34PFLEBUcbnH99twN3augmg7oYHHx9Aa9iodXmA4wtGEJr8h2XjJYqn2j1v5qHLjpWEe8aPihmC6jmsgomsuc9Zeh4ZushNk", ) # addresses from electrum expected_receive_addrs = [ "2ND4qfpdHyeXJboAUkKZqJsyiKyXvHRKhbi", "2N1T1HAC9TnNvhEDG4oDEuKNnmdsXs2HNwq", "2N7fdTu5JkQihTpo2mZ3QYudrfU2xMdgh3M", ] expected_change_addrs = [ "2MzQhXqN93igSKGW9CMvkpZ9TYowWgiNEF8", "2Msk2ckm2Ee4kJnzQuyQtpYDpMZrXf5XtKD", "2N5wXpJBKtAKSCiAZLwdh2sPwt5k2HBGtGC", ] # validate receive addrs match electrum for cnt, expected_receive_addr in enumerate(expected_receive_addrs): redeem_script = RedeemScript.create_p2sh_multisig( quorum_m=1, pubkey_hexes=[ child_xpriv_1.traverse(f"m/0/{cnt}").pub.sec().hex(), child_xpriv_2.traverse(f"m/0/{cnt}").pub.sec().hex(), ], # Electrum sorts child pubkeys lexicographically: sort_keys=True, ) derived_addr = redeem_script.address(network="testnet") self.assertEqual(derived_addr, expected_receive_addr) # validate change addrs match electrum for cnt, expected_change_addr in enumerate(expected_change_addrs): redeem_script = RedeemScript.create_p2sh_multisig( quorum_m=1, pubkey_hexes=[ child_xpriv_1.traverse(f"m/1/{cnt}").pub.sec().hex(), child_xpriv_2.traverse(f"m/1/{cnt}").pub.sec().hex(), ], # Electrum sorts lexicographically: sort_keys=True, ) derived_addr = redeem_script.address(network="testnet") self.assertEqual(derived_addr, expected_change_addr) # For recovery only misordered_pubkeys_addr = "2NFzScjC9jaMbo5ST4M1WVeeWgSaVT7xS1W" pubkey_hexes = [ child_xpriv_1.traverse("m/0/0").pub.sec().hex(), child_xpriv_2.traverse("m/0/0").pub.sec().hex(), ] misordered_redeem_script = RedeemScript.create_p2sh_multisig( quorum_m=1, # Note that the order here is intentional explicit and we're NOT sorting lexicographically (reverse=True) pubkey_hexes=sorted(pubkey_hexes, reverse=True), sort_keys=False, expected_addr=misordered_pubkeys_addr, expected_addr_network="testnet", ) self.assertEqual( misordered_redeem_script.address("testnet"), misordered_pubkeys_addr ) with self.assertRaises(ValueError): fake_addr = "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx" RedeemScript.create_p2sh_multisig( quorum_m=1, pubkey_hexes=pubkey_hexes, sort_keys=False, expected_addr=fake_addr, expected_addr_network="testnet", )
def test_full_wallet_functionality(self): """ Create a wallet, fund it, and spend it (validating on all devices first). Do this both with and without change. """ child_path = "m/0/0" base_path = "m/45'/0" xpubs = [ # Verified against electrum 2021-10 "tpubDAp5uHrhyCRnoQdMmF9xFU78bev55TKux39Viwn8VCnWywjTCBB3TFSA1i5i4scj8ZkD8RR37EZMPyqBwk4wD4VNpxyowgk2rQ7i8AdVauN", "tpubDBxHJMmnouRSes7kweA5wUp2Jm5F1xEPCG3fjYRnBSiQM7hjsp9gp8Wsag16A8yrpMgJkPX8iddXmwuxKaUnAWfdcGcuXbiATYeGXCKkEXK", "tpubDANTttKyukGYwXvhTmdCa6p1nHmsbFef2yRXb9b2JTvLGoekYk6ZG9oHXGHYJjTn5g3DijvQrZ4rH6EosYqpVngKiUx2jvWdPmq6ZSDr3ZE", ] xfps = ["9a6a2580", "8c7e2500", "0586b1ce"] pubkey_records = [] for cnt, seed_word in enumerate(("bacon", "flag", "gas")): seed_phrase = f"{seed_word} " * 24 hd_obj = HDPrivateKey.from_mnemonic(seed_phrase, network="testnet") # takes the form xfp, child_xpub, base_path pubkey_record = [ hd_obj.fingerprint().hex(), hd_obj.traverse(base_path).xpub(), base_path, ] pubkey_records.append(pubkey_record) self.assertEqual(pubkey_record, [xfps[cnt], xpubs[cnt], base_path]) pubkey_hexes = [ HDPublicKey.parse(tpub).traverse(child_path).sec().hex() for tpub in xpubs ] rs_obj = RedeemScript.create_p2sh_multisig(quorum_m=2, pubkey_hexes=pubkey_hexes, sort_keys=True) deposit_address = rs_obj.address(network="testnet") # We funded this testnet address in 2021-10: self.assertEqual(deposit_address, "2MzLzwPgo6ZTyPzkguNWKfFHgCXdZSJu9tL") input_dicts = [{ "quorum_m": 2, "path_dict": { # xfp: root_path "9a6a2580": "m/45'/0/0/0", "8c7e2500": "m/45'/0/0/0", "0586b1ce": "m/45'/0/0/0", }, "prev_tx_dict": { # This was the funding transaction send to 2MzLzwPgo6ZTyPzkguNWKfFHgCXdZSJu9tL "hex": "02000000000101045448b2faafc53a7586bd6a746a598d9a1cfc3dd548d8f8afd12c11c53c16740000000000feffffff02809a4b000000000017a914372becc78d20f8acb94059fa8d1e3219b67c1d9387a08601000000000017a9144de07b9788bc64143292f9610443a7f81cc5fc308702473044022059f2036e5b6da03e592863664082f957472dbb89330895d23ac66f94e3819f63022050d31d401cd86fe797c31ba7fb2a2cf9e12356bba8388897f4f522bc4999224f0121029c71554e111bb41463ad288b4808effef8136755da80d42aa2bcea12ed8a99bbba0e2000", "hash_hex": "838504ad934d97915d9ab58b0ece6900ed123abf3a51936563ba9d67b0e41fa4", "output_idx": 1, "output_sats": 100_000, }, }]
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, )