Browse Source

Object-oriented reimplementation of addr data structures

Reference wallet with checksums added to test/test.py
philemon 10 years ago
parent
commit
54efc7679e
14 changed files with 584 additions and 438 deletions
  1. 17 7
      mmgen/Opts.py
  2. 261 50
      mmgen/addr.py
  3. 9 2
      mmgen/config.py
  4. 12 10
      mmgen/crypto.py
  5. 8 22
      mmgen/main_addrgen.py
  6. 45 43
      mmgen/main_addrimport.py
  7. 44 28
      mmgen/main_txcreate.py
  8. 11 16
      mmgen/main_txsign.py
  9. 4 4
      mmgen/main_walletchk.py
  10. 2 1
      mmgen/main_walletgen.py
  11. 7 2
      mmgen/tool.py
  12. 30 171
      mmgen/tx.py
  13. 11 4
      mmgen/util.py
  14. 123 78
      test/test.py

+ 17 - 7
mmgen/Opts.py

@@ -23,7 +23,7 @@ Opts.py:  Option handling routines for the MMGen suite
 import sys
 import mmgen.config as g
 import mmgen.opt.Opts
-from mmgen.util import msg,check_infile,check_outfile,check_outdir
+from mmgen.util import msg,check_infile,check_outfile,check_outdir,msgrepr_exit,msgrepr
 
 def usage(hd): mmgen.opt.Opts.usage(hd)
 
@@ -60,17 +60,28 @@ def parse_opts(argv,help_data):
 	('quiet','verbose')
 	): warn_incompatible_opts(opts,l)
 
-	# check_opts() doesn't touch opts[]
+	if 'usr_randchars' in opts: g.use_urandchars = True
+
+	# check opts[] dictionary without modifying it
 	if not check_opts(opts,long_opts): sys.exit(1)
 
-	# If unset, set these to default values in mmgen.config (g):
+	# If user opt is unset, set it to default value in mmgen.config (g):
 	for v in g.dfl_vars:
 		if v in opts: typeconvert_override_var(opts,v)
 		else: opts[v] = g.__dict__[v]
 
-	# Opposite of above: if set, override the default values in mmgen.config (g):
-	for k in 'no_keyconv','verbose','quiet':
-		if k in opts: g.__dict__[k] = opts[k]
+	# Opposite of above: set the value in mmgen.config (g) from user opt:
+	for k in g.usr_set_vars:
+		if k in opts:
+			v = opts[k]
+			try: v = type(g.__dict__[k])(v)
+			except:
+				msg(
+	"Argument '%s' for option '--%s' cannot be converted to target type %s" %
+		(v,k.replace("_","-"),type(g.__dict__[k]))
+				)
+				sys.exit(1)
+			g.__dict__[k] = v
 
 	if g.debug: print "opts after typeconvert: %s" % opts
 
@@ -170,7 +181,6 @@ def check_opts(opts,long_opts):
 			if not opt_is_in_list(val,g.hash_presets.keys(),what): return False
 		elif opt == 'usr_randchars':
 			if not opt_is_int(val,what): return False
-			if val == '0': continue
 			if not opt_compares(val,">=",g.min_urandchars,what): return False
 			if not opt_compares(val,"<=",g.max_urandchars,what): return False
 		else:

+ 261 - 50
mmgen/addr.py

@@ -26,7 +26,9 @@ from hashlib import new as hashlib_new
 from binascii import hexlify, unhexlify
 
 from mmgen.bitcoin import numtowif
-from mmgen.util import msg,qmsg,qmsg_r
+# from mmgen.util import msg,qmsg,qmsg_r,make_chksum_N,get_lines_from_file,get_data_from_file,get_extension
+from mmgen.util import *
+from mmgen.tx import is_mmgen_idx,is_mmgen_seed_id,is_btc_addr,is_wip_key,get_wif2addr_f
 import mmgen.config as g
 
 addrmsgs = {
@@ -58,7 +60,10 @@ def test_for_keyconv():
 	return True
 
 
-def generate_addrs(seed, addrnums, opts, seed_id=""):
+def generate_addrs(seed, addrnums, opts):
+
+	from util import make_chksum_8
+	seed_id = make_chksum_8(seed) # Must do this before seed gets clobbered
 
 	if 'a' in opts['gen_what']:
 		if g.no_keyconv or test_for_keyconv() == False:
@@ -69,12 +74,6 @@ def generate_addrs(seed, addrnums, opts, seed_id=""):
 			from subprocess import check_output
 			keyconv = "keyconv"
 
-	ai_attrs = ("num,sec,wif,addr") if 'ka' in opts['gen_what'] else (
-		("num,sec,wif") if 'k' in opts['gen_what'] else ("num,addr"))
-
-	from collections import namedtuple
-	addrinfo = namedtuple("addrinfo",ai_attrs.split(","))
-
 	addrnums = sorted(set(addrnums)) # don't trust the calling function
 	t_addrs,num,pos,out = len(addrnums),0,0,[]
 
@@ -84,6 +83,8 @@ def generate_addrs(seed, addrnums, opts, seed_id=""):
 		'a':  ('address','es')
 	}[opts['gen_what']]
 
+	from mmgen.addr import AddrInfoEntry,AddrInfo
+
 	while pos != t_addrs:
 		seed = sha512(seed).digest()
 		num += 1 # round
@@ -95,71 +96,281 @@ def generate_addrs(seed, addrnums, opts, seed_id=""):
 
 		qmsg_r("\rGenerating %s #%s (%s of %s)" % (w[0],num,pos,t_addrs))
 
+		e = AddrInfoEntry()
+		e.idx = num
+
 		# Secret key is double sha256 of seed hash round /num/
 		sec = sha256(sha256(seed).digest()).hexdigest()
 		wif = numtowif(int(sec,16))
 
 		if 'a' in opts['gen_what']:
 			if keyconv:
-				addr = check_output([keyconv, wif]).split()[1]
+				e.addr = check_output([keyconv, wif]).split()[1]
 			else:
-				addr = privnum2addr(int(sec,16))
+				e.addr = privnum2addr(int(sec,16))
 
-		out.append(addrinfo(*eval(ai_attrs)))
+		if 'k' in opts['gen_what']: e.wif = wif
+		if 'b16' in opts: e.sec = sec
+
+		out.append(e)
 
 	m = w[0] if t_addrs == 1 else w[0]+w[1]
-	if seed_id:
-		qmsg("\r%s: %s %s generated%s" % (seed_id,t_addrs,m," "*15))
-	else:
-		qmsg("\rGenerated %s %s%s" % (t_addrs,m," "*15))
+	qmsg("\r%s: %s %s generated%s" % (seed_id,t_addrs,m," "*15))
+	a = AddrInfo(has_keys='k' in opts['gen_what'])
+	a.initialize(seed_id,out)
+	return a
 
-	return out
+def _parse_addrfile_body(lines,has_keys=False,check=False):
 
+	if has_keys and len(lines) % 2:
+		return "Key-address file has odd number of lines"
 
-def format_addr_data(addr_data, addr_data_chksum, seed_id, addr_idxs, opts):
+	ret = []
+	while lines:
+		a = AddrInfoEntry()
+		l = lines.pop(0)
+		d = l.split(None,2)
 
-	fs = "  {:<%s}  {}" % len(str(addr_data[-1].num))
+		if not is_mmgen_idx(d[0]):
+			return "'%s': invalid address num. in line: '%s'" % (d[0],l)
+		if not is_btc_addr(d[1]):
+			return "'%s': invalid Bitcoin address" % d[1]
 
-	if 'a' in opts['gen_what']:
-		out = [] if 'stdout' in opts else [addrmsgs['addrfile_header']+"\n"]
-		w = "Key-address" if 'k' in opts['gen_what'] else "Address"
-		out.append("# {} data checksum for {}[{}]: {}".format(
-					w, seed_id, fmt_addr_idxs(addr_idxs), addr_data_chksum))
-		out.append("# Record this value to a secure location\n")
-	else: out = []
-
-	out.append("%s {" % seed_id.upper())
-
-	for d in addr_data:
-		if 'a' in opts['gen_what']:  # First line with number
-			out.append(fs.format(d.num, d.addr))
+		if len(d) == 3: check_addr_label(d[2])
+		else:           d.append("")
+
+		a.idx,a.addr,a.comment = int(d[0]),unicode(d[1]),unicode(d[2])
+
+		if has_keys:
+			l = lines.pop(0)
+			d = l.split(None,2)
+
+			if d[0] != "wif:":
+				return "Invalid key line in file: '%s'" % l
+			if not is_wip_key(d[1]):
+				return "'%s': invalid Bitcoin key" % d[1]
+
+			a.wif = unicode(d[1])
+
+		ret.append(a)
+
+	if has_keys and keypress_confirm("Check key-to-address validity?"):
+		wif2addr_f = get_wif2addr_f()
+		llen = len(ret)
+		for n,e in enumerate(ret):
+			msg_r("\rVerifying keys %s/%s" % (n+1,llen))
+			if e.addr != wif2addr_f(e.wif):
+				return "Key doesn't match address!\n  %s\n  %s" % (e.wif,e.addr)
+		msg(" - done")
+
+	return ret
+
+
+def _parse_addrfile(fn,buf=[],has_keys=False,exit_on_error=True):
+
+	if buf: lines = remove_comments(buf.split("\n"))
+	else:   lines = get_lines_from_file(fn,"address data",trim_comments=True)
+
+	try:
+		sid,obrace = lines[0].split()
+	except:
+		errmsg = "Invalid first line: '%s'" % lines[0]
+	else:
+		cbrace = lines[-1]
+		if obrace != '{':
+			errmsg = "'%s': invalid first line" % lines[0]
+		elif cbrace != '}':
+			errmsg = "'%s': invalid last line" % cbrace
+		elif not is_mmgen_seed_id(sid):
+			errmsg = "'%s': invalid Seed ID" % sid
 		else:
-			out.append(fs.format(d.num, "wif: "+d.wif))
+			ret = _parse_addrfile_body(lines[1:-1],has_keys)
+			if type(ret) == list: return sid,ret
+			else: errmsg = ret
+
+	if exit_on_error:
+		msg(errmsg)
+		sys.exit(3)
+	else:
+		return False
 
-		if 'k' in opts['gen_what']:   # Subsequent lines
-			if 'b16' in opts:
-				out.append(fs.format("", "hex: "+d.sec))
-			if 'a' in opts['gen_what']:
-				out.append(fs.format("", "wif: "+d.wif))
 
-	out.append("}")
+def _parse_keyaddr_file(infile):
+	d = get_data_from_file(infile,"%s key-address file data" % g.proj_name)
+	enc_ext = get_extension(infile) == g.mmenc_ext
+	if enc_ext or not is_utf8(d):
+		m = "Decrypting" if enc_ext else "Attempting to decrypt"
+		msg("%s key-address file %s" % (m,infile))
+		from crypto import mmgen_decrypt_retry
+		d = mmgen_decrypt_retry(d,"key-address file")
+	return _parse_addrfile("",buf=d,has_keys=True,exit_on_error=False)
 
-	return "\n".join(out) + "\n"
 
+class AddrInfoList(object):
 
-def fmt_addr_idxs(addr_idxs):
+	def __init__(self,addrinfo=None):
+		self.data = {}
 
-	addr_idxs = list(sorted(set(addr_idxs)))
+	def seed_ids(self):
+		return self.data.keys()
 
-	prev = addr_idxs[0]
-	ret = prev,
+	def addrinfo(self,sid):
+		# TODO: Validate sid
+		if sid in self.data:
+			return self.data[sid]
 
-	for i in addr_idxs[1:]:
-		if i == prev + 1:
-			if i == addr_idxs[-1]: ret += "-", i
+	def add(self,addrinfo):
+		if type(addrinfo) == AddrInfo:
+			self.data[addrinfo.seed_id] = addrinfo
+			return True
 		else:
-			if prev != ret[-1]: ret += "-", prev
-			ret += ",", i
-		prev = i
+			msg("Error: object %s is not of type AddrInfo" % repr(addrinfo))
+			sys.exit(1)
+
+	def make_reverse_dict(self,btcaddrs):
+		d = {}
+		for k in self.data.keys():
+			d.update(self.data[k].make_reverse_dict(btcaddrs))
+		return d
+
+class AddrInfoEntry(object):
+
+	def __init__(self):
+		pass
+
+class AddrInfo(object):
+
+	def __init__(self,addrfile="",has_keys=False):
+		self.has_keys=has_keys
+		if addrfile:
+			f = _parse_keyaddr_file if has_keys else _parse_addrfile
+			sid,adata = f(addrfile)
+			self.initialize(sid,adata)
+
+	def initialize(self,seed_id,addrdata):
+		if seed_id in self.__dict__:
+			msg("Seed ID already set for object %s" % self)
+			return False
+		self.seed_id = seed_id
+		self.addrdata = addrdata
+		self.num_addrs = len(addrdata)
+		self.make_addrdata_chksum()
+		self.fmt_addr_idxs()
+		w = "key" if self.has_keys else "addr"
+		qmsg("Computed checksum for %s data %s[%s]: %s" %
+				(w,self.seed_id,self.idxs_fmt,self.checksum))
+		qmsg("Check this value against your records")
+
+	def idxs(self):
+		return [e.idx for e in self.addrdata]
+
+	def addrs(self):
+		return ["%s:%s"%(self.seed_id,e.idx) for e in self.addrdata]
+
+	def addrpairs(self):
+		return [(e.idx,e.addr) for e in self.addrdata]
+
+	def btcaddrs(self):
+		return [e.addr for e in self.addrdata]
+
+	def comments(self):
+		return [e.comment for e in self.addrdata]
+
+	def entry(self,idx):
+		for e in self.addrdata:
+			if idx == e.idx: return e
+
+	def btcaddr(self,idx):
+		for e in self.addrdata:
+			if idx == e.idx: return e.addr
+
+	def comment(self,idx):
+		for e in self.addrdata:
+			if idx == e.idx: return e.comment
+
+	def set_comment(self,idx,comment):
+		for e in self.addrdata:
+			if idx == e.idx: e.comment = comment
+
+	def make_reverse_dict(self,btcaddrs):
+		d = {}
+		for e in self.addrdata:
+			try:
+				i = btcaddrs.index(e.addr)
+				d[btcaddrs[i]] = ("%s:%s"%(self.seed_id,e.idx),e.comment)
+			except: pass
+		return d
+
+	def make_addrdata_chksum(self):
+		nchars = 24
+		lines = [" ".join([str(e.idx),e.addr]+([e.wif] if self.has_keys else []))
+						for e in self.addrdata]
+		self.checksum = make_chksum_N(" ".join(lines), nchars, sep=True)
+
+	def fmt_data(self):
+
+		fs = "  {:<%s}  {}" % len(str(self.addrdata[-1].idx))
+
+		# Header
+		have_addrs,have_wifs,have_secs = True,True,True
+
+		try: self.addrdata[0].addr
+		except: have_addrs = False
+
+		try: self.addrdata[0].wif
+		except: have_wifs = False
+
+		try: self.addrdata[0].sec
+		except: have_secs = False
+
+		if not (have_addrs or have_wifs):
+			msg("No addresses or wifs in addr data!")
+			sys.exit(3)
+
+		out = []
+		if have_addrs:
+			from mmgen.addr import addrmsgs
+			out.append(addrmsgs['addrfile_header'] + "\n")
+			w = "Key-address" if have_wifs else "Address"
+			out.append("# {} data checksum for {}[{}]: {}".format(
+						w, self.seed_id, self.idxs_fmt, self.checksum))
+			out.append("# Record this value to a secure location\n")
+
+		out.append("%s {" % self.seed_id)
+
+		for e in self.addrdata:
+			if have_addrs:  # First line with idx
+				out.append(fs.format(e.idx, e.addr))
+			else:
+				out.append(fs.format(e.idx, "wif: "+e.wif))
+
+			if have_wifs:   # Subsequent lines
+				if have_secs:
+					out.append(fs.format("", "hex: "+e.sec))
+				if have_addrs:
+					out.append(fs.format("", "wif: "+e.wif))
+
+		out.append("}")
+
+		return "\n".join(out)
+
+	def fmt_addr_idxs(self):
+
+		try: int(self.addrdata[0].idx)
+		except:
+			self.idxs_fmt = "(no idxs)"
+			return
+
+		addr_idxs = [e.idx for e in self.addrdata]
+		prev = addr_idxs[0]
+		ret = prev,
+
+		for i in addr_idxs[1:]:
+			if i == prev + 1:
+				if i == addr_idxs[-1]: ret += "-", i
+			else:
+				if prev != ret[-1]: ret += "-", prev
+				ret += ",", i
+			prev = i
 
-	return "".join([str(i) for i in ret])
+		self.idxs_fmt = "".join([str(i) for i in ret])

+ 9 - 2
mmgen/config.py

@@ -59,7 +59,11 @@ mmenc_ext    = "mmenc"
 default_wl    = "electrum"
 #default_wl    = "tirosh"
 
-dfl_vars = "seed_len","hash_preset","usr_randchars"
+# Global value sets user opt
+dfl_vars = "seed_len","hash_preset"
+
+# User opt sets global value
+usr_set_vars = "no_keyconv","verbose","quiet","usr_randchars"
 
 seed_lens = 128,192,256
 seed_len  = 256
@@ -78,8 +82,11 @@ disable_hold_protect = os.getenv("MMGEN_DISABLE_HOLD_PROTECT")
 
 mins_per_block = 8.5
 passwd_max_tries = 5
-usr_randchars,usr_randchars_dfl = -1,30 # see get_random()
+
+usr_randchars = 30
 max_urandchars,min_urandchars = 80,10
+use_urandchars = False
+
 salt_len    = 16
 aesctr_iv_len  = 16
 

+ 12 - 10
mmgen/crypto.py

@@ -117,7 +117,7 @@ def encrypt_data(data, key, iv=1, what="data", verify=True):
 				counter=Counter.new(g.aesctr_iv_len*8,initial_value=iv))
 		dec_data = c.decrypt(enc_data)
 
-		if dec_data == data: vmsg("done\n")
+		if dec_data == data: vmsg("done")
 		else:
 			msg("ERROR.\nDecrypted %s doesn't match original %s" % (what,what))
 			sys.exit(2)
@@ -149,10 +149,12 @@ def scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32):
 	return scrypt.hash(passwd, salt, 2**N, r, p, buflen=buflen)
 
 
-def make_key(passwd, salt, hash_preset, what="encryption key", verbose=False):
+def make_key(passwd,salt,hash_preset,
+		what="encryption key",from_what="passphrase",verbose=False):
 
+	if from_what: what += " from "
 	if g.verbose or verbose:
-		msg_r("Generating %s from passphrase.\nPlease wait..." % what)
+		msg_r("Generating %s%s.\nPlease wait..." % (what,from_what))
 	key = scrypt_hash_passphrase(passwd, salt, hash_preset)
 	if g.verbose or verbose:
 		msg("done")
@@ -200,15 +202,15 @@ def get_random_data_from_user(uchars):
 def get_random(length,opts):
 	from Crypto import Random
 	os_rand = Random.new().read(length)
-	if 'usr_randchars' in opts and opts['usr_randchars'] not in (0,-1):
-		kwhat = "a key from OS random data plus "
+	if g.use_urandchars:
+		from_what = "OS random data"
 		if not g.user_entropy:
-			g.user_entropy = sha256(
-				get_random_data_from_user(opts['usr_randchars'])).digest()
-			kwhat += "user entropy"
+			g.user_entropy = \
+				sha256(get_random_data_from_user(g.usr_randchars)).digest()
+			from_what += " plus user-supplied entropy"
 		else:
-			kwhat += "saved user entropy"
-		key = make_key(g.user_entropy, "", '2', what=kwhat, verbose=True)
+			from_what += " plus saved user-supplied entropy"
+		key = make_key(g.user_entropy, "", '2', from_what=from_what, verbose=True)
 		return encrypt_data(os_rand,key,what="random data",verify=False)
 	else:
 		return os_rand

+ 8 - 22
mmgen/main_addrgen.py

@@ -29,7 +29,6 @@ from mmgen.license import *
 from mmgen.util import *
 from mmgen.crypto import *
 from mmgen.addr import *
-from mmgen.tx import make_addr_data_chksum
 
 what = "keys" if sys.argv[0].split("-")[-1] == "keygen" else "addresses"
 
@@ -148,40 +147,27 @@ if what == "keys" and not g.quiet:
 # Generate data:
 
 seed    = get_seed_retry(infile,opts)
-seed_id = make_chksum_8(seed)
 
 opts['gen_what'] = "a" if what == "addresses" else (
 	"k" if 'no_addresses' in opts else "ka")
 
-addr_data = generate_addrs(seed, addr_idxs, opts)
+ainfo = generate_addrs(seed, addr_idxs, opts)
 
-if 'a' in opts['gen_what']:
-	if 'k' in opts['gen_what']:
-		def l(a): return ( a.num, (a.addr,"",a.wif) )
-		keys = True
-	else:
-		def l(a): return ( a.num, (a.addr,) )
-		keys = False
-	addr_data_chksum = make_addr_data_chksum([l(a) for a in addr_data],keys)
-else:
-	addr_data_chksum = ""
-
-addr_data_str = format_addr_data(
-		addr_data, addr_data_chksum, seed_id, addr_idxs, opts)
+addrdata_str = ainfo.fmt_data()
+outfile_base = "{}[{}]".format(make_chksum_8(seed), ainfo.idxs_fmt)
 
-outfile_base = "{}[{}]".format(seed_id, fmt_addr_idxs(addr_idxs))
 if 'a' in opts['gen_what']:
 	w = "key-address" if 'k' in opts['gen_what'] else "address"
-	qmsg("Checksum for %s data %s: %s" % (w,outfile_base,addr_data_chksum))
+	qmsg("Checksum for %s data %s: %s" % (w,outfile_base,ainfo.checksum))
 	if 'save_checksum' in opts:
 		write_to_file(outfile_base+"."+g.addrfile_chksum_ext,
-			addr_data_chksum+"\n",opts,"%s data checksum" % w,True,True,False)
+			ainfo.checksum+"\n",opts,"%s data checksum" % w,True,True,False)
 	else:
 		qmsg("This checksum will be used to verify the %s file in the future."%w)
 		qmsg("Record it to a safe location.")
 
 if 'k' in opts['gen_what'] and keypress_confirm("Encrypt key list?"):
-	addr_data_str = mmgen_encrypt(addr_data_str,"new key list","",opts)
+	addrdata_str = mmgen_encrypt(addrdata_str,"new key list","",opts)
 	enc_ext = "." + g.mmenc_ext
 else: enc_ext = ""
 
@@ -190,11 +176,11 @@ if 'stdout' in opts or not sys.stdout.isatty():
 	if enc_ext and sys.stdout.isatty():
 		msg("Cannot write encrypted data to screen.  Exiting")
 		sys.exit(2)
-	write_to_stdout(addr_data_str,what,
+	write_to_stdout(addrdata_str,what,
 		(what=="keys"and not g.quiet and sys.stdout.isatty()))
 else:
 	outfile = "%s.%s%s" % (outfile_base, (
 		g.keyaddrfile_ext if "ka" in opts['gen_what'] else (
 		g.keyfile_ext if "k" in opts['gen_what'] else
 		g.addrfile_ext)), enc_ext)
-	write_to_file(outfile,addr_data_str,opts,what,not g.quiet,True)
+	write_to_file(outfile,addrdata_str,opts,what,not g.quiet,True)

+ 45 - 43
mmgen/main_addrimport.py

@@ -20,11 +20,12 @@
 mmgen-addrimport: Import addresses into a MMGen bitcoind tracking wallet
 """
 
-import sys
+import sys, time
 from mmgen.Opts   import *
 from mmgen.license import *
 from mmgen.util import *
-from mmgen.tx import connect_to_bitcoind,parse_addrfile,parse_keyaddr_file
+from mmgen.tx import connect_to_bitcoind
+from mmgen.addr import AddrInfo,AddrInfoEntry
 
 help_data = {
 	'prog_name': g.prog_name,
@@ -38,6 +39,7 @@ help_data = {
 -q, --quiet        Suppress warnings
 -r, --rescan       Rescan the blockchain.  Required if address to import is
                    on the blockchain and has a balance.  Rescanning is slow.
+-t, --test         Simulate operation; don't actually import addresses
 """,
 	'notes': """\n
 This command can also be used to update the comment fields of addresses already
@@ -51,35 +53,38 @@ if len(cmd_args) == 1:
 	infile = cmd_args[0]
 	check_infile(infile)
 	if 'addrlist' in opts:
-		lines = get_lines_from_file(infile,"non-{} addresses".format(g.proj_name),
-				trim_comments=True)
-		addr_list = [(None,l) for l in lines]
-		seed_id = ""
+		lines = get_lines_from_file(
+			infile,"non-{} addresses".format(g.proj_name),trim_comments=True)
+		ai,adata = AddrInfo(),[]
+		for btcaddr in lines:
+			a = AddrInfoEntry()
+			a.idx,a.addr,a.comment = None,btcaddr,None
+			adata.append(a)
+		ai.initialize(None,adata)
 	else:
-		addr_data = {}
-		pf = parse_keyaddr_file if 'keyaddr_file' in opts else parse_addrfile
-		pf(infile,addr_data)
-		seed_id = addr_data.keys()[0]
-		e = addr_data[seed_id]
-		def s_addrdata(a): return ("{:>0%s}"%g.mmgen_idx_max_digits).format(a)
-		addr_list = [(k,e[k][0],e[k][1]) for k in sorted(e.keys(),key=s_addrdata)]
+		ai = AddrInfo(infile,has_keys='keyaddr_file' in opts)
 else:
-	msg_r("You must specify an mmgen address list (or a list of ")
-	msg("non-%s addresses with\nthe '--addrlist' option)" % g.proj_name)
+	msg("""
+"You must specify an mmgen address file (or a list of non-%s addresses
+with the '--addrlist' option)
+""".strip() % g.proj_name)
 	sys.exit(1)
 
 from mmgen.bitcoin import verify_addr
 qmsg_r("Validating addresses...")
-for n,i in enumerate(addr_list,1):
-	if not verify_addr(i[1],verbose=True):
-		msg("%s: invalid address" % i)
+for e in ai.addrdata:
+	if not verify_addr(e.addr,verbose=True):
+		msg("%s: invalid address" % e.addr)
 		sys.exit(2)
-qmsg("OK. %s addresses%s" % (n," from seed ID "+seed_id if seed_id else ""))
+
+m = (" from seed ID %s" % ai.seed_id) if ai.seed_id else ""
+qmsg("OK. %s addresses%s" % (ai.num_addrs,m))
 
 import mmgen.config as g
 g.http_timeout = 3600
 
-c = connect_to_bitcoind()
+if not 'test' in opts:
+	c = connect_to_bitcoind()
 
 m = """
 WARNING: You've chosen the '--rescan' option.  Rescanning the blockchain is
@@ -101,31 +106,30 @@ err_flag = False
 
 def import_address(addr,label,rescan):
 	try:
-		c.importaddress(addr,label,rescan)
+		if not 'test' in opts:
+			c.importaddress(addr,label,rescan)
 	except:
 		global err_flag
 		err_flag = True
 
-
-w1 = len(str(len(addr_list))) * 2 + 2
-w2 = "" if 'addrlist' in opts else \
-		len(str(max([i[0] for i in addr_list if i[0]]))) + 12 \
+w_n_of_m = len(str(ai.num_addrs)) * 2 + 2
+w_mmid   = "" if 'addrlist' in opts else len(str(max(ai.idxs()))) + 12
 
 if "rescan" in opts:
 	import threading
-	import time
-	msg_fmt = "\r%s %-" + str(w1) + "s %-34s %-" + str(w2) + "s"
+	msg_fmt = "\r%s %-{}s %-34s %s".format(w_n_of_m)
 else:
-	msg_fmt = "\r%-" + str(w1) + "s %-34s %-" + str(w2) + "s"
+	msg_fmt = "\r%-{}s %-34s %s".format(w_n_of_m, w_mmid)
 
 msg("Importing addresses")
-for n,i in enumerate(addr_list):
-	if i[0]:
-		label = "%s:%s%s" % (seed_id,i[0], (" "+i[2] if i[2] else ""))
-	else: label = "non-mmgen"
+for n,e in enumerate(ai.addrdata):
+	if e.idx:
+		label = "%s:%s" % (ai.seed_id,e.idx)
+		if e.comment: label += " " + e.comment
+	else: label = "non-%s" % g.proj_name
 
 	if "rescan" in opts:
-		t = threading.Thread(target=import_address, args=(i[1],label,True))
+		t = threading.Thread(target=import_address, args=(e.addr,label,True))
 		t.daemon = True
 		t.start()
 
@@ -134,20 +138,18 @@ for n,i in enumerate(addr_list):
 		while True:
 			if t.is_alive():
 				elapsed = int(time.time() - start)
-				msg_r(msg_fmt % (
-						secs_to_hms(elapsed),
-						("%s/%s:" % (n+1,len(addr_list))),
-						i[1], "(" + label + ")"
-					)
-				)
+				count = "%s/%s:" % (n+1, ai.num_addrs)
+				msg_r(msg_fmt % (secs_to_hms(elapsed),count,e.addr,"(%s)"%label))
 				time.sleep(1)
 			else:
 				if err_flag: msg("\nImport failed"); sys.exit(2)
 				msg("\nOK")
 				break
 	else:
-		import_address(i[1],label,rescan=False)
-		msg_r(msg_fmt % (("%s/%s:" % (n+1,len(addr_list))),
-							i[1], "(" + label + ")"))
-		if err_flag: msg("\nImport failed"); sys.exit(2)
+		import_address(e.addr,label,rescan=False)
+		count = "%s/%s:" % (n+1, ai.num_addrs)
+		msg_r(msg_fmt % (count, e.addr, "(%s)"%label))
+		if err_flag:
+			msg("\nImport failed")
+			sys.exit(2)
 		msg(" - OK")

+ 44 - 28
mmgen/main_txcreate.py

@@ -115,7 +115,7 @@ def format_unspent_outputs_for_printing(out,sort_info,total):
 			if i.skip == "txid" and "grouped" in sort_info else str(i.txid)
 
 		s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr,
-				i.mmid,i.amt,i.confirmations,i.days,i.label)
+				i.mmid,i.amt,i.confirmations,i.days,i.comment)
 		pout.append(s.rstrip())
 
 	return \
@@ -131,15 +131,16 @@ def sort_and_view(unspent,opts):
 	def s_addr(i):  return i.address
 	def s_age(i):   return i.confirmations
 	def s_mmgen(i):
-		m = parse_mmgen_label(i.account)[0]
-		if m: return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *m.split(":"))
-		else: return "G" + i.account
+		if i.mmid:
+			return "{}:{:>0{w}}".format(
+				*i.mmid.split(":"), w=g.mmgen_idx_max_digits)
+		else: return "G" + i.comment
 
 	sort,group,show_days,show_mmaddr,reverse = "age",False,False,True,True
 	unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default
 
 	total = trim_exponent(sum([i.amount for i in unspent]))
-	max_acct_len = max([len(i.account) for i in unspent])
+	max_acct_len = max([len(i.mmid+" "+i.comment) for i in unspent])
 
 	hdr_fmt   = "UNSPENT OUTPUTS (sort order: %s)  Total BTC: %s"
 	options_msg = """
@@ -149,6 +150,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 	prompt = \
 "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
 
+	mmid_w = max(len(i.mmid) for i in unspent)
 	from copy import deepcopy
 	from mmgen.term import get_terminal_size
 
@@ -184,7 +186,6 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 			i.amt = " "*lfill + amt
 			i.days = int(i.confirmations * g.mins_per_block / (60*24))
 			i.age = i.days if show_days else i.confirmations
-			i.mmid,i.label = parse_mmgen_label(i.account)
 
 			if i.skip == "addr":
 				i.addr = "|" + "." * 33
@@ -193,8 +194,10 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 					dots = ".." if btaddr_w < len(i.address) else ""
 					i.addr = "%s%s %s" % (
 						i.address[:btaddr_w-len(dots)],
-						dots,
-						i.account[:acct_w])
+						dots, (
+                        ("{:<{w}} ".format(i.mmid,w=mmid_w) if i.mmid else "")
+                            + i.comment)[:acct_w]
+                        )
 				else:
 					i.addr = i.address
 
@@ -295,7 +298,7 @@ def get_acct_data_from_wallet(c,acct_data):
 
 def mmaddr2btcaddr_unspent(unspent,mmaddr):
 	vmsg_r("Searching for {g.proj_name} address {m} in wallet...".format(g=g,m=mmaddr))
-	m = [u for u in unspent if u.account.split()[0] == mmaddr]
+	m = [u for u in unspent if u.mmid == mmaddr]
 	if len(m) == 0:
 		vmsg("not found")
 		return "",""
@@ -303,18 +306,19 @@ def mmaddr2btcaddr_unspent(unspent,mmaddr):
 		msg(wmsg['too_many_acct_addresses'] % acct); sys.exit(2)
 	else:
 		vmsg("success (%s)" % m[0].address)
-		return m[0].address, split2(m[0].account)[1]
+		return m[0].address, m[0].comment
 	sys.exit()
 
 
-def mmaddr2btcaddr(c,mmaddr,acct_data,addr_data,b2m_map):
+def mmaddr2btcaddr(c,mmaddr,acct_data,ail):
 	# assume mmaddr has already been checked
 	if not acct_data: get_acct_data_from_wallet(c,acct_data)
-	btcaddr,comment = mmaddr2btcaddr_addrdata(mmaddr,acct_data,"wallet")
+	btcaddr = mmaddr2btcaddr_addrdata(mmaddr,acct_data,"wallet")[0]
 #	btcaddr,comment = mmaddr2btcaddr_unspent(us,mmaddr)
 	if not btcaddr:
-		if addr_data:
-			btcaddr,comment = mmaddr2btcaddr_addrdata(mmaddr,addr_data,"addr file")
+		if ail:
+			sid,idx = mmaddr.split(":")
+			btcaddr = ail.addrinfo(sid).btcaddr(int(idx))
 			if btcaddr:
 				msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr))
 				if not keypress_confirm("Continue anyway?"):
@@ -326,7 +330,6 @@ def mmaddr2btcaddr(c,mmaddr,acct_data,addr_data,b2m_map):
 			msg(wmsg['addr_not_found_no_addrfile'].format(mmgenaddr=mmaddr))
 			sys.exit(2)
 
-	b2m_map[btcaddr] = mmaddr,comment
 	return btcaddr
 
 
@@ -342,14 +345,16 @@ c = connect_to_bitcoind()
 if not 'info' in opts:
 	do_license_msg(immed=True)
 
-	tx_out,addr_data,b2m_map,acct_data,change_addr = {},{},{},{},""
+	tx_out,acct_data,change_addr = {},{},""
+	from mmgen.addr import AddrInfo,AddrInfoList
+	ail = AddrInfoList()
 
 	addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext]
 	cmd_args = set(cmd_args) - set(addrfiles)
 
 	for a in addrfiles:
 		check_infile(a)
-		parse_addrfile(a,addr_data)
+		ail.add(AddrInfo(a))
 
 	for a in cmd_args:
 		if "," in a:
@@ -357,13 +362,14 @@ if not 'info' in opts:
 			if is_btc_addr(a1):
 				btcaddr = a1
 			elif is_mmgen_addr(a1):
-				btcaddr = mmaddr2btcaddr(c,a1,acct_data,addr_data,b2m_map)
+				btcaddr = mmaddr2btcaddr(c,a1,acct_data,ail)
 			else:
 				msg("%s: unrecognized subargument in argument '%s'" % (a1,a))
 				sys.exit(2)
 
-			if is_btc_amt(a2):
-				tx_out[btcaddr] = normalize_btc_amt(a2)
+			ret = normalize_btc_amt(a2)
+			if ret:
+				tx_out[btcaddr] = ret
 			else:
 				msg("%s: invalid amount in argument '%s'" % (a2,a))
 				sys.exit(2)
@@ -373,7 +379,7 @@ if not 'info' in opts:
 						(change_addr, a))
 				sys.exit(2)
 			change_addr = a if is_btc_addr(a) else \
-				mmaddr2btcaddr(c,a,acct_data,addr_data,b2m_map)
+							mmaddr2btcaddr(c,a,acct_data,ail)
 			tx_out[change_addr] = 0
 		else:
 			msg("%s: unrecognized argument" % a)
@@ -398,7 +404,9 @@ else:
 #	write_to_file("bogus_unspent.json", repr(us), opts); sys.exit()
 
 if not us: msg(wmsg['no_spendable_outputs']); sys.exit(2)
-
+for o in us:
+	o.mmid,o.comment = parse_mmgen_label(o.account)
+	del o.account
 unspent = sort_and_view(us,opts)
 
 total = trim_exponent(sum([i.amount for i in unspent]))
@@ -418,7 +426,7 @@ while True:
 	)
 	sel_unspent = [unspent[i-1] for i in sel_nums]
 
-	mmaddrs = set([parse_mmgen_label(i.account)[0] for i in sel_unspent])
+	mmaddrs = set([i.mmid for i in sel_unspent])
 	mmaddrs.discard("")
 
 	if mmaddrs and len(mmaddrs) < len(sel_unspent):
@@ -468,15 +476,23 @@ qmsg("Transaction successfully created")
 amt = send_amt or change
 tx_id = make_chksum_6(unhexlify(tx_hex)).upper()
 metadata = tx_id, amt, make_timestamp()
+sel_unspent = [i.__dict__ for i in sel_unspent]
+
+def make_b2m_map(inputs_data,tx_out):
+	m = [(d['address'],(d['mmid'],d['comment'])) for d in inputs_data if d['mmid']]
+	d = ail.make_reverse_dict(tx_out.keys())
+	d.update(m)
+	return d
+
+b2m_map = make_b2m_map(sel_unspent,tx_out)
 
 prompt_and_view_tx_data(c,"View decoded transaction?",
-	[i.__dict__ for i in sel_unspent],tx_hex,b2m_map,comment,metadata)
+		sel_unspent,tx_hex,b2m_map,comment,metadata)
 
-prompt = "Save transaction?"
-if keypress_confirm(prompt,default_yes=True):
+if keypress_confirm("Save transaction?",default_yes=False):
 	outfile = "tx_%s[%s].%s" % (tx_id,amt,g.rawtx_ext)
-	data = make_tx_data("{} {} {}".format(*metadata), tx_hex,
-			[i.__dict__ for i in sel_unspent], b2m_map, comment)
+	data = make_tx_data("{} {} {}".format(*metadata),
+				tx_hex,sel_unspent,b2m_map,comment)
 	write_to_file(outfile,data,opts,"transaction",False,True)
 else:
 	msg("Transaction not saved")

+ 11 - 16
mmgen/main_txsign.py

@@ -139,9 +139,8 @@ def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds,opts):
 		# Returns only if seed is found
 		seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts)
 		addr_nums = [int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id]
-#		num sec wif addr
-		d += [("{}:{}".format(seed_id,r.num),r.addr,r.wif)
-			for r in generate_addrs(seed,addr_nums,{'gen_what':"ka"},seed_id)]
+		ai = generate_addrs(seed,addr_nums,{'gen_what':"ka"})
+		d += [("{}:{}".format(seed_id,e.idx),e.addr,e.wif) for e in ai.addrdata]
 	return d
 
 
@@ -216,15 +215,13 @@ for the following non-{} address{}:\n    {}""".format(
 
 
 def parse_mmgen_keyaddr_file(opts):
-	adata = {}
-	parse_keyaddr_file(opts['mmgen_keys_from_file'],adata)
-	for sid in adata.keys(): # one seed id, one loop
-		idxs = adata[sid]
-		count = len(idxs.keys())
-		vmsg("Found %s wif key%s for seed ID %s" % (count,suf(count,"k"),sid))
-		# idx: (0=addr, 1=comment 2=wif) -> mmaddr: (0=addr, 1=wif)
-		return dict([("{}:{}".format(sid,k),(idxs[k][0],idxs[k][2]))
-				for k in idxs.keys()])
+	from mmgen.addr import AddrInfo
+	ai = AddrInfo(opts['mmgen_keys_from_file'],has_keys=True)
+	vmsg("Found %s wif key%s for seed ID %s" %
+			(ai.num_addrs, suf(ai.num_addrs,"k"), ai.seed_id))
+	# idx: (0=addr, 1=comment 2=wif) -> mmaddr: (0=addr, 1=wif)
+	return dict(
+		[("%s:%s"%(ai.seed_id,e.idx), (e.addr,e.wif)) for e in ai.addrdata])
 
 
 def parse_keylist(opts,from_file):
@@ -335,8 +332,7 @@ for tx_num,tx_file in enumerate(tx_files,1):
 		inputs_data,tx_hex,b2m_map,comment,metadata)
 
 	# Start
-	other_addrs = list(set([i['address'] for i in inputs_data
-			if not parse_mmgen_label(i['account'])[0]]))
+	other_addrs = list(set([i['address'] for i in inputs_data if not i['mmid']]))
 
 	keys = get_keys_from_keylist(from_file['kldata'],other_addrs)
 
@@ -344,8 +340,7 @@ for tx_num,tx_file in enumerate(tx_files,1):
 		missing_keys_errormsg(other_addrs)
 		sys.exit(2)
 
-	imap = dict([(i['account'].split()[0],i['address']) for i in inputs_data
-					if parse_mmgen_label(i['account'])[0]])
+	imap = dict([(i['mmid'],i['address']) for i in inputs_data if i['mmid']])
 	omap = dict([(j[0],i) for i,j in b2m_map.items()])
 	sids = set([i[:8] for i in imap.keys()])
 

+ 4 - 4
mmgen/main_walletchk.py

@@ -87,8 +87,7 @@ def wallet_to_incog_data(infile,opts):
 
 	# IV is used BOTH to initialize counter and to salt password!
 	key = make_key(passwd, iv, preset, "incog wrapper key")
-	m = "incog data"
-	wrap_enc = encrypt_data(salt + enc_seed, key, int(hexlify(iv),16), m)
+	wrap_enc = encrypt_data(salt+enc_seed,key,int(hexlify(iv),16),"incog data")
 
 	return iv+wrap_enc,seed_id,key_id,iv_id,preset
 
@@ -118,15 +117,16 @@ if len(cmd_args) != 1: usage(help_data)
 
 check_infile(cmd_args[0])
 
-if set(['outdir','export_incog_hidden']).issubset(set(opts.keys())):
+if set(['outdir','export_incog_hidden']) <= set(opts.keys()):
 	msg("Warning: '--outdir' option is ignored when exporting hidden incog data")
 
+g.use_urandchars = True
+
 if 'export_mnemonic' in opts:
 	qmsg("Exporting mnemonic data to file by user request")
 elif 'export_seed' in opts:
 	qmsg("Exporting seed data to file by user request")
 elif 'export_incog' in opts:
-	if opts['usr_randchars'] == -1: opts['usr_randchars'] = g.usr_randchars_dfl
 	qmsg("Exporting wallet to incognito format by user request")
 	incog_enc,seed_id,key_id,iv_id,preset = \
 		wallet_to_incog_data(cmd_args[0],opts)

+ 2 - 1
mmgen/main_walletgen.py

@@ -124,7 +124,6 @@ future, you must continue using these same parameters
 opts,cmd_args = parse_opts(sys.argv,help_data)
 
 if 'show_hash_presets' in opts: show_hash_presets()
-if opts['usr_randchars'] == -1: opts['usr_randchars'] = g.usr_randchars_dfl
 
 if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 
@@ -143,6 +142,8 @@ elif len(cmd_args) == 0:
 	infile = ""
 else: usage(help_data)
 
+g.use_urandchars = True
+
 # Begin execution
 
 do_license_msg()

+ 7 - 2
mmgen/tool.py

@@ -431,8 +431,13 @@ def txview(infile,pager=False,terse=False):
 	metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,infile)
 	view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager,pause=False,terse=terse)
 
-def addrfile_chksum(infile): parse_addrfile(infile,{})
-def keyaddrfile_chksum(infile): parse_keyaddr_file(infile,{})
+def addrfile_chksum(infile):
+	from mmgen.addr import AddrInfo
+	AddrInfo(infile)
+
+def keyaddrfile_chksum(infile):
+	from mmgen.addr import AddrInfo
+	AddrInfo(infile,has_keys=True)
 
 def hexreverse(hex_str):
 	print ba.hexlify(decode_pretty_hexdump(hex_str)[::-1])

+ 30 - 171
mmgen/tx.py

@@ -23,6 +23,7 @@ tx.py:  Bitcoin transaction routines
 import sys, os
 from binascii import unhexlify
 from decimal import Decimal
+from collections import OrderedDict
 
 import mmgen.config as g
 from mmgen.util import *
@@ -34,7 +35,7 @@ def trim_exponent(n):
 	d = Decimal(n)
 	return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
 
-def is_btc_amt(amt):
+def normalize_btc_amt(amt):
 	# amt must be a string!
 
 	from decimal import Decimal
@@ -57,12 +58,6 @@ def is_btc_amt(amt):
 
 	return trim_exponent(ret)
 
-def normalize_btc_amt(amt):
-	# amt must be a string!
-	ret = is_btc_amt(amt)
-	if ret: return ret
-	else:   sys.exit(3)
-
 def parse_mmgen_label(s,check_label_len=False):
 	l = split2(s)
 	if not is_mmgen_addr(l[0]): return "",s
@@ -74,9 +69,9 @@ def is_mmgen_seed_id(s):
 	return re.match(r"^[0123456789ABCDEF]{8}$",s) is not None
 
 def is_mmgen_idx(s):
-	import re
-	m = g.mmgen_idx_max_digits
-	return re.match(r"^[0123456789]{1,"+str(m)+r"}$",s) is not None
+	try: int(s)
+	except: return False
+	return len(s) <= g.mmgen_idx_max_digits
 
 def is_mmgen_addr(s):
 	seed_id,idx = split2(s,":")
@@ -88,11 +83,9 @@ def is_btc_addr(s):
 
 def is_b58_str(s):
 	from mmgen.bitcoin import b58a
-	for ch in s:
-		if ch not in b58a: return False
-	return True
+	return set(list(s)) <= set(b58a)
 
-def is_btc_key(s):
+def is_wip_key(s):
 	if s == "": return False
 	compressed = not s[0] == '5'
 	from mmgen.bitcoin import wiftohex
@@ -132,14 +125,14 @@ Only ASCII printable characters are permitted.
 """.strip() % (ch,label))
 			sys.exit(3)
 
-def prompt_and_view_tx_data(c,prompt,inputs_data,tx_hex,b2m_map,comment,metadata):
+def prompt_and_view_tx_data(c,prompt,inputs_data,tx_hex,adata,comment,metadata):
 
 	prompt += " (y)es, (N)o, pager (v)iew, (t)erse view"
 
 	reply = prompt_and_get_char(prompt,"YyNnVvTt",enter_ok=True)
 
 	if reply and reply in "YyVvTt":
-		view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,
+		view_tx_data(c,inputs_data,tx_hex,adata,comment,metadata,
 				pager=reply in "Vv",terse=reply in "Tt")
 
 
@@ -155,26 +148,24 @@ def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False,pause
 	if comment: out += "Comment: %s\n%s" % (comment,enl)
 	out += "Inputs:\n" + enl
 
+	nonmm_str = "non-%s address" % g.proj_name
+
 	total_in = 0
 	for n,i in enumerate(td['vin']):
 		for j in inputs_data:
 			if j['txid'] == i['txid'] and j['vout'] == i['vout']:
 				days = int(j['confirmations'] * g.mins_per_block / (60*24))
 				total_in += j['amount']
-				mmid,label,mmid_str = "","",""
-				if 'account' in j:
-					mmid,label = parse_mmgen_label(j['account'])
-					if not mmid: mmid = "non-%s address" % g.proj_name
-					mmid_str = " ({:>{l}})".format(mmid,l=34-len(j['address']))
-
+				if not j['mmid']: j['mmid'] = nonmm_str
+				mmid_fmt = " ({:>{l}})".format(j['mmid'],l=34-len(j['address']))
 				if terse:
-					out += "  %s: %-54s %s BTC" % (n+1,j['address'] + mmid_str,
+					out += "  %s: %-54s %s BTC" % (n+1,j['address'] + mmid_fmt,
 							trim_exponent(j['amount']))
 				else:
 					for d in (
 	(n+1, "tx,vout:",       "%s,%s" % (i['txid'], i['vout'])),
-	("",  "address:",       j['address'] + mmid_str),
-	("",  "label:",         label),
+	("",  "address:",       j['address'] + mmid_fmt),
+	("",  "comment:",       j['comment']),
 	("",  "amount:",        "%s BTC" % trim_exponent(j['amount'])),
 	("",  "confirmations:", "%s (around %s days)" % (j['confirmations'], days))
 					):
@@ -185,18 +176,17 @@ def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False,pause
 	total_out = 0
 	out += "Outputs:\n" + enl
 	for n,i in enumerate(td['vout']):
-		addr = i['scriptPubKey']['addresses'][0]
-		mmid,label = b2m_map[addr] if addr in b2m_map else ("","")
-		if not mmid: mmid = "non-%s address" % g.proj_name
-		mmid_str = " ({:>{l}})".format(mmid,l=34-len(j['address']))
+		btcaddr = i['scriptPubKey']['addresses'][0]
+		mmid,comment=b2m_map[btcaddr] if btcaddr in b2m_map else (nonmm_str,"")
+		mmid_fmt = " ({:>{l}})".format(mmid,l=34-len(j['address']))
 		total_out += i['value']
 		if terse:
-			out += "  %s: %-54s %s BTC" % (n+1,addr + mmid_str,
+			out += "  %s: %-54s %s BTC" % (n+1,btcaddr + mmid_fmt,
 					trim_exponent(i['value']))
 		else:
 			for d in (
-					(n+1, "address:",  addr + mmid_str),
-					("",  "label:",    label),
+					(n+1, "address:",  btcaddr + mmid_fmt),
+					("",  "comment:",  comment),
 					("",  "amount:",   trim_exponent(i['value']))
 				):
 				if d[2]: out += ("%3s %-8s %s\n" % d)
@@ -213,7 +203,7 @@ def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False,pause
 	o = out.encode("utf8")
 	if pager: do_pager(o)
 	else:
-		print o
+		sys.stdout.write(o)
 		if pause:
 			get_char("Press any key to continue: ")
 			msg("")
@@ -262,142 +252,19 @@ def parse_tx_file(tx_data,infile):
 		return metadata.split(),tx_hex,inputs_data,outputs_data,comment
 
 
+def wiftoaddr_keyconv(wif):
+	if wif[0] == '5':
+		from subprocess import check_output
+		return check_output(["keyconv", wif]).split()[1]
+	else:
+		return wiftoaddr(wif)
+
 def get_wif2addr_f():
 	if g.no_keyconv: return wiftoaddr
 	from mmgen.addr import test_for_keyconv
 	return wiftoaddr_keyconv if test_for_keyconv() else wiftoaddr
 
 
-def make_addr_data_chksum(adata,keys=False):
-	nchars = 24
-	return make_chksum_N(" ".join([" ".join(
-					[str(n),d[0],d[2]] if keys else [str(n),d[0]]
-				) for n,d in adata]), nchars, sep=True)
-
-
-def get_addr_data_hash(e,keys=False):
-	def s_addrdata(a): return int(a[0])
-	adata = [(k,e[k]) for k in e.keys()]
-	return make_addr_data_chksum(sorted(adata,key=s_addrdata),keys)
-
-
-def _parse_addrfile_body(lines,keys=False,check=False):
-
-	def parse_addr_lines(lines):
-		ret = []
-		for l in lines:
-			d = l.split(None,2)
-
-			if not is_mmgen_idx(d[0]):
-				msg("'%s': invalid address num. in line: '%s'" % (d[0],l))
-				sys.exit(3)
-
-			if not is_btc_addr(d[1]):
-				msg("'%s': invalid Bitcoin address" % d[1])
-				sys.exit(3)
-
-			if len(d) == 3:
-				comment = d[2]
-				check_addr_label(comment)
-			else:
-				comment = ""
-
-			ret.append([d[0],d[1],comment])
-
-		return ret
-
-	def parse_key_lines(lines):
-		ret = []
-		for l in lines:
-			d = l.split(None,2)
-
-			if d[0] != "wif:":
-				msg("Invalid key line in file: '%s'" % l)
-				sys.exit(3)
-
-			if not is_btc_key(d[1]):
-				msg("'%s': invalid Bitcoin key" % d[1])
-				sys.exit(3)
-
-			ret.append(d[1])
-
-		return ret
-
-	z = len(lines) / 2
-	if keys:
-        # returns list of lists
-		adata = parse_addr_lines([lines[i*2] for i in range(z)])
-        # returns list of strings
-		kdata = parse_key_lines([lines[i*2+1] for i in range(z)])
-		if len(adata) != len(kdata):
-			msg("Odd number of lines in key file")
-			sys.exit(2)
-		if check or keypress_confirm("Check key-to-address validity?"):
-			wif2addr_f = get_wif2addr_f()
-			for i in range(z):
-				msg_r("\rVerifying keys %s/%s" % (i+1,z))
-				if adata[i][1] != wif2addr_f(kdata[i]):
-					msg("Key doesn't match address!\n  %s\n  %s" %
-							kdata[i],adata[i][1])
-					sys.exit(2)
-			msg(" - done")
-		return [adata[i] + [kdata[i]] for i in range(z)]
-	else:
-		return parse_addr_lines(lines)
-
-
-def parse_addrfile(f,addr_data,keys=False,return_chk_and_sid=False):
-	return parse_addrfile_lines(
-				get_lines_from_file(f,"address data",trim_comments=True),
-					addr_data,keys,return_chk_and_sid=return_chk_and_sid)
-
-def parse_addrfile_lines(lines,addr_data,keys=False,exit_on_error=True,return_chk_and_sid=False):
-
-	try:
-		seed_id,obrace = lines[0].split()
-	except:
-		errmsg = "Invalid first line: '%s'" % lines[0]
-	else:
-		cbrace = lines[-1]
-		if obrace != '{':
-			errmsg = "'%s': invalid first line" % lines[0]
-		elif cbrace != '}':
-			errmsg = "'%s': invalid last line" % cbrace
-		elif not is_mmgen_seed_id(seed_id):
-			errmsg = "'%s': invalid Seed ID" % seed_id
-		else:
-			ldata = _parse_addrfile_body(lines[1:-1],keys)
-			if seed_id not in addr_data: addr_data[seed_id] = {}
-			for l in ldata:
-				addr_data[seed_id][l[0]] = l[1:]
-			chk = get_addr_data_hash(addr_data[seed_id],keys)
-			if return_chk_and_sid: return chk,seed_id
-			from mmgen.addr import fmt_addr_idxs
-			fl = fmt_addr_idxs([int(i) for i in addr_data[seed_id].keys()])
-			w = "key" if keys else "addr"
-			qmsg_r("Computed checksum for "+w+" data ",w.capitalize()+" checksum ")
-			msg("{}[{}]: {}".format(seed_id,fl,chk))
-			qmsg("Check this value against your records")
-			return True
-
-	if exit_on_error:
-		msg(errmsg)
-		sys.exit(3)
-	else:
-		return False
-
-
-def parse_keyaddr_file(infile,addr_data):
-	d = get_data_from_file(infile,"%s key-address file data" % g.proj_name)
-	enc_ext = get_extension(infile) == g.mmenc_ext
-	if enc_ext or not is_utf8(d):
-		m = "Decrypting" if enc_ext else "Attempting to decrypt"
-		msg("%s key-address file %s" % (m,infile))
-		from crypto import mmgen_decrypt_retry
-		d = mmgen_decrypt_retry(d,"key-address file")
-	parse_addrfile_lines(remove_comments(d.split("\n")),addr_data,True,False)
-
-
 def get_tx_comment_from_file(infile):
 	s = get_data_from_file(infile,"transaction comment")
 	if is_valid_tx_comment(s, verbose=True):
@@ -469,11 +336,3 @@ def connect_to_bitcoind():
 		sys.exit(2)
 
 	return c
-
-
-def wiftoaddr_keyconv(wif):
-	if wif[0] == '5':
-		from subprocess import check_output
-		return check_output(["keyconv", wif]).split()[1]
-	else:
-		return wiftoaddr(wif)

+ 11 - 4
mmgen/util.py

@@ -41,6 +41,14 @@ def vmsg(s):
 def vmsg_r(s):
 	if g.verbose: sys.stderr.write(s)
 
+def msgrepr(*args):
+	for d in args:
+		sys.stdout.write(repr(d)+"\n")
+def msgrepr_exit(*args):
+	for d in args:
+		sys.stdout.write(repr(d)+"\n")
+	sys.exit()
+
 def suf(arg,what):
 	t = type(arg)
 	if t == int:
@@ -65,6 +73,7 @@ def make_chksum_N(s,n,sep=False):
 	s = sha256(sha256(s).digest()).hexdigest().upper()
 	sep = " " if sep else ""
 	return sep.join([s[i*4:i*4+4] for i in range(n/4)])
+
 def make_chksum_8(s,sep=False):
 	s = sha256(sha256(s).digest()).hexdigest()[:8].upper()
 	return "{} {}".format(s[:4],s[4:]) if sep else s
@@ -206,11 +215,11 @@ def _validate_addr_num(n):
 
 	try: n = int(n)
 	except:
-		msg("'%s': address must be an integer" % n)
+		msg("'%s': addr index must be an integer" % n)
 		return False
 
 	if n < 1:
-		msg("'%s': address must be greater than zero" % n)
+		msg("'%s': addr index must be greater than zero" % n)
 		return False
 
 	return n
@@ -280,8 +289,6 @@ def confirm_or_exit(message, question, expect="YES"):
 
 def confirm_or_false(message, question, expect="YES"):
 
-	vmsg("")
-
 	m = message.strip()
 	if m: msg(m)
 

+ 123 - 78
test/test.py

@@ -8,12 +8,18 @@ pn = os.path.dirname(sys.argv[0])
 os.chdir(os.path.join(pn,os.pardir))
 sys.path.__setitem__(0,os.path.abspath(os.curdir))
 
+from mmgen.util import msgrepr, msgrepr_exit
+
 hincog_fn = "rand_data"
 non_mmgen_fn = "btckey"
 
 from collections import OrderedDict
 cmd_data = OrderedDict([
 #     test               description                  depends
+	['refwalletgen',    (6,'reference wallet seed ID',    [[[],6]])],
+	['refaddrgen',      (6,'reference wallet address checksum', [[["mmdat"],6]])],
+	['refkeyaddrgen',   (6,'reference wallet key-address checksum', [[["mmdat"],6]])],
+
 	['walletgen',       (1,'wallet generation',        [[[],1]])],
 	['walletchk',       (1,'wallet check',             [[["mmdat"],1]])],
 	['passchg',         (5,'password, label and hash preset change',[[["mmdat"],1]])],
@@ -39,18 +45,18 @@ cmd_data = OrderedDict([
 	['keyaddrgen',    (1,'key-address file generation', [[["mmdat"],1]])],
 	['txsign_keyaddr',(1,'transaction signing with key-address file', [[["akeys.mmenc","raw"],1]])],
 
-	['walletgen2',(2,'wallet generation (2)',     [])],
+	['walletgen2',(2,'wallet generation (2), 128-bit seed (WIP)',     [])],
 	['addrgen2',  (2,'address generation (2)',    [[["mmdat"],2]])],
 	['txcreate2', (2,'transaction creation (2)',  [[["addrs"],2]])],
 	['txsign2',   (2,'transaction signing, two transactions',[[["mmdat","raw"],1],[["mmdat","raw"],2]])],
-	['export_mnemonic2', (2,'seed export to mmwords format (2)',[[["mmdat"],2]])],
+	['export_mnemonic2', (2,'seed export to mmwords format (2), 128-bit seed (WIP)',[[["mmdat"],2]])],
 
 	['walletgen3',(3,'wallet generation (3)',         [])],
 	['addrgen3',  (3,'address generation (3)',        [[["mmdat"],3]])],
 	['txcreate3', (3,'tx creation with inputs and outputs from two wallets', [[["addrs"],1],[["addrs"],3]])],
 	['txsign3',   (3,'tx signing with inputs and outputs from two wallets',[[["mmdat"],1],[["mmdat","raw"],3]])],
 
-	['walletgen4',(4,'wallet generation (4) (brainwallet)', [])],
+	['walletgen4',(4,'wallet generation (4) (brainwallet, 192-bit seed (WIP))', [])],
 	['addrgen4',  (4,'address generation (4)',              [[["mmdat"],4]])],
 	['txcreate4', (4,'tx creation with inputs and outputs from four seed sources, plus non-MMGen inputs and outputs', [[["addrs"],1],[["addrs"],2],[["addrs"],3],[["addrs"],4]])],
 	['txsign4',   (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet and brainwallet, plus non-MMGen inputs and outputs', [[["mmincog"],1],[["mmwords"],2],[["mmdat"],3],[["mmbrain","raw"],4]])],
@@ -63,6 +69,25 @@ utils = {
 
 addrs_per_wallet = 8
 cfgs = {
+	'6': {
+		'name':            "reference wallet check",
+		'bw_passwd':       "abc",
+		'bw_hashparams':   "256,1",
+		'key_id':          "98831F3A",
+		'addrfile_chk':    "6FEF 6FB9 7B13 5D91 854A 0BD3",
+		'keyaddrfile_chk': "9F2D D781 1812 8BAD C396 9DEB",
+
+		'wpasswd':       "reference password",
+		'tmpdir':        "test/tmp6",
+		'kapasswd':      "",
+		'addr_idx_list': "1010,500-501,31-33,1,33,500,1011", # 8 addresses
+		'dep_generators':  {
+			'mmdat':       "refwalletgen",
+			'addrs':       "refaddrgen",
+			'akeys.mmenc': "refkeyaddrgen"
+		},
+
+	},
 	'1': {
 		'tmpdir':        "test/tmp1",
 		'wpasswd':       "Dorian",
@@ -85,6 +110,7 @@ cfgs = {
 		'tmpdir':        "test/tmp2",
 		'wpasswd':       "Hodling away",
 		'addr_idx_list': "37,45,3-6,22-23",  # 8 addresses
+        'seed_len':      128,
 		'dep_generators': {
 			'mmdat':       "walletgen2",
 			'addrs':       "addrgen2",
@@ -108,6 +134,7 @@ cfgs = {
 		'tmpdir':        "test/tmp4",
 		'wpasswd':       "Hashrate rising",
 		'addr_idx_list': "63,1004,542-544,7-9", # 8 addresses
+        'seed_len':      192,
 		'dep_generators': {
 			'mmdat':       "walletgen4",
 			'mmbrain':     "walletgen4",
@@ -116,7 +143,7 @@ cfgs = {
 			'sig':         "txsign4",
 		},
 		'bw_filename': "brainwallet.mmbrain",
-		'bw_params':   "256,1",
+		'bw_params':   "192,1",
 	},
 	'5': {
 		'tmpdir':        "test/tmp5",
@@ -130,15 +157,6 @@ cfgs = {
 from binascii import hexlify
 def getrand(n): return int(hexlify(os.urandom(n)),16)
 
-def msgrepr(*args):
-	for d in args:
-		sys.stdout.write(repr(d)+"\n")
-
-def msgrepr_exit(*args):
-	for d in args:
-		sys.stdout.write(repr(d)+"\n")
-	sys.exit()
-
 # total of two outputs must be < 10 BTC
 for k in cfgs.keys():
 	cfgs[k]['amts'] = [0,0]
@@ -146,6 +164,7 @@ for k in cfgs.keys():
 		cfgs[k]['amts'][idx] = "%s.%s" % ((getrand(2) % mod), str(getrand(4))[:5])
 
 meta_cmds = OrderedDict([
+	['ref',    (6,("refwalletgen","refaddrgen","refkeyaddrgen"))],
 	['gen',    (1,("walletgen","walletchk","addrgen"))],
 	['pass',   (5,("passchg","walletchk_newpass"))],
 	['tx',     (1,("txcreate","txsign","txsend"))],
@@ -217,6 +236,7 @@ def end_silence():
 		sys.stderr = stderr_save
 
 def errmsg(s): stderr_save.write(s+"\n")
+def errmsg_r(s): stderr_save.write(s)
 
 def Msg(s): sys.stdout.write(s+"\n")
 
@@ -237,7 +257,7 @@ if "list_cmds" in opts:
 
 import pexpect,time,re
 import mmgen.config as g
-from mmgen.util import get_data_from_file, write_to_file, get_lines_from_file
+from mmgen.util import get_data_from_file,write_to_file,get_lines_from_file
 
 redc,grnc,yelc,cyac,reset = (
 	["\033[%sm" % c for c in "31;1","32;1","33;1","36;1","0"]
@@ -310,8 +330,8 @@ def get_file_with_ext(ext,mydir,delete=True):
 def get_addrfile_checksum(display=False):
 	addrfile = get_file_with_ext("addrs",cfg['tmpdir'])
 	silence()
-	from mmgen.tx import parse_addrfile
-	chk = parse_addrfile(addrfile,{},return_chk_and_sid=True)[0]
+	from mmgen.addr import AddrInfo
+	chk = AddrInfo(addrfile).checksum
 	if verbose and display: msg("Checksum: %s" % cyan(chk))
 	end_silence()
 	return chk
@@ -370,11 +390,6 @@ class MMGenExpect(object):
 		my_expect(self.p,("Enter hash preset for %s, or ENTER .*?:" % what),
 				str(preset)+"\n",regex=True)
 
-	def ok(self):
-		if verbose or exact_output:
-			sys.stderr.write(green("OK\n"))
-		else: msg(" OK")
-
 	def written_to_file(self,what,overwrite_unlikely=False,query="Overwrite?  "):
 		s1 = "%s written to file " % what
 		s2 = query + "Type uppercase 'YES' to confirm: "
@@ -425,7 +440,7 @@ from mmgen.bitcoin import verify_addr
 def add_fake_unspent_entry(out,address,comment):
 	out.append(TransactionInfo(
 		account = unicode(comment),
-		vout = (getrand(4) % 8),
+		vout = int(getrand(4) % 8),
 		txid = unicode(hexlify(os.urandom(32))),
 		amount = Decimal("%s.%s" % (10+(getrand(4) % 40), getrand(4) % 100000000)),
 		address = address,
@@ -434,14 +449,14 @@ def add_fake_unspent_entry(out,address,comment):
 		confirmations = getrand(4) % 500
 	))
 
-def create_fake_unspent_data(addr_data,unspent_data_file,tx_data,non_mmgen_input=''):
+def create_fake_unspent_data(adata,unspent_data_file,tx_data,non_mmgen_input=''):
 
 	out = []
 	for s in tx_data.keys():
 		sid = tx_data[s]['sid']
-		for idx in addr_data[sid].keys():
-			address = unicode(addr_data[sid][idx][0])
-			add_fake_unspent_entry(out,address, "%s:%s Test Wallet" % (sid,idx))
+		a = adata.addrinfo(sid)
+		for idx,btcaddr in a.addrpairs():
+			add_fake_unspent_entry(out,btcaddr,"%s:%s Test Wallet" % (sid,idx))
 
 	if non_mmgen_input:
 		from mmgen.bitcoin import privnum2addr,hextowif
@@ -453,25 +468,17 @@ def create_fake_unspent_data(addr_data,unspent_data_file,tx_data,non_mmgen_input
 
 		add_fake_unspent_entry(out,btcaddr,"Non-MMGen address")
 
+#	msg("\n".join([repr(o) for o in out])); sys.exit()
 	write_to_file(unspent_data_file,repr(out),{},"Unspent outputs",verbose=True)
 
 
 def add_comments_to_addr_file(addrfile,tfile):
 	silence()
 	msg(green("Adding comments to address file '%s'" % addrfile))
-	d = get_lines_from_file(addrfile)
-	addr_data = {}
-	from mmgen.tx import parse_addrfile
-	parse_addrfile(addrfile,addr_data)
-	sid = addr_data.keys()[0]
-	def s(k): return int(k)
-	keys = sorted(addr_data[sid].keys(),key=s)
-	for n,k in enumerate(keys,1):
-		addr_data[sid][k][1] = ("Test address " + str(n))
-	d = "#\n# Test address file with comments\n#\n%s {\n%s\n}\n" % (sid,
-		"\n".join(["    {:<3} {:<36} {}".format(k,*addr_data[sid][k]) for k in keys]))
-	msg_r(d)
-	write_to_file(tfile,d,{})
+	from mmgen.addr import AddrInfo
+	a = AddrInfo(addrfile)
+	for i in a.idxs(): a.set_comment(idx,"Test address %s" % idx)
+	write_to_file(tfile,a.fmt_data(),{})
 	end_silence()
 
 def make_brainwallet_file(fn):
@@ -568,6 +575,22 @@ def mk_tmpdir(cfg):
 		if e.errno != 17: raise
 	else: msg("Created directory '%s'" % cfg['tmpdir'])
 
+def refcheck(what,chk,refchk):
+	vmsg("Comparing %s '%s' to stored reference" % (what,chk))
+	if chk == refchk:
+		ok()
+	else:
+		if not verbose: errmsg("")
+		errmsg(red("""
+Fatal error - %s '%s' does not match reference value '%s'.  Aborting test
+""".strip() % (what,chk,refchk)))
+		sys.exit(3)
+
+def ok():
+	if verbose or exact_output:
+		sys.stderr.write(green("OK\n"))
+	else: msg(" OK")
+
 
 class MMGenTestSuite(object):
 
@@ -599,7 +622,7 @@ class MMGenTestSuite(object):
 
 
 	def clean(self,name,dirs=[]):
-		dirlist = dirs if dirs else cfgs.keys()
+		dirlist = dirs if dirs else sorted(cfgs.keys())
 		for k in dirlist:
 			if k in cfgs:
 				cleandir(cfgs[k]['tmpdir'])
@@ -611,6 +634,7 @@ class MMGenTestSuite(object):
 		mk_tmpdir(cfg)
 
 		args = ["-d",cfg['tmpdir'],"-p1","-r10"]
+#        if 'seed_len' in cfg: args += ["-l",cfg['seed_len']]
 		if brain:
 			bwf = os.path.join(cfg['tmpdir'],cfg['bw_filename'])
 			args += ["-b",cfg['bw_params'],bwf]
@@ -625,14 +649,23 @@ class MMGenTestSuite(object):
 			t.expect("Type uppercase 'YES' to confirm: ","YES\n")
 
 		t.usr_rand(10)
-		t.expect("Generating a key from OS random data plus user entropy")
-
-		if not brain:
-			t.expect("Generating a key from OS random data plus saved user entropy")
+		for s in "user-supplied entropy","saved user-supplied entropy":
+			t.expect("Generating encryption key from OS random data plus %s" % s)
+			if brain: break
 
 		t.passphrase_new("MMGen wallet",cfg['wpasswd'])
 		t.written_to_file("Wallet")
-		t.ok()
+		ok()
+
+	def refwalletgen(self,name):
+		mk_tmpdir(cfg)
+		args = ["-q","-d",cfg['tmpdir'],"-p1","-r10","-b"+cfg['bw_hashparams']]
+		t = MMGenExpect(name,"mmgen-walletgen", args)
+		t.expect("passphrase: ",cfg['bw_passwd']+"\n")
+		t.usr_rand(10)
+		t.passphrase_new("MMGen wallet",cfg['wpasswd'])
+		key_id = t.written_to_file("Wallet").split("-")[0].split("/")[-1]
+		refcheck("key id",key_id,cfg['key_id'])
 
 	def passchg(self,name,walletfile):
 		mk_tmpdir(cfg)
@@ -647,11 +680,11 @@ class MMGenTestSuite(object):
 		t.usr_rand(16)
 		t.expect_getend("Key ID changed: ")
 		t.written_to_file("Wallet")
-		t.ok()
+		ok()
 
 	def walletchk_newpass(self,name,walletfile):
 		t = self.walletchk_beg(name,[walletfile])
-		t.ok()
+		ok()
 
 	def walletchk_beg(self,name,args):
 		t = MMGenExpect(name,"mmgen-walletchk", args)
@@ -663,17 +696,23 @@ class MMGenTestSuite(object):
 
 	def walletchk(self,name,walletfile):
 		t = self.walletchk_beg(name,[walletfile])
-		t.ok()
+		ok()
 
-	def addrgen(self,name,walletfile):
+	def addrgen(self,name,walletfile,check_ref=False):
 		t = MMGenExpect(name,"mmgen-addrgen",["-d",cfg['tmpdir'],walletfile,cfg['addr_idx_list']])
 		t.license()
 		t.passphrase("MMGen wallet",cfg['wpasswd'])
 		t.expect("Passphrase is OK")
-		t.expect("Generated [0-9]+ addresses",regex=True)
-		t.expect_getend(r"Checksum for address data .*?: ",regex=True)
+		t.expect("[0-9]+ addresses generated",regex=True)
+		chk = t.expect_getend(r"Checksum for address data .*?: ",regex=True)
+		if check_ref:
+			refcheck("address data checksum",chk,cfg['addrfile_chk'])
+			return
 		t.written_to_file("Addresses")
-		t.ok()
+		ok()
+
+	def refaddrgen(self,name,walletfile):
+		self.addrgen(name,walletfile,check_ref=True)
 
 	def addrimport(self,name,addrfile):
 		outfile = os.path.join(cfg['tmpdir'],"addrfile_w_comments")
@@ -683,7 +722,7 @@ class MMGenTestSuite(object):
 		t.expect_getend("Validating addresses...OK. ")
 		t.expect("Type uppercase 'YES' to confirm: ","\n")
 		vmsg("This is a simulation, so no addresses were actually imported into the tracking\nwallet")
-		t.ok()
+		ok()
 
 	def txcreate(self,name,addrfile):
 		self.txcreate_common(name,sources=['1'])
@@ -692,26 +731,27 @@ class MMGenTestSuite(object):
 		if verbose or exact_output:
 			sys.stderr.write(green("Generating fake transaction info\n"))
 		silence()
-		tx_data,addr_data = {},{}
-		from mmgen.tx import parse_addrfile
+		from mmgen.addr import AddrInfo,AddrInfoList
+		tx_data,ail = {},AddrInfoList()
 		from mmgen.util import parse_addr_idxs
 		for s in sources:
 			afile = get_file_with_ext("addrs",cfgs[s]["tmpdir"])
-			chk,sid = parse_addrfile(afile,addr_data,return_chk_and_sid=True)
+			ai = AddrInfo(afile)
+			ail.add(ai)
 			aix = parse_addr_idxs(cfgs[s]['addr_idx_list'])
 			if len(aix) != addrs_per_wallet:
 				errmsg(red("Addr index list length != %s: %s" %
 							(addrs_per_wallet,repr(aix))))
 				sys.exit()
 			tx_data[s] = {
-				'addrfile': get_file_with_ext("addrs",cfgs[s]['tmpdir']),
-				'chk': chk,
-				'sid': sid,
+				'addrfile': afile,
+				'chk': ai.checksum,
+				'sid': ai.seed_id,
 				'addr_idxs': aix[-2:],
 			}
 
 		unspent_data_file = os.path.join(cfg['tmpdir'],"unspent.json")
-		create_fake_unspent_data(addr_data,unspent_data_file,tx_data,non_mmgen_input)
+		create_fake_unspent_data(ail,unspent_data_file,tx_data,non_mmgen_input)
 
 		# make the command line
 		from mmgen.bitcoin import privnum2addr
@@ -739,7 +779,6 @@ class MMGenTestSuite(object):
 		t.license()
 		for num in tx_data.keys():
 			t.expect_getend("Getting address data from file ")
-			from mmgen.addr import fmt_addr_idxs
 			chk=t.expect_getend(r"Computed checksum for addr data .*?: ",regex=True)
 			verify_checksum_or_exit(tx_data[num]['chk'],chk)
 
@@ -763,9 +802,9 @@ class MMGenTestSuite(object):
 		t.expect("OK? (Y/n): ","y")
 		t.expect("Add a comment to transaction? (y/N): ","\n")
 		t.tx_view()
-		t.expect("Save transaction? (Y/n): ","\n")
+		t.expect("Save transaction? (y/N): ","y")
 		t.written_to_file("Transaction")
-		t.ok()
+		ok()
 
 	def txsign(self,name,txfile,walletfile):
 		t = MMGenExpect(name,"mmgen-txsign", ["-d",cfg['tmpdir'],txfile,walletfile])
@@ -774,7 +813,7 @@ class MMGenTestSuite(object):
 		t.passphrase("MMGen wallet",cfg['wpasswd'])
 		t.expect("Edit transaction comment? (y/N): ","\n")
 		t.written_to_file("Signed transaction")
-		t.ok()
+		ok()
 
 	def txsend(self,name,sigfile):
 		t = MMGenExpect(name,"mmgen-txsend", ["-d",cfg['tmpdir'],sigfile])
@@ -785,7 +824,7 @@ class MMGenTestSuite(object):
 		t.expect("Type uppercase 'YES, I REALLY WANT TO DO THIS' to confirm: ","\n")
 		t.expect("Exiting at user request")
 		vmsg("This is a simulation, so no transaction was sent")
-		t.ok()
+		ok()
 
 	def export_seed(self,name,walletfile):
 		t = self.walletchk_beg(name,["-s","-d",cfg['tmpdir'],walletfile])
@@ -793,7 +832,7 @@ class MMGenTestSuite(object):
 		silence()
 		msg("Seed data: %s" % cyan(get_data_from_file(f,"seed data")))
 		end_silence()
-		t.ok()
+		ok()
 
 	def export_mnemonic(self,name,walletfile):
 		t = self.walletchk_beg(name,["-m","-d",cfg['tmpdir'],walletfile])
@@ -801,7 +840,7 @@ class MMGenTestSuite(object):
 		silence()
 		msg_r("Mnemonic data: %s" % cyan(get_data_from_file(f,"mnemonic data")))
 		end_silence()
-		t.ok()
+		ok()
 
 	def export_incog(self,name,walletfile,args=["-g"]):
 		t = MMGenExpect(name,"mmgen-walletchk",args+["-d",cfg['tmpdir'],"-r","10",walletfile])
@@ -810,7 +849,7 @@ class MMGenTestSuite(object):
 		t.expect_getend("Incog ID: ")
 		if args[0] == "-G": return t
 		t.written_to_file("Incognito wallet data",overwrite_unlikely=True)
-		t.ok()
+		ok()
 
 	def export_incog_hex(self,name,walletfile):
 		self.export_incog(name,walletfile,args=["-X"])
@@ -822,7 +861,7 @@ class MMGenTestSuite(object):
 		write_to_file(rf,rd,{},verbose=verbose)
 		t = self.export_incog(name,walletfile,args=["-G","%s,%s"%(rf,hincog_offset)])
 		t.written_to_file("Data",query="")
-		t.ok()
+		ok()
 
 	def addrgen_seed(self,name,walletfile,foo,what="seed data",arg="-s"):
 		t = MMGenExpect(name,"mmgen-addrgen",
@@ -833,7 +872,7 @@ class MMGenTestSuite(object):
 		chk = t.expect_getend(r"Checksum for address data .*?: ",regex=True)
 		verify_checksum_or_exit(get_addrfile_checksum(),chk)
 		t.no_overwrite()
-		t.ok()
+		ok()
 
 	def addrgen_mnemonic(self,name,walletfile,foo):
 		self.addrgen_seed(name,walletfile,foo,what="mnemonic",arg="-m")
@@ -849,7 +888,7 @@ class MMGenTestSuite(object):
 		chk = t.expect_getend(r"Checksum for address data .*?: ",regex=True)
 		verify_checksum_or_exit(get_addrfile_checksum(),chk)
 		t.no_overwrite()
-		t.ok()
+		ok()
 
 	def addrgen_incog_hex(self,name,walletfile,foo):
 		self.addrgen_incog(name,walletfile,foo,args=["-X"])
@@ -859,18 +898,24 @@ class MMGenTestSuite(object):
 		self.addrgen_incog(name,walletfile,foo,
 				args=["-G","%s,%s,%s"%(rf,hincog_offset,hincog_seedlen)])
 
-	def keyaddrgen(self,name,walletfile):
+	def keyaddrgen(self,name,walletfile,check_ref=False):
 		t = MMGenExpect(name,"mmgen-keygen",
 				["-d",cfg['tmpdir'],walletfile,cfg['addr_idx_list']])
 		t.license()
 		t.expect("Type uppercase 'YES' to confirm: ","YES\n")
 		t.passphrase("MMGen wallet",cfg['wpasswd'])
-		t.expect_getend(r"Checksum for key-address data .*?: ",regex=True)
+		chk = t.expect_getend(r"Checksum for key-address data .*?: ",regex=True)
+		if check_ref:
+			refcheck("key-address data checksum",chk,cfg['keyaddrfile_chk'])
+			return
 		t.expect("Encrypt key list? (y/N): ","y")
 		t.hash_preset("new key list",'1')
 		t.passphrase_new("key list",cfg['kapasswd'])
 		t.written_to_file("Keys")
-		t.ok()
+		ok()
+
+	def refkeyaddrgen(self,name,walletfile):
+		self.keyaddrgen(name,walletfile,check_ref=True)
 
 	def txsign_keyaddr(self,name,keyaddr_file,txfile):
 		t = MMGenExpect(name,"mmgen-txsign", ["-d",cfg['tmpdir'],"-M",keyaddr_file,txfile])
@@ -882,7 +927,7 @@ class MMGenTestSuite(object):
 		t.expect("Signing transaction...OK")
 		t.expect("Edit transaction comment? (y/N): ","\n")
 		t.written_to_file("Signed transaction")
-		t.ok()
+		ok()
 
 	def walletgen2(self,name):
 		self.walletgen(name)
@@ -904,7 +949,7 @@ class MMGenTestSuite(object):
 			t.expect("Edit transaction comment? (y/N): ","\n")
 			t.written_to_file("Signed transaction #%s" % cnum)
 
-		t.ok()
+		ok()
 
 	def export_mnemonic2(self,name,walletfile):
 		self.export_mnemonic(name,walletfile)
@@ -930,7 +975,7 @@ class MMGenTestSuite(object):
 		t.expect_getend("Signing transaction")
 		t.expect("Edit transaction comment? (y/N): ","\n")
 		t.written_to_file("Signed transaction")
-		t.ok()
+		ok()
 
 	def walletgen4(self,name):
 		self.walletgen(name,brain=True)
@@ -957,7 +1002,7 @@ class MMGenTestSuite(object):
 		t.expect_getend("Signing transaction")
 		t.expect("Edit transaction comment? (y/N): ","\n")
 		t.written_to_file("Signed transaction")
-		t.ok()
+		ok()
 
 # main()
 ts = MMGenTestSuite()