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 9 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:
 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:
 Install the Python Cryptography Toolkit:
 
 
 		$ sudo pip install pycrypto
 		$ sudo pip install pycrypto
 
 
-Install the secp256k1 library
+Install the secp256k1 library:
 
 
 		$ git clone https://github.com/bitcoin-core/secp256k1.git
 		$ git clone https://github.com/bitcoin-core/secp256k1.git
 		$ cd secp256k1
 		$ cd secp256k1

+ 3 - 3
mmgen/addr.py

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

+ 17 - 10
mmgen/bitcoin.py

@@ -51,13 +51,15 @@ b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
 # The 'zero address':
 # The 'zero address':
 # 1111111111111111111114oLvT2 (use step2 = ('0' * 40) to generate)
 # 1111111111111111111114oLvT2 (use step2 = ('0' * 40) to generate)
 
 
+import mmgen.globalvars as g
+
 def pubhex2hexaddr(pubhex):
 def pubhex2hexaddr(pubhex):
 	step1 = sha256(unhexlify(pubhex)).digest()
 	step1 = sha256(unhexlify(pubhex)).digest()
 	return hashlib_new('ripemd160',step1).hexdigest()
 	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()
 	step1 = sha256(unhexlify(hexaddr2)).digest()
 	step2 = sha256(step1).hexdigest()
 	step2 = sha256(step1).hexdigest()
 	pubkey = hexaddr2 + step2[:8]
 	pubkey = hexaddr2 + step2[:8]
@@ -65,9 +67,8 @@ def hexaddr2addr(hexaddr, vers_num='00'):
 	return ('1' * lzeroes) + _numtob58(int(pubkey,16))
 	return ('1' * lzeroes) + _numtob58(int(pubkey,16))
 
 
 def verify_addr(addr,verbose=False,return_hex=False):
 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)
 		num = _b58tonum(addr)
 		if num == False: break
 		if num == False: break
 		addr_hex = '{:050x}'.format(num)
 		addr_hex = '{:050x}'.format(num)
@@ -145,7 +146,7 @@ def b58decode_pad(s):
 # Compressed address support:
 # Compressed address support:
 
 
 def wif2hex(wif):
 def wif2hex(wif):
-	compressed = wif[0] != '5'
+	compressed = wif[0] != ('5','9')[g.testnet]
 	idx = (66,68)[bool(compressed)]
 	idx = (66,68)[bool(compressed)]
 	num = _b58tonum(wif)
 	num = _b58tonum(wif)
 	if num == False: return False
 	if num == False: return False
@@ -153,19 +154,25 @@ def wif2hex(wif):
 	if compressed and key[66:68] != '01': return False
 	if compressed and key[66:68] != '01': return False
 	round1 = sha256(unhexlify(key[:idx])).digest()
 	round1 = sha256(unhexlify(key[:idx])).digest()
 	round2 = sha256(round1).hexdigest()
 	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):
 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()
 	step2 = sha256(unhexlify(step1)).digest()
 	step3 = sha256(step2).hexdigest()
 	step3 = sha256(step2).hexdigest()
 	key = step1 + step3[:8]
 	key = step1 + step3[:8]
 	return _numtob58(int(key,16))
 	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):
 def privnum2pubhex(numpriv,compressed=False):
 	pko = ecdsa.SigningKey.from_secret_exponent(numpriv,_secp256k1)
 	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())
 	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']
 		p = ('03','02')[pubkey[-1] in '02468ace']
 		return p+pubkey[:64]
 		return p+pubkey[:64]
 	else:
 	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')
 bogus_wallet_data    = os.getenv('MMGEN_BOGUS_WALLET_DATA')
 disable_hold_protect = os.getenv('MMGEN_DISABLE_HOLD_PROTECT')
 disable_hold_protect = os.getenv('MMGEN_DISABLE_HOLD_PROTECT')
 color = (False,True)[sys.stdout.isatty() and not os.getenv('MMGEN_DISABLE_COLOR')]
 color = (False,True)[sys.stdout.isatty() and not os.getenv('MMGEN_DISABLE_COLOR')]
+testnet = (False,True)[bool(os.getenv('MMGEN_TESTNET'))]
 
 
 proj_name = 'MMGen'
 proj_name = 'MMGen'
 prog_name = os.path.basename(sys.argv[0])
 prog_name = os.path.basename(sys.argv[0])
@@ -63,12 +64,14 @@ incompatible_opts = (
 	('label','keep_label'),
 	('label','keep_label'),
 	('tx_id', 'info'),
 	('tx_id', 'info'),
 	('tx_id', 'terse_info'),
 	('tx_id', 'terse_info'),
+	('batch', 'rescan'),
 )
 )
 
 
 min_screen_width = 80
 min_screen_width = 80
+minconf = 1
 
 
 # Global value sets user opt
 # 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'
 keyconv_exec = 'keyconv'
 
 

+ 16 - 10
mmgen/main_addrimport.py

@@ -25,14 +25,17 @@ import time
 from mmgen.common import *
 from mmgen.common import *
 from mmgen.addr import AddrList,KeyAddrList
 from mmgen.addr import AddrList,KeyAddrList
 
 
+# In batch mode, bitcoind just rescans each address separately anyway, so make
+# --batch and --rescan incompatible.
+
 opts_data = {
 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),
                      tracking wallet""".format(pnm=g.proj_name),
 	'usage':'[opts] [mmgen address file]',
 	'usage':'[opts] [mmgen address file]',
 	'options': """
 	'options': """
 -h, --help         Print this help message
 -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
 -k, --keyaddr-file Address source is a key-address file
 -q, --quiet        Suppress warnings
 -q, --quiet        Suppress warnings
 -r, --rescan       Rescan the blockchain.  Required if address to import is
 -r, --rescan       Rescan the blockchain.  Required if address to import is
@@ -42,6 +45,8 @@ opts_data = {
 	'notes': """\n
 	'notes': """\n
 This command can also be used to update the comment fields of addresses already
 This command can also be used to update the comment fields of addresses already
 in the tracking wallet.
 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:
 	if e.idx:
 		label = '%s:%s' % (ai.seed_id,e.idx)
 		label = '%s:%s' % (ai.seed_id,e.idx)
 		if e.label: label += ' ' + e.label
 		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:
 	if opt.batch:
 		arg_list.append((e.addr,label,False))
 		arg_list.append((e.addr,label,False))
 	elif opt.rescan:
 	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.daemon = True
 		t.start()
 		t.start()
 
 
@@ -131,7 +139,7 @@ for n,e in enumerate(ai.data):
 			if t.is_alive():
 			if t.is_alive():
 				elapsed = int(time.time() - start)
 				elapsed = int(time.time() - start)
 				count = '%s/%s:' % (n+1, ai.num_addrs)
 				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)
 				time.sleep(1)
 			else:
 			else:
 				if err_flag: die(2,'\nImport failed')
 				if err_flag: die(2,'\nImport failed')
@@ -140,12 +148,10 @@ for n,e in enumerate(ai.data):
 	else:
 	else:
 		import_address(e.addr,label,False)
 		import_address(e.addr,label,False)
 		count = '%s/%s:' % (n+1, ai.num_addrs)
 		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')
 		if err_flag: die(2,'\nImport failed')
 		msg(' - OK')
 		msg(' - OK')
 
 
 if opt.batch:
 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))
 	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, --comment-file= f Source the transaction's comment from file 'f'
 -C, --tx-confs=     c Desired number of confirmations (default: {g.tx_confs})
 -C, --tx-confs=     c Desired number of confirmations (default: {g.tx_confs})
 -d, --outdir=       d Specify an alternate directory 'd' for output
 -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))
 -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
 -i, --info            Display unspent outputs and exit
 -q, --quiet           Suppress warnings; overwrite files without prompting
 -q, --quiet           Suppress warnings; overwrite files without prompting
 -v, --verbose         Produce more verbose output
 -v, --verbose         Produce more verbose output
@@ -199,7 +200,7 @@ if not opt.info:
 
 
 	fee_estimate = get_fee_estimate()
 	fee_estimate = get_fee_estimate()
 
 
-tw = MMGenTrackingWallet()
+tw = MMGenTrackingWallet(minconf=opt.minconf)
 tw.view_and_sort()
 tw.view_and_sort()
 tw.display_total()
 tw.display_total()
 
 

+ 1 - 2
mmgen/main_txsign.py

@@ -227,10 +227,9 @@ if opt.mmgen_keys_from_file:
 
 
 if opt.keys_from_file:
 if opt.keys_from_file:
 	l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True)
 	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')
 	if kal: kl.remove_dups(kal,key='wif')
 	kl.generate_addrs()
 	kl.generate_addrs()
-# pp_die(kl)
 
 
 tx_num_str = ''
 tx_num_str = ''
 for tx_num,tx_file in enumerate(tx_files,1):
 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
 				m = "'%s': addr idx cannot be less than one" % num
 			else:
 			else:
 				return me
 				return me
+
 		return cls.init_fail(m,on_fail)
 		return cls.init_fail(m,on_fail)
 
 
 class AddrIdxList(list,InitErrors):
 class AddrIdxList(list,InitErrors):
@@ -349,7 +350,7 @@ class BTCAddr(str,Hilite,InitErrors):
 		cls.arg_chk(cls,on_fail)
 		cls.arg_chk(cls,on_fail)
 		me = str.__new__(cls,s)
 		me = str.__new__(cls,s)
 		from mmgen.bitcoin import verify_addr
 		from mmgen.bitcoin import verify_addr
-		if verify_addr(s):
+		if type(s) in (str,unicode,BTCAddr) and verify_addr(s):
 			return me
 			return me
 		else:
 		else:
 			m = "'%s': value is not a Bitcoin address" % s
 			m = "'%s': value is not a Bitcoin address" % s
@@ -375,13 +376,14 @@ class SeedID(str,Hilite,InitErrors):
 		if seed:
 		if seed:
 			from mmgen.seed import Seed
 			from mmgen.seed import Seed
 			from mmgen.util import make_chksum_8
 			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:
 		elif sid:
 			from string import hexdigits
 			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)
 		return cls.init_fail(m,on_fail)
 
 
 class MMGenID(str,Hilite,InitErrors):
 class MMGenID(str,Hilite,InitErrors):
@@ -395,9 +397,9 @@ class MMGenID(str,Hilite,InitErrors):
 		s = str(s)
 		s = str(s)
 		if ':' in s:
 		if ':' in s:
 			a,b = s.split(':',1)
 			a,b = s.split(':',1)
-			sid = SeedID(sid=a,on_fail='return')
+			sid = SeedID(sid=a,on_fail='silent')
 			if sid:
 			if sid:
-				idx = AddrIdx(b,on_fail='return')
+				idx = AddrIdx(b,on_fail='silent')
 				if idx:
 				if idx:
 					return str.__new__(cls,'%s:%s' % (sid,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
 rpc.py:  Bitcoin RPC library for the MMGen suite
 """
 """
 
 
-import httplib,base64,json,decimal
+import httplib,base64,json
 
 
 from mmgen.common import *
 from mmgen.common import *
+from decimal import Decimal
+from mmgen.obj import BTCAmt
 
 
 class BitcoinRPCConnection(object):
 class BitcoinRPCConnection(object):
 
 
@@ -30,7 +32,7 @@ class BitcoinRPCConnection(object):
 
 
 	def __init__(
 	def __init__(
 				self,
 				self,
-				host='localhost',port=8332,
+				host='localhost',port=(8332,18332)[g.testnet],
 				user=None,passwd=None,auth_cookie=None,
 				user=None,passwd=None,auth_cookie=None,
 			):
 			):
 
 
@@ -64,7 +66,7 @@ class BitcoinRPCConnection(object):
 		for k in cf:
 		for k in cf:
 			if k in kwargs and kwargs[k]: cf[k] = kwargs[k]
 			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']:
 		if cf['batch']:
 			p = [{'method':cmd,'params':r,'id':n} for n,r in enumerate(args[0],1)]
 			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.py debug ===')
 		dmsg('    RPC POST data ==> %s\n' % p)
 		dmsg('    RPC POST data ==> %s\n' % p)
 
 
-		from mmgen.obj import BTCAmt
 		caller = self
 		caller = self
 		class MyJSONEncoder(json.JSONEncoder):
 		class MyJSONEncoder(json.JSONEncoder):
 			def default(self, obj):
 			def default(self, obj):
@@ -94,14 +95,14 @@ class BitcoinRPCConnection(object):
 # 			print(dump)
 # 			print(dump)
 
 
 		try:
 		try:
-			c.request('POST', '/', json.dumps(p,cls=MyJSONEncoder), {
+			hc.request('POST', '/', json.dumps(p,cls=MyJSONEncoder), {
 				'Host': self.host,
 				'Host': self.host,
 				'Authorization': 'Basic ' + base64.b64encode(self.auth_str)
 				'Authorization': 'Basic ' + base64.b64encode(self.auth_str)
 			})
 			})
 		except Exception as e:
 		except Exception as e:
 			return die_maybe(None,2,'%s\nUnable to connect to bitcoind' % 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:
 		if r.status != 200:
 			e1 = r.read()
 			e1 = r.read()
@@ -118,7 +119,7 @@ class BitcoinRPCConnection(object):
 		if not r2:
 		if not r2:
 			return die_maybe(r,2,'Error: empty reply')
 			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)
 		r3 = json.loads(r2.decode('utf8'), parse_float=Decimal)
 		ret = []
 		ret = []
 
 

+ 0 - 3
mmgen/term.py

@@ -23,9 +23,6 @@ term.py:  Terminal-handling routines for the MMGen suite
 import os,struct
 import os,struct
 from mmgen.common import *
 from mmgen.common import *
 
 
-CUR_SHOW = '\033[?25h'
-CUR_HIDE = '\033[?25l'
-
 def _kb_hold_protect_unix():
 def _kb_hold_protect_unix():
 
 
 	fd = sys.stdin.fileno()
 	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)
 	return read_from_file(os.path.join(cfg['tmpdir'],fn),binary=binary)
 
 
 def ok():
 def ok():
+	if opt.profile: return
 	if opt.verbose or opt.exact_output:
 	if opt.verbose or opt.exact_output:
 		sys.stderr.write(green('OK\n'))
 		sys.stderr.write(green('OK\n'))
 	else: msg(' OK')
 	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]']),
 	('listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']),
 	('getbalance',   ['minconf [int=1]']),
 	('getbalance',   ['minconf [int=1]']),
 	('txview',       ['<{} TX file> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]']),
 	('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]']),
 	('add_label',       ['<{} address> [str]'.format(pnm),'<label> [str]']),
 	('remove_label',    ['<{} address> [str]'.format(pnm)]),
 	('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):
 	for d in c.listunspent(0):
 		mmaddr,comment = split2(d['account'])
 		mmaddr,comment = split2(d['account'])
 		if usr_addr_list and (mmaddr not in usr_addr_list): continue
 		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(':','_')
 			key = mmaddr.replace(':','_')
 			if key in addrs:
 			if key in addrs:
 				if addrs[key][2] != d['address']:
 				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:
 		for acct in accts:
 			mmaddr,comment = split2(acct)
 			mmaddr,comment = split2(acct)
 			if usr_addr_list and (mmaddr not in usr_addr_list): continue
 			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(':','_')
 				key = mmaddr.replace(':','_')
 				if key not in addrs:
 				if key not in addrs:
 					if showbtcaddrs: save_a.append([acct])
 					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])
 		die(0,('No addresses with balances!','No tracked addresses!')[showempty])
 
 
 	fs = ('{mid} {lbl} {amt}','{mid} {addr} {lbl} {amt}')[showbtcaddrs]
 	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
 	max_lbl_len =  max(len(addrs[k][1]) for k in addrs) or 7
 	out = [fs.format(
 	out = [fs.format(
 			mid=MMGenID.fmtc('MMGenID',width=max_mmid_len),
 			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 = ''
 	old_sid = ''
 	def s_mmgen(k): return '{:>0{w}}'.format(k,w=AddrIdx.max_digits+9) # TODO
 	def s_mmgen(k): return '{:>0{w}}'.format(k,w=AddrIdx.max_digits+9) # TODO
 	for k in sorted(addrs,key=s_mmgen):
 	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(
 		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),
 			addr=(addrs[k][2].fmt(color=True) if showbtcaddrs else None),
 			lbl=addrs[k][1].fmt(width=max_lbl_len,color=True,nullrepl='-'),
 			lbl=addrs[k][1].fmt(width=max_lbl_len,color=True,nullrepl='-'),
 			amt=addrs[k][0].fmt('3.0',color=True)))
 			amt=addrs[k][0].fmt('3.0',color=True)))
@@ -460,9 +461,9 @@ def txview(infile,pager=False,terse=False):
 	tx = MMGenTX(infile)
 	tx = MMGenTX(infile)
 	tx.view(pager,pause=False,terse=terse)
 	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
 	from mmgen.tw import MMGenTrackingWallet
-	tw = MMGenTrackingWallet()
+	tw = MMGenTrackingWallet(minconf=minconf)
 	tw.do_sort(sort,reverse=reverse)
 	tw.do_sort(sort,reverse=reverse)
 	out = tw.format_for_printing(color=True) if wide else tw.format_for_display()
 	out = tw.format_for_printing(color=True) if wide else tw.format_for_display()
 	do_pager(out) if pager else sys.stdout.write(out)
 	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.common import *
 from mmgen.obj import *
 from mmgen.obj import *
 from mmgen.term import get_char
 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):
 def parse_tw_acct_label(s):
 	ret = s.split(None,1)
 	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):
 class MMGenTWOutput(MMGenListItem):
 	attrs_reassign = 'label','skip'
 	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'
 	sort_keys = 'addr','age','amt','txid','mmid'
 
 
-	def __init__(self):
+	def __init__(self,minconf=1):
 		self.unspent      = []
 		self.unspent      = []
 		self.fmt_display  = ''
 		self.fmt_display  = ''
 		self.fmt_print    = ''
 		self.fmt_print    = ''
@@ -57,6 +58,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		self.group        = False
 		self.group        = False
 		self.show_days    = True
 		self.show_days    = True
 		self.show_mmid    = True
 		self.show_mmid    = True
+		self.minconf      = minconf
 		self.get_data()
 		self.get_data()
 		self.sort_key     = 'age'
 		self.sort_key     = 'age'
 		self.do_sort()
 		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
 		if g.bogus_wallet_data: # for debugging purposes only
 			us_rpc = eval(get_data_from_file(g.bogus_wallet_data))
 			us_rpc = eval(get_data_from_file(g.bogus_wallet_data))
 		else:
 		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')
 #		write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
 #		sys.exit()
 #		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))
 			my_raw_input(m1+'\n'+m2.format(g.min_screen_width))
 
 
 	def display(self):
 	def display(self):
+		if opt.clear_screen: msg(CUR_HOME+ERASE_ALL)
 		msg(self.format_for_display())
 		msg(self.format_for_display())
 
 
 	def format_for_display(self):
 	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):
 		for n,i in enumerate(unsp):
 			addr_dots = '|' + '.'*33
 			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' % (
 				addr_out = '%s %s' % (
 					type(i.addr).fmtc(addr_dots,width=btaddr_w,color=True) if i.skip == 'addr' \
 					type(i.addr).fmtc(addr_dots,width=btaddr_w,color=True) if i.skip == 'addr' \
 						else i.addr.fmt(width=btaddr_w,color=True),
 						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:
 			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' \
 			tx = ' ' * (tx_w-4) + '|...' if i.skip == 'txid' \
 					else i.txid[:tx_w-len(txdots)]+txdots
 					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),
 		out = [fs % ('Num','Tx ID,Vout','Address'.ljust(34),'MMGen ID'.ljust(15),
 			'Amount(BTC)','Conf.','Age(d)', 'Label')]
 			'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):
 		for n,i in enumerate(self.unspent):
 			addr = '=' if i.skip == 'addr' and self.group else i.addr.fmt(color=color)
 			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)
 			tx = ' ' * 63 + '=' if i.skip == 'txid' and self.group else str(i.txid)
 			s = fs % (str(n+1)+')', tx+','+str(i.vout),addr,
 			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.amt.fmt(color=color),i.confs,i.days,
 					i.label.hl(color=color) if i.label else
 					i.label.hl(color=color) if i.label else
 						MMGenAddrLabel.fmtc('',color=color,nullrepl='-',width=max_lbl_len))
 						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
 			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):
 			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))
 				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:
 			else:
 				while True:
 				while True:
 					s = my_raw_input("Enter label text (or 'q' to return to main menu): ")
 					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
 	# returns on failure
 	@classmethod
 	@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
 			if not BTCAddr(addr,on_fail='return'): return False
 		else:
 		else:
 			from mmgen.addr import AddrData
 			from mmgen.addr import AddrData

+ 2 - 1
mmgen/util.py

@@ -662,7 +662,8 @@ def get_bitcoind_auth_cookie():
 
 
 def bitcoin_connection():
 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))
 	cfg = get_bitcoind_cfg_options((user,passwd))
 	auth_cookie = get_bitcoind_auth_cookie()
 	auth_cookie = get_bitcoind_auth_cookie()
 
 

+ 115 - 49
test/gentest.py

@@ -35,73 +35,139 @@ start_mscolor()
 
 
 rounds = 100
 rounds = 100
 opts_data = {
 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': """
 	'options': """
 -h, --help         Print this help message
 -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
 -v, --verbose      Produce more verbose output
+-q, --quiet        Produce quieter output
 """,
 """,
 	'notes': """
 	'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'])
 cmd_args = opts.init(opts_data,add_opts=['exact_output'])
 
 
 if not 1 <= len(cmd_args) <= 2: opts.usage()
 if not 1 <= len(cmd_args) <= 2: opts.usage()
 
 
+urounds,fh = None,None
+dump = []
 if len(cmd_args) == 2:
 if len(cmd_args) == 2:
 	try:
 	try:
-		rounds = int(cmd_args[1])
-		assert rounds > 0
+		urounds = int(cmd_args[1])
+		assert urounds > 0
 	except:
 	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:
 try:
 	a,b = cmd_args[0].split(':')
 	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:
 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
 # Import these _after_ local path's been added to sys.path
 from mmgen.common import *
 from mmgen.common import *
 from mmgen.test import *
 from mmgen.test import *
+tn_desc = ('','.testnet')[g.testnet]
 
 
 start_mscolor()
 start_mscolor()
 
 
@@ -175,8 +176,8 @@ cfgs = {
 		'seed_len':        128,
 		'seed_len':        128,
 		'seed_id':         'FE3C6545',
 		'seed_id':         'FE3C6545',
 		'ref_bw_seed_id':  '33F10310',
 		'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',
 		'wpasswd':         'reference password',
 		'ref_wallet':      'FE3C6545-D782B529[128,1].mmdat',
 		'ref_wallet':      'FE3C6545-D782B529[128,1].mmdat',
 		'ic_wallet':       'FE3C6545-E29303EA-5E229E30[128,1].mmincog',
 		'ic_wallet':       'FE3C6545-E29303EA-5E229E30[128,1].mmincog',
@@ -201,8 +202,8 @@ cfgs = {
 		'seed_len':        192,
 		'seed_len':        192,
 		'seed_id':         '1378FC64',
 		'seed_id':         '1378FC64',
 		'ref_bw_seed_id':  'CE918388',
 		'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',
 		'wpasswd':         'reference password',
 		'ref_wallet':      '1378FC64-6F0F9BB4[192,1].mmdat',
 		'ref_wallet':      '1378FC64-6F0F9BB4[192,1].mmdat',
 		'ic_wallet':       '1378FC64-2907DE97-F980D21F[192,1].mmincog',
 		'ic_wallet':       '1378FC64-2907DE97-F980D21F[192,1].mmincog',
@@ -227,17 +228,17 @@ cfgs = {
 		'seed_len':        256,
 		'seed_len':        256,
 		'seed_id':         '98831F3A',
 		'seed_id':         '98831F3A',
 		'ref_bw_seed_id':  'B48CD7FC',
 		'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',
 		'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_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':       '98831F3A-5482381C-18460FB1[256,1].mmincog',
 		'ic_wallet_hex':   '98831F3A-1630A9F2-870376A9[256,1].mmincox',
 		'ic_wallet_hex':   '98831F3A-1630A9F2-870376A9[256,1].mmincox',
 
 
@@ -483,6 +484,7 @@ opts_data = {
 -n, --names         Display command names instead of descriptions.
 -n, --names         Display command names instead of descriptions.
 -I, --non-interactive Non-interactive operation (MS Windows mode)
 -I, --non-interactive Non-interactive operation (MS Windows mode)
 -p, --pause         Pause between tests, resuming on keypress.
 -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.
 -q, --quiet         Produce minimal output.  Suppress dependency info.
 -r, --resume=c      Resume at command 'c' after interrupted run
 -r, --resume=c      Resume at command 'c' after interrupted run
 -s, --system        Test scripts and modules installed on system rather
 -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)
 cmd_args = opts.init(opts_data)
 
 
+if opt.profile: opt.names = True
 if opt.resume: opt.skip_deps = True
 if opt.resume: opt.skip_deps = True
 if opt.log:
 if opt.log:
 	log_fd = open(log_file,'a')
 	log_fd = open(log_file,'a')
@@ -1040,7 +1043,10 @@ class MMGenTestSuite(object):
 			else:
 			else:
 				return
 				return
 
 
+		if opt.profile: start = time.time()
 		self.__class__.__dict__[cmd](*([self,cmd] + al))
 		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):
 	def generate_file_deps(self,cmd):
 		return [(str(n),e) for exts,n in cmd_data[cmd][2] for e in exts]
 		return [(str(n),e) for exts,n in cmd_data[cmd][2] for e in exts]
@@ -1410,6 +1416,7 @@ class MMGenTestSuite(object):
 			return
 			return
 		t.expect('Encrypt key list? (y/N): ','y')
 		t.expect('Encrypt key list? (y/N): ','y')
 		t.hash_preset('new key list','1')
 		t.hash_preset('new key list','1')
+#		t.passphrase_new('new key list','kafile password')
 		t.passphrase_new('new key list',cfg['kapasswd'])
 		t.passphrase_new('new key list',cfg['kapasswd'])
 		t.written_to_file('Secret keys',oo=True)
 		t.written_to_file('Secret keys',oo=True)
 		ok()
 		ok()
@@ -1558,27 +1565,6 @@ class MMGenTestSuite(object):
 		os.unlink(f1)
 		os.unlink(f1)
 		cmp_or_die(hincog_offset,int(o))
 		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
 	# Saved reference file tests
 	def ref_wallet_conv(self,name):
 	def ref_wallet_conv(self,name):
 		wf = os.path.join(ref_dir,cfg['ref_wallet'])
 		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")
 				m = grnbg("Answer 'y' at the interactive prompt if Seed ID is")
 				n = cyan(cfg['seed_id'])
 				n = cyan(cfg['seed_id'])
 				msg('\n%s %s' % (m,n))
 				msg('\n%s %s' % (m,n))
+			if wtype == 'hic_wallet_old' and opt.profile: msg('')
 			t = MMGenExpect(name,'mmgen-walletchk',
 			t = MMGenExpect(name,'mmgen-walletchk',
 				add_args + slarg + hparg + of_arg + ic_arg,
 				add_args + slarg + hparg + of_arg + ic_arg,
 				extra_desc=edesc)
 				extra_desc=edesc)
@@ -1789,6 +1776,7 @@ class MMGenTestSuite(object):
 		t.close()
 		t.close()
 		ok()
 		ok()
 		# back check of result
 		# back check of result
+		if opt.profile: msg('')
 		self.walletchk(name,wf,pf=None,
 		self.walletchk(name,wf,pf=None,
 				desc='mnemonic data',
 				desc='mnemonic data',
 				sid=cfg['seed_id'],
 				sid=cfg['seed_id'],
@@ -1844,6 +1832,7 @@ class MMGenTestSuite(object):
 		if desc == 'hidden incognito data':
 		if desc == 'hidden incognito data':
 			add_args += uopts_chk
 			add_args += uopts_chk
 			wf = None
 			wf = None
+		if opt.profile: msg('')
 		self.walletchk(name,wf,pf=pf,
 		self.walletchk(name,wf,pf=pf,
 			desc=desc,sid=cfg['seed_id'],pw=pw,
 			desc=desc,sid=cfg['seed_id'],pw=pw,
 			add_args=add_args,
 			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)
 if opt.system: sys.path.pop(0)