Browse Source

Key/address generation for Ethereum, Eth Classic, DASH, and Zcash

- supported commands:
  - mmgen-addrgen
  - mmgen-keygen (decrypt encrypted key-addr files with `mmgen-tool decrypt`)
  - mmgen-tool addrfile_chksum, keyaddrfile_chksum

- ETH and ETC keys are distinct, so users needn't worry about key reuse
- Only Zcash-t addresses supported for now (but z-addresses coming soon)

- Test the new functionality with `scripts/test-release.sh -Pn master alts`
MMGen 7 years ago
parent
commit
bafea57134

+ 4 - 0
MANIFEST.in

@@ -3,6 +3,10 @@ include doc/wiki/using-mmgen/*
 include test/*.py
 include test/ref/*
 include test/ref/litecoin/*
+include test/ref/zcash/*
+include test/ref/dash/*
+include test/ref/ethereum/*
+include test/ref/ethereum_classic/*
 
 include scripts/bitcoind-walletunlock.py
 include scripts/compute-file-chksum.py

+ 54 - 38
mmgen/addr.py

@@ -27,27 +27,32 @@ from mmgen.obj import *
 
 pnm = g.proj_name
 
+def sc_dmsg(desc,data):
+	if os.getenv('MMGEN_DEBUG_ADDRLIST'):
+		Msg('sc_debug_{}: {}'.format(desc,data))
+
 class AddrGenerator(MMGenObject):
 	def __new__(cls,atype):
 		d = {
 			'p2pkh':  AddrGeneratorP2PKH,
-			'segwit': AddrGeneratorSegwit
+			'segwit': AddrGeneratorSegwit,
+			'ethereum': AddrGeneratorEthereum
 		}
 		assert atype in d
-		return super(cls,cls).__new__(d[atype])
+		me = super(cls,cls).__new__(d[atype])
+		me.desc = d
+		return me
 
 class AddrGeneratorP2PKH(AddrGenerator):
-	desc = 'p2pkh'
 	def to_addr(self,pubhex):
 		from mmgen.protocol import hash160
 		assert type(pubhex) == PubKey
-		return CoinAddr(g.proto.hexaddr2addr(hash160(pubhex),p2sh=False))
+		return CoinAddr(g.proto.pubhash2addr(hash160(pubhex),p2sh=False))
 
 	def to_segwit_redeem_script(self,pubhex):
 		raise NotImplementedError
 
 class AddrGeneratorSegwit(AddrGenerator):
-	desc = 'segwit'
 	def to_addr(self,pubhex):
 		assert pubhex.compressed
 		return CoinAddr(g.proto.pubhex2segwitaddr(pubhex))
@@ -56,6 +61,15 @@ class AddrGeneratorSegwit(AddrGenerator):
 		assert pubhex.compressed
 		return HexStr(g.proto.pubhex2redeem_script(pubhex))
 
+class AddrGeneratorEthereum(AddrGenerator):
+	def to_addr(self,pubhex):
+		assert type(pubhex) == PubKey
+		import sha3
+		return CoinAddr(sha3.keccak_256(pubhex[2:].decode('hex')).digest()[12:].encode('hex'))
+
+	def to_segwit_redeem_script(self,pubhex):
+		raise NotImplementedError
+
 class KeyGenerator(MMGenObject):
 
 	def __new__(cls,generator=None,silent=False):
@@ -161,8 +175,10 @@ class AddrListIDStr(unicode,Hilite):
 		if fmt_str:
 			ret = fmt_str.format(s)
 		else:
-			bc,mt = g.proto.base_coin,addrlist.al_id.mmtype
-			ret = '{}{}{}[{}]'.format(addrlist.al_id.sid,('-'+bc,'')[bc=='BTC'],('-'+mt,'')[mt=='L'],s)
+			bc = (g.proto.base_coin,g.coin)[g.proto.base_coin=='ETH']
+			mt = addrlist.al_id.mmtype
+			ret = '{}{}{}[{}]'.format(addrlist.al_id.sid,('-'+bc,'')[bc=='BTC'],('-'+mt,'')[mt in ('L','E')],s)
+			sc_dmsg('id_str',ret[8:].split('[')[0])
 
 		return unicode.__new__(cls,ret)
 
@@ -196,14 +212,14 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 	gen_keys = False
 	has_keys = False
 	ext      = 'addrs'
-	cook_hash_rounds = 10  # not too many rounds, so hand decoding can still be feasible
+	scramble_hash_rounds = 10  # not too many rounds, so hand decoding can still be feasible
 	chksum_rec_f = lambda foo,e: (str(e.idx), e.addr)
 
 	def __init__(self,addrfile='',al_id='',adata=[],seed='',addr_idxs='',src='',
 					addrlist='',keylist='',mmtype=None,do_chksum=True,chksum_only=False):
 
 		self.update_msgs()
-		mmtype = mmtype or MMGenAddrType.dfl_mmtype
+		mmtype = mmtype or g.proto.dfl_mmtype
 		assert mmtype in MMGenAddrType.mmtypes
 
 		if seed and addr_idxs:   # data from seed + idxs
@@ -255,7 +271,8 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 		assert type(addrnums) is AddrIdxList
 
 		seed = seed.get_data()
-		seed = self.cook_seed(seed)
+		seed = self.scramble_seed(seed)
+		sc_dmsg('seed',seed[:8].encode('hex'))
 
 		if self.gen_addrs:
 			kg = KeyGenerator()
@@ -296,17 +313,18 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 
 	def check_format(self,addr): return True # format is checked when added to list entry object
 
-	def cook_seed(self,seed):
+	def scramble_seed(self,seed):
 		is_btcfork = g.proto.base_coin == 'BTC'
 		if is_btcfork and self.al_id.mmtype == 'L':
+			sc_dmsg('str','(none)')
 			return seed
+		if g.proto.base_coin == 'ETH':
+			scramble_key = g.coin.lower()
 		else:
-			from mmgen.crypto import sha256_rounds
-			import hmac
-			key = (g.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
-			cseed = hmac.new(seed,key,sha256).digest()
-			dmsg('Seed:  {}\nKey: {}\nCseed: {}\nCseed len: {}'.format(hexlify(seed),key,hexlify(cseed),len(cseed)))
-			return sha256_rounds(cseed,self.cook_hash_rounds)
+			scramble_key = (g.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
+		sc_dmsg('str',scramble_key)
+		from mmgen.crypto import scramble_seed
+		return scramble_seed(seed,scramble_key,self.scramble_hash_rounds)
 
 	def encrypt(self,desc='new key list'):
 		from mmgen.crypto import mmgen_encrypt
@@ -397,12 +415,16 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 			out.append('# Record this value to a secure location.\n')
 
 		if type(self) == PasswordList:
-			out.append(u'{} {} {}:{} {{'.format(
-				self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len))
+			lbl = u'{} {} {}:{}'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len)
 		else:
 			bc,mt = g.proto.base_coin,self.al_id.mmtype
-			lbl = ':'.join(([bc],[])[bc=='BTC']+([mt.name.upper()],[])[mt=='L'])
-			out.append('{} {}{{'.format(self.al_id.sid,('',lbl+' ')[bool(lbl)]))
+			l_coin = [] if bc == 'BTC' else [g.coin] if bc == 'ETH' else [bc]
+			l_type = [] if mt in ('L','E') else [mt.name.upper()]
+			lbl_p2 = ':'.join(l_coin+l_type)
+			lbl = self.al_id.sid + ('',' ')[bool(lbl_p2)] + lbl_p2
+
+		sc_dmsg('lbl',lbl[9:])
+		out.append(u'{} {{'.format(lbl))
 
 		fs = '  {:<%s}  {:<34}{}' % len(str(self.data[-1].idx))
 		for e in self.data:
@@ -492,20 +514,19 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 			return do_error("'%s': invalid Seed ID" % ls[0])
 
 		def parse_addrfile_label(lbl): # we must maintain backwards compat, so parse is tricky
-			al_base_coin,al_mmtype = None,None
+			al_coin,al_mmtype = None,None
 			lbl = lbl.split(':',1)
 			if len(lbl) == 2:
-				al_base_coin = lbl[0]
-				al_mmtype    = lbl[1].lower()
+				al_coin,al_mmtype = lbl[0],lbl[1].lower()
 			else:
 				if lbl[0].lower() in MMGenAddrType.get_names():
 					al_mmtype = lbl[0].lower()
 				else:
-					al_base_coin = lbl[0]
+					al_coin = lbl[0]
 
 			# this block fails if al_mmtype is invalid for g.coin
 			if not al_mmtype:
-				mmtype = MMGenAddrType('L')
+				mmtype = MMGenAddrType('E' if al_coin in ('ETH','ETC') else 'L')
 			else:
 				try:
 					mmtype = MMGenAddrType(al_mmtype)
@@ -514,9 +535,9 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 						mmtype.upper(),' '.join([i['name'].upper() for i in MMGenAddrType.mmtypes.values()])))
 
 			from mmgen.protocol import CoinProtocol
-			base_coin = CoinProtocol(al_base_coin or 'BTC',testnet=False).base_coin
+			base_coin = CoinProtocol(al_coin or 'BTC',testnet=False).base_coin
 			if not base_coin:
-				die(2,"'{}': unknown base coin in address file label!".format(al_base_coin))
+				die(2,"'{}': unknown base coin in address file label!".format(al_coin))
 			return base_coin,mmtype
 
 		def check_coin_mismatch(base_coin): # die if addrfile coin doesn't match g.coin
@@ -686,18 +707,13 @@ Record this checksum: it will be used to verify the password file in the future
 			return False
 		return True
 
-	def cook_seed(self,seed):
-		from mmgen.crypto import sha256_rounds
-		# Changing either pw_fmt, pw_len or cook_str will cause a different,
+	def scramble_seed(self,seed):
+		# Changing either pw_fmt, pw_len or scramble_key will cause a different,
 		# unrelated set of passwords to be generated: this is what we want.
 		# NB: In original implementation, pw_id_str was 'baseN', not 'bN'
-		cook_str = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str.encode('utf8'))
-		dmsg(u'Full ID string: {}'.format(cook_str.decode('utf8')))
-		# Original implementation was 'cseed = seed + cook_str'; hmac was not used
-		import hmac
-		cseed = hmac.new(seed,cook_str,sha256).digest()
-		dmsg('Seed: {}\nCooked seed: {}\nCooked seed len: {}'.format(hexlify(seed),hexlify(cseed),len(cseed)))
-		return sha256_rounds(cseed,self.cook_hash_rounds)
+		scramble_key = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str.encode('utf8'))
+		from mmgen.crypto import scramble_seed
+		return scramble_seed(seed,scramble_key,self.scramble_hash_rounds)
 
 class AddrData(MMGenObject):
 	msgs = {

+ 2 - 2
mmgen/main_addrgen.py

@@ -80,7 +80,7 @@ opts_data = lambda: {
 	kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 	kg=g.key_generator,
 	what=gen_what,g=g,
-	dmat="'{}' or '{}'".format(MAT.dfl_mmtype,MAT.mmtypes[MAT.dfl_mmtype]['name'])
+	dmat="'{}' or '{}'".format(g.proto.dfl_mmtype,MAT.mmtypes[g.proto.dfl_mmtype]['name'])
 ),
 	'notes': """
 
@@ -118,7 +118,7 @@ FMT CODES:
 cmd_args = opts.init(opts_data,add_opts=['b16'],opt_filter=opt_filter)
 
 errmsg = "'{}': invalid parameter for --type option".format(opt.type)
-addr_type = MAT(opt.type or MAT.dfl_mmtype,errmsg=errmsg)
+addr_type = MAT(opt.type or g.proto.dfl_mmtype,errmsg=errmsg)
 
 if len(cmd_args) < 1: opts.usage()
 idxs = AddrIdxList(fmt_str=cmd_args.pop())

+ 1 - 1
mmgen/main_tool.py

@@ -32,7 +32,7 @@ cmd_help = """
 Cryptocoin address/key operations (compressed public keys supported):
   addr2hexaddr   - convert coin address from base58 to hex format
   hex2wif        - convert a private key from hex to WIF format
-  hexaddr2addr   - convert coin address from hex to base58 format
+  pubhash2addr   - convert public key hash to address
   privhex2addr   - generate coin address from private key in hex format
   privhex2pubhex - generate a hex public key from a hex private key
   pubhex2addr    - convert a hex pubkey to an address

+ 22 - 11
mmgen/obj.py

@@ -355,7 +355,6 @@ class LTCAmt(BTCAmt): max_amt = 84000000
 
 class CoinAddr(str,Hilite,InitErrors,MMGenObject):
 	color = 'cyan'
-	width = 35 # max len of testnet p2sh addr
 	def __new__(cls,s,on_fail='die'):
 		if type(s) == cls: return s
 		cls.arg_chk(cls,on_fail)
@@ -367,6 +366,7 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject):
 			assert va,'failed verification'
 			me.addr_fmt = va['format']
 			me.hex = va['hex']
+			cls.width = va['width']
 			return me
 		except Exception as e:
 			m = "{!r}: value cannot be converted to {} address ({})"
@@ -385,10 +385,17 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject):
 	def is_for_chain(self,chain):
 		from mmgen.globalvars import g
 		vn = g.proto.get_protocol_by_chain(chain).addr_ver_num
+
+		def pfx_ok(pfx):
+			if type(pfx) == tuple:
+				if self[0] in pfx: return True
+			elif self[:len(pfx)] == pfx: return True
+			return False
+
 		if self.addr_fmt == 'p2sh' and 'p2sh2' in vn:
-			return self[0] in vn['p2sh'][1] or self[0] in vn['p2sh2'][1]
+			return pfx_ok(vn['p2sh'][1]) or pfx_ok(vn['p2sh2'][1])
 		else:
-			return self[0] in vn[self.addr_fmt][1]
+			return pfx_ok(vn[self.addr_fmt][1])
 
 	def is_in_tracking_wallet(self):
 		from mmgen.rpc import rpc_init
@@ -427,7 +434,7 @@ class MMGenID(str,Hilite,InitErrors,MMGenObject):
 		try:
 			ss = str(s).split(':')
 			assert len(ss) in (2,3),'not 2 or 3 colon-separated items'
-			t = MMGenAddrType((ss[1],MMGenAddrType.dfl_mmtype)[len(ss)==2],on_fail='raise')
+			t = MMGenAddrType((ss[1],g.proto.dfl_mmtype)[len(ss)==2],on_fail='raise')
 			me = str.__new__(cls,'{}:{}:{}'.format(ss[0],t,ss[-1]))
 			me.sid = SeedID(sid=ss[0],on_fail='raise')
 			me.idx = AddrIdx(ss[-1],on_fail='raise')
@@ -587,7 +594,7 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject):
 			me.wif = me.towif()
 			return me
 		except Exception as e:
-			fs = "Key={!r}\nCompressed={}\nValue pair cannot be converted to PrivKey ({!r})"
+			fs = "Key={!r}\nCompressed={}\nValue pair cannot be converted to PrivKey\n({})"
 			return cls.init_fail(fs.format(s,compressed,e),on_fail)
 
 	def towif(self):
@@ -671,18 +678,22 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
 				'gen':'p2pkh',
 				'fmt':'p2pkh',
 				'desc':'Legacy uncompressed address'},
+		'C': {  'name':'compressed',
+				'comp':True,
+				'gen':'p2pkh',
+				'fmt':'p2pkh',
+				'desc':'Compressed P2PKH address'},
 		'S': {  'name':'segwit',
 				'comp':True,
 				'gen':'segwit',
 				'fmt':'p2sh',
 				'desc':'Segwit P2SH-P2WPKH address' },
-		'C': {  'name':'compressed',
-				'comp':True,
-				'gen':'p2pkh',
-				'fmt':'p2pkh',
-				'desc':'Compressed P2PKH address'}
+		'E': {  'name':'ethereum',
+				'comp':False,
+				'gen':'ethereum',
+				'fmt':'ethereum',
+				'desc':'Ethereum address' },
 	}
-	dfl_mmtype = 'L'
 	def __new__(cls,s,on_fail='die',errmsg=None):
 		if type(s) == cls: return s
 		cls.arg_chk(cls,on_fail)

+ 97 - 30
mmgen/protocol.py

@@ -22,7 +22,7 @@ protocol.py: Coin protocol functions, classes and methods
 
 import os,hashlib
 from binascii import unhexlify
-from mmgen.util import msg,pmsg
+from mmgen.util import msg,pmsg,Msg
 from mmgen.obj import MMGenObject,BTCAmt,LTCAmt,BCHAmt,B2XAmt
 from mmgen.globalvars import g
 
@@ -53,12 +53,14 @@ def _b58tonum(b58num):
 		if not i in _b58a: return False
 	return sum(_b58a.index(n) * (58**i) for i,n in enumerate(list(b58num[::-1])))
 
+# chainparams.cpp
 class BitcoinProtocol(MMGenObject):
 	name            = 'bitcoin'
 	daemon_name     = 'bitcoind'
-	addr_ver_num    = { 'p2pkh': ('00','1'), 'p2sh':  ('05','3') } # chainparams.cpp
-	privkey_pfx     = '80'
+	addr_ver_num    = { 'p2pkh': ('00','1'), 'p2sh':  ('05','3') }
+	wif_ver_num     = '80'
 	mmtypes         = ('L','C','S')
+	dfl_mmtype      = 'L'
 	data_subdir     = ''
 	rpc_port        = 8332
 	secs_per_block  = 600
@@ -74,7 +76,9 @@ class BitcoinProtocol(MMGenObject):
 		(None,'','b2x',True)
 	]
 	caps = ('rbf','segwit')
+	mmcaps = ('key','addr','rpc')
 	base_coin = 'BTC'
+	addr_width = 34
 
 	@staticmethod
 	def get_protocol_by_chain(chain):
@@ -89,7 +93,7 @@ class BitcoinProtocol(MMGenObject):
 
 	@classmethod
 	def hex2wif(cls,hexpriv,compressed=False):
-		s = cls.privkey_pfx + hexpriv + ('','01')[bool(compressed)]
+		s = cls.wif_ver_num + hexpriv + ('','01')[bool(compressed)]
 		return _numtob58(int(s+hash256(s)[:8],16))
 
 	@classmethod
@@ -101,34 +105,39 @@ class BitcoinProtocol(MMGenObject):
 		compressed = len(key) == 76
 		if compressed and key[66:68] != '01': return False
 		klen = (66,68)[compressed]
-		if (key[:2] == cls.privkey_pfx and key[klen:] == hash256(key[:klen])[:8]):
+		if (key[:2] == cls.wif_ver_num and key[klen:] == hash256(key[:klen])[:8]):
 			return { 'hex':key[2:66], 'compressed':compressed }
 		else:
 			return False
 
 	@classmethod
-	def verify_addr(cls,addr,verbose=False,return_dict=False):
+	def verify_addr(cls,addr,return_dict=False):
 		for addr_fmt in cls.addr_ver_num:
-			ver_num,ldigit = cls.addr_ver_num[addr_fmt]
-			if addr[0] not in ldigit: continue
+			ver_num,pfx = cls.addr_ver_num[addr_fmt]
+			if type(pfx) == tuple:
+				if addr[0] not in pfx: continue
+			elif addr[:len(pfx)] != pfx: continue
 			num = _b58tonum(addr)
-			if num == False: break
-			addr_hex = '{:050x}'.format(num)
-			if addr_hex[:2] != ver_num: continue
-			if hash256(addr_hex[:42])[:8] == addr_hex[42:]:
+			if num == False:
+				if g.debug: Msg('Address cannot be converted to base 58')
+				break
+			addr_hex = '{:0{}x}'.format(num,48+len(ver_num))
+			if addr_hex[:len(ver_num)] != ver_num: continue
+			if hash256(addr_hex[:-8])[:8] == addr_hex[-8:]:
 				return {
-					'hex':    addr_hex[2:42],
+					'hex': addr_hex[len(ver_num):-8],
 					'format': {'p2pkh':'p2pkh','p2sh':'p2sh','p2sh2':'p2sh'}[addr_fmt],
+					'width': cls.addr_width
 				} if return_dict else True
 			else:
-				if verbose: Msg("Invalid checksum in address '{}'".format(addr))
+				if g.debug: Msg('Invalid checksum in address')
 				break
-		if verbose: Msg("Invalid address '{}'".format(addr))
+		if g.debug: Msg("Invalid address '{}'".format(addr))
 		return False
 
 	@classmethod
-	def hexaddr2addr(cls,hexaddr,p2sh):
-		s = cls.addr_ver_num[('p2pkh','p2sh')[p2sh]][0] + hexaddr
+	def pubhash2addr(cls,pubkey_hash,p2sh):
+		s = cls.addr_ver_num[('p2pkh','p2sh')[p2sh]][0] + pubkey_hash
 		lzeroes = (len(s) - len(s.lstrip('0'))) / 2 # non-zero only for ver num '00' (BTC p2pkh)
 		return ('1' * lzeroes) + _numtob58(int(s+hash256(s)[:8],16))
 
@@ -142,14 +151,15 @@ class BitcoinProtocol(MMGenObject):
 
 	@classmethod
 	def pubhex2segwitaddr(cls,pubhex):
-		return cls.hexaddr2addr(hash160(cls.pubhex2redeem_script(pubhex)),p2sh=True)
+		return cls.pubhash2addr(hash160(cls.pubhex2redeem_script(pubhex)),p2sh=True)
 
 class BitcoinTestnetProtocol(BitcoinProtocol):
-	addr_ver_num         = { 'p2pkh': ('6f','mn'), 'p2sh':  ('c4','2') }
-	privkey_pfx          = 'ef'
+	addr_ver_num         = { 'p2pkh': ('6f',('m','n')), 'p2sh':  ('c4','2') }
+	wif_ver_num          = 'ef'
 	data_subdir          = 'testnet'
 	daemon_data_subdir   = 'testnet3'
 	rpc_port             = 18332
+	addr_width           = 35
 
 class BitcoinCashProtocol(BitcoinProtocol):
 	# TODO: assumes MSWin user installs in custom dir 'Bitcoin_ABC'
@@ -173,10 +183,11 @@ class BitcoinCashProtocol(BitcoinProtocol):
 
 class BitcoinCashTestnetProtocol(BitcoinCashProtocol):
 	rpc_port      = 18442
-	addr_ver_num  = { 'p2pkh': ('6f','mn'), 'p2sh':  ('c4','2') }
-	privkey_pfx   = 'ef'
+	addr_ver_num  = { 'p2pkh': ('6f',('m','n')), 'p2sh':  ('c4','2') }
+	wif_ver_num   = 'ef'
 	data_subdir   = 'testnet'
 	daemon_data_subdir = 'testnet3'
+	addr_width     = 35
 
 class B2XProtocol(BitcoinProtocol):
 	daemon_name    = 'bitcoind-2x'
@@ -190,11 +201,12 @@ class B2XProtocol(BitcoinProtocol):
 	]
 
 class B2XTestnetProtocol(B2XProtocol):
-	addr_ver_num         = { 'p2pkh': ('6f','mn'), 'p2sh':  ('c4','2') }
-	privkey_pfx          = 'ef'
+	addr_ver_num         = { 'p2pkh': ('6f',('m','n')), 'p2sh':  ('c4','2') }
+	wif_ver_num          = 'ef'
 	data_subdir          = 'testnet'
 	daemon_data_subdir   = 'testnet5'
 	rpc_port             = 18338
+	addr_width     = 35
 
 class LitecoinProtocol(BitcoinProtocol):
 	block0         = '12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2'
@@ -203,7 +215,7 @@ class LitecoinProtocol(BitcoinProtocol):
 	daemon_data_dir = os.path.join(os.getenv('APPDATA'),'Litecoin') if g.platform == 'win' \
 						else os.path.join(g.home_dir,'.litecoin')
 	addr_ver_num   = { 'p2pkh': ('30','L'), 'p2sh':  ('32','M'), 'p2sh2':  ('05','3') } # 'p2sh' is new fmt
-	privkey_pfx    = 'b0'
+	wif_ver_num    = 'b0'
 	secs_per_block = 150
 	rpc_port       = 9332
 	coin_amt       = LTCAmt
@@ -213,16 +225,68 @@ class LitecoinProtocol(BitcoinProtocol):
 
 class LitecoinTestnetProtocol(LitecoinProtocol):
 	# addr ver nums same as Bitcoin testnet, except for 'p2sh'
-	addr_ver_num   = { 'p2pkh': ('6f','mn'), 'p2sh':  ('3a','Q'), 'p2sh2':  ('c4','2') }
-	privkey_pfx    = 'ef' # same as Bitcoin testnet
+	addr_ver_num   = { 'p2pkh': ('6f',('m','n')), 'p2sh':  ('3a','Q'), 'p2sh2':  ('c4','2') }
+	wif_ver_num    = 'ef' # same as Bitcoin testnet
 	data_subdir    = 'testnet'
 	daemon_data_subdir = 'testnet4'
 	rpc_port       = 19332
+	addr_width     = 35
+
+class BitcoinProtocolAddrgen(BitcoinProtocol): mmcaps = ('key','addr')
+class BitcoinProtocolKeygen(BitcoinProtocol):  mmcaps = ('key',)
+
+class EthereumProtocol(BitcoinProtocolAddrgen):
+
+	addr_width = 40
+	mmtypes    = ('E',)
+	dfl_mmtype = 'E'
+	name = 'ethereum'
+	base_coin = 'ETH'
+
+	@classmethod
+	def hex2wif(cls,hexpriv,compressed=False):
+		assert compressed == False,'Ethereum does not support compressed pubkeys!'
+		return str(hexpriv)
+
+	@classmethod
+	def wif2hex(cls,wif):
+		return { 'hex':str(wif), 'compressed':False }
 
-class EthereumProtocol(MMGenObject):
-	base_coin      = 'ETH'
+	@classmethod
+	def verify_addr(cls,addr,return_dict=False):
+		from mmgen.util import is_hex_str_lc
+		if is_hex_str_lc(addr) and len(addr) == 40:
+			return { 'hex': addr, 'format': 'ethereum', 'width': cls.addr_width } if return_dict else True
+		if g.debug: Msg("Invalid address '{}'".format(addr))
+		return False
 
 class EthereumTestnetProtocol(EthereumProtocol): pass
+class EthereumClassicProtocol(EthereumProtocol):
+	name = 'ethereum_classic'
+class EthereumClassicTestnetProtocol(EthereumClassicProtocol): pass
+
+class ZcashProtocol(BitcoinProtocolAddrgen):
+	name         = 'zcash'
+	base_coin    = 'ZEC'
+	addr_ver_num = { 'p2pkh': ('1cb8','t1'), 'p2sh':  ('1cbd','t3') }
+	wif_ver_num  = '80'
+	mmtypes      = ('C',)
+	dfl_mmtype   = 'C'
+
+class ZcashTestnetProtocol(object): pass
+
+class DashProtocol(BitcoinProtocolAddrgen):
+	name         = 'dash'
+	base_coin    = 'DASH'
+	addr_ver_num = { 'p2pkh': ('4c','X'), 'p2sh':  ('10','7') }
+	wif_ver_num  = 'cc'
+	mmtypes      = ('C',)
+	dfl_mmtype   = 'C'
+
+class DashTestnetProtocol(DashProtocol):
+	# "Dash", "testnet", "tDASH", b'\xef', b'\x8c', b'\x13'
+	addr_ver_num   = { 'p2pkh': ('8c','y'), 'p2sh':  ('13','?') }
+	wif_ver_num    = 'ef'
 
 class CoinProtocol(MMGenObject):
 	coins = {
@@ -230,7 +294,10 @@ class CoinProtocol(MMGenObject):
 		'bch': (BitcoinCashProtocol,BitcoinCashTestnetProtocol),
 		'b2x': (B2XProtocol,B2XTestnetProtocol),
 		'ltc': (LitecoinProtocol,LitecoinTestnetProtocol),
-#		'eth': (EthereumProtocol,EthereumTestnetProtocol),
+		'dash': (DashProtocol,DashTestnetProtocol),
+		'zec': (ZcashProtocol,ZcashTestnetProtocol),
+		'eth': (EthereumProtocol,EthereumTestnetProtocol),
+		'etc': (EthereumClassicProtocol,EthereumClassicTestnetProtocol),
 	}
 	def __new__(cls,coin,testnet):
 		coin = coin.lower()

+ 6 - 6
mmgen/tool.py

@@ -61,7 +61,7 @@ cmd_data = OrderedDict([
 	('Wif2hex',    ['<wif> [str-]']),
 	('Wif2addr',   ['<wif> [str-]','segwit [bool=False]']),
 	('Wif2segwit_pair',['<wif> [str-]']),
-	('Hexaddr2addr', ['<coin address in hex format> [str-]','p2sh [bool=False]']),
+	('Pubhash2addr', ['<coin address in hex format> [str-]','p2sh [bool=False]']),
 	('Addr2hexaddr', ['<coin address> [str-]']),
 	('Privhex2addr', ['<private key in hex format> [str-]','compressed [bool=False]','segwit [bool=False]']),
 	('Privhex2pubhex',['<private key in hex format> [str-]','compressed [bool=False]']),
@@ -279,11 +279,11 @@ def Wif2segwit_pair(wif):
 	rs = ag.to_segwit_redeem_script(pubhex)
 	Msg('{}\n{}'.format(rs,addr))
 
-def Hexaddr2addr(hexaddr,p2sh=False):          Msg(g.proto.hexaddr2addr(hexaddr,p2sh=p2sh))
-def Addr2hexaddr(addr):                        Msg(g.proto.verify_addr(addr,return_dict=True)['hex'])
-def Hash160(pubkeyhex):                        Msg(hash160(pubkeyhex))
-def Pubhex2addr(pubkeyhex,p2sh=False):         Msg(g.proto.hexaddr2addr(hash160(pubkeyhex),p2sh=p2sh))
-def Wif2hex(wif):                              Msg(wif2hex(wif))
+def Pubhash2addr(pubhash,p2sh=False):   Msg(g.proto.pubhash2addr(pubhash,p2sh=p2sh))
+def Addr2hexaddr(addr):                 Msg(g.proto.verify_addr(addr,return_dict=True)['hex'])
+def Hash160(pubkeyhex):                 Msg(hash160(pubkeyhex))
+def Pubhex2addr(pubkeyhex,p2sh=False):  Msg(g.proto.pubhash2addr(hash160(pubkeyhex),p2sh=p2sh))
+def Wif2hex(wif):                       Msg(wif2hex(wif))
 def Hex2wif(hexpriv,compressed=False):
 	Msg(g.proto.hex2wif(hexpriv,compressed))
 def Privhex2addr(privhex,compressed=False,segwit=False,output_pubhex=False):

+ 1 - 1
mmgen/tx.py

@@ -134,7 +134,7 @@ def scriptPubKey2addr(s):
 	if len(s) == 50 and s[:6] == '76a914' and s[-4:] == '88ac': addr_hex,p2sh = s[6:-4],False
 	elif len(s) == 46 and s[:4] == 'a914' and s[-2:] == '87':   addr_hex,p2sh = s[4:-2],True
 	else: raise NotImplementedError,'Unknown scriptPubKey'
-	return g.proto.hexaddr2addr(addr_hex,p2sh)
+	return g.proto.pubhash2addr(addr_hex,p2sh)
 
 from collections import OrderedDict
 class DeserializedTX(OrderedDict,MMGenObject): # need to add MMGen types

+ 3 - 0
mmgen/util.py

@@ -799,6 +799,9 @@ def get_coin_daemon_auth_cookie():
 
 def rpc_init(reinit=False):
 
+	if not 'rpc' in g.proto.mmcaps:
+		die(1,'Coin daemon operations not supported for coin {}!'.format(g.coin))
+
 	if g.rpch != None and not reinit: return g.rpch
 
 	def check_chainfork_mismatch(conn):

+ 32 - 10
scripts/test-release.sh

@@ -2,7 +2,7 @@
 # Tested on Linux, MinGW-64
 # MinGW's bash 3.1.17 doesn't do ${var^^}
 
-dfl_tests='obj misc btc btc_tn btc_rt bch bch_rt b2x b2x_rt ltc ltc_tn ltc_rt tool gen'
+dfl_tests='obj alts misc btc btc_tn btc_rt bch bch_rt ltc ltc_tn ltc_rt tool gen'
 PROGNAME=$(basename $0)
 while getopts hinPt OPT
 do
@@ -16,14 +16,15 @@ do
 		echo   "           '-t'  Print the tests without running them"
 		echo   "  AVAILABLE TESTS:"
 		echo   "     obj    - data objects"
+		echo   "     alts   - operations for all supported gen-only altcoins"
 		echo   "     misc   - miscellaneous operations"
 		echo   "     btc    - bitcoin"
 		echo   "     btc_tn - bitcoin testnet"
 		echo   "     btc_rt - bitcoin regtest"
 		echo   "     bch    - bitcoin cash (BCH)"
 		echo   "     bch_rt - bitcoin cash (BCH) regtest"
-		echo   "     b2x    - bitcoin 2x (B2X)"
-		echo   "     b2x_rt - bitcoin 2x (B2X) regtest"
+# 		echo   "     b2x    - bitcoin 2x (B2X)"
+# 		echo   "     b2x_rt - bitcoin 2x (B2X) regtest"
 		echo   "     ltc    - litecoin"
 		echo   "     ltc_tn - litecoin testnet"
 		echo   "     ltc_rt - litecoin regtest"
@@ -86,7 +87,7 @@ do_test() {
 		[ "$TESTING" ] || eval "$i" || { echo -e $RED'Test failed!'$RESET; exit; }
 	done
 }
-i_obj='Data objects'
+i_obj='Data object'
 s_obj='Testing data objects'
 t_obj=(
     'test/objtest.py --coin=btc -S'
@@ -95,6 +96,20 @@ t_obj=(
     'test/objtest.py --coin=ltc --testnet=1 -S')
 f_obj='Data object test complete'
 
+i_alts='Gen-only altcoin'
+s_alts='The following tests will test generation operations for all supported altcoins'
+t_alts=(
+	'test/test.py -n altcoin_ref'
+	'test/gentest.py --coin=btc 2:ext 100'
+	'test/gentest.py --coin=ltc 2:ext 100'
+	'test/gentest.py --coin=zec 2:ext 100'
+	'test/gentest.py --coin=dash 2:ext 100'
+	'test/gentest.py --coin=etc 2:ext 100'
+	'test/gentest.py --coin=eth 2:ext 100'
+	'test/scrambletest.py'
+	)
+f_alts='Gen-only altcoin tests completed'
+
 i_misc='Miscellaneous operations' # includes autosign!
 s_misc='The bitcoin, bitcoin-abc and litecoin (mainnet) daemons must be running for the following tests'
 t_misc=(
@@ -125,7 +140,7 @@ s_btc_rt="The following tests will test MMGen's regtest (Bob and Alice) mode"
 t_btc_rt=(
 	'test/test.py -On regtest'
 	'test/test.py -On regtest_split')
-f_btc_rt="Regtest (Bob and Alice) mode tests for BTC completed"
+f_btc_rt='Regtest (Bob and Alice) mode tests for BTC completed'
 
 i_bch='Bitcoin cash (BCH)'
 s_bch='The bitcoin cash daemon (Bitcoin ABC) must both be running for the following tests'
@@ -135,7 +150,7 @@ f_bch='You may stop the Bitcoin ABC daemon if you wish'
 i_bch_rt='Bitcoin cash (BCH) regtest'
 s_bch_rt="The following tests will test MMGen's regtest (Bob and Alice) mode"
 t_bch_rt=('test/test.py --coin=bch -On regtest')
-f_bch_rt="Regtest (Bob and Alice) mode tests for BCH completed"
+f_bch_rt='Regtest (Bob and Alice) mode tests for BCH completed'
 
 i_b2x='Bitcoin 2X (B2X)'
 s_b2x='The bitcoin 2X daemon (BTC1) must both be running for the following tests'
@@ -145,7 +160,7 @@ f_b2x='You may stop the Bitcoin 2X daemon if you wish'
 i_b2x_rt='Bitcoin 2X (B2X) regtest'
 s_b2x_rt="The following tests will test MMGen's regtest (Bob and Alice) mode"
 t_b2x_rt=('test/test.py --coin=b2x -On regtest')
-f_b2x_rt="Regtest (Bob and Alice) mode tests for B2X completed"
+f_b2x_rt='Regtest (Bob and Alice) mode tests for B2X completed'
 
 i_ltc='Litecoin'
 s_ltc='The litecoin daemon must both be running for the following tests'
@@ -169,8 +184,9 @@ f_ltc_tn='You may stop the litecoin testnet daemon if you wish'
 i_ltc_rt='Litecoin regtest'
 s_ltc_rt="The following tests will test MMGen's regtest (Bob and Alice) mode"
 t_ltc_rt=('test/test.py --coin=ltc -On regtest')
-f_ltc_rt="Regtest (Bob and Alice) mode tests for LTC completed"
+f_ltc_rt='Regtest (Bob and Alice) mode tests for LTC completed'
 
+# TODO: ethereum support for tooltest
 i_tool='Tooltest'
 s_tool='The following tests will run test/tooltest.py for all supported coins'
 t_tool=(
@@ -180,8 +196,14 @@ t_tool=(
 	'test/tooltest.py --coin=ltc util'
 	'test/tooltest.py --coin=ltc cryptocoin'
 	'test/tooltest.py --coin=ltc mnemonic'
+	'test/tooltest.py --coin=zec util'
+	'test/tooltest.py --coin=zec cryptocoin'
+	'test/tooltest.py --coin=zec mnemonic'
+	'test/tooltest.py --coin=dash util'
+	'test/tooltest.py --coin=dash cryptocoin'
+	'test/tooltest.py --coin=dash mnemonic'
 	)
-f_tool="tooltest tests completed"
+f_tool='tooltest tests completed'
 
 i_gen='Gentest'
 s_gen='The following tests will run test/gentest.py on mainnet and testnet for all supported coins'
@@ -199,7 +221,7 @@ t_gen=(
 	'test/gentest.py -q --coin=ltc --testnet=1 1:2 10'
 	'test/gentest.py -q --coin=ltc --testnet=1 --segwit 1:2 10'
 	)
-f_gen="gentest tests completed"
+f_gen='gentest tests completed'
 
 [ -d .git -a -z "$NO_INSTALL"  -a -z "$TESTING" ] && {
 	check

+ 62 - 15
test/gentest.py

@@ -53,10 +53,16 @@ opts_data = lambda: {
 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:
+    + pyethereum (for ETH,ETC)          https://github.com/ethereum/pyethereum
+    + pycoin     (for all other coins)  https://github.com/richardkiss/pycoin
 """.format(prog='gentest.py',pnm=g.proj_name,snum=rounds,dn=g.proto.daemon_name)
 }
 
@@ -66,6 +72,23 @@ cmd_args = opts.init(opts_data,add_opts=['exact_output'])
 
 if not 1 <= len(cmd_args) <= 2: opts.usage()
 
+def pyethereum_sec2addr(sec):
+	return sec,eth.privtoaddr(sec).encode('hex')
+
+def pycoin_sec2addr(sec):
+	if g.testnet: # pycoin/networks/all.py pycoin/networks/legacy_networks.py
+		coin = { 'BTC':'XTN', 'LTC':'XLT', 'DASH':'tDASH' }[g.coin]
+	else:
+		coin = g.coin
+	key = pcku.parse_key(sec,PREFIX_TRANSFORMS,coin)
+	if key is None: die(1,"can't parse {}".format(sec))
+	o = pcku.create_output(sec,key)[0]
+#	pmsg(o)
+	suf = ('_uncompressed','')[compressed]
+	wif = o['wif{}'.format(suf)]
+	addr = o['{}_address{}'.format(coin,suf)]
+	return wif,addr
+
 urounds,fh = None,None
 dump = []
 if len(cmd_args) == 2:
@@ -97,8 +120,27 @@ 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)
+		a = int(a)
+		assert 1 <= a <= len(g.key_generators)
+		if b == 'ext':
+			if g.coin in ('ETH','ETC'):
+				try:
+					import ethereum.utils as eth
+				except:
+					die(1,"Unable to import 'pyethereum' module. Is pyethereum installed?")
+				sec2addr = pyethereum_sec2addr
+				ext_lib = 'pyethereum'
+			else:
+				try:
+					import pycoin.cmds.ku as pcku
+				except:
+					die(1,"Unable to import module 'ku'. Is pycoin installed?")
+				PREFIX_TRANSFORMS = pcku.prefix_transforms_for_network(g.coin)
+				sec2addr = pycoin_sec2addr
+				ext_lib = 'pycoin'
+		else:
+			b = int(b)
+			assert 1 <= b <= len(g.key_generators)
 		assert a != b
 	except:
 		die(1,"%s: invalid generator IDs" % cmd_args[0])
@@ -111,40 +153,45 @@ def match_error(sec,wif,a_addr,b_addr,a,b):
   WIF key   : {}
   {a:10}: {}
   {b:10}: {}
-""".format(sec,wif,a_addr,b_addr,pnm=g.proj_name,a=m[a],b=m[b]).rstrip())
+""".format(sec,wif,a_addr,b_addr,pnm=g.proj_name,a=m[a],b=m[b] if b in m else b).rstrip())
 
 # Begin execution
-compressed = True
+compressed = False if g.coin in ('ETH','ETC') else True
 
 from mmgen.addr import KeyGenerator,AddrGenerator
 from mmgen.obj import PrivKey
-ag = AddrGenerator(('p2pkh','segwit')[bool(opt.segwit)])
+ag = AddrGenerator('ethereum' if g.coin in ('ETH','ETC') else ('p2pkh','segwit')[bool(opt.segwit)])
 
 if a and b:
-	m = "Comparing address generators '{}' and '{}'"
-	qmsg(green(m.format(g.key_generators[a-1],g.key_generators[b-1])))
+	m = "Comparing address generators '{}' and '{}' for coin {}"
+	qmsg(green(m.format(g.key_generators[a-1],(ext_lib if b == 'ext' else g.key_generators[b-1]),g.coin)))
 	last_t = time.time()
 	kg_a = KeyGenerator(a)
-	kg_b = KeyGenerator(b)
+	if b != 'ext': kg_b = KeyGenerator(b)
 
 	for i in range(rounds):
-		if time.time() - last_t >= 0.1:
+		if opt.verbose or time.time() - last_t >= 0.1:
 			qmsg_r('\rRound %s/%s ' % (i+1,rounds))
 			last_t = time.time()
 		sec = PrivKey(os.urandom(32),compressed)
 		a_addr = ag.to_addr(kg_a.to_pubhex(sec))
-		b_addr = ag.to_addr(kg_b.to_pubhex(sec))
+		if b == 'ext':
+			b_wif,b_addr = sec2addr(sec)
+			if b_wif != sec.wif:
+				match_error(sec,sec.wif,sec.wif,b_wif,a,b)
+		else:
+			b_addr = ag.to_addr(kg_b.to_pubhex(sec))
 		vmsg('\nkey:  %s\naddr: %s\n' % (sec.wif,a_addr))
 		if a_addr != b_addr:
-			match_error(sec,sec.wif,a_addr,b_addr,a,b)
-		if not opt.segwit:
+			match_error(sec,sec.wif,a_addr,b_addr,a,ext_lib if b == 'ext' else b)
+		if not opt.segwit and 'L' in g.proto.mmtypes:
 			compressed = not compressed
 	qmsg_r('\rRound %s/%s ' % (i+1,rounds))
 
 	qmsg(green(('\n','')[bool(opt.verbose)] + 'OK'))
 elif a and not fh:
-	m = "Testing speed of address generator '{}'"
-	qmsg(green(m.format(g.key_generators[a-1])))
+	m = "Testing speed of address generator '{}' for coin {}"
+	qmsg(green(m.format(g.key_generators[a-1],g.coin)))
 	from struct import pack,unpack
 	seed = os.urandom(28)
 	print 'Incrementing key with each round'
@@ -160,7 +207,7 @@ elif a and not fh:
 		sec = PrivKey(seed+pack('I',i),compressed)
 		a_addr = ag.to_addr(kg.to_pubhex(sec))
 		vmsg('\nkey:  %s\naddr: %s\n' % (sec.wif,a_addr))
-		if not opt.segwit:
+		if not opt.segwit and g.coin not in ('ETC','ETC'):
 			compressed = not compressed
 	qmsg_r('\rRound %s/%s ' % (i+1,rounds))
 

+ 19 - 0
test/ref/dash/98831F3A-DASH-C[1,31-33,500-501,1010-1011].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 tracking wallet label upon import.
+# The label may contain any printable ASCII symbol.
+# Address data checksum for 98831F3A-DASH-C[1,31-33,500-501,1010-1011]: FBC1 6B6A 0988 4403
+# Record this value to a secure location.
+98831F3A DASH:COMPRESSED {
+  1     XsjAJvCxkxYh55ZvCZMFEv2eJUVo5xxbwi
+  31    XdxyGv5KDFCqQH8N82gyqQ6vGVu9M1eERy
+  32    XwQSEW9ut1c2itk9bFZCEFcQjMwczLaiJZ
+  33    XaqBXjNHm484ansvkbBzweDG79LNXY9qXT
+  500   Xne3CdDVaH5gyf751pWKpTq7kJR5zuvkYi
+  501   Xqjr9qPHBoUR3kY4D5ZF3RGyw7JMxBLD5Y
+  1010  XoQ5xaSPTKVENQ5adyDBuaf44ySYMfkWwk
+  1011  XhSrc1yoNLJvk9uvr2xZM8LUwwLyYTWrLo
+}

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


+ 19 - 0
test/ref/ethereum/98831F3A-ETH[1,31-33,500-501,1010-1011].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 tracking wallet label upon import.
+# The label may contain any printable ASCII symbol.
+# Address data checksum for 98831F3A-ETH[1,31-33,500-501,1010-1011]: E554 076E 7AF6 66A3
+# Record this value to a secure location.
+98831F3A ETH {
+  1     e704b6cfd9f0edb2e6cfbd0c913438d37ede7b35
+  31    62ff8e4dbd251b98102e3fb5e4b14119e24cadde
+  32    5fb2f71cd3a2fadeb00e4ac5327cc48941655a74
+  33    130e4118ed2badb5938e55ab2ff0c1e05072cd6c
+  500   92594035614a1e10a2add25690a120d537f9ccbf
+  501   84d763e98a24063f92f004f242d7c9282a44f09b
+  1010  04125d2de2355b5b21980ca5c51c33caa4865d2a
+  1011  b01ea3045d4b2cbb3e801822d4173c772cfc48d9
+}

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


+ 19 - 0
test/ref/ethereum_classic/98831F3A-ETC[1,31-33,500-501,1010-1011].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 tracking wallet label upon import.
+# The label may contain any printable ASCII symbol.
+# Address data checksum for 98831F3A-ETC[1,31-33,500-501,1010-1011]: E97A D796 B495 E8BC
+# Record this value to a secure location.
+98831F3A ETC {
+  1     1a6acbef8c38f52f20d04ecded2992b04d8608d7
+  31    91a3da377e7b454ccb6135a35126ec81e93d2a43
+  32    8b6b7368d896d0b8a128aa73b4d4a197ce74fbf5
+  33    36583a66cf8cc74571f7c267b6361e8951f71869
+  500   93c04597594058499735fcfb6da07cb0187df30b
+  501   084f53a77ced2ea7ee9f96e941d95d581efcc3b2
+  1010  e985efcc82520bd203af430000322f1346c7719a
+  1011  61adf2969ea8fd387a474fefbb4c1df7bca851ce
+}

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


+ 19 - 0
test/ref/zcash/98831F3A-ZEC-C[1,31-33,500-501,1010-1011].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 tracking wallet label upon import.
+# The label may contain any printable ASCII symbol.
+# Address data checksum for 98831F3A-ZEC-C[1,31-33,500-501,1010-1011]: 903E 7225 DD86 6E01
+# Record this value to a secure location.
+98831F3A ZEC:COMPRESSED {
+  1     t1d47QeTehQye4Mms1Lmx7dPjKVoTtHXKmu
+  31    t1ZVJyRRwKcpHn4s6ddqDMgXn9v81GddzFv
+  32    t1bgBCPE2qXtGNoBY9LZWs6JL16eJ5tKLhT
+  33    t1dzBjbV66hymb9KHUuax1FtMh9X8xaMhhf
+  500   t1KiqudpmQPhnKx33w5kLittD9PihAKe5hU
+  501   t1JEQh88x4TgqNqMkUmZbzJCh89KAuqPKGQ
+  1010  t1W5cD3RWq7BJQte5WAbhe67S8ogYbthCnv
+  1011  t1WKKvpybUSndW3KHuMcU5XRTK7XRmtpude
+}

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


+ 103 - 0
test/scrambletest.py

@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+test/scrambletest.py: seed scrambling and addrlist metadata generation tests for all supported altcoins
+"""
+
+import sys,os,subprocess
+repo_root = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]),os.pardir)))
+os.chdir(repo_root)
+sys.path.__setitem__(0,repo_root)
+
+# Import this _after_ local path's been added to sys.path
+from mmgen.common import *
+
+opts_data = lambda: {
+	'desc': 'Test seed scrambling and addrlist metadata generation for all supported altcoins',
+	'usage':'[options] [command]',
+	'options': """
+-h, --help          Print this help message
+--, --longhelp      Print help message for long options (common options)
+-l, --list-cmds     List and describe the tests and commands in this test suite
+-s, --system        Test scripts and modules installed on system rather than
+                    those in the repo root
+-v, --verbose       Produce more verbose output
+""",
+	'notes': """
+
+If no command is given, the whole suite of tests is run.
+"""
+}
+
+cmd_args = opts.init(opts_data)
+
+os.environ['MMGEN_DEBUG_ADDRLIST'] = '1'
+if not opt.system:
+	os.environ['PYTHONPATH'] = repo_root
+
+from collections import OrderedDict
+test_data = OrderedDict([
+#                      SCRAMBLED_SEED[:8]  SCRAMBLE_KEY      ID_STR      LBL
+	('btc',            ('456d7f5f1c4bfe3b', '(none)',         '',        '')),
+	('btc_compressed', ('bf98a4af5464a4ef', 'compressed',     '-C',      'COMPRESSED')),
+	('btc_segwit',     ('b56962d829ffc678', 'segwit',         '-S',      'SEGWIT')),
+	('bch',            ('456d7f5f1c4bfe3b', '(none)',         '',        '')),
+	('bch_compressed', ('bf98a4af5464a4ef', 'compressed',     '-C',      'COMPRESSED')),
+	('ltc',            ('b11f16632e63ba92', 'ltc:legacy',     '-LTC',    'LTC')),
+	('ltc_compressed', ('7ccf465d466ee7d3', 'ltc:compressed', '-LTC-C',  'LTC:COMPRESSED')),
+	('ltc_segwit',     ('9460f5ba15e82768', 'ltc:segwit',     '-LTC-S',  'LTC:SEGWIT')),
+	('dash',           ('bb21cf88c198ab8c', 'dash:compressed','-DASH-C', 'DASH:COMPRESSED')),
+	('zec',            ('637f7b8117b524ed', 'zec:compressed', '-ZEC-C',  'ZEC:COMPRESSED')),
+	('eth',            ('213ed116869b19f2', 'eth',            '-ETH',    'ETH')),
+	('etc',            ('909def37096f5ab8', 'etc',            '-ETC',    'ETC')),
+])
+
+def run_tests():
+	for test in test_data:
+		try:    coin,mmtype = test.split('_')
+		except: coin,mmtype = test,None
+		cmd_name = 'cmds/mmgen-addrgen'
+		wf = 'test/ref/98831F3A.mmwords'
+		type_arg = ['--type='+mmtype] if mmtype else []
+		cmd = [cmd_name,'-qS','--coin='+coin] + type_arg + [wf,'1']
+		vmsg(green('Executing: {}'.format(' '.join(cmd))))
+		msg_r('Testing: --coin {:4} {:22}'.format(coin.upper(),type_arg[0] if type_arg else ''))
+		p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
+		o = p.stdout.read().splitlines()
+#		pmsg(o)
+		d = [e for e in o if len(e) > 4 and e[:9] == 'sc_debug_']
+#		pmsg(d)
+		for n,k in enumerate(['seed','str','id_str','lbl']):
+			kk = 'sc_debug_'+k
+			a = test_data[test][n]
+			b = [e for e in d if e[:len(kk)] == kk][0][len(kk)+2:]
+#			pmsg(b); continue
+			if b == a:
+				vmsg('sc_{}: {}'.format(k,a))
+			else:
+				rdie(1,'\nError: sc_{} value {} does not match reference value {}'.format(k,b,a))
+		msg('OK')
+
+start_time = int(time.time())
+
+run_tests()
+
+t = int(time.time()) - start_time
+m =	'\nAll requested tests finished OK, elapsed time: {:02}:{:02}'
+msg(green(m.format(t/60,t%60)))

+ 62 - 10
test/test.py

@@ -431,9 +431,9 @@ cfgs = {
 		'passfile32_chk':  'F6C1 CDFB 97D9 FCAE',
 		'wpasswd':         'reference password',
 		'ref_wallet':      '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.testnet]),
-		'ref_addrfile':    '98831F3A{}[1,31-33,500-501,1010-1011]{}.addrs'.format(altcoin_pfx,tn_ext),
-		'ref_segwitaddrfile':'98831F3A{}-S[1,31-33,500-501,1010-1011]{}.addrs'.format(altcoin_pfx,tn_ext),
-		'ref_keyaddrfile': '98831F3A{}[1,31-33,500-501,1010-1011]{}.akeys.mmenc'.format(altcoin_pfx,tn_ext),
+		'ref_addrfile':    '98831F3A{}[1,31-33,500-501,1010-1011]{}.addrs',
+		'ref_segwitaddrfile':'98831F3A{}-S[1,31-33,500-501,1010-1011]{}.addrs',
+		'ref_keyaddrfile': '98831F3A{}[1,31-33,500-501,1010-1011]{}.akeys.mmenc',
 		'ref_passwdfile':  '98831F3A-фубар@crypto.org-b58-20[1,4,9-11,1100].pws',
 		'ref_addrfile_chksum': {
 			'btc': ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E'),
@@ -447,7 +447,15 @@ cfgs = {
 			'btc': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2'),
 			'ltc': ('B804 978A 8796 3ED4','93A6 844C 8ECC BEF4'),
 		},
-		'ref_passwdfile_chksum':  'A983 DAB9 5514 27FB',
+		'ref_addrfile_chksum_zec': '903E 7225 DD86 6E01',
+		'ref_addrfile_chksum_dash':'FBC1 6B6A 0988 4403',
+		'ref_addrfile_chksum_eth': 'E554 076E 7AF6 66A3',
+		'ref_addrfile_chksum_etc': 'E97A D796 B495 E8BC',
+		'ref_keyaddrfile_chksum_zec': 'F05A 5A5C 0C8E 2617',
+		'ref_keyaddrfile_chksum_dash': 'E83D 2C63 FEA2 4142',
+		'ref_keyaddrfile_chksum_eth': '3635 4DCF B752 8772',
+		'ref_keyaddrfile_chksum_etc': '9BAC 38E7 5C8E 42E0',
+		'ref_passwdfile_chksum':   'A983 DAB9 5514 27FB',
 #		'ref_fake_unspent_data':'98831F3A_unspent.json',
 		'ref_tx_file': {
 			'btc': 'FFB367[1.234]{}.rawtx',
@@ -700,6 +708,17 @@ cmd_group['misc'] = (
 	('autosign', 'transaction autosigning (BTC,BCH,LTC)'),
 )
 
+cmd_group['altcoin_ref'] = (
+	('ref_addrfile_chk_zec', 'reference address file (ZEC)'),
+	('ref_addrfile_chk_dash','reference address file (DASH)'),
+	('ref_addrfile_chk_eth', 'reference address file (ETH)'),
+	('ref_addrfile_chk_etc', 'reference address file (ETC)'),
+	('ref_keyaddrfile_chk_zec', 'reference key-address file (ZEC)'),
+	('ref_keyaddrfile_chk_dash','reference key-address file (DASH)'),
+	('ref_keyaddrfile_chk_eth', 'reference key-address file (ETH)'),
+	('ref_keyaddrfile_chk_etc', 'reference key-address file (ETC)'),
+)
+
 # undocumented admin cmds
 cmd_group_admin = OrderedDict()
 cmd_group_admin['create_ref_tx'] = (
@@ -778,6 +797,11 @@ for a,b in cmd_group['misc']:
 	cmd_list['misc'].append(a)
 	cmd_data[a] = (18,b,[[[],18]])
 
+cmd_data['info_altcoin_ref'] = 'altcoin reference files',[8]
+for a,b in cmd_group['altcoin_ref']:
+	cmd_list['altcoin_ref'].append(a)
+	cmd_data[a] = (8,b,[[[],8]])
+
 utils = {
 	'check_deps': 'check dependencies for specified command',
 	'clean':      'clean specified tmp dir(s) 1,2,3,4,5 or 6 (no arg = all dirs)',
@@ -2006,20 +2030,48 @@ class MMGenTestSuite(object):
 			t.close()
 			cmp_or_die(cfg['seed_id'],chk)
 
-	def ref_addrfile_chk(self,name,ftype='addr'):
+	def ref_addrfile_chk(self,name,ftype='addr',coin=None,subdir=None,pfx=None,mmtype=None):
 		af_key = 'ref_{}file'.format(ftype)
-		af = os.path.join(ref_dir,(ref_subdir,'')[ftype=='passwd'],cfg[af_key])
-		t = MMGenExpect(name,'mmgen-tool',[ftype.replace('segwit','')+'file_chksum',af])
+		af_fn = cfg[af_key].format(pfx or altcoin_pfx,'' if coin else tn_ext)
+		af = os.path.join(ref_dir,(subdir or ref_subdir,'')[ftype=='passwd'],af_fn)
+		coin_arg = [] if coin == None else ['--coin='+coin]
+		t = MMGenExpect(name,'mmgen-tool',coin_arg+[ftype.replace('segwit','')+'file_chksum',af])
 		if ftype == 'keyaddr':
 			w = 'key-address data'
 			t.hash_preset(w,ref_kafile_hash_preset)
 			t.passphrase(w,ref_kafile_pass)
 			t.expect('Check key-to-address validity? (y/N): ','y')
 		o = t.read().strip().split('\n')[-1]
-		rc = cfg['ref_'+ftype+'file_chksum']
-		ref_chksum = rc if ftype == 'passwd' else rc[g.proto.base_coin.lower()][g.testnet]
+		rc = cfg[ 'ref_' + ftype + 'file_chksum' +
+				  ('_'+coin.lower() if coin else '') +
+				  ('_'+mmtype if mmtype else '')]
+		ref_chksum = rc if (ftype == 'passwd' or coin) else rc[g.proto.base_coin.lower()][g.testnet]
 		cmp_or_die(ref_chksum,o)
 
+	def ref_addrfile_chk_zec(self,name):
+		self.ref_addrfile_chk(name,ftype='addr',coin='ZEC',subdir='zcash',pfx='-ZEC-C')
+
+	def ref_addrfile_chk_dash(self,name):
+		self.ref_addrfile_chk(name,ftype='addr',coin='DASH',subdir='dash',pfx='-DASH-C')
+
+	def ref_addrfile_chk_eth(self,name):
+		self.ref_addrfile_chk(name,ftype='addr',coin='ETH',subdir='ethereum',pfx='-ETH')
+
+	def ref_addrfile_chk_etc(self,name):
+		self.ref_addrfile_chk(name,ftype='addr',coin='ETC',subdir='ethereum_classic',pfx='-ETC')
+
+	def ref_keyaddrfile_chk_zec(self,name):
+		self.ref_addrfile_chk(name,ftype='keyaddr',coin='ZEC',subdir='zcash',pfx='-ZEC-C')
+
+	def ref_keyaddrfile_chk_dash(self,name):
+		self.ref_addrfile_chk(name,ftype='keyaddr',coin='DASH',subdir='dash',pfx='-DASH-C')
+
+	def ref_keyaddrfile_chk_eth(self,name):
+		self.ref_addrfile_chk(name,ftype='keyaddr',coin='ETH',subdir='ethereum',pfx='-ETH')
+
+	def ref_keyaddrfile_chk_etc(self,name):
+		self.ref_addrfile_chk(name,ftype='keyaddr',coin='ETC',subdir='ethereum_classic',pfx='-ETC')
+
 	def ref_keyaddrfile_chk(self,name):
 		self.ref_addrfile_chk(name,ftype='keyaddr')
 
@@ -2597,7 +2649,7 @@ class MMGenTestSuite(object):
 		g.proto = psave
 
 		for k in ('in_addrs','out_addrs'):
-			tx[k+'_conv'] = [g.proto.hexaddr2addr(h,(False,True)[f=='p2sh']) for h,f in tx[k+'_hex']]
+			tx[k+'_conv'] = [g.proto.pubhash2addr(h,(False,True)[f=='p2sh']) for h,f in tx[k+'_hex']]
 
 		for k in ('in','out'):
 			for i in range(len(tx[k])):

+ 2 - 2
test/tooltest.py

@@ -71,7 +71,7 @@ cmd_data = OrderedDict([
 				('Privhex2addr',   ('Wif2hex','o3')), # compare with output of Randpair
 				('Hex2wif',        ('Wif2hex','io2')),
 				('Addr2hexaddr',   ('Randpair','o2')),
-				('Hexaddr2addr',   ('Addr2hexaddr','io2')),
+				('Pubhash2addr',   ('Addr2hexaddr','io2')),
 
 				('Pipetest',       ('Randpair','o3')),
 			])
@@ -366,7 +366,7 @@ class MMGenToolTestSuite(object):
 		for n,f,m in ((1,f1,''),(2,f2,'from compressed')):
 			addr = read_from_file(f).split()[-1]
 			self.run_cmd_out(name,addr,fn_idx=n,extra_msg=m)
-	def Hexaddr2addr(self,name,f1,f2,f3,f4):
+	def Pubhash2addr(self,name,f1,f2,f3,f4):
 		for n,fi,fo,m in ((1,f1,f2,''),(2,f3,f4,'from compressed')):
 			self.run_cmd_chk(name,fi,fo,extra_msg=m)
 	def Privhex2pubhex(self,name,f1,f2,f3): # from Hex2wif