@@ -26,13 +26,15 @@ def skip_test_if_missing_module(self):
2626 self .skip_if_no_wallet ()
2727 self .skip_if_no_sqlite ()
2828
29- def _get_xpub (self , wallet ):
29+ @staticmethod
30+ def _get_xpub (wallet ):
3031 """Extract the wallet's xpubs using `listdescriptors` and pick the one from the `pkh` descriptor since it's least likely to be accidentally reused (legacy addresses)."""
3132 descriptor = next (filter (lambda d : d ["desc" ].startswith ("pkh" ), wallet .listdescriptors ()["descriptors" ]))
3233 return descriptor ["desc" ].split ("]" )[- 1 ].split ("/" )[0 ]
3334
34- def _check_psbt (self , psbt , to , value , multisig ):
35- """Helper method for any of the N participants to check the psbt with decodepsbt and verify it is OK before signing."""
35+ @staticmethod
36+ def _check_psbt (psbt , to , value , multisig ):
37+ """Helper function for any of the N participants to check the psbt with decodepsbt and verify it is OK before signing."""
3638 tx = multisig .decodepsbt (psbt )["tx" ]
3739 amount = 0
3840 for vout in tx ["vout" ]:
@@ -42,25 +44,19 @@ def _check_psbt(self, psbt, to, value, multisig):
4244 amount += vout ["value" ]
4345 assert_approx (amount , float (value ), vspan = 0.001 )
4446
45- def generate_and_exchange_xpubs (self , participants ):
46- """Every participant generates an xpub. The most straightforward way is to create a new descriptor wallet. Avoid reusing this wallet for any other purpose.."""
47- for i , node in enumerate (participants ):
48- node .createwallet (wallet_name = f"participant_{ i } " , descriptors = True )
49- yield self ._get_xpub (node .get_wallet_rpc (f"participant_{ i } " ))
50-
51- def participants_import_descriptors (self , participants , xpubs ):
47+ def participants_create_multisigs (self , xpubs ):
5248 """The multisig is created by importing the following descriptors. The resulting wallet is watch-only and every participant can do this."""
5349 # some simple validation
5450 assert_equal (len (xpubs ), self .N )
5551 # a sanity-check/assertion, this will throw if the base58 checksum of any of the provided xpubs are invalid
5652 for xpub in xpubs :
5753 base58_to_byte (xpub )
5854
59- for i , node in enumerate (participants ):
55+ for i , node in enumerate (self . nodes ):
6056 node .createwallet (wallet_name = f"{ self .name } _{ i } " , blank = True , descriptors = True , disable_private_keys = True )
6157 multisig = node .get_wallet_rpc (f"{ self .name } _{ i } " )
62- external = multisig .getdescriptorinfo (f"wsh(sortedmulti({ self .M } ,{ f'/{ 0 } /*,' .join (xpubs )} /{ 0 } /*))" )
63- internal = multisig .getdescriptorinfo (f"wsh(sortedmulti({ self .M } ,{ f'/{ 1 } /*,' .join (xpubs )} /{ 1 } /*))" )
58+ external = multisig .getdescriptorinfo (f"wsh(sortedmulti({ self .M } ,{ f'/0 /*,' .join (xpubs )} /0 /*))" )
59+ internal = multisig .getdescriptorinfo (f"wsh(sortedmulti({ self .M } ,{ f'/1 /*,' .join (xpubs )} /1 /*))" )
6460 result = multisig .importdescriptors ([
6561 { # receiving addresses (internal: False)
6662 "desc" : external ["descriptor" ],
@@ -76,73 +72,81 @@ def participants_import_descriptors(self, participants, xpubs):
7672 },
7773 ])
7874 assert all (r ["success" ] for r in result )
79-
80- def get_multisig_receiving_address (self ):
81- """We will send funds to the resulting address (every participant should get the same addresses)."""
82- multisig = self .nodes [0 ].get_wallet_rpc (f"{ self .name } _{ 0 } " )
83- receiving_address = multisig .getnewaddress ()
84- for i in range (1 , self .N ):
85- assert_equal (receiving_address , self .nodes [i ].get_wallet_rpc (f"{ self .name } _{ i } " ).getnewaddress ())
86- return receiving_address
87-
88- def make_sending_transaction (self , to , value ):
89- """Make a sending transaction, created using walletcreatefundedpsbt (anyone can initiate this)."""
90- return self .nodes [0 ].get_wallet_rpc (f"{ self .name } _{ 0 } " ).walletcreatefundedpsbt (inputs = [], outputs = {to : value }, options = {"feeRate" : 0.00010 })
75+ yield multisig
9176
9277 def run_test (self ):
9378 self .M = 2
9479 self .N = self .num_nodes
9580 self .name = f"{ self .M } _of_{ self .N } _multisig"
9681 self .log .info (f"Testing { self .name } ..." )
9782
83+ participants = {
84+ # Every participant generates an xpub. The most straightforward way is to create a new descriptor wallet.
85+ # This wallet will be the participant's `signer` for the resulting multisig. Avoid reusing this wallet for any other purpose (for privacy reasons).
86+ "signers" : [node .get_wallet_rpc (node .createwallet (wallet_name = f"participant_{ self .nodes .index (node )} " , descriptors = True )["name" ]) for node in self .nodes ],
87+ # After participants generate and exchange their xpubs they will each create their own watch-only multisig.
88+ # Note: these multisigs are all the same, this justs highlights that each participant can independently verify everything on their own node.
89+ "multisigs" : []
90+ }
91+
9892 self .log .info ("Generate and exchange xpubs..." )
99- xpubs = list ( self .generate_and_exchange_xpubs ( self . nodes ))
93+ xpubs = [ self ._get_xpub ( signer ) for signer in participants [ "signers" ]]
10094
10195 self .log .info ("Every participant imports the following descriptors to create the watch-only multisig..." )
102- self .participants_import_descriptors (self .nodes , xpubs )
96+ participants ["multisigs" ] = list (self .participants_create_multisigs (xpubs ))
97+
98+ self .log .info ("Check that every participant's multisig generates the same addresses..." )
99+ for _ in range (10 ): # we check that the first 10 generated addresses are the same for all participant's multisigs
100+ receive_addresses = [multisig .getnewaddress () for multisig in participants ["multisigs" ]]
101+ all (address == receive_addresses [0 ] for address in receive_addresses )
102+ change_addresses = [multisig .getrawchangeaddress () for multisig in participants ["multisigs" ]]
103+ all (address == change_addresses [0 ] for address in change_addresses )
103104
104105 self .log .info ("Get a mature utxo to send to the multisig..." )
105- coordinator_wallet = self . nodes [ 0 ]. get_wallet_rpc ( f"participant_ { 0 } " )
106+ coordinator_wallet = participants [ "signers" ][ 0 ]
106107 coordinator_wallet .generatetoaddress (101 , coordinator_wallet .getnewaddress ())
107108
108109 deposit_amount = 6.15
109- multisig_receiving_address = self . get_multisig_receiving_address ()
110+ multisig_receiving_address = participants [ "multisigs" ][ 0 ]. getnewaddress ()
110111 self .log .info ("Send funds to the resulting multisig receiving address..." )
111112 coordinator_wallet .sendtoaddress (multisig_receiving_address , deposit_amount )
112113 self .nodes [0 ].generate (1 )
113114 self .sync_all ()
114- for n in range ( self . N ) :
115- assert_approx (self . nodes [ n ]. get_wallet_rpc ( f" { self . name } _ { n } " ) .getbalance (), deposit_amount , vspan = 0.001 )
115+ for participant in participants [ "multisigs" ] :
116+ assert_approx (participant .getbalance (), deposit_amount , vspan = 0.001 )
116117
117118 self .log .info ("Send a transaction from the multisig!" )
118- to = self . nodes [ self . N - 1 ]. get_wallet_rpc ( f"participant_ { self .N - 1 } " ) .getnewaddress ()
119+ to = participants [ "signers" ][ self .N - 1 ] .getnewaddress ()
119120 value = 1
120- psbt = self .make_sending_transaction (to , value )
121+ self .log .info ("First, make a sending transaction, created using `walletcreatefundedpsbt` (anyone can initiate this)..." )
122+ psbt = participants ["multisigs" ][0 ].walletcreatefundedpsbt (inputs = [], outputs = {to : value }, options = {"feeRate" : 0.00010 })
121123
122124 psbts = []
123- self .log .info ("At least M users check the psbt with decodepsbt and (if OK) signs it with walletprocesspsbt..." )
125+ self .log .info ("Now at least M users check the psbt with decodepsbt and (if OK) signs it with walletprocesspsbt..." )
124126 for m in range (self .M ):
125- signers_multisig = self . nodes [ m ]. get_wallet_rpc ( f" { self . name } _ { m } " )
127+ signers_multisig = participants [ "multisigs" ][ m ]
126128 self ._check_psbt (psbt ["psbt" ], to , value , signers_multisig )
127- signing_wallet = self . nodes [ m ]. get_wallet_rpc ( f"participant_ { m } " )
129+ signing_wallet = participants [ "signers" ][ m ]
128130 partially_signed_psbt = signing_wallet .walletprocesspsbt (psbt ["psbt" ])
129131 psbts .append (partially_signed_psbt ["psbt" ])
130132
131- self .log .info ("Collect the signed PSBTs with combinepsbt, finalizepsbt, then broadcast the resulting transaction..." )
133+ self .log .info ("Finally, collect the signed PSBTs with combinepsbt, finalizepsbt, then broadcast the resulting transaction..." )
132134 combined = coordinator_wallet .combinepsbt (psbts )
133135 finalized = coordinator_wallet .finalizepsbt (combined )
134136 coordinator_wallet .sendrawtransaction (finalized ["hex" ])
135137
136138 self .log .info ("Check that balances are correct after the transaction has been included in a block." )
137139 self .nodes [0 ].generate (1 )
138140 self .sync_all ()
139- assert_approx (self . nodes [ 0 ]. get_wallet_rpc ( f" { self . name } _ { 0 } " ) .getbalance (), deposit_amount - value , vspan = 0.001 )
140- assert_equal (self . nodes [ self . N - 1 ]. get_wallet_rpc ( f"participant_ { self .N - 1 } " ) .getbalance (), value )
141+ assert_approx (participants [ "multisigs" ][ 0 ] .getbalance (), deposit_amount - value , vspan = 0.001 )
142+ assert_equal (participants [ "signers" ][ self .N - 1 ] .getbalance (), value )
141143
142144 self .log .info ("Send another transaction from the multisig, this time with a daisy chained signing flow (one after another in series)!" )
143- psbt = self . make_sending_transaction ( to , value )
145+ psbt = participants [ "multisigs" ][ 0 ]. walletcreatefundedpsbt ( inputs = [], outputs = { to : value }, options = { "feeRate" : 0.00010 } )
144146 for m in range (self .M ):
145- signing_wallet = self .nodes [m ].get_wallet_rpc (f"participant_{ m } " )
147+ signers_multisig = participants ["multisigs" ][m ]
148+ self ._check_psbt (psbt ["psbt" ], to , value , signers_multisig )
149+ signing_wallet = participants ["signers" ][m ]
146150 psbt = signing_wallet .walletprocesspsbt (psbt ["psbt" ])
147151 assert_equal (psbt ["complete" ], m == self .M - 1 )
148152 finalized = coordinator_wallet .finalizepsbt (psbt ["psbt" ])
@@ -151,8 +155,8 @@ def run_test(self):
151155 self .log .info ("Check that balances are correct after the transaction has been included in a block." )
152156 self .nodes [0 ].generate (1 )
153157 self .sync_all ()
154- assert_approx (self . nodes [ 0 ]. get_wallet_rpc ( f" { self . name } _ { 0 } " ) .getbalance (), deposit_amount - (value * 2 ), vspan = 0.001 )
155- assert_equal (self . nodes [ self . N - 1 ]. get_wallet_rpc ( f"participant_ { self .N - 1 } " ) .getbalance (), value * 2 )
158+ assert_approx (participants [ "multisigs" ][ 0 ] .getbalance (), deposit_amount - (value * 2 ), vspan = 0.001 )
159+ assert_equal (participants [ "signers" ][ self .N - 1 ] .getbalance (), value * 2 )
156160
157161
158162if __name__ == "__main__" :
0 commit comments