Merge pull request #8914

45b52de wallet-rpc: restore from multisig seed (jeffro256)
This commit is contained in:
luigi1111 2023-08-17 10:20:20 -05:00
commit 3b67d5fc5b
No known key found for this signature in database
GPG key ID: F4ACA0183641E010
7 changed files with 217 additions and 35 deletions

View file

@ -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,42 +37,74 @@ 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])
@ -81,6 +114,24 @@ class MultisigTest():
self.import_multisig_info([0, 1, 2, 3], 7)
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()
@ -93,6 +144,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 = [
@ -103,6 +159,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):
@ -114,10 +172,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):
@ -125,6 +185,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
@ -132,13 +193,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
@ -148,10 +211,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
@ -159,6 +229,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 = [
@ -251,7 +388,7 @@ class MultisigTest():
assert res.n_outputs == expected_outputs
def transfer(self, signers):
assert len(signers) >= 2
assert len(signers) >= 1
daemon = Daemon()