mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-09-11 21:20:39 +02:00
Integrate MemPoolFeeRateEstimator into FeeRateEstimatorManager. When both estimators succeed, select the lower of the block policy and mempool estimates. When either estimator fails, return its error instead of falling back to the block policy estimate: if the mempool estimator cannot produce an estimate, the combined estimate fails. Callers that want a block-policy-only estimate can request it explicitly via fee_rate_estimator option. estimatesmartfee now emits the estimator field only for successful manager-selected estimates. Add a test that ensures estimatesmartfee returns the mempool fee rate estimate when it is lower than the block policy estimate, and can request the mempool policy estimator explicitly Two wallet functional tests also need adjusting. When the mempool is too sparse to fill its percentile buckets, MemPoolFeeRateEstimator returns a relayable floor of max(min relay fee, mempool min fee), so in regtest getFeeRateEstimate now returns the min relay fee where the wallet previously had no estimate and fell back to a higher rate: - wallet_taproot.py: the cleanup sendall used automatic fee estimation. GetMinimumFeeRate previously fell back to the wallet fallback fee (fallbackfee, 20 sat/vB in the test framework); it now uses the min relay fee floor. At that lower feerate the wallet's underestimate of the taproot script-path witness size drops the effective feerate below min relay, so the transaction is rejected. Pin fee_rate=20 to match the framework fallbackfee. - wallet_bumpfee.py: GetDiscardRate() previously fell back to the wallet discard rate (-discardfee); it now takes the minimum of that and the estimate, so the min relay fee floor collapses the discard rate down to the dust relay feerate. The lower discard rate reduces the cost of change, so the ~614 sat leftover change in test_dust_to_fee is now retained instead of being dropped to fee. Rework the test to leave a sub-dust (20/270 sat) change that is dropped regardless of the discard rate. Co-authored-by: willcl-ark <will@256k1.dev>
383 lines
18 KiB
Python
Executable File
383 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
|
|
|
|
# Match the framework fallbackfee; otherwise the underestimated taproot
|
|
# script-path spend size can produce an effective feerate below min relay.
|
|
txid = rpc_online.sendall(recipients=[self.boring.getnewaddress()], fee_rate=20)["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
|
|
|
|
# Match the framework fallbackfee; otherwise the underestimated taproot
|
|
# script-path spend size can produce an effective feerate below min relay.
|
|
psbt = psbt_online.sendall(recipients=[self.boring.getnewaddress()], psbt=True, fee_rate=20)["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()
|