123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566 |
- #!/usr/bin/env python3
- #
- # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
- # Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- """
- ts_xmrwallet.py: xmrwallet tests for the test.py test suite
- """
- import sys,os,atexit,asyncio
- from subprocess import run,PIPE
- from mmgen.globalvars import g
- from mmgen.opts import opt
- from mmgen.obj import MMGenRange,XMRAmt
- from mmgen.addr import KeyAddrList,AddrIdxList
- from ..include.common import *
- from .common import *
- from .ts_base import *
- class TestSuiteXMRWallet(TestSuiteBase):
- """
- Monero wallet operations
- """
- networks = ('xmr',)
- passthru_opts = ('coin',)
- tmpdir_nums = [29]
- dfl_random_txs = 3
- cmd_group = (
- ('gen_kafiles', 'generating key-address files'),
- ('create_wallets', 'creating Monero wallets'),
- ('set_dest_miner', 'opening miner wallet'),
- ('mine_blocks', 'mining blocks'),
- ('fund_alice', 'sending funds'),
- ('mine_blocks_tx', 'mining blocks'),
- ('sync_wallets', 'syncing all wallets'),
- ('sync_wallets_selected', 'syncing selected wallets'),
- ('sweep_to_address_proxy', 'sweeping to new address (via TX relay + proxy)'),
- ('mine_blocks', 'mining blocks'),
- ('sweep_to_account', 'sweeping to new account'),
- ('mine_blocks', 'mining blocks'),
- ('sweep_to_address_noproxy', 'sweeping to new address (via TX relay)'),
- ('mine_blocks', 'mining blocks'),
- )
- def __init__(self,trunner,cfgs,spawn):
- TestSuiteBase.__init__(self,trunner,cfgs,spawn)
- if trunner == None:
- return
- from mmgen.protocol import init_proto
- self.proto = init_proto('XMR',network='testnet')
- self.datadir_base = os.path.join('test','daemons','xmrtest')
- self.long_opts = ['--testnet=1', '--monero-wallet-rpc-password=passw0rd']
- self.init_users()
- self.init_daemon_args()
- for v in self.users.values():
- run(['mkdir','-p',v.udir])
- self.init_proxy()
- if not opt.no_daemon_autostart:
- self.start_daemons()
- self.start_wallet_daemons()
- if not opt.no_daemon_stop:
- atexit.register(self.stop_daemons)
- atexit.register(self.stop_wallet_daemons)
- self.balance = None
- # init methods
- def init_proxy(self):
- def port_in_use(port):
- import socket
- try: socket.create_connection(('localhost',port)).close()
- except: return False
- else: return True
- def start_proxy():
- if not opt.no_daemon_autostart:
- run(a+b2)
- omsg(f'SSH SOCKS server started, listening at localhost:{self.socks_port}')
- def kill_proxy():
- omsg(f'Killing SSH SOCKS server at localhost:{self.socks_port}')
- cmd = [ 'pkill', '-f', ' '.join(a + b2) ]
- run(cmd)
- self.use_proxy = False
- self.socks_port = 9060
- a = ['ssh','-x','-o','ExitOnForwardFailure=True','-D',f'localhost:{self.socks_port}']
- b0 = ['-o','PasswordAuthentication=False']
- b1 = ['localhost','true']
- b2 = ['-fN','-E','txrelay-proxy.debug','localhost']
- if port_in_use(self.socks_port):
- omsg(f'Port {self.socks_port} already in use. Assuming SSH SOCKS server is running')
- self.use_proxy = True
- else:
- cp = run(a+b0+b1,stdout=PIPE,stderr=PIPE)
- err = cp.stderr.decode()
- if err:
- omsg(err)
- if cp.returncode == 0:
- start_proxy()
- self.use_proxy = True
- elif 'onnection refused' in err:
- die(2,fmt("""
- The SSH daemon must be running and listening on localhost in order to test
- XMR TX relaying via SOCKS proxy. If sshd is not running, please start it.
- Otherwise, add the line 'ListenAddress 127.0.0.1' to your sshd_config, and
- then restart the daemon.
- """,indent=' '))
- elif 'ermission denied' in err:
- msg(fmt(f"""
- In order to test XMR TX relaying via SOCKS proxy, it’s desirable to enable
- SSH to localhost without a password, which is not currently supported by
- your configuration. Your possible courses of action:
- 1. Continue by answering 'y' at this prompt, and enter your system password
- at the following prompt;
- 2. Exit the test here, add your user SSH public key to your user
- 'authorized_keys' file, and restart the test; or
- 3. Exit the test here, start the SSH SOCKS proxy manually by entering the
- following command, and restart the test:
- {' '.join(a+b2)}
- """,indent=' ',strip_char='\t'))
- if keypress_confirm('Continue?'):
- start_proxy()
- self.use_proxy = True
- else:
- die(1,'Exiting at user request')
- else:
- die(2,fmt(f"""
- Please start the SSH SOCKS proxy by entering the following command:
- {' '.join(a+b2)}
- Then restart the test.
- """,indent=' '))
- if not opt.no_daemon_stop:
- atexit.register(kill_proxy)
- def init_users(self):
- from mmgen.daemon import CoinDaemon,MoneroWalletDaemon
- from mmgen.rpc import MoneroRPCClient,MoneroRPCClientRaw,MoneroWalletRPCClient
- self.users = {}
- n = self.tmpdir_nums[0]
- ud = namedtuple('user_data',[
- 'sid',
- 'mmwords',
- 'udir',
- 'datadir',
- 'kal_range',
- 'kafile',
- 'walletfile_fs',
- 'addrfile_fs',
- 'md',
- 'md_rpc',
- 'md_json_rpc',
- 'wd',
- 'wd_rpc',
- ])
- for user,sid,shift,kal_range in ( # kal_range must be None, a single digit, or a single hyphenated range
- ('miner', '98831F3A', 130, '1'),
- ('bob', '1378FC64', 140, None),
- ('alice', 'FE3C6545', 150, '1-4'),
- ):
- udir = os.path.join('test',f'tmp{n}',user)
- datadir = os.path.join(self.datadir_base,user)
- md = CoinDaemon(
- proto = self.proto,
- test_suite = True,
- port_shift = shift,
- opts = ['online'],
- datadir = datadir
- )
- md_rpc = MoneroRPCClientRaw(
- host = md.host,
- port = md.rpc_port,
- user = None,
- passwd = None,
- test_connection = False,
- )
- md_json_rpc = MoneroRPCClient(
- host = md.host,
- port = md.rpc_port,
- user = None,
- passwd = None,
- test_connection = False,
- )
- wd = MoneroWalletDaemon(
- user = 'foo',
- passwd = 'bar',
- wallet_dir = udir,
- test_suite = True,
- port_shift = shift,
- datadir = os.path.join('test','daemons'),
- daemon_addr = f'127.0.0.1:{md.rpc_port}',
- testnet = True
- )
- wd_rpc = MoneroWalletRPCClient(
- host = wd.host,
- port = wd.rpc_port,
- user = wd.user,
- passwd = wd.passwd,
- test_connection = False,
- )
- self.users[user] = ud(
- sid = sid,
- mmwords = f'test/ref/{sid}.mmwords',
- udir = udir,
- datadir = datadir,
- kal_range = kal_range,
- kafile = f'{udir}/{sid}-XMR-M[{kal_range}].testnet.akeys',
- walletfile_fs = f'{udir}/{sid}-{{}}-MoneroWallet.testnet',
- addrfile_fs = f'{udir}/{sid}-{{}}-MoneroWallet.testnet.address.txt',
- md = md,
- md_rpc = md_rpc,
- md_json_rpc = md_json_rpc,
- wd = wd,
- wd_rpc = wd_rpc,
- )
- def init_daemon_args(self):
- common_args = ['--p2p-bind-ip=127.0.0.1','--fixed-difficulty=1'] # ,'--rpc-ssl-allow-any-cert']
- for u in self.users:
- other_ports = [self.users[u2].md.p2p_port for u2 in self.users if u2 != u]
- node_args = [f'--add-exclusive-node=127.0.0.1:{p}' for p in other_ports]
- self.users[u].md.usr_coind_args = common_args + node_args
- # cmd_group methods
- def gen_kafiles(self):
- for user,data in self.users.items():
- if not data.kal_range:
- continue
- run(['mkdir','-p',data.udir])
- run(f'rm -f {data.kafile}',shell=True)
- t = self.spawn(
- 'mmgen-keygen', [
- '--testnet=1','-q', '--accept-defaults', '--coin=xmr',
- f'--outdir={data.udir}', data.mmwords, data.kal_range
- ],
- extra_desc = f'({capfirst(user)})' )
- t.read()
- t.ok()
- t.skip_ok = True
- return t
- def create_wallets(self):
- for user,data in self.users.items():
- if not data.kal_range:
- continue
- run('rm -f {}*'.format( data.walletfile_fs.format('*') ),shell=True)
- dir_opt = [f'--outdir={data.udir}']
- t = self.spawn(
- 'mmgen-xmrwallet',
- self.long_opts + dir_opt + [ 'create', data.kafile, data.kal_range ],
- extra_desc = f'({capfirst(user)})' )
- t.expect('Check key-to-address validity? (y/N): ','n')
- for i in MMGenRange(data.kal_range).items:
- t.expect('Address: ')
- t.read()
- t.ok()
- t.skip_ok = True
- return t
- async def set_dest_miner(self):
- self.do_msg()
- self.set_dest('miner',1,0,lambda x: x > 20,'unlocked balance > 20')
- await self.open_wallet_user('miner',1)
- return 'ok'
- async def fund_alice(self):
- self.do_msg()
- await self.transfer(
- 'miner',
- 1234567891234,
- read_from_file(self.users['alice'].addrfile_fs.format(1)),
- )
- self.set_dest('alice',1,0,lambda x: x > 1,'unlocked balance > 1')
- return 'ok'
- def sync_wallets_selected(self):
- return self.sync_wallets(wallets='1,3-4')
- def sync_wallets(self,wallets=None):
- data = self.users['alice']
- dir_opt = [f'--outdir={data.udir}']
- cmd_opts = [f'--daemon=localhost:{data.md.rpc_port}']
- t = self.spawn(
- 'mmgen-xmrwallet',
- self.long_opts + dir_opt + cmd_opts + [ 'sync', data.kafile ] + ([wallets] if wallets else []) )
- t.expect('Check key-to-address validity? (y/N): ','n')
- wlist = AddrIdxList(wallets) if wallets else MMGenRange(data.kal_range).items
- for n,wnum in enumerate(wlist):
- t.expect('Syncing wallet {}/{} ({})'.format(
- n+1,
- len(wlist),
- os.path.basename(data.walletfile_fs.format(wnum)),
- ))
- t.expect('Chain height: ')
- t.expect('Wallet height: ')
- t.expect('Balance: ')
- t.read()
- return t
- def _sweep_user(self,user,spec,tx_relay_daemon=None):
- data = self.users[user]
- dir_opt = [f'--outdir={data.udir}']
- cmd_opts = list_gen(
- [f'--daemon=localhost:{data.md.rpc_port}'],
- [f'--tx-relay-daemon={tx_relay_daemon}', tx_relay_daemon]
- )
- t = self.spawn(
- 'mmgen-xmrwallet',
- self.long_opts + dir_opt + cmd_opts + [ 'sweep', data.kafile, spec ],
- extra_desc = f'({capfirst(user)})' )
- t.expect('Check key-to-address validity? (y/N): ','n')
- t.expect(
- 'Create new {} .* \(y/N\): '.format('account' if ',' in spec else 'address'),
- 'y', regex=True )
- t.expect('Relay sweep transaction? (y/N): ','y')
- t.read()
- return t
- def sweep_to_address_proxy(self):
- ret = self._sweep_user(
- 'alice',
- '1:0',
- tx_relay_daemon = 'localhost:{}:127.0.0.1:{}'.format( # proxy must be IP, not 'localhost'
- self.users['bob'].md.rpc_port,
- self.socks_port
- ) if self.use_proxy else None
- )
- self.set_dest('alice',1,0,lambda x: x > 1,'unlocked balance > 1')
- return ret
- def sweep_to_account(self):
- ret = self._sweep_user('alice','1:0,2')
- self.set_dest('alice',2,1,lambda x: x > 1,'unlocked balance > 1')
- return ret
- def sweep_to_address_noproxy(self):
- ret = self._sweep_user(
- 'alice',
- '2:1',
- tx_relay_daemon = 'localhost:{}'.format(self.users['bob'].md.rpc_port)
- )
- self.set_dest('alice',2,1,lambda x: x > 1,'unlocked balance > 1')
- return ret
- # wallet methods
- async def open_wallet_user(self,user,wnum):
- data = self.users[user]
- silence()
- kal = KeyAddrList(self.proto,data.kafile,skip_key_address_validity_check=True)
- end_silence()
- return await data.wd_rpc.call(
- 'open_wallet',
- filename = os.path.basename(data.walletfile_fs.format(wnum)),
- password = kal.entry(wnum).wallet_passwd )
- async def close_wallet_user(self,user):
- ret = await self.users[user].wd_rpc.call('close_wallet')
- return 'ok'
- # mining methods
- async def start_mining(self):
- data = self.users['miner']
- addr = read_from_file(data.addrfile_fs.format(1)) # mine to wallet #1, account 0
- for i in range(20):
- ret = await data.md_rpc.call(
- 'start_mining',
- do_background_mining = False, # run mining in background or foreground
- ignore_battery = True, # ignore battery state (on laptop)
- miner_address = addr, # account address to mine to
- threads_count = 3 ) # number of mining threads to run
- status = self.get_status(ret)
- if status == 'OK':
- return True
- elif status == 'BUSY':
- await asyncio.sleep(5)
- omsg('Daemon busy. Attempting to start mining...')
- else:
- die(2,f'Monerod returned status {status}')
- else:
- die(2,'Max retries exceeded')
- async def stop_mining(self):
- ret = await self.users['miner'].md_rpc.call('stop_mining')
- return self.get_status(ret)
- async def mine_blocks(self,random_txs=None):
- """
- - open destination wallet
- - optionally create and broadcast random TXs
- - start mining
- - mine until funds appear in wallet
- - stop mining
- - close wallet
- """
- async def get_height():
- u = self.users['miner']
- for i in range(20):
- try:
- return (await u.md_json_rpc.call('get_last_block_header'))['block_header']['height']
- except Exception as e:
- if 'onnection refused' in str(e):
- omsg(f'{e}\nMonerod appears to have crashed. Attempting to restart...')
- await asyncio.sleep(5)
- u.md.restart()
- await asyncio.sleep(5)
- await self.start_mining()
- else:
- raise
- else:
- die(2,'Restart attempt limit exceeded')
- async def send_random_txs():
- from mmgen.tool import tool_api
- t = tool_api()
- t.init_coin('XMR','testnet')
- t.usr_randchars = 0
- imsg_r(f'Sending random transactions: ')
- for i in range(random_txs):
- await self.transfer(
- 'miner',
- 123456789,
- t.randpair()[1],
- )
- imsg_r(f'{i+1} ')
- oqmsg_r('+')
- await asyncio.sleep(0.5)
- imsg('')
- def print_balance(dest,ub):
- imsg('Total balance in {}’s wallet #{}, account {}: {}'.format(
- capfirst(dest.user),
- dest.wnum,
- dest.account,
- ub.hl()
- ))
- async def get_balance(dest):
- data = self.users[dest.user]
- await data.wd_rpc.call('refresh')
- ret = await data.wd_rpc.call('get_accounts')
- return XMRAmt(ret['subaddress_accounts'][dest.account]['unlocked_balance'],from_unit='atomic')
- self.do_msg(extra_desc=f'+{random_txs} random TXs' if random_txs else None)
- if self.dest.user != 'miner':
- await self.open_wallet_user(self.dest.user,self.dest.wnum)
- if random_txs:
- await send_random_txs()
- await self.start_mining()
- h = await get_height()
- imsg_r(f'Chain height: {h} ')
- while True:
- ub = await get_balance(self.dest)
- if self.dest.test(ub):
- imsg('')
- oqmsg_r('+')
- print_balance(self.dest,ub)
- break
- # else:
- # imsg(f'Test {self.dest.test_desc!r} failed')
- await asyncio.sleep(2)
- h = await get_height()
- imsg_r(f'{h} ')
- oqmsg_r('+')
- await self.stop_mining()
- if self.dest.user != 'miner':
- await self.close_wallet_user(self.dest.user)
- return 'ok'
- async def mine_blocks_tx(self):
- return await self.mine_blocks(random_txs=self.dfl_random_txs)
- # util methods
- def get_status(self,ret):
- if ret['status'] != 'OK':
- imsg( 'RPC status: {}'.format(ret['status']) )
- return ret['status']
- def do_msg(self,extra_desc=None):
- self.spawn(
- '',
- msg_only = True,
- extra_desc = f'({extra_desc})' if extra_desc else None
- )
- def set_dest(self,user,wnum,account,test,test_desc):
- self.dest = namedtuple(
- 'dest_info',['user','wnum','account','test','test_desc'])(user,wnum,account,test,test_desc)
- async def transfer(self,user,amt,addr):
- return await self.users[user].wd_rpc.call('transfer',destinations=[{'amount':amt,'address':addr}])
- # daemon start/stop methods
- def start_daemons(self):
- self.stop_daemons()
- for v in self.users.values():
- run(['mkdir','-p',v.datadir])
- v.md.start()
- def stop_daemons(self):
- for v in self.users.values():
- if v.md.state != 'stopped':
- v.md.stop()
- run(['rm','-rf',self.datadir_base])
- def start_wallet_daemons(self):
- for v in self.users.values():
- if v.kal_range:
- v.wd.start()
- def stop_wallet_daemons(self):
- for v in self.users.values():
- if v.kal_range and v.wd.state != 'stopped':
- v.wd.stop()
|