Skip to content

Commit 313f112

Browse files
committed
test: Test MuSig2 in the wallet
1 parent c089d3e commit 313f112

File tree

2 files changed

+198
-0
lines changed

2 files changed

+198
-0
lines changed

test/functional/test_runner.py

+1
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@
387387
'mempool_datacarrier.py',
388388
'feature_coinstatsindex.py',
389389
'wallet_orphanedreward.py',
390+
'wallet_musig.py --descriptors',
390391
'wallet_timelock.py',
391392
'p2p_permissions.py',
392393
'feature_blocksdir.py',

test/functional/wallet_musig.py

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2024 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
import re
7+
8+
from test_framework.descriptors import descsum_create
9+
from test_framework.key import H_POINT
10+
from test_framework.test_framework import BitcoinTestFramework
11+
from test_framework.util import assert_equal
12+
13+
PRIVKEY_RE = re.compile(r"^tr\((.+?)/.+\)#.{8}$")
14+
PUBKEY_RE = re.compile(r"^tr\((\[.+?\].+?)/.+\)#.{8}$")
15+
ORIGIN_PATH_RE = re.compile(r"^\[\w{8}(/.*)\].*$")
16+
MULTIPATH_RE = re.compile(r"(.*?)<(\d+);(\d+)>")
17+
18+
19+
class WalletMuSigTest(BitcoinTestFramework):
20+
WALLET_NUM = 0
21+
def add_options(self, parser):
22+
self.add_wallet_options(parser, legacy=False)
23+
24+
def set_test_params(self):
25+
self.num_nodes = 1
26+
27+
def skip_test_if_missing_module(self):
28+
self.skip_if_no_wallet()
29+
30+
def do_test(self, comment, pattern):
31+
self.log.info(f"Testing {comment}")
32+
def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
33+
has_int = "<" in pattern and ">" in pattern
34+
35+
wallets = []
36+
keys = []
37+
38+
pat = pattern.replace("$H", H_POINT)
39+
40+
# Figure out how many wallets are needed and create them
41+
exp_key_leaf = 0
42+
for i in range(10):
43+
if f"${i}" in pat:
44+
exp_key_leaf += pat.count(f"${i}")
45+
wallet_name = f"musig_{self.WALLET_NUM}"
46+
self.WALLET_NUM += 1
47+
self.nodes[0].createwallet(wallet_name)
48+
wallet = self.nodes[0].get_wallet_rpc(wallet_name)
49+
wallets.append(wallet)
50+
51+
for priv_desc in wallet.listdescriptors(True)["descriptors"]:
52+
desc = priv_desc["desc"]
53+
if not desc.startswith("tr("):
54+
continue
55+
privkey = PRIVKEY_RE.search(desc).group(1)
56+
break
57+
for pub_desc in wallet.listdescriptors()["descriptors"]:
58+
desc = pub_desc["desc"]
59+
if not desc.startswith("tr("):
60+
continue
61+
pubkey = PUBKEY_RE.search(desc).group(1)
62+
# Since the pubkey is derived from the private key that we have, we need
63+
# to extract and insert the origin path from the pubkey as well.
64+
privkey += ORIGIN_PATH_RE.search(pubkey).group(1)
65+
break
66+
keys.append((privkey, pubkey))
67+
68+
# Construct and import each wallet's musig descriptor
69+
for i, wallet in enumerate(wallets):
70+
desc = pat
71+
import_descs = []
72+
for j, (priv, pub) in enumerate(keys):
73+
if j == i:
74+
desc = desc.replace(f"${i}", priv)
75+
else:
76+
desc = desc.replace(f"${j}", pub)
77+
78+
import_descs.append({
79+
"desc": descsum_create(desc),
80+
"active": True,
81+
"timestamp": "now",
82+
})
83+
84+
res = wallet.importdescriptors(import_descs)
85+
for r in res:
86+
assert_equal(r["success"], True)
87+
88+
# Check that the wallets agree on the same musig address
89+
addr = None
90+
change_addr = None
91+
for wallet in wallets:
92+
if addr is None:
93+
addr = wallet.getnewaddress(address_type="bech32m")
94+
else:
95+
assert_equal(addr, wallet.getnewaddress(address_type="bech32m"))
96+
if has_int:
97+
if change_addr is None:
98+
change_addr = wallet.getrawchangeaddress(address_type="bech32m")
99+
else:
100+
assert_equal(change_addr, wallet.getrawchangeaddress(address_type="bech32m"))
101+
102+
# Fund that address
103+
def_wallet.sendtoaddress(addr, 10)
104+
self.generate(self.nodes[0], 1)
105+
106+
# Spend that UTXO
107+
utxo = wallets[0].listunspent()[0]
108+
psbt = wallets[0].send(outputs=[{def_wallet.getnewaddress(): 5}], inputs=[utxo], change_type="bech32m")["psbt"]
109+
110+
dec_psbt = self.nodes[0].decodepsbt(psbt)
111+
assert_equal(len(dec_psbt["inputs"]), 1)
112+
assert_equal(len(dec_psbt["inputs"][0]["musig2_participant_pubkeys"]), pattern.count("musig("))
113+
114+
# Retrieve all participant pubkeys
115+
part_pks = set()
116+
for agg in dec_psbt["inputs"][0]["musig2_participant_pubkeys"]:
117+
for part_pub in agg["participant_pubkeys"]:
118+
part_pks.add(part_pub[2:])
119+
# Check that there are as many participants as we expected
120+
assert_equal(len(part_pks), len(keys))
121+
# Check that each participant has a derivation path
122+
for deriv_path in dec_psbt["inputs"][0]["taproot_bip32_derivs"]:
123+
if deriv_path["pubkey"] in part_pks:
124+
part_pks.remove(deriv_path["pubkey"])
125+
assert_equal(len(part_pks), 0)
126+
127+
# Add pubnonces
128+
nonce_psbts = []
129+
for wallet in wallets:
130+
proc = wallet.walletprocesspsbt(psbt)
131+
assert_equal(proc["complete"], False)
132+
nonce_psbts.append(proc["psbt"])
133+
134+
comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)
135+
136+
dec_psbt = self.nodes[0].decodepsbt(comb_nonce_psbt)
137+
assert_equal(len(dec_psbt["inputs"][0]["musig2_pubnonces"]), exp_key_leaf)
138+
for pn in dec_psbt["inputs"][0]["musig2_pubnonces"]:
139+
pubkey = pn["aggregate_pubkey"][2:]
140+
if pubkey in dec_psbt["inputs"][0]["witness_utxo"]["scriptPubKey"]["hex"]:
141+
continue
142+
elif "taproot_internal_key" in dec_psbt["inputs"][0] and pubkey in dec_psbt["inputs"][0]["taproot_internal_key"]:
143+
continue
144+
elif "taproot_scripts" in dec_psbt["inputs"][0]:
145+
for leaf_scripts in dec_psbt["inputs"][0]["taproot_scripts"]:
146+
if pubkey in leaf_scripts["script"]:
147+
break
148+
else:
149+
assert False, "Aggregate pubkey for pubnonce not seen as output key, internal key, or in any scripts"
150+
else:
151+
assert False, "Aggregate pubkey for pubnonce not seen as output key or internal key"
152+
153+
# Add partial sigs
154+
psig_psbts = []
155+
for wallet in wallets:
156+
proc = wallet.walletprocesspsbt(comb_nonce_psbt)
157+
assert_equal(proc["complete"], False)
158+
psig_psbts.append(proc["psbt"])
159+
160+
comb_psig_psbt = self.nodes[0].combinepsbt(psig_psbts)
161+
162+
dec_psbt = self.nodes[0].decodepsbt(comb_psig_psbt)
163+
assert_equal(len(dec_psbt["inputs"][0]["musig2_partial_sigs"]), exp_key_leaf)
164+
for ps in dec_psbt["inputs"][0]["musig2_partial_sigs"]:
165+
pubkey = ps["aggregate_pubkey"][2:]
166+
if pubkey in dec_psbt["inputs"][0]["witness_utxo"]["scriptPubKey"]["hex"]:
167+
continue
168+
elif "taproot_internal_key" in dec_psbt["inputs"][0] and pubkey in dec_psbt["inputs"][0]["taproot_internal_key"]:
169+
continue
170+
elif "taproot_scripts" in dec_psbt["inputs"][0]:
171+
for leaf_scripts in dec_psbt["inputs"][0]["taproot_scripts"]:
172+
if pubkey in leaf_scripts["script"]:
173+
break
174+
else:
175+
assert False, "Aggregate pubkey for partial sig not seen as output key, internal key, or in any scripts"
176+
else:
177+
assert False, "Aggregate pubkey for partial sig not seen as output key or internal key"
178+
179+
# Non-participant aggregates partial sigs and send
180+
finalized = self.nodes[0].finalizepsbt(comb_psig_psbt)
181+
assert_equal(finalized["complete"], True)
182+
assert "hex" in finalized
183+
self.nodes[0].sendrawtransaction(finalized["hex"])
184+
185+
def run_test(self):
186+
self.do_test("rawtr(musig(keys/*))", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
187+
self.do_test("tr(musig(keys/*))", "tr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
188+
self.do_test("rawtr(musig/*)", "rawtr(musig($0,$1,$2)/<0;1>/*)")
189+
self.do_test("tr(musig/*)", "tr(musig($0,$1,$2)/<0;1>/*)")
190+
self.do_test("tr(H, pk(musig(keys/*)))", "tr($H,pk(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*)))")
191+
self.do_test("tr(H,pk(musig/*))", "tr($H,pk(musig($0,$1,$2)/<0;1>/*))")
192+
self.do_test("tr(H,{pk(musig/*), pk(musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($3,$4,$5)/0/*)})")
193+
self.do_test("tr(H,{pk(musig/*), pk(same keys different musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($1,$2)/0/*)})")
194+
195+
196+
if __name__ == '__main__':
197+
WalletMuSigTest(__file__).main()

0 commit comments

Comments
 (0)