Skip to content

Commit 88c7493

Browse files
achow101Raimo33
authored andcommitted
test: Test MuSig2 in the wallet
1 parent f4e85e5 commit 88c7493

File tree

2 files changed

+243
-0
lines changed

2 files changed

+243
-0
lines changed

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@
348348
'feature_coinstatsindex.py',
349349
'feature_coinstatsindex_compatibility.py',
350350
'wallet_orphanedreward.py',
351+
'wallet_musig.py',
351352
'wallet_timelock.py',
352353
'p2p_permissions.py',
353354
'feature_blocksdir.py',

test/functional/wallet_musig.py

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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 (
12+
assert_equal,
13+
assert_greater_than,
14+
)
15+
16+
PRIVKEY_RE = re.compile(r"^tr\((.+?)/.+\)#.{8}$")
17+
PUBKEY_RE = re.compile(r"^tr\((\[.+?\].+?)/.+\)#.{8}$")
18+
ORIGIN_PATH_RE = re.compile(r"^\[\w{8}(/.*)\].*$")
19+
MULTIPATH_TWO_RE = re.compile(r"<(\d+);(\d+)>")
20+
MUSIG_RE = re.compile(r"musig\((.*?)\)")
21+
PLACEHOLDER_RE = re.compile(r"\$\d")
22+
23+
class WalletMuSigTest(BitcoinTestFramework):
24+
WALLET_NUM = 0
25+
def set_test_params(self):
26+
self.num_nodes = 1
27+
28+
def skip_test_if_missing_module(self):
29+
self.skip_if_no_wallet()
30+
31+
def do_test(self, comment, pattern, sighash_type=None, scriptpath=False, nosign_wallets=None, only_one_musig_wallet=False):
32+
self.log.info(f"Testing {comment}")
33+
has_internal = MULTIPATH_TWO_RE.search(pattern) is not None
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+
expected_pubnonces = 0
42+
expected_partial_sigs = 0
43+
for musig in MUSIG_RE.findall(pat):
44+
musig_partial_sigs = 0
45+
for placeholder in PLACEHOLDER_RE.findall(musig):
46+
wallet_index = int(placeholder[1:])
47+
if nosign_wallets is None or wallet_index not in nosign_wallets:
48+
expected_pubnonces += 1
49+
else:
50+
musig_partial_sigs = None
51+
if musig_partial_sigs is not None:
52+
musig_partial_sigs += 1
53+
if wallet_index < len(wallets):
54+
continue
55+
wallet_name = f"musig_{self.WALLET_NUM}"
56+
self.WALLET_NUM += 1
57+
self.nodes[0].createwallet(wallet_name)
58+
wallet = self.nodes[0].get_wallet_rpc(wallet_name)
59+
wallets.append(wallet)
60+
61+
for priv_desc in wallet.listdescriptors(True)["descriptors"]:
62+
desc = priv_desc["desc"]
63+
if not desc.startswith("tr("):
64+
continue
65+
privkey = PRIVKEY_RE.search(desc).group(1)
66+
break
67+
for pub_desc in wallet.listdescriptors()["descriptors"]:
68+
desc = pub_desc["desc"]
69+
if not desc.startswith("tr("):
70+
continue
71+
pubkey = PUBKEY_RE.search(desc).group(1)
72+
# Since the pubkey is derived from the private key that we have, we need
73+
# to extract and insert the origin path from the pubkey as well.
74+
privkey += ORIGIN_PATH_RE.search(pubkey).group(1)
75+
break
76+
keys.append((privkey, pubkey))
77+
if musig_partial_sigs is not None:
78+
expected_partial_sigs += musig_partial_sigs
79+
80+
# Construct and import each wallet's musig descriptor that
81+
# contains the private key from that wallet and pubkeys of the others
82+
for i, wallet in enumerate(wallets):
83+
if only_one_musig_wallet and i > 0:
84+
continue
85+
desc = pat
86+
import_descs = []
87+
for j, (priv, pub) in enumerate(keys):
88+
if j == i:
89+
desc = desc.replace(f"${i}", priv)
90+
else:
91+
desc = desc.replace(f"${j}", pub)
92+
93+
import_descs.append({
94+
"desc": descsum_create(desc),
95+
"active": True,
96+
"timestamp": "now",
97+
})
98+
99+
res = wallet.importdescriptors(import_descs)
100+
for r in res:
101+
assert_equal(r["success"], True)
102+
103+
# Check that the wallets agree on the same musig address
104+
addr = None
105+
change_addr = None
106+
for i, wallet in enumerate(wallets):
107+
if only_one_musig_wallet and i > 0:
108+
continue
109+
if addr is None:
110+
addr = wallet.getnewaddress(address_type="bech32m")
111+
else:
112+
assert_equal(addr, wallet.getnewaddress(address_type="bech32m"))
113+
if has_internal:
114+
if change_addr is None:
115+
change_addr = wallet.getrawchangeaddress(address_type="bech32m")
116+
else:
117+
assert_equal(change_addr, wallet.getrawchangeaddress(address_type="bech32m"))
118+
119+
# Fund that address
120+
self.def_wallet.sendtoaddress(addr, 10)
121+
self.generate(self.nodes[0], 1)
122+
123+
# Spend that UTXO
124+
utxo = None
125+
for i, wallet in enumerate(wallets):
126+
if only_one_musig_wallet and i > 0:
127+
continue
128+
if utxo is None:
129+
utxo = wallet.listunspent()[0]
130+
else:
131+
assert_equal(utxo, wallet.listunspent()[0])
132+
psbt = wallets[0].walletcreatefundedpsbt(outputs=[{self.def_wallet.getnewaddress(): 5}], inputs=[utxo], change_type="bech32m", changePosition=1)["psbt"]
133+
134+
dec_psbt = self.nodes[0].decodepsbt(psbt)
135+
assert_equal(len(dec_psbt["inputs"]), 1)
136+
assert_equal(len(dec_psbt["inputs"][0]["musig2_participant_pubkeys"]), pattern.count("musig("))
137+
if has_internal:
138+
assert_equal(len(dec_psbt["outputs"][1]["musig2_participant_pubkeys"]), pattern.count("musig("))
139+
140+
# Check all participant pubkeys in the input and change output
141+
psbt_maps = [dec_psbt["inputs"][0]]
142+
if has_internal:
143+
psbt_maps.append(dec_psbt["outputs"][1])
144+
for psbt_map in psbt_maps:
145+
part_pks = set()
146+
for agg in psbt_map["musig2_participant_pubkeys"]:
147+
for part_pub in agg["participant_pubkeys"]:
148+
part_pks.add(part_pub[2:])
149+
# Check that there are as many participants as we expected
150+
assert_equal(len(part_pks), len(keys))
151+
# Check that each participant has a derivation path
152+
for deriv_path in psbt_map["taproot_bip32_derivs"]:
153+
if deriv_path["pubkey"] in part_pks:
154+
part_pks.remove(deriv_path["pubkey"])
155+
assert_equal(len(part_pks), 0)
156+
157+
# Add pubnonces
158+
nonce_psbts = []
159+
for i, wallet in enumerate(wallets):
160+
if nosign_wallets and i in nosign_wallets:
161+
continue
162+
proc = wallet.walletprocesspsbt(psbt=psbt, sighashtype=sighash_type)
163+
assert_equal(proc["complete"], False)
164+
nonce_psbts.append(proc["psbt"])
165+
166+
comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)
167+
168+
dec_psbt = self.nodes[0].decodepsbt(comb_nonce_psbt)
169+
assert_equal(len(dec_psbt["inputs"][0]["musig2_pubnonces"]), expected_pubnonces)
170+
for pn in dec_psbt["inputs"][0]["musig2_pubnonces"]:
171+
pubkey = pn["aggregate_pubkey"][2:]
172+
if pubkey in dec_psbt["inputs"][0]["witness_utxo"]["scriptPubKey"]["hex"]:
173+
continue
174+
elif "taproot_scripts" in dec_psbt["inputs"][0]:
175+
for leaf_scripts in dec_psbt["inputs"][0]["taproot_scripts"]:
176+
if pubkey in leaf_scripts["script"]:
177+
break
178+
else:
179+
assert False, "Aggregate pubkey for pubnonce not seen as output key, or in any scripts"
180+
else:
181+
assert False, "Aggregate pubkey for pubnonce not seen as output key or internal key"
182+
183+
# Add partial sigs
184+
psig_psbts = []
185+
for i, wallet in enumerate(wallets):
186+
if nosign_wallets and i in nosign_wallets:
187+
continue
188+
proc = wallet.walletprocesspsbt(psbt=comb_nonce_psbt, sighashtype=sighash_type)
189+
assert_equal(proc["complete"], False)
190+
psig_psbts.append(proc["psbt"])
191+
192+
comb_psig_psbt = self.nodes[0].combinepsbt(psig_psbts)
193+
194+
dec_psbt = self.nodes[0].decodepsbt(comb_psig_psbt)
195+
assert_equal(len(dec_psbt["inputs"][0]["musig2_partial_sigs"]), expected_partial_sigs)
196+
for ps in dec_psbt["inputs"][0]["musig2_partial_sigs"]:
197+
pubkey = ps["aggregate_pubkey"][2:]
198+
if pubkey in dec_psbt["inputs"][0]["witness_utxo"]["scriptPubKey"]["hex"]:
199+
continue
200+
elif "taproot_scripts" in dec_psbt["inputs"][0]:
201+
for leaf_scripts in dec_psbt["inputs"][0]["taproot_scripts"]:
202+
if pubkey in leaf_scripts["script"]:
203+
break
204+
else:
205+
assert False, "Aggregate pubkey for partial sig not seen as output key or in any scripts"
206+
else:
207+
assert False, "Aggregate pubkey for partial sig not seen as output key"
208+
209+
# Non-participant aggregates partial sigs and send
210+
finalized = self.nodes[0].finalizepsbt(psbt=comb_psig_psbt, extract=False)
211+
assert_equal(finalized["complete"], True)
212+
witness = self.nodes[0].decodepsbt(finalized["psbt"])["inputs"][0]["final_scriptwitness"]
213+
if scriptpath:
214+
assert_greater_than(len(witness), 1)
215+
else:
216+
assert_equal(len(witness), 1)
217+
finalized = self.nodes[0].finalizepsbt(comb_psig_psbt)
218+
assert "hex" in finalized
219+
self.nodes[0].sendrawtransaction(finalized["hex"])
220+
221+
def run_test(self):
222+
self.def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
223+
224+
self.do_test("rawtr(musig(keys/*))", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
225+
self.do_test("rawtr(musig(keys/*)) with ALL|ANYONECANPAY", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))", "ALL|ANYONECANPAY")
226+
self.do_test("tr(musig(keys/*)) no multipath", "tr(musig($0/0/*,$1/1/*,$2/2/*))")
227+
self.do_test("tr(musig(keys/*)) 2 index multipath", "tr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
228+
self.do_test("tr(musig(keys/*)) 3 index multipath", "tr(musig($0/<0;1;2>/*,$1/<1;2;3>/*,$2/<2;3;4>/*))")
229+
self.do_test("rawtr(musig/*)", "rawtr(musig($0,$1,$2)/<0;1>/*)")
230+
self.do_test("tr(musig/*)", "tr(musig($0,$1,$2)/<0;1>/*)")
231+
self.do_test("rawtr(musig(keys/*)) without all wallets importing", "rawtr(musig($0/<0;1>/*,$1/<0;1>/*,$2/<0;1>/*))", only_one_musig_wallet=True)
232+
self.do_test("tr(musig(keys/*)) without all wallets importing", "tr(musig($0/<0;1>/*,$1/<0;1>/*,$2/<0;1>/*))", only_one_musig_wallet=True)
233+
self.do_test("tr(H, pk(musig(keys/*)))", "tr($H,pk(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*)))", scriptpath=True)
234+
self.do_test("tr(H,pk(musig/*))", "tr($H,pk(musig($0,$1,$2)/<0;1>/*))", scriptpath=True)
235+
self.do_test("tr(H,{pk(musig/*), pk(musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($3,$4,$5)/0/*)})", scriptpath=True)
236+
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/*)})", scriptpath=True)
237+
self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})}", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})")
238+
self.do_test("tr(musig/*,{pk(partial keys diff musig-1/*),pk(partial keys diff musig-2/*)})} script-path", "tr(musig($0,$1,$2)/<3;4>/*,{pk(musig($0,$1)/<5;6>/*),pk(musig($1,$2)/7/*)})", scriptpath=True, nosign_wallets=[0])
239+
240+
241+
if __name__ == '__main__':
242+
WalletMuSigTest(__file__).main()

0 commit comments

Comments
 (0)