Browse Source

Assorted fixes/improvements:

- Importing addresses with --rescan working again
- Tracking and spending non-MMGen addresses now fully functional
- mmgen-txcreate: improvements in unspent outputs display
- mmgen-txsign: use bitcoind wallet dump as keylist fixed

- Testnet support:
  - Practice sending transactions without risking funds
  	(free testnet coins: https://tpfaucet.appspot.com/)
  - Test suite fully supported
  - To enable, set MMGEN_TESTNET environment variable
philemon 8 years ago
parent
commit
73ca40ea8d

+ 2 - 2
doc/wiki/install-linux/Install-MMGen-on-Debian-or-Ubuntu-Linux.md

@@ -2,13 +2,13 @@
 
 Install required Debian/Ubuntu packages:
 
-		$ sudo apt-get install python-pip python-dev python-pexpect python-ecdsa python-scrypt libssl-dev git
+		$ sudo apt-get install python-pip python-dev python-pexpect python-ecdsa python-scrypt libssl-dev git autoconf libtool
 
 Install the Python Cryptography Toolkit:
 
 		$ sudo pip install pycrypto
 
-Install the secp256k1 library
+Install the secp256k1 library:
 
 		$ git clone https://github.com/bitcoin-core/secp256k1.git
 		$ cd secp256k1

+ 3 - 3
mmgen/addr.py

@@ -58,17 +58,17 @@ internal ECDSA library for address generation.
 def _wif2addr_python(wif):
 	privhex = wif2hex(wif)
 	if not privhex: return False
-	return privnum2addr(int(privhex,16),wif[0] != '5')
+	return privnum2addr(int(privhex,16),wif[0] != ('5','9')[g.testnet])
 
 def _wif2addr_keyconv(wif):
-	if wif[0] == '5':
+	if wif[0] == ('5','9')[g.testnet]:
 		from subprocess import check_output
 		return check_output(['keyconv', wif]).split()[1]
 	else:
 		return _wif2addr_python(wif)
 
 def _wif2addr_secp256k1(wif):
-	return _privhex2addr_secp256k1(wif2hex(wif),wif[0] != '5')
+	return _privhex2addr_secp256k1(wif2hex(wif),wif[0] != ('5','9')[g.testnet])
 
 def _privhex2addr_python(privhex,compressed=False):
 	return privnum2addr(int(privhex,16),compressed)

+ 17 - 10
mmgen/bitcoin.py

@@ -51,13 +51,15 @@ b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
 # The 'zero address':
 # 1111111111111111111114oLvT2 (use step2 = ('0' * 40) to generate)
 
+import mmgen.globalvars as g
+
 def pubhex2hexaddr(pubhex):
 	step1 = sha256(unhexlify(pubhex)).digest()
 	return hashlib_new('ripemd160',step1).hexdigest()
 
-def hexaddr2addr(hexaddr, vers_num='00'):
-	# See above:
-	hexaddr2 = vers_num + hexaddr
+def hexaddr2addr(hexaddr,p2sh=False):
+	# devdoc/ref_transactions.md:
+	hexaddr2 = ('00','6f','05','c4')[g.testnet+(2*p2sh)] + hexaddr
 	step1 = sha256(unhexlify(hexaddr2)).digest()
 	step2 = sha256(step1).hexdigest()
 	pubkey = hexaddr2 + step2[:8]
@@ -65,9 +67,8 @@ def hexaddr2addr(hexaddr, vers_num='00'):
 	return ('1' * lzeroes) + _numtob58(int(pubkey,16))
 
 def verify_addr(addr,verbose=False,return_hex=False):
-
-	for vers_num,ldigit in ('00','1'),('05','3'):
-		if addr[0] != ldigit: continue
+	for vers_num,ldigit in ('00','1'),('05','3'),('6f','mn'),('c4','2'):
+		if addr[0] not in ldigit: continue
 		num = _b58tonum(addr)
 		if num == False: break
 		addr_hex = '{:050x}'.format(num)
@@ -145,7 +146,7 @@ def b58decode_pad(s):
 # Compressed address support:
 
 def wif2hex(wif):
-	compressed = wif[0] != '5'
+	compressed = wif[0] != ('5','9')[g.testnet]
 	idx = (66,68)[bool(compressed)]
 	num = _b58tonum(wif)
 	if num == False: return False
@@ -153,19 +154,25 @@ def wif2hex(wif):
 	if compressed and key[66:68] != '01': return False
 	round1 = sha256(unhexlify(key[:idx])).digest()
 	round2 = sha256(round1).hexdigest()
-	return key[2:66] if (key[:2] == '80' and key[idx:] == round2[:8]) else False
+	return key[2:66] if (key[:2] == ('80','ef')[g.testnet] and key[idx:] == round2[:8]) else False
 
 def hex2wif(hexpriv,compressed=False):
-	step1 = '80' + hexpriv + ('','01')[bool(compressed)]
+	step1 = ('80','ef')[g.testnet] + hexpriv + ('','01')[bool(compressed)]
 	step2 = sha256(unhexlify(step1)).digest()
 	step3 = sha256(step2).hexdigest()
 	key = step1 + step3[:8]
 	return _numtob58(int(key,16))
 
+# devdoc/guide_wallets.md:
+# Uncompressed public keys start with 0x04; compressed public keys begin with
+# 0x03 or 0x02 depending on whether they're greater or less than the midpoint
+# of the curve.
 def privnum2pubhex(numpriv,compressed=False):
 	pko = ecdsa.SigningKey.from_secret_exponent(numpriv,_secp256k1)
+	# pubkey = 32-byte X coord + 32-byte Y coord (unsigned big-endian)
 	pubkey = hexlify(pko.get_verifying_key().to_string())
-	if compressed:
+	if compressed: # discard Y coord, replace with appropriate version byte
+		# even Y: <0, odd Y: >0 -- https://bitcointalk.org/index.php?topic=129652.0
 		p = ('03','02')[pubkey[-1] in '02468ace']
 		return p+pubkey[:64]
 	else:

+ 4 - 1
mmgen/globalvars.py

@@ -45,6 +45,7 @@ no_license           = os.getenv('MMGEN_NOLICENSE')
 bogus_wallet_data    = os.getenv('MMGEN_BOGUS_WALLET_DATA')
 disable_hold_protect = os.getenv('MMGEN_DISABLE_HOLD_PROTECT')
 color = (False,True)[sys.stdout.isatty() and not os.getenv('MMGEN_DISABLE_COLOR')]
+testnet = (False,True)[bool(os.getenv('MMGEN_TESTNET'))]
 
 proj_name = 'MMGen'
 prog_name = os.path.basename(sys.argv[0])
@@ -63,12 +64,14 @@ incompatible_opts = (
 	('label','keep_label'),
 	('tx_id', 'info'),
 	('tx_id', 'terse_info'),
+	('batch', 'rescan'),
 )
 
 min_screen_width = 80
+minconf = 1
 
 # Global value sets user opt
-dfl_vars = 'seed_len','hash_preset','usr_randchars','debug','tx_confs','tx_fee_adj','tx_fee','key_generator'
+dfl_vars = 'minconf','seed_len','hash_preset','usr_randchars','debug','tx_confs','tx_fee_adj','tx_fee','key_generator'
 
 keyconv_exec = 'keyconv'
 

+ 16 - 10
mmgen/main_addrimport.py

@@ -25,14 +25,17 @@ import time
 from mmgen.common import *
 from mmgen.addr import AddrList,KeyAddrList
 
+# In batch mode, bitcoind just rescans each address separately anyway, so make
+# --batch and --rescan incompatible.
+
 opts_data = {
-	'desc': """Import addresses (both {pnm} and non-{pnm}) into a bitcoind
+	'desc': """Import addresses (both {pnm} and non-{pnm}) into an {pnm}
                      tracking wallet""".format(pnm=g.proj_name),
 	'usage':'[opts] [mmgen address file]',
 	'options': """
 -h, --help         Print this help message
--b, --batch        Batch mode.  Import all addresses in one RPC call
--l, --addrlist     Address source is a flat list of addresses
+-b, --batch        Import all addresses in one RPC call.
+-l, --addrlist     Address source is a flat list of (non-MMGen) Bitcoin addresses
 -k, --keyaddr-file Address source is a key-address file
 -q, --quiet        Suppress warnings
 -r, --rescan       Rescan the blockchain.  Required if address to import is
@@ -42,6 +45,8 @@ opts_data = {
 	'notes': """\n
 This command can also be used to update the comment fields of addresses already
 in the tracking wallet.
+
+The --batch option cannot be used with the --rescan option.
 """
 }
 
@@ -116,12 +121,15 @@ for n,e in enumerate(ai.data):
 	if e.idx:
 		label = '%s:%s' % (ai.seed_id,e.idx)
 		if e.label: label += ' ' + e.label
-	else: label = 'non-{pnm}'.format(pnm=g.proj_name)
+		m = label
+	else:
+		label = 'btc:{}'.format(e.addr)
+		m = 'non-'+g.proj_name
 
 	if opt.batch:
 		arg_list.append((e.addr,label,False))
 	elif opt.rescan:
-		t = threading.Thread(target=import_address, args=(e.addr,label,True))
+		t = threading.Thread(target=import_address,args=[e.addr,label,True])
 		t.daemon = True
 		t.start()
 
@@ -131,7 +139,7 @@ for n,e in enumerate(ai.data):
 			if t.is_alive():
 				elapsed = int(time.time() - start)
 				count = '%s/%s:' % (n+1, ai.num_addrs)
-				msg_r(msg_fmt % (secs_to_hms(elapsed),count,e.addr,'(%s)'%label))
+				msg_r(msg_fmt % (secs_to_hms(elapsed),count,e.addr,'(%s)' % m))
 				time.sleep(1)
 			else:
 				if err_flag: die(2,'\nImport failed')
@@ -140,12 +148,10 @@ for n,e in enumerate(ai.data):
 	else:
 		import_address(e.addr,label,False)
 		count = '%s/%s:' % (n+1, ai.num_addrs)
-		msg_r(msg_fmt % (count, e.addr, '(%s)'%label))
+		msg_r(msg_fmt % (count, e.addr, '(%s)' % m))
 		if err_flag: die(2,'\nImport failed')
 		msg(' - OK')
 
 if opt.batch:
-	if opt.rescan:
-		msg('Warning: this command may take a long time to complete!')
-	ret = c.importaddress(arg_list,batch=True,timeout=(False,3600)[bool(opt.rescan)])
+	ret = c.importaddress(arg_list,batch=True)
 	msg('OK: %s addresses imported' % len(ret))

+ 3 - 2
mmgen/main_txcreate.py

@@ -36,8 +36,9 @@ opts_data = {
 -c, --comment-file= f Source the transaction's comment from file 'f'
 -C, --tx-confs=     c Desired number of confirmations (default: {g.tx_confs})
 -d, --outdir=       d Specify an alternate directory 'd' for output
--e, --echo-passphrase Print passphrase to screen when typing it
+-e, --clear-screen    Clear screen before displaying unspent outputs
 -f, --tx-fee=       f Transaction fee (default: {g.tx_fee} BTC (but see below))
+-m, --minconf=      n Minimum number of confirmations required to spend outputs (default: 1)
 -i, --info            Display unspent outputs and exit
 -q, --quiet           Suppress warnings; overwrite files without prompting
 -v, --verbose         Produce more verbose output
@@ -199,7 +200,7 @@ if not opt.info:
 
 	fee_estimate = get_fee_estimate()
 
-tw = MMGenTrackingWallet()
+tw = MMGenTrackingWallet(minconf=opt.minconf)
 tw.view_and_sort()
 tw.display_total()
 

+ 1 - 2
mmgen/main_txsign.py

@@ -227,10 +227,9 @@ if opt.mmgen_keys_from_file:
 
 if opt.keys_from_file:
 	l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True)
-	kl = KeyAddrList(keylist=l)
+	kl = KeyAddrList(keylist=[m.split()[0] for m in l]) # accept bitcoind wallet dumps
 	if kal: kl.remove_dups(kal,key='wif')
 	kl.generate_addrs()
-# pp_die(kl)
 
 tx_num_str = ''
 for tx_num,tx_file in enumerate(tx_files,1):

+ 10 - 8
mmgen/obj.py

@@ -195,6 +195,7 @@ class AddrIdx(int,InitErrors):
 				m = "'%s': addr idx cannot be less than one" % num
 			else:
 				return me
+
 		return cls.init_fail(m,on_fail)
 
 class AddrIdxList(list,InitErrors):
@@ -349,7 +350,7 @@ class BTCAddr(str,Hilite,InitErrors):
 		cls.arg_chk(cls,on_fail)
 		me = str.__new__(cls,s)
 		from mmgen.bitcoin import verify_addr
-		if verify_addr(s):
+		if type(s) in (str,unicode,BTCAddr) and verify_addr(s):
 			return me
 		else:
 			m = "'%s': value is not a Bitcoin address" % s
@@ -375,13 +376,14 @@ class SeedID(str,Hilite,InitErrors):
 		if seed:
 			from mmgen.seed import Seed
 			from mmgen.util import make_chksum_8
-			assert type(seed) == Seed
-			return str.__new__(cls,make_chksum_8(seed.get_data()))
+			if type(seed) == Seed:
+				return str.__new__(cls,make_chksum_8(seed.get_data()))
 		elif sid:
 			from string import hexdigits
-			assert len(sid) == cls.width and set(sid) <= set(hexdigits.upper())
-			return str.__new__(cls,sid)
-		m = "'%s': value cannot be converted to SeedID" % s
+			if len(sid) == cls.width and set(sid) <= set(hexdigits.upper()):
+				return str.__new__(cls,sid)
+
+		m = "'%s': value cannot be converted to SeedID" % str(seed or sid)
 		return cls.init_fail(m,on_fail)
 
 class MMGenID(str,Hilite,InitErrors):
@@ -395,9 +397,9 @@ class MMGenID(str,Hilite,InitErrors):
 		s = str(s)
 		if ':' in s:
 			a,b = s.split(':',1)
-			sid = SeedID(sid=a,on_fail='return')
+			sid = SeedID(sid=a,on_fail='silent')
 			if sid:
-				idx = AddrIdx(b,on_fail='return')
+				idx = AddrIdx(b,on_fail='silent')
 				if idx:
 					return str.__new__(cls,'%s:%s' % (sid,idx))
 

+ 8 - 7
mmgen/rpc.py

@@ -20,9 +20,11 @@
 rpc.py:  Bitcoin RPC library for the MMGen suite
 """
 
-import httplib,base64,json,decimal
+import httplib,base64,json
 
 from mmgen.common import *
+from decimal import Decimal
+from mmgen.obj import BTCAmt
 
 class BitcoinRPCConnection(object):
 
@@ -30,7 +32,7 @@ class BitcoinRPCConnection(object):
 
 	def __init__(
 				self,
-				host='localhost',port=8332,
+				host='localhost',port=(8332,18332)[g.testnet],
 				user=None,passwd=None,auth_cookie=None,
 			):
 
@@ -64,7 +66,7 @@ class BitcoinRPCConnection(object):
 		for k in cf:
 			if k in kwargs and kwargs[k]: cf[k] = kwargs[k]
 
-		c = httplib.HTTPConnection(self.host, self.port, False, cf['timeout'])
+		hc = httplib.HTTPConnection(self.host, self.port, False, cf['timeout'])
 
 		if cf['batch']:
 			p = [{'method':cmd,'params':r,'id':n} for n,r in enumerate(args[0],1)]
@@ -80,7 +82,6 @@ class BitcoinRPCConnection(object):
 		dmsg('=== rpc.py debug ===')
 		dmsg('    RPC POST data ==> %s\n' % p)
 
-		from mmgen.obj import BTCAmt
 		caller = self
 		class MyJSONEncoder(json.JSONEncoder):
 			def default(self, obj):
@@ -94,14 +95,14 @@ class BitcoinRPCConnection(object):
 # 			print(dump)
 
 		try:
-			c.request('POST', '/', json.dumps(p,cls=MyJSONEncoder), {
+			hc.request('POST', '/', json.dumps(p,cls=MyJSONEncoder), {
 				'Host': self.host,
 				'Authorization': 'Basic ' + base64.b64encode(self.auth_str)
 			})
 		except Exception as e:
 			return die_maybe(None,2,'%s\nUnable to connect to bitcoind' % e)
 
-		r = c.getresponse() # returns HTTPResponse instance
+		r = hc.getresponse() # returns HTTPResponse instance
 
 		if r.status != 200:
 			e1 = r.read()
@@ -118,7 +119,7 @@ class BitcoinRPCConnection(object):
 		if not r2:
 			return die_maybe(r,2,'Error: empty reply')
 
-		from decimal import Decimal
+#		from decimal import Decimal
 		r3 = json.loads(r2.decode('utf8'), parse_float=Decimal)
 		ret = []
 

+ 0 - 3
mmgen/term.py

@@ -23,9 +23,6 @@ term.py:  Terminal-handling routines for the MMGen suite
 import os,struct
 from mmgen.common import *
 
-CUR_SHOW = '\033[?25h'
-CUR_HIDE = '\033[?25l'
-
 def _kb_hold_protect_unix():
 
 	fd = sys.stdin.fileno()

+ 1 - 0
mmgen/test.py

@@ -80,6 +80,7 @@ def read_from_tmpfile(cfg,fn,binary=False):
 	return read_from_file(os.path.join(cfg['tmpdir'],fn),binary=binary)
 
 def ok():
+	if opt.profile: return
 	if opt.verbose or opt.exact_output:
 		sys.stderr.write(green('OK\n'))
 	else: msg(' OK')

+ 10 - 9
mmgen/tool.py

@@ -74,7 +74,7 @@ cmd_data = OrderedDict([
 	('listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']),
 	('getbalance',   ['minconf [int=1]']),
 	('txview',       ['<{} TX file> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]']),
-	('twview',       ["sort [str='age']",'reverse [bool=False]','wide [bool=False]','pager [bool=False]']),
+	('twview',       ["sort [str='age']",'reverse [bool=False]','minconf [int=1]','wide [bool=False]','pager [bool=False]']),
 
 	('add_label',       ['<{} address> [str]'.format(pnm),'<label> [str]']),
 	('remove_label',    ['<{} address> [str]'.format(pnm)]),
@@ -373,7 +373,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 	for d in c.listunspent(0):
 		mmaddr,comment = split2(d['account'])
 		if usr_addr_list and (mmaddr not in usr_addr_list): continue
-		if is_mmgen_id(mmaddr) and d['confirmations'] >= minconf:
+		if (mmaddr[:4] == 'btc:' or is_mmgen_id(mmaddr)) and d['confirmations'] >= minconf:
 			key = mmaddr.replace(':','_')
 			if key in addrs:
 				if addrs[key][2] != d['address']:
@@ -391,7 +391,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 		for acct in accts:
 			mmaddr,comment = split2(acct)
 			if usr_addr_list and (mmaddr not in usr_addr_list): continue
-			if is_mmgen_id(mmaddr):
+			if mmaddr[:4] == 'btc:' or is_mmgen_id(mmaddr):
 				key = mmaddr.replace(':','_')
 				if key not in addrs:
 					if showbtcaddrs: save_a.append([acct])
@@ -407,7 +407,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 		die(0,('No addresses with balances!','No tracked addresses!')[showempty])
 
 	fs = ('{mid} {lbl} {amt}','{mid} {addr} {lbl} {amt}')[showbtcaddrs]
-	max_mmid_len = max(len(k) for k in addrs) or 10
+	max_mmid_len = max([len(k) for k in addrs if k[:4] != 'btc_'] or [10])
 	max_lbl_len =  max(len(addrs[k][1]) for k in addrs) or 7
 	out = [fs.format(
 			mid=MMGenID.fmtc('MMGenID',width=max_mmid_len),
@@ -419,10 +419,11 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 	old_sid = ''
 	def s_mmgen(k): return '{:>0{w}}'.format(k,w=AddrIdx.max_digits+9) # TODO
 	for k in sorted(addrs,key=s_mmgen):
-		if old_sid and old_sid != k[:8]: out.append('')
-		old_sid = k[:8]
+		if old_sid and old_sid != k.split('_')[0]: out.append('')
+		old_sid = k.split('_')[0]
+		m = 'non-'+g.proj_name if k[:4] == 'btc_' else k.replace('_',':')
 		out.append(fs.format(
-			mid=MMGenID(k.replace('_',':')).fmt(width=max_mmid_len,color=True),
+			mid = MMGenID.fmtc(m,width=max_mmid_len,color=True),
 			addr=(addrs[k][2].fmt(color=True) if showbtcaddrs else None),
 			lbl=addrs[k][1].fmt(width=max_lbl_len,color=True,nullrepl='-'),
 			amt=addrs[k][0].fmt('3.0',color=True)))
@@ -460,9 +461,9 @@ def txview(infile,pager=False,terse=False):
 	tx = MMGenTX(infile)
 	tx.view(pager,pause=False,terse=terse)
 
-def twview(pager=False,reverse=False,wide=False,sort='age'):
+def twview(pager=False,reverse=False,wide=False,minconf=1,sort='age'):
 	from mmgen.tw import MMGenTrackingWallet
-	tw = MMGenTrackingWallet()
+	tw = MMGenTrackingWallet(minconf=minconf)
 	tw.do_sort(sort,reverse=reverse)
 	out = tw.format_for_printing(color=True) if wide else tw.format_for_display()
 	do_pager(out) if pager else sys.stdout.write(out)

+ 36 - 25
mmgen/tw.py

@@ -23,16 +23,17 @@ tw: Tracking wallet methods for the MMGen suite
 from mmgen.common import *
 from mmgen.obj import *
 from mmgen.term import get_char
+from mmgen.tx import is_mmgen_id
+
+CUR_HOME,ERASE_ALL = '\033[H','\033[0J'
 
 def parse_tw_acct_label(s):
 	ret = s.split(None,1)
-	if ret and MMGenID(ret[0],on_fail='silent'):
-		if len(ret) == 2:
-			return tuple(ret)
-		else:
-			return ret[0],None
-	else:
-		return None,None
+	a1,a2 = None,None
+	if ret:
+		a1 = ret[0] if is_mmgen_id(ret[0]) else '' if ret[0][:4] == 'btc:' else None
+		a2 = ret[1] if len(ret) == 2 else None
+	return a1,a2
 
 class MMGenTWOutput(MMGenListItem):
 	attrs_reassign = 'label','skip'
@@ -48,7 +49,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 	}
 	sort_keys = 'addr','age','amt','txid','mmid'
 
-	def __init__(self):
+	def __init__(self,minconf=1):
 		self.unspent      = []
 		self.fmt_display  = ''
 		self.fmt_print    = ''
@@ -57,6 +58,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		self.group        = False
 		self.show_days    = True
 		self.show_mmid    = True
+		self.minconf      = minconf
 		self.get_data()
 		self.sort_key     = 'age'
 		self.do_sort()
@@ -69,7 +71,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		if g.bogus_wallet_data: # for debugging purposes only
 			us_rpc = eval(get_data_from_file(g.bogus_wallet_data))
 		else:
-			us_rpc = bitcoin_connection().listunspent()
+			us_rpc = bitcoin_connection().listunspent(self.minconf)
 #		write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
 #		sys.exit()
 
@@ -121,6 +123,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 			my_raw_input(m1+'\n'+m2.format(g.min_screen_width))
 
 	def display(self):
+		if opt.clear_screen: msg(CUR_HOME+ERASE_ALL)
 		msg(self.format_for_display())
 
 	def format_for_display(self):
@@ -158,18 +161,18 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 
 		for n,i in enumerate(unsp):
 			addr_dots = '|' + '.'*33
-			mmid_disp = (MMGenID.hlc('.'*mmid_w) \
-							if i.skip=='addr' else i.mmid.fmt(width=mmid_w,color=True)) \
-								if i.mmid else ' ' * mmid_w
-			if self.show_mmid and i.mmid:
+			mmid_disp = MMGenID.fmtc('.'*mmid_w if i.skip=='addr'
+				else i.mmid or 'Non-{}'.format(g.proj_name),width=mmid_w,color=True)
+			if self.show_mmid:
 				addr_out = '%s %s' % (
 					type(i.addr).fmtc(addr_dots,width=btaddr_w,color=True) if i.skip == 'addr' \
 						else i.addr.fmt(width=btaddr_w,color=True),
-					'{} {}'.format(mmid_disp,i.label.fmt(width=label_w,color=True) if label_w > 0 else '')
+					'{} {}'.format(mmid_disp,i.label.fmt(width=label_w,color=True) \
+							if label_w > 0 else '')
 				)
 			else:
-				addr_out = type(i.addr).fmtc(addr_dots,width=addr_w,color=True) if i.skip=='addr' \
-								else i.addr.fmt(width=addr_w,color=True)
+				addr_out = type(i.addr).fmtc(addr_dots,width=addr_w,color=True) \
+					if i.skip=='addr' else i.addr.fmt(width=addr_w,color=True)
 
 			tx = ' ' * (tx_w-4) + '|...' if i.skip == 'txid' \
 					else i.txid[:tx_w-len(txdots)]+txdots
@@ -186,13 +189,12 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		out = [fs % ('Num','Tx ID,Vout','Address'.ljust(34),'MMGen ID'.ljust(15),
 			'Amount(BTC)','Conf.','Age(d)', 'Label')]
 
-		max_lbl_len = max(len(i.label) for i in self.unspent if i.label) or 1
+		max_lbl_len = max([len(i.label) for i in self.unspent if i.label] or [1])
 		for n,i in enumerate(self.unspent):
 			addr = '=' if i.skip == 'addr' and self.group else i.addr.fmt(color=color)
 			tx = ' ' * 63 + '=' if i.skip == 'txid' and self.group else str(i.txid)
 			s = fs % (str(n+1)+')', tx+','+str(i.vout),addr,
-					(i.mmid.fmt(width=14,color=color) if i.mmid else
-						MMGenID.fmtc('',width=14,nullrepl='-',color=color)),
+					MMGenID.fmtc(i.mmid or 'Non-{}'.format(g.proj_name),width=14,color=color),
 					i.amt.fmt(color=color),i.confs,i.days,
 					i.label.hl(color=color) if i.label else
 						MMGenAddrLabel.fmtc('',color=color,nullrepl='-',width=max_lbl_len))
@@ -218,9 +220,9 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 			n = AddrIdx(ret,on_fail='silent') # hacky way to test and convert to integer
 			if not n or n < 1 or n > len(self.unspent):
 				msg('Choice must be a single number between 1 and %s' % len(self.unspent))
-			elif not self.unspent[n-1].mmid:
-				msg('Address #%s is not an %s address. No label can be added to it' %
-						(n,g.proj_name))
+# 			elif not self.unspent[n-1].mmid:
+# 				msg('Address #%s is not an %s address. No label can be added to it' %
+# 						(n,g.proj_name))
 			else:
 				while True:
 					s = my_raw_input("Enter label text (or 'q' to return to main menu): ")
@@ -290,10 +292,19 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 
 	# returns on failure
 	@classmethod
-	def add_label(cls,mmaddr,label='',addr=None):
-		mmaddr = MMGenID(mmaddr)
+	def add_label(cls,arg1,label='',addr=None):
+		from mmgen.tx import is_mmgen_id,is_btc_addr
+		if is_mmgen_id(arg1):
+			mmaddr = MMGenID(arg1)
+		elif is_btc_addr(arg1):             # called from 'mmgen-tool add_label'
+			addr = arg1
+			mmaddr = 'btc:'+arg1
+		elif not arg1 and is_btc_addr(addr): # called from view_and_sort(), non-MMGen addr
+			mmaddr = 'btc:'+addr
+		else:
+			die(3,'{}: not a BTC address or {} ID'.format(arg1,g.proj_name))
 
-		if addr: # called from view_and_sort()
+		if addr:
 			if not BTCAddr(addr,on_fail='return'): return False
 		else:
 			from mmgen.addr import AddrData

+ 2 - 1
mmgen/util.py

@@ -662,7 +662,8 @@ def get_bitcoind_auth_cookie():
 
 def bitcoin_connection():
 
-	host,port,user,passwd = 'localhost',8332,'rpcuser','rpcpassword'
+	port = (8332,18332)[g.testnet]
+	host,user,passwd = 'localhost','rpcuser','rpcpassword'
 	cfg = get_bitcoind_cfg_options((user,passwd))
 	auth_cookie = get_bitcoind_auth_cookie()
 

+ 115 - 49
test/gentest.py

@@ -35,73 +35,139 @@ start_mscolor()
 
 rounds = 100
 opts_data = {
-	'desc': "Test address generation using various methods",
-	'usage':'[options] a:b [rounds]',
+	'desc': "Test address generation in various ways",
+	'usage':'[options] [spec] [rounds | dump file]',
 	'options': """
 -h, --help         Print this help message
--s, --system       Test scripts and modules installed on system rather than
-                   those in the repo root
 -v, --verbose      Produce more verbose output
+-q, --quiet        Produce quieter output
 """,
 	'notes': """
-{pnm} can generate addresses from secret keys using one of three methods,
-as specified by the user:
+    Tests:
+       A/B:     {prog} a:b [rounds]  (compare output of two key generators)
+       Speed:   {prog} a [rounds]    (test speed of one key generator)
+       Compare: {prog} a <dump file> (compare output of a key generator against wallet dump)
+          where a and b are one of:
+             '1' - native Python ecdsa library (very slow)
+             '2' - 'keyconv' utility from the 'vanitygen' package (old default)
+             '3' - bitcoincore.org's secp256k1 library (default from v0.8.6)
 
-    1) with the native Python ecdsa library (very slow)
-    2) with the 'keyconv' utility from the 'vanitygen' package (old default)
-    3) using bitcoincore.org's secp256k1 library (default from v0.8.6)
-
-This test suite compares the output of these different methods against each
-other over set of randomly generated secret keys ({snum} by default).
-
-EXAMPLE:
-  gentest.py 2:3 1000
-  (compare output of 'keyconv' with secp256k1 library, 1000 rounds)
-""".format(pnm=g.proj_name,snum=rounds)
+EXAMPLES:
+  {prog} 2:3 1000
+    (compare output of 'keyconv' with secp256k1 library, 1000 rounds)
+  {prog} 3 1000
+    (test speed of secp256k1 library address generation, 1000 rounds)
+  {prog} 3 my.dump
+    (compare addrs generated with secp256k1 library to bitcoind wallet dump)
+""".format(prog='gentest.py',pnm=g.proj_name,snum=rounds)
 }
 cmd_args = opts.init(opts_data,add_opts=['exact_output'])
 
 if not 1 <= len(cmd_args) <= 2: opts.usage()
 
+urounds,fh = None,None
+dump = []
 if len(cmd_args) == 2:
 	try:
-		rounds = int(cmd_args[1])
-		assert rounds > 0
+		urounds = int(cmd_args[1])
+		assert urounds > 0
 	except:
-		die(1,"'rounds' must be a positive integer")
+		try:
+			fh = open(cmd_args[1])
+		except:
+			die(1,"Second argument must be filename or positive integer")
+		else:
+			for line in fh.readlines():
+				if 'addr=' in line:
+					x,addr = line.split('addr=')
+					dump.append([x.split()[0],addr.split()[0]])
 
+if urounds: rounds = urounds
+
+a,b = None,None
 try:
 	a,b = cmd_args[0].split(':')
-	a,b = int(a),int(b)
-	for i in a,b: assert 1 <= i <= len(g.key_generators)
-	assert a != b
 except:
-	die(1,"%s: incorrect 'a:b' specifier" % cmd_args[0])
+	try:
+		a = cmd_args[0]
+		a = int(a)
+		assert 1 <= a <= len(g.key_generators)
+	except:
+		die(1,"First argument must be one or two generator IDs, colon separated")
+else:
+	try:
+		a,b = int(a),int(b)
+		for i in a,b: assert 1 <= i <= len(g.key_generators)
+		assert a != b
+	except:
+		die(1,"%s: invalid generator IDs" % cmd_args[0])
 
-if opt.system: sys.path.pop(0)
+def match_error(sec,wif,a_addr,b_addr,a,b):
+	m = ['','py-ecdsa','keyconv','secp256k1','dump']
+	msg_r(red('\nERROR: Addresses do not match!'))
+	die(3,"""
+  sec key   : {}
+  WIF key   : {}
+  {a:10}: {}
+  {b:10}: {}
+""".format(sec,wif,a_addr,b_addr,pnm=g.proj_name,a=m[a],b=m[b]).rstrip())
 
-m = "Comparing address generators '{}' and '{}'"
-msg(green(m.format(g.key_generators[a-1],g.key_generators[b-1])))
-from mmgen.addr import get_privhex2addr_f
-gen_a = get_privhex2addr_f(selector=a)
-gen_b = get_privhex2addr_f(selector=b)
-compressed = False
-for i in range(1,rounds+1):
-	msg_r('\rRound %s/%s ' % (i,rounds))
-	sec = hexlify(os.urandom(32))
-	wif = hex2wif(sec,compressed=compressed)
-	a_addr = gen_a(sec,compressed)
-	b_addr = gen_b(sec,compressed)
-	vmsg('\nkey:  %s\naddr: %s\n' % (wif,a_addr))
-	if a_addr != b_addr:
-		msg_r(red('\nERROR: Addresses do not match!'))
-		die(3,"""
-  sec key: {}
-  WIF key: {}
-  {pnm}:   {}
-  keyconv: {}
-""".format(sec,wif,a_addr,b_addr,pnm=g.proj_name).rstrip())
-	if a != 2 and b != 2:
-		compressed = not compressed
+if a and b:
+	m = "Comparing address generators '{}' and '{}'"
+	msg(green(m.format(g.key_generators[a-1],g.key_generators[b-1])))
+	from mmgen.addr import get_privhex2addr_f
+	gen_a = get_privhex2addr_f(selector=a)
+	gen_b = get_privhex2addr_f(selector=b)
+	compressed = False
+	for i in range(1,rounds+1):
+		msg_r('\rRound %s/%s ' % (i,rounds))
+		sec = hexlify(os.urandom(32))
+		wif = hex2wif(sec,compressed=compressed)
+		a_addr = gen_a(sec,compressed)
+		b_addr = gen_b(sec,compressed)
+		vmsg('\nkey:  %s\naddr: %s\n' % (wif,a_addr))
+		if a_addr != b_addr:
+			match_error(sec,wif,a_addr,b_addr,a,b)
+		if a != 2 and b != 2:
+			compressed = not compressed
 
-msg(green(('\n','')[bool(opt.verbose)] + 'OK'))
+	msg(green(('\n','')[bool(opt.verbose)] + 'OK'))
+elif a and not fh:
+	m = "Testing speed of address generator '{}'"
+	msg(green(m.format(g.key_generators[a-1])))
+	from mmgen.addr import get_privhex2addr_f
+	gen_a = get_privhex2addr_f(selector=a)
+	import time
+	start = time.time()
+	from struct import pack,unpack
+	seed = os.urandom(28)
+	print 'Incrementing key with each round'
+	print 'Starting key:', hexlify(seed+pack('I',0))
+	compressed = False
+	for i in range(rounds):
+		if not opt.quiet: msg_r('\rRound %s/%s ' % (i+1,rounds))
+		sec = hexlify(seed+pack('I',i))
+		wif = hex2wif(sec,compressed=compressed)
+		a_addr = gen_a(sec,compressed)
+		vmsg('\nkey:  %s\naddr: %s\n' % (wif,a_addr))
+		if a != 2:
+			compressed = not compressed
+	elapsed = int(time.time() - start)
+	if not opt.quiet: msg('')
+	msg('%s addresses generated in %s second%s' % (rounds,elapsed,('s','')[elapsed==1]))
+elif a and dump:
+	m = "Comparing output of address generator '{}' against wallet dump '{}'"
+	msg(green(m.format(g.key_generators[a-1],cmd_args[1])))
+	if a == 2:
+		msg("NOTE: for compressed addresses, 'python-ecdsa' generator will be used")
+	from mmgen.addr import get_privhex2addr_f
+	gen_a = get_privhex2addr_f(selector=a)
+	from mmgen.bitcoin import wif2hex
+	for n,[wif,a_addr] in enumerate(dump,1):
+		msg_r('\rKey %s/%s ' % (n,len(dump)))
+		sec = wif2hex(wif)
+		compressed = wif[0] != ('5','9')[g.testnet]
+		b_addr = gen_a(sec,compressed)
+		if a_addr != b_addr:
+			match_error(sec,wif,a_addr,b_addr,1 if compressed and a==2 else a,4)
+	msg(green(('\n','')[bool(opt.verbose)] + 'OK'))

+ 6 - 0
test/ref/98831F3A-E2687906[256,1].mmdat

@@ -0,0 +1,6 @@
+9cc19b
+test.py ref. wallet (pw 'abc', seed len 256)
+98831f3a e2687906 256 NE 20161110_135346
+1: 12 8 1
+70413d 74ev zjeq Zw2g DspF RKpE 7H
+7c26e6 1otd mVTn 5MCR cDTF sZqY uNKA rsAm mjTw EJmS yzwX ZPJd

+ 19 - 0
test/ref/98831F3A[1,31-33,500-501,1010-1011].testnet.addrs

@@ -0,0 +1,19 @@
+# MMGen address file
+#
+# This file is editable.
+# Everything following a hash symbol '#' is a comment and ignored by MMGen.
+# A text label of 32 characters or less may be added to the right of each
+# address, and it will be appended to the bitcoind wallet label upon import.
+# The label may contain any printable ASCII symbol.
+# Address data checksum for 98831F3A[1,31-33,500-501,1010-1011]: 3C2C 8558 BB54 079E
+# Record this value to a secure location.
+98831F3A {
+  1     n1z4XgmpMzaZJ9Ywjefnv7yrPc2w4h6UxL
+  31    mfqYRquF5Uw9YCKGoqh7tRqfYQQhdAQi5Q
+  32    mp31EPM2a8evsuZDYPiPUmA5ChwCLTr9x1
+  33    mmdqJpHeV1nmVpBmebGyR4Ziu2chE1gc43
+  500   mu5LCgiNbbzWMod2DAFJpqfwF3vMSGPdnb
+  501   mtxxVcPsLb237x4tQpiztcE2jHFVWdSs8d
+  1010  mtVqD1xmEBVMijdDXpfidbF4otQsLzmNxi
+  1011  msj54iM9CYCtpvcGnb8fz54hG23fQPuueN
+}

BIN
test/ref/98831F3A[1,31-33,500-501,1010-1011].testnet.akeys.mmenc


+ 6 - 0
test/ref/FFB367[1.234].testnet.rawtx

@@ -0,0 +1,6 @@
+3c0e60
+FFB367 1.234 20150405_102927 350828
+01000000013364630b6d290a82c822facc2f7c1db4452cea459b2ce22371135530485a5d010600000000ffffffff0205d7d600010000001976a914bba3993079ccdf40c9bbbe495473f0b3d2dc5eec88ac40ef5a07000000001976a914abe58e1e45f6176910a4c1ac1ee62328d5cc4fd588ac00000000
+[{'label': u'Test Wallet', 'mmid': u'98831F3A:500', 'vout': 6, 'txid': u'015d5a483055137123e22c9b45ea2c45b41d7c2fccfa22c8820a296d0b636433', 'amt': BTCAmt('44.32452045'), 'confs': 495L, 'addr': u'mu5LCgiNbbzWMod2DAFJpqfwF3vMSGPdnb', 'scriptPubKey': '76a91494b93bbe8a32f1db80b307482e83c25fa4e99b8c88ac'}]
+[{'amt': BTCAmt('43.09047045'), 'mmid': '98831F3A:3', 'addr': u'mxd6dwbbhg4g7tqGxwQ4pueajob7kjWHBG'}, {'amt': BTCAmt('1.23400000'), 'mmid': '98831F3A:2', 'addr': u'mwBrqdQGfj4yH6594qAzZVqmYfLdmB1C7W'}]
+TvwWgaAnrkQFpAxxjBa4PHvJ8NsJDsurtiv2HuzdnXWjQmY7LHyt6PZn5J7BNtB5VzHtBG7bUosCAMFon8yxUe2mYTZoH9e6dpoAz9E6JDZtUNYz9YnF1Z3jFND1X89RuKAk6YVBrfWseeyHR8vZDdaFzBPK5SPos

+ 22 - 33
test/test.py

@@ -45,6 +45,7 @@ sys.path.__setitem__(0,os.path.abspath(os.curdir))
 # Import these _after_ local path's been added to sys.path
 from mmgen.common import *
 from mmgen.test import *
+tn_desc = ('','.testnet')[g.testnet]
 
 start_mscolor()
 
@@ -175,8 +176,8 @@ cfgs = {
 		'seed_len':        128,
 		'seed_id':         'FE3C6545',
 		'ref_bw_seed_id':  '33F10310',
-		'addrfile_chk':    'B230 7526 638F 38CB',
-		'keyaddrfile_chk': 'CF83 32FB 8A8B 08E2',
+		'addrfile_chk':    ('B230 7526 638F 38CB','B64D 7327 EF2A 60FE')[g.testnet],
+		'keyaddrfile_chk': ('CF83 32FB 8A8B 08E2','FEBF 7878 97BB CC35')[g.testnet],
 		'wpasswd':         'reference password',
 		'ref_wallet':      'FE3C6545-D782B529[128,1].mmdat',
 		'ic_wallet':       'FE3C6545-E29303EA-5E229E30[128,1].mmincog',
@@ -201,8 +202,8 @@ cfgs = {
 		'seed_len':        192,
 		'seed_id':         '1378FC64',
 		'ref_bw_seed_id':  'CE918388',
-		'addrfile_chk':    '8C17 A5FA 0470 6E89',
-		'keyaddrfile_chk': '9648 5132 B98E 3AD9',
+		'addrfile_chk':    ('8C17 A5FA 0470 6E89','0A59 C8CD 9439 8B81')[g.testnet],
+		'keyaddrfile_chk': ('9648 5132 B98E 3AD9','2F72 C83F 44C5 0FAC')[g.testnet],
 		'wpasswd':         'reference password',
 		'ref_wallet':      '1378FC64-6F0F9BB4[192,1].mmdat',
 		'ic_wallet':       '1378FC64-2907DE97-F980D21F[192,1].mmincog',
@@ -227,17 +228,17 @@ cfgs = {
 		'seed_len':        256,
 		'seed_id':         '98831F3A',
 		'ref_bw_seed_id':  'B48CD7FC',
-		'addrfile_chk':    '6FEF 6FB9 7B13 5D91',
-		'keyaddrfile_chk': '9F2D D781 1812 8BAD',
+		'addrfile_chk':    ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E')[g.testnet],
+		'keyaddrfile_chk': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2')[g.testnet],
 		'wpasswd':         'reference password',
-		'ref_wallet':      '98831F3A-27F2BF93[256,1].mmdat',
-		'ref_addrfile':    '98831F3A[1,31-33,500-501,1010-1011].addrs',
-		'ref_keyaddrfile': '98831F3A[1,31-33,500-501,1010-1011].akeys.mmenc',
-		'ref_addrfile_chksum':    '6FEF 6FB9 7B13 5D91',
-		'ref_keyaddrfile_chksum': '9F2D D781 1812 8BAD',
+		'ref_wallet':      '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.testnet]),
+		'ref_addrfile':    '98831F3A[1,31-33,500-501,1010-1011]{}.addrs'.format(tn_desc),
+		'ref_keyaddrfile': '98831F3A[1,31-33,500-501,1010-1011]{}.akeys.mmenc'.format(tn_desc),
+		'ref_addrfile_chksum':    ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E')[g.testnet],
+		'ref_keyaddrfile_chksum': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2')[g.testnet],
 
 #		'ref_fake_unspent_data':'98831F3A_unspent.json',
-		'ref_tx_file':     'FFB367[1.234].rawtx',
+		'ref_tx_file':     'FFB367[1.234]{}.rawtx'.format(tn_desc),
 		'ic_wallet':       '98831F3A-5482381C-18460FB1[256,1].mmincog',
 		'ic_wallet_hex':   '98831F3A-1630A9F2-870376A9[256,1].mmincox',
 
@@ -483,6 +484,7 @@ opts_data = {
 -n, --names         Display command names instead of descriptions.
 -I, --non-interactive Non-interactive operation (MS Windows mode)
 -p, --pause         Pause between tests, resuming on keypress.
+-P, --profile       Record the execution time of each script.
 -q, --quiet         Produce minimal output.  Suppress dependency info.
 -r, --resume=c      Resume at command 'c' after interrupted run
 -s, --system        Test scripts and modules installed on system rather
@@ -500,6 +502,7 @@ If no command is given, the whole suite of tests is run.
 
 cmd_args = opts.init(opts_data)
 
+if opt.profile: opt.names = True
 if opt.resume: opt.skip_deps = True
 if opt.log:
 	log_fd = open(log_file,'a')
@@ -1040,7 +1043,10 @@ class MMGenTestSuite(object):
 			else:
 				return
 
+		if opt.profile: start = time.time()
 		self.__class__.__dict__[cmd](*([self,cmd] + al))
+		if opt.profile:
+			msg('\r\033[50C{:.4f}'.format(time.time() - start))
 
 	def generate_file_deps(self,cmd):
 		return [(str(n),e) for exts,n in cmd_data[cmd][2] for e in exts]
@@ -1410,6 +1416,7 @@ class MMGenTestSuite(object):
 			return
 		t.expect('Encrypt key list? (y/N): ','y')
 		t.hash_preset('new key list','1')
+#		t.passphrase_new('new key list','kafile password')
 		t.passphrase_new('new key list',cfg['kapasswd'])
 		t.written_to_file('Secret keys',oo=True)
 		ok()
@@ -1558,27 +1565,6 @@ class MMGenTestSuite(object):
 		os.unlink(f1)
 		cmp_or_die(hincog_offset,int(o))
 
-# 	def pywallet(self,name):  # TODO - check output
-# 		pf = get_tmpfile_fn(cfg,pwfile)
-# 		write_data_to_file(pf,cfg['wpasswd']+'\n',silent=True)
-# 		args = ([],['-q','-P',pf])[ni]
-# 		unenc_wf = os.path.join(ref_dir,'wallet-unenc.dat')
-# 		enc_wf   = os.path.join(ref_dir,'wallet-enc.dat')
-# 		for wf,enc in (unenc_wf,False),(enc_wf,True):
-# 			for w,o,pk in (
-# 				('addresses','a',False),
-# 				('private keys','k',True),
-# 				('json dump','j',True)
-# 			):
-# 				ed = '(%sencrypted wallet, %s)' % (('un','')[bool(enc)],w)
-# 				t = MMGenExpect(name,'mmgen-pywallet', args +
-# 						['-'+o,'-d',cfg['tmpdir']] + [wf], extra_desc=ed)
-# 				if ni: continue
-# 				if pk and enc and not ni:
-# 					t.expect('Enter password: ',cfg['wpasswd']+'\n')
-# 				t.written_to_file(capfirst(w),oo=True)
-# 				if not ni: ok()
-
 	# Saved reference file tests
 	def ref_wallet_conv(self,name):
 		wf = os.path.join(ref_dir,cfg['ref_wallet'])
@@ -1685,6 +1671,7 @@ class MMGenTestSuite(object):
 				m = grnbg("Answer 'y' at the interactive prompt if Seed ID is")
 				n = cyan(cfg['seed_id'])
 				msg('\n%s %s' % (m,n))
+			if wtype == 'hic_wallet_old' and opt.profile: msg('')
 			t = MMGenExpect(name,'mmgen-walletchk',
 				add_args + slarg + hparg + of_arg + ic_arg,
 				extra_desc=edesc)
@@ -1789,6 +1776,7 @@ class MMGenTestSuite(object):
 		t.close()
 		ok()
 		# back check of result
+		if opt.profile: msg('')
 		self.walletchk(name,wf,pf=None,
 				desc='mnemonic data',
 				sid=cfg['seed_id'],
@@ -1844,6 +1832,7 @@ class MMGenTestSuite(object):
 		if desc == 'hidden incognito data':
 			add_args += uopts_chk
 			wf = None
+		if opt.profile: msg('')
 		self.walletchk(name,wf,pf=pf,
 			desc=desc,sid=cfg['seed_id'],pw=pw,
 			add_args=add_args,

+ 1 - 1
test/tooltest.py

@@ -124,7 +124,7 @@ If no command is given, the whole suite of tests is run.
 """
 }
 
-cmd_args = opts.init(opts_data,add_opts=['exact_output'])
+cmd_args = opts.init(opts_data,add_opts=['exact_output','profile'])
 
 if opt.system: sys.path.pop(0)