mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-07-17 18:43:07 +02:00
381 lines
18 KiB
Python
Executable File
381 lines
18 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (c) 2021-present The Bitcoin Core developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
"""Test generation and spending of P2TR addresses."""
|
|
|
|
import random
|
|
import uuid
|
|
|
|
from decimal import Decimal
|
|
from test_framework.address import output_key_to_p2tr
|
|
from test_framework.key import H_POINT, compute_xonly_pubkey
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.util import assert_equal
|
|
from test_framework.descriptors import descsum_create
|
|
from test_framework.extendedkey import ExtendedPrivateKey
|
|
from test_framework.script import (
|
|
CScript,
|
|
MAX_PUBKEYS_PER_MULTI_A,
|
|
OP_CHECKSIG,
|
|
OP_CHECKSIGADD,
|
|
OP_NUMEQUAL,
|
|
taproot_construct,
|
|
)
|
|
from test_framework.segwit_addr import encode_segwit_address
|
|
|
|
def key(hex_key):
|
|
"""Construct an x-only pubkey from its hex representation."""
|
|
return bytes.fromhex(hex_key)
|
|
|
|
def pk(hex_key):
|
|
"""Construct a script expression for taproot_construct for pk(hex_key)."""
|
|
return (None, CScript([bytes.fromhex(hex_key), OP_CHECKSIG]))
|
|
|
|
def multi_a(k, hex_keys, sort=False):
|
|
"""Construct a script expression for taproot_construct for a multi_a script."""
|
|
xkeys = [bytes.fromhex(hex_key) for hex_key in hex_keys]
|
|
if sort:
|
|
xkeys.sort()
|
|
ops = [xkeys[0], OP_CHECKSIG]
|
|
for i in range(1, len(hex_keys)):
|
|
ops += [xkeys[i], OP_CHECKSIGADD]
|
|
ops += [k, OP_NUMEQUAL]
|
|
return (None, CScript(ops))
|
|
|
|
def compute_taproot_address(pubkey, scripts):
|
|
"""Compute the address for a taproot output with given inner key and scripts."""
|
|
return output_key_to_p2tr(taproot_construct(pubkey, scripts).output_pubkey)
|
|
|
|
def compute_raw_taproot_address(pubkey):
|
|
return encode_segwit_address("bcrt", 1, pubkey)
|
|
|
|
class WalletTaprootTest(BitcoinTestFramework):
|
|
"""Test generation and spending of P2TR address outputs."""
|
|
|
|
def set_test_params(self):
|
|
self.num_nodes = 2
|
|
self.setup_clean_chain = True
|
|
self.extra_args = [['-keypool=100'], ['-keypool=100']]
|
|
|
|
def skip_test_if_missing_module(self):
|
|
self.skip_if_no_wallet()
|
|
|
|
def setup_network(self):
|
|
self.setup_nodes()
|
|
|
|
def init_wallet(self, *, node):
|
|
pass
|
|
|
|
@staticmethod
|
|
def make_desc(pattern, privmap, keys, pub_only = False):
|
|
pat = pattern.replace("$H", H_POINT)
|
|
for i in range(len(privmap)):
|
|
if privmap[i] and not pub_only:
|
|
pat = pat.replace("$%i" % (i + 1), keys[i]['xprv'])
|
|
else:
|
|
pat = pat.replace("$%i" % (i + 1), keys[i]['xpub'])
|
|
return descsum_create(pat)
|
|
|
|
@staticmethod
|
|
def make_addr(treefn, keys, i):
|
|
args = []
|
|
for j in range(len(keys)):
|
|
args.append(keys[j]['pubs'][i])
|
|
tree = treefn(*args)
|
|
if isinstance(tree, tuple):
|
|
return compute_taproot_address(*tree)
|
|
if isinstance(tree, bytes):
|
|
return compute_raw_taproot_address(tree)
|
|
assert False
|
|
|
|
def do_test_addr(self, comment, pattern, privmap, treefn, keys):
|
|
self.log.info("Testing %s address derivation" % comment)
|
|
|
|
# Create wallets
|
|
wallet_uuid = uuid.uuid4().hex
|
|
self.nodes[0].createwallet(wallet_name=f"privs_tr_enabled_{wallet_uuid}", blank=True)
|
|
self.nodes[0].createwallet(wallet_name=f"pubs_tr_enabled_{wallet_uuid}", blank=True, disable_private_keys=True)
|
|
self.nodes[0].createwallet(wallet_name=f"addr_gen_{wallet_uuid}", disable_private_keys=True, blank=True)
|
|
privs_tr_enabled = self.nodes[0].get_wallet_rpc(f"privs_tr_enabled_{wallet_uuid}")
|
|
pubs_tr_enabled = self.nodes[0].get_wallet_rpc(f"pubs_tr_enabled_{wallet_uuid}")
|
|
addr_gen = self.nodes[0].get_wallet_rpc(f"addr_gen_{wallet_uuid}")
|
|
|
|
desc = self.make_desc(pattern, privmap, keys, False)
|
|
desc_pub = self.make_desc(pattern, privmap, keys, True)
|
|
assert_equal(self.nodes[0].getdescriptorinfo(desc)['descriptor'], desc_pub)
|
|
result = addr_gen.importdescriptors([{"desc": desc_pub, "active": True, "timestamp": "now"}])
|
|
assert result[0]['success']
|
|
address_type = "bech32m" if "tr" in pattern else "bech32"
|
|
for i in range(4):
|
|
addr_g = addr_gen.getnewaddress(address_type=address_type)
|
|
if treefn is not None:
|
|
addr_r = self.make_addr(treefn, keys, i)
|
|
assert_equal(addr_g, addr_r)
|
|
desc_a = addr_gen.getaddressinfo(addr_g)['desc']
|
|
if desc.startswith("tr("):
|
|
assert desc_a.startswith("tr(")
|
|
rederive = self.nodes[1].deriveaddresses(desc_a)
|
|
assert_equal(len(rederive), 1)
|
|
assert_equal(rederive[0], addr_g)
|
|
|
|
# tr descriptors can be imported
|
|
result = privs_tr_enabled.importdescriptors([{"desc": desc, "timestamp": "now"}])
|
|
assert result[0]['success']
|
|
result = pubs_tr_enabled.importdescriptors([{"desc": desc_pub, "timestamp": "now"}])
|
|
assert result[0]["success"]
|
|
|
|
# Cleanup
|
|
privs_tr_enabled.unloadwallet()
|
|
pubs_tr_enabled.unloadwallet()
|
|
addr_gen.unloadwallet()
|
|
|
|
def do_test_sendtoaddress(self, comment, pattern, privmap, treefn, keys_pay, keys_change):
|
|
self.log.info("Testing %s through sendtoaddress" % comment)
|
|
|
|
# Create wallets
|
|
wallet_uuid = uuid.uuid4().hex
|
|
self.nodes[0].createwallet(wallet_name=f"rpc_online_{wallet_uuid}", blank=True)
|
|
rpc_online = self.nodes[0].get_wallet_rpc(f"rpc_online_{wallet_uuid}")
|
|
|
|
desc_pay = self.make_desc(pattern, privmap, keys_pay)
|
|
desc_change = self.make_desc(pattern, privmap, keys_change)
|
|
desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True)
|
|
desc_change_pub = self.make_desc(pattern, privmap, keys_change, True)
|
|
assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub)
|
|
assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub)
|
|
result = rpc_online.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}])
|
|
assert result[0]['success']
|
|
result = rpc_online.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}])
|
|
assert result[0]['success']
|
|
address_type = "bech32m" if "tr" in pattern else "bech32"
|
|
for i in range(4):
|
|
addr_g = rpc_online.getnewaddress(address_type=address_type)
|
|
if treefn is not None:
|
|
addr_r = self.make_addr(treefn, keys_pay, i)
|
|
assert_equal(addr_g, addr_r)
|
|
boring_balance = int(self.boring.getbalance() * 100000000)
|
|
to_amnt = random.randrange(1000000, boring_balance)
|
|
self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True)
|
|
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
|
|
test_balance = int(rpc_online.getbalance() * 100000000)
|
|
ret_amnt = random.randrange(100000, test_balance)
|
|
# Increase fee_rate to compensate for the wallet's inability to estimate fees for script path spends.
|
|
res = rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=Decimal(ret_amnt) / 100000000, subtractfeefromamount=True, fee_rate=200)
|
|
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
|
|
assert rpc_online.gettransaction(res)["confirmations"] > 0
|
|
|
|
# Cleanup
|
|
txid = rpc_online.sendall(recipients=[self.boring.getnewaddress()])["txid"]
|
|
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
|
|
assert rpc_online.gettransaction(txid)["confirmations"] > 0
|
|
rpc_online.unloadwallet()
|
|
|
|
def do_test_psbt(self, comment, pattern, privmap, treefn, keys_pay, keys_change):
|
|
self.log.info("Testing %s through PSBT" % comment)
|
|
|
|
# Create wallets
|
|
wallet_uuid = uuid.uuid4().hex
|
|
self.nodes[0].createwallet(wallet_name=f"psbt_online_{wallet_uuid}", disable_private_keys=True, blank=True)
|
|
self.nodes[1].createwallet(wallet_name=f"psbt_offline_{wallet_uuid}", blank=True)
|
|
self.nodes[1].createwallet(f"key_only_wallet_{wallet_uuid}", blank=True)
|
|
psbt_online = self.nodes[0].get_wallet_rpc(f"psbt_online_{wallet_uuid}")
|
|
psbt_offline = self.nodes[1].get_wallet_rpc(f"psbt_offline_{wallet_uuid}")
|
|
key_only_wallet = self.nodes[1].get_wallet_rpc(f"key_only_wallet_{wallet_uuid}")
|
|
|
|
desc_pay = self.make_desc(pattern, privmap, keys_pay, False)
|
|
desc_change = self.make_desc(pattern, privmap, keys_change, False)
|
|
desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True)
|
|
desc_change_pub = self.make_desc(pattern, privmap, keys_change, True)
|
|
assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub)
|
|
assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub)
|
|
result = psbt_online.importdescriptors([{"desc": desc_pay_pub, "active": True, "timestamp": "now"}])
|
|
assert result[0]['success']
|
|
result = psbt_online.importdescriptors([{"desc": desc_change_pub, "active": True, "timestamp": "now", "internal": True}])
|
|
assert result[0]['success']
|
|
result = psbt_offline.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}])
|
|
assert result[0]['success']
|
|
result = psbt_offline.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}])
|
|
assert result[0]['success']
|
|
for key in keys_pay + keys_change:
|
|
result = key_only_wallet.importdescriptors([{"desc": descsum_create(f"wpkh({key['xprv']}/*)"), "timestamp":"now"}])
|
|
assert result[0]["success"]
|
|
address_type = "bech32m" if "tr" in pattern else "bech32"
|
|
for i in range(4):
|
|
addr_g = psbt_online.getnewaddress(address_type=address_type)
|
|
if treefn is not None:
|
|
addr_r = self.make_addr(treefn, keys_pay, i)
|
|
assert_equal(addr_g, addr_r)
|
|
boring_balance = int(self.boring.getbalance() * 100000000)
|
|
to_amnt = random.randrange(1000000, boring_balance)
|
|
self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True)
|
|
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
|
|
test_balance = int(psbt_online.getbalance() * 100000000)
|
|
ret_amnt = random.randrange(100000, test_balance)
|
|
# Increase fee_rate to compensate for the wallet's inability to estimate fees for script path spends.
|
|
psbt = psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): Decimal(ret_amnt) / 100000000}], None, {"subtractFeeFromOutputs":[0], "fee_rate": 200, "change_type": address_type})['psbt']
|
|
res = psbt_offline.walletprocesspsbt(psbt=psbt, finalize=False)
|
|
for wallet in [psbt_offline, key_only_wallet]:
|
|
res = wallet.walletprocesspsbt(psbt=psbt, finalize=False)
|
|
|
|
decoded = wallet.decodepsbt(res["psbt"])
|
|
if pattern.startswith("tr("):
|
|
for psbtin in decoded["inputs"]:
|
|
assert "non_witness_utxo" not in psbtin
|
|
assert "witness_utxo" in psbtin
|
|
assert "taproot_internal_key" in psbtin
|
|
assert "taproot_bip32_derivs" in psbtin
|
|
assert "taproot_key_path_sig" in psbtin or "taproot_script_path_sigs" in psbtin
|
|
if "taproot_script_path_sigs" in psbtin:
|
|
assert "taproot_merkle_root" in psbtin
|
|
assert "taproot_scripts" in psbtin
|
|
|
|
rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']
|
|
res = self.nodes[0].testmempoolaccept([rawtx])
|
|
assert res[0]["allowed"]
|
|
|
|
txid = self.nodes[0].sendrawtransaction(rawtx)
|
|
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
|
|
assert psbt_online.gettransaction(txid)['confirmations'] > 0
|
|
|
|
# Cleanup
|
|
psbt = psbt_online.sendall(recipients=[self.boring.getnewaddress()], psbt=True)["psbt"]
|
|
res = psbt_offline.walletprocesspsbt(psbt=psbt, finalize=False)
|
|
rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']
|
|
txid = self.nodes[0].sendrawtransaction(rawtx)
|
|
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
|
|
assert psbt_online.gettransaction(txid)['confirmations'] > 0
|
|
psbt_online.unloadwallet()
|
|
psbt_offline.unloadwallet()
|
|
|
|
def do_test(self, comment, pattern, privmap, treefn):
|
|
nkeys = len(privmap)
|
|
keys = random.sample(self.keys, nkeys * 4)
|
|
self.do_test_addr(comment, pattern, privmap, treefn, keys[0:nkeys])
|
|
self.do_test_sendtoaddress(comment, pattern, privmap, treefn, keys[0:nkeys], keys[nkeys:2*nkeys])
|
|
self.do_test_psbt(comment, pattern, privmap, treefn, keys[2*nkeys:3*nkeys], keys[3*nkeys:4*nkeys])
|
|
|
|
def generate_test_keys(self):
|
|
xprvs = [ExtendedPrivateKey.generate() for _ in range(0, 13)]
|
|
return [{
|
|
"xprv": xprv.to_string(),
|
|
"xpub": xprv.pubkey().to_string(),
|
|
"pubs": [compute_xonly_pubkey(xprv.derive_path(f"m/{i}").key.get_bytes())[0].hex() for i in range(0, 4)]
|
|
} for xprv in xprvs]
|
|
|
|
def run_test(self):
|
|
self.keys = self.generate_test_keys()
|
|
self.nodes[0].createwallet(wallet_name="boring")
|
|
self.boring = self.nodes[0].get_wallet_rpc("boring")
|
|
|
|
self.log.info("Mining blocks...")
|
|
gen_addr = self.boring.getnewaddress()
|
|
self.generatetoaddress(self.nodes[0], 101, gen_addr, sync_fun=self.no_op)
|
|
|
|
self.do_test(
|
|
"tr(XPRV)",
|
|
"tr($1/*)",
|
|
[True],
|
|
lambda k1: (key(k1), [])
|
|
)
|
|
self.do_test(
|
|
"tr(H,XPRV)",
|
|
"tr($H,pk($1/*))",
|
|
[True],
|
|
lambda k1: (key(H_POINT), [pk(k1)])
|
|
)
|
|
self.do_test(
|
|
"wpkh(XPRV)",
|
|
"wpkh($1/*)",
|
|
[True],
|
|
None
|
|
)
|
|
self.do_test(
|
|
"tr(XPRV,{H,{H,XPUB}})",
|
|
"tr($1/*,{pk($H),{pk($H),pk($2/*)}})",
|
|
[True, False],
|
|
lambda k1, k2: (key(k1), [pk(H_POINT), [pk(H_POINT), pk(k2)]])
|
|
)
|
|
self.do_test(
|
|
"wsh(multi(1,XPRV,XPUB))",
|
|
"wsh(multi(1,$1/*,$2/*))",
|
|
[True, False],
|
|
None
|
|
)
|
|
self.do_test(
|
|
"tr(XPRV,{XPUB,XPUB})",
|
|
"tr($1/*,{pk($2/*),pk($2/*)})",
|
|
[True, False],
|
|
lambda k1, k2: (key(k1), [pk(k2), pk(k2)])
|
|
)
|
|
self.do_test(
|
|
"tr(XPRV,{{XPUB,H},{H,XPUB}})",
|
|
"tr($1/*,{{pk($2/*),pk($H)},{pk($H),pk($2/*)}})",
|
|
[True, False],
|
|
lambda k1, k2: (key(k1), [[pk(k2), pk(H_POINT)], [pk(H_POINT), pk(k2)]])
|
|
)
|
|
self.do_test(
|
|
"tr(XPUB,{{H,{H,XPUB}},{H,{H,{H,XPRV}}}})",
|
|
"tr($1/*,{{pk($H),{pk($H),pk($2/*)}},{pk($H),{pk($H),{pk($H),pk($3/*)}}}})",
|
|
[False, False, True],
|
|
lambda k1, k2, k3: (key(k1), [[pk(H_POINT), [pk(H_POINT), pk(k2)]], [pk(H_POINT), [pk(H_POINT), [pk(H_POINT), pk(k3)]]]])
|
|
)
|
|
self.do_test(
|
|
"tr(XPRV,{XPUB,{{XPUB,{H,H}},{{H,H},XPUB}}})",
|
|
"tr($1/*,{pk($2/*),{{pk($2/*),{pk($H),pk($H)}},{{pk($H),pk($H)},pk($2/*)}}})",
|
|
[True, False],
|
|
lambda k1, k2: (key(k1), [pk(k2), [[pk(k2), [pk(H_POINT), pk(H_POINT)]], [[pk(H_POINT), pk(H_POINT)], pk(k2)]]])
|
|
)
|
|
self.do_test(
|
|
"tr(H,multi_a(1,XPRV))",
|
|
"tr($H,multi_a(1,$1/*))",
|
|
[True],
|
|
lambda k1: (key(H_POINT), [multi_a(1, [k1])])
|
|
)
|
|
self.do_test(
|
|
"tr(H,sortedmulti_a(1,XPRV,XPUB))",
|
|
"tr($H,sortedmulti_a(1,$1/*,$2/*))",
|
|
[True, False],
|
|
lambda k1, k2: (key(H_POINT), [multi_a(1, [k1, k2], True)])
|
|
)
|
|
self.do_test(
|
|
"tr(H,{H,multi_a(1,XPUB,XPRV)})",
|
|
"tr($H,{pk($H),multi_a(1,$1/*,$2/*)})",
|
|
[False, True],
|
|
lambda k1, k2: (key(H_POINT), [pk(H_POINT), [multi_a(1, [k1, k2])]])
|
|
)
|
|
self.do_test(
|
|
"tr(H,sortedmulti_a(1,XPUB,XPRV,XPRV))",
|
|
"tr($H,sortedmulti_a(1,$1/*,$2/*,$3/*))",
|
|
[False, True, True],
|
|
lambda k1, k2, k3: (key(H_POINT), [multi_a(1, [k1, k2, k3], True)])
|
|
)
|
|
self.do_test(
|
|
"tr(H,multi_a(2,XPRV,XPUB,XPRV))",
|
|
"tr($H,multi_a(2,$1/*,$2/*,$3/*))",
|
|
[True, False, True],
|
|
lambda k1, k2, k3: (key(H_POINT), [multi_a(2, [k1, k2, k3])])
|
|
)
|
|
self.do_test(
|
|
"tr(XPUB,{{XPUB,{XPUB,sortedmulti_a(2,XPRV,XPUB,XPRV)}})",
|
|
"tr($2/*,{pk($2/*),{pk($2/*),sortedmulti_a(2,$1/*,$2/*,$3/*)}})",
|
|
[True, False, True],
|
|
lambda k1, k2, k3: (key(k2), [pk(k2), [pk(k2), multi_a(2, [k1, k2, k3], True)]])
|
|
)
|
|
rnd_pos = random.randrange(MAX_PUBKEYS_PER_MULTI_A)
|
|
self.do_test(
|
|
"tr(XPUB,multi_a(1,H...,XPRV,H...))",
|
|
"tr($2/*,multi_a(1" + (",$H" * rnd_pos) + ",$1/*" + (",$H" * (MAX_PUBKEYS_PER_MULTI_A - 1 - rnd_pos)) + "))",
|
|
[True, False],
|
|
lambda k1, k2: (key(k2), [multi_a(1, ([H_POINT] * rnd_pos) + [k1] + ([H_POINT] * (MAX_PUBKEYS_PER_MULTI_A - 1 - rnd_pos)))])
|
|
)
|
|
self.do_test(
|
|
"rawtr(XPRV)",
|
|
"rawtr($1/*)",
|
|
[True],
|
|
lambda k1: key(k1)
|
|
)
|
|
|
|
if __name__ == '__main__':
|
|
WalletTaprootTest(__file__).main()
|