aboutsummaryrefslogtreecommitdiff
path: root/tests/functional_tests/multisig.py
blob: 3da983d694b758fd1fc1e18c720187ffc9bcf2ff (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
#!/usr/bin/env python3

# Copyright (c) 2019-2022, The Monero Project
# 
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without modification, are
# permitted provided that the following conditions are met:
# 
# 1. Redistributions of source code must retain the above copyright notice, this list of
#    conditions and the following disclaimer.
# 
# 2. Redistributions in binary form must reproduce the above copyright notice, this list
#    of conditions and the following disclaimer in the documentation and/or other
#    materials provided with the distribution.
# 
# 3. Neither the name of the copyright holder nor the names of its contributors may be
#    used to endorse or promote products derived from this software without specific
#    prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from __future__ import print_function
import random

"""Test multisig transfers
"""

from framework.daemon import Daemon
from framework.wallet import Wallet

TEST_CASES = \
[
#  M  N  Primary Address
  [2, 2, '45J58b7PmKJFSiNPFFrTdtfMcFGnruP7V4CMuRpX7NsH4j3jGHKAjo3YJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ3gr7sG'],
  [2, 3, '44G2TQNfsiURKkvxp7gbgaJY8WynZvANnhmyMAwv6WeEbAvyAWMfKXRhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5duN94i'],
  [3, 3, '41mro238grj56GnrWkakAKTkBy2yDcXYsUZ2iXCM9pe5Ueajd2RRc6Fhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5ief4ZP'],
  [3, 4, '44vZSprQKJQRFe6t1VHgU4ESvq2dv7TjBLVGE7QscKxMdFSiyyPCEV64NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6dakeff'],
  [2, 4, '47puypSwsV1gvUDratmX4y58fSwikXVehEiBhVLxJA1gRCxHyrRgTDr4NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6aRPj5U'],
  [1, 2, '4A8RnBQixry4VXkqeWhmg8L7vWJVDJj4FN9PV4E7Mgad5ZZ6LKQdn8dYJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ4S8RSB']
]

PUB_ADDRS = [case[2] for case in TEST_CASES]

class MultisigTest():
    def run_test(self):
        self.reset()
        for pub_addr in PUB_ADDRS:
            self.mine(pub_addr, 4)
        self.mine('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 80)

        self.test_states()

        self.fund_addrs_with_normal_wallet(PUB_ADDRS)

        for M, N, pub_addr in TEST_CASES:
            assert M <= N
            shuffled_participants = list(range(N))
            random.shuffle(shuffled_participants)
            shuffled_signers = shuffled_participants[:M]

            expected_outputs = 5 # each wallet owns four mined outputs & one transferred output

            # Create multisig wallet and test transferring
            self.create_multisig_wallets(M, N, pub_addr)
            self.import_multisig_info(shuffled_signers if M != 1 else shuffled_participants, expected_outputs)
            txid = self.transfer(shuffled_signers)
            expected_outputs += 1
            self.import_multisig_info(shuffled_participants, expected_outputs)
            self.check_transaction(txid)

            # If more than 1 signer, try to freeze key image of one signer, make tx using that key
            # image on another signer, then have first signer sign multisg_txset. Should fail
            if M != 1:
                txid = self.try_transfer_frozen(shuffled_signers)
                expected_outputs += 1
                self.import_multisig_info(shuffled_participants, expected_outputs)
                self.check_transaction(txid)

            # Recreate wallet from multisig seed and test transferring
            self.remake_some_multisig_wallets_by_multsig_seed(M)
            self.import_multisig_info(shuffled_signers if M != 1 else shuffled_participants, expected_outputs)
            txid = self.transfer(shuffled_signers)
            expected_outputs += 1
            self.import_multisig_info(shuffled_participants, expected_outputs)
            self.check_transaction(txid)

    def reset(self):
        print('Resetting blockchain')
        daemon = Daemon()
        res = daemon.get_height()
        daemon.pop_blocks(res.height - 1)
        daemon.flush_txpool()

    def mine(self, address, blocks):
        print("Mining some blocks")
        daemon = Daemon()
        daemon.generateblocks(address, blocks)

    # This method sets up N_total wallets with a threshold of M_threshold doing the following steps:
    #   * restore_deterministic_wallet(w/ hardcoded seeds)
    #   * prepare_multisig(enable_multisig_experimental = True)
    #   * make_multisig()
    #   * exchange_multisig_keys()
    def create_multisig_wallets(self, M_threshold, N_total, expected_address):
      print('Creating ' + str(M_threshold) + '/' + str(N_total) + ' multisig wallet')
      seeds = [
        'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted',
        'peeled mixture ionic radar utopia puddle buying illness nuns gadget river spout cavernous bounced paradise drunk looking cottage jump tequila melting went winter adjust spout',
        'dilute gutter certain antics pamphlet macro enjoy left slid guarded bogeys upload nineteen bomb jubilee enhanced irritate turnip eggs swung jukebox loudly reduce sedan slid',
        'waking gown buffet negative reorder speedy baffles hotel pliers dewdrop actress diplomat lymph emit ajar mailed kennel cynical jaunt justice weavers height teardrop toyed lymph',
      ]
      assert M_threshold <= N_total
      assert N_total <= len(seeds)

      # restore_deterministic_wallet() & prepare_multisig()
      self.wallet = [None] * N_total
      info = []
      for i in range(N_total):
        self.wallet[i] = Wallet(idx = i)
        try: self.wallet[i].close_wallet()
        except: pass
        res = self.wallet[i].restore_deterministic_wallet(seed = seeds[i])
        res = self.wallet[i].prepare_multisig(enable_multisig_experimental = True)
        assert len(res.multisig_info) > 0
        info.append(res.multisig_info)

      # Assert that all wallets are multisig
      for i in range(N_total):
        res = self.wallet[i].is_multisig()
        assert res.multisig == False

      # make_multisig() with each other's info
      addresses = []
      next_stage = []
      for i in range(N_total):
        res = self.wallet[i].make_multisig(info, M_threshold)
        addresses.append(res.address)
        next_stage.append(res.multisig_info)

      # Assert multisig paramaters M/N for each wallet
      for i in range(N_total):
        res = self.wallet[i].is_multisig()
        assert res.multisig == True
        assert not res.ready
        assert res.threshold == M_threshold
        assert res.total == N_total

      # exchange_multisig_keys()
      num_exchange_multisig_keys_stages = 0
      while True: # while not all wallets are ready
        n_ready = 0
        for i in range(N_total):
          res = self.wallet[i].is_multisig()
          if res.ready == True:
            n_ready += 1
        assert n_ready == 0 or n_ready == N_total # No partial readiness
        if n_ready == N_total:
          break
        info = next_stage
        next_stage = []
        addresses = []
        for i in range(N_total):
          res = self.wallet[i].exchange_multisig_keys(info)
          next_stage.append(res.multisig_info)
          addresses.append(res.address)
        num_exchange_multisig_keys_stages += 1

      # We should only need N - M + 1 key exchange rounds
      assert num_exchange_multisig_keys_stages == N_total - M_threshold + 1

      # Assert that the all wallets have expected public address
      for i in range(N_total):
        assert addresses[i] == expected_address, addresses[i]
      self.wallet_address = expected_address

      # Assert multisig paramaters M/N and "ready" for each wallet
      for i in range(N_total):
        res = self.wallet[i].is_multisig()
        assert res.multisig == True
        assert res.ready == True
        assert res.threshold == M_threshold
        assert res.total == N_total

    # We want to test if multisig wallets can receive normal transfers as well and mining transfers
    def fund_addrs_with_normal_wallet(self, addrs):
      print("Funding multisig wallets with normal wallet-to-wallet transfers")

      # Generate normal deterministic wallet
      normal_seed = 'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted'
      assert not hasattr(self, 'wallet') or not self.wallet
      self.wallet = [Wallet(idx = 0)]
      res = self.wallet[0].restore_deterministic_wallet(seed = normal_seed)
      assert res.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'

      self.wallet[0].refresh()

      # Check that we own enough spendable enotes
      res = self.wallet[0].incoming_transfers(transfer_type = 'available')
      assert 'transfers' in res
      num_outs_spendable = 0
      min_out_amount = None
      for t in res.transfers:
          if not t.spent:
            num_outs_spendable += 1
            min_out_amount = min(min_out_amount, t.amount) if min_out_amount is not None else t.amount
      assert num_outs_spendable >= 2 * len(addrs)

      # Transfer to addrs and mine to confirm tx
      dsts = [{'address': addr, 'amount': int(min_out_amount * 0.95)} for addr in addrs]
      res = self.wallet[0].transfer(dsts, get_tx_metadata = True)
      tx_hex = res.tx_metadata
      res = self.wallet[0].relay_tx(tx_hex)
      self.mine('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 10)

    def remake_some_multisig_wallets_by_multsig_seed(self, threshold):
      N = len(self.wallet)
      num_signers_to_remake = random.randint(1, N) # Do at least one
      signers_to_remake = list(range(N))
      random.shuffle(signers_to_remake)
      signers_to_remake = signers_to_remake[:num_signers_to_remake]

      for i in signers_to_remake:
        print("Remaking {}/{} multsig wallet from multisig seed: #{}".format(threshold, N, i+1))

        otherwise_unused_seed = \
          'factual wiggle awakened maul sash biscuit pause reinvest fonts sleepless knowledge tossed jewels request gusts dagger gumball onward dotted amended powder cynical strained topic request'

        # Get information about wallet, will compare against later
        old_viewkey = self.wallet[i].query_key('view_key').key
        old_spendkey = self.wallet[i].query_key('spend_key').key
        old_multisig_seed = self.wallet[i].query_key('mnemonic').key

        # Close old wallet and restore w/ random seed so we know that restoring actually did something
        self.wallet[i].close_wallet()
        self.wallet[i].restore_deterministic_wallet(seed=otherwise_unused_seed)
        mid_viewkey = self.wallet[i].query_key('view_key').key
        assert mid_viewkey != old_viewkey

        # Now restore w/ old multisig seed and check against original
        self.wallet[i].close_wallet()
        self.wallet[i].restore_deterministic_wallet(seed=old_multisig_seed, enable_multisig_experimental=True)
        new_viewkey = self.wallet[i].query_key('view_key').key
        new_spendkey = self.wallet[i].query_key('spend_key').key
        new_multisig_seed = self.wallet[i].query_key('mnemonic').key
        assert new_viewkey == old_viewkey
        assert new_spendkey == old_spendkey
        assert new_multisig_seed == old_multisig_seed

        self.wallet[i].refresh()

    def test_states(self):
        print('Testing multisig states')
        seeds = [
            'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted',
            'peeled mixture ionic radar utopia puddle buying illness nuns gadget river spout cavernous bounced paradise drunk looking cottage jump tequila melting went winter adjust spout',
            'dilute gutter certain antics pamphlet macro enjoy left slid guarded bogeys upload nineteen bomb jubilee enhanced irritate turnip eggs swung jukebox loudly reduce sedan slid',
        ]
        info2of2 = []
        wallet2of2 = [None, None]
        for i in range(2):
            wallet2of2[i] = Wallet(idx = i)
            try: wallet2of2[i].close_wallet()
            except: pass
            res = wallet2of2[i].restore_deterministic_wallet(seed = seeds[i])
            res = wallet2of2[i].is_multisig()
            assert not res.multisig
            res = wallet2of2[i].prepare_multisig(enable_multisig_experimental = True)
            assert len(res.multisig_info) > 0
            info2of2.append(res.multisig_info)

        kex_info = []
        res = wallet2of2[0].make_multisig(info2of2, 2)
        kex_info.append(res.multisig_info)
        res = wallet2of2[1].make_multisig(info2of2, 2)
        kex_info.append(res.multisig_info)
        res = wallet2of2[0].exchange_multisig_keys(kex_info)
        res = wallet2of2[0].is_multisig()
        assert res.multisig
        assert res.ready

        ok = False
        try: res = wallet2of2[0].prepare_multisig(enable_multisig_experimental = True)
        except: ok = True
        assert ok

        ok = False
        try: res = wallet2of2[0].make_multisig(info2of2, 2)
        except: ok = True
        assert ok

        info2of3 = []
        wallet2of3 = [None, None, None]
        for i in range(3):
            wallet2of3[i] = Wallet(idx = i)
            try: wallet2of3[i].close_wallet()
            except: pass
            res = wallet2of3[i].restore_deterministic_wallet(seed = seeds[i])
            res = wallet2of3[i].is_multisig()
            assert not res.multisig
            res = wallet2of3[i].prepare_multisig(enable_multisig_experimental = True)
            assert len(res.multisig_info) > 0
            info2of3.append(res.multisig_info)

        for i in range(3):
            ok = False
            try: res = wallet2of3[i].exchange_multisig_keys(info)
            except: ok = True
            assert ok
            res = wallet2of3[i].is_multisig()
            assert not res.multisig

        res = wallet2of3[1].make_multisig(info2of3, 2)
        res = wallet2of3[1].is_multisig()
        assert res.multisig
        assert not res.ready

        ok = False
        try: res = wallet2of3[1].prepare_multisig(enable_multisig_experimental = True)
        except: ok = True
        assert ok

        ok = False
        try: res = wallet2of3[1].make_multisig(info2of3[0:2], 2)
        except: ok = True
        assert ok

    def import_multisig_info(self, signers, expected_outputs):
        assert len(signers) >= 2

        print('Importing multisig info from ' + str(signers))

        info = []
        for i in signers:
          self.wallet[i].refresh()
          res = self.wallet[i].export_multisig_info()
          assert len(res.info) > 0
          info.append(res.info)
        for i in signers:
          res = self.wallet[i].import_multisig_info(info)
          assert res.n_outputs == expected_outputs

    def transfer(self, signers):
        assert len(signers) >= 1

        daemon = Daemon()

        print("Creating multisig transaction from wallet " + str(signers[0]))

        dst = {'address': '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 'amount': 1000000000000}
        res = self.wallet[signers[0]].transfer([dst])
        assert len(res.tx_hash) == 0 # not known yet
        txid = res.tx_hash
        assert len(res.tx_key) == 32*2
        assert res.amount > 0
        amount = res.amount
        assert res.fee > 0
        fee = res.fee
        assert len(res.tx_blob) == 0
        assert len(res.tx_metadata) == 0
        assert len(res.multisig_txset) > 0
        assert len(res.unsigned_txset) == 0
        multisig_txset = res.multisig_txset

        daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
        for i in range(len(self.wallet)):
          self.wallet[i].refresh()

        for i in range(len(signers[1:])):
          print('Signing multisig transaction with wallet ' + str(signers[i+1]))
          res = self.wallet[signers[i+1]].describe_transfer(multisig_txset = multisig_txset)
          assert len(res.desc) == 1
          desc = res.desc[0]
          assert desc.amount_in >= amount + fee
          assert desc.amount_out == desc.amount_in - fee
          assert desc.ring_size == 16
          assert desc.unlock_time == 0
          assert not 'payment_id' in desc or desc.payment_id in ['', '0000000000000000']
          assert desc.change_amount == desc.amount_in - 1000000000000 - fee
          assert desc.change_address == self.wallet_address
          assert desc.fee == fee
          assert len(desc.recipients) == 1
          rec = desc.recipients[0]
          assert rec.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
          assert rec.amount == 1000000000000

          res = self.wallet[signers[i+1]].sign_multisig(multisig_txset)
          multisig_txset = res.tx_data_hex
          assert len(res.tx_hash_list if 'tx_hash_list' in res else []) == (i == len(signers[1:]) - 1)

          if i < len(signers[1:]) - 1:
            print('Submitting multisig transaction prematurely with wallet ' + str(signers[-1]))
            ok = False
            try: self.wallet[signers[-1]].submit_multisig(multisig_txset)
            except: ok = True
            assert ok

        print('Submitting multisig transaction with wallet ' + str(signers[-1]))
        res = self.wallet[signers[-1]].submit_multisig(multisig_txset)
        assert len(res.tx_hash_list) == 1
        txid = res.tx_hash_list[0]

        for i in range(len(self.wallet)):
          self.wallet[i].refresh()
          res = self.wallet[i].get_transfers()
          assert len([x for x in (res['pending'] if 'pending' in res else []) if x.txid == txid]) == (1 if i == signers[-1] else 0)
          assert len([x for x in (res['out'] if 'out' in res else []) if x.txid == txid]) == 0

        daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
        return txid

    def try_transfer_frozen(self, signers):
        assert len(signers) >= 2

        daemon = Daemon()

        print("Creating multisig transaction from wallet " + str(signers[0]))

        dst = {'address': '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 'amount': 1000000000000}
        res = self.wallet[signers[0]].transfer([dst])
        assert len(res.tx_hash) == 0 # not known yet
        txid = res.tx_hash
        assert len(res.tx_key) == 32*2
        assert res.amount > 0
        amount = res.amount
        assert res.fee > 0
        fee = res.fee
        assert len(res.tx_blob) == 0
        assert len(res.tx_metadata) == 0
        assert len(res.multisig_txset) > 0
        assert len(res.unsigned_txset) == 0
        spent_key_images = res.spent_key_images.key_images
        multisig_txset = res.multisig_txset

        daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
        for i in range(len(self.wallet)):
          self.wallet[i].refresh()

        for i in range(len(signers[1:])):
          # Check that each signer wallet has key image and it is not frozen
          for ki in spent_key_images:
            frozen = self.wallet[signers[i+1]].frozen(ki).frozen
            assert not frozen

        # Freeze key image involved with initiated transfer
        assert len(spent_key_images)
        ki0 = spent_key_images[0]
        print("Freezing involved key image:", ki0)
        self.wallet[signers[1]].freeze(ki0)
        frozen = self.wallet[signers[1]].frozen(ki).frozen
        assert frozen

        # Try signing multisig (this operation should fail b/c of the frozen key image)
        print("Attemping to sign with frozen key image. This should fail")
        try:
          res = self.wallet[signers[1]].sign_multisig(multisig_txset)
          raise ValueError('sign_multisig should not have succeeded w/ frozen enotes')
        except AssertionError:
          pass

        # Thaw key image and continue transfer as normal
        print("Thawing key image and continuing transfer as normal")
        self.wallet[signers[1]].thaw(ki0)
        frozen = self.wallet[signers[1]].frozen(ki).frozen
        assert not frozen

        for i in range(len(signers[1:])):
          print('Signing multisig transaction with wallet ' + str(signers[i+1]))
          res = self.wallet[signers[i+1]].describe_transfer(multisig_txset = multisig_txset)
          assert len(res.desc) == 1
          desc = res.desc[0]
          assert desc.amount_in >= amount + fee
          assert desc.amount_out == desc.amount_in - fee
          assert desc.ring_size == 16
          assert desc.unlock_time == 0
          assert not 'payment_id' in desc or desc.payment_id in ['', '0000000000000000']
          assert desc.change_amount == desc.amount_in - 1000000000000 - fee
          assert desc.change_address == self.wallet_address
          assert desc.fee == fee
          assert len(desc.recipients) == 1
          rec = desc.recipients[0]
          assert rec.address == '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm'
          assert rec.amount == 1000000000000

          res = self.wallet[signers[i+1]].sign_multisig(multisig_txset)
          multisig_txset = res.tx_data_hex
          assert len(res.tx_hash_list if 'tx_hash_list' in res else []) == (i == len(signers[1:]) - 1)

          if i < len(signers[1:]) - 1:
            print('Submitting multisig transaction prematurely with wallet ' + str(signers[-1]))
            ok = False
            try: self.wallet[signers[-1]].submit_multisig(multisig_txset)
            except: ok = True
            assert ok

        print('Submitting multisig transaction with wallet ' + str(signers[-1]))
        res = self.wallet[signers[-1]].submit_multisig(multisig_txset)
        assert len(res.tx_hash_list) == 1
        txid = res.tx_hash_list[0]

        for i in range(len(self.wallet)):
          self.wallet[i].refresh()
          res = self.wallet[i].get_transfers()
          assert len([x for x in (res['pending'] if 'pending' in res else []) if x.txid == txid]) == (1 if i == signers[-1] else 0)
          assert len([x for x in (res['out'] if 'out' in res else []) if x.txid == txid]) == 0

        daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
        return txid

    def check_transaction(self, txid):
        for i in range(len(self.wallet)):
          self.wallet[i].refresh()
          res = self.wallet[i].get_transfers()
          assert len([x for x in (res['pending'] if 'pending' in res else []) if x.txid == txid]) == 0
          assert len([x for x in (res['out'] if 'out' in res else []) if x.txid == txid]) == 1


class Guard:
    def __enter__(self):
        for i in range(4):
            Wallet(idx = i).auto_refresh(False)
    def __exit__(self, exc_type, exc_value, traceback):
        for i in range(4):
            Wallet(idx = i).auto_refresh(True)

if __name__ == '__main__':
    with Guard() as guard:
        MultisigTest().run_test()