ct_input.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # Project source code repository: https://github.com/mmgen/mmgen-wallet
  7. # Licensed according to the terms of GPL Version 3. See LICENSE for details.
  8. """
  9. test.cmdtest_d.ct_input: user input tests for the MMGen cmdtest.py test suite
  10. """
  11. import sys, os
  12. from mmgen.cfg import gc
  13. from mmgen.util import fmt, capfirst, remove_whitespace
  14. from mmgen.wallet import get_wallet_cls
  15. from ..include.common import (
  16. cfg,
  17. imsg,
  18. imsg_r,
  19. sample_mn,
  20. get_data_from_file,
  21. read_from_file,
  22. strip_ansi_escapes
  23. )
  24. from .common import Ctrl_U, ref_dir
  25. from .ct_base import CmdTestBase
  26. from .input import stealth_mnemonic_entry, user_dieroll_entry
  27. hold_protect_delay = 2 if sys.platform == 'darwin' else None
  28. class CmdTestInput(CmdTestBase):
  29. 'user input'
  30. networks = ('btc',)
  31. tmpdir_nums = [1]
  32. color = True
  33. cmd_group_in = (
  34. ('subgroup.char', []),
  35. ('subgroup.line', []),
  36. ('subgroup.password', []),
  37. ('subgroup.misc', []),
  38. ('subgroup.wallet', []),
  39. ('subgroup.mnemonic', []),
  40. ('subgroup.dieroll', []),
  41. )
  42. cmd_subgroups = {
  43. 'char': (
  44. 'get_char() function',
  45. ('get_char1', 'get_char()'),
  46. ('get_char2', 'get_char() [multiple characters]'),
  47. ('get_char3', 'get_char() [no prompt]'),
  48. ('get_char4', 'get_char() [utf8]'),
  49. ('get_char_term1', 'get_char() [term, utf8]'),
  50. ('get_char_term2', 'get_char() [term, multiple characters]'),
  51. ('get_char_term3', 'get_char() [term, prehold_protect=False]'),
  52. ('get_char_term4', 'get_char() [term, immed_chars="xyz"]'),
  53. ),
  54. 'line': (
  55. 'line_input() function',
  56. ('line_input', 'line_input()'),
  57. ('line_input_term1', 'line_input() [term]'),
  58. ('line_input_term2', 'line_input() [term, no hold protect]'),
  59. ('line_input_insert', 'line_input() [inserted text]'),
  60. ('line_input_insert_term1', 'line_input() [inserted text, term]'),
  61. ('line_input_insert_term2', 'line_input() [inserted text, term, no hold protect]'),
  62. ('line_input_edit_term', 'line_input() [edited text, term, utf8]'),
  63. ('line_input_edit_term_insert', 'line_input() [inserted + edited text, term, utf8]'),
  64. ('line_input_erase_term', 'line_input() [inserted + erased text, term]'),
  65. ),
  66. 'password': (
  67. 'password entry via line_input()',
  68. ('password_entry_noecho', 'utf8 password entry'),
  69. ('password_entry_noecho_term', 'utf8 password entry [term]'),
  70. ('password_entry_echo', 'utf8 password entry (echoed)'),
  71. ('password_entry_echo_term', 'utf8 password entry (echoed) [term]'),
  72. ),
  73. 'misc': (
  74. 'miscellaneous user-level UI functions',
  75. ('get_seed_from_stdin', 'reading seed phrase from STDIN'),
  76. ),
  77. 'wallet': (
  78. 'hash preset, password and label entry',
  79. ('get_passphrase_ui', 'hash preset, password and label (wallet.py)'),
  80. ('get_passphrase_cmdline', 'hash preset, password and label (wallet.py - from cmdline)'),
  81. ('get_passphrase_crypto', 'hash preset, password and label (crypto.py)'),
  82. ),
  83. 'mnemonic': (
  84. 'mnemonic entry',
  85. ('mnemonic_entry_mmgen', 'stealth mnemonic entry (mmgen)'),
  86. ('mnemonic_entry_mmgen_minimal', 'stealth mnemonic entry (mmgen - minimal entry mode)'),
  87. ('mnemonic_entry_bip39', 'stealth mnemonic entry (bip39)'),
  88. ('mnemonic_entry_bip39_short', 'stealth mnemonic entry (bip39 - short entry mode)'),
  89. ('mn2hex_interactive_mmgen', 'mn2hex_interactive (mmgen)'),
  90. ('mn2hex_interactive_mmgen_fixed', 'mn2hex_interactive (mmgen - fixed (10-letter) entry mode)'),
  91. ('mn2hex_interactive_bip39', 'mn2hex_interactive (bip39)'),
  92. ('mn2hex_interactive_bip39_short', 'mn2hex_interactive (bip39 - short entry mode (+pad entry))'),
  93. ('mn2hex_interactive_bip39_fixed', 'mn2hex_interactive (bip39 - fixed (4-letter) entry mode)'),
  94. ('mn2hex_interactive_xmr', 'mn2hex_interactive (xmrseed)'),
  95. ('mn2hex_interactive_xmr_short', 'mn2hex_interactive (xmrseed - short entry mode)'),
  96. ),
  97. 'dieroll': (
  98. 'dieroll entry',
  99. ('dieroll_entry', 'dieroll entry (base6d)'),
  100. ('dieroll_entry_usrrand', 'dieroll entry (base6d) with added user entropy'),
  101. )
  102. }
  103. def get_seed_from_stdin(self):
  104. self.spawn('', msg_only=True)
  105. from subprocess import run, PIPE
  106. cmd = ['python3', 'cmds/mmgen-walletconv', '--in-fmt=words', '--out-fmt=words', '--outdir=test/trash']
  107. mn = sample_mn['mmgen']['mn']
  108. run_env = dict(os.environ)
  109. run_env['MMGEN_TEST_SUITE'] = ''
  110. # the test can fail the first time if cfg file has changed, so run it twice if necessary:
  111. for _ in range(2):
  112. cp = run(cmd, input=mn.encode(), stdout=PIPE, stderr=PIPE, env=run_env)
  113. if b'written to file' in cp.stderr:
  114. break
  115. from mmgen.color import set_vt100
  116. set_vt100()
  117. imsg(cp.stderr.decode().strip())
  118. res = get_data_from_file(cfg, 'test/trash/A773B05C[128].mmwords', silent=True).strip()
  119. assert res == mn, f'{res} != {mn}'
  120. return 'ok' if b'written to file' in cp.stderr else 'error'
  121. def get_passphrase_ui(self):
  122. t = self.spawn('test/misc/get_passphrase.py', ['--usr-randchars=0', 'seed'], cmd_dir='.')
  123. # 1 - new wallet, default hp, label;empty pw
  124. t.expect('accept the default.*: ', '\n', regex=True)
  125. # bad repeat
  126. t.expect('new MMGen wallet: ', 'pass1\n')
  127. t.expect('peat passphrase: ', 'pass2\n')
  128. # good repeat
  129. t.expect('new MMGen wallet: ', '\n')
  130. t.expect('peat passphrase: ', '\n')
  131. t.expect('mpty pass')
  132. t.expect('no label: ', '\n')
  133. t.expect('[][3][No Label]')
  134. # 2 - new wallet, user-selected hp, pw, label
  135. t.expect('accept the default.*: ', '1\n', regex=True)
  136. t.expect('new MMGen wallet: ', 'pass1\n')
  137. t.expect('peat passphrase: ', 'pass1\n')
  138. t.expect('no label: ', 'lbl1\n')
  139. t.expect('[pass1][1][lbl1]')
  140. # 3 - passchg, nothing changes
  141. t.expect('new hash preset')
  142. t.expect('reuse the old value.*: ', '\n', regex=True)
  143. t.expect('unchanged')
  144. t.expect('new passphrase.*: ', 'pass1\n', regex=True)
  145. t.expect('peat new passphrase: ', 'pass1\n')
  146. t.expect('unchanged')
  147. t.expect('reuse the label .*: ', '\n', regex=True)
  148. t.expect('unchanged')
  149. t.expect('[pass1][1][lbl1]')
  150. # 4 - passchg, everything changes
  151. t.expect('new hash preset')
  152. t.expect('reuse the old value.*: ', '2\n', regex=True)
  153. t.expect(' changed to')
  154. t.expect('new passphrase.*: ', 'pass2\n', regex=True)
  155. t.expect('peat new passphrase: ', 'pass2\n')
  156. t.expect(' changed')
  157. t.expect('reuse the label .*: ', 'lbl2\n', regex=True)
  158. t.expect(' changed to')
  159. t.expect('[pass2][2][lbl2]')
  160. # 5 - wallet from file
  161. t.expect('from file')
  162. # bad passphrase
  163. t.expect('passphrase for MMGen wallet: ', 'bad\n')
  164. t.expect('Trying again')
  165. # good passphrase
  166. t.expect('passphrase for MMGen wallet: ', 'reference password\n')
  167. t.expect('[reference password][1][No Label]')
  168. return t
  169. def get_passphrase_cmdline(self):
  170. with open('test/trash/pwfile', 'w') as fp:
  171. fp.write('reference password\n')
  172. t = self.spawn('test/misc/get_passphrase.py', [
  173. '--usr-randchars=0',
  174. '--label=MyLabel',
  175. '--passwd-file=test/trash/pwfile',
  176. '--hash-preset=1',
  177. 'seed'],
  178. cmd_dir = '.')
  179. for _ in range(4):
  180. t.expect('[reference password][1][MyLabel]')
  181. return t
  182. def get_passphrase_crypto(self):
  183. t = self.spawn('test/misc/get_passphrase.py', ['--usr-randchars=0', 'crypto'], cmd_dir='.')
  184. # new passwd
  185. t.expect('passphrase for .*: ', 'x\n', regex=True)
  186. t.expect('peat passphrase: ', '\n')
  187. t.expect('passphrase for .*: ', 'pass1\n', regex=True)
  188. t.expect('peat passphrase: ', 'pass1\n')
  189. t.expect('[pass1]')
  190. # existing passwd
  191. t.expect('passphrase for .*: ', 'pass2\n', regex=True)
  192. t.expect('[pass2]')
  193. # hash preset
  194. t.expect('accept the default .*: ', '0\n', regex=True)
  195. t.expect('nvalid')
  196. t.expect('accept the default .*: ', '8\n', regex=True)
  197. t.expect('nvalid')
  198. t.expect('accept the default .*: ', '7\n', regex=True)
  199. t.expect('[7]')
  200. # hash preset (default)
  201. t.expect('accept the default .*: ', '\n', regex=True)
  202. t.expect(f'[{gc.dfl_hash_preset}]')
  203. return t
  204. def _input_func(self, func_name, arg_dfls, func_args, text, expect, term, delay=None):
  205. if term and sys.platform == 'win32':
  206. return ('skip_warn', 'pexpect_spawn not supported on Windows platform')
  207. func_args = dict(zip(arg_dfls.keys(), func_args))
  208. t = self.spawn(
  209. 'test/misc/input_func.py',
  210. [func_name, repr(func_args)],
  211. cmd_dir='.',
  212. pexpect_spawn=term)
  213. imsg('Parameters:')
  214. imsg(f' pexpect_spawn: {term}')
  215. imsg(f' sending: {text!r}')
  216. imsg(f' expecting: {expect!r}')
  217. imsg('\nFunction args:')
  218. for k, v in func_args.items():
  219. imsg(' {:14} {!r}'.format(k+':', v))
  220. imsg_r('\nScript output: ')
  221. prompt_add = (func_args['insert_txt'] if term else '') if func_name == 'line_input' else ''
  222. prompt = func_args['prompt'] + prompt_add
  223. t.expect('Calling ')
  224. if prompt:
  225. t.expect(prompt, text, delay=delay)
  226. else:
  227. t.send(text, delay=delay)
  228. ret = t.expect_getend(' ==> ')
  229. assert ret == repr(expect), f'Text mismatch! {ret} != {repr(expect)}'
  230. return t
  231. def _get_char(self, func_args, text, expect, term):
  232. arg_dfls = {
  233. 'prompt': '',
  234. 'immed_chars': '',
  235. 'prehold_protect': True,
  236. 'num_bytes': 5,
  237. }
  238. return self._input_func('get_char', arg_dfls, func_args, text, expect, term)
  239. def _line_input(self, func_args, text, expect, term, delay=None):
  240. arg_dfls = {
  241. 'prompt': '', # positional
  242. 'echo': True,
  243. 'insert_txt': '',
  244. 'hold_protect': True,
  245. }
  246. return self._input_func('line_input', arg_dfls, func_args, text+'\n', expect, term, delay=delay)
  247. def get_char1(self):
  248. return self._get_char(['prompt> ', '', True, 5], 'x', 'x', False)
  249. def get_char2(self):
  250. expect = 'x' if sys.platform == 'win32' else 'xxxxx'
  251. return self._get_char(['prompt> ', '', True, 5], 'xxxxx', expect, False)
  252. def get_char3(self):
  253. return self._get_char(['', '', True, 5], 'x', 'x', False)
  254. def get_char4(self):
  255. return self._get_char(['prompt> ', '', True, 2], 'α', 'α', False) # UTF-8, must get 2 bytes
  256. def get_char_term1(self):
  257. return self._get_char(['prompt> ', '', True, 2], 'β', 'β', True) # UTF-8, must get 2 bytes
  258. def get_char_term2(self):
  259. return self._get_char(['prompt> ', '', True, 5], 'xxxxx', 'xxxxx', True)
  260. def get_char_term3(self):
  261. return self._get_char(['', '', False, 5], 'x', 'x', True)
  262. def get_char_term4(self):
  263. return self._get_char(['prompt> ', 'xyz', False, 5], 'x', 'x', True)
  264. def line_input(self):
  265. return self._line_input(
  266. ['prompt> ', True, '', True],
  267. 'foo',
  268. 'foo',
  269. False)
  270. def line_input_term1(self):
  271. return self._line_input(
  272. ['prompt> ', True, '', True],
  273. 'foo',
  274. 'foo',
  275. True,
  276. hold_protect_delay)
  277. def line_input_term2(self):
  278. return self._line_input(
  279. ['prompt> ', True, '', False],
  280. 'foo',
  281. 'foo',
  282. True)
  283. def line_input_insert(self):
  284. return self._line_input(
  285. ['prompt> ', True, 'inserted text', True],
  286. 'foo',
  287. 'foo',
  288. False)
  289. def line_input_insert_term1(self):
  290. if self.skip_for_mac('readline text buffer issues'):
  291. return 'skip'
  292. return self._line_input(
  293. ['prompt> ', True, 'foo', True],
  294. 'bar',
  295. 'foobar',
  296. True,
  297. hold_protect_delay)
  298. def line_input_insert_term2(self):
  299. if self.skip_for_mac('readline text buffer issues'):
  300. return 'skip'
  301. return self._line_input(
  302. ['prompt> ', True, 'foo', False],
  303. 'bar',
  304. 'foobar',
  305. True)
  306. def line_input_edit_term(self):
  307. return self._line_input(
  308. ['prompt> ', True, '', True],
  309. '\b\bφυφυ\b\bβαρ',
  310. 'φυβαρ',
  311. True,
  312. hold_protect_delay)
  313. def line_input_edit_term_insert(self):
  314. if self.skip_for_mac('readline text buffer issues'):
  315. return 'skip'
  316. return self._line_input(
  317. ['prompt> ', True, 'φυφυ', True],
  318. '\b\bβαρ',
  319. 'φυβαρ',
  320. True,
  321. hold_protect_delay)
  322. def line_input_erase_term(self):
  323. if self.skip_for_mac('readline text buffer issues'):
  324. return 'skip'
  325. return self._line_input(
  326. ['prompt> ', True, 'foobarbaz', True],
  327. Ctrl_U + 'foobar',
  328. 'foobar',
  329. True,
  330. hold_protect_delay)
  331. def _password_entry(self, prompt, opts=[], term=False):
  332. if term and sys.platform == 'win32':
  333. return ('skip_warn', 'pexpect_spawn not supported on Windows platform')
  334. t = self.spawn('test/misc/input_func.py', opts + ['passphrase'], cmd_dir='.', pexpect_spawn=term)
  335. imsg(f'Terminal: {term}')
  336. pw = 'abc-α'
  337. t.expect(prompt, pw+'\n')
  338. ret = t.expect_getend('Entered: ')
  339. assert ret == pw, f'Password mismatch! {ret} != {pw}'
  340. return t
  341. winskip_msg = """
  342. pexpect_spawn not supported on Windows platform
  343. Perform the following test by hand with non-ASCII password abc-α
  344. or another password in your native alphabet:
  345. test/misc/input_func.py{} passphrase
  346. """
  347. def password_entry_noecho(self, term=False):
  348. return self._password_entry('Enter passphrase: ', term=term)
  349. def password_entry_noecho_term(self):
  350. if self.skip_for_win('no pexpect_spawn'):
  351. return ('skip_warn', '\n' + fmt(self.winskip_msg.format(''), strip_char='\t'))
  352. return self.password_entry_noecho(term=True)
  353. def password_entry_echo(self, term=False):
  354. return self._password_entry('Enter passphrase (echoed): ', ['--echo-passphrase'], term=term)
  355. def password_entry_echo_term(self):
  356. if self.skip_for_win('no pexpect_spawn'):
  357. return ('skip_warn', '\n' + fmt(self.winskip_msg.format(' --echo-passphrase'), strip_char='\t'))
  358. return self.password_entry_echo(term=True)
  359. def _mn2hex(self, fmt, entry_mode='full', mn=None, pad_entry=False, enter_for_dfl=False):
  360. mn = mn or sample_mn[fmt]['mn'].split()
  361. t = self.spawn('mmgen-tool', ['mn2hex_interactive', 'fmt='+fmt, 'mn_len=12', 'print_mn=1'])
  362. from mmgen.mn_entry import mn_entry
  363. mne = mn_entry(cfg, fmt, entry_mode)
  364. t.expect(
  365. 'Type a number.*: ',
  366. ('\n' if enter_for_dfl else str(mne.entry_modes.index(entry_mode)+1)),
  367. regex = True)
  368. t.expect(r'Using entry mode (\S+)', regex=True)
  369. mode = strip_ansi_escapes(t.p.match.group(1)).lower()
  370. assert mode == mne.em.name.lower(), f'{mode} != {mne.em.name.lower()}'
  371. stealth_mnemonic_entry(t, mne, mn, entry_mode=entry_mode, pad_entry=pad_entry)
  372. t.expect(sample_mn[fmt]['hex'])
  373. return t
  374. def _user_seed_entry(
  375. self,
  376. fmt,
  377. usr_rand = False,
  378. out_fmt = None,
  379. entry_mode = 'full',
  380. mn = None,
  381. seedlen_opt = False):
  382. wcls = get_wallet_cls(fmt_code=fmt)
  383. wf = os.path.join(ref_dir, f'FE3C6545.{wcls.ext}')
  384. if wcls.base_type == 'mnemonic':
  385. mn = mn or read_from_file(wf).strip().split()
  386. elif wcls.type == 'dieroll':
  387. mn = mn or list(remove_whitespace(read_from_file(wf)))
  388. for idx, val in ((5, 'x'), (18, '0'), (30, '7'), (44, '9')):
  389. mn.insert(idx, val)
  390. t = self.spawn(
  391. 'mmgen-walletconv',
  392. ['--usr-randchars=10', '--stdout']
  393. + (['--seed-len=128'] if seedlen_opt else [])
  394. + [f'--in-fmt={fmt}', f'--out-fmt={out_fmt or fmt}']
  395. )
  396. t.expect(f'{capfirst(wcls.base_type or wcls.type)} type:.*{wcls.mn_type}', regex=True)
  397. if not seedlen_opt:
  398. t.expect(wcls.choose_seedlen_prompt, '1')
  399. t.expect('(Y/n): ', 'y')
  400. if wcls.base_type == 'mnemonic':
  401. t.expect('Type a number.*: ', '6', regex=True)
  402. t.expect('invalid')
  403. from mmgen.mn_entry import mn_entry
  404. mne = mn_entry(cfg, fmt, entry_mode)
  405. t.expect('Type a number.*: ', str(mne.entry_modes.index(entry_mode)+1), regex=True)
  406. t.expect(r'Using entry mode (\S+)', regex=True)
  407. mode = strip_ansi_escapes(t.p.match.group(1)).lower()
  408. assert mode == mne.em.name.lower(), f'{mode} != {mne.em.name.lower()}'
  409. stealth_mnemonic_entry(t, mne, mn, entry_mode=entry_mode)
  410. elif wcls.type == 'dieroll':
  411. user_dieroll_entry(t, mn)
  412. if usr_rand:
  413. t.expect(wcls.user_entropy_prompt, 'y')
  414. t.usr_rand(10)
  415. else:
  416. t.expect(wcls.user_entropy_prompt, 'n')
  417. if not usr_rand:
  418. sid_chk = 'FE3C6545'
  419. sid = t.expect_getend(f'Valid {wcls.desc} for Seed ID')
  420. sid = strip_ansi_escapes(sid.split(',')[0])
  421. assert sid_chk in sid, f'Seed ID mismatch! {sid_chk} not found in {sid}'
  422. t.expect('to confirm: ', 'YES\n')
  423. return t
  424. def mnemonic_entry_mmgen_minimal(self):
  425. from mmgen.mn_entry import mn_entry
  426. # erase_chars: '\b\x7f'
  427. m = mn_entry(cfg, 'mmgen', 'minimal')
  428. np = 2
  429. mn = (
  430. 'z',
  431. 'aa',
  432. '1d2ud',
  433. 'fo{}ot{}#'.format('1' * np, '2' * (m.em.pad_max - np)), # substring of 'football'
  434. 'des1p)%erate\n', # substring of 'desperately'
  435. '#t!(ie',
  436. '!)sto8o',
  437. 'the123m8!%s',
  438. '349t(5)rip',
  439. 'di\b\bdesce',
  440. 'cea',
  441. 'bu\x7f\x7fsuic',
  442. 'app\bpl',
  443. 'wd',
  444. 'busy')
  445. return self._user_seed_entry('words', entry_mode='minimal', mn=mn)
  446. def mnemonic_entry_mmgen(self):
  447. return self._user_seed_entry('words', entry_mode='full')
  448. def mnemonic_entry_bip39(self):
  449. return self._user_seed_entry('bip39', entry_mode='full')
  450. def mnemonic_entry_bip39_short(self):
  451. return self._user_seed_entry('bip39', entry_mode='short')
  452. def mn2hex_interactive_mmgen(self):
  453. return self._mn2hex('mmgen', entry_mode='full')
  454. def mn2hex_interactive_mmgen_fixed(self):
  455. return self._mn2hex('mmgen', entry_mode='fixed')
  456. def mn2hex_interactive_bip39(self):
  457. return self._mn2hex('bip39', entry_mode='full')
  458. def mn2hex_interactive_bip39_short(self):
  459. return self._mn2hex('bip39', entry_mode='short', pad_entry=True)
  460. def mn2hex_interactive_bip39_fixed(self):
  461. return self._mn2hex('bip39', entry_mode='fixed', enter_for_dfl=True)
  462. def mn2hex_interactive_xmr(self):
  463. return self._mn2hex('xmrseed', entry_mode='full')
  464. def mn2hex_interactive_xmr_short(self):
  465. return self._mn2hex('xmrseed', entry_mode='short')
  466. def dieroll_entry(self):
  467. return self._user_seed_entry('dieroll', seedlen_opt=True)
  468. def dieroll_entry_usrrand(self):
  469. return self._user_seed_entry('dieroll', usr_rand=True, out_fmt='bip39')