Browse Source

obj.py rewrite+test suite, Bob and Alice regtest mode, compressed addresses

- basic data types in obj.py rewritten
- new test suite 'test/objtest.py' for testing basic data types
- new compressed address type with the 'C' identifier

- Bob and Alice regtest mode for testing MMGen in a mock two-user environment
  * All MMGen commands are available in this mode.
  * Set up with 'mmgen-regtest setup'. Bob and Alice's wallets are funded with
    500 BTC each.  Use the --mixed switch to import mixed address types in
    Bob and Alice's tracking wallets.
  * Transact as Bob by adding --bob switch to MMGen commands
  * Transact as Alice by adding --alice switch to MMGen commands
  * After sending a transaction, mine a block to confirm it with
    'mmgen-regtest generate'
    The bitcoin daemon is stopped and restarted automatically when switching
    between users.
philemon 7 years ago
parent
commit
b23b497d77
20 changed files with 813 additions and 461 deletions
  1. 25 0
      mmgen-regtest
  2. 14 19
      mmgen/addr.py
  3. 28 29
      mmgen/bitcoin.py
  4. 6 2
      mmgen/globalvars.py
  5. 3 2
      mmgen/main_addrgen.py
  6. 1 10
      mmgen/main_addrimport.py
  7. 65 0
      mmgen/main_regtest.py
  8. 1 1
      mmgen/main_txsend.py
  9. 302 324
      mmgen/obj.py
  10. 12 0
      mmgen/opts.py
  11. 64 46
      mmgen/tool.py
  12. 8 2
      mmgen/tx.py
  13. 6 2
      mmgen/txsign.py
  14. 13 2
      mmgen/util.py
  15. 4 1
      scripts/traceback.py
  16. 3 0
      setup.py
  17. 0 3
      test/gentest.py
  18. 249 0
      test/objtest.py
  19. 8 17
      test/test.py
  20. 1 1
      test/tooltest.py

+ 25 - 0
mmgen-regtest

@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+mmgen-regtest: Bitcoind regression test mode setup and operations for the MMGen
+               suite
+"""
+
+from mmgen.main import launch
+launch("regtest")

+ 14 - 19
mmgen/addr.py

@@ -61,7 +61,7 @@ class AddrGeneratorSegwit(MMGenObject):
 class KeyGenerator(MMGenObject):
 	def __new__(cls,generator=None,silent=False):
 		if cls.test_for_secp256k1(silent=silent) and generator != 1:
-			if opt.key_generator != 1:
+			if (not hasattr(opt,'key_generator')) or opt.key_generator == 2 or generator == 2:
 				return super(cls,cls).__new__(KeyGeneratorSecp256k1)
 		else:
 			msg('Using (slow) native Python ECDSA library for address generation')
@@ -182,7 +182,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 
 		if seed and addr_idxs:   # data from seed + idxs
 			self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
-			adata = self.generate(seed,addr_idxs,compressed=(mmtype=='S'))
+			adata = self.generate(seed,addr_idxs)
 		elif addrfile:           # data from MMGen address file
 			adata = self.parse_file(addrfile) # sets self.al_id
 		elif al_id and adata:    # data from tracking wallet
@@ -210,7 +210,6 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 		if self.al_id == None: return
 
 		self.id_str = AddrListIDStr(self)
-
 		if type(self) == KeyList: return
 
 		if do_chksum:
@@ -223,21 +222,18 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 				qmsg(self.msgs[('check_chksum','record_chksum')[src=='gen']])
 
 	def update_msgs(self):
-		if type(self).msgs and type(self) != AddrList:
-			for k in AddrList.msgs:
-				if k not in self.msgs:
-					self.msgs[k] = AddrList.msgs[k]
+		self.msgs = AddrList.msgs
+ 		self.msgs.update(type(self).msgs)
 
-	def generate(self,seed,addrnums,compressed):
+	def generate(self,seed,addrnums):
 		assert type(addrnums) is AddrIdxList
-		assert type(compressed) is bool
 
 		seed = seed.get_data()
 		seed = self.cook_seed(seed)
 
 		if self.gen_addrs:
 			kg = KeyGenerator()
-			ag = AddrGenerator(('p2pkh','segwit')[self.al_id.mmtype=='S'])
+			ag = AddrGenerator(self.al_id.mmtype.gen_method)
 
 		t_addrs,num,pos,out = len(addrnums),0,0,AddrListList()
 		le = self.entry_type
@@ -256,7 +252,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 			e = le(idx=num)
 
 			# Secret key is double sha256 of seed hash round /num/
-			e.sec = PrivKey(sha256(sha256(seed).digest()).digest(),compressed)
+			e.sec = PrivKey(sha256(sha256(seed).digest()).digest(),self.al_id.mmtype.compressed)
 
 			if self.gen_addrs:
 				e.addr = ag.to_addr(kg.to_pubhex(e.sec))
@@ -278,8 +274,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 	def is_for_current_chain(self):
 		return self.data[0].addr.is_for_current_chain()
 
-	def chk_addr_or_pw(self,addr):
-		return {'L':'p2pkh','S':'p2sh'}[self.al_id.mmtype] == is_btc_addr(addr).addr_fmt
+	def check_format(self,addr): return True # format is checked when added to list entry object
 
 	def cook_seed(self,seed):
 		if self.al_id.mmtype == 'L':
@@ -386,7 +381,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 		elif self.al_id.mmtype == 'L':
 			out.append('{} {{'.format(self.al_id.sid))
 		else:
-			out.append('{} {} {{'.format(self.al_id.sid,MMGenAddrType.mmtypes[self.al_id.mmtype].upper()))
+			out.append('{} {} {{'.format(self.al_id.sid,self.al_id.mmtype.name.upper()))
 
 		fs = '  {:<%s}  {:<34}{}' % len(str(self.data[-1].idx))
 		for e in self.data:
@@ -419,7 +414,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 			if not is_mmgen_idx(d[0]):
 				return "'%s': invalid address num. in line: '%s'" % (d[0],l)
 
-			if not self.chk_addr_or_pw(d[1]):
+			if not self.check_format(d[1]):
 				return "'{}': invalid {}".format(d[1],self.data_desc)
 
 			if len(d) != 3: d.append('')
@@ -441,7 +436,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 
 		if self.has_keys and keypress_confirm('Check key-to-address validity?'):
 			kg = KeyGenerator()
-			ag = AddrGenerator(('p2pkh','segwit')[self.al_id.mmtype=='S'])
+			ag = AddrGenerator(self.al_id.mmtype.gen_method)
 			llen = len(ret)
 			for n,e in enumerate(ret):
 				msg_r('\rVerifying keys %s/%s' % (n+1,llen))
@@ -489,7 +484,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 				mmtype = MMGenAddrType(mmtype)
 			except:
 				return do_error(u"'{}': invalid address type in address file. Must be one of: {}".format(
-					mmtype.upper(),' '.join(MMGenAddrType.mmtypes.values()).upper()))
+					mmtype.upper(),' '.join([i['name'].upper() for i in MMGenAddrType.mmtypes.values()])))
 		elif len(ls) == 0:
 			mmtype = MMGenAddrType('L')
 		else:
@@ -582,7 +577,7 @@ Record this checksum: it will be used to verify the password file in the future
 			self.set_pw_len(pw_len)
 			if chk_params_only: return
 			self.al_id = AddrListID(seed.sid,MMGenPasswordType('P'))
-			self.data = self.generate(seed,pw_idxs,compressed=False)
+			self.data = self.generate(seed,pw_idxs)
 
 		self.num_addrs = len(self.data)
 		self.fmt_data = ''
@@ -632,7 +627,7 @@ Record this checksum: it will be used to verify the password file in the future
 		# we take least significant part
 		return ''.join(baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len))[-self.pw_len:]
 
-	def chk_addr_or_pw(self,pw):
+	def check_format(self,pw):
 		if not (is_b58_str,is_b32_str)[self.pw_fmt=='b32'](pw):
 			msg('Password is not a valid {} string'.format(self.pw_fmt))
 			return False

+ 28 - 29
mmgen/bitcoin.py

@@ -60,8 +60,6 @@ def _b58tonum(b58num):
 		if not i in _b58a: return False
 	return sum(_b58a.index(n) * (58**i) for i,n in enumerate(list(b58num[::-1])))
 
-from mmgen.globalvars import g
-
 def hash160(hexnum): # take hex, return hex - OP_HASH160
 	return hashlib_new('ripemd160',sha256(unhexlify(hexnum)).digest()).hexdigest()
 
@@ -69,56 +67,57 @@ def hash256(hexnum): # take hex, return hex - OP_HASH256
 	return sha256(sha256(unhexlify(hexnum)).digest()).hexdigest()
 
 # devdoc/ref_transactions.md:
-btc_ver_nums = {
-	'p2pkh': (('00','1'),('6f','mn')),
-	'p2sh':  (('05','3'),('c4','2'))
+btc_addr_ver_nums = {
+	'p2pkh': { 'mainnet': ('00','1'), 'testnet': ('6f','mn') },
+	'p2sh':  { 'mainnet': ('05','3'), 'testnet': ('c4','2') }
 }
-addr_pfxs = { 'mainnet': '13', 'testnet': 'mn2', 'regtest': 'mn2' }
-vnum_all = tuple([k for k,v in btc_ver_nums['p2pkh'] + btc_ver_nums['p2sh']])
-
-def hexaddr2addr(hexaddr,p2sh=False):
-	s = vnum_all[g.testnet+(2*p2sh)] + hexaddr.strip()
-	lzeroes = (len(s) - len(s.lstrip('0'))) / 2
-	return ('1' * lzeroes) + _numtob58(int(s+hash256(s)[:8],16))
+btc_addr_pfxs             = { 'mainnet': '13', 'testnet': 'mn2', 'regtest': 'mn2' }
+btc_uncompressed_wif_pfxs = { 'mainnet':'5','testnet':'9' }
+btc_privkey_pfxs          = { 'mainnet':'80','testnet':'ef' }
 
-def verify_addr(addr,verbose=False,return_hex=False,return_type=False):
-	addr = addr.strip()
+from mmgen.globalvars import g
 
-	for k in ('p2pkh','p2sh'):
-		for ver_num,ldigit in btc_ver_nums[k]:
+def verify_addr(addr,verbose=False,return_dict=False,testnet=None):
+	testnet = testnet if testnet != None else g.testnet # allow override
+	for addr_fmt in ('p2pkh','p2sh'):
+		for net in ('mainnet','testnet'):
+			ver_num,ldigit = btc_addr_ver_nums[addr_fmt][net]
 			if addr[0] not in ldigit: continue
 			num = _b58tonum(addr)
 			if num == False: break
 			addr_hex = '{:050x}'.format(num)
 			if addr_hex[:2] != ver_num: continue
 			if hash256(addr_hex[:42])[:8] == addr_hex[42:]:
-				return addr_hex[2:42] if return_hex else k if return_type else True
+				return {'hex':addr_hex[2:42],'format':addr_fmt,'net':net} if return_dict else True
 			else:
-				if verbose: Msg("Invalid checksum in address '%s'" % addr)
+				if verbose: Msg("Invalid checksum in address '{}'".format(addr))
 				break
 
-	if verbose: Msg("Invalid address '%s'" % addr)
+	if verbose: Msg("Invalid address '{}'".format(addr))
 	return False
 
-# Compressed address support:
-
-def wif_is_compressed(wif): return wif[0] != ('5','9')[g.testnet]
+def hexaddr2addr(hexaddr,p2sh=False,testnet=None):
+	testnet = testnet if testnet != None else g.testnet # allow override
+	s = btc_addr_ver_nums[('p2pkh','p2sh')[p2sh]][('mainnet','testnet')[testnet]][0] + hexaddr
+	lzeroes = (len(s) - len(s.lstrip('0'))) / 2
+	return ('1' * lzeroes) + _numtob58(int(s+hash256(s)[:8],16))
 
-def wif2hex(wif):
-	wif = wif.strip()
-	compressed = wif_is_compressed(wif)
+def wif2hex(wif,testnet=None):
+	testnet = testnet if testnet != None else g.testnet # allow override
 	num = _b58tonum(wif)
 	if num == False: return False
 	key = '{:x}'.format(num)
+	compressed = wif[0] != btc_uncompressed_wif_pfxs[('mainnet','testnet')[testnet]]
 	klen = (66,68)[bool(compressed)]
 	if compressed and key[66:68] != '01': return False
-	if (key[:2] == ('80','ef')[g.testnet] and key[klen:] == hash256(key[:klen])[:8]):
-		return key[2:66]
+	if (key[:2] == btc_privkey_pfxs[('mainnet','testnet')[testnet]] and key[klen:] == hash256(key[:klen])[:8]):
+		return {'hex':key[2:66],'compressed':compressed,'testnet':testnet}
 	else:
 		return False
 
-def hex2wif(hexpriv,compressed=False):
-	s = ('80','ef')[g.testnet] + hexpriv.strip() + ('','01')[bool(compressed)]
+def hex2wif(hexpriv,compressed=False,testnet=None):
+	testnet = testnet if testnet != None else g.testnet # allow override
+	s = btc_privkey_pfxs[('mainnet','testnet')[testnet]] + hexpriv + ('','01')[bool(compressed)]
 	return _numtob58(int(s+hash256(s)[:8],16))
 
 # devdoc/guide_wallets.md:

+ 6 - 2
mmgen/globalvars.py

@@ -86,6 +86,9 @@ class g(object):
 	rpc_password         = ''
 	testnet_name         = 'testnet3'
 
+	bob                  = False
+	alice                = False
+
 	# test suite:
 	bogus_wallet_data    = ''
 	traceback_cmd        = 'scripts/traceback.py'
@@ -110,14 +113,15 @@ class g(object):
 	# User opt sets global var:
 	common_opts = (
 		'color','no_license','rpc_host','rpc_port','testnet','rpc_user','rpc_password',
-		'bitcoin_data_dir','force_256_color','regtest','coin'
+		'bitcoin_data_dir','force_256_color','regtest','coin','bob','alice'
 	)
 	required_opts = (
 		'quiet','verbose','debug','outdir','echo_passphrase','passwd_file','stdout',
 		'show_hash_presets','label','keep_passphrase','keep_hash_preset','yes',
-		'brain_params','b16','usr_randchars','coin'
+		'brain_params','b16','usr_randchars','coin','bob','alice'
 	)
 	incompatible_opts = (
+		('bob','alice'),
 		('quiet','verbose'),
 		('label','keep_label'),
 		('tx_id','info'),

+ 3 - 2
mmgen/main_addrgen.py

@@ -80,7 +80,7 @@ opts_data = lambda: {
 	kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 	kg=g.key_generator,
 	what=gen_what,g=g,
-	dmat="'{}' or '{}'".format(MAT.dfl_mmtype,MAT.mmtypes[MAT.dfl_mmtype])
+	dmat="'{}' or '{}'".format(MAT.dfl_mmtype,MAT.mmtypes[MAT.dfl_mmtype]['name'])
 ),
 	'notes': """
 
@@ -95,6 +95,7 @@ range(s).
 ADDRESS TYPES:
   {n_at}
 
+
                       NOTES FOR ALL GENERATOR COMMANDS
 
 {pwn}
@@ -106,7 +107,7 @@ FMT CODES:
 """.format(
 		n_secp=note_secp256k1,n_addrkey=note_addrkey,pwn=pw_note,bwn=bw_note,
 		f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
-		n_at='\n  '.join(["'{}', '{}'".format(k,v) for k,v in MAT.mmtypes.items()]),
+		n_at='\n  '.join(["'{}','{:<12} - {}".format(k,v['name']+"'",v['desc']) for k,v in MAT.mmtypes.items()]),
 		o=opts
 	)
 }

+ 1 - 10
mmgen/main_addrimport.py

@@ -64,16 +64,7 @@ def import_mmgen_list(infile):
 	return al
 
 def import_flat_list(lines):
-	al = AddrList(addrlist=lines)
-	from mmgen.bitcoin import verify_addr
-	qmsg_r('Validating addresses...')
-	for e in al.data:
-		if not verify_addr(e.addr,verbose=True):
-			die(2,'\n%s: invalid address' % e.addr)
-		if e.addr.addr_fmt == 'p2sh':
-			fs = "\n'{}':\n  Non-{} P2SH addresses may not be imported into the tracking wallet"
-			rdie(2,fs.format(e.addr,g.proj_name))
-	return al
+	return AddrList(addrlist=lines)
 
 if len(cmd_args) == 1:
 	infile = cmd_args[0]

+ 65 - 0
mmgen/main_regtest.py

@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+mmgen-regtest: Bitcoind regression test mode setup and operations for the MMGen
+               suite
+"""
+
+from mmgen.common import *
+opts_data = lambda: {
+	'desc': 'Bitcoind regression test mode setup and operations for the {} suite'.format(g.proj_name),
+	'usage':   '[opts] <command>',
+	'sets': ( ('yes', True, 'quiet', True), ),
+	'options': """
+-h, --help          Print this help message
+-m, --mixed         Create Bob and Alice's wallets with mixed address types
+--, --longhelp      Print help message for long options (common options)
+-q, --quiet         Produce quieter output
+-v, --verbose       Produce more verbose output
+""",
+	'notes': """
+
+
+                           AVAILABLE COMMANDS
+
+    setup           - setup up system for regtest operation with MMGen
+    stop            - stop the regtest bitcoind
+    bob             - switch to Bob's wallet, starting daemon if necessary
+    alice           - switch to Alice's wallet, starting daemon if necessary
+    user            - show current user
+    generate        - mine a block
+    test_daemon     - test whether daemon is running
+    get_balances    - get balances of Bob and Alice
+	"""
+}
+
+cmd_args = opts.init(opts_data)
+
+if len(cmd_args) != 1:
+	opts.usage()
+
+cmds = ('setup','stop','generate','test_daemon','create_data_dir','bob','alice','user',
+		'wait_for_daemon','wait_for_exit','get_current_user','get_balances')
+
+if cmd_args[0] not in cmds:
+	opts.usage()
+
+from mmgen.regtest import *
+
+globals()[cmd_args[0]]()

+ 1 - 1
mmgen/main_txsend.py

@@ -48,7 +48,7 @@ if not opt.status: do_license_msg()
 
 c = bitcoin_connection()
 tx = MMGenTX(infile) # sig check performed here
-qmsg("Signed transaction file '%s' is valid" % infile)
+vmsg("Signed transaction file '%s' is valid" % infile)
 
 if not tx.marked_signed(c):
 	die(1,'Transaction is not signed!')

+ 302 - 324
mmgen/obj.py

@@ -23,6 +23,7 @@ obj.py: MMGen native classes
 import sys
 from decimal import *
 from mmgen.color import *
+from string import hexdigits,ascii_letters,digits
 
 def is_mmgen_seed_id(s): return SeedID(sid=s,on_fail='silent')
 def is_mmgen_idx(s):     return AddrIdx(s,on_fail='silent')
@@ -38,7 +39,6 @@ class MMGenObject(object):
 	def pmsg(self): print(self.pformat())
 	def pdie(self): print(self.pformat()); sys.exit(0)
 	def pformat(self,lvl=0):
-		from decimal import Decimal
 		scalars = (str,unicode,int,float,Decimal)
 		def do_list(out,e,lvl=0,is_dict=False):
 			out.append('\n')
@@ -100,9 +100,71 @@ class MMGenObject(object):
 
 class MMGenList(list,MMGenObject): pass
 class MMGenDict(dict,MMGenObject): pass
+class AddrListList(list,MMGenObject): pass
+
+class InitErrors(object):
+
+	@staticmethod
+	def arg_chk(cls,on_fail):
+		assert on_fail in ('die','return','silent','raise'),'arg_chk in class {}'.format(cls.__name__)
 
-# for attrs that are always present in the data instance
-# reassignment and deletion forbidden
+	@staticmethod
+	def init_fail(m,on_fail,silent=False):
+		if silent: m = ''
+		from mmgen.util import die,msg
+		if on_fail == 'die': die(1,m)
+		elif on_fail == 'return':
+			if m: msg(m)
+			return None # TODO: change to False
+		elif on_fail == 'silent': return None # same here
+		elif on_fail == 'raise':  raise ValueError,m
+
+class Hilite(object):
+
+	color = 'red'
+	color_always = False
+	width = 0
+	trunc_ok = True
+
+	@classmethod
+	def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None,
+				center=False,nullrepl='',app='',appcolor=False):
+		if width == None: width = cls.width
+		if trunc_ok == None: trunc_ok = cls.trunc_ok
+		assert width > 0
+		if s == '' and nullrepl:
+			s,center = nullrepl,True
+		if center: s = s.center(width)
+		assert type(encl) is str and len(encl) in (0,2)
+		a,b = list(encl) if encl else ('','')
+		if trunc_ok and len(s) > width: s = s[:width]
+		if app:
+			return cls.colorize(a+s+b,color=color) + \
+					cls.colorize(app.ljust(width-len(a+s+b)),color=appcolor)
+		else:
+			return cls.colorize((a+s+b).ljust(width),color=color)
+
+	def fmt(self,*args,**kwargs):
+		assert args == () # forbid invocation w/o keywords
+		return self.fmtc(self,*args,**kwargs)
+
+	@classmethod
+	def hlc(cls,s,color=True):
+		return cls.colorize(s,color=color)
+
+	def hl(self,color=True):
+		return self.colorize(self,color=color)
+
+	def __str__(self):
+		return self.colorize(self,color=False)
+
+	@classmethod
+	def colorize(cls,s,color=True):
+		k = color if type(color) is str else cls.color # hack: override color with str value
+		return globals()[k](s) if (color or cls.color_always) else s
+
+# For attrs that are always present in the data instance
+# Reassignment and deletion forbidden
 class MMGenImmutableAttr(object): # Descriptor
 
 	def __init__(self,name,dtype,typeconv=True):
@@ -124,7 +186,7 @@ class MMGenImmutableAttr(object): # Descriptor
 			raise AttributeError(m.format(self.name,type(instance)))
 		if self.typeconv:   # convert type
 			instance.__dict__[self.name] = \
-				globals()[self.dtype](value) if type(self.dtype) == str else self.dtype(value)
+				globals()[self.dtype](value,on_fail='raise') if type(self.dtype) == str else self.dtype(value)
 		else:               # check type
 			if type(value) != self.dtype:
 				m = "Attribute '{}' of {} instance must of type {}"
@@ -135,9 +197,9 @@ class MMGenImmutableAttr(object): # Descriptor
 		m = "Atribute '{}' of {} instance cannot be deleted"
 		raise AttributeError(m.format(self.name,type(instance)))
 
-# for attrs that might not be present in the data instance
-# reassignment or deletion allowed if specified
-class MMGenListItemAttr(MMGenImmutableAttr):
+# For attrs that might not be present in the data instance
+# Reassignment or deletion allowed if specified
+class MMGenListItemAttr(MMGenImmutableAttr): # Descriptor
 
 	def __init__(self,name,dtype,typeconv=True,reassign_ok=False,delete_ok=False):
 		self.reassign_ok = reassign_ok
@@ -175,146 +237,68 @@ class MMGenListItem(MMGenObject):
 			raise AttributeError(m.format(name,type(self)))
 		return object.__setattr__(self,name,value)
 
-class InitErrors(object):
-
-	@staticmethod
-	def arg_chk(cls,on_fail):
-		assert on_fail in ('die','return','silent','raise'),'arg_chk in class {}'.format(cls.__name__)
-
-	@staticmethod
-	def init_fail(m,on_fail,silent=False):
-		if silent: m = ''
-		from mmgen.util import die,msg
-		if on_fail == 'die':      die(1,m)
-		elif on_fail == 'return':
-			if m: msg(m)
-			return None # TODO: change to False
-		elif on_fail == 'silent': return None # same here
-		elif on_fail == 'raise':  raise ValueError,m
-
 class AddrIdx(int,InitErrors):
-
 	max_digits = 7
-
 	def __new__(cls,num,on_fail='die'):
 		cls.arg_chk(cls,on_fail)
 		try:
-			assert type(num) is not float
+			assert type(num) is not float,'is float'
 			me = int.__new__(cls,num)
-		except:
-			m = "'%s': value cannot be converted to address index" % num
-		else:
-			if len(str(me)) > cls.max_digits:
-				m = "'%s': too many digits in addr idx" % num
-			elif me < 1:
-				m = "'%s': addr idx cannot be less than one" % num
-			else:
-				return me
-
-		return cls.init_fail(m,on_fail)
+			assert len(str(me)) <= cls.max_digits,'is more than {} digits'.format(cls.max_digits)
+			assert me > 0,'is less than one'
+			return me
+		except Exception as e:
+			m = "{!r}: value cannot be converted to address index ({})"
+			return cls.init_fail(m.format(num,e[0]),on_fail)
 
 class AddrIdxList(list,InitErrors,MMGenObject):
-
 	max_len = 1000000
-
 	def __init__(self,fmt_str=None,idx_list=None,on_fail='die',sep=','):
 		self.arg_chk(type(self),on_fail)
-		assert fmt_str or idx_list
-		if idx_list:
-			# dies on failure
-			return list.__init__(self,sorted(set(AddrIdx(i) for i in idx_list)))
-		elif fmt_str:
-			desc = fmt_str
-			ret,fs = [],"'%s': value cannot be converted to address index"
-			from mmgen.util import msg
-			for i in (fmt_str.split(sep)):
-				j = i.split('-')
-				if len(j) == 1:
-					idx = AddrIdx(i,on_fail='return')
-					if not idx: break
-					ret.append(idx)
-				elif len(j) == 2:
-					beg = AddrIdx(j[0],on_fail='return')
-					if not beg: break
-					end = AddrIdx(j[1],on_fail='return')
-					if not beg: break
-					if end < beg:
-						msg(fs % "%s-%s (invalid range)" % (beg,end)); break
-					ret.extend([AddrIdx(x) for x in range(beg,end+1)])
+		try:
+			if idx_list:
+				return list.__init__(self,sorted(set(AddrIdx(i,on_fail='raise') for i in idx_list)))
+			elif fmt_str:
+				ret = []
+				for i in (fmt_str.split(sep)):
+					j = i.split('-')
+					if len(j) == 1:
+						idx = AddrIdx(i,on_fail='raise')
+						if not idx: break
+						ret.append(idx)
+					elif len(j) == 2:
+						beg = AddrIdx(j[0],on_fail='raise')
+						if not beg: break
+						end = AddrIdx(j[1],on_fail='raise')
+						if not beg: break
+						if end < beg: break
+						ret.extend([AddrIdx(x,on_fail='raise') for x in range(beg,end+1)])
+					else: break
 				else:
-					msg((fs % i) + ' list'); break
-			else:
-				return list.__init__(self,sorted(set(ret))) # fell off end of loop - success
-
-			return self.init_fail((fs + ' list') % desc,on_fail)
-
-class Hilite(object):
-
-	color = 'red'
-	color_always = False
-	width = 0
-	trunc_ok = True
-
-	@classmethod
-	def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None,
-				center=False,nullrepl='',app='',appcolor=False):
-		if width == None: width = cls.width
-		if trunc_ok == None: trunc_ok = cls.trunc_ok
-		assert width > 0
-		if s == '' and nullrepl:
-			s,center = nullrepl,True
-		if center: s = s.center(width)
-		assert type(encl) is str and len(encl) in (0,2)
-		a,b = list(encl) if encl else ('','')
-		if trunc_ok and len(s) > width: s = s[:width]
-		if app:
-			return cls.colorize(a+s+b,color=color) + \
-					cls.colorize(app.ljust(width-len(a+s+b)),color=appcolor)
-		else:
-			return cls.colorize((a+s+b).ljust(width),color=color)
-
-	def fmt(self,*args,**kwargs):
-		assert args == () # forbid invocation w/o keywords
-		return self.fmtc(self,*args,**kwargs)
-
-	@classmethod
-	def hlc(cls,s,color=True):
-		return cls.colorize(s,color=color)
-
-	def hl(self,color=True):
-		return self.colorize(self,color=color)
-
-	def __str__(self):
-		return self.colorize(self,color=False)
-
-	@classmethod
-	def colorize(cls,s,color=True):
-		k = color if type(color) is str else cls.color # hack: override color with str value
-		return globals()[k](s) if (color or cls.color_always) else s
+					return list.__init__(self,sorted(set(ret))) # fell off end of loop - success
+				raise ValueError,"{!r}: invalid range".format(i)
+		except Exception as e:
+			m = "{!r}: value cannot be converted to AddrIdxList ({})"
+			return type(self).init_fail(m.format(idx_list or fmt_str,e[0]),on_fail)
 
 class BTCAmt(Decimal,Hilite,InitErrors):
 	color = 'yellow'
 	max_prec = 8
 	max_amt = 21000000
-
 	def __new__(cls,num,on_fail='die'):
+		if type(num) == cls: return num
 		cls.arg_chk(cls,on_fail)
 		try:
+			assert type(num) is not float,'number is floating-point'
+			assert type(num) is not long,'number is a long integer'
 			me = Decimal.__new__(cls,str(num))
-		except:
-			m = "'%s': value cannot be converted to decimal" % num
-		else:
-			if me.normalize().as_tuple()[-1] < -cls.max_prec:
-				from mmgen.globalvars import g
-				m = "'{}': too many decimal places in {} amount".format(num,g.coin)
-			elif me > cls.max_amt:
-				from mmgen.globalvars import g
-				m = "'{}': {} amount too large (>{})".format(num,g.coin,cls.max_amt)
-#			elif me.as_tuple()[0]:
-#				m = "'%s': BTC amount cannot be negative" % num
-			else:
-				return me
-		return cls.init_fail(m,on_fail)
+			assert me.normalize().as_tuple()[-1] >= -cls.max_prec,'too many decimal places in coin amount'
+			assert me <= cls.max_amt,'coin amount too large (>{})'.format(cls.max_amt)
+			assert me >= 0,'coin amount cannot be negative'
+			return me
+		except Exception as e:
+			m = "{!r}: value cannot be converted to BTCAmt ({})"
+			return cls.init_fail(m.format(num,e[0]),on_fail)
 
 	@classmethod
 	def fmtc(cls):
@@ -367,16 +351,21 @@ class BTCAddr(str,Hilite,InitErrors,MMGenObject):
 	color = 'cyan'
 	width = 35 # max len of testnet p2sh addr
 	def __new__(cls,s,on_fail='die'):
+		if type(s) == cls: return s
 		cls.arg_chk(cls,on_fail)
-		m = "'%s': value is not a Bitcoin address" % s
-		me = str.__new__(cls,s)
-		from mmgen.bitcoin import verify_addr,addr_pfxs
-		if type(s) in (str,unicode,BTCAddr):
-			me.addr_fmt = verify_addr(s,return_type=True)
-			me.testnet = s[0] in addr_pfxs['testnet']
-			if me.addr_fmt:
-				return me
-		return cls.init_fail(m,on_fail)
+		try:
+			assert set(s) <= set(ascii_letters+digits),'contains non-ascii characters'
+			me = str.__new__(cls,s)
+			from mmgen.bitcoin import verify_addr
+			va = verify_addr(s,return_dict=True)
+			assert va,'failed verification'
+			me.addr_fmt = va['format']
+			me.hex = va['hex']
+			me.testnet = va['net'] == 'testnet'
+			return me
+		except Exception as e:
+			m = "{!r}: value cannot be converted to Bitcoin address ({})"
+			return cls.init_fail(m.format(s,e[0]),on_fail)
 
 	@classmethod
 	def fmtc(cls,s,**kwargs):
@@ -390,13 +379,13 @@ class BTCAddr(str,Hilite,InitErrors,MMGenObject):
 
 	def is_for_current_chain(self):
 		from mmgen.globalvars import g
-		assert g.chain, 'global chain variable unset'
-		from bitcoin import addr_pfxs
-		return self[0] in addr_pfxs[g.chain]
+		assert g.chain,'global chain variable unset'
+		from bitcoin import btc_addr_pfxs
+		return self[0] in btc_addr_pfxs[g.chain]
 
 	def is_mainnet(self):
-		from bitcoin import addr_pfxs
-		return self[0] in addr_pfxs['mainnet']
+		from bitcoin import btc_addr_pfxs
+		return self[0] in btc_addr_pfxs['mainnet']
 
 	def is_in_tracking_wallet(self):
 		from mmgen.rpc import bitcoin_connection
@@ -408,122 +397,118 @@ class SeedID(str,Hilite,InitErrors):
 	width = 8
 	trunc_ok = False
 	def __new__(cls,seed=None,sid=None,on_fail='die'):
+		if type(sid) == cls: return sid
 		cls.arg_chk(cls,on_fail)
-		assert seed or sid
-		if seed:
-			from mmgen.seed import Seed
-			from mmgen.util import make_chksum_8
-			if type(seed) == Seed:
+		try:
+			if seed:
+				from mmgen.seed import Seed
+				assert type(seed) == Seed,'not a Seed instance'
+				from mmgen.util import make_chksum_8
 				return str.__new__(cls,make_chksum_8(seed.get_data()))
-		elif sid:
-			sid = str(sid)
-			from string import hexdigits
-			if len(sid) == cls.width and set(sid) <= set(hexdigits.upper()):
+			elif sid:
+				assert set(sid) <= set(hexdigits.upper()),'not uppercase hex digits'
+				assert len(sid) == cls.width,'not {} characters wide'.format(cls.width)
 				return str.__new__(cls,sid)
-
-		m = "'%s': value cannot be converted to SeedID" % str(seed or sid)
-		return cls.init_fail(m,on_fail)
+			raise ValueError,'no arguments provided'
+		except Exception as e:
+			m = "{!r}: value cannot be converted to SeedID ({})"
+			return cls.init_fail(m.format(seed or sid,e[0]),on_fail)
 
 class MMGenID(str,Hilite,InitErrors,MMGenObject):
-
 	color = 'orange'
 	width = 0
 	trunc_ok = False
-
 	def __new__(cls,s,on_fail='die'):
 		cls.arg_chk(cls,on_fail)
-		s = str(s)
 		try:
-			ss = s.split(':')
-			assert len(ss) in (2,3)
-			sid = SeedID(sid=ss[0],on_fail='silent')
-			assert sid
-			idx = AddrIdx(ss[-1],on_fail='silent')
-			assert idx
-			t = MMGenAddrType((MMGenAddrType.dfl_mmtype,ss[1])[len(ss) != 2],on_fail='silent')
-			assert t
-			me = str.__new__(cls,'{}:{}:{}'.format(sid,t,idx))
-			me.sid = sid
+			ss = str(s).split(':')
+			assert len(ss) in (2,3),'not 2 or 3 colon-separated items'
+			t = MMGenAddrType((ss[1],MMGenAddrType.dfl_mmtype)[len(ss)==2],on_fail='raise')
+			me = str.__new__(cls,'{}:{}:{}'.format(ss[0],t,ss[-1]))
+			me.sid = SeedID(sid=ss[0],on_fail='raise')
+			me.idx = AddrIdx(ss[-1],on_fail='raise')
 			me.mmtype = t
-			me.idx = idx
-			me.al_id = AddrListID(sid,me.mmtype) # key with colon!
-			assert me.al_id
-			me.sort_key = '{}:{}:{:0{w}}'.format(sid,t,idx,w=idx.max_digits)
+			me.al_id = str.__new__(AddrListID,me.sid+':'+me.mmtype) # checks already done
+			me.sort_key = '{}:{}:{:0{w}}'.format(me.sid,me.mmtype,me.idx,w=me.idx.max_digits)
 			return me
-		except:
-			m = "'%s': value cannot be converted to MMGenID" % s
-			return cls.init_fail(m,on_fail)
+		except Exception as e:
+			m = "{}\n{!r}: value cannot be converted to MMGenID"
+			return cls.init_fail(m.format(e[0],s),on_fail)
 
 class TwMMGenID(str,Hilite,InitErrors,MMGenObject):
-
 	color = 'orange'
 	width = 0
 	trunc_ok = False
-
 	def __new__(cls,s,on_fail='die'):
+		if type(s) == cls: return s
 		cls.arg_chk(cls,on_fail)
-		obj,sort_key = None,None
+		ret = None
 		try:
-			obj = MMGenID(s,on_fail='silent')
-			sort_key,t = obj.sort_key,'mmgen'
-		except:
+			ret = MMGenID(s,on_fail='raise')
+			sort_key,idtype = ret.sort_key,'mmgen'
+		except Exception as e:
 			try:
-				assert len(s) > 4 and s[:4] == 'btc:'
-				obj,sort_key,t = str(s),'z_'+s,'non-mmgen'
-			except:
-				pass
-
-		if obj and sort_key:
-			me = str.__new__(cls,obj)
-			me.obj = obj
-			me.sort_key = sort_key
-			me.type = t
-			return me
-
-		m = "'{}': value cannot be converted to {}".format(s,cls.__name__)
-		return cls.init_fail(m,on_fail)
+				assert s[:4] == 'btc:',"not a string beginning with the prefix 'btc:'"
+				assert set(s[4:]) <= set(ascii_letters+digits),'contains non-ascii characters'
+				assert len(s) > 4,'not more that four characters long'
+				ret,sort_key,idtype = str(s),'z_'+s,'non-mmgen'
+			except Exception as f:
+				m = "{}\nValue is {}\n{!r}: value cannot be converted to TwMMGenID"
+				return cls.init_fail(m.format(e[0],f[0],s),on_fail)
+
+		me = str.__new__(cls,ret)
+		me.obj = ret
+		me.sort_key = sort_key
+		me.type = idtype
+		return me
 
 # contains TwMMGenID,TwComment.  Not for display
 class TwLabel(str,InitErrors,MMGenObject):
-
 	def __new__(cls,s,on_fail='die'):
+		if type(s) == cls: return s
 		cls.arg_chk(cls,on_fail)
 		try:
 			ss = s.split(None,1)
-			me = str.__new__(cls,s)
-			me.mmid = TwMMGenID(ss[0],on_fail='silent')
-			assert me.mmid
-			me.comment = TwComment(ss[1] if len(ss) == 2 else '',on_fail='silent')
-			assert me.comment != None
+			mmid = TwMMGenID(ss[0],on_fail='raise')
+			comment = TwComment(ss[1] if len(ss) == 2 else '',on_fail='raise')
+			me = str.__new__(cls,'{}{}'.format(mmid,' {}'.format(comment) if comment else ''))
+			me.mmid = mmid
+			me.comment = comment
 			return me
-		except:
-			m = "'{}': value cannot be converted to {}".format(s,cls.__name__)
-			return cls.init_fail(m,on_fail)
+		except Exception as e:
+			m = u"{}\n{!r}: value cannot be converted to TwLabel"
+			return cls.init_fail(m.format(e[0],s),on_fail)
 
 class HexStr(str,Hilite,InitErrors):
 	color = 'red'
 	trunc_ok = False
 	def __new__(cls,s,on_fail='die',case='lower'):
+		if type(s) == cls: return s
 		assert case in ('upper','lower')
 		cls.arg_chk(cls,on_fail)
-		from string import hexdigits
-		if set(s) <= set(getattr(hexdigits,case)()) and not len(s) % 2:
+		try:
+			assert type(s) in (str,unicode,bytes),'not a string'
+			assert set(s) <= set(getattr(hexdigits,case)()),'not {}case hexadecimal symbols'.format(case)
+			assert not len(s) % 2,'odd-length string'
 			return str.__new__(cls,s)
-		m = "'{}': value cannot be converted to {}".format(s,cls.__name__)
-		return cls.init_fail(m,on_fail)
+		except Exception as e:
+			m = "{!r}: value cannot be converted to {} (value is {})"
+			return cls.init_fail(m.format(s,cls.__name__,e[0]),on_fail)
 
-class MMGenTxID(str,Hilite,InitErrors):
+class MMGenTxID(HexStr,Hilite,InitErrors):
 	color = 'red'
 	width = 6
 	trunc_ok = False
 	hexcase = 'upper'
 	def __new__(cls,s,on_fail='die'):
 		cls.arg_chk(cls,on_fail)
-		from string import hexdigits
-		if len(s) == cls.width and set(s) <= set(getattr(hexdigits,cls.hexcase)()):
-			return str.__new__(cls,s)
-		m = "'{}': value cannot be converted to {}".format(s,cls.__name__)
-		return cls.init_fail(m,on_fail)
+		try:
+			ret = HexStr.__new__(cls,s,case=cls.hexcase,on_fail='raise')
+			assert len(s) == cls.width,'Value is not {} characters wide'.format(cls.width)
+			return ret
+		except Exception as e:
+			m = "{}\n{!r}: value cannot be converted to {}"
+			return cls.init_fail(m.format(e[0],s,cls.__name__),on_fail)
 
 class BitcoinTxID(MMGenTxID):
 	color = 'purple'
@@ -533,34 +518,29 @@ class BitcoinTxID(MMGenTxID):
 class WifKey(str,Hilite,InitErrors):
 	width = 53
 	color = 'blue'
-	desc = 'WIF key'
-	def __new__(cls,s,on_fail='die',errmsg=None):
-		cls.arg_chk(cls,on_fail)
-		from mmgen.bitcoin import wif2hex
-		if wif2hex(s):
-			me = str.__new__(cls,s)
-			return me
-		m = errmsg or "'{}': invalid value for {}".format(s,cls.desc)
-		return cls.init_fail(m,on_fail)
-
-class HexStr(str,Hilite,InitErrors):
-	color = 'red'
-	trunc_ok = False
-	def __new__(cls,s,on_fail='die',case='lower'):
-		assert case in ('upper','lower')
+	def __new__(cls,s,on_fail='die',testnet=None): # fall back to g.testnet
+		if type(s) == cls: return s
 		cls.arg_chk(cls,on_fail)
-		from string import hexdigits
-		if set(s) <= set(getattr(hexdigits,case)()) and not len(s) % 2:
-			return str.__new__(cls,s)
-		m = "'{}': value cannot be converted to {}".format(s,cls.__name__)
-		return cls.init_fail(m,on_fail)
+		try:
+			assert set(s) <= set(ascii_letters+digits),'not an ascii string'
+			from mmgen.bitcoin import wif2hex
+			if wif2hex(s,testnet=testnet):
+				return str.__new__(cls,s)
+			raise ValueError,'failed verification'
+		except Exception as e:
+			m = '{!r}: invalid value for WIF key ({})'.format(s,e[0])
+			return cls.init_fail(m,on_fail)
 
-class PubKey(HexStr,MMGenObject):
+class PubKey(HexStr,MMGenObject): # TODO: add some real checks
 	def __new__(cls,s,compressed,on_fail='die'):
-		assert type(compressed) == bool
-		me = HexStr.__new__(cls,s,case='lower')
-		me.compressed = compressed
-		return me
+		try:
+			assert type(compressed) == bool,"'compressed' must be of type bool"
+			me = HexStr.__new__(cls,s,case='lower',on_fail='raise')
+			me.compressed = compressed
+			return me
+		except Exception as e:
+			m = '{!r}: invalid value for pubkey ({})'.format(s,e[0])
+			return cls.init_fail(m,on_fail)
 
 class PrivKey(str,Hilite,InitErrors,MMGenObject):
 
@@ -571,104 +551,71 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject):
 	compressed = MMGenImmutableAttr('compressed',bool,typeconv=False)
 	wif        = MMGenImmutableAttr('wif',WifKey,typeconv=False)
 
-	def __new__(*args,**kwargs): # initialize with (priv_bin,compressed), WIF or self
-		cls = args[0]
-		assert set(kwargs) <= set(['on_fail','wif'])
-		on_fail = kwargs['on_fail'] if 'on_fail' in kwargs else 'die'
-		cls.arg_chk(cls,on_fail)
+	# initialize with (priv_bin,compressed), WIF or self
+	def __new__(cls,s=None,compressed=None,wif=None,on_fail='die',testnet=None): # default to g.testnet
 
-		if len(args) == 2:
-			assert type(args[1]) == cls
-			return args[1]
+		if type(s) == cls: return s
+		assert wif or (s and type(compressed) == bool),'Incorrect args for PrivKey()'
+		cls.arg_chk(cls,on_fail)
 
-		if 'wif' in kwargs:
-			assert len(args) == 1
+		if wif:
 			try:
-				from mmgen.bitcoin import wif2hex,wif_is_compressed # TODO: move these here
-				wif = WifKey(kwargs['wif'])
-				me = str.__new__(cls,wif2hex(wif))
-				me.compressed = wif_is_compressed(wif)
-				me.wif = wif
+				assert set(wif) <= set(ascii_letters+digits),'not an ascii string'
+				from mmgen.bitcoin import wif2hex
+				w2h = wif2hex(wif,testnet=testnet)
+				assert w2h,"wif2hex() failed for wif key {!r}".format(wif)
+				me = str.__new__(cls,w2h['hex'])
+				me.compressed = w2h['compressed']
+				me.testnet = w2h['testnet']
+				me.wif = str.__new__(WifKey,wif) # check has been done
 				return me
-			except:
-				fs = "Value '{}' cannot be converted to WIF key"
-				errmsg = fs.format(kwargs['wif'])
-				return cls.init_fail(errmsg,on_fail)
-
-		cls,s,compressed = args
+			except Exception as e:
+				fs = "Value {!r} cannot be converted to WIF key ({})"
+				return cls.init_fail(fs.format(wif,e[0]),on_fail)
 
 		try:
 			from binascii import hexlify
-			assert len(s) == cls.width / 2
+			assert len(s) == cls.width / 2,'Key length must be {}'.format(cls.width/2)
 			me = str.__new__(cls,hexlify(s))
 			me.compressed = compressed
-			me.wif = me.towif()
+			me.wif = me.towif(testnet=testnet)
+#			me.testnet = testnet # leave uninitialized for now
 			return me
-		except:
-			fs = "Key={}\nCompressed={}\nValue pair cannot be converted to {}"
-			errmsg = fs.format(repr(s),compressed,cls.__name__)
-			return cls.init_fail(errmsg,on_fail)
+		except Exception as e:
+			fs = "Key={}\nCompressed={}\nValue pair cannot be converted to PrivKey ({})"
+			return cls.init_fail(fs.format(repr(s),compressed,e[0]),on_fail)
 
-	def towif(self):
+	def towif(self,testnet=None):
 		from mmgen.bitcoin import hex2wif
-		return WifKey(hex2wif(self,compressed=self.compressed))
-
-class MMGenAddrType(str,Hilite,InitErrors):
-	width = 1
-	trunc_ok = False
-	color = 'blue'
-	mmtypes = {
-		# TODO 'L' is ambiguous: For user, it means MMGen legacy uncompressed address.
-		# For generator functions, 'L' means any p2pkh address, and 'S' any ps2h address
-		'L': 'legacy',
-		'S': 'segwit',
-# 		'l': 'litecoin',
-# 		'e': 'ethereum',
-# 		'E': 'ethereum_classic',
-# 		'm': 'monero',
-# 		'z': 'zcash',
-	}
-	dfl_mmtype = 'L'
-	def __new__(cls,s,on_fail='die',errmsg=None):
-		cls.arg_chk(cls,on_fail)
-		for k,v in cls.mmtypes.items():
-			if s in (k,v):
-				if s == v: s = k
-				me = str.__new__(cls,s)
-				me.name = cls.mmtypes[s]
-				return me
-		m = errmsg or "'{}': invalid value for {}".format(s,cls.__name__)
-		return cls.init_fail(m,on_fail)
-
-class MMGenPasswordType(MMGenAddrType):
-	mmtypes = { 'P': 'password' }
+		return WifKey(hex2wif(self,compressed=self.compressed),on_fail='raise',testnet=testnet)
 
-class AddrListID(str,Hilite,InitErrors):
+class AddrListID(str,Hilite,InitErrors,MMGenObject):
 	width = 10
 	trunc_ok = False
 	color = 'yellow'
 	def __new__(cls,sid,mmtype,on_fail='die'):
 		cls.arg_chk(cls,on_fail)
-		m = "'{}': not a SeedID. Cannot create {}".format(sid,cls.__name__)
-		if type(sid) == SeedID:
-			m = "'{}': not an MMGenAddrType object. Cannot create {}".format(mmtype,cls.__name__)
-			if type(mmtype) in (MMGenAddrType,MMGenPasswordType):
-				me = str.__new__(cls,sid+':'+mmtype) # colon in key is OK
-				me.sid = sid
-				me.mmtype = mmtype
-				return me
-		return cls.init_fail(m,on_fail)
+		try:
+			assert type(sid) == SeedID,"{!r} not a SeedID instance".format(sid)
+			t = MMGenAddrType,MMGenPasswordType
+			assert type(mmtype) in t,"{!r} not an instance of {}".format(mmtype,','.join([i.__name__ for i in t]))
+			me = str.__new__(cls,sid+':'+mmtype)
+			me.sid = sid
+			me.mmtype = mmtype
+			return me
+		except Exception as e:
+			m = "Cannot create AddrListID ({})".format(e[0])
+			return cls.init_fail(m,on_fail)
 
 class MMGenLabel(unicode,Hilite,InitErrors):
-
 	color = 'pink'
 	allowed = []
 	forbidden = []
 	max_len = 0
 	min_len = 0
 	desc = 'label'
-
 	def __new__(cls,s,on_fail='die',msg=None):
+		if type(s) == cls: return s
 		cls.arg_chk(cls,on_fail)
 		for k in cls.forbidden,cls.allowed:
 			assert type(k) == list
@@ -677,23 +624,17 @@ class MMGenLabel(unicode,Hilite,InitErrors):
 			s = s.strip()
 			if type(s) != unicode:
 				s = s.decode('utf8')
-		except:
-			m = "'%s': value is not a valid UTF-8 string" % s
-		else:
 			from mmgen.util import capfirst
-			if len(s) > cls.max_len:
-				m = u"'{}': {} too long (>{} symbols)".format(s,capfirst(cls.desc),cls.max_len)
-			elif len(s) < cls.min_len:
-				m = u"'{}': {} too short (<{} symbols)".format(s,capfirst(cls.desc),cls.min_len)
-			elif cls.allowed and not set(list(s)).issubset(set(cls.allowed)):
-				m = u"{} '{}' contains non-allowed symbols: {}".format(capfirst(cls.desc),s,
-					' '.join(set(list(s)) - set(cls.allowed)))
-			elif cls.forbidden and any(ch in s for ch in cls.forbidden):
-				m = u"{} '{}' contains one of these forbidden symbols: '{}'".format(capfirst(cls.desc),s,
-					"', '".join(cls.forbidden))
-			else:
-				return unicode.__new__(cls,s)
-		return cls.init_fail((msg+'\n' if msg else '') + m,on_fail)
+			assert len(s) <= cls.max_len, 'too long (>{} symbols)'.format(cls.max_len)
+			assert len(s) >= cls.min_len, 'too short (<{} symbols)'.format(cls.min_len)
+			assert not cls.allowed or set(list(s)).issubset(set(cls.allowed)),\
+				u'contains non-allowed symbols: {}'.format(' '.join(set(list(s)) - set(cls.allowed)))
+			assert not cls.forbidden or not any(ch in s for ch in cls.forbidden),\
+				u"contains one of these forbidden symbols: '{}'".format("', '".join(cls.forbidden))
+			return unicode.__new__(cls,s)
+		except Exception as e:
+			m = u"{!r}: value cannot be converted to {} ({})"
+			return cls.init_fail(m.format(s,cls.__name__,e),on_fail)
 
 class MMGenWalletLabel(MMGenLabel):
 	max_len = 48
@@ -715,4 +656,41 @@ class MMGenPWIDString(MMGenLabel):
 	desc = 'password ID string'
 	forbidden = list(u' :/\\')
 
-class AddrListList(list,MMGenObject): pass
+class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
+	width = 1
+	trunc_ok = False
+	color = 'blue'
+	mmtypes = { # since 'name' is used to cook the seed, it must never change!
+'L': {'name':'legacy','comp':False,'gen':'p2pkh', 'fmt':'p2pkh','desc':'Legacy uncompressed Bitcoin address'},
+'S': {'name':'segwit','comp':True, 'gen':'segwit','fmt':'p2sh', 'desc':'Bitcoin Segwit P2SH-P2WPK address' },
+'C': {'name':'compressed','comp':True,'gen':'p2pkh','fmt':'p2pkh','desc':'Compressed Bitcoin P2PKH address'}
+# 		'l': 'litecoin',
+# 		'e': 'ethereum',
+# 		'E': 'ethereum_classic',
+# 		'm': 'monero',
+# 		'z': 'zcash',
+	}
+	dfl_mmtype = 'L'
+	def __new__(cls,s,on_fail='die',errmsg=None):
+		if type(s) == cls: return s
+		cls.arg_chk(cls,on_fail)
+		try:
+			for k,v in cls.mmtypes.items():
+				if s in (k,v['name']):
+					if s == v['name']: s = k
+					me = str.__new__(cls,s)
+					me.name = v['name']
+					me.compressed = v['comp']
+					me.gen_method = v['gen']
+					me.desc = v['desc']
+					me.addr_fmt = v['fmt']
+					return me
+			raise ValueError,'not found'
+		except Exception as e:
+			m = errmsg or '{!r}: invalid value for {} ({})'.format(s,cls.__name__,e[0])
+			return cls.init_fail(m,on_fail)
+
+class MMGenPasswordType(MMGenAddrType):
+	mmtypes = {
+		'P': {'name':'password','comp':False,'gen':None,'fmt':None,'desc':'Password generated from MMGen seed'}
+	}

+ 12 - 0
mmgen/opts.py

@@ -61,6 +61,8 @@ common_opts_data = """
 --, --testnet=0|1         Disable or enable testnet
 --, --skip-cfg-file       Skip reading the configuration file
 --, --version             Print version information and exit
+--, --bob                 Switch to user "Bob" in MMGen regtest setup
+--, --alice               Switch to user "Alice" in MMGen regtest setup
 """.format(
 	pnm=g.proj_name,
 	cu_dfl=g.coin,
@@ -250,6 +252,16 @@ def init(opts_f,add_opts=[],opt_filter=None):
 	for k in ('prog_name','desc','usage','options','notes'):
 		if k in opts_data: del opts_data[k]
 
+	if g.bob or g.alice:
+		import regtest as rt
+		rt.user(('alice','bob')[g.bob],quiet=True)
+		g.testnet = True
+		g.rpc_host = 'localhost'
+		g.rpc_port = rt.rpc_port
+		g.rpc_user = rt.rpc_user
+		g.rpc_password = rt.rpc_password
+		g.data_dir = os.path.join(g.home_dir,'.'+g.proj_name.lower(),'regtest')
+
 	if g.debug: opt_postproc_debug()
 
 	return args

+ 64 - 46
mmgen/tool.py

@@ -27,6 +27,7 @@ import mmgen.bitcoin as mmb
 from mmgen.common import *
 from mmgen.crypto import *
 from mmgen.tx import *
+from mmgen.addr import *
 
 pnm = g.proj_name
 
@@ -78,7 +79,7 @@ cmd_data = OrderedDict([
 
 	('Listaddress',['<{} address> [str]'.format(pnm),'minconf [int=1]','pager [bool=False]','showempty [bool=True]''showbtcaddr [bool=True]']),
 	('Listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']),
-	('Getbalance',   ['minconf [int=1]']),
+	('Getbalance',   ['minconf [int=1]','quiet [bool=False]']),
 	('Txview',       ['<{} TX file(s)> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]',"sort [str='mtime'] (options: 'ctime','atime')",'MARGS']),
 	('Twview',       ["sort [str='age']",'reverse [bool=False]','show_days [bool=True]','show_mmid [bool=True]','minconf [int=1]','wide [bool=False]','pager [bool=False]']),
 
@@ -308,6 +309,8 @@ def print_convert_results(indata,enc,dec,dtype):
 	if error:
 		die(3,"Error! Recoded data doesn't match input!")
 
+kg = KeyGenerator()
+
 def Hexdump(infile, cols=8, line_nums=True):
 	Msg(pretty_hexdump(
 			get_data_from_file(infile,dash=True,silent=True,binary=True),
@@ -330,41 +333,37 @@ def Randhex(nbytes='32'):
 	Msg(ba.hexlify(get_random(int(nbytes))))
 
 def Randwif(compressed=False):
-	r_hex = ba.hexlify(get_random(32))
-	enc = mmb.hex2wif(r_hex,compressed)
-	dec = wif2hex(enc)
-	print_convert_results(r_hex,enc,dec,'hex')
+	Msg(PrivKey(get_random(32),compressed).wif)
 
 def Randpair(compressed=False,segwit=False):
 	if segwit: compressed = True
-	r_hex = ba.hexlify(get_random(32))
-	wif = mmb.hex2wif(r_hex,compressed)
-	addr = mmb.privnum2addr(int(r_hex,16),compressed,segwit=segwit)
-	Vmsg('Key (hex):  %s' % r_hex)
-	Vmsg_r('Key (WIF):  '); Msg(wif)
+	ag = AddrGenerator(('p2pkh','segwit')[bool(segwit)])
+	privhex = PrivKey(get_random(32),compressed)
+	addr = ag.to_addr(kg.to_pubhex(privhex))
+	Vmsg('Key (hex):  %s' % privhex)
+	Vmsg_r('Key (WIF):  '); Msg(privhex.wif)
 	Vmsg_r('Addr:       '); Msg(addr)
 
 def Wif2addr(wif,segwit=False):
-	compressed = mmb.wif_is_compressed(wif)
-	if segwit and not compressed:
-		die(1,'Segwit address cannot be generated from uncompressed WIF')
-	privhex = wif2hex(wif)
-	addr = mmb.privnum2addr(int(privhex,16),compressed,segwit=segwit)
+	privhex = PrivKey(wif=wif)
+	if segwit and not privhex.compressed:
+		die(2,'Segwit addresses must use compressed public keys')
+	ag = AddrGenerator(('p2pkh','segwit')[bool(segwit)])
+	addr = ag.to_addr(kg.to_pubhex(privhex))
 	Vmsg_r('Addr: '); Msg(addr)
 
 def Wif2segwit_pair(wif):
-	if not mmb.wif_is_compressed(wif):
+	privhex = PrivKey(wif=wif)
+	if not privhex.compressed:
 		die(1,'Segwit address cannot be generated from uncompressed WIF')
-	privhex = wif2hex(wif)
-	pubhex = mmb.privnum2pubhex(int(privhex,16),compressed=True)
-	rs = mmb.pubhex2redeem_script(pubhex)
-	addr = mmb.hexaddr2addr(mmb.hash160(rs),p2sh=True)
-	addr_chk = mmb.privnum2addr(int(privhex,16),compressed=True,segwit=True)
-	assert addr == addr_chk
+	ag = AddrGenerator('segwit')
+	pubhex = kg.to_pubhex(privhex)
+	addr = ag.to_addr(pubhex)
+	rs = ag.to_segwit_redeem_script(pubhex)
 	Msg('{}\n{}'.format(rs,addr))
 
 def Hexaddr2addr(hexaddr):                     Msg(mmb.hexaddr2addr(hexaddr))
-def Addr2hexaddr(addr):                        Msg(mmb.verify_addr(addr,return_hex=True))
+def Addr2hexaddr(addr):                        Msg(mmb.verify_addr(addr,return_dict=True)['hex'])
 def Hash160(pubkeyhex):                        Msg(mmb.hash160(pubkeyhex))
 def Pubhex2addr(pubkeyhex,p2sh=False):         Msg(mmb.hexaddr2addr(mmb.hash160(pubkeyhex),p2sh=p2sh))
 def Wif2hex(wif):                              Msg(wif2hex(wif))
@@ -379,13 +378,14 @@ def Privhex2pubhex(privhex,compressed=False): # new
 def Pubhex2redeem_script(pubhex): # new
 	Msg(mmb.pubhex2redeem_script(pubhex))
 def Wif2redeem_script(wif): # new
-	if not mmb.wif_is_compressed(wif):
-		die(1,'Witness redeem script cannot be generated from uncompressed WIF')
-	pubhex = mmb.privnum2pubhex(int(wif2hex(wif),16),compressed=True)
-	Msg(mmb.pubhex2redeem_script(pubhex))
+	privhex = PrivKey(wif=wif)
+	if not privhex.compressed:
+		die(1,'Segwit redeem script cannot be generated from uncompressed WIF')
+	ag = AddrGenerator('segwit')
+	Msg(ag.to_segwit_redeem_script(kg.to_pubhex(privhex)))
 
 def wif2hex(wif): # wrapper
-	ret = mmb.wif2hex(wif)
+	ret = PrivKey(wif=wif)
 	return ret or die(1,'{}: Invalid WIF'.format(wif))
 
 wordlists = 'electrum','tirosh'
@@ -452,7 +452,7 @@ def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 	"""
 		m_prev = None
 
-		for m in sorted([l.mmid for l in accts]):
+		for m in sorted(b.mmid for b in [a for a in accts if a]):
 			if m == m_prev:
 				msg('Duplicate MMGen ID ({}) discovered in tracking wallet!\n'.format(m))
 				bad_accts = MMGenList([l for l in accts if l.mmid == m])
@@ -464,6 +464,18 @@ def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 				die(3,red('Exiting on error'))
 			m_prev = m
 
+	def check_addr_array_lens(acct_pairs):
+		err = False
+		for label,addrs in acct_pairs:
+			if not label: continue
+			if len(addrs) != 1:
+				err = True
+				if len(addrs) == 0:
+					msg("Label '{}': has no associated address!".format(label))
+				else:
+					msg("'{}': more than one {} address in account!".format(addrs,g.coin))
+		if err: rdie(3,'Tracking wallet is corrupted!')
+
 	usr_addr_list = []
 	if addrs:
 		a = addrs.rsplit(':',1)
@@ -494,20 +506,22 @@ def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 
 	# We use listaccounts only for empty addresses, as it shows false positive balances
 	if showempty:
-		# args: minconf,watchonly
-		accts = MMGenList([b for b in [TwLabel(a,on_fail='silent') for a in c.listaccounts(0,True)] if b])
-		check_dup_mmid(accts)
-		acct_addrs = c.getaddressesbyaccount([[a] for a in accts],batch=True)
-		assert len(accts) == len(acct_addrs), 'listaccounts() and getaddressesbyaccount() not of same length'
-		for a in acct_addrs:
-			if len(a) != 1:
-				die(2,"'{}': more than one {} address in account!".format(a,g.coin))
-		for label,addr in zip(accts,[b[0] for b in acct_addrs]):
+		# for compatibility with old mmids, must use raw RPC rather than native data for matching
+		# args: minconf,watchonly, MUST use keys() so we get list, not dict
+		acct_list = c.listaccounts(0,True).keys() # raw list, no 'L'
+		acct_labels = MMGenList([TwLabel(a,on_fail='silent') for a in acct_list])
+		check_dup_mmid(acct_labels)
+		acct_addrs = c.getaddressesbyaccount([[a] for a in acct_list],batch=True) # use raw list here
+		assert len(acct_list) == len(acct_addrs), 'listaccounts() and getaddressesbyaccount() not equal in length'
+		addr_pairs = zip(acct_labels,acct_addrs)
+		check_addr_array_lens(addr_pairs)
+		for label,addr_arr in addr_pairs:
+			if not label: continue
 			if usr_addr_list and (label.mmid not in usr_addr_list): continue
 			if label.mmid not in addrs:
 				addrs[label.mmid] = { 'amt':BTCAmt('0'), 'lbl':label, 'addr':'' }
 				if showbtcaddrs:
-					addrs[label.mmid]['addr'] = BTCAddr(addr)
+					addrs[label.mmid]['addr'] = BTCAddr(addr_arr[0])
 
 	if not addrs:
 		die(0,('No tracked addresses with balances!','No tracked addresses!')[showempty])
@@ -547,7 +561,7 @@ def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 	o = '\n'.join(out)
 	return do_pager(o) if pager else Msg(o)
 
-def Getbalance(minconf=1):
+def Getbalance(minconf=1,quiet=False):
 	accts = {}
 	for d in bitcoin_connection().listunspent(0):
 		ma = split2(d['account'] if 'account' in d else '')[0] # include coinbase outputs if spendable
@@ -562,12 +576,16 @@ def Getbalance(minconf=1):
 			for j in ([],[0])[confs==0] + [i]:
 				accts[key][j] += d['amount']
 
-	fs = '{:13} {} {} {}'
-	mc,lbl = str(minconf),'confirms'
-	Msg(fs.format('Wallet',
-		*[s.ljust(16) for s in ' Unconfirmed',' <%s %s'%(mc,lbl),' >=%s %s'%(mc,lbl)]))
-	for key in sorted(accts.keys()):
-		Msg(fs.format(key+':', *[a.fmt(color=True,suf=' '+g.coin) for a in accts[key]]))
+	if quiet:
+		Msg('{}'.format(accts['TOTAL'][2]))
+	else:
+		fs = '{:13} {} {} {}'
+		mc,lbl = str(minconf),'confirms'
+		Msg(fs.format('Wallet',
+			*[s.ljust(16) for s in ' Unconfirmed',' <%s %s'%(mc,lbl),' >=%s %s'%(mc,lbl)]))
+		for key in sorted(accts.keys()):
+			Msg(fs.format(key+':', *[a.fmt(color=True,suf=' '+g.coin) for a in accts[key]]))
+
 	if 'SPENDABLE' in accts:
 		Msg(red('Warning: this wallet contains PRIVATE KEYS for the SPENDABLE balance!'))
 

+ 8 - 2
mmgen/tx.py

@@ -457,6 +457,8 @@ class MMGenTX(MMGenObject):
 		msg_r('Signing transaction{}...'.format(tx_num_str))
 		ht = ('ALL','ALL|FORKID')[g.coin=='BCH'] # sighashtype defaults to 'ALL'
 		wifs = [d.sec.wif for d in keys]
+#		keys.pmsg()
+#		pmsg(wifs)
 		ret = c.signrawtransaction(self.hex,sig_data,wifs,ht,on_fail='return')
 
 		from mmgen.rpc import rpc_error,rpc_errmsg
@@ -525,7 +527,10 @@ class MMGenTX(MMGenObject):
 
 	def is_in_wallet(self,c):
 		ret = c.gettransaction(self.btc_txid,on_fail='silent')
-		return 'confirmations' in ret and ret['confirmations'] > 0
+		if 'confirmations' in ret and ret['confirmations'] > 0:
+			return ret['confirmations']
+		else:
+			return False
 
 	def is_replaced(self,c):
 		if self.is_in_mempool(c): return False
@@ -541,7 +546,8 @@ class MMGenTX(MMGenObject):
 		if self.is_in_mempool(c):
 			msg(('Warning: transaction is in mempool!','Transaction is in mempool')[status])
 		elif self.is_in_wallet(c):
-			die(1,'Transaction has been confirmed{}'.format('' if status else '!'))
+			confs = self.is_in_wallet(c)
+			die(0,'Transaction has {} confirmation{}'.format(confs,suf(confs,'s')))
 		elif self.is_in_utxos(c):
 			die(2,red('ERROR: transaction is in the blockchain (but not in the tracking wallet)!'))
 		ret = self.is_replaced(c) # 1: replacement in mempool, 2: replacement confirmed

+ 6 - 2
mmgen/txsign.py

@@ -138,8 +138,12 @@ def get_seed_files(opt,args):
 	# favor unencrypted seed sources first, as they don't require passwords
 	u,e = SeedSourceUnenc,SeedSourceEnc
 	ret = _pop_and_return(args,u.get_extensions())
-	from mmgen.filename import find_file_in_dir
-	wf = find_file_in_dir(Wallet,g.data_dir) # Make this the first encrypted ss in the list
+	from mmgen.filename import find_file_in_dir,find_files_in_dir
+	if g.bob or g.alice:
+		import regtest as rt
+		wf = rt.mmwords[('alice','bob')[g.bob]]
+	else:
+		wf = find_file_in_dir(Wallet,g.data_dir) # Make this the first encrypted ss in the list
 	if wf: ret.append(wf)
 	ret += _pop_and_return(args,e.get_extensions())
 	if not (ret or opt.mmgen_keys_from_file or opt.keys_from_file): # or opt.use_wallet_dat

+ 13 - 2
mmgen/util.py

@@ -31,6 +31,10 @@ def msg_r(s):  sys.stderr.write(s.encode('utf8'))
 def Msg(s):    sys.stdout.write(s.encode('utf8') + '\n')
 def Msg_r(s):  sys.stdout.write(s.encode('utf8'))
 def msgred(s): msg(red(s))
+def ymsg(s):   msg(yellow(s))
+def ymsg_r(s): msg_r(yellow(s))
+def gmsg(s):   msg(green(s))
+def gmsg_r(s): msg_r(green(s))
 
 def mmsg(*args):
 	for d in args: Msg(repr(d))
@@ -464,7 +468,11 @@ def make_full_path(outdir,outfile):
 def get_seed_file(cmd_args,nargs,invoked_as=None):
 	from mmgen.filename import find_file_in_dir
 	from mmgen.seed import Wallet
-	wf = find_file_in_dir(Wallet,g.data_dir)
+	if g.bob or g.alice:
+		import regtest as rt
+		wf = rt.mmwords[('alice','bob')[g.bob]]
+	else:
+		wf = find_file_in_dir(Wallet,g.data_dir)
 
 	wd_from_opt = bool(opt.hidden_incog_input_params or opt.in_fmt) # have wallet data from opt?
 
@@ -800,9 +808,12 @@ def get_bitcoind_auth_cookie():
 def bitcoin_connection():
 
 	def	check_coin_mismatch(c):
+		if c.getblockcount() == 0:
+			msg('Warning: no blockchain, so skipping block mismatch check')
+			return
 		fb = '00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148'
 		err = []
-		if int(c.getblockchaininfo()['blocks']) <= 478558 or c.getblockhash(478559) == fb:
+		if c.getblockchaininfo()['blocks'] <= 478558 or c.getblockhash(478559) == fb:
 			if g.coin == 'BCH': err = 'BCH','BTC'
 		elif g.coin == 'BTC': err = 'BTC','BCH'
 		if err: wdie(2,"'{}' requested, but this is the {} chain!".format(*err))

+ 4 - 1
scripts/traceback.py

@@ -10,7 +10,10 @@ try:
 	sys.argv.pop(0)
 	execfile(sys.argv[0])
 except SystemExit:
-	sys.exit(int(str(sys.exc_info()[1])))
+	try:
+		sys.exit(int(str(sys.exc_info()[1])))
+	except:
+		sys.exit(1)
 except:
 	l = traceback.format_exception(*sys.exc_info())
 	exc = l.pop()

+ 3 - 0
setup.py

@@ -120,6 +120,7 @@ setup(
 			'mmgen.mn_tirosh',
 			'mmgen.obj',
 			'mmgen.opts',
+			'mmgen.regtest',
 			'mmgen.rpc',
 			'mmgen.seed',
 			'mmgen.term',
@@ -134,6 +135,7 @@ setup(
 			'mmgen.main_addrgen',
 			'mmgen.main_passgen',
 			'mmgen.main_addrimport',
+			'mmgen.main_regtest',
 			'mmgen.main_txcreate',
 			'mmgen.main_txbump',
 			'mmgen.main_txsign',
@@ -152,6 +154,7 @@ setup(
 			'mmgen-passgen',
 			'mmgen-addrimport',
 			'mmgen-passchg',
+			'mmgen-regtest',
 			'mmgen-walletchk',
 			'mmgen-walletconv',
 			'mmgen-walletgen',

+ 0 - 3
test/gentest.py

@@ -29,7 +29,6 @@ from binascii import hexlify
 
 # Import these _after_ local path's been added to sys.path
 from mmgen.common import *
-from mmgen.bitcoin import hex2wif
 
 rounds = 100
 opts_data = lambda: {
@@ -115,7 +114,6 @@ def match_error(sec,wif,a_addr,b_addr,a,b):
 """.format(sec,wif,a_addr,b_addr,pnm=g.proj_name,a=m[a],b=m[b]).rstrip())
 
 # Begin execution
-mmtype = ('L','S')[bool(opt.segwit)]
 compressed = True
 
 from mmgen.addr import KeyGenerator,AddrGenerator
@@ -177,7 +175,6 @@ elif a and dump:
 			sec = PrivKey(wif=wif)
 		except:
 			die(2,'\nInvalid {}net WIF address in dump file: {}'.format(('main','test')[g.testnet],wif))
-		compressed = wif[0] != ('5','9')[g.testnet]
 		b_addr = ag.to_addr(kg.to_pubhex(sec))
 		if a_addr != b_addr:
 			match_error(sec,wif,a_addr,b_addr,3,a)

+ 249 - 0
test/objtest.py

@@ -0,0 +1,249 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+test/objtest.py:  Test MMGen data objects
+"""
+
+import sys,os
+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 binascii import hexlify
+
+# Import these _after_ local path's been added to sys.path
+from mmgen.common import *
+from mmgen.obj import *
+from mmgen.seed import *
+
+opts_data = lambda: {
+	'desc': 'Test MMGen data objects',
+	'sets': ( ('super_silent', True, 'silent', True), ),
+	'usage':'[options] [object]',
+	'options': """
+-h, --help         Print this help message
+--, --longhelp     Print help message for long options (common options)
+-q, --quiet        Produce quieter output
+-s, --silent       Silence output of tested objects
+-S, --super-silent Silence all output except for errors
+-v, --verbose      Produce more verbose output
+"""
+}
+
+cmd_args = opts.init(opts_data)
+
+def run_test(test,arg,input_data):
+	arg_copy = arg
+	kwargs = {'on_fail':'silent'} if opt.silent else {}
+	ret_chk = arg
+	if input_data == 'good' and type(arg) == tuple: arg,ret_chk = arg
+	if type(arg) == dict: # pass one arg + kwargs to constructor
+		arg_copy = arg.copy()
+		if 'arg' in arg:
+			args = [arg['arg']]
+			ret_chk = args[0]
+			del arg['arg']
+		else:
+			args = []
+			ret_chk = arg.values()[0] # assume only one key present
+		if 'ret' in arg:
+			ret_chk = arg['ret']
+			del arg['ret']
+			del arg_copy['ret']
+		kwargs.update(arg)
+	else:
+		args = [arg]
+	try:
+		if not opt.super_silent:
+			msg_r((orange,green)[input_data=='good']('{:<22}'.format(repr(arg_copy)+':')))
+		cls = globals()[test]
+		ret = cls(*args,**kwargs)
+		bad_ret = list() if issubclass(cls,list) else None
+		if (opt.silent and input_data=='bad' and ret!=bad_ret) or (not opt.silent and input_data=='bad'):
+			raise UserWarning,"Non-'None' return value {} with bad input data".format(repr(ret))
+		if opt.silent and input_data=='good' and ret==bad_ret:
+			raise UserWarning,"'None' returned with good input data"
+		if input_data=='good' and ret != ret_chk and repr(ret) != repr(ret_chk):
+			raise UserWarning,"Return value ({!r}) doesn't match expected value ({!r})".format(ret,ret_chk)
+		if not opt.super_silent:
+			msg(u'==> {}'.format(ret))
+		if opt.verbose and issubclass(cls,MMGenObject):
+			ret.pmsg()
+	except SystemExit as e:
+		if input_data == 'good':
+			raise ValueError,'Error on good input data'
+		if opt.verbose:
+			msg('exitval: {}'.format(e[0]))
+	except UserWarning as e:
+		msg('==> {!r}'.format(ret))
+		die(2,red('{}'.format(e[0])))
+
+r32,r24,r16,r17,r18 = os.urandom(32),os.urandom(24),os.urandom(16),os.urandom(17),os.urandom(18)
+
+from collections import OrderedDict
+tests = OrderedDict([
+	('AddrIdx', {
+		'bad':  ('s',1.1,12345678,-1),
+		'good': (('7',7),)
+		}),
+	('AddrIdxList', {
+		'bad':  ('x','5,9,1-2-3','8,-11','66,3-2'),
+		'good': (
+			('3,2,2',[2,3]),
+			('101,1,3,5,2-7,99',[1,2,3,4,5,6,7,99,101]),
+			({'idx_list':AddrIdxList('1-5')},[1,2,3,4,5])
+		)}),
+	('BTCAmt', {
+		'bad':  ('-3.2','0.123456789',123L,'123L',22000000,20999999.12345678),
+		'good': (('20999999.12345678',Decimal('20999999.12345678')),)
+		}),
+	('BTCAddr', {
+		'bad':  (1,'x','я'),
+		'good': (
+			'1MjjELEy6EJwk8fSNfpS8b5teFRo4X5fZr',
+			'32GiSWo9zJQgkCmjAaLRrbPwXhKry2jHhj',
+			'n2FgXPKwuFkCXF946EnoxWJDWF2VwQ6q8J',
+			'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'
+	)}),
+	('SeedID', {
+		'bad':  (
+			{'sid':'я'},
+			{'sid':'F00F00'},
+			{'sid':'xF00F00x'},
+			{'sid':1},
+			{'sid':'F00BAA123'},
+			{'sid':'f00baa12'},
+			'я',r32,'abc'),
+		'good': (({'sid':'F00BAA12'},'F00BAA12'),(Seed(r16),Seed(r16).sid))
+	}),
+	('MMGenID', {
+		'bad':  ('x',1,'f00f00f','a:b','x:L:3','F00BAA12:0','F00BAA12:Z:99'),
+		'good': (('F00BAA12:99','F00BAA12:L:99'),'F00BAA12:L:99','F00BAA12:S:99')
+	}),
+	('TwMMGenID', {
+		'bad':  ('x','я','я:я',1,'f00f00f','a:b','x:L:3','F00BAA12:0','F00BAA12:Z:99','btc:','btc:я'),
+		'good': (('F00BAA12:99','F00BAA12:L:99'),'F00BAA12:L:99','F00BAA12:S:9999999','btc:x')
+	}),
+	('TwComment', {
+		'bad':  ('я',"comment too long for tracking wallet",),
+		'good': ('OK comment',)
+	}),
+	('TwLabel', {
+		'bad':  ('x x','x я','я:я',1,'f00f00f','a:b','x:L:3','F00BAA12:0 x',
+				'F00BAA12:Z:99','F00BAA12:L:99 я','btc: x','btc:я x'),
+		'good': (
+			('F00BAA12:99 a comment','F00BAA12:L:99 a comment'),
+			'F00BAA12:L:99 comment',
+			'F00BAA12:S:9999999 comment',
+			'btc:x comment')
+	}),
+	('HexStr', {
+		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00'),
+		'good': ('deadbeef','f00baa12')
+	}),
+	('MMGenTxID', {
+		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00','F00F0012'),
+		'good': ('DEADBE','F00BAA')
+	}),
+	('BitcoinTxID',{
+		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00','F00F0012',hexlify(r16),hexlify(r32)+'ee'),
+		'good': (hexlify(r32),)
+	}),
+	('WifKey', {
+		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00',hexlify(r16),'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'),
+		'good': (
+			'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb',
+			'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk',
+			{'arg':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6','testnet':True},
+			{'arg':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR','testnet':True}
+		)
+	}),
+	('PubKey', {
+		'bad':  ({'arg':1,'compressed':False},{'arg':'F00BAA12','compressed':False},),
+		'good': ({'arg':'deadbeef','compressed':True},) # TODO: add real pubkeys
+	}),
+	('PrivKey', {
+		'bad':  ({'wif':1},),
+		'good': (
+			{'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb',
+			 'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'},
+			{'wif':'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk',
+			 'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'},
+			{'wif':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6','testnet':True,
+			 'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'},
+			{'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR','testnet':True,
+			'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'},
+			{'s':r32,'compressed':False,'ret':hexlify(r32)},
+			{'s':r32,'compressed':True,'ret':hexlify(r32)}
+		)
+	}),
+	('AddrListID', { # a rather pointless test, but do it anyway
+		'bad':  (
+			{'sid':SeedID(sid='F00BAA12'),'mmtype':'Z','ret':'F00BAA12:Z'},
+		),
+		'good':  (
+			{'sid':SeedID(sid='F00BAA12'),'mmtype':MMGenAddrType('S'),'ret':'F00BAA12:S'},
+			{'sid':SeedID(sid='F00BAA12'),'mmtype':MMGenAddrType('L'),'ret':'F00BAA12:L'},
+		)
+	}),
+	('MMGenWalletLabel', {
+		'bad': ('яqwerty','This text is too long to fit in an MMGen wallet label'),
+		'good':  ('a good label',)
+	}),
+	('TwComment', {
+		'bad': (u'яqwerty','This text is too long for a TW comment'),
+		'good':  ('a good comment',)
+	}),
+	('MMGenTXLabel',{
+		'bad': ('This text is too long for a transaction comment. '*2,),
+		'good':  (u'UTF-8 is OK: я','a good comment',)
+	}),
+	('MMGenPWIDString', { #	forbidden = list(u' :/\\')
+		'bad': ('foo/','foo:','foo:\\'),
+		'good':  (u'qwerty@яяя',)
+	}),
+	('MMGenAddrType', {
+		'bad': ('U','z','xx',1,'dogecoin'),
+		'good':  (
+		{'s':'segwit','ret':'S'},
+		{'s':'S','ret':'S'},
+		{'s':'legacy','ret':'L'},
+		{'s':'L','ret':'L'},
+		{'s':'compressed','ret':'C'},
+		{'s':'C','ret':'C'}
+	)}),
+	('MMGenPasswordType', {
+		'bad': ('U','z','я',1,'passw0rd'),
+		'good':  (
+		{'s':'password','ret':'P'},
+		{'s':'P','ret':'P'},
+	)}),
+])
+
+def do_loop():
+	utests = cmd_args
+	for test in tests:
+		if utests and test not in utests: continue
+		msg((blue,nocolor)[bool(opt.super_silent)]('Testing {}'.format(test)))
+		for k in ('bad','good'):
+			for arg in tests[test][k]:
+				run_test(test,arg,input_data=k)
+
+do_loop()

+ 8 - 17
test/test.py

@@ -50,7 +50,8 @@ ref_wallet_brainpass = 'abc'
 ref_wallet_hash_preset = '1'
 ref_wallet_incog_offset = 123
 
-from mmgen.obj import MMGenTXLabel
+from mmgen.obj import MMGenTXLabel,PrivKey,BTCAmt
+from mmgen.addr import AddrGenerator,KeyGenerator,AddrList,AddrData,AddrIdxList
 ref_tx_label = ''.join([unichr(i) for i in  range(65,91) +
 											range(1040,1072) + # cyrillic
 											range(913,939) +   # greek
@@ -709,7 +710,6 @@ def find_generated_exts(cmd):
 def get_addrfile_checksum(display=False):
 	addrfile = get_file_with_ext('addrs',cfg['tmpdir'])
 	silence()
-	from mmgen.addr import AddrList
 	chk = AddrList(addrfile).chksum
 	if opt.verbose and display: msg('Checksum: %s' % cyan(chk))
 	end_silence()
@@ -728,9 +728,6 @@ class MMGenExpect(MMGenPexpect):
 		desc = (cmd_data[name][1],name)[bool(opt.names)] + (' ' + extra_desc).strip()
 		return MMGenPexpect.__init__(self,name,mmgen_cmd,cmd_args,desc,no_output=no_output)
 
-from mmgen.obj import BTCAmt
-from mmgen.bitcoin import verify_addr
-
 def create_fake_unspent_entry(btcaddr,al_id=None,idx=None,lbl=None,non_mmgen=False,segwit=False):
 	if lbl: lbl = ' ' + lbl
 	spk1,spk2 = (('76a914','88ac'),('a914','87'))[segwit and btcaddr.addr_fmt=='p2sh']
@@ -741,7 +738,7 @@ def create_fake_unspent_entry(btcaddr,al_id=None,idx=None,lbl=None,non_mmgen=Fal
 		'amount': BTCAmt('%s.%s' % (10+(getrandnum(4) % 40), getrandnum(4) % 100000000)),
 		'address': btcaddr,
 		'spendable': False,
-		'scriptPubKey': (spk1+verify_addr(btcaddr,return_hex=True)+spk2),
+		'scriptPubKey': '{}{}{}'.format(spk1,btcaddr.hex,spk2),
 		'confirmations': getrandnum(4) % 50000
 	}
 
@@ -784,14 +781,10 @@ def create_fake_unspent_data(adata,tx_data,non_mmgen_input=''):
 				out.append(create_fake_unspent_entry(btcaddr,d['al_id'],idx,lbl,segwit=d['segwit']))
 
 	if non_mmgen_input:
-		privnum = getrandnum(32)
-		from mmgen.bitcoin import privnum2addr,hex2wif
-		from mmgen.obj import BTCAddr
-		btcaddr = BTCAddr(privnum2addr(privnum,compressed=True))
+		privkey = PrivKey(os.urandom(32),compressed=True)
+		btcaddr = AddrGenerator('p2pkh').to_addr(KeyGenerator().to_pubhex(privkey))
 		of = os.path.join(cfgs[non_mmgen_input]['tmpdir'],non_mmgen_fn)
-		wif = hex2wif('{:064x}'.format(privnum),compressed=True)
-#		Msg(yellow(wif + ' ' + btcaddr))
-		write_data_to_file(of,wif+'\n','compressed bitcoin key',silent=True)
+		write_data_to_file(of,privkey.wif+'\n','compressed bitcoin key',silent=True)
 		out.append(create_fake_unspent_entry(btcaddr,non_mmgen=True,segwit=False))
 
 #	msg('\n'.join([repr(o) for o in out])); sys.exit(0)
@@ -808,7 +801,6 @@ def	write_fake_data_to_file(d):
 		sys.stderr.write("Fake transaction wallet data written to file '%s'\n" % unspent_data_file)
 
 def create_tx_data(sources):
-	from mmgen.addr import AddrList,AddrData,AddrIdxList
 	tx_data,ad = {},AddrData()
 	for s in sources:
 		afile = get_file_with_ext('addrs',cfgs[s]['tmpdir'])
@@ -829,8 +821,8 @@ def create_tx_data(sources):
 	return ad,tx_data
 
 def make_txcreate_cmdline(tx_data):
-	from mmgen.bitcoin import privnum2addr
-	btcaddr = privnum2addr(getrandnum(32),compressed=True)
+	privkey = PrivKey(os.urandom(32),compressed=True)
+	btcaddr = AddrGenerator('segwit').to_addr(KeyGenerator().to_pubhex(privkey))
 
 	cmd_args = ['-d',cfg['tmpdir']]
 	for num in tx_data:
@@ -848,7 +840,6 @@ def make_txcreate_cmdline(tx_data):
 def add_comments_to_addr_file(addrfile,outfile):
 	silence()
 	msg(green("Adding comments to address file '%s'" % addrfile))
-	from mmgen.addr import AddrList
 	a = AddrList(addrfile)
 	for n,idx in enumerate(a.idxs(),1):
 		if n % 2: a.set_comment(idx,'Test address %s' % n)

+ 1 - 1
test/tooltest.py

@@ -354,7 +354,7 @@ class MMGenToolTestSuite(object):
 	def Hexaddr2addr(self,name,f1,f2,f3,f4):
 		for n,fi,fo,m in ((1,f1,f2,''),(2,f3,f4,'from compressed')):
 			self.run_cmd_chk(name,fi,fo,extra_msg=m)
-	def Privhex2pubhex(self,name,f1,f2,f3): # from hex2wif
+	def Privhex2pubhex(self,name,f1,f2,f3): # from Hex2wif
 		addr = read_from_file(f3).strip()
 		self.run_cmd_out(name,addr,kwargs='compressed=1',fn_idx=3)
 	def Pubhex2redeem_script(self,name,f1,f2,f3): # from above