diff options
author | jeffro256 <jeffro256@tutanota.com> | 2023-06-22 09:15:12 +0200 |
---|---|---|
committer | jeffro256 <jeffro256@tutanota.com> | 2023-07-28 12:17:14 -0500 |
commit | 45b52de28ecde0d31134f5cb6a1be29524ab2853 (patch) | |
tree | 20abe8ac628afa409f7e7dadce8e9499f21f67a8 /tests/functional_tests/multisig.py | |
parent | Merge pull request #8842 (diff) | |
download | monero-45b52de28ecde0d31134f5cb6a1be29524ab2853.tar.xz |
wallet-rpc: restore from multisig seed
Diffstat (limited to 'tests/functional_tests/multisig.py')
-rwxr-xr-x | tests/functional_tests/multisig.py | 155 |
1 files changed, 146 insertions, 9 deletions
diff --git a/tests/functional_tests/multisig.py b/tests/functional_tests/multisig.py index c6db82d4f..501d59f4e 100755 --- a/tests/functional_tests/multisig.py +++ b/tests/functional_tests/multisig.py @@ -29,6 +29,7 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import print_function +import random """Test multisig transfers """ @@ -36,48 +37,98 @@ from __future__ import print_function from framework.daemon import Daemon from framework.wallet import Wallet +MULTISIG_PUB_ADDRS = [ + '45J58b7PmKJFSiNPFFrTdtfMcFGnruP7V4CMuRpX7NsH4j3jGHKAjo3YJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ3gr7sG', # 2/2 + '44G2TQNfsiURKkvxp7gbgaJY8WynZvANnhmyMAwv6WeEbAvyAWMfKXRhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5duN94i', # 2/3 + '41mro238grj56GnrWkakAKTkBy2yDcXYsUZ2iXCM9pe5Ueajd2RRc6Fhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5ief4ZP', # 3/3 + '44vZSprQKJQRFe6t1VHgU4ESvq2dv7TjBLVGE7QscKxMdFSiyyPCEV64NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6dakeff', # 3/4 + '47puypSwsV1gvUDratmX4y58fSwikXVehEiBhVLxJA1gRCxHyrRgTDr4NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6aRPj5U', # 2/4 + '4A8RnBQixry4VXkqeWhmg8L7vWJVDJj4FN9PV4E7Mgad5ZZ6LKQdn8dYJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ4S8RSB' # 1/2 +] + class MultisigTest(): def run_test(self): self.reset() - self.mine('45J58b7PmKJFSiNPFFrTdtfMcFGnruP7V4CMuRpX7NsH4j3jGHKAjo3YJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ3gr7sG', 5) - self.mine('44G2TQNfsiURKkvxp7gbgaJY8WynZvANnhmyMAwv6WeEbAvyAWMfKXRhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5duN94i', 5) - self.mine('41mro238grj56GnrWkakAKTkBy2yDcXYsUZ2iXCM9pe5Ueajd2RRc6Fhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5ief4ZP', 5) - self.mine('44vZSprQKJQRFe6t1VHgU4ESvq2dv7TjBLVGE7QscKxMdFSiyyPCEV64NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6dakeff', 5) - self.mine('47puypSwsV1gvUDratmX4y58fSwikXVehEiBhVLxJA1gRCxHyrRgTDr4NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6aRPj5U', 5) + for pub_addr in MULTISIG_PUB_ADDRS: + self.mine(pub_addr, 4) self.mine('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 80) self.test_states() + self.fund_addrs_with_normal_wallet(MULTISIG_PUB_ADDRS) + self.create_multisig_wallets(2, 2, '45J58b7PmKJFSiNPFFrTdtfMcFGnruP7V4CMuRpX7NsH4j3jGHKAjo3YJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ3gr7sG') self.import_multisig_info([1, 0], 5) txid = self.transfer([1, 0]) self.import_multisig_info([0, 1], 6) self.check_transaction(txid) + self.remake_some_multisig_wallets_by_multsig_seed(2) + self.import_multisig_info([0, 1], 6) # six outputs, same as before + txid = self.transfer([0, 1]) + self.import_multisig_info([0, 1], 7) # seven outputs b/c we're dest plus change + self.check_transaction(txid) + self.create_multisig_wallets(2, 3, '44G2TQNfsiURKkvxp7gbgaJY8WynZvANnhmyMAwv6WeEbAvyAWMfKXRhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5duN94i') self.import_multisig_info([0, 2], 5) txid = self.transfer([0, 2]) self.import_multisig_info([0, 1, 2], 6) self.check_transaction(txid) + self.remake_some_multisig_wallets_by_multsig_seed(2) + self.import_multisig_info([0, 2], 6) # six outputs, same as before + txid = self.transfer([0, 2]) + self.import_multisig_info([0, 1, 2], 7) # seven outputs b/c we're dest plus change + self.check_transaction(txid) + self.create_multisig_wallets(3, 3, '41mro238grj56GnrWkakAKTkBy2yDcXYsUZ2iXCM9pe5Ueajd2RRc6Fhh3uBXT2UAKhAsUJ7Fg5zjjF2U1iGciFk5ief4ZP') self.import_multisig_info([2, 0, 1], 5) txid = self.transfer([2, 1, 0]) self.import_multisig_info([0, 2, 1], 6) self.check_transaction(txid) + self.remake_some_multisig_wallets_by_multsig_seed(3) + self.import_multisig_info([2, 0, 1], 6) # six outputs, same as before + txid = self.transfer([2, 1, 0]) + self.import_multisig_info([0, 2, 1], 7) # seven outputs b/c we're dest plus change + self.check_transaction(txid) + self.create_multisig_wallets(3, 4, '44vZSprQKJQRFe6t1VHgU4ESvq2dv7TjBLVGE7QscKxMdFSiyyPCEV64NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6dakeff') self.import_multisig_info([0, 2, 3], 5) txid = self.transfer([0, 2, 3]) self.import_multisig_info([0, 1, 2, 3], 6) self.check_transaction(txid) + self.remake_some_multisig_wallets_by_multsig_seed(3) + self.import_multisig_info([0, 2, 3], 6) # six outputs, same as before + txid = self.transfer([0, 2, 3]) + self.import_multisig_info([0, 1, 2, 3], 7) # seven outputs b/c we're dest plus change + self.check_transaction(txid) + self.create_multisig_wallets(2, 4, '47puypSwsV1gvUDratmX4y58fSwikXVehEiBhVLxJA1gRCxHyrRgTDr4NnKUQssFPyWxc2meyt7j63F2S2qtCTRL6aRPj5U') self.import_multisig_info([1, 2], 5) txid = self.transfer([1, 2]) self.import_multisig_info([0, 1, 2, 3], 6) self.check_transaction(txid) + self.remake_some_multisig_wallets_by_multsig_seed(2) + self.import_multisig_info([0, 1, 2, 3], 6) # six outputs, same as before + txid = self.transfer([2, 3]) + self.import_multisig_info([0, 1, 2, 3], 7) # seven outputs b/c we're dest plus change + self.check_transaction(txid) + + self.create_multisig_wallets(1, 2, '4A8RnBQixry4VXkqeWhmg8L7vWJVDJj4FN9PV4E7Mgad5ZZ6LKQdn8dYJP2RePX6HMaSkbvTbrWUFhDNcNcHgtNmQ4S8RSB') + self.import_multisig_info([0, 1], 5) + txid = self.transfer([0]) + self.import_multisig_info([0, 1], 6) + self.check_transaction(txid) + + self.remake_some_multisig_wallets_by_multsig_seed(1) + self.import_multisig_info([0, 1], 6) # six outputs, same as before + txid = self.transfer([1]) + self.import_multisig_info([0, 1], 7) # seven outputs b/c we're dest plus change + self.check_transaction(txid) + def reset(self): print('Resetting blockchain') daemon = Daemon() @@ -90,6 +141,11 @@ class MultisigTest(): 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 = [ @@ -100,6 +156,8 @@ class MultisigTest(): ] 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): @@ -111,10 +169,12 @@ class MultisigTest(): 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): @@ -122,6 +182,7 @@ class MultisigTest(): 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 @@ -129,13 +190,15 @@ class MultisigTest(): assert res.threshold == M_threshold assert res.total == N_total - while True: + # 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 + assert n_ready == 0 or n_ready == N_total # No partial readiness if n_ready == N_total: break info = next_stage @@ -145,10 +208,17 @@ class MultisigTest(): 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 + 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 @@ -156,6 +226,73 @@ class MultisigTest(): 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) + signers_to_remake = set() + num_signers_to_remake = random.randint(1, N) # Do at least one + while len(signers_to_remake) < num_signers_to_remake: + signers_to_remake.add(random.randint(0, N - 1)) + + 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 = [ @@ -248,7 +385,7 @@ class MultisigTest(): assert res.n_outputs == expected_outputs def transfer(self, signers): - assert len(signers) >= 2 + assert len(signers) >= 1 daemon = Daemon() |