autosign.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2026 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. test.cmdtest_d.autosign: Autosign tests for the cmdtest.py test suite
  20. """
  21. import sys, os, time, shutil, atexit
  22. from subprocess import run, DEVNULL
  23. from pathlib import Path
  24. from mmgen.cfg import Config
  25. from mmgen.color import red, blue, yellow, cyan, orange, purple, gray
  26. from mmgen.util import msg, suf, die, indent, fmt
  27. from mmgen.led import LEDControl
  28. from mmgen.autosign import Autosign
  29. from mmgen.autosign.signable import Signable
  30. from ..include.common import (
  31. omsg,
  32. omsg_r,
  33. oqmsg,
  34. oqmsg_r,
  35. start_test_daemons,
  36. stop_test_daemons,
  37. joinpath,
  38. imsg,
  39. read_from_file,
  40. silence,
  41. end_silence,
  42. VirtBlockDevice,
  43. )
  44. from .include.common import ref_dir, dfl_words_file, dfl_bip39_file
  45. from .base import CmdTestBase
  46. from .include.input import mnemonic_entry
  47. class CmdTestAutosignBase(CmdTestBase):
  48. networks = ('btc',)
  49. tmpdir_nums = [18]
  50. color = True
  51. platform_skip = ('win32',)
  52. threaded = False
  53. daemon_coins = []
  54. def __init__(self, cfg, trunner, cfgs, spawn):
  55. CmdTestBase.__init__(self, cfg, trunner, cfgs, spawn)
  56. if trunner is None:
  57. return
  58. self.silent_mount = self.live or self.tr.quiet
  59. self.network_ids = [c+'_tn' for c in self.daemon_coins] + self.daemon_coins
  60. self._create_autosign_instances(create_dirs=not cfg.skipping_deps)
  61. self.fs_image_path = Path(self.tmpdir).absolute() / 'removable_device_image'
  62. if sys.platform == 'linux':
  63. self.txdev = VirtBlockDevice(self.fs_image_path, '10M')
  64. if not (cfg.skipping_deps or self.live):
  65. self._create_removable_device()
  66. if sys.platform == 'darwin' and not cfg.no_daemon_stop:
  67. atexit.register(self._macOS_eject_disk, self.asi.dev_label)
  68. self.opts = ['--coins='+','.join(self.coins)]
  69. self.txhex_file = f'{self.tmpdir}/tx_dump.hex'
  70. if not self.live:
  71. self.spawn_env['MMGEN_TEST_SUITE_ROOT_PFX'] = self.tmpdir
  72. if self.threaded:
  73. self.spawn_env['MMGEN_TEST_SUITE_AUTOSIGN_THREADED'] = '1'
  74. def __del__(self):
  75. if hasattr(self, 'have_dummy_control_files'):
  76. db = LEDControl.boards['dummy']
  77. for fn in (db.control, db.trigger):
  78. run(f'sudo rm -f {fn}'.split(), check=True)
  79. if hasattr(self, 'txdev'):
  80. del self.txdev
  81. if not self.cfg.no_daemon_stop:
  82. if sys.platform == 'darwin':
  83. for label in (self.asi.dev_label, self.asi.ramdisk.label):
  84. self._macOS_eject_disk(label)
  85. def _create_autosign_instances(self, create_dirs):
  86. d = {'offline': {'name':'asi'}}
  87. if self.have_online:
  88. d['online'] = {'name':'asi_online'}
  89. for subdir, data in d.items():
  90. asi = Autosign(
  91. Config({
  92. 'coins': ','.join(self.coins),
  93. 'test_suite': True,
  94. 'test_suite_xmr_autosign': self.name == 'CmdTestXMRAutosign',
  95. 'test_suite_autosign_threaded': self.threaded,
  96. 'test_suite_root_pfx': None if self.live else self.tmpdir,
  97. 'online': subdir == 'online'}))
  98. if create_dirs and not self.live:
  99. for k in ('mountpoint', 'shm_dir', 'wallet_dir'):
  100. if subdir == 'online' and k in ('shm_dir', 'wallet_dir'):
  101. continue
  102. if sys.platform == 'darwin' and k != 'mountpoint':
  103. continue
  104. getattr(asi, k).mkdir(parents=True, exist_ok=True)
  105. if sys.platform == 'darwin' and k == 'mountpoint':
  106. asi.mountpoint.rmdir()
  107. continue
  108. setattr(self, data['name'], asi)
  109. def _set_e2label(self, label):
  110. imsg(f'Setting label to {label}')
  111. for cmd in ('/sbin/e2label', 'e2label'):
  112. try:
  113. run([cmd, str(self.txdev.img_path), label], check=True)
  114. break
  115. except:
  116. if cmd == 'e2label':
  117. raise
  118. def _create_removable_device(self):
  119. match sys.platform:
  120. case 'linux':
  121. self.txdev.create()
  122. self.txdev.attach(silent=True)
  123. args = [
  124. '-E', 'root_owner={}:{}'.format(os.getuid(), os.getgid()),
  125. '-L', self.asi.dev_label,
  126. str(self.txdev.img_path)]
  127. redir = DEVNULL
  128. for cmd in ('/sbin/mkfs.ext2', 'mkfs.ext2'):
  129. try:
  130. run([cmd] + args, stdout=redir, stderr=redir, check=True)
  131. break
  132. except:
  133. if cmd == 'mkfs.ext2':
  134. raise
  135. self.txdev.detach(silent=True)
  136. case 'darwin':
  137. cmd = [
  138. 'hdiutil', 'create', '-size', '10M', '-fs', 'exFAT',
  139. '-volname', self.asi.dev_label,
  140. str(self.fs_image_path)]
  141. redir = DEVNULL if self.tr.quiet else None
  142. run(cmd, stdout=redir, check=True)
  143. def _macOS_mount_fs_image(self, loc):
  144. time.sleep(0.2)
  145. run(
  146. ['hdiutil', 'attach', '-mountpoint', str(loc.mountpoint), f'{self.fs_image_path}.dmg'],
  147. stdout=DEVNULL, check=True)
  148. def _macOS_eject_disk(self, label):
  149. try:
  150. run(['diskutil' , 'eject', label], stdout=DEVNULL, stderr=DEVNULL)
  151. except:
  152. pass
  153. def start_daemons(self):
  154. self.spawn(msg_only=True)
  155. start_test_daemons(*(self.network_ids + self.extra_daemons))
  156. return 'ok'
  157. def stop_daemons(self):
  158. self.spawn(msg_only=True)
  159. stop_test_daemons(*(self.network_ids + self.extra_daemons), remove_datadir=True)
  160. return 'ok'
  161. def delete_offline_shmdir(self):
  162. self.spawn(msg_only=True)
  163. imsg(f'Deleting ‘{self.asi.wallet_dir}’')
  164. shutil.rmtree(self.asi.wallet_dir, ignore_errors=True)
  165. return 'ok'
  166. def run_setup(
  167. self,
  168. mn_type = None,
  169. mn_file = None,
  170. use_dfl_wallet = False,
  171. seed_len = None,
  172. usr_entry_modes = False,
  173. wallet_passwd = None,
  174. keylist_passwd = None,
  175. add_opts = [],
  176. expect_args = []):
  177. mn_desc = mn_type or 'default'
  178. mn_type = mn_type or 'mmgen'
  179. if sys.platform == 'darwin' and not self.cfg.no_daemon_stop:
  180. self._macOS_eject_disk(self.asi.ramdisk.label)
  181. self.insert_device()
  182. t = self.spawn(
  183. 'mmgen-autosign',
  184. self.opts + add_opts
  185. + ([] if mn_desc == 'default' else [f'--mnemonic-fmt={mn_type}'])
  186. + ([f'--seed-len={seed_len}'] if seed_len else [])
  187. + ['setup'],
  188. no_passthru_opts = True)
  189. if use_dfl_wallet:
  190. t.expect('Use default wallet for autosigning? (Y/n): ', 'y')
  191. t.passphrase('MMGen wallet', wallet_passwd)
  192. else:
  193. mn_file = mn_file or {'mmgen': dfl_words_file, 'bip39': dfl_bip39_file}[mn_type]
  194. mn = read_from_file(mn_file).strip().split()
  195. if not seed_len:
  196. t.expect('words: ', {12:'1', 18:'2', 24:'3'}[len(mn)])
  197. t.expect('OK? (Y/n): ', '\n')
  198. from mmgen.mn_entry import mn_entry
  199. entry_mode = 'full'
  200. mne = mn_entry(self.cfg, mn_type, entry_mode=entry_mode)
  201. if usr_entry_modes:
  202. t.expect('user-configured')
  203. else:
  204. t.expect(
  205. 'Type a number.*: ',
  206. str(mne.entry_modes.index(entry_mode) + 1),
  207. regex = True)
  208. mnemonic_entry(t, mne, mn)
  209. t.written_to_file('Autosign wallet')
  210. if expect_args:
  211. t.expect(*expect_args)
  212. if keylist_passwd:
  213. t.passphrase('keylist data', keylist_passwd)
  214. t.expect('(y/N): ', 'y')
  215. t.read()
  216. self.remove_device()
  217. if sys.platform == 'darwin' and not self.cfg.no_daemon_stop:
  218. atexit.register(self._macOS_eject_disk, self.asi.ramdisk.label)
  219. return t
  220. def insert_device(self, asi='asi'):
  221. if self.live:
  222. return
  223. loc = getattr(self, asi)
  224. match sys.platform:
  225. case 'linux':
  226. self._set_e2label(loc.dev_label)
  227. self.txdev.attach()
  228. for _ in range(20):
  229. if loc.device_inserted:
  230. break
  231. time.sleep(0.1)
  232. else:
  233. die(2, f'device insert timeout exceeded {loc.dev_label}')
  234. case 'darwin':
  235. self._macOS_mount_fs_image(loc)
  236. def remove_device(self, asi='asi'):
  237. if self.live:
  238. return
  239. loc = getattr(self, asi)
  240. match sys.platform:
  241. case 'linux':
  242. self.txdev.detach()
  243. for _ in range(20):
  244. if not loc.device_inserted:
  245. break
  246. time.sleep(0.1)
  247. else:
  248. die(2, f'device remove timeout exceeded {loc.dev_label}')
  249. case 'darwin':
  250. self._macOS_eject_disk(loc.dev_label)
  251. def _mount_ops(self, loc, cmd, *args, **kwargs):
  252. return getattr(getattr(self, loc), cmd)(*args, silent=self.silent_mount, **kwargs)
  253. def do_mount(self, *args, **kwargs):
  254. return self._mount_ops('asi', 'do_mount', *args, **kwargs)
  255. def do_umount(self, *args, **kwargs):
  256. return self._mount_ops('asi', 'do_umount', *args, **kwargs)
  257. def _gen_listing(self):
  258. for k in self.asi.dirs:
  259. d = getattr(self.asi, k)
  260. if d.is_dir():
  261. yield '{:12} {}'.format(
  262. str(Path(*d.parts[6:])) + ':',
  263. ' '.join(sorted(i.name for i in d.iterdir()))).strip()
  264. class CmdTestAutosignClean(CmdTestAutosignBase):
  265. 'autosign directory cleaning operations'
  266. have_online = False
  267. live = False
  268. simulate_led = True
  269. no_insert_check = False
  270. coins = ['btc']
  271. tmpdir_nums = [38]
  272. cmd_group = (
  273. ('clean_no_xmr', 'cleaning signable file directories (no XMR)'),
  274. ('clean_xmr_only', 'cleaning signable file directories (XMR-only)'),
  275. ('clean_all', 'cleaning signable file directories (with XMR)'),
  276. )
  277. def create_fake_tx_files(self):
  278. imsg('Creating fake transaction files')
  279. if not self.asi.xmr_only:
  280. for fn in (
  281. 'a.rawtx', 'a.sigtx',
  282. 'b.rawtx', 'b.sigtx',
  283. 'c.rawtx',
  284. 'd.sigtx',
  285. ):
  286. (self.asi.tx_dir / fn).touch()
  287. for fn in (
  288. 'a.arawtx', 'a.asigtx', 'a.asubtx',
  289. 'b.arawtx', 'b.asigtx',
  290. 'c.asubtx',
  291. 'd.arawtx', 'd.asubtx',
  292. 'e.arawtx',
  293. 'f.asigtx', 'f.asubtx',
  294. ):
  295. (self.asi.txauto_dir / fn).touch()
  296. for fn in (
  297. 'a.rawmsg.json', 'a.sigmsg.json',
  298. 'b.rawmsg.json',
  299. 'c.sigmsg.json',
  300. 'd.rawmsg.json', 'd.sigmsg.json',
  301. ):
  302. (self.asi.msg_dir / fn).touch()
  303. if self.asi.have_xmr:
  304. for fn in (
  305. 'a.rawtx', 'a.sigtx', 'a.subtx',
  306. 'b.rawtx', 'b.sigtx',
  307. 'c.subtx',
  308. 'd.rawtx', 'd.subtx',
  309. 'e.rawtx',
  310. 'f.sigtx', 'f.subtx',
  311. ):
  312. (self.asi.xmr_tx_dir / fn).touch()
  313. for fn in (
  314. 'a.raw', 'a.sig',
  315. 'b.raw',
  316. 'c.sig',
  317. ):
  318. (self.asi.xmr_outputs_dir / fn).touch()
  319. return 'ok'
  320. def clean_no_xmr(self):
  321. return self._clean('btc,ltc,eth')
  322. def clean_xmr_only(self):
  323. self.asi = Autosign(Config({'_clone': self.asi.cfg, 'coins': 'xmr'}))
  324. return self._clean('xmr')
  325. def clean_all(self):
  326. self.asi = Autosign(Config({'_clone': self.asi.cfg, 'coins': 'xmr,btc,bch,eth'}))
  327. return self._clean('xmr,btc,bch,eth')
  328. def _clean(self, coins):
  329. self.spawn(msg_only=True)
  330. self.insert_device()
  331. silence()
  332. self.do_mount()
  333. end_silence()
  334. self.create_fake_tx_files()
  335. before = '\n'.join(self._gen_listing())
  336. t = self.spawn('mmgen-autosign', [f'--coins={coins}', 'clean'], no_msg=True)
  337. out = t.read()
  338. if sys.platform == 'darwin':
  339. self.insert_device()
  340. silence()
  341. self.do_mount()
  342. end_silence()
  343. after = '\n'.join(self._gen_listing())
  344. chk_non_xmr = """
  345. tx: a.sigtx b.sigtx c.rawtx d.sigtx
  346. txauto: a.asubtx b.asigtx c.asubtx d.asubtx e.arawtx f.asubtx
  347. msg: a.sigmsg.json b.rawmsg.json c.sigmsg.json d.sigmsg.json
  348. """
  349. chk_xmr = """
  350. xmr: outputs tx
  351. xmr/tx: a.subtx b.sigtx c.subtx d.subtx e.rawtx f.subtx
  352. xmr/outputs:
  353. """
  354. chk = ''
  355. shred_count = 0
  356. if not self.asi.xmr_only:
  357. for k in ('tx_dir', 'txauto_dir', 'msg_dir'):
  358. shutil.rmtree(getattr(self.asi, k))
  359. chk += chk_non_xmr.rstrip()
  360. shred_count += 9
  361. if self.asi.have_xmr:
  362. shutil.rmtree(self.asi.xmr_dir)
  363. chk += chk_xmr.rstrip()
  364. shred_count += 9
  365. self.do_umount()
  366. self.remove_device()
  367. imsg(f'\nBefore cleaning:\n{before}')
  368. imsg(f'\nAfter cleaning:\n{after}')
  369. assert f'{shred_count} files shredded' in out
  370. assert after + '\n' == fmt(chk), f'\n{after}\n!=\n{fmt(chk)}'
  371. return t
  372. class CmdTestAutosignThreaded(CmdTestAutosignBase):
  373. have_online = True
  374. live = False
  375. no_insert_check = False
  376. threaded = True
  377. def _user_txcreate(
  378. self,
  379. user,
  380. progname = 'txcreate',
  381. ui_handler = None,
  382. chg_addr = None,
  383. opts = [],
  384. output_args = [],
  385. exit_val = 0,
  386. inputs = '1',
  387. tweaks = [],
  388. expect_str = None,
  389. data_arg = None,
  390. need_rbf = False):
  391. if output_args:
  392. assert not chg_addr
  393. if chg_addr:
  394. assert not output_args
  395. if need_rbf and not self.proto.cap('rbf'):
  396. return 'skip'
  397. def do_return():
  398. if expect_str:
  399. t.expect(expect_str)
  400. t.read()
  401. self.remove_device_online()
  402. return t
  403. self.insert_device_online()
  404. sid = self._user_sid(user)
  405. t = self.spawn(
  406. f'mmgen-{progname}',
  407. opts
  408. + [f'--{user}', '--autosign']
  409. + ([data_arg] if data_arg else [])
  410. + (output_args or [f'{self.burn_addr},1.23456', f'{sid}:{chg_addr}']),
  411. exit_val = exit_val or None)
  412. if exit_val:
  413. return do_return()
  414. t = (ui_handler or self.txcreate_ui_common)(
  415. t,
  416. inputs = inputs,
  417. interactive_fee = '32s',
  418. tweaks = tweaks,
  419. file_desc = 'Unsigned automount transaction')
  420. return do_return()
  421. def _user_txsend(
  422. self,
  423. user,
  424. *,
  425. comment = None,
  426. no_wait = False,
  427. need_rbf = False,
  428. dump_hex = False,
  429. mark_sent = False):
  430. if need_rbf and not self.proto.cap('rbf'):
  431. return 'skip'
  432. if not no_wait:
  433. self._wait_signed('transaction')
  434. extra_opt = (
  435. [f'--dump-hex={self.txhex_file}'] if dump_hex
  436. else ['--mark-sent'] if mark_sent
  437. else [])
  438. self.insert_device_online()
  439. t = self.spawn('mmgen-txsend',
  440. [f'--{user}', '--quiet', '--autosign'] + extra_opt, no_passthru_opts=['coin'])
  441. if mark_sent:
  442. t.written_to_file('Sent automount transaction')
  443. else:
  444. t.view_tx('t')
  445. t.do_comment(comment)
  446. if dump_hex:
  447. t.written_to_file('Serialized transaction hex data')
  448. else:
  449. self._do_confirm_send(t, quiet=True)
  450. t.written_to_file('Sent automount transaction')
  451. t.read()
  452. self.remove_device_online()
  453. return t
  454. def _wait_loop_start(self, opts=[], add_opts=[]):
  455. t = self.spawn(
  456. 'mmgen-autosign',
  457. (opts or self.opts) + add_opts + ['--full-summary', 'wait'],
  458. direct_exec = True,
  459. no_passthru_opts = True,
  460. spawn_env_override = self.spawn_env | {'EXEC_WRAPPER_DO_RUNTIME_MSG': ''})
  461. self.write_to_tmpfile('autosign_thread_pid', str(t.ep.pid))
  462. return t
  463. def wait_loop_start(self, opts=[], add_opts=[]):
  464. import threading
  465. threading.Thread(
  466. target = self._wait_loop_start,
  467. kwargs = {'opts': opts, 'add_opts': add_opts},
  468. name = 'Autosign wait loop').start()
  469. time.sleep(0.1) # try to ensure test output is displayed before next test starts
  470. return 'silent'
  471. def wait_loop_kill(self):
  472. return self._kill_process_from_pid_file('autosign_thread_pid', 'autosign wait loop')
  473. def _wait_signed(self, desc):
  474. oqmsg_r(gray(f'→ offline wallet{"s" if desc.endswith("s") else ""} waiting for {desc}'))
  475. assert not self.asi.device_inserted, f'‘{self.asi.dev_label}’ is inserted!'
  476. assert not self.asi.mountpoint.is_mount(), f'‘{self.asi.mountpoint}’ is mounted!'
  477. self.insert_device()
  478. while True:
  479. oqmsg_r(gray('.'))
  480. if self.asi.mountpoint.is_mount():
  481. oqmsg_r(gray(' signing '))
  482. break
  483. time.sleep(0.1)
  484. while True:
  485. oqmsg_r(gray('>'))
  486. if not self.asi.mountpoint.is_mount():
  487. oqmsg(gray(' done'))
  488. break
  489. time.sleep(0.1)
  490. imsg('')
  491. self.remove_device()
  492. return 'ok'
  493. def insert_device_online(self):
  494. return self.insert_device(asi='asi_online')
  495. def remove_device_online(self):
  496. return self.remove_device(asi='asi_online')
  497. def do_mount_online(self, *args, **kwargs):
  498. return self._mount_ops('asi_online', 'do_mount', *args, **kwargs)
  499. def do_umount_online(self, *args, **kwargs):
  500. return self._mount_ops('asi_online', 'do_umount', *args, **kwargs)
  501. async def txview(self):
  502. self.spawn(msg_only=True)
  503. self.insert_device()
  504. self.do_mount()
  505. src = Path(self.asi.txauto_dir)
  506. from mmgen.tx import CompletedTX
  507. txs = sorted(
  508. [await CompletedTX(cfg=self.cfg, filename=path, quiet_open=True) for path in sorted(src.iterdir())],
  509. key = lambda x: x.timestamp)
  510. for tx in txs:
  511. if not tx.is_compat:
  512. imsg(blue(f'\nViewing ‘{tx.infile.name}’:'))
  513. out = tx.info.format(terse=True)
  514. imsg(indent(out, indent=' '))
  515. self.do_umount()
  516. self.remove_device()
  517. return 'ok'
  518. class CmdTestAutosign(CmdTestAutosignBase):
  519. 'autosigning transactions for all supported coins'
  520. coins = ['btc', 'bch', 'ltc', 'eth']
  521. daemon_coins = ['btc', 'bch', 'ltc']
  522. txfile_coins = ['btc', 'bch', 'ltc', 'eth', 'mm1', 'etc']
  523. have_online = False
  524. live = False
  525. simulate_led = True
  526. no_insert_check = True
  527. wallet_passwd = 'abc'
  528. filedir_map = (
  529. ('btc', ''),
  530. ('bch', ''),
  531. ('ltc', 'litecoin'),
  532. ('eth', 'ethereum'),
  533. ('mm1', 'ethereum'),
  534. ('etc', 'ethereum_classic'),
  535. )
  536. cmd_group = (
  537. ('test_led', 'testing for LED support'),
  538. ('list_led', 'listing LED-supported boards'),
  539. ('start_daemons', 'starting daemons'),
  540. ('copy_tx_files', 'copying transaction files'),
  541. ('gen_key', 'generating key'),
  542. ('create_dfl_wallet', 'creating default MMGen wallet'),
  543. ('bad_opt1', 'running ‘mmgen-autosign’ with --seed-len in invalid context'),
  544. ('bad_opt2', 'running ‘mmgen-autosign’ with --mnemonic-fmt in invalid context'),
  545. ('bad_opt3', 'running ‘mmgen-autosign’ with --led in invalid context'),
  546. ('run_setup_dfl_wallet', 'running ‘autosign setup’ (with default wallet)'),
  547. ('sign_quiet', 'signing transactions (--quiet)'),
  548. ('remove_signed_txfiles', 'removing signed transaction files'),
  549. ('run_setup_bip39', 'running ‘autosign setup’ (BIP39 mnemonic)'),
  550. ('create_bad_txfiles', 'creating bad transaction files'),
  551. ('sign_full_summary', 'signing transactions (--full-summary)'),
  552. ('remove_signed_txfiles_btc', 'removing transaction files (BTC only)'),
  553. ('remove_bad_txfiles', 'removing bad transaction files'),
  554. ('sign_led', 'signing transactions (--led - BTC files only)'),
  555. ('remove_signed_txfiles', 'removing signed transaction files'),
  556. ('sign_stealth_led', 'signing transactions (--stealth-led)'),
  557. ('remove_signed_txfiles', 'removing signed transaction files'),
  558. ('copy_msgfiles', 'copying message files'),
  559. ('sign_quiet_msg', 'signing transactions and messages (--quiet)'),
  560. ('remove_signed_txfiles', 'removing signed transaction files'),
  561. ('create_bad_txfiles2', 'creating bad transaction files'),
  562. ('remove_signed_msgfiles', 'removing signed message files'),
  563. ('create_invalid_msgfile', 'creating invalid message file'),
  564. ('sign_full_summary_msg', 'signing transactions and messages (--full-summary)'),
  565. ('remove_invalid_msgfile', 'removing invalid message file'),
  566. ('remove_bad_txfiles2', 'removing bad transaction files'),
  567. ('sign_no_unsigned', 'signing transactions and messages (nothing to sign)'),
  568. ('sign_no_unsigned_xmr', 'signing transactions and messages (nothing to sign, with XMR)'),
  569. ('sign_no_unsigned_xmronly', 'signing transactions and messages (nothing to sign, XMR-only)'),
  570. ('stop_daemons', 'stopping daemons'),
  571. ('sign_bad_no_daemon', 'signing transactions (error, no daemons running)'),
  572. ('wipe_key', 'wiping the wallet encryption key'),
  573. )
  574. def __init__(self, cfg, trunner, cfgs, spawn):
  575. super().__init__(cfg, trunner, cfgs, spawn)
  576. if trunner is None:
  577. return
  578. if self.live and not self.cfg.exact_output:
  579. die(1, red('autosign_live tests must be run with --exact-output enabled!'))
  580. if self.no_insert_check:
  581. self.opts.append('--no-insert-check')
  582. self.tx_file_ops('set_count') # initialize self.tx_count here so we can resume anywhere
  583. self.bad_tx_count = 0
  584. def gen_msg_fns():
  585. fmap = dict(self.filedir_map)
  586. for coin in self.coins:
  587. if coin == 'xmr':
  588. continue
  589. sdir = os.path.join('test', 'ref', fmap[coin])
  590. for fn in os.listdir(sdir):
  591. if fn.endswith(f'[{coin.upper()}].rawmsg.json'):
  592. yield os.path.join(sdir, fn)
  593. self.ref_msgfiles = tuple(gen_msg_fns())
  594. self.good_msg_count = 0
  595. self.bad_msg_count = 0
  596. if self.simulate_led:
  597. db = LEDControl.boards['dummy']
  598. for fn in (db.control, db.trigger):
  599. run(f'sudo rm -f {fn}'.split(), check=True)
  600. LEDControl.create_dummy_control_files()
  601. usrgrp = {'linux': 'root:root', 'darwin': 'root:wheel'}[sys.platform]
  602. for fn in (db.control, db.trigger): # trigger the auto-chmod feature
  603. run(f'sudo chmod 644 {fn}'.split(), check=True)
  604. run(f'sudo chown {usrgrp} {fn}'.split(), check=True)
  605. self.have_dummy_control_files = True
  606. self.spawn_env['MMGEN_TEST_SUITE_AUTOSIGN_LED_SIMULATE'] = '1'
  607. def gen_key(self):
  608. self.insert_device()
  609. t = self.spawn('mmgen-autosign', self.opts + ['gen_key'])
  610. t.expect_getend('Wrote key file ')
  611. t.read()
  612. self.remove_device()
  613. return t
  614. def create_dfl_wallet(self):
  615. t = self.spawn('mmgen-walletconv', [
  616. f'--outdir={self.cfg.data_dir}',
  617. '--usr-randchars=0', '--quiet', '--hash-preset=1', '--label=foo',
  618. 'test/ref/98831F3A.hex'
  619. ]
  620. )
  621. t.passphrase_new('new MMGen wallet', self.wallet_passwd)
  622. t.written_to_file('MMGen wallet')
  623. return t
  624. def _bad_opt(self, cmdline, expect):
  625. t = self.spawn('mmgen-autosign', ['--coins=btc'] + cmdline, exit_val=1)
  626. t.expect(expect)
  627. return t
  628. def bad_opt1(self):
  629. return self._bad_opt(['--seed-len=128'], 'is valid')
  630. def bad_opt2(self):
  631. return self._bad_opt(['--mnemonic-fmt=bip39', 'wait'], 'is valid')
  632. def bad_opt3(self):
  633. return self._bad_opt(['--led', 'gen_key'], 'is not valid')
  634. def run_setup_dfl_wallet(self):
  635. return self.run_setup(mn_type='default', use_dfl_wallet=True, wallet_passwd=self.wallet_passwd)
  636. def run_setup_bip39(self):
  637. from mmgen.cfgfile import mmgen_cfg_file
  638. fn = mmgen_cfg_file(self.cfg, 'usr').fn
  639. old_data = mmgen_cfg_file(self.cfg, 'usr').get_data(fn)
  640. new_data = [d.replace('bip39:fixed', 'bip39:full')[2:]
  641. if d.startswith('# mnemonic_entry_modes') else d for d in old_data]
  642. with open(fn, 'w') as fh:
  643. fh.write('\n'.join(new_data) + '\n')
  644. t = self.run_setup(
  645. mn_type = 'bip39',
  646. seed_len = 256,
  647. usr_entry_modes = True)
  648. with open(fn, 'w') as fh:
  649. fh.write('\n'.join(old_data) + '\n')
  650. return t
  651. def copy_tx_files(self):
  652. self.spawn(msg_only=True)
  653. return self.tx_file_ops('copy')
  654. def remove_signed_txfiles(self):
  655. self.tx_file_ops('remove_signed')
  656. return 'skip'
  657. def remove_signed_txfiles_btc(self):
  658. self.tx_file_ops('remove_signed', txfile_coins=['btc'])
  659. return 'skip'
  660. def tx_file_ops(self, op, txfile_coins=[]):
  661. assert op in ('copy', 'set_count', 'remove_signed')
  662. from .ref import CmdTestRef
  663. def gen():
  664. d = CmdTestRef.sources['ref_tx_file']
  665. dirmap = [e for e in self.filedir_map if e[0] in (txfile_coins or self.txfile_coins)]
  666. for coin, coindir in dirmap:
  667. for network in (0, 1):
  668. fn = d[coin][network]
  669. if fn:
  670. yield (coindir, fn)
  671. data = list(gen()) + [('', '25EFA3[2.34].testnet.rawtx')] # TX with 2 non-MMGen outputs
  672. self.tx_count = len(data)
  673. if op == 'set_count':
  674. return
  675. self.insert_device()
  676. silence()
  677. self.do_mount(verbose=not self.tr.quiet)
  678. end_silence()
  679. for coindir, fn in data:
  680. src = joinpath(ref_dir, coindir, fn)
  681. if self.cfg.debug_utf8:
  682. ext = '.testnet.rawtx' if fn.endswith('.testnet.rawtx') else '.rawtx'
  683. fn = fn[:-len(ext)] + '-α' + ext
  684. target = joinpath(self.asi.tx_dir, fn)
  685. if not op == 'remove_signed':
  686. shutil.copyfile(src, target)
  687. try:
  688. os.unlink(target.replace('.rawtx', '.sigtx'))
  689. except:
  690. pass
  691. self.do_umount()
  692. self.remove_device()
  693. return 'ok'
  694. def create_bad_txfiles(self):
  695. return self.bad_txfiles('create')
  696. def remove_bad_txfiles(self):
  697. return self.bad_txfiles('remove')
  698. create_bad_txfiles2 = create_bad_txfiles
  699. remove_bad_txfiles2 = remove_bad_txfiles
  700. def bad_txfiles(self, op):
  701. self.insert_device()
  702. self.do_mount()
  703. # create or delete 2 bad tx files
  704. self.spawn(msg_only=True)
  705. fns = [joinpath(self.asi.tx_dir, f'bad{n}.rawtx') for n in (1, 2)]
  706. match op:
  707. case 'create':
  708. for fn in fns:
  709. with open(fn, 'w') as fp:
  710. fp.write('bad tx data\n')
  711. self.bad_tx_count = 2
  712. case 'remove':
  713. for fn in fns:
  714. try:
  715. os.unlink(fn)
  716. except:
  717. pass
  718. self.bad_tx_count = 0
  719. self.do_umount()
  720. self.remove_device()
  721. return 'ok'
  722. def copy_msgfiles(self):
  723. return self.msgfile_ops('copy')
  724. def remove_signed_msgfiles(self):
  725. return self.msgfile_ops('remove_signed')
  726. def create_invalid_msgfile(self):
  727. return self.msgfile_ops('create_invalid')
  728. def remove_invalid_msgfile(self):
  729. return self.msgfile_ops('remove_invalid')
  730. def msgfile_ops(self, op):
  731. self.spawn(msg_only=True)
  732. destdir = joinpath(self.asi.mountpoint, 'msg')
  733. self.insert_device()
  734. self.do_mount()
  735. os.makedirs(destdir, exist_ok=True)
  736. match op:
  737. case 'create_invalid' | 'remove_invalid':
  738. fn = os.path.join(destdir, 'DEADBE[BTC].rawmsg.json')
  739. if op == 'create_invalid':
  740. with open(fn, 'w') as fp:
  741. fp.write('bad data\n')
  742. self.bad_msg_count += 1
  743. else:
  744. os.unlink(fn)
  745. self.bad_msg_count -= 1
  746. case 'copy':
  747. for fn in self.ref_msgfiles:
  748. if os.path.basename(fn) == 'ED405C[BTC].rawmsg.json': # contains bad Seed ID
  749. self.bad_msg_count += 1
  750. else:
  751. self.good_msg_count += 1
  752. imsg(f'Copying: {fn} -> {destdir}')
  753. shutil.copy2(fn, destdir)
  754. case 'remove_signed':
  755. for fn in self.ref_msgfiles:
  756. os.unlink(os.path.join(destdir, os.path.basename(fn).replace('rawmsg', 'sigmsg')))
  757. self.do_umount()
  758. self.remove_device()
  759. return 'ok'
  760. def do_sign(self, args=[], have_msg=False, exc_exit_val=None, expect_str=None):
  761. tx_desc = Signable.transaction.desc
  762. self.insert_device()
  763. def do_return():
  764. if expect_str:
  765. t.expect(expect_str)
  766. t.read()
  767. self.remove_device()
  768. imsg('')
  769. return t
  770. t = self.spawn(
  771. 'mmgen-autosign',
  772. self.opts + args,
  773. exit_val = exc_exit_val or (1 if self.bad_tx_count or (have_msg and self.bad_msg_count) else None))
  774. if exc_exit_val:
  775. return do_return()
  776. t.expect(
  777. f'{self.tx_count} {tx_desc}{suf(self.tx_count)} signed' if self.tx_count else
  778. f'No unsigned {tx_desc}s')
  779. if self.bad_tx_count:
  780. t.expect(f'{self.bad_tx_count} {tx_desc}{suf(self.bad_tx_count)} failed to sign')
  781. if have_msg:
  782. t.expect(
  783. f'{self.good_msg_count} message file{suf(self.good_msg_count)}{{0,1}} signed'
  784. if self.good_msg_count else
  785. 'No unsigned message files', regex=True)
  786. if self.bad_msg_count:
  787. t.expect(
  788. f'{self.bad_msg_count} message file{suf(self.bad_msg_count)}{{0,1}} failed to sign',
  789. regex = True)
  790. return do_return()
  791. def sign_quiet(self):
  792. return self.do_sign(['--quiet'])
  793. def sign_full_summary(self):
  794. return self.do_sign(['--full-summary'])
  795. def sign_led(self):
  796. return self.do_sign(['--quiet', '--led'])
  797. def sign_stealth_led(self):
  798. return self.do_sign(['--quiet', '--stealth-led'])
  799. def sign_quiet_msg(self):
  800. return self.do_sign(['--quiet'], have_msg=True)
  801. def sign_full_summary_msg(self):
  802. return self.do_sign(['--full-summary'], have_msg=True)
  803. def sign_bad_no_daemon(self):
  804. return self.do_sign(exc_exit_val=2, expect_str='listening on the correct port')
  805. def sign_no_unsigned(self):
  806. return self._sign_no_unsigned(
  807. coins = 'BTC',
  808. present = ['non_xmr_signables'],
  809. absent = ['xmr_signables'])
  810. def sign_no_unsigned_xmr(self):
  811. if self.coins == ['btc']:
  812. return 'skip'
  813. return self._sign_no_unsigned(
  814. coins = 'XMR,BTC',
  815. present = ['xmr_signables', 'non_xmr_signables'])
  816. def sign_no_unsigned_xmronly(self):
  817. if self.coins == ['btc']:
  818. return 'skip'
  819. return self._sign_no_unsigned(
  820. coins = 'XMR',
  821. present = ['xmr_signables'],
  822. absent = ['non_xmr_signables'])
  823. def _sign_no_unsigned(self, coins, present=[], absent=[]):
  824. self.insert_device()
  825. t = self.spawn('mmgen-autosign', ['--quiet', '--no-insert-check', f'--coins={coins}'])
  826. res = t.read()
  827. self.remove_device()
  828. for signable_list in present:
  829. for signable_clsname in getattr(Autosign, signable_list):
  830. desc = getattr(Signable, signable_clsname).desc
  831. assert f'No unsigned {desc}s' in res, f'‘No unsigned {desc}s’ missing in output'
  832. for signable_list in absent:
  833. for signable_clsname in getattr(Autosign, signable_list):
  834. desc = getattr(Signable, signable_clsname).desc
  835. assert not f'No unsigned {desc}s' in res, f'‘No unsigned {desc}s’ should be absent in output'
  836. return t
  837. def wipe_key(self):
  838. self.insert_device()
  839. t = self.spawn('mmgen-autosign', ['--quiet', '--no-insert-check', 'wipe_key'])
  840. t.expect('Shredding')
  841. t.read()
  842. self.remove_device()
  843. return t
  844. def test_led(self, *, op='test_led', expect_str='LED signaling'):
  845. t = self.spawn('mmgen-autosign', ['--quiet', '--no-insert-check', op])
  846. t.expect(expect_str)
  847. return t
  848. def list_led(self):
  849. return self.test_led(op='list_led', expect_str='Boards with')
  850. class CmdTestAutosignBTC(CmdTestAutosign):
  851. 'autosigning BTC transactions'
  852. coins = ['btc']
  853. daemon_coins = ['btc']
  854. txfile_coins = ['btc']
  855. class CmdTestAutosignLive(CmdTestAutosignBTC):
  856. 'live autosigning BTC transactions'
  857. live = True
  858. simulate_led = False
  859. no_insert_check = False
  860. cmd_group = (
  861. ('start_daemons', 'starting daemons'),
  862. ('copy_tx_files', 'copying transaction files'),
  863. ('gen_key', 'generating key'),
  864. ('run_setup_mmgen', 'running ‘autosign setup’ (MMGen native mnemonic)'),
  865. ('sign_live', 'signing transactions'),
  866. ('create_bad_txfiles', 'creating bad transaction files'),
  867. ('sign_live_led', 'signing transactions (--led)'),
  868. ('remove_bad_txfiles', 'removing bad transaction files'),
  869. ('sign_live_stealth_led', 'signing transactions (--stealth-led)'),
  870. ('stop_daemons', 'stopping daemons'),
  871. )
  872. def __init__(self, cfg, trunner, cfgs, spawn):
  873. super().__init__(cfg, trunner, cfgs, spawn)
  874. if trunner is None:
  875. return
  876. try:
  877. led = LEDControl(enabled=True, simulate=self.simulate_led)
  878. except Exception as e:
  879. msg(str(e))
  880. die(2, 'LEDControl initialization failed')
  881. self.color = led.board.color
  882. def run_setup_mmgen(self):
  883. return self.run_setup(mn_type='mmgen', use_dfl_wallet=None)
  884. def sign_live(self):
  885. return self.do_sign_live()
  886. def sign_live_led(self):
  887. return self.do_sign_live(['--led'], f'The {self.color} LED should start blinking slowly now')
  888. def sign_live_stealth_led(self):
  889. return self.do_sign_live(['--stealth-led'], 'You should see no LED activity now')
  890. def do_sign_live(self, led_opts=None, led_msg=None):
  891. def prompt_remove():
  892. omsg_r(orange('\nExtract removable device and then hit ENTER '))
  893. input()
  894. def prompt_insert_sign(t):
  895. omsg(orange(insert_msg))
  896. t.expect(f'{self.tx_count} non-automount transactions signed')
  897. if self.bad_tx_count:
  898. t.expect(f'{self.bad_tx_count} non-automount transactions failed to sign')
  899. t.expect('Waiting for device insertion')
  900. if led_opts:
  901. opts_msg = '‘' + ' '.join(led_opts) + '’'
  902. info_msg = 'Running ‘mmgen-autosign wait’ with {}. {}'.format(opts_msg, led_msg)
  903. insert_msg = f'Insert removable device and watch for fast {self.color} LED activity during signing'
  904. else:
  905. opts_msg = 'no LED'
  906. info_msg = 'Running ‘mmgen-autosign wait’'
  907. insert_msg = 'Insert removable device '
  908. self.spawn(msg_only=True)
  909. self.do_umount()
  910. prompt_remove()
  911. omsg('\n' + cyan(indent(info_msg)))
  912. t = self.spawn(
  913. 'mmgen-autosign',
  914. self.opts + (led_opts or []) + ['--quiet', '--no-summary', 'wait'],
  915. no_msg = True,
  916. exit_val = 1)
  917. if self.tr.quiet:
  918. omsg('')
  919. prompt_insert_sign(t)
  920. self.do_mount() # race condition due to device insertion detection
  921. self.remove_signed_txfiles()
  922. self.do_umount()
  923. imsg(purple('\nKilling wait loop!'))
  924. t.kill(2) # 2 = SIGINT
  925. if self.simulate_led and led_opts:
  926. t.expect(f'Resetting {self.color} LED')
  927. return t
  928. class CmdTestAutosignLiveSimulate(CmdTestAutosignLive):
  929. 'live autosigning BTC transactions with simulated LED support'
  930. simulate_led = True