Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) 2021-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.5"""Test generation and spending of P2TR addresses."""7import random8import uuid10from decimal import Decimal11from test_framework.address import output_key_to_p2tr12from test_framework.key import H_POINT, compute_xonly_pubkey13from test_framework.test_framework import BitcoinTestFramework14from test_framework.util import assert_equal15from test_framework.descriptors import descsum_create16from test_framework.extendedkey import ExtendedPrivateKey17from test_framework.script import (18CScript,19MAX_PUBKEYS_PER_MULTI_A,20OP_CHECKSIG,21OP_CHECKSIGADD,22OP_NUMEQUAL,23taproot_construct,24)25from test_framework.segwit_addr import encode_segwit_address27def key(hex_key):28"""Construct an x-only pubkey from its hex representation."""29return bytes.fromhex(hex_key)31def pk(hex_key):32"""Construct a script expression for taproot_construct for pk(hex_key)."""33return (None, CScript([bytes.fromhex(hex_key), OP_CHECKSIG]))35def multi_a(k, hex_keys, sort=False):36"""Construct a script expression for taproot_construct for a multi_a script."""37xkeys = [bytes.fromhex(hex_key) for hex_key in hex_keys]38if sort:39xkeys.sort()40ops = [xkeys[0], OP_CHECKSIG]41for i in range(1, len(hex_keys)):42ops += [xkeys[i], OP_CHECKSIGADD]43ops += [k, OP_NUMEQUAL]44return (None, CScript(ops))46def compute_taproot_address(pubkey, scripts):47"""Compute the address for a taproot output with given inner key and scripts."""48return output_key_to_p2tr(taproot_construct(pubkey, scripts).output_pubkey)50def compute_raw_taproot_address(pubkey):51return encode_segwit_address("bcrt", 1, pubkey)53class WalletTaprootTest(BitcoinTestFramework):54"""Test generation and spending of P2TR address outputs."""56def set_test_params(self):57self.num_nodes = 258self.setup_clean_chain = True59self.extra_args = [['-keypool=100'], ['-keypool=100']]61def skip_test_if_missing_module(self):62self.skip_if_no_wallet()64def setup_network(self):65self.setup_nodes()67def init_wallet(self, *, node):68pass70@staticmethod71def make_desc(pattern, privmap, keys, pub_only = False):72pat = pattern.replace("$H", H_POINT)73for i in range(len(privmap)):74if privmap[i] and not pub_only:75pat = pat.replace("$%i" % (i + 1), keys[i]['xprv'])76else:77pat = pat.replace("$%i" % (i + 1), keys[i]['xpub'])78return descsum_create(pat)80@staticmethod81def make_addr(treefn, keys, i):82args = []83for j in range(len(keys)):84args.append(keys[j]['pubs'][i])85tree = treefn(*args)86if isinstance(tree, tuple):87return compute_taproot_address(*tree)88if isinstance(tree, bytes):89return compute_raw_taproot_address(tree)90assert False92def do_test_addr(self, comment, pattern, privmap, treefn, keys):93self.log.info("Testing %s address derivation" % comment)95# Create wallets96wallet_uuid = uuid.uuid4().hex97self.nodes[0].createwallet(wallet_name=f"privs_tr_enabled_{wallet_uuid}", blank=True)98self.nodes[0].createwallet(wallet_name=f"pubs_tr_enabled_{wallet_uuid}", blank=True, disable_private_keys=True)99self.nodes[0].createwallet(wallet_name=f"addr_gen_{wallet_uuid}", disable_private_keys=True, blank=True)100privs_tr_enabled = self.nodes[0].get_wallet_rpc(f"privs_tr_enabled_{wallet_uuid}")101pubs_tr_enabled = self.nodes[0].get_wallet_rpc(f"pubs_tr_enabled_{wallet_uuid}")102addr_gen = self.nodes[0].get_wallet_rpc(f"addr_gen_{wallet_uuid}")104desc = self.make_desc(pattern, privmap, keys, False)105desc_pub = self.make_desc(pattern, privmap, keys, True)106assert_equal(self.nodes[0].getdescriptorinfo(desc)['descriptor'], desc_pub)107result = addr_gen.importdescriptors([{"desc": desc_pub, "active": True, "timestamp": "now"}])108assert result[0]['success']109address_type = "bech32m" if "tr" in pattern else "bech32"110for i in range(4):111addr_g = addr_gen.getnewaddress(address_type=address_type)112if treefn is not None:113addr_r = self.make_addr(treefn, keys, i)114assert_equal(addr_g, addr_r)115desc_a = addr_gen.getaddressinfo(addr_g)['desc']116if desc.startswith("tr("):117assert desc_a.startswith("tr(")118rederive = self.nodes[1].deriveaddresses(desc_a)119assert_equal(len(rederive), 1)120assert_equal(rederive[0], addr_g)122# tr descriptors can be imported123result = privs_tr_enabled.importdescriptors([{"desc": desc, "timestamp": "now"}])124assert result[0]['success']125result = pubs_tr_enabled.importdescriptors([{"desc": desc_pub, "timestamp": "now"}])126assert result[0]["success"]128# Cleanup129privs_tr_enabled.unloadwallet()130pubs_tr_enabled.unloadwallet()131addr_gen.unloadwallet()133def do_test_sendtoaddress(self, comment, pattern, privmap, treefn, keys_pay, keys_change):134self.log.info("Testing %s through sendtoaddress" % comment)136# Create wallets137wallet_uuid = uuid.uuid4().hex138self.nodes[0].createwallet(wallet_name=f"rpc_online_{wallet_uuid}", blank=True)139rpc_online = self.nodes[0].get_wallet_rpc(f"rpc_online_{wallet_uuid}")141desc_pay = self.make_desc(pattern, privmap, keys_pay)142desc_change = self.make_desc(pattern, privmap, keys_change)143desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True)144desc_change_pub = self.make_desc(pattern, privmap, keys_change, True)145assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub)146assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub)147result = rpc_online.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}])148assert result[0]['success']149result = rpc_online.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}])150assert result[0]['success']151address_type = "bech32m" if "tr" in pattern else "bech32"152for i in range(4):153addr_g = rpc_online.getnewaddress(address_type=address_type)154if treefn is not None:155addr_r = self.make_addr(treefn, keys_pay, i)156assert_equal(addr_g, addr_r)157boring_balance = int(self.boring.getbalance() * 100000000)158to_amnt = random.randrange(1000000, boring_balance)159self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True)160self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)161test_balance = int(rpc_online.getbalance() * 100000000)162ret_amnt = random.randrange(100000, test_balance)163# Increase fee_rate to compensate for the wallet's inability to estimate fees for script path spends.164res = rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=Decimal(ret_amnt) / 100000000, subtractfeefromamount=True, fee_rate=200)165self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)166assert rpc_online.gettransaction(res)["confirmations"] > 0168# Match the framework fallbackfee; otherwise the underestimated taproot169# script-path spend size can produce an effective feerate below min relay.170txid = rpc_online.sendall(recipients=[self.boring.getnewaddress()], fee_rate=20)["txid"]171self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)172assert rpc_online.gettransaction(txid)["confirmations"] > 0173rpc_online.unloadwallet()175def do_test_psbt(self, comment, pattern, privmap, treefn, keys_pay, keys_change):176self.log.info("Testing %s through PSBT" % comment)178# Create wallets179wallet_uuid = uuid.uuid4().hex180self.nodes[0].createwallet(wallet_name=f"psbt_online_{wallet_uuid}", disable_private_keys=True, blank=True)181self.nodes[1].createwallet(wallet_name=f"psbt_offline_{wallet_uuid}", blank=True)182self.nodes[1].createwallet(f"key_only_wallet_{wallet_uuid}", blank=True)183psbt_online = self.nodes[0].get_wallet_rpc(f"psbt_online_{wallet_uuid}")184psbt_offline = self.nodes[1].get_wallet_rpc(f"psbt_offline_{wallet_uuid}")185key_only_wallet = self.nodes[1].get_wallet_rpc(f"key_only_wallet_{wallet_uuid}")187desc_pay = self.make_desc(pattern, privmap, keys_pay, False)188desc_change = self.make_desc(pattern, privmap, keys_change, False)189desc_pay_pub = self.make_desc(pattern, privmap, keys_pay, True)190desc_change_pub = self.make_desc(pattern, privmap, keys_change, True)191assert_equal(self.nodes[0].getdescriptorinfo(desc_pay)['descriptor'], desc_pay_pub)192assert_equal(self.nodes[0].getdescriptorinfo(desc_change)['descriptor'], desc_change_pub)193result = psbt_online.importdescriptors([{"desc": desc_pay_pub, "active": True, "timestamp": "now"}])194assert result[0]['success']195result = psbt_online.importdescriptors([{"desc": desc_change_pub, "active": True, "timestamp": "now", "internal": True}])196assert result[0]['success']197result = psbt_offline.importdescriptors([{"desc": desc_pay, "active": True, "timestamp": "now"}])198assert result[0]['success']199result = psbt_offline.importdescriptors([{"desc": desc_change, "active": True, "timestamp": "now", "internal": True}])200assert result[0]['success']201for key in keys_pay + keys_change:202result = key_only_wallet.importdescriptors([{"desc": descsum_create(f"wpkh({key['xprv']}/*)"), "timestamp":"now"}])203assert result[0]["success"]204address_type = "bech32m" if "tr" in pattern else "bech32"205for i in range(4):206addr_g = psbt_online.getnewaddress(address_type=address_type)207if treefn is not None:208addr_r = self.make_addr(treefn, keys_pay, i)209assert_equal(addr_g, addr_r)210boring_balance = int(self.boring.getbalance() * 100000000)211to_amnt = random.randrange(1000000, boring_balance)212self.boring.sendtoaddress(address=addr_g, amount=Decimal(to_amnt) / 100000000, subtractfeefromamount=True)213self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)214test_balance = int(psbt_online.getbalance() * 100000000)215ret_amnt = random.randrange(100000, test_balance)216# Increase fee_rate to compensate for the wallet's inability to estimate fees for script path spends.217psbt = psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): Decimal(ret_amnt) / 100000000}], None, {"subtractFeeFromOutputs":[0], "fee_rate": 200, "change_type": address_type})['psbt']218res = psbt_offline.walletprocesspsbt(psbt=psbt, finalize=False)219for wallet in [psbt_offline, key_only_wallet]:220res = wallet.walletprocesspsbt(psbt=psbt, finalize=False)222decoded = wallet.decodepsbt(res["psbt"])223if pattern.startswith("tr("):224for psbtin in decoded["inputs"]:225assert "non_witness_utxo" not in psbtin226assert "witness_utxo" in psbtin227assert "taproot_internal_key" in psbtin228assert "taproot_bip32_derivs" in psbtin229assert "taproot_key_path_sig" in psbtin or "taproot_script_path_sigs" in psbtin230if "taproot_script_path_sigs" in psbtin:231assert "taproot_merkle_root" in psbtin232assert "taproot_scripts" in psbtin234rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']235res = self.nodes[0].testmempoolaccept([rawtx])236assert res[0]["allowed"]238txid = self.nodes[0].sendrawtransaction(rawtx)239self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)240assert psbt_online.gettransaction(txid)['confirmations'] > 0242# Match the framework fallbackfee; otherwise the underestimated taproot243# script-path spend size can produce an effective feerate below min relay.244psbt = psbt_online.sendall(recipients=[self.boring.getnewaddress()], psbt=True, fee_rate=20)["psbt"]245res = psbt_offline.walletprocesspsbt(psbt=psbt, finalize=False)246rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']247txid = self.nodes[0].sendrawtransaction(rawtx)248self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)249assert psbt_online.gettransaction(txid)['confirmations'] > 0250psbt_online.unloadwallet()
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.