Browse Source

base conversion: rework pad API, forbid empty input and output

The MMGen Project 5 years ago
parent
commit
186f223646
6 changed files with 144 additions and 66 deletions
  1. 4 0
      mmgen/exception.py
  2. 6 6
      mmgen/seed.py
  3. 1 1
      mmgen/tool.py
  4. 72 34
      mmgen/util.py
  5. 9 9
      test/tooltest2.py
  6. 52 16
      test/unit_tests_d/ut_baseconv.py

+ 4 - 0
mmgen/exception.py

@@ -38,12 +38,16 @@ class UnrecognizedTokenSymbol(Exception): mmcode = 2
 class TokenNotInBlockchain(Exception):    mmcode = 2
 class TokenNotInWallet(Exception):        mmcode = 2
 class BadTwComment(Exception):            mmcode = 2
+class BaseConversionError(Exception):     mmcode = 2
+class BaseConversionPadError(Exception):  mmcode = 2
 
 # 3: yellow hl, 'MMGen Error' + exception + message
 class RPCFailure(Exception):              mmcode = 3
 class BadTxSizeEstimate(Exception):       mmcode = 3
 class MaxInputSizeExceeded(Exception):    mmcode = 3
 class WalletFileError(Exception):         mmcode = 3
+class HexadecimalStringError(Exception):  mmcode = 3
+class SeedLengthError(Exception):         mmcode = 3
 
 # 4: red hl, 'MMGen Fatal Error' + exception + message
 class BadMMGenTxID(Exception):            mmcode = 4

+ 6 - 6
mmgen/seed.py

@@ -972,7 +972,7 @@ class MMGenSeedFile(SeedSourceUnenc):
 	ext = 'mmseed'
 
 	def _format(self):
-		b58seed = baseconv.b58encode(self.seed.data,pad=True)
+		b58seed = baseconv.b58encode(self.seed.data,pad='seed')
 		self.ssdata.chksum = make_chksum_6(b58seed)
 		self.ssdata.b58seed = b58seed
 		self.fmt_data = '{} {}\n'.format(self.ssdata.chksum,split_into_cols(4,b58seed))
@@ -1000,7 +1000,7 @@ class MMGenSeedFile(SeedSourceUnenc):
 		if not compare_chksums(a,'file',make_chksum_6(b),'computed',verbose=True):
 			return False
 
-		ret = baseconv.b58decode(b,pad=True)
+		ret = baseconv.b58decode(b,pad='seed')
 
 		if ret == False:
 			msg('Invalid base-58 encoded seed: {}'.format(val))
@@ -1146,8 +1146,8 @@ class Wallet (SeedSourceEnc):
 	def _format(self):
 		d = self.ssdata
 		s = self.seed
-		slt_fmt  = baseconv.b58encode(d.salt,pad=True)
-		es_fmt = baseconv.b58encode(d.enc_seed,pad=True)
+		slt_fmt  = baseconv.b58encode(d.salt,pad='seed')
+		es_fmt = baseconv.b58encode(d.enc_seed,pad='seed')
 		lines = (
 			d.label,
 			'{} {} {} {} {}'.format(s.sid.lower(), d.key_id.lower(),
@@ -1205,7 +1205,7 @@ class Wallet (SeedSourceEnc):
 			msg("Hash parameters '{}' don't match hash preset '{}'".format(' '.join(hash_params),d.hash_preset))
 			return False
 
-		lmin,foo,lmax = [v for k,v in baseconv.b58pad_lens] # 22,33,44
+		lmin,foo,lmax = sorted(baseconv.seed_pad_lens_rev['b58']) # 22,33,44
 		for i,key in (4,'salt'),(5,'enc_seed'):
 			l = lines[i].split(' ')
 			chk = l.pop(0)
@@ -1219,7 +1219,7 @@ class Wallet (SeedSourceEnc):
 					make_chksum_6(b58_val),'computed checksum',verbose=True):
 				return False
 
-			val = baseconv.b58decode(b58_val,pad=True)
+			val = baseconv.b58decode(b58_val,pad='seed')
 			if val == False:
 				msg('Invalid base 58 number: {}'.format(b58_val))
 				return False

+ 1 - 1
mmgen/tool.py

@@ -314,7 +314,7 @@ class MMGenToolCmdUtil(MMGenToolCmdBase):
 		return make_chksum_8(
 			get_data_from_file(infile,dash=True,quiet=True,binary=True))
 
-	def randb58(self,nbytes=32,pad=True):
+	def randb58(self,nbytes=32,pad=0):
 		"generate random data (default: 32 bytes) and convert it to base 58"
 		return baseconv.b58encode(get_random(nbytes),pad=pad)
 

+ 72 - 34
mmgen/util.py

@@ -295,8 +295,12 @@ class baseconv(object):
 		'tirosh': '48f05e1f', # tirosh truncated to mn_base (1626)
 		# 'tirosh1633': '1a5faeff'
 	}
-	b58pad_lens =     [(16,22), (24,33), (32,44)]
-	b58pad_lens_rev = [(v,k) for k,v in b58pad_lens]
+	seed_pad_lens = {
+		'b58': { 16:22, 24:33, 32:44 },
+	}
+	seed_pad_lens_rev = {
+		'b58': { 22:16, 33:24, 44:32 },
+	}
 
 	@classmethod
 	def init_mn(cls,mn_id):
@@ -310,31 +314,6 @@ class baseconv(object):
 		else: # bip39
 			cls.digits[mn_id] = cls.words
 
-	@classmethod
-	def b58encode(cls,s,pad=None):
-		pad = cls._get_pad(s,pad,'b58encode',cls.b58pad_lens,(bytes,))
-		return cls.fromhex(s.hex(),'b58',pad=pad,tostr=True)
-
-	@classmethod
-	def b58decode(cls,s,pad=None):
-		pad = cls._get_pad(s,pad,'b58decode',cls.b58pad_lens_rev,(bytes,str))
-		return bytes.fromhex(cls.tohex(s,'b58',pad=pad*2 if pad else None))
-
-	@staticmethod
-	def _get_pad(s,pad,op_desc,pad_map,ok_types):
-		if not isinstance(s,ok_types):
-			m = "{}() input must be one of {}, not '{}'"
-			raise ValueError(m.format(op_desc,repr([t.__name__ for t in ok_types]),type(s).__name__))
-		if pad:
-			assert type(pad) == bool,"'pad' must be boolean type"
-			d = dict(pad_map)
-			if not len(s) in d:
-				m = 'Invalid data length for {}(pad=True) (must be one of {})'
-				raise ValueError(m.format(op_desc,repr([e[0] for e in pad_map])))
-			return d[len(s)]
-		else:
-			return None
-
 	@classmethod
 	def get_wordlist(cls,wl_id):
 		cls.init_mn(wl_id)
@@ -364,38 +343,97 @@ class baseconv(object):
 
 		qmsg('List is sorted') if tuple(sorted(wl)) == wl else die(3,'ERROR: List is not sorted!')
 
+	@classmethod
+	def get_pad(cls,pad,seed_pad_func):
+		"""
+		'pad' argument to applicable baseconv methods must be either None, 'seed' or an integer.
+		If None, output of minimum (but never zero) length will be produced.
+		If 'seed', output length will be mapped from input length using seed_pad_lens.
+		If an integer, it refers to the minimum allowable *string length* of the output.
+		"""
+		if pad == None:
+			return 0
+		elif isinstance(pad,int) and type(pad) != bool:
+			return pad
+		elif pad == 'seed':
+			return seed_pad_func()
+		else:
+			m = "{!r}: illegal value for 'pad' (must be None,'seed' or int)"
+			raise BaseConversionPadError(m.format(pad))
+
 	@classmethod
 	def tohex(cls,words_arg,wl_id,pad=None):
+		"convert string or list data of base specified by 'wl_id' to hex string"
 
 		words = words_arg if isinstance(words_arg,(list,tuple)) else tuple(words_arg.strip())
 
+		if len(words) == 0:
+			raise BaseConversionError('empty {} data'.format(wl_id))
+
+		def get_seed_pad():
+			assert wl_id in cls.seed_pad_lens_rev,'seed padding not supported for base {!r}'.format(wl_id)
+			d = cls.seed_pad_lens_rev[wl_id]
+			if not len(words) in d:
+				m = '{}: invalid length for seed-padded {} data in base conversion'
+				raise BaseConversionError(m.format(len(words),wl_id))
+			return d[len(words)] * 2
+
+		pad = max(cls.get_pad(pad,get_seed_pad),2)
 		wl = cls.digits[wl_id]
 		base = len(wl)
 
 		if not set(words) <= set(wl):
-			die(2,'{} is not in {} (base{}) format'.format(repr(words_arg),wl_id,base))
+			m = '{!r}: not in {} (base{}) format'
+			raise BaseConversionError(m.format(words_arg,wl_id,base))
 
 		deconv =  [wl.index(words[::-1][i])*(base**i) for i in range(len(words))]
-		ret = ('{:0{w}x}'.format(sum(deconv),w=pad or 0))
+		ret = ('{:0{w}x}'.format(sum(deconv),w=pad))
 		return (('','0')[len(ret) % 2] + ret)
 
 	@classmethod
-	def fromhex(cls,hexnum,wl_id,pad=None,tostr=False):
+	def fromhex(cls,hexstr,wl_id,pad=None,tostr=False):
+		"convert hex string to list or string data of base specified by 'wl_id'"
 		if wl_id in ('mmgen','tirosh','bip39'):
 			assert tostr == False,"'tostr' must be False for '{}'".format(wl_id)
 
-		if not is_hex_str(hexnum):
-			die(2,"{!r}: not a hexadecimal number".format(hexnum))
+		if not is_hex_str(hexstr):
+			m = '{!r}: not a hexadecimal string'
+			raise HexadecimalStringError(m.format(hexstr))
+
+		if not hexstr:
+			m = 'empty hex strings not allowed in base conversion'
+			raise HexadecimalStringError(m)
 
+		def get_seed_pad():
+			assert wl_id in cls.seed_pad_lens,'seed padding not supported for base {!r}'.format(wl_id)
+			d = cls.seed_pad_lens[wl_id]
+			slen = len(hexstr) // 2
+			if not slen in d:
+				m = '{}: invalid seed byte length for seed-padded base conversion'
+				raise SeedLengthError(m.format(slen))
+			return d[slen]
+
+		pad = max(cls.get_pad(pad,get_seed_pad),1)
 		wl = cls.digits[wl_id]
 		base = len(wl)
-		num,ret = int(hexnum,16),[]
+
+		num,ret = int(hexstr,16),[]
 		while num:
 			ret.append(num % base)
 			num //= base
-		o = [wl[n] for n in [0] * ((pad or 0)-len(ret)) + ret[::-1]]
+		o = [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]]
 		return ''.join(o) if tostr else o
 
+	@classmethod
+	def b58decode(cls,s,pad=None):
+		'convert base58 string to bytes'
+		return bytes.fromhex(cls.tohex(s,'b58',pad=pad))
+
+	@classmethod
+	def b58encode(cls,s,pad=None):
+		'convert bytes to base58 string'
+		return cls.fromhex(s.hex(),'b58',pad=pad,tostr=True)
+
 def match_ext(addr,ext):
 	return addr.split('.')[-1] == ext
 

+ 9 - 9
test/tooltest2.py

@@ -163,7 +163,7 @@ tests = {
 			( ['deadbeef'], 'DPK3PXP' ),
 			( ['deadbeefdeadbeef'], 'N5LN657PK3PXP' ),
 			( ['ffffffffffffffff'], 'P777777777777' ),
-			( ['0000000000000000'], '' ),
+			( ['0000000000000000'], 'A' ),
 			( ['0000000000000000','pad=10'], 'AAAAAAAAAA' ),
 			( ['ff','pad=10'], 'AAAAAAAAH7' ),
 		],
@@ -171,7 +171,7 @@ tests = {
 			( ['DPK3PXP'], 'deadbeef' ),
 			( ['N5LN657PK3PXP'], 'deadbeefdeadbeef' ),
 			( ['P777777777777'], 'ffffffffffffffff' ),
-			( ['','pad=16'], '0000000000000000' ),
+			( ['A','pad=16'], '0000000000000000' ),
 			( ['AAAAAAAAAA','pad=16'], '0000000000000000' ),
 			( ['AAAAAAAAH7','pad=2'], 'ff' ),
 		],
@@ -180,7 +180,7 @@ tests = {
 			( ['deadbeefdeadbeef'], '5CizhNNRPYpBjrbYX' ),
 			( ['ffffffffffffffff'], '5qCHTcgbQwprzjWrb' ),
 			( ['0000000000000000'], '111111114FCKVB' ),
-			( [''], '3QJmnh' ),
+			( ['00'], '1Wh4bh' ),
 			( ['000000000000000000000000000000000000000000'], '1111111111111111111114oLvT2' ),
 		],
 		'b58chktohex': [
@@ -195,7 +195,7 @@ tests = {
 			( [b'\xde\xad\xbe\xef'], '6h8cQN' ),
 			( [b'\xde\xad\xbe\xef\xde\xad\xbe\xef'], 'eFGDJURJykA' ),
 			( [b'\xff\xff\xff\xff\xff\xff\xff\xff'], 'jpXCZedGfVQ' ),
-			( [b'\x00\x00\x00\x00\x00\x00\x00\x00'], '' ),
+			( [b'\x00\x00\x00\x00\x00\x00\x00\x00'], '1' ),
 			( [b'\x00\x00\x00\x00\x00\x00\x00\x00','pad=10'], '1111111111' ),
 			( [b'\xff','pad=10'], '111111115Q' ),
 		],
@@ -203,7 +203,7 @@ tests = {
 			( ['6h8cQN'], b'\xde\xad\xbe\xef' ),
 			( ['eFGDJURJykA'], b'\xde\xad\xbe\xef\xde\xad\xbe\xef' ),
 			( ['jpXCZedGfVQ'], b'\xff\xff\xff\xff\xff\xff\xff\xff' ),
-			( ['','pad=16'], b'\x00\x00\x00\x00\x00\x00\x00\x00' ),
+			( ['1','pad=16'],  b'\x00\x00\x00\x00\x00\x00\x00\x00' ),
 			( ['1111111111','pad=16'], b'\x00\x00\x00\x00\x00\x00\x00\x00' ),
 			( ['111111115Q','pad=2'], b'\xff' ),
 		],
@@ -211,7 +211,7 @@ tests = {
 			( ['deadbeef'], '6h8cQN' ),
 			( ['deadbeefdeadbeef'], 'eFGDJURJykA' ),
 			( ['ffffffffffffffff'], 'jpXCZedGfVQ' ),
-			( ['0000000000000000'], '' ),
+			( ['0000000000000000'], '1' ),
 			( ['0000000000000000','pad=10'], '1111111111' ),
 			( ['ff','pad=10'], '111111115Q' ),
 		],
@@ -219,7 +219,7 @@ tests = {
 			( ['6h8cQN'], 'deadbeef' ),
 			( ['eFGDJURJykA'], 'deadbeefdeadbeef' ),
 			( ['jpXCZedGfVQ'], 'ffffffffffffffff' ),
-			( ['','pad=16'], '0000000000000000' ),
+			( ['1','pad=16'],  '0000000000000000' ),
 			( ['1111111111','pad=16'], '0000000000000000' ),
 			( ['111111115Q','pad=2'], 'ff' ),
 		],
@@ -274,9 +274,9 @@ tests = {
 			( ['nbytes=6'], {'boolfunc':is_hex_str,'len':12}, ['-r0'] ),
 		],
 		'randb58': [
-			( [], {'boolfunc':is_b58_str,'len':44}, ['-r0'] ),
+			( [], {'boolfunc':is_b58_str}, ['-r0'] ),
 			( ['nbytes=16'], {'boolfunc':is_b58_str,'len':22}, ['-r0'] ),
-			( ['nbytes=12','pad=false'], is_b58_str, ['-r0'] ),
+			( ['nbytes=12','pad=0'], is_b58_str, ['-r0'] ),
 		],
 	},
 	'Wallet': {

+ 52 - 16
test/unit_tests_d/ut_baseconv.py

@@ -10,7 +10,7 @@ class unit_test(object):
 
 	vectors = {
 		'b58': (
-			(('00',None),''),
+			(('00',None),'1'),
 			(('00',1),'1'),
 			(('00',2),'11'),
 			(('01',None),'2'),
@@ -21,14 +21,20 @@ class unit_test(object):
 			(('0f',2),'1G'),
 			(('deadbeef',None),'6h8cQN'),
 			(('deadbeef',20),'111111111111116h8cQN'),
-			(('00000000',None),''),
+			(('00000000',None),'1'),
 			(('00000000',20),'11111111111111111111'),
 			(('ffffffff',None),'7YXq9G'),
 			(('ffffffff',20),'111111111111117YXq9G'),
+			(('ff'*16,'seed'),'YcVfxkQb6JRzqk5kF2tNLv'),
+			(('ff'*24,'seed'),'QLbz7JHiBTspS962RLKV8GndWFwiEaqKL'),
+			(('ff'*32,'seed'),'JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFG'),
+			(('00'*16,'seed'),'1111111111111111111111'),
+			(('00'*24,'seed'),'111111111111111111111111111111111'),
+			(('00'*32,'seed'),'11111111111111111111111111111111111111111111'),
 		),
 		# MMGen-flavored base32 using simple base conversion
 		'b32': (
-			(('00',None),''),
+			(('00',None),'A'),
 			(('00',1),'A'),
 			(('00',2),'AA'),
 			(('01',None),'B'),
@@ -39,13 +45,13 @@ class unit_test(object):
 			(('0f',2),'AP'),
 			(('deadbeef',None),'DPK3PXP'),
 			(('deadbeef',20),'AAAAAAAAAAAAADPK3PXP'),
-			(('00000000',None),''),
+			(('00000000',None),'A'),
 			(('00000000',20),'AAAAAAAAAAAAAAAAAAAA'),
 			(('ffffffff',None),'D777777'),
 			(('ffffffff',20),'AAAAAAAAAAAAAD777777'),
 		),
 		'b16': (
-			(('00',None),''),
+			(('00',None),'0'),
 			(('00',1),'0'),
 			(('00',2),'00'),
 			(('01',None),'1'),
@@ -56,13 +62,13 @@ class unit_test(object):
 			(('0f',2),'0f'),
 			(('deadbeef',None),'deadbeef'),
 			(('deadbeef',20),'000000000000deadbeef'),
-			(('00000000',None),''),
+			(('00000000',None),'0'),
 			(('00000000',20),'00000000000000000000'),
 			(('ffffffff',None),'ffffffff'),
 			(('ffffffff',20),'000000000000ffffffff'),
 		),
 		'b10': (
-			(('00',None),''),
+			(('00',None),'0'),
 			(('00',1),'0'),
 			(('00',2),'00'),
 			(('01',None),'1'),
@@ -73,13 +79,13 @@ class unit_test(object):
 			(('0f',2),'15'),
 			(('deadbeef',None),'3735928559'),
 			(('deadbeef',20),'00000000003735928559'),
-			(('00000000',None),''),
+			(('00000000',None),'0'),
 			(('00000000',20),'00000000000000000000'),
 			(('ffffffff',None),'4294967295'),
 			(('ffffffff',20),'00000000004294967295'),
 		),
 		'b8': (
-			(('00',None),''),
+			(('00',None),'0'),
 			(('00',1),'0'),
 			(('00',2),'00'),
 			(('01',None),'1'),
@@ -90,7 +96,7 @@ class unit_test(object):
 			(('0f',2),'17'),
 			(('deadbeef',None),'33653337357'),
 			(('deadbeef',20),'00000000033653337357'),
-			(('00000000',None),''),
+			(('00000000',None),'0'),
 			(('00000000',20),'00000000000000000000'),
 			(('ffffffff',None),'37777777777'),
 			(('ffffffff',20),'00000000037777777777'),
@@ -106,32 +112,62 @@ class unit_test(object):
 		rerr = "return value ({!r}) does not match reference value ({!r})"
 
 		qmsg_r('\nChecking hex-to-base conversion:')
-		fs = "  {h:10} {p:6} {r}"
 		for base,data in self.vectors.items():
+			fs = "  {h:%s}  {p:<6} {r}" % max(len(d[0][0]) for d in data)
 			if not opt.verbose: qmsg_r(' {}'.format(base))
 			vmsg('\nBase: {}'.format(base))
 			vmsg(fs.format(h='Input',p='Pad',r='Output'))
 			for (hexstr,pad),ret_chk in data:
 				ret = baseconv.fromhex(hexstr,wl_id=base,pad=pad,tostr=True)
-				assert len(ret) >= (pad or 0), perr.format(ret,pad)
+				if pad != 'seed':
+					assert len(ret) >= (pad or 0), perr.format(ret,pad)
 				assert ret == ret_chk, rerr.format(ret,ret_chk)
 				vmsg(fs.format(h=hexstr,r=ret,p=str(pad)))
 #				msg("(('{h}',{p}),'{r}'),".format(h=hexstr,r=ret,c=ret_chk,p=pad))
 #			msg('')
 #		return True
 		qmsg_r('\nChecking base-to-hex conversion:')
-		fs = "  {h:24} {p:<6} {r}"
 		for base,data in self.vectors.items():
+			fs = "  {h:%s}  {p:<6} {r}" % max(len(d[1]) for d in data)
 			if not opt.verbose: qmsg_r(' {}'.format(base))
 			vmsg('\nBase: {}'.format(base))
 			vmsg(fs.format(h='Input',p='Pad',r='Output'))
 			for (hexstr,pad),ret_chk in data:
-				ret = baseconv.tohex(ret_chk,wl_id=base,pad=len(hexstr))
-				assert ret == hexstr, rerr.format(ret,ret_chk)
-				vmsg(fs.format(h=ret_chk,r=ret,p=len(hexstr)))
+				if type(pad) == int:
+					pad = len(hexstr)
+				ret = baseconv.tohex(ret_chk,wl_id=base,pad=pad)
+				if pad == None:
+					assert int(ret,16) == int(hexstr,16), rerr.format(ret,ret_chk)
+				else:
+					assert ret == hexstr, rerr.format(ret,ret_chk)
+				vmsg(fs.format(h=ret_chk,r=ret,p=str(pad)))
 #				msg("(('{h}',{p}),'{r}'),".format(h=hexstr,r=ret_chk,c=ret_chk,p=pad))
 
 		qmsg('')
+
+		vmsg('')
+		qmsg('Checking error handling:')
+
+		b = baseconv
+		bad_data = (
+('bad hexstr',       'HexadecimalStringError','not a hexadecimal str',   lambda:b.fromhex('x','b58')),
+('empty hexstr',     'HexadecimalStringError','empty hex strings not',   lambda:b.fromhex('','b58')),
+('bad b58 data',     'BaseConversionError',   'not in b58',              lambda:b.tohex('IfFzZ','b58')),
+('empty b58 data',   'BaseConversionError',   'empty b58 data',          lambda:b.tohex('','b58')),
+('empty b8 data' ,   'BaseConversionError',   'empty b8 data',           lambda:b.tohex('','b8')),
+('bad b32 data',     'BaseConversionError',   'not in b32',              lambda:b.tohex('1az','b32')),
+('bad pad arg (in)', 'BaseConversionPadError',"illegal value for 'pad'", lambda:b.fromhex('ff','b58',pad='foo')),
+('bad pad arg (in)', 'BaseConversionPadError',"illegal value for 'pad'", lambda:b.fromhex('ff','b58',pad=False)),
+('bad pad arg (in)', 'BaseConversionPadError',"illegal value for 'pad'", lambda:b.fromhex('ff','b58',pad=True)),
+('bad seedlen (in)', 'SeedLengthError',       "invalid seed byte length",lambda:b.fromhex('ff','b58',pad='seed')),
+('bad pad arg (out)','BaseConversionPadError',"illegal value for 'pad'", lambda:b.tohex('Z','b58',pad='foo')),
+('bad pad arg (out)','BaseConversionPadError',"illegal value for 'pad'", lambda:b.tohex('Z','b58',pad=False)),
+('bad pad arg (out)','BaseConversionPadError',"illegal value for 'pad'", lambda:b.tohex('Z','b58',pad=True)),
+('bad seedlen (out)','BaseConversionError',   "invalid length for seed", lambda:b.tohex('Z','b58',pad='seed')),
+		)
+
+		ut.process_bad_data(bad_data)
+
 		msg('OK')
 
 		return True