Browse Source

limited Monero mnemonic seed phrase ('xmrseed') support

- only 256-bit (25-word) new-style mnemonics are supported

Testing:

  $ test/unit_tests.py baseconv
  $ test/tooltest2.py hex2mn mn2hex
  $ test/scrambletest.py pw
  $ test/test.py ref_xmrseed_25_passwdgen_3
  $ test/test.py ref_passwdfile_chk_xmrseed_25

The following operations are supported:

  Generate a random Monero mnemonic:

  $ mmgen-tool mn_rand256 fmt=xmrseed

  Generate a Monero mnemonic from hexadecimal data:

  $ mmgen-tool hex2mn deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef fmt=xmrseed

  Convert the resulting mnemonic back to hexadecimal data:

  $ mmgen-tool mn2hex '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' fmt=xmrseed

  Note that the result of the reversal does not match the original input.  This
  is because input data is reduced to a spendkey before conversion so that a
  canonical seed phrase is produced.  This is required because Monero seeds,
  unlike ordinary wallet seeds, are tied to a concrete key/address pair.  The
  spendkey can be generated directly using the `hex2wif` command:

  $ mmgen-tool --coin=xmr hex2wif deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef

  Generate a list of passwords in Monero mnemonic format with ID 'mymonero':

  $ mmgen-passgen -f xmrseed 'mymonero' 1-10
The MMGen Project 5 years ago
parent
commit
cfa16418

+ 21 - 4
mmgen/addr.py

@@ -783,6 +783,9 @@ def is_bip39_str(s):
 	from mmgen.bip39 import bip39
 	from mmgen.bip39 import bip39
 	return bool(bip39.tohex(s.split(),wl_id='bip39'))
 	return bool(bip39.tohex(s.split(),wl_id='bip39'))
 
 
+def is_xmrseed(s):
+	return bool(baseconv.tobytes(s.split(),wl_id='xmrseed'))
+
 from collections import namedtuple
 from collections import namedtuple
 class PasswordList(AddrList):
 class PasswordList(AddrList):
 	msgs = {
 	msgs = {
@@ -824,6 +827,7 @@ Record this checksum: it will be used to verify the password file in the future
 		'b32':   pwinfo(10, 42 ,24, None,       'base32 password',       is_b32_str),   # 32**24 < 2**128
 		'b32':   pwinfo(10, 42 ,24, None,       'base32 password',       is_b32_str),   # 32**24 < 2**128
 		'b58':   pwinfo(8,  36 ,20, None,       'base58 password',       is_b58_str),   # 58**20 < 2**128
 		'b58':   pwinfo(8,  36 ,20, None,       'base58 password',       is_b58_str),   # 58**20 < 2**128
 		'bip39': pwinfo(12, 24 ,24, [12,18,24], 'BIP39 mnemonic',        is_bip39_str),
 		'bip39': pwinfo(12, 24 ,24, [12,18,24], 'BIP39 mnemonic',        is_bip39_str),
+		'xmrseed': pwinfo(25, 25, 25, [25],     'Monero new-style mnemonic', is_xmrseed),
 		'hex':   pwinfo(32, 64 ,64, [32,48,64], 'hexadecimal password',  is_hex_str),
 		'hex':   pwinfo(32, 64 ,64, [32,48,64], 'hexadecimal password',  is_hex_str),
 	}
 	}
 	chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
 	chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
@@ -855,7 +859,7 @@ Record this checksum: it will be used to verify the password file in the future
 			self.al_id = AddrListID(seed.sid,MMGenPasswordType('P'))
 			self.al_id = AddrListID(seed.sid,MMGenPasswordType('P'))
 			self.data = self.generate(seed,pw_idxs)
 			self.data = self.generate(seed,pw_idxs)
 
 
-		if self.pw_fmt == 'bip39':
+		if self.pw_fmt in ('bip39','xmrseed'):
 			self.msgs['file_header'] = self.msgs['file_header_mn'].format(self.pw_fmt.upper())
 			self.msgs['file_header'] = self.msgs['file_header_mn'].format(self.pw_fmt.upper())
 
 
 		self.num_addrs = len(self.data)
 		self.num_addrs = len(self.data)
@@ -916,7 +920,13 @@ Record this checksum: it will be used to verify the password file in the future
 		elif pf == 'bip39':
 		elif pf == 'bip39':
 			from mmgen.bip39 import bip39
 			from mmgen.bip39 import bip39
 			pw_bytes = bip39.nwords2seedlen(self.pw_len,in_bytes=True)
 			pw_bytes = bip39.nwords2seedlen(self.pw_len,in_bytes=True)
-			good_pw_len = bip39.seedlen2nwords(len(seed.data),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]
+			try:
+				good_pw_len = baseconv.seedlen_map['xmrseed'][seed.byte_len]
+			except:
+				die(1,'{}: unsupported seed length for Monero new-style mnemonic'.format(seed.byte_len*8))
 		elif pf in ('b32','b58'):
 		elif pf in ('b32','b58'):
 			pw_int = (32 if pf == 'b32' else 58) ** self.pw_len
 			pw_int = (32 if pf == 'b32' else 58) ** self.pw_len
 			pw_bytes = pw_int.bit_length() // 8
 			pw_bytes = pw_int.bit_length() // 8
@@ -946,6 +956,13 @@ Record this checksum: it will be used to verify the password file in the future
 			pw_len_hex = bip39.nwords2seedlen(self.pw_len,in_hex=True)
 			pw_len_hex = bip39.nwords2seedlen(self.pw_len,in_hex=True)
 			# take most significant part
 			# take most significant part
 			return ' '.join(bip39.fromhex(hex_sec[:pw_len_hex],wl_id='bip39'))
 			return ' '.join(bip39.fromhex(hex_sec[:pw_len_hex],wl_id='bip39'))
+		elif self.pw_fmt == 'xmrseed':
+			pw_len_hex = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] * 2
+			# take most significant part
+			bytes_trunc = bytes.fromhex(hex_sec[:pw_len_hex])
+			from mmgen.protocol import MoneroProtocol
+			bytes_preproc = MoneroProtocol.preprocess_key(bytes_trunc,None)
+			return ' '.join(baseconv.frombytes(bytes_preproc,wl_id='xmrseed'))
 		else:
 		else:
 			# take least significant part
 			# take least significant part
 			return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:]
 			return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:]
@@ -953,7 +970,7 @@ Record this checksum: it will be used to verify the password file in the future
 	def check_format(self,pw):
 	def check_format(self,pw):
 		if not self.pw_info[self.pw_fmt].chk_func(pw):
 		if not self.pw_info[self.pw_fmt].chk_func(pw):
 			raise ValueError('Password is not valid {} data'.format(self.pw_info[self.pw_fmt].desc))
 			raise ValueError('Password is not valid {} data'.format(self.pw_info[self.pw_fmt].desc))
-		pwlen = len(pw.split()) if self.pw_fmt == 'bip39' else len(pw)
+		pwlen = len(pw.split()) if self.pw_fmt in ('bip39','xmrseed') else len(pw)
 		if pwlen != self.pw_len:
 		if pwlen != self.pw_len:
 			raise ValueError('Password has incorrect length ({} != {})'.format(pwlen,self.pw_len))
 			raise ValueError('Password has incorrect length ({} != {})'.format(pwlen,self.pw_len))
 		return True
 		return True
@@ -975,7 +992,7 @@ Record this checksum: it will be used to verify the password file in the future
 
 
 	def get_line(self,lines):
 	def get_line(self,lines):
 		self.line_ctr += 1
 		self.line_ctr += 1
-		if self.pw_fmt == 'bip39':
+		if self.pw_fmt in ('bip39','xmrseed'):
 			ret = lines.pop(0).split(None,self.pw_len+1)
 			ret = lines.pop(0).split(None,self.pw_len+1)
 			if len(ret) > self.pw_len+1:
 			if len(ret) > self.pw_len+1:
 				m1 = 'extraneous text {!r} found after password'.format(ret[self.pw_len+1])
 				m1 = 'extraneous text {!r} found after password'.format(ret[self.pw_len+1])

+ 52 - 7
mmgen/baseconv.py

@@ -22,6 +22,7 @@ baseconv.py:  base conversion class for the MMGen suite
 
 
 from hashlib import sha256
 from hashlib import sha256
 from mmgen.exception import *
 from mmgen.exception import *
+from mmgen.util import die
 
 
 def is_b58_str(s): return set(list(s)) <= set(baseconv.digits['b58'])
 def is_b58_str(s): return set(list(s)) <= set(baseconv.digits['b58'])
 def is_b32_str(s): return set(list(s)) <= set(baseconv.digits['b32'])
 def is_b32_str(s): return set(list(s)) <= set(baseconv.digits['b32'])
@@ -38,6 +39,7 @@ class baseconv(object):
 		'tirosh':('Tirosh mnemonic',   'base1626 mnemonic using truncated Tirosh wordlist'), # not used by wallet
 		'tirosh':('Tirosh mnemonic',   'base1626 mnemonic using truncated Tirosh wordlist'), # not used by wallet
 		'mmgen': ('MMGen native mnemonic',
 		'mmgen': ('MMGen native mnemonic',
 		'MMGen native mnemonic seed phrase created using old Electrum wordlist and simple base conversion'),
 		'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://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet
 	# https://tools.ietf.org/html/rfc4648
 	# https://tools.ietf.org/html/rfc4648
@@ -52,6 +54,7 @@ class baseconv(object):
 	mn_base = 1626 # tirosh list is 1633 words long!
 	mn_base = 1626 # tirosh list is 1633 words long!
 	wl_chksums = {
 	wl_chksums = {
 		'mmgen':  '5ca31424',
 		'mmgen':  '5ca31424',
+		'xmrseed':'3c381ebb',
 		'tirosh': '48f05e1f', # tirosh truncated to mn_base (1626)
 		'tirosh': '48f05e1f', # tirosh truncated to mn_base (1626)
 		# 'tirosh1633': '1a5faeff'
 		# 'tirosh1633': '1a5faeff'
 	}
 	}
@@ -59,11 +62,13 @@ class baseconv(object):
 		'b58': { 16:22, 24:33, 32:44 },
 		'b58': { 16:22, 24:33, 32:44 },
 		'b6d': { 16:50, 24:75, 32:100 },
 		'b6d': { 16:50, 24:75, 32:100 },
 		'mmgen': { 16:12, 24:18, 32:24 },
 		'mmgen': { 16:12, 24:18, 32:24 },
+		'xmrseed': { 32:25 },
 	}
 	}
 	seedlen_map_rev = {
 	seedlen_map_rev = {
 		'b58': { 22:16, 33:24, 44:32 },
 		'b58': { 22:16, 33:24, 44:32 },
 		'b6d': { 50:16, 75:24, 100:32 },
 		'b6d': { 50:16, 75:24, 100:32 },
 		'mmgen': { 12:16, 18:24, 24:32 },
 		'mmgen': { 12:16, 18:24, 24:32 },
+		'xmrseed': { 25:32 },
 	}
 	}
 
 
 	@classmethod
 	@classmethod
@@ -73,6 +78,9 @@ class baseconv(object):
 		if mn_id == 'mmgen':
 		if mn_id == 'mmgen':
 			from mmgen.mn_electrum import words
 			from mmgen.mn_electrum import words
 			cls.digits[mn_id] = words
 			cls.digits[mn_id] = words
+		elif mn_id == 'xmrseed':
+			from mmgen.mn_monero import words
+			cls.digits[mn_id] = words
 		elif mn_id == 'tirosh':
 		elif mn_id == 'tirosh':
 			from mmgen.mn_tirosh import words
 			from mmgen.mn_tirosh import words
 			cls.digits[mn_id] = words[:cls.mn_base]
 			cls.digits[mn_id] = words[:cls.mn_base]
@@ -127,6 +135,12 @@ class baseconv(object):
 			m = "{!r}: illegal value for 'pad' (must be None,'seed' or int)"
 			m = "{!r}: illegal value for 'pad' (must be None,'seed' or int)"
 			raise BaseConversionPadError(m.format(pad))
 			raise BaseConversionPadError(m.format(pad))
 
 
+	@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)]
+
 	@classmethod
 	@classmethod
 	def tohex(cls,words_arg,wl_id,pad=None):
 	def tohex(cls,words_arg,wl_id,pad=None):
 		"convert string or list data of base 'wl_id' to hex string"
 		"convert string or list data of base 'wl_id' to hex string"
@@ -161,6 +175,21 @@ class baseconv(object):
 			m = ('{w!r}:','seed data')[pad=='seed'] + ' not in {d} format'
 			m = ('{w!r}:','seed data')[pad=='seed'] + ' not in {d} format'
 			raise BaseConversionError(m.format(w=words_arg,d=desc))
 			raise BaseConversionError(m.format(w=words_arg,d=desc))
 
 
+		if wl_id == 'xmrseed':
+			if len(words) not in cls.seedlen_map_rev['xmrseed']:
+				die(2,'{}: invalid length for Monero mnemonic'.format(len(words)))
+
+			z = cls.monero_mn_checksum(words[:-1])
+			assert z == words[-1],'{!r}: invalid Monero checksum (should be {!r})'.format(words[-1],z)
+			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))])
 		ret = sum([wl.index(words[::-1][i])*(base**i) for i in range(len(words))])
 		bl = ret.bit_length()
 		bl = ret.bit_length()
 		return ret.to_bytes(max(pad_val,bl//8+bool(bl%8)),'big')
 		return ret.to_bytes(max(pad_val,bl//8+bool(bl%8)),'big')
@@ -198,10 +227,26 @@ class baseconv(object):
 		wl = cls.digits[wl_id]
 		wl = cls.digits[wl_id]
 		base = len(wl)
 		base = len(wl)
 
 
-		num = int.from_bytes(bytestr,'big')
-		ret = []
-		while num:
-			ret.append(num % base)
-			num //= base
-		o = [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]]
-		return ''.join(o) if tostr else o
+		if wl_id == 'xmrseed':
+			if len(bytestr) not in cls.seedlen_map['xmrseed']:
+				die(2,'{}: invalid seed byte length for Monero mnemonic'.format(len(bytestr)))
+
+			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(cls.monero_mn_checksum(o))
+		else:
+			num = int.from_bytes(bytestr,'big')
+			ret = []
+			while num:
+				ret.append(num % base)
+				num //= base
+			o = [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]]
+
+		return (' ' if wl_id in ('mmgen','xmrseed') else '').join(o) if tostr else o

+ 1632 - 0
mmgen/mn_monero.py

@@ -0,0 +1,1632 @@
+#!/usr/bin/env python3
+#
+# Source: https://github.com/monero-project/monero/blob/master/src/mnemonics/english.h
+
+words = tuple("""
+abbey
+abducts
+ability
+ablaze
+abnormal
+abort
+abrasive
+absorb
+abyss
+academy
+aces
+aching
+acidic
+acoustic
+acquire
+across
+actress
+acumen
+adapt
+addicted
+adept
+adhesive
+adjust
+adopt
+adrenalin
+adult
+adventure
+aerial
+afar
+affair
+afield
+afloat
+afoot
+afraid
+after
+against
+agenda
+aggravate
+agile
+aglow
+agnostic
+agony
+agreed
+ahead
+aided
+ailments
+aimless
+airport
+aisle
+ajar
+akin
+alarms
+album
+alchemy
+alerts
+algebra
+alkaline
+alley
+almost
+aloof
+alpine
+already
+also
+altitude
+alumni
+always
+amaze
+ambush
+amended
+amidst
+ammo
+amnesty
+among
+amply
+amused
+anchor
+android
+anecdote
+angled
+ankle
+annoyed
+answers
+antics
+anvil
+anxiety
+anybody
+apart
+apex
+aphid
+aplomb
+apology
+apply
+apricot
+aptitude
+aquarium
+arbitrary
+archer
+ardent
+arena
+argue
+arises
+army
+around
+arrow
+arsenic
+artistic
+ascend
+ashtray
+aside
+asked
+asleep
+aspire
+assorted
+asylum
+athlete
+atlas
+atom
+atrium
+attire
+auburn
+auctions
+audio
+august
+aunt
+austere
+autumn
+avatar
+avidly
+avoid
+awakened
+awesome
+awful
+awkward
+awning
+awoken
+axes
+axis
+axle
+aztec
+azure
+baby
+bacon
+badge
+baffles
+bagpipe
+bailed
+bakery
+balding
+bamboo
+banjo
+baptism
+basin
+batch
+bawled
+bays
+because
+beer
+befit
+begun
+behind
+being
+below
+bemused
+benches
+berries
+bested
+betting
+bevel
+beware
+beyond
+bias
+bicycle
+bids
+bifocals
+biggest
+bikini
+bimonthly
+binocular
+biology
+biplane
+birth
+biscuit
+bite
+biweekly
+blender
+blip
+bluntly
+boat
+bobsled
+bodies
+bogeys
+boil
+boldly
+bomb
+border
+boss
+both
+bounced
+bovine
+bowling
+boxes
+boyfriend
+broken
+brunt
+bubble
+buckets
+budget
+buffet
+bugs
+building
+bulb
+bumper
+bunch
+business
+butter
+buying
+buzzer
+bygones
+byline
+bypass
+cabin
+cactus
+cadets
+cafe
+cage
+cajun
+cake
+calamity
+camp
+candy
+casket
+catch
+cause
+cavernous
+cease
+cedar
+ceiling
+cell
+cement
+cent
+certain
+chlorine
+chrome
+cider
+cigar
+cinema
+circle
+cistern
+citadel
+civilian
+claim
+click
+clue
+coal
+cobra
+cocoa
+code
+coexist
+coffee
+cogs
+cohesive
+coils
+colony
+comb
+cool
+copy
+corrode
+costume
+cottage
+cousin
+cowl
+criminal
+cube
+cucumber
+cuddled
+cuffs
+cuisine
+cunning
+cupcake
+custom
+cycling
+cylinder
+cynical
+dabbing
+dads
+daft
+dagger
+daily
+damp
+dangerous
+dapper
+darted
+dash
+dating
+dauntless
+dawn
+daytime
+dazed
+debut
+decay
+dedicated
+deepest
+deftly
+degrees
+dehydrate
+deity
+dejected
+delayed
+demonstrate
+dented
+deodorant
+depth
+desk
+devoid
+dewdrop
+dexterity
+dialect
+dice
+diet
+different
+digit
+dilute
+dime
+dinner
+diode
+diplomat
+directed
+distance
+ditch
+divers
+dizzy
+doctor
+dodge
+does
+dogs
+doing
+dolphin
+domestic
+donuts
+doorway
+dormant
+dosage
+dotted
+double
+dove
+down
+dozen
+dreams
+drinks
+drowning
+drunk
+drying
+dual
+dubbed
+duckling
+dude
+duets
+duke
+dullness
+dummy
+dunes
+duplex
+duration
+dusted
+duties
+dwarf
+dwelt
+dwindling
+dying
+dynamite
+dyslexic
+each
+eagle
+earth
+easy
+eating
+eavesdrop
+eccentric
+echo
+eclipse
+economics
+ecstatic
+eden
+edgy
+edited
+educated
+eels
+efficient
+eggs
+egotistic
+eight
+either
+eject
+elapse
+elbow
+eldest
+eleven
+elite
+elope
+else
+eluded
+emails
+ember
+emerge
+emit
+emotion
+empty
+emulate
+energy
+enforce
+enhanced
+enigma
+enjoy
+enlist
+enmity
+enough
+enraged
+ensign
+entrance
+envy
+epoxy
+equip
+erase
+erected
+erosion
+error
+eskimos
+espionage
+essential
+estate
+etched
+eternal
+ethics
+etiquette
+evaluate
+evenings
+evicted
+evolved
+examine
+excess
+exhale
+exit
+exotic
+exquisite
+extra
+exult
+fabrics
+factual
+fading
+fainted
+faked
+fall
+family
+fancy
+farming
+fatal
+faulty
+fawns
+faxed
+fazed
+feast
+february
+federal
+feel
+feline
+females
+fences
+ferry
+festival
+fetches
+fever
+fewest
+fiat
+fibula
+fictional
+fidget
+fierce
+fifteen
+fight
+films
+firm
+fishing
+fitting
+five
+fixate
+fizzle
+fleet
+flippant
+flying
+foamy
+focus
+foes
+foggy
+foiled
+folding
+fonts
+foolish
+fossil
+fountain
+fowls
+foxes
+foyer
+framed
+friendly
+frown
+fruit
+frying
+fudge
+fuel
+fugitive
+fully
+fuming
+fungal
+furnished
+fuselage
+future
+fuzzy
+gables
+gadget
+gags
+gained
+galaxy
+gambit
+gang
+gasp
+gather
+gauze
+gave
+gawk
+gaze
+gearbox
+gecko
+geek
+gels
+gemstone
+general
+geometry
+germs
+gesture
+getting
+geyser
+ghetto
+ghost
+giant
+giddy
+gifts
+gigantic
+gills
+gimmick
+ginger
+girth
+giving
+glass
+gleeful
+glide
+gnaw
+gnome
+goat
+goblet
+godfather
+goes
+goggles
+going
+goldfish
+gone
+goodbye
+gopher
+gorilla
+gossip
+gotten
+gourmet
+governing
+gown
+greater
+grunt
+guarded
+guest
+guide
+gulp
+gumball
+guru
+gusts
+gutter
+guys
+gymnast
+gypsy
+gyrate
+habitat
+hacksaw
+haggled
+hairy
+hamburger
+happens
+hashing
+hatchet
+haunted
+having
+hawk
+haystack
+hazard
+hectare
+hedgehog
+heels
+hefty
+height
+hemlock
+hence
+heron
+hesitate
+hexagon
+hickory
+hiding
+highway
+hijack
+hiker
+hills
+himself
+hinder
+hippo
+hire
+history
+hitched
+hive
+hoax
+hobby
+hockey
+hoisting
+hold
+honked
+hookup
+hope
+hornet
+hospital
+hotel
+hounded
+hover
+howls
+hubcaps
+huddle
+huge
+hull
+humid
+hunter
+hurried
+husband
+huts
+hybrid
+hydrogen
+hyper
+iceberg
+icing
+icon
+identity
+idiom
+idled
+idols
+igloo
+ignore
+iguana
+illness
+imagine
+imbalance
+imitate
+impel
+inactive
+inbound
+incur
+industrial
+inexact
+inflamed
+ingested
+initiate
+injury
+inkling
+inline
+inmate
+innocent
+inorganic
+input
+inquest
+inroads
+insult
+intended
+inundate
+invoke
+inwardly
+ionic
+irate
+iris
+irony
+irritate
+island
+isolated
+issued
+italics
+itches
+items
+itinerary
+itself
+ivory
+jabbed
+jackets
+jaded
+jagged
+jailed
+jamming
+january
+jargon
+jaunt
+javelin
+jaws
+jazz
+jeans
+jeers
+jellyfish
+jeopardy
+jerseys
+jester
+jetting
+jewels
+jigsaw
+jingle
+jittery
+jive
+jobs
+jockey
+jogger
+joining
+joking
+jolted
+jostle
+journal
+joyous
+jubilee
+judge
+juggled
+juicy
+jukebox
+july
+jump
+junk
+jury
+justice
+juvenile
+kangaroo
+karate
+keep
+kennel
+kept
+kernels
+kettle
+keyboard
+kickoff
+kidneys
+king
+kiosk
+kisses
+kitchens
+kiwi
+knapsack
+knee
+knife
+knowledge
+knuckle
+koala
+laboratory
+ladder
+lagoon
+lair
+lakes
+lamb
+language
+laptop
+large
+last
+later
+launching
+lava
+lawsuit
+layout
+lazy
+lectures
+ledge
+leech
+left
+legion
+leisure
+lemon
+lending
+leopard
+lesson
+lettuce
+lexicon
+liar
+library
+licks
+lids
+lied
+lifestyle
+light
+likewise
+lilac
+limits
+linen
+lion
+lipstick
+liquid
+listen
+lively
+loaded
+lobster
+locker
+lodge
+lofty
+logic
+loincloth
+long
+looking
+lopped
+lordship
+losing
+lottery
+loudly
+love
+lower
+loyal
+lucky
+luggage
+lukewarm
+lullaby
+lumber
+lunar
+lurk
+lush
+luxury
+lymph
+lynx
+lyrics
+macro
+madness
+magically
+mailed
+major
+makeup
+malady
+mammal
+maps
+masterful
+match
+maul
+maverick
+maximum
+mayor
+maze
+meant
+mechanic
+medicate
+meeting
+megabyte
+melting
+memoir
+menu
+merger
+mesh
+metro
+mews
+mice
+midst
+mighty
+mime
+mirror
+misery
+mittens
+mixture
+moat
+mobile
+mocked
+mohawk
+moisture
+molten
+moment
+money
+moon
+mops
+morsel
+mostly
+motherly
+mouth
+movement
+mowing
+much
+muddy
+muffin
+mugged
+mullet
+mumble
+mundane
+muppet
+mural
+musical
+muzzle
+myriad
+mystery
+myth
+nabbing
+nagged
+nail
+names
+nanny
+napkin
+narrate
+nasty
+natural
+nautical
+navy
+nearby
+necklace
+needed
+negative
+neither
+neon
+nephew
+nerves
+nestle
+network
+neutral
+never
+newt
+nexus
+nibs
+niche
+niece
+nifty
+nightly
+nimbly
+nineteen
+nirvana
+nitrogen
+nobody
+nocturnal
+nodes
+noises
+nomad
+noodles
+northern
+nostril
+noted
+nouns
+novelty
+nowhere
+nozzle
+nuance
+nucleus
+nudged
+nugget
+nuisance
+null
+number
+nuns
+nurse
+nutshell
+nylon
+oaks
+oars
+oasis
+oatmeal
+obedient
+object
+obliged
+obnoxious
+observant
+obtains
+obvious
+occur
+ocean
+october
+odds
+odometer
+offend
+often
+oilfield
+ointment
+okay
+older
+olive
+olympics
+omega
+omission
+omnibus
+onboard
+oncoming
+oneself
+ongoing
+onion
+online
+onslaught
+onto
+onward
+oozed
+opacity
+opened
+opposite
+optical
+opus
+orange
+orbit
+orchid
+orders
+organs
+origin
+ornament
+orphans
+oscar
+ostrich
+otherwise
+otter
+ouch
+ought
+ounce
+ourselves
+oust
+outbreak
+oval
+oven
+owed
+owls
+owner
+oxidant
+oxygen
+oyster
+ozone
+pact
+paddles
+pager
+pairing
+palace
+pamphlet
+pancakes
+paper
+paradise
+pastry
+patio
+pause
+pavements
+pawnshop
+payment
+peaches
+pebbles
+peculiar
+pedantic
+peeled
+pegs
+pelican
+pencil
+people
+pepper
+perfect
+pests
+petals
+phase
+pheasants
+phone
+phrases
+physics
+piano
+picked
+pierce
+pigment
+piloted
+pimple
+pinched
+pioneer
+pipeline
+pirate
+pistons
+pitched
+pivot
+pixels
+pizza
+playful
+pledge
+pliers
+plotting
+plus
+plywood
+poaching
+pockets
+podcast
+poetry
+point
+poker
+polar
+ponies
+pool
+popular
+portents
+possible
+potato
+pouch
+poverty
+powder
+pram
+present
+pride
+problems
+pruned
+prying
+psychic
+public
+puck
+puddle
+puffin
+pulp
+pumpkins
+punch
+puppy
+purged
+push
+putty
+puzzled
+pylons
+pyramid
+python
+queen
+quick
+quote
+rabbits
+racetrack
+radar
+rafts
+rage
+railway
+raking
+rally
+ramped
+randomly
+rapid
+rarest
+rash
+rated
+ravine
+rays
+razor
+react
+rebel
+recipe
+reduce
+reef
+refer
+regular
+reheat
+reinvest
+rejoices
+rekindle
+relic
+remedy
+renting
+reorder
+repent
+request
+reruns
+rest
+return
+reunion
+revamp
+rewind
+rhino
+rhythm
+ribbon
+richly
+ridges
+rift
+rigid
+rims
+ringing
+riots
+ripped
+rising
+ritual
+river
+roared
+robot
+rockets
+rodent
+rogue
+roles
+romance
+roomy
+roped
+roster
+rotate
+rounded
+rover
+rowboat
+royal
+ruby
+rudely
+ruffled
+rugged
+ruined
+ruling
+rumble
+runway
+rural
+rustled
+ruthless
+sabotage
+sack
+sadness
+safety
+saga
+sailor
+sake
+salads
+sample
+sanity
+sapling
+sarcasm
+sash
+satin
+saucepan
+saved
+sawmill
+saxophone
+sayings
+scamper
+scenic
+school
+science
+scoop
+scrub
+scuba
+seasons
+second
+sedan
+seeded
+segments
+seismic
+selfish
+semifinal
+sensible
+september
+sequence
+serving
+session
+setup
+seventh
+sewage
+shackles
+shelter
+shipped
+shocking
+shrugged
+shuffled
+shyness
+siblings
+sickness
+sidekick
+sieve
+sifting
+sighting
+silk
+simplest
+sincerely
+sipped
+siren
+situated
+sixteen
+sizes
+skater
+skew
+skirting
+skulls
+skydive
+slackens
+sleepless
+slid
+slower
+slug
+smash
+smelting
+smidgen
+smog
+smuggled
+snake
+sneeze
+sniff
+snout
+snug
+soapy
+sober
+soccer
+soda
+software
+soggy
+soil
+solved
+somewhere
+sonic
+soothe
+soprano
+sorry
+southern
+sovereign
+sowed
+soya
+space
+speedy
+sphere
+spiders
+splendid
+spout
+sprig
+spud
+spying
+square
+stacking
+stellar
+stick
+stockpile
+strained
+stunning
+stylishly
+subtly
+succeed
+suddenly
+suede
+suffice
+sugar
+suitcase
+sulking
+summon
+sunken
+superior
+surfer
+sushi
+suture
+swagger
+swept
+swiftly
+sword
+swung
+syllabus
+symptoms
+syndrome
+syringe
+system
+taboo
+tacit
+tadpoles
+tagged
+tail
+taken
+talent
+tamper
+tanks
+tapestry
+tarnished
+tasked
+tattoo
+taunts
+tavern
+tawny
+taxi
+teardrop
+technical
+tedious
+teeming
+tell
+template
+tender
+tepid
+tequila
+terminal
+testing
+tether
+textbook
+thaw
+theatrics
+thirsty
+thorn
+threaten
+thumbs
+thwart
+ticket
+tidy
+tiers
+tiger
+tilt
+timber
+tinted
+tipsy
+tirade
+tissue
+titans
+toaster
+tobacco
+today
+toenail
+toffee
+together
+toilet
+token
+tolerant
+tomorrow
+tonic
+toolbox
+topic
+torch
+tossed
+total
+touchy
+towel
+toxic
+toyed
+trash
+trendy
+tribal
+trolling
+truth
+trying
+tsunami
+tubes
+tucks
+tudor
+tuesday
+tufts
+tugs
+tuition
+tulips
+tumbling
+tunnel
+turnip
+tusks
+tutor
+tuxedo
+twang
+tweezers
+twice
+twofold
+tycoon
+typist
+tyrant
+ugly
+ulcers
+ultimate
+umbrella
+umpire
+unafraid
+unbending
+uncle
+under
+uneven
+unfit
+ungainly
+unhappy
+union
+unjustly
+unknown
+unlikely
+unmask
+unnoticed
+unopened
+unplugs
+unquoted
+unrest
+unsafe
+until
+unusual
+unveil
+unwind
+unzip
+upbeat
+upcoming
+update
+upgrade
+uphill
+upkeep
+upload
+upon
+upper
+upright
+upstairs
+uptight
+upwards
+urban
+urchins
+urgent
+usage
+useful
+usher
+using
+usual
+utensils
+utility
+utmost
+utopia
+uttered
+vacation
+vague
+vain
+value
+vampire
+vane
+vapidly
+vary
+vastness
+vats
+vaults
+vector
+veered
+vegan
+vehicle
+vein
+velvet
+venomous
+verification
+vessel
+veteran
+vexed
+vials
+vibrate
+victim
+video
+viewpoint
+vigilant
+viking
+village
+vinegar
+violin
+vipers
+virtual
+visited
+vitals
+vivid
+vixen
+vocal
+vogue
+voice
+volcano
+vortex
+voted
+voucher
+vowels
+voyage
+vulture
+wade
+waffle
+wagtail
+waist
+waking
+wallets
+wanted
+warped
+washing
+water
+waveform
+waxing
+wayside
+weavers
+website
+wedge
+weekday
+weird
+welders
+went
+wept
+were
+western
+wetsuit
+whale
+when
+whipped
+whole
+wickets
+width
+wield
+wife
+wiggle
+wildly
+winter
+wipeout
+wiring
+wise
+withdrawn
+wives
+wizard
+wobbly
+woes
+woken
+wolf
+womanly
+wonders
+woozy
+worry
+wounded
+woven
+wrap
+wrist
+wrong
+yacht
+yahoo
+yanks
+yard
+yawning
+yearbook
+yellow
+yesterday
+yeti
+yields
+yodel
+yoga
+younger
+yoyo
+zapped
+zeal
+zebra
+zero
+zesty
+zigzags
+zinger
+zippers
+zodiac
+zombie
+zones
+zoom
+""".split())

+ 18 - 1
mmgen/tool.py

@@ -234,6 +234,7 @@ dfl_mnemonic_fmt = 'mmgen'
 mnemonic_fmts = {
 mnemonic_fmts = {
 	'mmgen': { 'fmt': 'words', 'conv_cls': lambda: baseconv },
 	'mmgen': { 'fmt': 'words', 'conv_cls': lambda: baseconv },
 	'bip39': { 'fmt': 'bip39', 'conv_cls': conv_cls_bip39 },
 	'bip39': { 'fmt': 'bip39', 'conv_cls': conv_cls_bip39 },
+	'xmrseed': { 'fmt': 'xmrseed','conv_cls': lambda: baseconv },
 }
 }
 mn_opts_disp = "(valid options: '{}')".format("', '".join(mnemonic_fmts))
 mn_opts_disp = "(valid options: '{}')".format("', '".join(mnemonic_fmts))
 
 
@@ -473,7 +474,7 @@ class MMGenToolCmdCoin(MMGenToolCmdBase):
 
 
 class MMGenToolCmdMnemonic(MMGenToolCmdBase):
 class MMGenToolCmdMnemonic(MMGenToolCmdBase):
 	"""
 	"""
-	seed phrase utilities (valid formats: 'mmgen' (default), 'bip39')
+	seed phrase utilities (valid formats: 'mmgen' (default), 'bip39', 'xmrseed')
 
 
 		IMPORTANT NOTE: MMGen's default seed phrase format uses the Electrum
 		IMPORTANT NOTE: MMGen's default seed phrase format uses the Electrum
 		wordlist, however seed phrases are computed using a different algorithm
 		wordlist, however seed phrases are computed using a different algorithm
@@ -484,10 +485,24 @@ class MMGenToolCmdMnemonic(MMGenToolCmdBase):
 		users should be aware that BIP39 support does not imply BIP32 support!
 		users should be aware that BIP39 support does not imply BIP32 support!
 		MMGen uses its own key derivation scheme differing from the one described
 		MMGen uses its own key derivation scheme differing from the one described
 		by the BIP32 protocol.
 		by the BIP32 protocol.
+
+		For Monero ('xmrseed') seed phrases, input data is reduced to a spendkey
+		before conversion so that a canonical seed phrase is produced.  This is
+		required because Monero seeds, unlike ordinary wallet seeds, are tied
+		to a concrete key/address pair.  To manually generate a Monero spendkey,
+		use the 'hex2wif' command.
 	"""
 	"""
+
+	@staticmethod
+	def _xmr_reduce(bytestr):
+		from mmgen.protocol import MoneroProtocol
+		return MoneroProtocol.preprocess_key(bytestr,None)
+
 	def _do_random_mn(self,nbytes:int,fmt:str):
 	def _do_random_mn(self,nbytes:int,fmt:str):
 		assert nbytes in (16,24,32), 'nbytes must be 16, 24 or 32'
 		assert nbytes in (16,24,32), 'nbytes must be 16, 24 or 32'
 		randbytes = get_random(nbytes)
 		randbytes = get_random(nbytes)
+		if fmt == 'xmrseed':
+			randbytes = self._xmr_reduce(randbytes)
 		if opt.verbose:
 		if opt.verbose:
 			msg('Seed: {}'.format(randbytes.hex()))
 			msg('Seed: {}'.format(randbytes.hex()))
 		return self.hex2mn(randbytes.hex(),fmt=fmt)
 		return self.hex2mn(randbytes.hex(),fmt=fmt)
@@ -518,6 +533,8 @@ class MMGenToolCmdMnemonic(MMGenToolCmdBase):
 			return ' '.join(bip39.fromhex(hexstr,fmt))
 			return ' '.join(bip39.fromhex(hexstr,fmt))
 		else:
 		else:
 			bytestr = bytes.fromhex(hexstr)
 			bytestr = bytes.fromhex(hexstr)
+			if fmt == 'xmrseed':
+				bytestr = self._xmr_reduce(bytestr)
 			return baseconv.frombytes(bytestr,fmt,'seed',tostr=True)
 			return baseconv.frombytes(bytestr,fmt,'seed',tostr=True)
 
 
 	def mn2hex( self, seed_mnemonic:'sstr', fmt:mn_opts_disp = dfl_mnemonic_fmt ):
 	def mn2hex( self, seed_mnemonic:'sstr', fmt:mn_opts_disp = dfl_mnemonic_fmt ):

+ 1 - 0
setup.py

@@ -113,6 +113,7 @@ setup(
 			'mmgen.keccak',
 			'mmgen.keccak',
 			'mmgen.license',
 			'mmgen.license',
 			'mmgen.mn_electrum',
 			'mmgen.mn_electrum',
+			'mmgen.mn_monero',
 			'mmgen.mn_tirosh',
 			'mmgen.mn_tirosh',
 			'mmgen.obj',
 			'mmgen.obj',
 			'mmgen.opts',
 			'mmgen.opts',

+ 6 - 0
test/ref/98831F3A-фубар@crypto.org-xmrseed-25[1,4,1100].pws

@@ -0,0 +1,6 @@
+# B488 21D3 4539 968D
+98831F3A фубар@crypto.org xmrseed:25 {
+  1     amended jailed extra apart hinder upbeat dating onboard yesterday aided irony guide cogs apricot aggravate twofold tepid nuance ripped vessel soothe woven vials when aided
+  4     godfather arrow hobby mailed educated abnormal boss cavernous skirting alumni voted hunter vessel unsafe mohawk pylons amused rift jury code assorted oscar himself inquest oscar
+  1100  molten icon lectures amaze foes lurk camp divers goldfish zombie neutral drunk topic abort boldly recipe natural verification circle polar woozy biplane yoyo adapt abort
+}

+ 1 - 0
test/scrambletest.py

@@ -88,6 +88,7 @@ passwd_data = {
 'bip39_dfl_αω':td('95b383d5092a55df', 'bip39:24:αω','-αω-bip39-24','αω bip39:24','treat athlete brand top beauty poverty senior unhappy vacant domain yellow scale fossil aim lonely fatal sun nuclear such ancient stage require stool similar'),
 'bip39_dfl_αω':td('95b383d5092a55df', 'bip39:24:αω','-αω-bip39-24','αω bip39:24','treat athlete brand top beauty poverty senior unhappy vacant domain yellow scale fossil aim lonely fatal sun nuclear such ancient stage require stool similar'),
 'bip39_18_αω': td('29e5a605ffa36142', 'bip39:18:αω','-αω-bip39-18','αω bip39:18','better legal various ketchup then range festival either tomato cradle say absorb solar earth alter pattern canyon liar'),
 'bip39_18_αω': td('29e5a605ffa36142', 'bip39:18:αω','-αω-bip39-18','αω bip39:18','better legal various ketchup then range festival either tomato cradle say absorb solar earth alter pattern canyon liar'),
 'bip39_12_αω': td('efa13cb309d7fc1d', 'bip39:12:αω','-αω-bip39-12','αω bip39:12','lady oppose theme fit position merry reopen acquire tuna dentist young chunk'),
 'bip39_12_αω': td('efa13cb309d7fc1d', 'bip39:12:αω','-αω-bip39-12','αω bip39:12','lady oppose theme fit position merry reopen acquire tuna dentist young chunk'),
+'xmrseed_dfl_αω':td('62f5b72a5ca89cab', 'xmrseed:25:αω','-αω-xmrseed-25','αω xmrseed:25','tequila eden skulls giving jester hospital dreams bakery adjust nanny cactus inwardly films amply nanny soggy vials muppet yellow woken ashtray organs exhale foes eden'),
 }
 }
 
 
 cvr_opts = ' -m trace --count --coverdir={} --file={}'.format(*init_coverage()) if opt.coverage else ''
 cvr_opts = ' -m trace --count --coverdir={} --file={}'.format(*init_coverage()) if opt.coverage else ''

+ 4 - 0
test/test_py_d/ts_ref.py

@@ -53,6 +53,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 		'ref_passwdfile_bip39_12': '98831F3A-фубар@crypto.org-bip39-12[1,4,1100].pws',
 		'ref_passwdfile_bip39_12': '98831F3A-фубар@crypto.org-bip39-12[1,4,1100].pws',
 		'ref_passwdfile_bip39_18': '98831F3A-фубар@crypto.org-bip39-18[1,4,1100].pws',
 		'ref_passwdfile_bip39_18': '98831F3A-фубар@crypto.org-bip39-18[1,4,1100].pws',
 		'ref_passwdfile_bip39_24': '98831F3A-фубар@crypto.org-bip39-24[1,4,1100].pws',
 		'ref_passwdfile_bip39_24': '98831F3A-фубар@crypto.org-bip39-24[1,4,1100].pws',
+		'ref_passwdfile_xmrseed_25': '98831F3A-фубар@crypto.org-xmrseed-25[1,4,1100].pws',
 		'ref_passwdfile_hex2bip39_12': '98831F3A-фубар@crypto.org-hex2bip39-12[1,4,1100].pws',
 		'ref_passwdfile_hex2bip39_12': '98831F3A-фубар@crypto.org-hex2bip39-12[1,4,1100].pws',
 		'ref_tx_file': { # data shared with ref_altcoin, autosign
 		'ref_tx_file': { # data shared with ref_altcoin, autosign
 			'btc': ('0B8D5A[15.31789,14,tl=1320969600].rawtx',
 			'btc': ('0B8D5A[15.31789,14,tl=1320969600].rawtx',
@@ -99,6 +100,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 		'ref_passwdfile_bip39_12_chksum': 'BF57 02A3 5229 CF18',
 		'ref_passwdfile_bip39_12_chksum': 'BF57 02A3 5229 CF18',
 		'ref_passwdfile_bip39_18_chksum': '31D3 1656 B7DC 27CF',
 		'ref_passwdfile_bip39_18_chksum': '31D3 1656 B7DC 27CF',
 		'ref_passwdfile_bip39_24_chksum': 'E565 3A59 7D91 4671',
 		'ref_passwdfile_bip39_24_chksum': 'E565 3A59 7D91 4671',
+		'ref_passwdfile_xmrseed_25_chksum': 'B488 21D3 4539 968D',
 		'ref_passwdfile_hex2bip39_12_chksum': '93AD 4AE2 03D1 8A0A',
 		'ref_passwdfile_hex2bip39_12_chksum': '93AD 4AE2 03D1 8A0A',
 	}
 	}
 	cmd_group = ( # TODO: move to tooltest2
 	cmd_group = ( # TODO: move to tooltest2
@@ -123,6 +125,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 		('ref_passwdfile_chk_bip39_12','saved reference password file (BIP39, 12 words)'),
 		('ref_passwdfile_chk_bip39_12','saved reference password file (BIP39, 12 words)'),
 		('ref_passwdfile_chk_bip39_18','saved reference password file (BIP39, 18 words)'),
 		('ref_passwdfile_chk_bip39_18','saved reference password file (BIP39, 18 words)'),
 		('ref_passwdfile_chk_bip39_24','saved reference password file (BIP39, 24 words)'),
 		('ref_passwdfile_chk_bip39_24','saved reference password file (BIP39, 24 words)'),
+		('ref_passwdfile_chk_xmrseed_25','saved reference password file (Monero new-style mnemonic, 25 words)'),
 		('ref_passwdfile_chk_hex2bip39_12','saved reference password file (hex-to-BIP39, 12 words)'),
 		('ref_passwdfile_chk_hex2bip39_12','saved reference password file (hex-to-BIP39, 12 words)'),
 
 
 #	Create the fake inputs:
 #	Create the fake inputs:
@@ -252,6 +255,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 	def ref_passwdfile_chk_bip39_12(self): return self.ref_passwdfile_chk(key='bip39_12',pat=r'BIP39.*len.* 12\b')
 	def ref_passwdfile_chk_bip39_12(self): return self.ref_passwdfile_chk(key='bip39_12',pat=r'BIP39.*len.* 12\b')
 	def ref_passwdfile_chk_bip39_18(self): return self.ref_passwdfile_chk(key='bip39_18',pat=r'BIP39.*len.* 18\b')
 	def ref_passwdfile_chk_bip39_18(self): return self.ref_passwdfile_chk(key='bip39_18',pat=r'BIP39.*len.* 18\b')
 	def ref_passwdfile_chk_bip39_24(self): return self.ref_passwdfile_chk(key='bip39_24',pat=r'BIP39.*len.* 24\b')
 	def ref_passwdfile_chk_bip39_24(self): return self.ref_passwdfile_chk(key='bip39_24',pat=r'BIP39.*len.* 24\b')
+	def ref_passwdfile_chk_xmrseed_25(self): return self.ref_passwdfile_chk(key='xmrseed_25',pat=r'Mon.*len.* 25\b')
 	def ref_passwdfile_chk_hex2bip39_12(self): return self.ref_passwdfile_chk(key='hex2bip39_12',pat=r'BIP39.*len.* 12\b')
 	def ref_passwdfile_chk_hex2bip39_12(self): return self.ref_passwdfile_chk(key='hex2bip39_12',pat=r'BIP39.*len.* 12\b')
 
 
 	def ref_tx_chk(self):
 	def ref_tx_chk(self):

+ 9 - 0
test/test_py_d/ts_ref_3seed.py

@@ -42,6 +42,10 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared):
 		'sids': ('FE3C6545', '1378FC64', '98831F3A'),
 		'sids': ('FE3C6545', '1378FC64', '98831F3A'),
 	}
 	}
 	shared_deps = ['mmdat',pwfile]
 	shared_deps = ['mmdat',pwfile]
+	skip_cmds = (
+		'ref_xmrseed_25_passwdgen_1',
+		'ref_xmrseed_25_passwdgen_2',
+	)
 	cmd_group = (
 	cmd_group = (
 		# reading saved reference wallets
 		# reading saved reference wallets
 		('ref_wallet_chk',  ([],'saved reference wallet')),
 		('ref_wallet_chk',  ([],'saved reference wallet')),
@@ -319,6 +323,7 @@ class TestSuiteRef3Addr(TestSuiteRef3Seed):
 		'ref_bip39_18_passwdgen_3': 'EF87 9904 88E2 5884',
 		'ref_bip39_18_passwdgen_3': 'EF87 9904 88E2 5884',
 		'ref_bip39_24_passwdgen_3': 'EBE8 2A8F 8F8C 7DBD',
 		'ref_bip39_24_passwdgen_3': 'EBE8 2A8F 8F8C 7DBD',
 		'ref_hex2bip39_24_passwdgen_3': '93FA 5EFD 33F3 760E',
 		'ref_hex2bip39_24_passwdgen_3': '93FA 5EFD 33F3 760E',
+		'ref_xmrseed_25_passwdgen_3': '91AE E76A 2827 C8CC',
 	}
 	}
 
 
 	cmd_group = (
 	cmd_group = (
@@ -339,6 +344,7 @@ class TestSuiteRef3Addr(TestSuiteRef3Seed):
 		('ref_bip39_12_passwdgen',    ([],'new refwallet passwd file chksum (BIP39, 12 words)')),
 		('ref_bip39_12_passwdgen',    ([],'new refwallet passwd file chksum (BIP39, 12 words)')),
 		('ref_bip39_18_passwdgen',    ([],'new refwallet passwd file chksum (BIP39, up to 18 words)')),
 		('ref_bip39_18_passwdgen',    ([],'new refwallet passwd file chksum (BIP39, up to 18 words)')),
 		('ref_bip39_24_passwdgen',    ([],'new refwallet passwd file chksum (BIP39, up to 24 words)')),
 		('ref_bip39_24_passwdgen',    ([],'new refwallet passwd file chksum (BIP39, up to 24 words)')),
+		('ref_xmrseed_25_passwdgen',  ([],'new refwallet passwd file chksum (Monero new-style mnemonic, 25 words)')),
 		('ref_hex2bip39_24_passwdgen',([],'new refwallet passwd file chksum (hex-to-BIP39, up to 24 words)')),
 		('ref_hex2bip39_24_passwdgen',([],'new refwallet passwd file chksum (hex-to-BIP39, up to 24 words)')),
 	)
 	)
 
 
@@ -383,6 +389,8 @@ class TestSuiteRef3Addr(TestSuiteRef3Seed):
 
 
 	def mn_pwgen(self,req_pw_len,pwfmt,ftype='passbip39',stdout=False):
 	def mn_pwgen(self,req_pw_len,pwfmt,ftype='passbip39',stdout=False):
 		pwlen = min(req_pw_len,{'1':12,'2':18,'3':24}[self.test_name[-1]])
 		pwlen = min(req_pw_len,{'1':12,'2':18,'3':24}[self.test_name[-1]])
+		if pwfmt == 'xmrseed':
+			pwlen += 1
 		ea = ['--accept-defaults']
 		ea = ['--accept-defaults']
 		return self.pwgen(ftype,'фубар@crypto.org',pwfmt,pwlen,ea,stdout=stdout)
 		return self.pwgen(ftype,'фубар@crypto.org',pwfmt,pwlen,ea,stdout=stdout)
 
 
@@ -390,3 +398,4 @@ class TestSuiteRef3Addr(TestSuiteRef3Seed):
 	def ref_bip39_18_passwdgen(self):     return self.mn_pwgen(18,'bip39',stdout=True)
 	def ref_bip39_18_passwdgen(self):     return self.mn_pwgen(18,'bip39',stdout=True)
 	def ref_bip39_24_passwdgen(self):     return self.mn_pwgen(24,'bip39')
 	def ref_bip39_24_passwdgen(self):     return self.mn_pwgen(24,'bip39')
 	def ref_hex2bip39_24_passwdgen(self): return self.mn_pwgen(24,'hex2bip39')
 	def ref_hex2bip39_24_passwdgen(self): return self.mn_pwgen(24,'hex2bip39')
+	def ref_xmrseed_25_passwdgen(self):   return self.mn_pwgen(24,'xmrseed',ftype='passxmrseed')

+ 11 - 0
test/tooltest2.py

@@ -32,6 +32,7 @@ from mmgen.common import *
 from test.common import *
 from test.common import *
 from mmgen.obj import is_wif,is_coin_addr
 from mmgen.obj import is_wif,is_coin_addr
 from mmgen.seed import is_bip39_mnemonic,is_mmgen_mnemonic
 from mmgen.seed import is_bip39_mnemonic,is_mmgen_mnemonic
+from mmgen.addr import is_xmrseed
 from mmgen.baseconv import *
 from mmgen.baseconv import *
 
 
 NL = ('\n','\r\n')[g.platform=='win']
 NL = ('\n','\r\n')[g.platform=='win']
@@ -112,6 +113,9 @@ tests = {
 			( ['0000000000000000000000000000000000000000000000000000000000000001'],
 			( ['0000000000000000000000000000000000000000000000000000000000000001'],
 			('able able able able able able able able able able able able ' +
 			('able able able able able able able able able able able able ' +
 			'able able able able able able able able able able able about') ),
 			'able able able able able able able able able able able about') ),
+			( ['e8164dda6d42bd1e261a3406b2038dcbddadbeefdeadbeefdeadbeefdeadbe0f','fmt=xmrseed'],
+			('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') ),
 		] + [([a,'fmt=bip39'],b) for a,b in bip39.vectors],
 		] + [([a,'fmt=bip39'],b) for a,b in bip39.vectors],
 		'mn2hex': [
 		'mn2hex': [
 			( ['table cast forgive master funny gaze sadness ripple million paint moral match','fmt=mmgen'],
 			( ['table cast forgive master funny gaze sadness ripple million paint moral match','fmt=mmgen'],
@@ -133,6 +137,10 @@ tests = {
 			( ['able able able able able able able able able able able able ' +
 			( ['able able able able able able able able able able able able ' +
 				'able able able able able able able able able able able about'],
 				'able able able able able able able able able able able about'],
 				'0000000000000000000000000000000000000000000000000000000000000001'),
 				'0000000000000000000000000000000000000000000000000000000000000001'),
+			( ['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','fmt=xmrseed'],
+				'e8164dda6d42bd1e261a3406b2038dcbddadbeefdeadbeefdeadbeefdeadbe0f'),
 		] + [([b,'fmt=bip39'],a) for a,b in bip39.vectors],
 		] + [([b,'fmt=bip39'],a) for a,b in bip39.vectors],
 		'mn_rand128': [
 		'mn_rand128': [
 			( [], is_mmgen_mnemonic, ['-r0']),
 			( [], is_mmgen_mnemonic, ['-r0']),
@@ -146,16 +154,19 @@ tests = {
 		'mn_rand256': [
 		'mn_rand256': [
 			( ['fmt=mmgen'], is_mmgen_mnemonic, ['-r0']),
 			( ['fmt=mmgen'], is_mmgen_mnemonic, ['-r0']),
 			( ['fmt=bip39'], is_bip39_mnemonic, ['-r0']),
 			( ['fmt=bip39'], is_bip39_mnemonic, ['-r0']),
+			( ['fmt=xmrseed'], is_xmrseed, ['-r0']),
 		],
 		],
 		'mn_stats': [
 		'mn_stats': [
 			( [], is_str ),
 			( [], is_str ),
 			( ['fmt=mmgen'], is_str ),
 			( ['fmt=mmgen'], is_str ),
 			( ['fmt=bip39'], is_str ),
 			( ['fmt=bip39'], is_str ),
+			( ['fmt=xmrseed'], is_str ),
 		],
 		],
 		'mn_printlist': [
 		'mn_printlist': [
 			( [], is_str ),
 			( [], is_str ),
 			( ['fmt=mmgen'], is_str ),
 			( ['fmt=mmgen'], is_str ),
 			( ['fmt=bip39'], is_str ),
 			( ['fmt=bip39'], is_str ),
+			( ['fmt=xmrseed','enum=true'], is_str ),
 		],
 		],
 	},
 	},
 	'Util': {
 	'Util': {

+ 21 - 1
test/unit_tests_d/ut_baseconv.py

@@ -9,6 +9,23 @@ from mmgen.exception import *
 class unit_test(object):
 class unit_test(object):
 
 
 	vectors = {
 	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': (
 		'b58': (
 			(('00',None),'1'),
 			(('00',None),'1'),
 			(('00',1),'1'),
 			(('00',1),'1'),
@@ -161,7 +178,10 @@ class unit_test(object):
 			for (hexstr,pad),ret_chk in data:
 			for (hexstr,pad),ret_chk in data:
 				if type(pad) == int:
 				if type(pad) == int:
 					pad = len(hexstr)
 					pad = len(hexstr)
-				ret = baseconv.tohex(ret_chk,wl_id=base,pad=pad)
+				ret = baseconv.tohex(
+					ret_chk.split() if base == 'xmrseed' else ret_chk,
+					wl_id=base,
+					pad=pad)
 				if pad == None:
 				if pad == None:
 					assert int(ret,16) == int(hexstr,16), rerr.format(int(ret,16),int(hexstr,16))
 					assert int(ret,16) == int(hexstr,16), rerr.format(int(ret,16),int(hexstr,16))
 				else:
 				else: