From 3931cc7be14b003e2509e563574aa6898e903517 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Wed, 30 Oct 2019 12:49:27 +0000 Subject: [PATCH] MMGen & BIP39 mnemonic: refactor interactive input code, clean up test --- mmgen/baseconv.py | 2 ++ mmgen/bip39.py | 2 ++ mmgen/globalvars.py | 3 ++ mmgen/seed.py | 61 +++++++++++++++++++++++---------------- test/test_py_d/common.py | 2 -- test/test_py_d/ts_misc.py | 30 +++++++++++-------- 6 files changed, 61 insertions(+), 39 deletions(-) diff --git a/mmgen/baseconv.py b/mmgen/baseconv.py index a7dbb740..81d35e18 100755 --- a/mmgen/baseconv.py +++ b/mmgen/baseconv.py @@ -58,10 +58,12 @@ class baseconv(object): seedlen_map = { 'b58': { 16:22, 24:33, 32:44 }, 'b6d': { 16:50, 24:75, 32:100 }, + 'mmgen': { 16:12, 24:18, 32:24 }, } seedlen_map_rev = { 'b58': { 22:16, 33:24, 44:32 }, 'b6d': { 50:16, 75:24, 100:32 }, + 'mmgen': { 12:16, 18:24, 24:32 }, } @classmethod diff --git a/mmgen/bip39.py b/mmgen/bip39.py index 40e27e6b..b6f27061 100755 --- a/mmgen/bip39.py +++ b/mmgen/bip39.py @@ -2082,6 +2082,8 @@ zoo mn_base = 2048 wl_chksums = { 'bip39': 'f18b9a84' } + seedlen_map = { 'bip39': { 16:12, 24:18, 32:24 } } + seedlen_map_rev = { 'bip39': { 12:16, 18:24, 24:32 } } # ENT CS MS constants = { '128': (4, 12), diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index 68232318..959a0a29 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -204,6 +204,9 @@ class g(object): max_tx_file_size = 100000 max_input_size = 1024 * 1024 + # pexpect chokes on these utf8 chars under MSYS2 + lq,rq = (('“','”'),('"','"'))[bool(os.getenv('MMGEN_TEST_SUITE')) and platform=='win'] + passwd_max_tries = 5 max_urandchars = 80 diff --git a/mmgen/seed.py b/mmgen/seed.py index 91fa0f78..35fd50ba 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -692,6 +692,29 @@ class SeedSourceUnenc(SeedSource): self.ext, x='-α' if g.debug_utf8 else '') + def _choose_seedlen(self,desc,ok_lens,subtype): + + from mmgen.term import get_char + + def choose_len(): + prompt = self.choose_seedlen_prompt + while True: + r = get_char('\r'+prompt).decode() + if is_int(r) and 1 <= int(r) <= len(ok_lens): + break + msg_r(('\r','\n')[g.test_suite] + ' '*len(prompt) + '\r') + return ok_lens[int(r)-1] + + m1 = blue('{} type:'.format(capfirst(desc))) + m2 = yellow(subtype) + msg('{} {}'.format(m1,m2)) + + while True: + usr_len = choose_len() + prompt = self.choose_seedlen_confirm.format(usr_len) + if keypress_confirm(prompt,default_yes=True,no_nl=not g.test_suite): + return usr_len + class SeedSourceEnc(SeedSource): _msg = { @@ -819,40 +842,28 @@ class MMGenMnemonic(SeedSourceUnenc): stdin_ok = True fmt_codes = 'mmwords','words','mnemonic','mnem','mn','m' desc = 'MMGen native mnemonic data' - mn_name = 'MMGen native' + wclass = 'mnemonic' + mn_type = 'MMGen native' ext = 'mmwords' - mn_lens = [i // 32 * 3 for i in g.seed_lens] wl_id = 'mmgen' conv_cls = baseconv + choose_seedlen_prompt = 'Choose a mnemonic length: 1) 12 words, 2) 18 words, 3) 24 words: ' + choose_seedlen_confirm = 'Mnemonic length of {} words chosen. OK?' + + @property + def mn_lens(self): + return sorted(self.conv_cls.seedlen_map_rev[self.wl_id]) def _get_data_from_user(self,desc): if not g.stdin_tty: return get_data_from_user(desc) - from mmgen.term import get_char_raw,get_char - - def choose_mn_len(): - prompt = 'Choose a mnemonic length: 1) 12 words, 2) 18 words, 3) 24 words: ' - urange = [str(i+1) for i in range(len(self.mn_lens))] - while True: - r = get_char('\r'+prompt).decode() - if r in urange: break - msg_r(('\r','\n')[g.test_suite] + ' '*len(prompt) + '\r') - return self.mn_lens[int(r)-1] - - msg('{} {}'.format(blue('Mnemonic type:'),yellow(self.mn_name))) - - while True: - mn_len = choose_mn_len() - prompt = 'Mnemonic length of {} words chosen. OK?'.format(mn_len) - if keypress_confirm(prompt,default_yes=True,no_nl=not g.test_suite): - break + mn_len = self._choose_seedlen(self.wclass,self.mn_lens,self.mn_type) self.conv_cls.init_mn(self.wl_id) wl = self.conv_cls.digits[self.wl_id] longest_word = max(len(w) for w in wl) - from string import ascii_lowercase m = 'Enter your {ml}-word seed phrase, hitting ENTER or SPACE after each word.\n' m += "Optionally, you may use pad characters. Anything you type that's not a\n" @@ -861,10 +872,10 @@ class MMGenMnemonic(SeedSourceUnenc): m += "of words. For each word, once you've typed {lw} characters total (including\n" m += 'pad characters) any pad character will enter the word.' - # pexpect chokes on these utf8 chars under MSYS2 - lq,rq = (('“','”'),('"','"'))[g.test_suite and g.platform=='win'] - msg(m.format(ml=mn_len,lw=longest_word,lq=lq,rq=rq)) + msg(m.format(ml=mn_len,lw=longest_word,lq=g.lq,rq=g.rq)) + from string import ascii_lowercase + from mmgen.term import get_char_raw def get_word(): s,pad = '',0 while True: @@ -954,7 +965,7 @@ class BIP39Mnemonic(MMGenMnemonic): fmt_codes = ('bip39',) desc = 'BIP39 mnemonic data' - mn_name = 'BIP39' + mn_type = 'BIP39' ext = 'bip39' wl_id = 'bip39' diff --git a/test/test_py_d/common.py b/test/test_py_d/common.py index daa99845..9036cc38 100755 --- a/test/test_py_d/common.py +++ b/test/test_py_d/common.py @@ -46,8 +46,6 @@ non_mmgen_fn = 'coinkey' ref_dir = os.path.join('test','ref') dfl_words_file = os.path.join(ref_dir,'98831F3A.mmwords') -mn_words_mmgen = os.path.join(ref_dir,'FE3C6545.mmwords') -mn_words_bip39 = os.path.join(ref_dir,'FE3C6545.bip39') from mmgen.obj import MMGenTXLabel,TwComment diff --git a/test/test_py_d/ts_misc.py b/test/test_py_d/ts_misc.py index eb141d1a..dc4d852e 100755 --- a/test/test_py_d/ts_misc.py +++ b/test/test_py_d/ts_misc.py @@ -25,6 +25,7 @@ from test.common import * from test.test_py_d.common import * from test.test_py_d.ts_base import * from test.test_py_d.ts_main import TestSuiteMain +from mmgen.seed import SeedSource class TestSuiteHelp(TestSuiteBase): 'help, info and usage screens' @@ -143,23 +144,28 @@ class TestSuiteInput(TestSuiteBase): return 'skip' # pexpect double-escapes utf8, so skip return self.password_entry('Enter passphrase (echoed): ',['--echo-passphrase']) - def _mnemonic_entry(self,fmt,mn_name,wf): - mn = read_from_file(wf).strip().split() - mn = ['foo'] + mn[:5] + ['grac','graceful'] + mn[5:] - t = self.spawn('mmgen-walletconv',['-S','-i',fmt,'-o',fmt]) - t.expect('Mnemonic type: {}'.format(mn_name)) - t.expect('words: ','1') + def _user_seed_entry(self,fmt,usr_rand=False,out_fmt=None): + wcls = SeedSource.fmt_code_to_type(fmt) + wf = os.path.join(ref_dir,'FE3C6545.{}'.format(wcls.ext)) + if wcls.wclass == 'mnemonic': + mn = read_from_file(wf).strip().split() + mn = ['foo'] + mn[:5] + ['grac','graceful'] + mn[5:] + t = self.spawn('mmgen-walletconv',['-r10','-S','-i',fmt,'-o',out_fmt or fmt]) + t.expect('{} type: {}'.format(capfirst(wcls.wclass),wcls.mn_type)) + t.expect(wcls.choose_seedlen_prompt,'1') t.expect('(Y/n): ','y') - stealth_mnemonic_entry(t,mn,fmt=fmt) - sid_chk = 'FE3C6545' - sid = t.expect_getend('Valid {} mnemonic data for Seed ID '.format(mn_name))[:8] - assert sid == sid_chk,'Seed ID mismatch! {} != {}'.format(sid,sid_chk) + if wcls.wclass == 'mnemonic': + stealth_mnemonic_entry(t,mn,fmt=fmt) + if not usr_rand: + sid_chk = 'FE3C6545' + sid = t.expect_getend('Valid {} for Seed ID '.format(wcls.desc))[:8] + assert sid == sid_chk,'Seed ID mismatch! {} != {}'.format(sid,sid_chk) t.expect('to confirm: ','YES\n') t.read() return t - def mnemonic_entry_mmgen(self): return self._mnemonic_entry('words','MMGen native',mn_words_mmgen) - def mnemonic_entry_bip39(self): return self._mnemonic_entry('bip39','BIP39',mn_words_bip39) + def mnemonic_entry_mmgen(self): return self._user_seed_entry('words') + def mnemonic_entry_bip39(self): return self._user_seed_entry('bip39') class TestSuiteTool(TestSuiteMain,TestSuiteBase): "tests for interactive 'mmgen-tool' commands"