mirror of
https://github.com/monero-project/monero.git
synced 2025-12-05 20:40:22 -08:00
- Make sure to mark identified spends in the pool as spends. The wallet might not know these have been spent if it wasn't the wallet that relayed the tx to the daemon, or the wallet was cleared via rescan_bc. - Make sure to add spends to m_unconfirmed_txs if not present. - Make sure to process the entire pool again if refreshing for the first time. The wallet fetches pool and blocks at the same time. The wallet scans blocks first, then pool. If the wallet identifies received outputs in the chain, then it may have spent those received outputs in the pool. So we make sure to re-process the entire pool again after scanning the chain for the first time. - Multisig wallets that know about spent key images can now detect spend txs in the pool. Update tests for that.
688 lines
29 KiB
Python
Executable File
688 lines
29 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Copyright (c) 2019-2024, 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.
|
|
|
|
import json
|
|
import random
|
|
import time
|
|
|
|
"""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)
|
|
|
|
print('Testing in-depth transferring with many different multisig setups')
|
|
|
|
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.wallet = self.create_multisig_wallets(M, N, pub_addr)
|
|
self.wallet_address = 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)
|
|
|
|
@classmethod
|
|
def reset(cls):
|
|
print('Resetting blockchain')
|
|
daemon = Daemon()
|
|
res = daemon.get_height()
|
|
daemon.pop_blocks(res.height - 1)
|
|
daemon.flush_txpool()
|
|
|
|
@classmethod
|
|
def mine(cls, 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()
|
|
@classmethod
|
|
def create_multisig_wallets(cls, 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()
|
|
wallet = [None] * N_total
|
|
info = []
|
|
for i in range(N_total):
|
|
wallet[i] = Wallet(idx = i)
|
|
try: wallet[i].close_wallet()
|
|
except: pass
|
|
res = wallet[i].restore_deterministic_wallet(seed = seeds[i])
|
|
res = 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 = 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 = 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 = 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 = 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 = 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]
|
|
wallet_address = expected_address
|
|
|
|
# Assert multisig paramaters M/N and "ready" for each wallet
|
|
for i in range(N_total):
|
|
res = wallet[i].is_multisig()
|
|
assert res.multisig == True
|
|
assert res.ready == True
|
|
assert res.threshold == M_threshold
|
|
assert res.total == N_total
|
|
|
|
return wallet
|
|
|
|
# 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()
|
|
|
|
@classmethod
|
|
def test_states(cls):
|
|
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]
|
|
|
|
# Retrieve spent key images from daemon
|
|
res = daemon.get_transactions([txid], decode_as_json = True)
|
|
assert len(res.txs) == 1
|
|
tx = res.txs[0]
|
|
assert tx.tx_hash == txid
|
|
assert len(tx.as_json) > 0
|
|
try:
|
|
j = json.loads(tx.as_json)
|
|
except:
|
|
j = None
|
|
assert j
|
|
assert len(j['vin']) >= 1
|
|
spent_key_images = [vin['key']['k_image'] for vin in j['vin']]
|
|
assert len(spent_key_images) == len(j['vin'])
|
|
|
|
for i in range(len(self.wallet)):
|
|
# Check if the wallet knows about any spent key images (all signers *should*, non-signers *might*)
|
|
is_a_signer = len([x for x in signers if x == i]) > 0
|
|
knows_key_image = False
|
|
for ki in spent_key_images:
|
|
try:
|
|
res = self.wallet[i].frozen(ki)
|
|
knows_key_image = True
|
|
except AssertionError:
|
|
if is_a_signer:
|
|
raise ValueError('Signer should know about spent key image')
|
|
pass
|
|
self.wallet[i].refresh()
|
|
res = self.wallet[i].get_transfers()
|
|
# Any wallet that knows about any spent key images should be able to detect the spend in the pool
|
|
assert len([x for x in (res['pending'] if 'pending' in res else []) if x.txid == txid]) == (1 if knows_key_image 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)):
|
|
# Make sure wallet knows about the key image
|
|
frozen = self.wallet[i].frozen(ki).frozen
|
|
assert not frozen
|
|
self.wallet[i].refresh()
|
|
res = self.wallet[i].get_transfers()
|
|
# Since all wallets should have key image, all wallets should be able to detect the spend in the pool
|
|
assert len([x for x in (res['pending'] if 'pending' in res else []) if x.txid == txid]) == 1
|
|
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 MultisigImportTempRefreshFailTest():
|
|
def run_test(self):
|
|
m, n, addr_2_2 = TEST_CASES[0]
|
|
assert(m == 2)
|
|
assert(n == 2)
|
|
|
|
NUM_BLOCKS_TO_MINE = 80
|
|
MultisigTest.reset()
|
|
wallets = MultisigTest.create_multisig_wallets(m, n, addr_2_2)
|
|
MultisigTest.mine(addr_2_2, NUM_BLOCKS_TO_MINE)
|
|
|
|
print('Testing whether temporary failures in refreshing caused permanent failures for partial key image calculation')
|
|
|
|
# Export multisig info
|
|
ms_info = []
|
|
for wallet in wallets:
|
|
wallet.refresh()
|
|
res = wallet.export_multisig_info()
|
|
assert len(res.info) > 0
|
|
ms_info.append(res.info)
|
|
|
|
# Import multisig info to wallet 0
|
|
res = wallets[0].import_multisig_info(ms_info)
|
|
assert res.n_outputs == NUM_BLOCKS_TO_MINE
|
|
|
|
# Simulate daemon refresh failure by setting daemon to invalid URL and import multisig info to wallet 1
|
|
with WrongDaemonGuard(1) as wdguard:
|
|
try:
|
|
wallets[1].import_multisig_info(ms_info)
|
|
except:
|
|
pass
|
|
|
|
# Refresh wallet 1 again
|
|
wallets[1].refresh()
|
|
|
|
# Check unlocked balance
|
|
unlocked_balance = None
|
|
for wallet in wallets:
|
|
res = wallet.get_balance()
|
|
assert res.unlocked_balance > 0
|
|
assert unlocked_balance is None or unlocked_balance == res.unlocked_balance
|
|
unlocked_balance = res.unlocked_balance
|
|
|
|
# Construct outgoing transfer
|
|
dst = {'address': '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 'amount': unlocked_balance // 2}
|
|
res = wallets[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
|
|
|
|
# For later, assert we have 0 outgoing txs at this moment
|
|
for wallet in wallets:
|
|
res = wallet.get_transfers()
|
|
assert 'out' not in res or not res.out
|
|
|
|
# Try signing outgoing transfer w/ wallet 1 (this is where it will fail pre-PR#9863)
|
|
res = wallets[1].sign_multisig(multisig_txset)
|
|
multisig_txset = res.tx_data_hex
|
|
assert len(res.tx_hash_list if 'tx_hash_list' in res else []) == 1
|
|
|
|
# Submit the transaction for good measure and wait for all wallets to catch up
|
|
res = wallets[1].submit_multisig(multisig_txset)
|
|
assert len(res.tx_hash_list) == 1
|
|
txid = res.tx_hash_list[0]
|
|
|
|
MultisigTest.mine('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 1)
|
|
|
|
timeout = 15
|
|
wait_cutoff = time.monotonic() + timeout
|
|
synced_outgoing_tx = False
|
|
while True:
|
|
synced_outgoing_tx = True
|
|
for wallet in wallets:
|
|
wallet.refresh()
|
|
res = wallet.get_transfers()
|
|
if 'out' not in res or len(res.out) < 1:
|
|
synced_outgoing_tx = False
|
|
break
|
|
if synced_outgoing_tx:
|
|
break
|
|
max_delay = wait_cutoff - time.monotonic()
|
|
if max_delay <= 0:
|
|
break
|
|
time.sleep(min(.2, max_delay))
|
|
assert synced_outgoing_tx
|
|
|
|
|
|
class AutoRefreshGuard:
|
|
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)
|
|
|
|
class WrongDaemonGuard:
|
|
def __init__(self, idx, correct_port=18180):
|
|
self.idx = idx
|
|
self.correct_port = correct_port
|
|
def __enter__(self):
|
|
Wallet(idx = self.idx).set_daemon("localhost:0")
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
Wallet(idx = self.idx).set_daemon("localhost:" + str(self.correct_port))
|
|
|
|
if __name__ == '__main__':
|
|
with AutoRefreshGuard() as arguard:
|
|
print('$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$')
|
|
MultisigImportTempRefreshFailTest().run_test()
|
|
print('$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$')
|
|
MultisigTest().run_test()
|
|
print('$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$')
|
|
|