@@ -26,13 +26,15 @@ def skip_test_if_missing_module(self):
26
26
self .skip_if_no_wallet ()
27
27
self .skip_if_no_sqlite ()
28
28
29
- def _get_xpub (self , wallet ):
29
+ @staticmethod
30
+ def _get_xpub (wallet ):
30
31
"""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)."""
31
32
descriptor = next (filter (lambda d : d ["desc" ].startswith ("pkh" ), wallet .listdescriptors ()["descriptors" ]))
32
33
return descriptor ["desc" ].split ("]" )[- 1 ].split ("/" )[0 ]
33
34
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."""
36
38
tx = multisig .decodepsbt (psbt )["tx" ]
37
39
amount = 0
38
40
for vout in tx ["vout" ]:
@@ -42,25 +44,19 @@ def _check_psbt(self, psbt, to, value, multisig):
42
44
amount += vout ["value" ]
43
45
assert_approx (amount , float (value ), vspan = 0.001 )
44
46
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 ):
52
48
"""The multisig is created by importing the following descriptors. The resulting wallet is watch-only and every participant can do this."""
53
49
# some simple validation
54
50
assert_equal (len (xpubs ), self .N )
55
51
# a sanity-check/assertion, this will throw if the base58 checksum of any of the provided xpubs are invalid
56
52
for xpub in xpubs :
57
53
base58_to_byte (xpub )
58
54
59
- for i , node in enumerate (participants ):
55
+ for i , node in enumerate (self . nodes ):
60
56
node .createwallet (wallet_name = f"{ self .name } _{ i } " , blank = True , descriptors = True , disable_private_keys = True )
61
57
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 /*))" )
64
60
result = multisig .importdescriptors ([
65
61
{ # receiving addresses (internal: False)
66
62
"desc" : external ["descriptor" ],
@@ -76,73 +72,81 @@ def participants_import_descriptors(self, participants, xpubs):
76
72
},
77
73
])
78
74
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
91
76
92
77
def run_test (self ):
93
78
self .M = 2
94
79
self .N = self .num_nodes
95
80
self .name = f"{ self .M } _of_{ self .N } _multisig"
96
81
self .log .info (f"Testing { self .name } ..." )
97
82
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
+
98
92
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" ]]
100
94
101
95
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 )
103
104
104
105
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 ]
106
107
coordinator_wallet .generatetoaddress (101 , coordinator_wallet .getnewaddress ())
107
108
108
109
deposit_amount = 6.15
109
- multisig_receiving_address = self . get_multisig_receiving_address ()
110
+ multisig_receiving_address = participants [ "multisigs" ][ 0 ]. getnewaddress ()
110
111
self .log .info ("Send funds to the resulting multisig receiving address..." )
111
112
coordinator_wallet .sendtoaddress (multisig_receiving_address , deposit_amount )
112
113
self .nodes [0 ].generate (1 )
113
114
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 )
116
117
117
118
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 ()
119
120
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 })
121
123
122
124
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..." )
124
126
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 ]
126
128
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 ]
128
130
partially_signed_psbt = signing_wallet .walletprocesspsbt (psbt ["psbt" ])
129
131
psbts .append (partially_signed_psbt ["psbt" ])
130
132
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..." )
132
134
combined = coordinator_wallet .combinepsbt (psbts )
133
135
finalized = coordinator_wallet .finalizepsbt (combined )
134
136
coordinator_wallet .sendrawtransaction (finalized ["hex" ])
135
137
136
138
self .log .info ("Check that balances are correct after the transaction has been included in a block." )
137
139
self .nodes [0 ].generate (1 )
138
140
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 )
141
143
142
144
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 } )
144
146
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 ]
146
150
psbt = signing_wallet .walletprocesspsbt (psbt ["psbt" ])
147
151
assert_equal (psbt ["complete" ], m == self .M - 1 )
148
152
finalized = coordinator_wallet .finalizepsbt (psbt ["psbt" ])
@@ -151,8 +155,8 @@ def run_test(self):
151
155
self .log .info ("Check that balances are correct after the transaction has been included in a block." )
152
156
self .nodes [0 ].generate (1 )
153
157
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 )
156
160
157
161
158
162
if __name__ == "__main__" :
0 commit comments