ct_xmr_autosign.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. test.cmdtest_py_d.ct_xmr_autosign: xmr autosigning tests for the cmdtest.py test suite
  12. """
  13. import os,time,re,shutil
  14. from pathlib import Path
  15. from mmgen.color import yellow,purple,gray
  16. from mmgen.util import fmt,async_run
  17. from ..include.common import (
  18. cfg,
  19. oqmsg,
  20. oqmsg_r,
  21. imsg,
  22. silence,
  23. end_silence
  24. )
  25. from .common import get_file_with_ext
  26. from .ct_xmrwallet import CmdTestXMRWallet
  27. from .ct_autosign import CmdTestAutosignBase
  28. def make_burn_addr():
  29. from mmgen.tool.coin import tool_cmd
  30. return tool_cmd(
  31. cfg = cfg,
  32. cmdname = 'privhex2addr',
  33. proto = cfg._proto,
  34. mmtype = 'monero' ).privhex2addr('beadcafe'*8)
  35. class CmdTestXMRAutosign(CmdTestXMRWallet,CmdTestAutosignBase):
  36. """
  37. Monero autosigning operations
  38. """
  39. tmpdir_nums = [39]
  40. # ct_xmrwallet attrs:
  41. user_data = (
  42. ('miner', '98831F3A', False, 130, '1', []),
  43. ('alice', 'FE3C6545', True, 150, '1-2', []),
  44. )
  45. # ct_autosign attrs:
  46. coins = ['xmr']
  47. daemon_coins = []
  48. txfile_coins = []
  49. live = False
  50. simulate_led = False
  51. bad_tx_count = 0
  52. tx_relay_user = 'miner'
  53. no_insert_check = False
  54. win_skip = True
  55. have_online = True
  56. cmd_group = (
  57. ('daemon_version', 'checking daemon version'),
  58. ('create_tmp_wallets', 'creating temporary online wallets for Alice'),
  59. ('new_account_alice', 'adding an account to Alice’s tmp wallet'),
  60. ('new_address_alice', 'adding an address to Alice’s tmp wallet'),
  61. ('new_address_alice_label', 'adding an address to Alice’s tmp wallet (with label)'),
  62. ('dump_tmp_wallets', 'dumping Alice’s tmp wallets'),
  63. ('delete_tmp_wallets', 'deleting Alice’s tmp wallets'),
  64. ('autosign_clean', 'cleaning signable file directories'),
  65. ('autosign_setup', 'autosign setup with Alice’s seed'),
  66. ('create_watchonly_wallets', 'creating online (watch-only) wallets for Alice'),
  67. ('delete_tmp_dump_files', 'deleting Alice’s dump files'),
  68. ('gen_kafiles', 'generating key-address files for Miner'),
  69. ('create_wallets_miner', 'creating Monero wallets for Miner'),
  70. ('mine_initial_coins', 'mining initial coins'),
  71. ('fund_alice1', 'sending funds to Alice (wallet #1)'),
  72. ('fund_alice2', 'sending funds to Alice (wallet #2)'),
  73. ('autosign_start_thread', 'starting autosign wait loop'),
  74. ('create_transfer_tx1', 'creating a transfer TX'),
  75. ('submit_transfer_tx1', 'submitting the transfer TX'),
  76. ('resubmit_transfer_tx1', 'resubmitting the transfer TX'),
  77. ('export_outputs1', 'exporting outputs from Alice’s watch-only wallet #1'),
  78. ('import_key_images1', 'importing signed key images into Alice’s online wallets'),
  79. ('sync_chkbal1', 'syncing Alice’s wallet #1'),
  80. ('create_transfer_tx2', 'creating a transfer TX (for relaying via proxy)'),
  81. ('submit_transfer_tx2', 'submitting the transfer TX (relaying via proxy)'),
  82. ('sync_chkbal2', 'syncing Alice’s wallets and checking balance'),
  83. ('dump_wallets', 'dumping Alice’s wallets'),
  84. ('delete_wallets', 'deleting Alice’s wallets'),
  85. ('restore_wallets', 'creating online (watch-only) wallets for Alice'),
  86. ('delete_dump_files', 'deleting Alice’s dump files'),
  87. ('export_outputs2', 'exporting outputs from Alice’s watch-only wallets'),
  88. ('import_key_images2', 'importing signed key images into Alice’s online wallets'),
  89. ('sync_chkbal3', 'syncing Alice’s wallets and checking balance'),
  90. ('txlist', 'listing Alice’s submitted transactions'),
  91. ('check_tx_dirs', 'cleaning and checking signable file directories'),
  92. ('autosign_kill_thread', 'stopping autosign wait loop'),
  93. ('stop_daemons', 'stopping all wallet and coin daemons'),
  94. ('view', 'viewing Alice’s wallet in offline mode (wallet #1)'),
  95. ('listview', 'list-viewing Alice’s wallet in offline mode (wallet #2)'),
  96. )
  97. def __init__(self,trunner,cfgs,spawn):
  98. CmdTestAutosignBase.__init__(self,trunner,cfgs,spawn)
  99. CmdTestXMRWallet.__init__(self,trunner,cfgs,spawn)
  100. if trunner is None:
  101. return
  102. from mmgen.cfg import Config
  103. self.cfg = Config({
  104. 'coin': 'XMR',
  105. 'outdir': self.users['alice'].udir,
  106. 'wallet_dir': self.users['alice'].udir,
  107. 'wallet_rpc_password': 'passwOrd',
  108. 'test_suite': True,
  109. })
  110. self.burn_addr = make_burn_addr()
  111. self.opts.append('--xmrwallets={}'.format(self.users['alice'].kal_range)) # mmgen-autosign opts
  112. self.autosign_opts = ['--autosign'] # mmgen-xmrwallet opts
  113. self.tx_count = 1
  114. self.spawn_env['MMGEN_TEST_SUITE_XMR_AUTOSIGN'] = '1'
  115. def create_tmp_wallets(self):
  116. self.spawn('',msg_only=True)
  117. data = self.users['alice']
  118. from mmgen.wallet import Wallet
  119. from mmgen.xmrwallet import MoneroWalletOps,xmrwallet_uargs
  120. from mmgen.addrlist import KeyAddrList
  121. silence()
  122. kal = KeyAddrList(
  123. cfg = self.cfg,
  124. proto = self.proto,
  125. addr_idxs = '1-2',
  126. seed = Wallet(cfg,data.mmwords).seed,
  127. skip_chksum_msg = True,
  128. key_address_validity_check = False )
  129. kal.file.write(ask_overwrite=False)
  130. fn = get_file_with_ext(data.udir,'akeys')
  131. m = MoneroWalletOps.create(
  132. self.cfg,
  133. xmrwallet_uargs(fn, '1-2', None))
  134. async_run(m.main())
  135. async_run(m.stop_wallet_daemon())
  136. end_silence()
  137. return 'ok'
  138. def _new_addr_alice(self,*args):
  139. data = self.users['alice']
  140. return self.new_addr_alice(
  141. *args,
  142. kafile = get_file_with_ext(data.udir,'akeys') )
  143. def new_account_alice(self):
  144. return self._new_addr_alice(
  145. '2',
  146. 'start',
  147. r'Creating new account for wallet .*2.* with label .*‘xmrwallet new account .*y/N\): ')
  148. def new_address_alice(self):
  149. return self._new_addr_alice(
  150. '2:1',
  151. 'continue',
  152. r'Creating new address for wallet .*2.*, account .*#1.* with label .*‘xmrwallet new address .*y/N\): ')
  153. def new_address_alice_label(self):
  154. return self._new_addr_alice(
  155. '2:1,Alice’s new address',
  156. 'stop',
  157. r'Creating new address for wallet .*2.*, account .*#1.* with label .*‘Alice’s new address .*y/N\): ')
  158. def dump_tmp_wallets(self):
  159. return self._dump_wallets(autosign=False)
  160. def dump_wallets(self):
  161. return self._dump_wallets(autosign=True)
  162. def _dump_wallets(self,autosign):
  163. data = self.users['alice']
  164. self.insert_device_ts()
  165. t = self.spawn(
  166. 'mmgen-xmrwallet',
  167. self.extra_opts
  168. + [f'--wallet-dir={data.udir}', f'--daemon=localhost:{data.md.rpc_port}']
  169. + (self.autosign_opts if autosign else [])
  170. + ['dump']
  171. + ([] if autosign else [get_file_with_ext(data.udir,'akeys')]) )
  172. t.expect('2 wallets dumped')
  173. self.remove_device_ts()
  174. return t
  175. def _delete_files(self,*ext_list):
  176. data = self.users['alice']
  177. self.spawn('',msg_only=True)
  178. for ext in ext_list:
  179. get_file_with_ext(data.udir,ext,no_dot=True,delete_all=True)
  180. return 'ok'
  181. def delete_tmp_wallets(self):
  182. return self._delete_files( 'MoneroWallet', 'MoneroWallet.keys', '.akeys' )
  183. def delete_wallets(self):
  184. return self._delete_files( 'MoneroWatchOnlyWallet', '.keys', '.address.txt' )
  185. def delete_tmp_dump_files(self):
  186. return self._delete_files( '.dump' )
  187. def delete_dump_files(self):
  188. return self._delete_files( '.dump' )
  189. def fund_alice1(self):
  190. return self.fund_alice(wallet=1,check_bal=False)
  191. def fund_alice2(self):
  192. return self.fund_alice(wallet=2)
  193. def autosign_setup(self):
  194. self.do_mount_online(no_xmr_chk=True)
  195. self.asi_online.xmr_dir.mkdir(exist_ok=True)
  196. (self.asi_online.xmr_dir / 'old.vkeys').touch()
  197. self.do_umount_online()
  198. self.insert_device()
  199. t = self.run_setup(
  200. mn_type = 'mmgen',
  201. mn_file = self.users['alice'].mmwords,
  202. use_dfl_wallet = None )
  203. t.expect('Continue with Monero setup? (Y/n): ','y')
  204. t.written_to_file('View keys')
  205. self.remove_device()
  206. return t
  207. def autosign_start_thread(self):
  208. def run():
  209. t = self.spawn('mmgen-autosign', self.opts + ['wait'], direct_exec=True)
  210. self.write_to_tmpfile('autosign_thread_pid',str(t.ep.pid))
  211. import threading
  212. threading.Thread( target=run, name='Autosign wait loop' ).start()
  213. time.sleep(0.2)
  214. return 'silent'
  215. def autosign_kill_thread(self):
  216. self.spawn('',msg_only=True)
  217. pid = int(self.read_from_tmpfile('autosign_thread_pid'))
  218. self.delete_tmpfile('autosign_thread_pid')
  219. from signal import SIGTERM
  220. imsg(purple(f'Killing autosign wait loop [PID {pid}]'))
  221. try:
  222. os.kill(pid,SIGTERM)
  223. except:
  224. imsg(yellow(f'{pid}: no such process'))
  225. return 'ok'
  226. def create_watchonly_wallets(self):
  227. self.insert_device_ts()
  228. t = self.create_wallets('alice', op='restore')
  229. t.read() # required!
  230. self.remove_device_ts()
  231. return t
  232. def restore_wallets(self):
  233. return self.create_watchonly_wallets()
  234. def _create_transfer_tx(self,amt):
  235. self.insert_device_ts()
  236. t = self.do_op('transfer','alice',f'1:0:{self.burn_addr},{amt}',no_relay=True,do_ret=True)
  237. t.read() # required!
  238. self.remove_device_ts()
  239. return t
  240. def create_transfer_tx1(self):
  241. return self._create_transfer_tx('0.124')
  242. def create_transfer_tx2(self):
  243. self.do_mount_online()
  244. get_file_with_ext(self.asi_online.xmr_tx_dir,'rawtx',delete_all=True)
  245. get_file_with_ext(self.asi_online.xmr_tx_dir,'sigtx',delete_all=True)
  246. self.do_umount_online()
  247. return self._create_transfer_tx('0.257')
  248. def _wait_signed(self,dtype):
  249. oqmsg_r(gray(f'→ offline wallet{"s" if dtype.endswith("s") else ""} signing {dtype}'))
  250. assert not self.device_inserted, f'‘{self.asi.dev_label_path}’ is inserted!'
  251. assert not self.asi.mountpoint.is_mount(), f'‘{self.asi.mountpoint}’ is mounted!'
  252. self.insert_device()
  253. while True:
  254. oqmsg_r(gray('.'))
  255. if self.asi.mountpoint.is_mount():
  256. oqmsg_r(gray('..working..'))
  257. break
  258. time.sleep(0.5)
  259. while True:
  260. oqmsg_r(gray('.'))
  261. if not self.asi.mountpoint.is_mount():
  262. oqmsg(gray('..done'))
  263. break
  264. time.sleep(0.5)
  265. self.remove_device()
  266. def _xmr_autosign_op(
  267. self,
  268. op,
  269. desc = None,
  270. dtype = None,
  271. ext = None,
  272. wallet_arg = None,
  273. add_opts = [],
  274. wait_signed = False):
  275. if wait_signed:
  276. self._wait_signed(dtype)
  277. data = self.users['alice']
  278. args = (
  279. self.extra_opts
  280. + self.autosign_opts
  281. + [f'--wallet-dir={data.udir}', f'--daemon=localhost:{data.md.rpc_port}']
  282. + add_opts
  283. + [ op ]
  284. + ([get_file_with_ext(self.asi.xmr_tx_dir,ext)] if ext else [])
  285. + ([wallet_arg] if wallet_arg else [])
  286. )
  287. desc_pfx = f'{desc}, ' if desc else ''
  288. return self.spawn( 'mmgen-xmrwallet', args, extra_desc=f'({desc_pfx}Alice)' )
  289. def _sync_chkbal(self,wallet_arg,bal_chk_func):
  290. return self.sync_wallets(
  291. 'alice',
  292. op = 'sync',
  293. wallets = wallet_arg,
  294. bal_chk_func = bal_chk_func )
  295. def sync_chkbal1(self):
  296. return self._sync_chkbal( '1', lambda n,b,ub: b == ub and 1 < b < 1.12 )
  297. # 1.234567891234 - 0.124 = 1.110567891234 (minus fees)
  298. def sync_chkbal2(self):
  299. return self._sync_chkbal( '1', lambda n,b,ub: b == ub and 0.8 < b < 0.86 )
  300. # 1.234567891234 - 0.124 - 0.257 = 0.853567891234 (minus fees)
  301. def sync_chkbal3(self):
  302. return self._sync_chkbal(
  303. '1-2',
  304. lambda n,b,ub: b == ub and ((n == 1 and 0.8 < b < 0.86) or (n == 2 and b > 1.23)) )
  305. def _mine_chk(self,desc):
  306. bal_type = {'locked':'b','unlocked':'ub'}[desc]
  307. return self.mine_chk(
  308. 'alice', 1, 0,
  309. lambda x: 0 < getattr(x,bal_type) < 1.234567891234,
  310. f'{desc} balance 0 < 1.234567891234' )
  311. def submit_transfer_tx1(self):
  312. return self._submit_transfer_tx()
  313. def resubmit_transfer_tx1(self):
  314. return self._submit_transfer_tx(
  315. relay_parm = self.tx_relay_daemon_proxy_parm,
  316. op = 'resubmit',
  317. check_bal = False)
  318. def submit_transfer_tx2(self):
  319. return self._submit_transfer_tx( relay_parm=self.tx_relay_daemon_parm )
  320. def _submit_transfer_tx(self,relay_parm=None,ext=None,op='submit',check_bal=True):
  321. self.insert_device_ts()
  322. t = self._xmr_autosign_op(
  323. op = op,
  324. add_opts = [f'--tx-relay-daemon={relay_parm}'] if relay_parm else [],
  325. ext = ext,
  326. dtype = 'transaction',
  327. wait_signed = op == 'submit' )
  328. t.expect( f'{op.capitalize()} transaction? (y/N): ', 'y' )
  329. t.written_to_file('Submitted transaction')
  330. self.remove_device_ts()
  331. if check_bal:
  332. t.ok()
  333. return self._mine_chk('unlocked')
  334. else:
  335. return t
  336. def _export_outputs(self,wallet_arg,add_opts=[]):
  337. self.insert_device_ts()
  338. t = self._xmr_autosign_op(
  339. op = 'export-outputs',
  340. wallet_arg = wallet_arg,
  341. add_opts = add_opts )
  342. t.written_to_file('Wallet outputs')
  343. self.remove_device_ts()
  344. return t
  345. def export_outputs1(self):
  346. return self._export_outputs('1',['--rescan-blockchain'])
  347. def export_outputs2(self):
  348. return self._export_outputs('1-2')
  349. def _import_key_images(self,wallet_arg):
  350. self.insert_device_ts()
  351. t = self._xmr_autosign_op(
  352. op = 'import-key-images',
  353. wallet_arg = wallet_arg,
  354. dtype = 'wallet outputs',
  355. wait_signed = True )
  356. t.read()
  357. self.remove_device_ts()
  358. return t
  359. def import_key_images1(self):
  360. return self._import_key_images(None)
  361. def import_key_images2(self):
  362. return self._import_key_images(None)
  363. def create_fake_tx_files(self):
  364. imsg('Creating fake transaction files')
  365. self.asi_online.msg_dir.mkdir(exist_ok=True)
  366. self.asi_online.xmr_dir.mkdir(exist_ok=True)
  367. self.asi_online.xmr_tx_dir.mkdir(exist_ok=True)
  368. self.asi_online.xmr_outputs_dir.mkdir(exist_ok=True)
  369. for fn in (
  370. 'a.rawtx', 'a.sigtx',
  371. 'b.rawtx', 'b.sigtx',
  372. 'c.rawtx',
  373. 'd.sigtx',
  374. ):
  375. (self.asi_online.tx_dir / fn).touch()
  376. for fn in (
  377. 'a.rawmsg.json', 'a.sigmsg.json',
  378. 'b.rawmsg.json',
  379. 'c.sigmsg.json',
  380. 'd.rawmsg.json', 'd.sigmsg.json',
  381. ):
  382. (self.asi_online.msg_dir / fn).touch()
  383. for fn in (
  384. 'a.rawtx', 'a.sigtx', 'a.subtx',
  385. 'b.rawtx', 'b.sigtx',
  386. 'c.subtx',
  387. 'd.rawtx', 'd.subtx',
  388. 'e.rawtx',
  389. 'f.sigtx','f.subtx',
  390. ):
  391. (self.asi_online.xmr_tx_dir / fn).touch()
  392. for fn in (
  393. 'a.raw', 'a.sig',
  394. 'b.raw',
  395. 'c.sig',
  396. ):
  397. (self.asi_online.xmr_outputs_dir / fn).touch()
  398. return 'ok'
  399. def _gen_listing(self):
  400. for k in ('tx_dir','msg_dir','xmr_tx_dir','xmr_outputs_dir'):
  401. d = getattr(self.asi_online,k)
  402. if d.is_dir():
  403. yield '{:12} {}'.format(
  404. str(Path(*d.parts[6:])) + ':',
  405. ' '.join(sorted(i.name for i in d.iterdir()))).strip()
  406. def autosign_clean(self):
  407. self.do_mount_online(no_xmr_chk=True)
  408. self.create_fake_tx_files()
  409. before = '\n'.join(self._gen_listing())
  410. t = self.spawn('mmgen-autosign', self.opts + ['clean'])
  411. out = t.read()
  412. self.do_mount_online(no_xmr_chk=True)
  413. after = '\n'.join(self._gen_listing())
  414. for k in ('tx','msg','xmr'):
  415. shutil.rmtree(self.asi_online.mountpoint / k)
  416. self.asi_online.tx_dir.mkdir()
  417. self.do_umount_online()
  418. chk = """
  419. tx: a.sigtx b.sigtx c.rawtx d.sigtx
  420. msg: a.sigmsg.json b.rawmsg.json c.sigmsg.json d.sigmsg.json
  421. xmr/tx: a.subtx b.sigtx c.subtx d.subtx e.rawtx f.subtx
  422. xmr/outputs:
  423. """
  424. imsg(f'\nBefore cleaning:\n{before}')
  425. imsg(f'\nAfter cleaning:\n{after}')
  426. assert '13 files shredded' in out
  427. assert after + '\n' == fmt(chk), f'\n{after}\n!=\n{fmt(chk)}'
  428. return t
  429. def txlist(self):
  430. self.insert_device_ts()
  431. t = self.spawn( 'mmgen-xmrwallet', self.autosign_opts + ['txlist'] )
  432. t.match_expect_list([
  433. 'SUBMITTED',
  434. 'Network','Submitted',
  435. 'Transfer 1:0','-> ext',
  436. 'Transfer 1:0','-> ext'
  437. ])
  438. self.remove_device_ts()
  439. return t
  440. def check_tx_dirs(self):
  441. self.do_mount_online()
  442. before = '\n'.join(self._gen_listing())
  443. self.do_umount_online()
  444. t = self.spawn('mmgen-autosign', self.opts + ['clean'])
  445. t.read()
  446. self.do_mount_online()
  447. after = '\n'.join(self._gen_listing())
  448. self.do_umount_online()
  449. imsg(f'\nBefore cleaning:\n{before}')
  450. imsg(f'\nAfter cleaning:\n{after}')
  451. pat = r'xmr/tx: \s*\S+\.subtx \S+\.subtx\s+xmr/outputs:\s*$'
  452. assert re.search( pat, after, re.DOTALL ), f'regex search for {pat} failed'
  453. return t
  454. def view(self):
  455. return self.sync_wallets('alice', op='view', wallets='1')
  456. def listview(self):
  457. return self.sync_wallets('alice', op='listview', wallets='2')