Browse Source

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
The MMGen Project 3 years ago
parent
commit
d6f82fb6c9

+ 7 - 51
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

+ 1 - 1
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

+ 8 - 5
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(

+ 2 - 1
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)
 

+ 101 - 0
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 <mmgen@tuta.io>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+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)

+ 2 - 2
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' )
 

+ 1 - 0
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']

+ 1 - 20
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:

+ 2 - 2
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')

+ 126 - 0
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