From d6f82fb6c9800974079f4e5ad02ee0a94a1491a7 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Tue, 18 Jan 2022 09:10:59 +0000 Subject: [PATCH] baseconv: move Monero mnemonic code to new xmrseed class - a new unit test has also been added Testing: $ test/unit_tests.py mn_entry xmrseed $ test/tooltest2.py hex2mn mn2hex $ test/test.py ref ref3_addr --- mmgen/baseconv.py | 58 ++------------ mmgen/mn_entry.py | 2 +- mmgen/passwdlist.py | 13 ++-- mmgen/tool.py | 3 +- mmgen/xmrseed.py | 101 +++++++++++++++++++++++++ mmgen/xmrwallet.py | 4 +- test/tooltest2.py | 1 + test/unit_tests_d/ut_baseconv.py | 21 +----- test/unit_tests_d/ut_rpc.py | 4 +- test/unit_tests_d/ut_xmrseed.py | 126 +++++++++++++++++++++++++++++++ 10 files changed, 251 insertions(+), 82 deletions(-) create mode 100755 mmgen/xmrseed.py create mode 100755 test/unit_tests_d/ut_xmrseed.py diff --git a/mmgen/baseconv.py b/mmgen/baseconv.py index ab36b821..95d3b7a4 100755 --- a/mmgen/baseconv.py +++ b/mmgen/baseconv.py @@ -30,9 +30,6 @@ def is_b58_str(s): def is_b32_str(s): return set(list(s)) <= set(baseconv.digits['b32']) -def is_xmrseed(s): - return bool( baseconv('xmrseed').tobytes(s.split()) ) - class baseconv(object): desc = { @@ -45,7 +42,6 @@ class baseconv(object): # 'tirosh':('Tirosh mnemonic', 'base1626 mnemonic using truncated Tirosh wordlist'), # not used by wallet 'mmgen': ('MMGen native mnemonic', 'MMGen native mnemonic seed phrase created using old Electrum wordlist and simple base conversion'), - 'xmrseed': ('Monero mnemonic', 'Monero new-style mnemonic seed phrase'), } # https://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet # https://tools.ietf.org/html/rfc4648 @@ -60,7 +56,6 @@ class baseconv(object): mn_base = 1626 wl_chksums = { 'mmgen': '5ca31424', - 'xmrseed':'3c381ebb', # 'tirosh': '48f05e1f', # tirosh truncated to mn_base # 'tirosh1633': '1a5faeff' # tirosh list is 1633 words long! } @@ -68,13 +63,11 @@ class baseconv(object): 'b58': { 16:22, 24:33, 32:44 }, 'b6d': { 16:50, 24:75, 32:100 }, 'mmgen': { 16:12, 24:18, 32:24 }, - 'xmrseed': { 32:25 }, } seedlen_map_rev = { 'b58': { 22:16, 33:24, 44:32 }, 'b6d': { 50:16, 75:24, 100:32 }, 'mmgen': { 12:16, 18:24, 24:32 }, - 'xmrseed': { 25:32 }, } def __init__(self,wl_id): @@ -82,9 +75,6 @@ class baseconv(object): if wl_id == 'mmgen': from .mn_electrum import words self.digits[wl_id] = words - elif wl_id == 'xmrseed': - from .mn_monero import words - self.digits[wl_id] = words elif wl_id not in self.digits: raise ValueError(f'{wl_id}: unrecognized mnemonic ID') @@ -132,12 +122,6 @@ class baseconv(object): else: raise BaseConversionPadError(f"{pad!r}: illegal value for 'pad' (must be None,'seed' or int)") - @staticmethod - def monero_mn_checksum(words): - from binascii import crc32 - wstr = ''.join(word[:3] for word in words) - return words[crc32(wstr.encode()) % len(words)] - def tohex(self,words_arg,pad=None): "convert string or list data of instance base to hex string" return self.tobytes(words_arg,pad//2 if type(pad)==int else pad).hex() @@ -168,21 +152,6 @@ class baseconv(object): ( 'seed data' if pad == 'seed' else f'{words_arg!r}:' ) + f' not in {desc} format' ) - if self.wl_id == 'xmrseed': - if len(words) not in self.seedlen_map_rev['xmrseed']: - die(2,f'{len(words)}: invalid length for Monero mnemonic') - - z = self.monero_mn_checksum(words[:-1]) - assert z == words[-1],'invalid Monero mnemonic checksum' - words = tuple(words[:-1]) - - ret = b'' - for i in range(len(words)//3): - w1,w2,w3 = [wl.index(w) for w in words[3*i:3*i+3]] - x = w1 + base*((w2-w1)%base) + base*base*((w3-w2)%base) - ret += x.to_bytes(4,'big')[::-1] - return ret - ret = sum([wl.index(words[::-1][i])*(base**i) for i in range(len(words))]) bl = ret.bit_length() return ret.to_bytes(max(pad_val,bl//8+bool(bl%8)),'big') @@ -214,28 +183,15 @@ class baseconv(object): pad = max(self.get_pad(pad,get_seed_pad),1) wl = self.digits[self.wl_id] - base = len(wl) - if self.wl_id == 'xmrseed': - if len(bytestr) not in self.seedlen_map['xmrseed']: - die(2, f'{len(bytestr)}: invalid seed byte length for Monero mnemonic') - - def num2base_monero(num): - w1 = num % base - w2 = (num//base + w1) % base - w3 = (num//base//base + w2) % base - return [wl[w1], wl[w2], wl[w3]] - - o = [] - for i in range(len(bytestr)//4): - o += num2base_monero(int.from_bytes(bytestr[i*4:i*4+4][::-1],'big')) - o.append(self.monero_mn_checksum(o)) - else: + def gen(): num = int.from_bytes(bytestr,'big') - ret = [] + base = len(wl) while num: - ret.append(num % base) + yield num % base num //= base - o = [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]] - return (' ' if self.wl_id in ('mmgen','xmrseed') else '').join(o) if tostr else o + ret = list(gen()) + o = [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]] + + return (' ' if self.wl_id == 'mmgen' else '').join(o) if tostr else o diff --git a/mmgen/mn_entry.py b/mmgen/mn_entry.py index 536200ac..ada445f7 100755 --- a/mmgen/mn_entry.py +++ b/mmgen/mn_entry.py @@ -420,7 +420,7 @@ class MnemonicEntryBIP39(MnemonicEntry): class MnemonicEntryMonero(MnemonicEntry): wl_id = 'xmrseed' - modname = 'baseconv' + modname = 'xmrseed' entry_modes = ('full','short') dfl_entry_mode = 'short' has_chksum = True diff --git a/mmgen/passwdlist.py b/mmgen/passwdlist.py index 3f5550c1..33bfe120 100755 --- a/mmgen/passwdlist.py +++ b/mmgen/passwdlist.py @@ -25,8 +25,9 @@ from collections import namedtuple from .exception import InvalidPasswdFormat from .util import ymsg,is_hex_str,is_int,keypress_confirm from .obj import ImmutableAttr,ListItemAttr,MMGenPWIDString -from .baseconv import baseconv,is_b32_str,is_b58_str,is_xmrseed +from .baseconv import baseconv,is_b32_str,is_b58_str from .bip39 import is_bip39_str +from .xmrseed import is_xmrseed from .key import PrivKey from .addr import MMGenPasswordType,AddrIdx,AddrListID from .addrlist import ( @@ -158,9 +159,10 @@ class PasswordList(AddrList): pw_bytes = bip39.nwords2seedlen(self.pw_len,in_bytes=True) good_pw_len = bip39.seedlen2nwords(seed.byte_len,in_bytes=True) elif pf == 'xmrseed': - pw_bytes = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] + from .xmrseed import xmrseed + pw_bytes = xmrseed.seedlen_map_rev['xmrseed'][self.pw_len] try: - good_pw_len = baseconv.seedlen_map['xmrseed'][seed.byte_len] + good_pw_len = xmrseed.seedlen_map['xmrseed'][seed.byte_len] except: die(1,f'{seed.byte_len*8}: unsupported seed length for Monero new-style mnemonic') elif pf in ('b32','b58'): @@ -196,12 +198,13 @@ class PasswordList(AddrList): # take most significant part return ' '.join( bip39().fromhex(secbytes[:pw_len_bytes].hex()) ) elif self.pw_fmt == 'xmrseed': - pw_len_bytes = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] + from .xmrseed import xmrseed + pw_len_bytes = xmrseed.seedlen_map_rev['xmrseed'][self.pw_len] from .protocol import init_proto bytes_preproc = init_proto('xmr').preprocess_key( secbytes[:pw_len_bytes], # take most significant part None ) - return ' '.join( baseconv('xmrseed').frombytes(bytes_preproc) ) + return ' '.join( xmrseed().frombytes(bytes_preproc) ) else: # take least significant part return baseconv(self.pw_fmt).frombytes( diff --git a/mmgen/tool.py b/mmgen/tool.py index b6489ff0..5eeb667a 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -29,6 +29,7 @@ from .addr import * from .addrlist import AddrList,KeyAddrList from .passwdlist import PasswordList from .baseconv import baseconv +from .xmrseed import xmrseed from .bip39 import bip39 NL = ('\n','\r\n')[g.platform=='win'] @@ -238,7 +239,7 @@ mft = namedtuple('mnemonic_format',['fmt','pad','conv_cls']) mnemonic_fmts = { 'mmgen': mft( 'words', 'seed', baseconv ), 'bip39': mft( 'bip39', None, bip39 ), - 'xmrseed': mft( 'xmrseed', None, baseconv ), + 'xmrseed': mft( 'xmrseed', None, xmrseed ), } mn_opts_disp = _options_annot_str(mnemonic_fmts) diff --git a/mmgen/xmrseed.py b/mmgen/xmrseed.py new file mode 100755 index 00000000..e6786d82 --- /dev/null +++ b/mmgen/xmrseed.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2022 The MMGen Project +# +# 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 . + +""" +xmrseed.py: Monero mnemonic conversion class for the MMGen suite +""" + +from .exception import * +from .baseconv import baseconv +from .util import die + +def is_xmrseed(s): + return bool( xmrseed().tobytes(s.split()) ) + +# implements a subset of the baseconv API +class xmrseed(baseconv): + + desc = { 'xmrseed': ('Monero mnemonic', 'Monero new-style mnemonic seed phrase') } + wl_chksums = { 'xmrseed': '3c381ebb' } + seedlen_map = { 'xmrseed': { 32:25 } } + seedlen_map_rev = { 'xmrseed': { 25:32 } } + + def __init__(self,wl_id='xmrseed'): + assert wl_id == 'xmrseed', "initialize with 'xmrseed' for compatibility with baseconv API" + from .mn_monero import words + self.digits = { 'xmrseed': words } + self.wl_id = 'xmrseed' + + @staticmethod + def monero_mn_checksum(words): + from binascii import crc32 + wstr = ''.join(word[:3] for word in words) + return words[crc32(wstr.encode()) % len(words)] + + def tobytes(self,words,pad=None): + assert isinstance(words,(list,tuple)),'words must be list or tuple' + assert pad == None, f"{pad}: invalid 'pad' argument (must be None)" + + desc = self.desc[self.wl_id][0] + wl = self.digits[self.wl_id] + base = len(wl) + + if not set(words) <= set(wl): + raise MnemonicError( f'{words!r}: not in {desc} format' ) + + if len(words) not in self.seedlen_map_rev['xmrseed']: + raise MnemonicError( f'{len(words)}: invalid seed phrase length for {desc}' ) + + z = self.monero_mn_checksum(words[:-1]) + if z != words[-1]: + raise MnemonicError(f'invalid {desc} checksum') + words = tuple(words[:-1]) + + def gen(): + for i in range(len(words)//3): + w1,w2,w3 = [wl.index(w) for w in words[3*i:3*i+3]] + x = w1 + base*((w2-w1)%base) + base*base*((w3-w2)%base) + yield x.to_bytes(4,'big')[::-1] + + return b''.join(gen()) + + def frombytes(self,bytestr,pad=None,tostr=False): + assert pad == None, f"{pad}: invalid 'pad' argument (must be None)" + + desc = self.desc[self.wl_id][0] + wl = self.digits[self.wl_id] + base = len(wl) + + if len(bytestr) not in self.seedlen_map['xmrseed']: + raise SeedLengthError(f'{len(bytestr)}: invalid seed byte length for {desc}') + + def num2base_monero(num): + w1 = num % base + w2 = (num//base + w1) % base + w3 = (num//base//base + w2) % base + return ( wl[w1], wl[w2], wl[w3] ) + + def gen(): + for i in range(len(bytestr)//4): + for e in num2base_monero( int.from_bytes( bytestr[i*4:i*4+4][::-1], 'big' ) ): + yield e + + o = list(gen()) + o.append( self.monero_mn_checksum(o) ) + + return ' '.join(o) if tostr else tuple(o) diff --git a/mmgen/xmrwallet.py b/mmgen/xmrwallet.py index d6d5f2e3..0c07e301 100755 --- a/mmgen/xmrwallet.py +++ b/mmgen/xmrwallet.py @@ -579,12 +579,12 @@ class MoneroWalletOps: async def process_wallet(self,d,fn,last): msg_r('') # for pexpect - from .baseconv import baseconv + from .xmrseed import xmrseed ret = await self.c.call( 'restore_deterministic_wallet', filename = os.path.basename(fn), password = d.wallet_passwd, - seed = baseconv('xmrseed').fromhex(d.sec.wif,tostr=True), + seed = xmrseed().fromhex(d.sec.wif,tostr=True), restore_height = uopt.restore_height, language = 'English' ) diff --git a/test/tooltest2.py b/test/tooltest2.py index 0597ca1f..6e0ba53e 100755 --- a/test/tooltest2.py +++ b/test/tooltest2.py @@ -34,6 +34,7 @@ sys.path.insert(0,overlay_setup(repo_root)) from mmgen.common import * from test.include.common import * from mmgen.wallet import is_bip39_mnemonic,is_mmgen_mnemonic +from mmgen.xmrseed import is_xmrseed from mmgen.baseconv import * skipped_tests = ['mn2hex_interactive'] diff --git a/test/unit_tests_d/ut_baseconv.py b/test/unit_tests_d/ut_baseconv.py index 0937b84f..a197a659 100755 --- a/test/unit_tests_d/ut_baseconv.py +++ b/test/unit_tests_d/ut_baseconv.py @@ -9,23 +9,6 @@ from mmgen.exception import * class unit_test(object): vectors = { - 'xmrseed': ( - # 42nsXK8WbVGTNayQ6Kjw5UdgqbQY5KCCufdxdCgF7NgTfjC69Mna7DJSYyie77hZTQ8H92G2HwgFhgEUYnDzrnLnQdF28r3 - (('0000000000000000000000000000000000000000000000000000000000000001','seed'), # 0x1 - 'abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey bamboo jaws jerseys abbey'), - - # 49voQEbjouUQSDikRWKUt1PGbS47TBde4hiGyftN46CvTDd8LXCaimjHRGtofCJwY5Ed5QhYwc12P15AH5w7SxUAMCz1nr1 - (('1c95988d7431ecd670cf7d73f45befc6feffffffffffffffffffffffffffff0f','seed'), # 0xffffffff * 8 - 'powder directed sayings enmity bacon vapidly entrance bumper noodles iguana sleepless nasty flying soil software foamy solved soggy foamy solved soggy jury yawning ankle solved'), - - # 41i7saPWA53EoHenmJVRt34dubPxsXwoWMnw8AdMyx4mTD1svf7qYzcVjxxRfteLNdYrAxWUMmiPegFW9EfoNgXx7vDMExv - (('e8164dda6d42bd1e261a3406b2038dcbddadbeefdeadbeefdeadbeefdeadbe0f','seed'), # 0xdeadbeef * 8 - 'viewpoint donuts ardent template unveil agile meant unafraid urgent athlete rustled mime azure jaded hawk baby jagged haystack baby jagged haystack ramped oncoming point template'), - - # 42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm - (('148d78d2aba7dbca5cd8f6abcfb0b3c009ffbdbea1ff373d50ed94d78286640e','seed'), # Monero repo - 'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted'), - ), 'b58': ( (('00',None),'1'), (('00',1),'1'), @@ -180,9 +163,7 @@ class unit_test(object): for (hexstr,pad),ret_chk in data: if type(pad) == int: pad = len(hexstr) - ret = baseconv(base).tohex( - ret_chk.split() if base == 'xmrseed' else ret_chk, - pad=pad) + ret = baseconv(base).tohex( ret_chk, pad=pad ) if pad == None: assert int(ret,16) == int(hexstr,16), rerr.format(int(ret,16),int(hexstr,16)) else: diff --git a/test/unit_tests_d/ut_rpc.py b/test/unit_tests_d/ut_rpc.py index 1e8ec9ba..74119aa6 100755 --- a/test/unit_tests_d/ut_rpc.py +++ b/test/unit_tests_d/ut_rpc.py @@ -139,7 +139,7 @@ class unit_tests: 'restore_deterministic_wallet', filename = fn, password = 'foo', - seed = baseconv('xmrseed').fromhex('beadface'*8,tostr=True) ) + seed = xmrseed().fromhex('beadface'*8,tostr=True) ) qmsg(f'Opening {wd.network} wallet') await c.call( 'open_wallet', filename=fn, password='foo' ) @@ -152,7 +152,7 @@ class unit_tests: gmsg('OK') - from mmgen.baseconv import baseconv + from mmgen.xmrseed import xmrseed import shutil shutil.rmtree('test/trash2',ignore_errors=True) os.makedirs('test/trash2') diff --git a/test/unit_tests_d/ut_xmrseed.py b/test/unit_tests_d/ut_xmrseed.py new file mode 100755 index 00000000..ff36f527 --- /dev/null +++ b/test/unit_tests_d/ut_xmrseed.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +""" +test/unit_tests_d/ut_xmrseed: Monero mnemonic unit test for the MMGen suite +""" + +from mmgen.common import * +from mmgen.exception import * + +class unit_test(object): + + vectors = ( # private keys are reduced + ( '148d78d2aba7dbca5cd8f6abcfb0b3c009ffbdbea1ff373d50ed94d78286640e', # Monero repo + 'velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches ' + + 'lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted', + ), + ( 'e8164dda6d42bd1e261a3406b2038dcbddadbeefdeadbeefdeadbeefdeadbe0f', + 'viewpoint donuts ardent template unveil agile meant unafraid urgent athlete rustled mime azure ' + + 'jaded hawk baby jagged haystack baby jagged haystack ramped oncoming point template' + ), + ( '6900dea9753f5c7ced87b53bdcfb109a8417bca6a2797a708194157b227fb60b', + 'criminal bamboo scamper gnaw limits womanly wrong tuition birth mundane donuts square cohesive ' + + 'dolphin titans narrate fuel saved wrap aloof magically mirror together update wrap' + ), + ( '0000000000000000000000000000000000000000000000000000000000000001', + 'abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey abbey ' + + 'abbey abbey abbey abbey abbey bamboo jaws jerseys abbey' + ), + ( '1c95988d7431ecd670cf7d73f45befc6feffffffffffffffffffffffffffff0f', + 'powder directed sayings enmity bacon vapidly entrance bumper noodles iguana sleepless nasty flying ' + + 'soil software foamy solved soggy foamy solved soggy jury yawning ankle solved' + ), + ( '2c94988d7431ecd670cf7d73f45befc6feffffffffffffffffffffffffffff0f', + 'memoir apart olive enmity bacon vapidly entrance bumper noodles iguana sleepless nasty flying soil ' + + 'software foamy solved soggy foamy solved soggy jury yawning ankle foamy' + ), + ( '4bb0288c9673b69fa68c2174851884abbaaedce6af48a03bbfd25e8cd0364102', + 'rated bicycle pheasants dejected pouch fizzle shipped rash citadel queen avatar sample muzzle mews ' + + 'jagged origin yeti dunes obtains godfather unbending pastry vortex washing citadel' + ), + ( '4bb0288c9673b69fa68c2174851884abbaaedce6af48a03bbfd25e8cd0364100', + 'rated bicycle pheasants dejected pouch fizzle shipped rash citadel queen avatar sample muzzle mews ' + + 'jagged origin yeti dunes obtains godfather unbending kangaroo auctions audio citadel' + ), + ( '1d95988d7431ecd670cf7d73f45befc6feffffffffffffffffffffffffffff0e', + 'pram distance scamper enmity bacon vapidly entrance bumper noodles iguana sleepless nasty flying ' + + 'soil software foamy solved soggy foamy solved soggy hashing mullet onboard solved' + ), + ( 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0f', + 'foamy solved soggy foamy solved soggy foamy solved soggy foamy solved soggy foamy solved soggy ' + + 'foamy solved soggy foamy solved soggy jury yawning ankle soggy' + ), + ) + + def run_test(self,name,ut): + + def test_fromhex(b): + vmsg('') + qmsg('Checking seed to mnemonic conversion:') + for privhex,chk in self.vectors: + vmsg(f' {chk}') + chk = tuple(chk.split()) + res = b.fromhex(privhex) + if use_moneropy: + mp_chk = tuple( mnemonic.mn_encode(privhex) ) + assert res[:24] == mp_chk, f'check failed:\nres: {res[:24]}\nchk: {chk}' + assert res == chk, f'check failed:\nres: {res}\nchk: {chk}' + + def test_tohex(b): + vmsg('') + qmsg('Checking mnemonic to seed conversion:') + for chk,words in self.vectors: + vmsg(f' {chk}') + res = b.tohex( words.split() ) + if use_moneropy: + mp_chk = mnemonic.mn_decode( words.split() ) + assert res == mp_chk, f'check failed:\nres: {res}\nchk: {mp_chk}' + assert res == chk, f'check failed:\nres: {res}\nchk: {chk}' + + msg_r('Testing xmrseed conversion routines...') + qmsg('') + + from mmgen.xmrseed import xmrseed + + b = xmrseed() + b.check_wordlist() + + try: + from moneropy import mnemonic + except ImportError: + use_moneropy = False + ymsg('Warning: unable to import moneropy, skipping external library checks') + else: + use_moneropy = True + + test_fromhex(b) + test_tohex(b) + + vmsg('') + qmsg('Checking error handling:') + + bad_chksum_mn = ('abbey ' * 21 + 'bamboo jaws jerseys donuts').split() + bad_word_mn = "admire zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo".split() + bad_seed = 'deadbeef' + good_mn = self.vectors[0][1].split() + good_hex = self.vectors[0][0] + bad_len_mn = good_mn[:22] + + th = b.tohex + fh = b.fromhex + bad_data = ( +('hex', 'HexadecimalStringError', 'not a hexadecimal', lambda:fh('xx')), +('seed len', 'SeedLengthError', 'invalid seed byte len', lambda:fh(bad_seed)), +('mnemonic type', 'AssertionError', 'must be list', lambda:th('string')), +('pad arg (fromhex)', 'AssertionError', "invalid 'pad' arg", lambda:fh(good_hex,pad=23)), +('pad arg (tohex)', 'AssertionError', "invalid 'pad' arg", lambda:th(good_mn,pad=23)), +('word', 'MnemonicError', "not in Monero", lambda:th(bad_word_mn)), +('checksum', 'MnemonicError', "checksum", lambda:th(bad_chksum_mn)), +('seed phrase len', 'MnemonicError', "phrase len", lambda:th(bad_len_mn)), + ) + + ut.process_bad_data(bad_data) + + vmsg('') + msg('OK') + + return True