Browse Source

gentest.py: complete rewrite, move tool selection logic to altcoin.py

The MMGen Project 5 years ago
parent
commit
7451f122ed
3 changed files with 375 additions and 239 deletions
  1. 82 9
      mmgen/altcoin.py
  2. 277 214
      test/gentest.py
  3. 16 16
      test/test-release.sh

+ 82 - 9
mmgen/altcoin.py

@@ -510,16 +510,79 @@ class CoinInfo(object):
 		return tt
 
 	trust_override = {'BTC':3,'BCH':3,'LTC':3,'DASH':1,'EMC':2}
+
+	@classmethod
+	def get_test_support(cls,coin,addr_type,network,tool=None,verbose=False):
+		"""
+		If requested tool supports coin/addr_type/network triplet, return tool name.
+		If 'tool' is None, return tool that supports coin/addr_type/network triplet.
+		Return None on failure.
+		"""
+		tool_arg = tool
+		all_tools = [tool] if tool else list(cls.external_tests[network].keys())
+		coin = coin.upper()
+
+		for tool in all_tools:
+			if coin in cls.external_tests[network][tool]:
+				break
+		else:
+			if verbose:
+				m1 = 'Requested tool {t!r} does not support coin {c} on network {n}'
+				m2 = 'No test tool found for coin {c} on network {n}'
+				m = m1 if tool_arg else m2
+				msg(m.format(t=tool,c=coin,n=network))
+			return None
+
+		if addr_type == 'zcash_z':
+			if tool_arg in (None,'zcash-mini'):
+				return 'zcash-mini'
+			else:
+				if verbose:
+					m = "Address type {a!r} supported only by tool 'zcash-mini'"
+					msg(m.format(a=addr_type))
+				return None
+
+		try:
+			assert (
+				cls.external_tests_blacklist[addr_type][tool] == True
+				or coin in cls.external_tests_blacklist[addr_type][tool] )
+		except:
+			pass
+		else:
+			if verbose:
+				m = 'Tool {t!r} blacklisted for coin {c}, addr_type {a!r}'
+				msg(m.format(t=tool,c=coin,a=addr_type))
+			return None
+
+		if tool_arg: # skip whitelists
+			return tool
+
+		if addr_type in ('segwit','bech32'):
+			st = cls.external_tests_segwit_whitelist
+			if addr_type in st and coin in st[addr_type]:
+				return tool
+			else:
+				if verbose:
+					m1 = 'Requested tool {t!r} does not support coin {c}, addr_type {a!r}, on network {n}'
+					m2 = 'No test tool found supporting coin {c}, addr_type {a!r}, on network {n}'
+					m = m1 if tool_arg else m2
+					msg(m.format(t=tool,c=coin,n=network,a=addr_type))
+				return None
+
+		return tool
+
 	external_tests = {
 		'mainnet': {
-			'moneropy': ('XMR',),
+			# List in order of preference.
+			# If 'tool' is not specified, the first tool supporting the coin will be selected.
 			'pycoin': (
-				# broken: DASH - only compressed, LTC segwit old fmt
+				'DASH', # only compressed
 				'BTC','LTC','VIA','FTC','DOGE','MEC',
 				'JBS','MZC','RIC','DFC','FAI','ARG','ZEC','DCR'),
 			'ethkey': ('ETH','ETC'),
-			'zcash_mini': ('ZEC',),
-			'keyconv': ( # all supported by vanitygen-plus 'keyconv' util
+			'zcash-mini': ('ZEC',),
+			'moneropy': ('XMR',),
+			'keyconv': (
 				# broken: PIVX
 				'42','AC','AIB','ANC','ARS','ATMOS','AUR','BLK','BQC','BTC','TEST','BTCD','CCC','CCN','CDN',
 				'CLAM','CNC','CNOTE','CON','CRW','DEEPONION','DGB','DGC','DMD','DOGED','DOGE','DOPE',
@@ -531,16 +594,26 @@ class CoinInfo(object):
 		},
 		'testnet': {
 			'pycoin': {
-				# broken: DASH - only compressed { 'DASH':'tDASH' }
+				'DASH':'tDASH', # only compressed
 				'BTC':'XTN','LTC':'XLT','VIA':'TVI','FTC':'FTX','DOGE':'XDT','DCR':'DCRT'
 				},
 			'ethkey': {},
 			'keyconv': {}
 		}
 	}
-	external_tests_segwit_compressed = {
-		'segwit': ('BTC'),
+	external_tests_segwit_whitelist = {
+		# Whitelists apply to the *first* tool in cls.external_tests supporting the given coin/addr_type.
+		# They're ignored if specific tool is requested.
+		'segwit': ('BTC',), # LTC Segwit broken on pycoin: uses old fmt
+		'bech32': ('BTC','LTC'),
 		'compressed': (
-		'BTC','LTC','VIA','FTC','DOGE','DASH','MEC','MYR','UNO',
-		'JBS','MZC','RIC','DFC','FAI','ARG','ZEC','DCR','ZEC'),
+			'BTC','LTC','VIA','FTC','DOGE','DASH','MEC','MYR','UNO',
+			'JBS','MZC','RIC','DFC','FAI','ARG','ZEC','DCR','ZEC'
+		),
+	}
+	external_tests_blacklist = {
+		# Unconditionally block testing of the given coin/addr_type with given tool, or all coins if True
+		'legacy': { 'pycoin': ('DASH',) },
+		'segwit': { 'pycoin': ('LTC',), 'keyconv': True },
+		'bech32': { 'keyconv': True },
 	}

+ 277 - 214
test/gentest.py

@@ -28,53 +28,82 @@ os.environ['MMGEN_TEST_SUITE'] = '1'
 
 # Import these _after_ local path's been added to sys.path
 from mmgen.common import *
-from mmgen.obj import MMGenAddrType
 
 rounds = 100
 opts_data = {
 	'text': {
-		'desc': 'Test address generation in various ways',
+		'desc': 'Test address generation of the MMGen suite in various ways',
 		'usage':'[options] [spec] [rounds | dump file]',
 		'options': """
 -h, --help       Print this help message
--a, --all        Test all supported coins for external generator 'ext'
+-a, --all        Test all coins supported by specified external tool
 -k, --use-internal-keccak-module Force use of the internal keccak module
 --, --longhelp   Print help message for long options (common options)
 -q, --quiet      Produce quieter output
--t, --type=t     Specify address type (valid options: 'compressed','segwit','zcash_z')
+-t, --type=t     Specify address type (e.g. 'compressed','segwit','zcash_z','bech32')
 -v, --verbose    Produce more verbose output
 """,
 	'notes': """
-    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' - bitcoincore.org's secp256k1 library (default from v0.8.6)
+TEST TYPES:
+
+   A/B:     {prog} A:B [rounds]  (compare key generators A and B)
+   Speed:   {prog} A [rounds]    (test speed of key generator A)
+   Compare: {prog} A <dump file> (compare generator A to wallet dump)
+
+     where A and B are one of:
+       '1' - native Python ECDSA library (slow), or
+       '2' - bitcoincore.org's libsecp256k1 library (default);
+     or:
+       B is name of an external tool (see below) or 'ext'.
+       If B is 'ext', the external tool will be chosen automatically.
 
 EXAMPLES:
-  {prog} 1:2 100
-    (compare output of native Python ECDSA with secp256k1 library, 100 rounds)
-  {prog} 2:ext 100
-    (compare output of secp256k1 library with external library (see below), 100 rounds)
-  {prog} 2 1000
-    (test speed of secp256k1 library address generation, 1000 rounds)
-  {prog} 2 my.dump
-    (compare addrs generated with secp256k1 library to {dn} wallet dump)
-
-  External libraries required for the 'ext' generator:
-    + ethkey     (for ETH,ETC)           https://github.com/paritytech/parity-ethereum
-    + zcash-mini (for zcash_z addresses) https://github.com/FiloSottile/zcash-mini
-    + moneropy   (for Monero addresses)  https://github.com/bigreddmachine/MoneroPy
-    + pycoin     (for supported coins)   https://github.com/richardkiss/pycoin
-    + keyconv    (for all other coins)   https://github.com/exploitagency/vanitygen-plus
-                 ('keyconv' generates uncompressed addresses only)
+
+  Compare addresses generated by native Python ECDSA library and libsecp256k1,
+  100 rounds:
+  $ {prog} 1:2 100
+
+  Compare mmgen-secp256k1 Segwit address generation to pycoin library for all
+  supported coins, 100 rounds:
+  $ {prog} --all --type=segwit 2:pycoin 100
+
+  Compare mmgen-secp256k1 address generation to keyconv tool for all
+  supported coins, 100 rounds:
+  $ {prog} --all --type=compressed 2:keyconv 100
+
+  Compare mmgen-secp256k1 XMR address generation to configured external tool,
+  10 rounds:
+  $ {prog} --coin=xmr 2:ext 10
+
+  Test speed of mmgen-secp256k1 address generation, 10,000 rounds:
+  $ {prog} 2 10000
+
+  Compare mmgen-secp256k1-generated bech32 addrs to {dn} wallet dump:
+  $ {prog} --type=bech32 2 bech32wallet.dump
+
+Supported external tools:
+
+  + ethkey (for ETH,ETC)
+    https://github.com/paritytech/parity-ethereum
+    (build with 'cargo build -p ethkey-cli --release')
+
+  + zcash-mini (for Zcash Z-addresses)
+    https://github.com/FiloSottile/zcash-mini
+
+  + moneropy (for Monero addresses)
+    https://github.com/bigreddmachine/MoneroPy
+
+  + pycoin (for supported coins)
+    https://github.com/richardkiss/pycoin
+
+  + keyconv (for supported coins)
+    https://github.com/exploitagency/vanitygen-plus
+    ('keyconv' does not generate Segwit addresses)
 """
 	},
 	'code': {
 		'notes': lambda s: s.format(
-			prog='gentest.py',
+			prog='test/gentest.py',
 			pnm=g.proj_name,
 			snum=rounds,
 			dn=g.proto.daemon_name)
@@ -85,98 +114,120 @@ sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
 
 cmd_args = opts.init(opts_data,add_opts=['exact_output','use_old_ed25519'])
 
-if not 1 <= len(cmd_args) <= 2: opts.usage()
-
-addr_type = MMGenAddrType(opt.type or g.proto.dfl_mmtype)
-
-from collections import namedtuple
-ep = namedtuple('external_prog_output',['wif','addr','vk'])
+if not 1 <= len(cmd_args) <= 2:
+	opts.usage()
 
 from subprocess import run,PIPE,DEVNULL
 def get_cmd_output(cmd,input=None):
 	return run(cmd,input=input,stdout=PIPE,stderr=DEVNULL).stdout.decode().splitlines()
 
-def ethkey_sec2addr(sec):
-	o = get_cmd_output(['ethkey','info',sec])
-	return ep(o[0].split()[1],o[-1].split()[1],None)
-
-def keyconv_sec2addr(sec):
-	o = get_cmd_output(['keyconv','-C',g.coin,sec.wif])
-	return ep(o[1].split()[1],o[0].split()[1],None)
-
-def zcash_mini_sec2addr(sec):
-	o = get_cmd_output(['zcash-mini','-key','-simple'],input=(sec.wif+'\n').encode())
-	return ep(o[1],o[0],o[-1])
-
-def pycoin_sec2addr(sec):
-	coin = ci.external_tests['testnet']['pycoin'][g.coin] if g.testnet else g.coin
-	network = network_for_netcode(coin)
-	key = network.keys.private(secret_exponent=int(sec,16),is_compressed=addr_type.name != 'legacy')
-	if key is None:
-		die(1,"can't parse {}".format(sec))
-	if addr_type.name in ('segwit','bech32'):
-		hash160_c = key.hash160(is_compressed=True)
-		if addr_type.name == 'segwit':
-			p2sh_script = network.contract.for_p2pkh_wit(hash160_c)
-			addr = network.address.for_p2s(p2sh_script)
-		else:
-			addr = network.address.for_p2pkh_wit(hash160_c)
-	else:
-		addr = key.address()
-	return ep(key.wif(),addr,None)
-
-def moneropy_sec2addr(sec):
-	sk_t,vk_t,addr_t = mp_acc.account_from_spend_key(sec) # VERY slow!
-	return ep(sk_t,addr_t,vk_t)
-
-# pycoin/networks/all.py pycoin/networks/legacy_networks.py
-def init_external_prog():
-	global b,b_desc,ext_prog,ext_sec2addr,eth,addr_type
-
-	def test_support(k):
-		if b == k: return True
-		if b != 'ext' and b != k: return False
-		if g.coin in ci.external_tests['mainnet'][k] and not g.testnet: return True
-		if g.coin in ci.external_tests['testnet'][k]: return True
-		return False
-
-	if b == 'zcash_mini' or addr_type.name == 'zcash_z':
-		ext_sec2addr = zcash_mini_sec2addr
-		ext_prog = 'zcash_mini'
+from collections import namedtuple
+gtr = namedtuple('gen_tool_result',['wif','addr','vk'])
+
+class GenTool(object): pass
+
+class GenToolEthkey(GenTool):
+	desc = 'ethkey'
+	def __init__(self):
+		init_coin('eth')
+		global addr_type
+		addr_type = MMGenAddrType('E')
+
+	def run(self,sec):
+		o = get_cmd_output(['ethkey','info',sec])
+		return gtr(o[0].split()[1],o[-1].split()[1],None)
+
+class GenToolKeyconv(GenTool):
+	desc = 'keyconv'
+	def run(self,sec):
+		o = get_cmd_output(['keyconv','-C',g.coin,sec.wif])
+		return gtr(o[1].split()[1],o[0].split()[1],None)
+
+class GenToolZcash_mini(GenTool):
+	desc = 'zcash-mini'
+	def __init__(self):
 		init_coin('zec')
+		global addr_type
 		addr_type = MMGenAddrType('Z')
-	elif test_support('ethkey'): # build with 'cargo build -p ethkey-cli --release'
-		ext_sec2addr = ethkey_sec2addr
-		ext_prog = 'ethkey'
-	elif test_support('pycoin'):
-		global network_for_netcode
+
+	def run(self,sec):
+		o = get_cmd_output(['zcash-mini','-key','-simple'],input=(sec.wif+'\n').encode())
+		return gtr(o[1],o[0],o[-1])
+
+class GenToolPycoin(GenTool):
+	"""
+	pycoin/networks/all.py pycoin/networks/legacy_networks.py
+	"""
+	desc = 'pycoin'
+	def __init__(self):
+		m = "Unable to import pycoin.networks.registry.  Is pycoin installed on your system?"
 		try:
 			from pycoin.networks.registry import network_for_netcode
 		except:
-			raise ImportError("Unable to import pycoin.networks.registry.  Is pycoin installed on your system?")
-		ext_sec2addr = pycoin_sec2addr
-		ext_prog = 'pycoin'
-	elif test_support('moneropy'):
-		global mp_acc
+			raise ImportError(m)
+		self.nfnc = network_for_netcode
+
+	def run(self,sec):
+		coin = ci.external_tests['testnet']['pycoin'][g.coin] if g.testnet else g.coin
+		network = self.nfnc(coin)
+		key = network.keys.private(secret_exponent=int(sec,16),is_compressed=addr_type.name != 'legacy')
+		if key is None:
+			die(1,"can't parse {}".format(sec))
+		if addr_type.name in ('segwit','bech32'):
+			hash160_c = key.hash160(is_compressed=True)
+			if addr_type.name == 'segwit':
+				p2sh_script = network.contract.for_p2pkh_wit(hash160_c)
+				addr = network.address.for_p2s(p2sh_script)
+			else:
+				addr = network.address.for_p2pkh_wit(hash160_c)
+		else:
+			addr = key.address()
+		return gtr(key.wif(),addr,None)
+
+class GenToolMoneropy(GenTool):
+	desc = 'moneropy'
+	def __init__(self):
+
+		m = "Unable to import moneropy.  Is moneropy installed on your system?"
 		try:
-			import moneropy.account as mp_acc
+			import moneropy.account
 		except:
-			raise ImportError("Unable to import moneropy.  Is moneropy installed on your system?")
-		ext_sec2addr = moneropy_sec2addr
+			raise ImportError(m)
+
+		self.mpa = moneropy.account
 		init_coin('xmr')
-		ext_prog = 'moneropy'
+
+		global addr_type
 		addr_type = MMGenAddrType('M')
-	elif test_support('keyconv'):
-		ext_sec2addr = keyconv_sec2addr
-		ext_prog = 'keyconv'
-	else:
-		m = '{}: coin supported by MMGen but unsupported by gentest.py for {}'
-		raise ValueError(m.format(g.coin,('mainnet','testnet')[g.testnet]))
-	b_desc = ext_prog
-	b = 'ext'
 
-def test_equal(a_addr,b_addr,in_bytes,sec,wif,a,b):
-	if a_addr != b_addr:
+	def run(self,sec):
+		sk_t,vk_t,addr_t = self.mpa.account_from_spend_key(sec) # VERY slow!
+		return gtr(sk_t,addr_t,vk_t)
+
+def get_tool(arg):
+
+	if arg not in ext_progs + ['ext']:
+		die(1,'{!r}: unsupported tool for network {}'.format(arg,g.network))
+
+	if opt.all:
+		if arg == 'ext':
+			die(1,"'--all' must be combined with a specific external testing tool")
+		return arg
+	else:
+		tool = ci.get_test_support(
+			g.coin,
+			addr_type.name,
+			g.network,
+			verbose = not opt.quiet,
+			tool = arg if arg in ext_progs else None )
+		if not tool:
+			sys.exit(2)
+		if arg in ext_progs and arg != tool:
+			sys.exit(3)
+		return tool
+
+def test_equal(desc,a_val,b_val,in_bytes,sec,wif,a_desc,b_desc):
+	if a_val != b_val:
 		fs = """
   {i:{w}}: {}
   {s:{w}}: {}
@@ -185,32 +236,27 @@ def test_equal(a_addr,b_addr,in_bytes,sec,wif,a,b):
   {b:{w}}: {}
 			"""
 		die(3,
-			red('\nERROR: Values do not match!')
+			red('\nERROR: {} do not match!').format(desc)
 			+ fs.format(
-				in_bytes.hex(), sec, wif, a_addr, b_addr,
-				i='input', s='sec key', W='WIF key', a=kg_a.desc, b=b_desc,
-				w=max(len(e) for e in (kg_a.desc,b_desc)) + 1
+				in_bytes.hex(), sec, wif, a_val, b_val,
+				i='input', s='sec key', W='WIF key', a=a_desc, b=b_desc,
+				w=max(len(e) for e in (a_desc,b_desc)) + 1
 		).rstrip())
 
-def compare_test():
-	for k in ('segwit','compressed'):
-		if b == 'ext' and addr_type.name == k and g.coin not in ci.external_tests_segwit_compressed[k]:
-			m = 'skipping - external program does not support {} for coin {}'
-			msg(m.format(addr_type.name.capitalize(),g.coin))
-			return
-	if 'ext_prog' in globals():
-		if g.coin not in ci.external_tests[('mainnet','testnet')[g.testnet]][ext_prog]:
-			msg("Coin '{}' incompatible with external generator '{}'".format(g.coin,ext_prog))
-			return
+def gentool_test(kg_a,kg_b,ag,rounds):
+
+	m = "Comparing address generators '{A}' and '{B}' for {N} {c} ({n}), addrtype {a!r}"
+	e = ci.get_entry(g.coin,g.network)
+	qmsg(green(m.format(
+		A = kg_a.desc,
+		B = kg_b.desc,
+		N = g.network,
+		c = g.coin,
+		n = e.name if e else '---',
+		a = addr_type.name )))
+
 	global last_t
 	last_t = time.time()
-	A = kg_a.desc
-	B = b_desc
-	if A == B:
-		msg('skipping - generation methods A and B are the same ({})'.format(A))
-		return
-	m = "Comparing address generators '{}' and '{}' for coin {}, addrtype {!r}"
-	qmsg(green(m.format(A,B,g.coin,addr_type.name)))
 
 	def do_compare_test(n,trounds,in_bytes):
 		global last_t
@@ -221,24 +267,23 @@ def compare_test():
 		a_ph = kg_a.to_pubhex(sec)
 		a_addr = ag.to_addr(a_ph)
 		a_vk = None
-		if b == 'ext':
-			ret = ext_sec2addr(sec)
-			tinfo = (in_bytes,sec,sec.wif,a,ext_prog)
-			test_equal(sec.wif,ret.wif,*tinfo)
-			test_equal(a_addr,ret.addr,*tinfo)
-			if ret.vk:
+		tinfo = (in_bytes,sec,sec.wif,kg_a.desc,kg_b.desc)
+		if isinstance(kg_b,GenTool):
+			b = kg_b.run(sec)
+			test_equal('WIF keys',sec.wif,b.wif,*tinfo)
+			test_equal('addresses',a_addr,b.addr,*tinfo)
+			if b.vk:
 				a_vk = ag.to_viewkey(a_ph)
-				test_equal(a_vk,ret.vk,*tinfo)
+				test_equal('view keys',a_vk,b.vk,*tinfo)
 		else:
 			b_addr = ag.to_addr(kg_b.to_pubhex(sec))
-			tinfo = (in_bytes,sec,sec.wif,a,b)
-			test_equal(a_addr,b_addr,*tinfo)
-		vmsg(ct_fs.format(b=in_bytes.hex(),k=sec.wif,v=a_vk,a=a_addr))
+			test_equal('addresses',a_addr,b_addr,*tinfo)
+		vmsg(fs.format(b=in_bytes.hex(),k=sec.wif,v=a_vk,a=a_addr))
 		qmsg_r('\rRound {}/{} '.format(n+1,trounds))
 
-	ct_fs   = ( '\ninput:    {b}\n%-9s {k}\naddr:     {a}\n',
-				'\ninput:    {b}\n%-9s {k}\nviewkey:  {v}\naddr:     {a}\n')[
-					'viewkey' in addr_type.extra_attrs] % (addr_type.wif_label + ':')
+	fs  = ( '\ninput:    {b}\n%-9s {k}\naddr:     {a}\n',
+			'\ninput:    {b}\n%-9s {k}\nviewkey:  {v}\naddr:     {a}\n')[
+				'viewkey' in addr_type.extra_attrs] % (addr_type.wif_label + ':')
 
 	# test some important private key edge cases:
 	edgecase_sks = (
@@ -259,9 +304,9 @@ def compare_test():
 		do_compare_test(i,rounds,os.urandom(32))
 	qmsg(green('\rOK            ' if opt.verbose else 'OK'))
 
-def speed_test():
+def speed_test(kg,ag,rounds):
 	m = "Testing speed of address generator '{}' for coin {}"
-	qmsg(green(m.format(kg_a.desc,g.coin)))
+	qmsg(green(m.format(kg.desc,g.coin)))
 	from struct import pack,unpack
 	seed = os.urandom(28)
 	qmsg('Incrementing key with each round')
@@ -274,101 +319,119 @@ def speed_test():
 			qmsg_r('\rRound {}/{} '.format(i+1,rounds))
 			last_t = time.time()
 		sec = PrivKey(seed+pack('I',i),compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type)
-		a_addr = ag.to_addr(kg_a.to_pubhex(sec))
-		vmsg('\nkey:  {}\naddr: {}\n'.format(sec.wif,a_addr))
+		addr = ag.to_addr(kg.to_pubhex(sec))
+		vmsg('\nkey:  {}\naddr: {}\n'.format(sec.wif,addr))
 	qmsg_r('\rRound {}/{} '.format(i+1,rounds))
 	qmsg('\n{} addresses generated in {:.2f} seconds'.format(rounds,time.time()-start))
 
-def dump_test():
-	m = "Comparing output of address generator '{}' against wallet dump '{}'"
-	qmsg(green(m.format(kg_a.desc,cmd_args[1])))
-	for n,[wif,a_addr] in enumerate(dump,1):
-		qmsg_r('\rKey {}/{} '.format(n,len(dump)))
+def dump_test(kg,ag,fh):
+
+	dump = [[*(e.split()[0] for e in line.split('addr='))] for line in fh.readlines() if 'addr=' in line]
+	if not dump:
+		die(1,'File {!r} appears not to be a wallet dump'.format(fh.name))
+
+	m = 'Comparing output of address generator {!r} against wallet dump {!r}'
+	qmsg(green(m.format(kg.desc,fh.name)))
+
+	for count,(b_wif,b_addr) in enumerate(dump,1):
+		qmsg_r('\rKey {}/{} '.format(count,len(dump)))
 		try:
-			sec = PrivKey(wif=wif)
+			b_sec = PrivKey(wif=b_wif)
 		except:
-			die(2,'\nInvalid {}net WIF address in dump file: {}'.format(('main','test')[g.testnet],wif))
-		b_addr = ag.to_addr(kg_a.to_pubhex(sec))
-		vmsg('\nwif: {}\naddr: {}\n'.format(wif,b_addr))
-		tinfo = (bytes.fromhex(sec),sec,wif,3,a)
-		test_equal(a_addr,b_addr,*tinfo)
+			die(2,'\nInvalid {} WIF address in dump file: {}'.format(g.network,b_wif))
+		a_addr = ag.to_addr(kg.to_pubhex(b_sec))
+		vmsg('\nwif: {}\naddr: {}\n'.format(b_wif,b_addr))
+		tinfo = (bytes.fromhex(b_sec),b_sec,b_wif,kg.desc,fh.name)
+		test_equal('addresses',a_addr,b_addr,*tinfo)
 	qmsg(green(('\n','')[bool(opt.verbose)] + 'OK'))
 
-# begin execution
-from mmgen.protocol import init_coin
-from mmgen.altcoin import CoinInfo as ci
+def init_tool(tname):
+	return globals()['GenTool'+capfirst(tname.replace('-','_'))]()
 
-urounds,fh = None,None
-dump = []
+def parse_arg1(arg,arg_id):
 
-if len(cmd_args) == 2:
-	try:
-		urounds = int(cmd_args[1])
-		assert urounds > 0
-	except:
-		try:
-			fh = open(cmd_args[1])
-		except:
-			die(1,'Second argument must be filename or positive integer')
+	m1 = 'First argument must be a numeric generator ID or two colon-separated generator IDs'
+	m2 = 'Second part of first argument must be a numeric generator ID or one of {}'
+
+	def check_gen_num(n):
+		if not (1 <= int(n) <= len(g.key_generators)):
+			die(1,'{}: invalid generator ID'.format(n))
+		return int(n)
+
+	if arg_id == 'a':
+		if is_int(arg):
+			a_num = check_gen_num(arg)
+			return (KeyGenerator(addr_type,a_num),a_num)
 		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
-b_desc = 'unknown'
-try:
-	a,b = cmd_args[0].split(':')
-except:
+			die(1,m1)
+	elif arg_id == 'b':
+		if is_int(arg):
+			return KeyGenerator(addr_type,check_gen_num(arg))
+		elif arg in ext_progs + ['ext']:
+			return init_tool(get_tool(arg))
+		else:
+			die(1,m2.format(ext_progs))
+
+def parse_arg2():
+	m = 'Second argument must be dump filename or integer rounds specification'
+	if len(cmd_args) == 1:
+		return None
+	arg = cmd_args[1]
+	if is_int(arg) and int(arg) > 0:
+		return int(arg)
 	try:
-		a = cmd_args[0]
-		a = int(a)
-		assert 1 <= a <= len(g.key_generators)
+		return open(arg)
 	except:
-		die(1,'First argument must be one or two generator IDs, colon separated')
-else:
-	try:
-		a = int(a)
-		assert 1 <= a <= len(g.key_generators),'{}: invalid key generator'.format(a)
-		if b in ('ext','ethkey','pycoin','keyconv','zcash_mini','moneropy'):
-			init_external_prog()
-		else:
-			b = int(b)
-			assert 1 <= b <= len(g.key_generators),'{}: invalid key generator'.format(b)
-		assert a != b,'Key generators are the same!'
-	except Exception as e:
-		die(1,'{}\n{}: invalid generator argument'.format(e.args[0],cmd_args[0]))
+		die(1,m)
 
+# begin execution
+from mmgen.protocol import init_coin
+from mmgen.altcoin import CoinInfo as ci
+from mmgen.obj import MMGenAddrType,PrivKey
 from mmgen.addr import KeyGenerator,AddrGenerator
-from mmgen.obj import PrivKey
 
-kg_a = KeyGenerator(addr_type,a)
+addr_type = MMGenAddrType(opt.type or g.proto.dfl_mmtype)
+ext_progs = list(ci.external_tests[g.network])
+
+arg1 = cmd_args[0].split(':')
+if len(arg1) == 1:
+	a,a_num = parse_arg1(arg1[0],'a')
+	b = None
+elif len(arg1) == 2:
+	a,a_num = parse_arg1(arg1[0],'a')
+	b = parse_arg1(arg1[1],'b')
+else:
+	opts.usage()
+
+if type(a) == type(b):
+	die(1,'Address generators are the same!')
+
+arg2 = parse_arg2()
+
 ag = AddrGenerator(addr_type)
 
-if a and b:
+if not b and type(arg2) == int:
+	speed_test(a,ag,arg2)
+elif not b and hasattr(arg2,'read'):
+	dump_test(a,ag,arg2)
+elif a and b and type(arg2) == int:
 	if opt.all:
 		from mmgen.protocol import init_genonly_altcoins,CoinProtocol
 		init_genonly_altcoins()
-		for coin in ci.external_tests[('mainnet','testnet')[g.testnet]][ext_prog]:
-			if coin not in CoinProtocol.coins: continue
+		for coin in ci.external_tests[g.network][b.desc]:
+			if coin.lower() not in CoinProtocol.coins:
+#				ymsg('Coin {} not configured'.format(coin))
+				continue
 			init_coin(coin)
 			if addr_type not in g.proto.mmtypes:
-				addr_type = MMGenAddrType(g.proto.dfl_mmtype)
-			kg_a = KeyGenerator(addr_type,a)
+				continue
+			# g.proto has changed, so reinit kg and ag just to be on the safe side:
+			a = KeyGenerator(addr_type,a_num)
 			ag = AddrGenerator(addr_type)
-			compare_test()
+			b_chk = ci.get_test_support(g.coin,addr_type.name,g.network,tool=b.desc,verbose=not opt.quiet)
+			if b_chk == b.desc:
+				gentool_test(a,b,ag,arg2)
 	else:
-		if b != 'ext':
-			kg_b = KeyGenerator(addr_type,b)
-			b_desc = kg_b.desc
-		compare_test()
-elif a and not fh:
-	speed_test()
-elif a and dump:
-	b_desc = 'dump'
-	dump_test()
+		gentool_test(a,b,ag,arg2)
 else:
-	die(2,'Illegal invocation')
+	opts.usage()

+ 16 - 16
test/test-release.sh

@@ -97,7 +97,7 @@ do
 		mmgen_tool="$python $mmgen_tool"
 		mmgen_keygen="$python $mmgen_keygen" ;&
 	f)  FAST=1 rounds=10 rounds_min=3 rounds_mid=25 rounds_max=50 monero_addrs='3,23' unit_tests_py+=" --fast" ;;
-	F)  FAST=1 rounds=2 rounds_min=1 rounds_mid=3 rounds_max=5 monero_addrs='3,23' unit_tests_py+=" --fast" ;;
+	F)  FAST=1 rounds=3 rounds_min=1 rounds_mid=3 rounds_max=5 monero_addrs='3,23' unit_tests_py+=" --fast" ;;
 	i)  INSTALL=1 ;;
 	I)  INSTALL_ONLY=1 ;;
 	l)  echo -e "Default tests:\n  $dfl_tests"
@@ -241,8 +241,6 @@ f_ref='Miscellaneous reference data tests completed'
 i_alts='Gen-only altcoin'
 s_alts='The following tests will test generation operations for all supported altcoins'
 t_alts="
-	$gentest_py --all 2:keyconv $rounds_mid
-
 	# speed tests, no verification:
 	$gentest_py --coin=btc 2 $rounds
 	$gentest_py --coin=btc --type=compressed 2 $rounds
@@ -260,25 +258,27 @@ t_alts="
 	$gentest_py --coin=xmr --use-internal-keccak-module 2 $rounds_min
 	$gentest_py --coin=zec 2 $rounds
 	$gentest_py --coin=zec --type=zcash_z 2 $rounds_mid
-"
 
-# disabled, pycoin generates old-style LTC Segwit addrs:
-#	$gentest_py --coin=ltc --type=segwit 2:ext $rounds
+	# verification against external libraries and tools:
+	$gentest_py --all --type=legacy 2:keyconv $rounds
+	$gentest_py --all --type=compressed 2:keyconv $rounds
+	$gentest_py --all --coin=xmr 2:moneropy $rounds_min # very slow, be patient!
+"
 
-[ "$MSYS2" ] || { # no pycoin, zcash-mini
+[ "$MSYS2" ] || { # no pycoin (libsecp256k1), zcash-mini (golang), ethkey (?)
 	t_alts="$t_alts
-		# verification using external libraries and tools:
 		$gentest_py --all --type=legacy 2:pycoin $rounds
 		$gentest_py --all --type=compressed 2:pycoin $rounds
-		$gentest_py --coin=btc --type=segwit 2:ext $rounds
-		$gentest_py --coin=btc --type=bech32 2:ext $rounds
-		$gentest_py --coin=etc 2:ext $rounds
-		$gentest_py --coin=eth 2:ext $rounds
+		$gentest_py --all --type=segwit 2:pycoin $rounds
+		$gentest_py --all --type=bech32 2:pycoin $rounds
+
+		$gentest_py --all --type=legacy --testnet=1 2:pycoin $rounds
+		$gentest_py --all --type=compressed --testnet=1 2:pycoin $rounds
+		$gentest_py --all --type=segwit --testnet=1 2:pycoin $rounds
+		$gentest_py --all --type=bech32 --testnet=1 2:pycoin $rounds
+
+		$gentest_py --all 2:zcash-mini $rounds_mid
 		$gentest_py --all 2:ethkey $rounds
-		$gentest_py --coin=zec 2:ext $rounds
-		$gentest_py --coin=zec --type=zcash_z 2:ext $rounds_mid
-		$gentest_py --all 2:zcash_mini $rounds_mid
-		$gentest_py --all 2:moneropy $rounds_mid # very slow, be patient!
 	"
 }