Skip to content

Commit 217dbbb

Browse files
fjahrrkrux
andcommitted
test: Add musig failure scenarios
Also changes the the non-constant variable NUM_WALLETS to lower case and refactors the success case scenarios to reuse existing code. Co-authored-by: rkrux <[email protected]>
1 parent c9519c2 commit 217dbbb

File tree

1 file changed

+136
-41
lines changed

1 file changed

+136
-41
lines changed

test/functional/wallet_musig.py

Lines changed: 136 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,39 +21,26 @@
2121
PLACEHOLDER_RE = re.compile(r"\$\d")
2222

2323
class WalletMuSigTest(BitcoinTestFramework):
24-
WALLET_NUM = 0
24+
wallet_num = 0
2525
def set_test_params(self):
2626
self.num_nodes = 1
2727

2828
def skip_test_if_missing_module(self):
2929
self.skip_if_no_wallet()
3030

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-
31+
# Create wallets and extract keys
32+
def create_wallets_and_keys_from_pattern(self, pat):
3533
wallets = []
3634
keys = []
3735

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
4336
for musig in MUSIG_RE.findall(pat):
44-
musig_partial_sigs = 0
4537
for placeholder in PLACEHOLDER_RE.findall(musig):
4638
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
5339
if wallet_index < len(wallets):
5440
continue
55-
wallet_name = f"musig_{self.WALLET_NUM}"
56-
self.WALLET_NUM += 1
41+
42+
wallet_name = f"musig_{self.wallet_num}"
43+
self.wallet_num += 1
5744
self.nodes[0].createwallet(wallet_name)
5845
wallet = self.nodes[0].get_wallet_rpc(wallet_name)
5946
wallets.append(wallet)
@@ -74,32 +61,137 @@ def do_test(self, comment, pattern, sighash_type=None, scriptpath=False, nosign_
7461
privkey += ORIGIN_PATH_RE.search(pubkey).group(1)
7562
break
7663
keys.append((privkey, pubkey))
77-
if musig_partial_sigs is not None:
78-
expected_partial_sigs += musig_partial_sigs
7964

80-
# Construct and import each wallet's musig descriptor that
81-
# contains the private key from that wallet and pubkeys of the others
65+
return wallets, keys
66+
67+
# Construct and import each wallet's musig descriptor that
68+
# contains the private key from that wallet and pubkeys of the others
69+
def construct_and_import_musig_descriptor_in_wallets(self, pat, wallets, keys, only_one_musig_wallet=False):
8270
for i, wallet in enumerate(wallets):
8371
if only_one_musig_wallet and i > 0:
8472
continue
8573
desc = pat
86-
import_descs = []
8774
for j, (priv, pub) in enumerate(keys):
8875
if j == i:
8976
desc = desc.replace(f"${i}", priv)
9077
else:
9178
desc = desc.replace(f"${j}", pub)
9279

93-
import_descs.append({
80+
import_descs = [{
9481
"desc": descsum_create(desc),
9582
"active": True,
9683
"timestamp": "now",
97-
})
84+
}]
9885

9986
res = wallet.importdescriptors(import_descs)
10087
for r in res:
10188
assert_equal(r["success"], True)
10289

90+
def setup_musig_scenario(self, pat):
91+
wallets, keys = self.create_wallets_and_keys_from_pattern(pat)
92+
self.construct_and_import_musig_descriptor_in_wallets(pat, wallets, keys, only_one_musig_wallet=False)
93+
94+
# Fund address
95+
addr = wallets[0].getnewaddress(address_type="bech32m")
96+
for wallet in wallets[1:]:
97+
assert_equal(addr, wallet.getnewaddress(address_type="bech32m"))
98+
99+
self.def_wallet.sendtoaddress(addr, 10)
100+
self.generate(self.nodes[0], 1)
101+
102+
# Create PSBT
103+
utxo = wallets[0].listunspent()[0]
104+
psbt = wallets[0].walletcreatefundedpsbt(
105+
outputs=[{self.def_wallet.getnewaddress(): 5}],
106+
inputs=[utxo],
107+
change_type="bech32m",
108+
changePosition=1
109+
)["psbt"]
110+
111+
return wallets, psbt
112+
113+
def test_failure_case_1(self, comment, pat):
114+
self.log.info(f"Testing {comment}")
115+
wallets, psbt = self.setup_musig_scenario(pat)
116+
117+
# Only 2 out of 3 participants provide nonces
118+
nonce_psbts = []
119+
for i in range(2):
120+
proc = wallets[i].walletprocesspsbt(psbt=psbt)
121+
nonce_psbts.append(proc["psbt"])
122+
123+
comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)
124+
125+
# Attempt to create partial sigs. This should not complete due to the
126+
# missing nonce.
127+
for wallet in wallets[:2]:
128+
proc = wallet.walletprocesspsbt(psbt=comb_nonce_psbt)
129+
assert_equal(proc["complete"], False)
130+
# No partial sigs are created
131+
dec = self.nodes[0].decodepsbt(proc["psbt"])
132+
# There are still only two nonces
133+
assert_equal(len(dec["inputs"][0].get("musig2_pubnonces", [])), 2)
134+
135+
def test_failure_case_2(self, comment, pat):
136+
self.log.info(f"Testing {comment}")
137+
wallets, psbt = self.setup_musig_scenario(pat)
138+
nonce_psbts = [w.walletprocesspsbt(psbt=psbt)["psbt"] for w in wallets]
139+
comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)
140+
141+
# Only 2 out of 3 provide partial sigs
142+
psig_psbts = []
143+
for i in range(2):
144+
proc = wallets[i].walletprocesspsbt(psbt=comb_nonce_psbt)
145+
psig_psbts.append(proc["psbt"])
146+
147+
comb_psig_psbt = self.nodes[0].combinepsbt(psig_psbts)
148+
149+
# Finalization fails due to missing partial sig
150+
finalized = self.nodes[0].finalizepsbt(comb_psig_psbt)
151+
assert_equal(finalized["complete"], False)
152+
153+
# Still only two partial sigs in combined PSBT
154+
dec = self.nodes[0].decodepsbt(comb_psig_psbt)
155+
assert_equal(len(dec["inputs"][0]["musig2_partial_sigs"]), 2)
156+
157+
def test_failure_case_3(self, comment, pat):
158+
self.log.info(f"Testing {comment}")
159+
wallets, psbt = self.setup_musig_scenario(pat)
160+
nonce_psbts = [w.walletprocesspsbt(psbt=psbt)["psbt"] for w in wallets]
161+
comb_nonce_psbt = self.nodes[0].combinepsbt(nonce_psbts)
162+
163+
finalized = self.nodes[0].finalizepsbt(comb_nonce_psbt)
164+
assert_equal(finalized["complete"], False)
165+
166+
dec = self.nodes[0].decodepsbt(comb_nonce_psbt)
167+
assert "musig2_pubnonces" in dec["inputs"][0]
168+
assert "musig2_partial_sigs" not in dec["inputs"][0]
169+
170+
def test_success_case(self, comment, pattern, sighash_type=None, scriptpath=False, nosign_wallets=None, only_one_musig_wallet=False):
171+
self.log.info(f"Testing {comment}")
172+
has_internal = MULTIPATH_TWO_RE.search(pattern) is not None
173+
174+
pat = pattern.replace("$H", H_POINT)
175+
wallets, keys = self.create_wallets_and_keys_from_pattern(pat)
176+
self.construct_and_import_musig_descriptor_in_wallets(pat, wallets, keys, only_one_musig_wallet)
177+
178+
expected_pubnonces = 0
179+
expected_partial_sigs = 0
180+
for musig in MUSIG_RE.findall(pat):
181+
musig_partial_sigs = 0
182+
for placeholder in PLACEHOLDER_RE.findall(musig):
183+
wallet_index = int(placeholder[1:])
184+
if nosign_wallets is None or wallet_index not in nosign_wallets:
185+
expected_pubnonces += 1
186+
else:
187+
musig_partial_sigs = None
188+
if musig_partial_sigs is not None:
189+
musig_partial_sigs += 1
190+
if wallet_index < len(wallets):
191+
continue
192+
if musig_partial_sigs is not None:
193+
expected_partial_sigs += musig_partial_sigs
194+
103195
# Check that the wallets agree on the same musig address
104196
addr = None
105197
change_addr = None
@@ -221,22 +313,25 @@ def do_test(self, comment, pattern, sighash_type=None, scriptpath=False, nosign_
221313
def run_test(self):
222314
self.def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
223315

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])
316+
self.test_success_case("rawtr(musig(keys/*))", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
317+
self.test_success_case("rawtr(musig(keys/*)) with ALL|ANYONECANPAY", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))", "ALL|ANYONECANPAY")
318+
self.test_success_case("tr(musig(keys/*)) no multipath", "tr(musig($0/0/*,$1/1/*,$2/2/*))")
319+
self.test_success_case("tr(musig(keys/*)) 2 index multipath", "tr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
320+
self.test_success_case("tr(musig(keys/*)) 3 index multipath", "tr(musig($0/<0;1;2>/*,$1/<1;2;3>/*,$2/<2;3;4>/*))")
321+
self.test_success_case("rawtr(musig/*)", "rawtr(musig($0,$1,$2)/<0;1>/*)")
322+
self.test_success_case("tr(musig/*)", "tr(musig($0,$1,$2)/<0;1>/*)")
323+
self.test_success_case("rawtr(musig(keys/*)) without all wallets importing", "rawtr(musig($0/<0;1>/*,$1/<0;1>/*,$2/<0;1>/*))", only_one_musig_wallet=True)
324+
self.test_success_case("tr(musig(keys/*)) without all wallets importing", "tr(musig($0/<0;1>/*,$1/<0;1>/*,$2/<0;1>/*))", only_one_musig_wallet=True)
325+
self.test_success_case("tr(H, pk(musig(keys/*)))", "tr($H,pk(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*)))", scriptpath=True)
326+
self.test_success_case("tr(H,pk(musig/*))", "tr($H,pk(musig($0,$1,$2)/<0;1>/*))", scriptpath=True)
327+
self.test_success_case("tr(H,{pk(musig/*), pk(musig/*)})", "tr($H,{pk(musig($0,$1,$2)/<0;1>/*),pk(musig($3,$4,$5)/0/*)})", scriptpath=True)
328+
self.test_success_case("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)
329+
self.test_success_case("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/*)})")
330+
self.test_success_case("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])
239331

332+
self.test_failure_case_1("missing participant nonce", "tr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
333+
self.test_failure_case_2("insufficient partial signatures", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*,$2/<2;3>/*))")
334+
self.test_failure_case_3("finalize without partial sigs", "rawtr(musig($0/<0;1>/*,$1/<1;2>/*))")
240335

241336
if __name__ == '__main__':
242337
WalletMuSigTest(__file__).main()

0 commit comments

Comments
 (0)