Esempio n. 1
0
    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"))
Esempio n. 2
0
 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)
Esempio n. 3
0
 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])
Esempio n. 4
0
 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)
Esempio n. 5
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)
Esempio n. 6
0
    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",
            )
Esempio n. 7
0
    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,
            },
        }]
Esempio n. 8
0
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,
    )