2121PLACEHOLDER_RE = re .compile (r"\$\d" )
2222
2323class 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
241336if __name__ == '__main__' :
242337 WalletMuSigTest (__file__ ).main ()
0 commit comments