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

This commit is contained in:
The MMGen Project 2019-10-25 15:20:07 +00:00
commit 186f223646
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
6 changed files with 144 additions and 66 deletions

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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': {

View file

@ -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