mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-03-24 22:45:41 +01:00
Merge bitcoin/bitcoin#24043: Add (sorted)multi_a descriptor for k-of-n multisig inside tr
4828d53eccAdd (sorted)multi_a descriptors to doc/descriptors.md (Pieter Wuille)b5f33ac1f8Simplify wallet_taproot.py functional test (Pieter Wuille)eb0667ea96Add tests for (sorted)multi_a derivation/signing (Pieter Wuille)c17c6aa08dAdd signing support for (sorted)multi_a scripts (Pieter Wuille)3eed6fca57Add multi_a descriptor inference (Pieter Wuille)79728c4a3dAdd (sorted)multi_a descriptor and script derivation (Pieter Wuille)25e95f9ff8Merge/generalize IsValidMultisigKeyCount/GetMultisigKeyCount (Pieter Wuille) Pull request description: This adds a new `multi_a(k,key_1,key_2,...,key_n)` (and corresponding `sortedmulti_a`) descriptor for k-of-n policies inside `tr()`. Semantically it is very similar to the existing `multi()` descriptor, but with the following changes: * The corresponding script is `<key1> OP_CHECKSIG <key2> OP_CHECKSIGADD <key3> OP_CHECKSIGADD ... <key_n> OP_CHECKSIGADD <k> OP_NUMEQUAL`, rather than the traditional `OP_CHECKMULTISIG`-based script, making it usable inside the `tr()` descriptor. * The keys can optionally be specified in x-only notation. * Both the number of keys and the threshold can be as high as 999; this is the limit due to the consensus stacksize=1000 limit I expect that this functionality will later be replaced with a miniscript-based implementation, but I don't think it's necessary to wait for that. Limitations: * The wallet code will for not estimate witness size incorrectly for script path spends, which may result in a (dramatic) fee underpayment with large multi_a scripts. * The multi_a script construction is (slightly) suboptimal for n-of-n (where a `<key1> OP_CHECKSIGVERIFY ... <key_n-1> OP_CHECKSIGVERIFY <key_n> OP_CHECKSIG` would be better). Such a construction is not included here. ACKs for top commit: achow101: ACK4828d53eccgruve-p: ACK4828d53eccsanket1729: code review ACK4828d53eccdarosior: Code review ACK4828d53eccTree-SHA512: 5dcd434b79585f0ff830f7d501d27df5e346f5749f47a3109ec309ebf2cbbad0e1da541eec654026d911ab67fd7cf7793fab0f765628d68d81b96ef2a4d234ce
This commit is contained in:
@@ -27,6 +27,7 @@ from .messages import (
|
||||
from .ripemd160 import ripemd160
|
||||
|
||||
MAX_SCRIPT_ELEMENT_SIZE = 520
|
||||
MAX_PUBKEYS_PER_MULTI_A = 999
|
||||
LOCKTIME_THRESHOLD = 500000000
|
||||
ANNEX_TAG = 0x50
|
||||
|
||||
|
||||
@@ -12,8 +12,11 @@ from test_framework.util import assert_equal
|
||||
from test_framework.descriptors import descsum_create
|
||||
from test_framework.script import (
|
||||
CScript,
|
||||
MAX_PUBKEYS_PER_MULTI_A,
|
||||
OP_1,
|
||||
OP_CHECKSIG,
|
||||
OP_CHECKSIGADD,
|
||||
OP_NUMEQUAL,
|
||||
taproot_construct,
|
||||
)
|
||||
from test_framework.segwit_addr import encode_segwit_address
|
||||
@@ -167,6 +170,17 @@ 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."""
|
||||
tap = taproot_construct(pubkey, scripts)
|
||||
@@ -275,7 +289,8 @@ class WalletTaprootTest(BitcoinTestFramework):
|
||||
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
|
||||
test_balance = int(self.rpc_online.getbalance() * 100000000)
|
||||
ret_amnt = random.randrange(100000, test_balance)
|
||||
res = self.rpc_online.sendtoaddress(address=self.boring.getnewaddress(), amount=Decimal(ret_amnt) / 100000000, subtractfeefromamount=True)
|
||||
# Increase fee_rate to compensate for the wallet's inability to estimate fees for script path spends.
|
||||
res = self.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(self.rpc_online.gettransaction(res)["confirmations"] > 0)
|
||||
|
||||
@@ -306,7 +321,8 @@ class WalletTaprootTest(BitcoinTestFramework):
|
||||
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
|
||||
test_balance = int(self.psbt_online.getbalance() * 100000000)
|
||||
ret_amnt = random.randrange(100000, test_balance)
|
||||
psbt = self.psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): Decimal(ret_amnt) / 100000000}], None, {"subtractFeeFromOutputs":[0]})['psbt']
|
||||
# Increase fee_rate to compensate for the wallet's inability to estimate fees for script path spends.
|
||||
psbt = self.psbt_online.walletcreatefundedpsbt([], [{self.boring.getnewaddress(): Decimal(ret_amnt) / 100000000}], None, {"subtractFeeFromOutputs":[0], "fee_rate": 200})['psbt']
|
||||
res = self.psbt_offline.walletprocesspsbt(psbt)
|
||||
assert(res['complete'])
|
||||
rawtx = self.nodes[0].finalizepsbt(res['psbt'])['hex']
|
||||
@@ -314,7 +330,8 @@ class WalletTaprootTest(BitcoinTestFramework):
|
||||
self.generatetoaddress(self.nodes[0], 1, self.boring.getnewaddress(), sync_fun=self.no_op)
|
||||
assert(self.psbt_online.gettransaction(txid)['confirmations'] > 0)
|
||||
|
||||
def do_test(self, comment, pattern, privmap, treefn, nkeys):
|
||||
def do_test(self, comment, pattern, privmap, treefn):
|
||||
nkeys = len(privmap)
|
||||
keys = self.rand_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])
|
||||
@@ -349,64 +366,98 @@ class WalletTaprootTest(BitcoinTestFramework):
|
||||
"tr(XPRV)",
|
||||
"tr($1/*)",
|
||||
[True],
|
||||
lambda k1: (key(k1), []),
|
||||
1
|
||||
lambda k1: (key(k1), [])
|
||||
)
|
||||
self.do_test(
|
||||
"tr(H,XPRV)",
|
||||
"tr($H,pk($1/*))",
|
||||
[True],
|
||||
lambda k1: (key(H_POINT), [pk(k1)]),
|
||||
1
|
||||
lambda k1: (key(H_POINT), [pk(k1)])
|
||||
)
|
||||
self.do_test(
|
||||
"wpkh(XPRV)",
|
||||
"wpkh($1/*)",
|
||||
[True],
|
||||
None,
|
||||
1
|
||||
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)]]),
|
||||
2
|
||||
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,
|
||||
2
|
||||
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)]),
|
||||
2
|
||||
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)]]),
|
||||
2
|
||||
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)]]]]),
|
||||
3
|
||||
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)]]]),
|
||||
2
|
||||
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,multi_a(1,XPUB,XPRV))",
|
||||
"tr($H,multi_a(1,$1/*,$2/*))",
|
||||
[False, True],
|
||||
lambda k1, k2: (key(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.log.info("Sending everything back...")
|
||||
|
||||
Reference in New Issue
Block a user