Browse Source

overhaul public key and address generation code

- pubkey generation code has been rewritten and moved from addr.py to
  keygen.py
- address generation code has been rewritten and moved from addr.py to
  addrgen.py
- keygen/addrgen classes now present a consistent API across all pubkey and
  address types
- key/address operations and related data objects now use bytes internally
  instead of hex strings
- pubkey generator backends are now selected using the `--keygen-backend`
  option
- for Monero pubkeys, the new `nacl` backend has replaced `ed25519ll_djbec`
  as the default
- a minimal unit test has been added

Examples:

    # Generate a random Monero keypair using the unoptimized 'ed25519' backend:
    $ mmgen-tool --coin=xmr --keygen-backend=3 randpair

    # Generate an LTC Bech32 address list from the default wallet using the
    # 'python-ecdsa' backend:
    $ mmgen-addrgen --coin=ltc --type=bech32 --keygen-backend=2 1-10

Testing:

    # Run the minimal unit test:
    $ test/unit_tests_py gen

    # Compare BTC Segwit addresses from default 'libsecp256k1' backend to
    # 'pycoin' library, with edge cases and 10,000 random rounds:
    $ test/gentest.py --type=segwit 1:pycoin 10000

    # Test all configured Monero backends against 'moneropy', with edge cases
    # and 10 random rounds:
    $ test/gentest.py --coin=xmr all:moneropy 10

    # Test the 'nacl' and 'ed25519ll_djbec' backends against each other, with
    # edge cases and 1000 random rounds:
    $ test/gentest.py --coin=xmr 1:2 1000

    # Test the speed of the Monero 'nacl' backend using 10,000 rounds:
    $ test/gentest.py --coin=xmr 1 10000

    # Same for Zcash:
    $ test/gentest.py --coin=zec --type=zcash_z 1 10000
The MMGen Project 3 years ago
parent
commit
32c522c0

+ 37 - 254
mmgen/addr.py

@@ -17,7 +17,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-addr.py:  Address generation/display routines for the MMGen suite
+addr.py: MMGen address-related types
 """
 
 from string import ascii_letters,digits
@@ -179,260 +179,43 @@ class MoneroViewKey(HexStr):
 class ZcashViewKey(CoinAddr):
 	hex_width = 128
 
-from .opts import opt
-from .util import qmsg
-from .protocol import hash160
-from .key import PrivKey,PubKey
-from .baseconv import baseconv
+def KeyGenerator(proto,pubkey_type,backend=None,silent=False):
+	"""
+	factory function returning a key generator backend for the specified pubkey type
+	"""
+	assert pubkey_type in proto.pubkey_types, f'{pubkey_type!r}: invalid pubkey type for coin {proto.coin}'
 
-class AddrGenerator(MMGenObject):
-	def __new__(cls,proto,addr_type):
+	from .keygen import keygen_backend,_check_backend
 
-		if type(addr_type) == str:
-			addr_type = MMGenAddrType(proto=proto,id_str=addr_type)
-		elif type(addr_type) == MMGenAddrType:
-			assert addr_type in proto.mmtypes, f'{addr_type}: invalid address type for coin {proto.coin}'
-		else:
-			raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()')
-
-		addr_generators = {
-			'p2pkh':    AddrGeneratorP2PKH,
-			'segwit':   AddrGeneratorSegwit,
-			'bech32':   AddrGeneratorBech32,
-			'ethereum': AddrGeneratorEthereum,
-			'zcash_z':  AddrGeneratorZcashZ,
-			'monero':   AddrGeneratorMonero,
-		}
-		me = super(cls,cls).__new__(addr_generators[addr_type.gen_method])
-		me.desc = type(me).__name__
-		me.proto = proto
-		me.addr_type = addr_type
-		me.pubkey_type = addr_type.pubkey_type
-		return me
-
-class AddrGeneratorP2PKH(AddrGenerator):
-	def to_addr(self,pubhex):
-		assert pubhex.privkey.pubkey_type == self.pubkey_type
-		return CoinAddr(self.proto,self.proto.pubhash2addr(hash160(pubhex),p2sh=False))
-
-	def to_segwit_redeem_script(self,pubhex):
-		raise NotImplementedError('Segwit redeem script not supported by this address type')
-
-class AddrGeneratorSegwit(AddrGenerator):
-	def to_addr(self,pubhex):
-		assert pubhex.privkey.pubkey_type == self.pubkey_type
-		assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
-		return CoinAddr(self.proto,self.proto.pubhex2segwitaddr(pubhex))
-
-	def to_segwit_redeem_script(self,pubhex):
-		assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
-		return HexStr(self.proto.pubhex2redeem_script(pubhex))
-
-class AddrGeneratorBech32(AddrGenerator):
-	def to_addr(self,pubhex):
-		assert pubhex.privkey.pubkey_type == self.pubkey_type
-		assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
-		return CoinAddr(self.proto,self.proto.pubhash2bech32addr(hash160(pubhex)))
-
-	def to_segwit_redeem_script(self,pubhex):
-		raise NotImplementedError('Segwit redeem script not supported by this address type')
-
-class AddrGeneratorEthereum(AddrGenerator):
-
-	def __init__(self,proto,addr_type):
-
-		from .util import get_keccak
-		self.keccak_256 = get_keccak()
-
-		from .protocol import hash256
-		self.hash256 = hash256
-
-	def to_addr(self,pubhex):
-		assert pubhex.privkey.pubkey_type == self.pubkey_type
-		return CoinAddr(self.proto,self.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:])
-
-	def to_wallet_passwd(self,sk_hex):
-		return WalletPassword(self.hash256(sk_hex)[:32])
-
-	def to_segwit_redeem_script(self,pubhex):
-		raise NotImplementedError('Segwit redeem script not supported by this address type')
-
-# github.com/FiloSottile/zcash-mini/zcash/address.go
-class AddrGeneratorZcashZ(AddrGenerator):
-
-	def zhash256(self,s,t):
-		s = bytearray(s + bytes(32))
-		s[0] |= 0xc0
-		s[32] = t
-		from .sha2 import Sha256
-		return Sha256(s,preprocess=False).digest()
-
-	def to_addr(self,pubhex): # pubhex is really privhex
-		assert pubhex.privkey.pubkey_type == self.pubkey_type
-		key = bytes.fromhex(pubhex)
-		assert len(key) == 32, f'{len(key)}: incorrect privkey length'
-		from nacl.bindings import crypto_scalarmult_base
-		p2 = crypto_scalarmult_base(self.zhash256(key,1))
-		from .protocol import _b58chk_encode
-		ver_bytes = self.proto.addr_fmt_to_ver_bytes('zcash_z')
-		ret = _b58chk_encode(ver_bytes + self.zhash256(key,0) + p2)
-		return CoinAddr(self.proto,ret)
-
-	def to_viewkey(self,pubhex): # pubhex is really privhex
-		key = bytes.fromhex(pubhex)
-		assert len(key) == 32, f'{len(key)}: incorrect privkey length'
-		vk = bytearray(self.zhash256(key,0)+self.zhash256(key,1))
-		vk[32] &= 0xf8
-		vk[63] &= 0x7f
-		vk[63] |= 0x40
-		from .protocol import _b58chk_encode
-		ver_bytes = self.proto.addr_fmt_to_ver_bytes('viewkey')
-		ret = _b58chk_encode(ver_bytes + vk)
-		return ZcashViewKey(self.proto,ret)
-
-	def to_segwit_redeem_script(self,pubhex):
-		raise NotImplementedError('Zcash z-addresses incompatible with Segwit')
-
-class AddrGeneratorMonero(AddrGenerator):
-
-	def __init__(self,proto,addr_type):
-
-		from .util import get_keccak
-		self.keccak_256 = get_keccak()
-
-		from .protocol import hash256
-		self.hash256 = hash256
-
-		if getattr(opt,'use_old_ed25519',False):
-			from .ed25519 import edwards,encodepoint,B,scalarmult
-		else:
-			from .ed25519ll_djbec import scalarmult
-			from .ed25519 import edwards,encodepoint,B
-
-		self.edwards     = edwards
-		self.encodepoint = encodepoint
-		self.scalarmult  = scalarmult
-		self.B           = B
-
-	def b58enc(self,addr_bytes):
-		enc = baseconv.frombytes
-		l = len(addr_bytes)
-		a = ''.join([enc(addr_bytes[i*8:i*8+8],'b58',pad=11,tostr=True) for i in range(l//8)])
-		b = enc(addr_bytes[l-l%8:],'b58',pad=7,tostr=True)
-		return a + b
-
-	def to_addr(self,sk_hex): # sk_hex instead of pubhex
-		assert sk_hex.privkey.pubkey_type == self.pubkey_type
-
-		# Source and license for scalarmultbase function:
-		#   https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py
-		# Copyright (c) 2014-2016, The Monero Project
-		# All rights reserved.
-		def scalarmultbase(e):
-			if e == 0: return [0, 1]
-			Q = self.scalarmult(self.B, e//2)
-			Q = self.edwards(Q, Q)
-			if e & 1: Q = self.edwards(Q, self.B)
-			return Q
-
-		def hex2int_le(hexstr):
-			return int((bytes.fromhex(hexstr)[::-1]).hex(),16)
-
-		vk_hex = self.to_viewkey(sk_hex)
-		pk_str  = self.encodepoint(scalarmultbase(hex2int_le(sk_hex)))
-		pvk_str = self.encodepoint(scalarmultbase(hex2int_le(vk_hex)))
-		addr_p1 = self.proto.addr_fmt_to_ver_bytes('monero') + pk_str + pvk_str
-
-		return CoinAddr(
-			proto = self.proto,
-			addr = self.b58enc(addr_p1 + self.keccak_256(addr_p1).digest()[:4]) )
-
-	def to_wallet_passwd(self,sk_hex):
-		return WalletPassword(self.hash256(sk_hex)[:32])
-
-	def to_viewkey(self,sk_hex):
-		assert len(sk_hex) == 64, f'{len(sk_hex)}: incorrect privkey length'
-		return MoneroViewKey(
-			self.proto.preprocess_key(self.keccak_256(bytes.fromhex(sk_hex)).digest(),None).hex() )
-
-	def to_segwit_redeem_script(self,sk_hex):
-		raise NotImplementedError('Monero addresses incompatible with Segwit')
-
-class KeyGenerator(MMGenObject):
-
-	def __new__(cls,proto,addr_type,generator=None,silent=False):
-		if type(addr_type) == str: # allow override w/o check
-			pubkey_type = addr_type
-		elif type(addr_type) == MMGenAddrType:
-			assert addr_type in proto.mmtypes, f'{address}: invalid address type for coin {proto.coin}'
-			pubkey_type = addr_type.pubkey_type
-		else:
-			raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()')
-		if pubkey_type == 'std':
-			if cls.test_for_secp256k1(silent=silent) and generator != 1:
-				if not opt.key_generator or opt.key_generator == 2 or generator == 2:
-					me = super(cls,cls).__new__(KeyGeneratorSecp256k1)
-			else:
-				qmsg('Using (slow) native Python ECDSA library for address generation')
-				me = super(cls,cls).__new__(KeyGeneratorPython)
-		elif pubkey_type in ('zcash_z','monero'):
-			me = super(cls,cls).__new__(KeyGeneratorDummy)
-			me.desc = 'mmgen-'+pubkey_type
-		else:
-			raise ValueError(f'{pubkey_type}: invalid pubkey_type argument')
+	pubkey_type_cls = getattr(keygen_backend,pubkey_type)
 
-		me.proto = proto
-		return me
+	from .opts import opt
+	backend = backend or getattr(opt,'keygen_backend',None)
 
-	@classmethod
-	def test_for_secp256k1(self,silent=False):
-		try:
-			from .secp256k1 import priv2pub
-			m = 'Unable to execute priv2pub() from secp256k1 extension module'
-			assert priv2pub(bytes.fromhex('deadbeef'*8),1),m
-			return True
-		except Exception as e:
-			if not silent:
-				ymsg(str(e))
-			return False
-
-class KeyGeneratorPython(KeyGenerator):
-
-	desc = 'mmgen-python-ecdsa'
-
-	# 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(self,numpriv,compressed=False):
-		import ecdsa
-		pko = ecdsa.SigningKey.from_secret_exponent(numpriv,curve=ecdsa.SECP256k1)
-		# pubkey = x (32 bytes) + y (32 bytes) (unsigned big-endian)
-		pubkey = pko.get_verifying_key().to_string().hex()
-		if compressed: # discard Y coord, replace with appropriate version byte
-			# even y: <0, odd y: >0 -- https://bitcointalk.org/index.php?topic=129652.0
-			return ('03','02')[pubkey[-1] in '02468ace'] + pubkey[:64]
-		else:
-			return '04' + pubkey
-
-	def to_pubhex(self,privhex):
-		assert type(privhex) == PrivKey
-		return PubKey(
-			s       = self.privnum2pubhex(int(privhex,16),compressed=privhex.compressed),
-			privkey = privhex )
-
-class KeyGeneratorSecp256k1(KeyGenerator):
-	desc = 'mmgen-secp256k1'
-	def to_pubhex(self,privhex):
-		assert type(privhex) == PrivKey
-		from .secp256k1 import priv2pub
-		return PubKey(
-			s       = priv2pub(bytes.fromhex(privhex),int(privhex.compressed)).hex(),
-			privkey = privhex )
-
-class KeyGeneratorDummy(KeyGenerator):
-	desc = 'mmgen-dummy'
-	def to_pubhex(self,privhex):
-		assert type(privhex) == PrivKey
-		return PubKey(
-			s       = privhex,
-			privkey = privhex )
+	if backend:
+		_check_backend(backend,pubkey_type)
+
+	backend_id = pubkey_type_cls.backends[int(backend) - 1 if backend else 0]
+
+	if backend_id == 'libsecp256k1':
+		if not pubkey_type_cls.libsecp256k1.test_avail(silent=silent):
+			backend_id = 'python-ecdsa'
+			if not backend:
+				qmsg('Using (slow) native Python ECDSA library for public key generation')
+
+	return getattr(pubkey_type_cls,backend_id.replace('-','_'))()
+
+def AddrGenerator(proto,addr_type):
+	"""
+	factory function returning an address generator for the specified address type
+	"""
+	if type(addr_type) == str:
+		addr_type = MMGenAddrType(proto=proto,id_str=addr_type)
+	elif type(addr_type) == MMGenAddrType:
+		assert addr_type in proto.mmtypes, f'{addr_type}: invalid address type for coin {proto.coin}'
+	else:
+		raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()')
+
+	from .addrgen import addr_generator
+
+	return getattr(addr_generator,addr_type.name)(proto,addr_type)

+ 2 - 2
mmgen/addrfile.py

@@ -119,7 +119,7 @@ class AddrFile(MMGenObject):
 				if p.has_keys:
 					from .opts import opt
 					if opt.b16:
-						out.append(fs.format( '', f'orig_hex: {e.sec.orig_hex()}', c ))
+						out.append(fs.format( '', f'orig_hex: {e.sec.orig_bytes.hex()}', c ))
 					out.append(fs.format( '', f'{p.al_id.mmtype.wif_label}: {e.sec.wif}', c ))
 					for k in ('viewkey','wallet_passwd'):
 						v = getattr(e,k)
@@ -174,7 +174,7 @@ class AddrFile(MMGenObject):
 				llen = len(ret)
 				for n,e in enumerate(ret):
 					qmsg_r(f'\rVerifying keys {n+1}/{llen}')
-					assert e.addr == ag.to_addr(kg.to_pubhex(e.sec)),(
+					assert e.addr == ag.to_addr(kg.gen_data(e.sec)),(
 						f'Key doesn’t match address!\n  {e.sec.wif}\n  {e.addr}')
 				qmsg(' - done')
 

+ 131 - 0
mmgen/addrgen.py

@@ -0,0 +1,131 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+addrgen.py: Address and view key generation classes for the MMGen suite
+"""
+
+from .protocol import hash160,_b58chk_encode
+from .addr import CoinAddr,MMGenAddrType,MoneroViewKey,ZcashViewKey
+
+# decorator for to_addr() and to_viewkey()
+def check_data(orig_func):
+	def f(self,data):
+		assert data.pubkey_type == self.pubkey_type, 'addrgen.py:check_data() pubkey_type mismatch'
+		assert data.compressed == self.compressed,(
+	f'addrgen.py:check_data() expected compressed={self.compressed} but got compressed={data.compressed}'
+		)
+		return orig_func(self,data)
+	return f
+
+class addr_generator:
+	"""
+	provide a generator for each supported address format
+	"""
+	class base:
+
+		def __init__(self,proto,addr_type):
+			self.proto = proto
+			self.pubkey_type = addr_type.pubkey_type
+			self.compressed = addr_type.compressed
+			desc = f'AddrGenerator {type(self).__name__!r}'
+
+		def to_segwit_redeem_script(self,data):
+			raise NotImplementedError('Segwit redeem script not supported by this address type')
+
+	class p2pkh(base):
+
+		@check_data
+		def to_addr(self,data):
+			return CoinAddr(
+				self.proto,
+				self.proto.pubhash2addr( hash160(data.pubkey), p2sh=False ))
+
+	class legacy(p2pkh): pass
+	class compressed(p2pkh): pass
+
+	class segwit(base):
+
+		@check_data
+		def to_addr(self,data):
+			return CoinAddr(
+				self.proto,
+				self.proto.pubkey2segwitaddr(data.pubkey) )
+
+		def to_segwit_redeem_script(self,data): # NB: returns hex
+			return self.proto.pubkey2redeem_script(data.pubkey).hex()
+
+	class bech32(base):
+
+		@check_data
+		def to_addr(self,data):
+			return CoinAddr(
+				self.proto,
+				self.proto.pubhash2bech32addr( hash160(data.pubkey)) )
+
+	class keccak(base):
+
+		def __init__(self,proto,addr_type):
+			super().__init__(proto,addr_type)
+			from .util import get_keccak
+			self.keccak_256 = get_keccak()
+
+	class ethereum(keccak):
+
+		@check_data
+		def to_addr(self,data):
+			return CoinAddr(
+				self.proto,
+				self.keccak_256(data.pubkey[1:]).hexdigest()[24:] )
+
+	class monero(keccak):
+
+		def b58enc(self,addr_bytes):
+			from .baseconv import baseconv
+			enc = baseconv.frombytes
+			l = len(addr_bytes)
+			a = ''.join([enc( addr_bytes[i*8:i*8+8], 'b58', pad=11, tostr=True ) for i in range(l//8)])
+			b = enc( addr_bytes[l-l%8:], 'b58', pad=7, tostr=True )
+			return a + b
+
+		@check_data
+		def to_addr(self,data):
+			step1 = self.proto.addr_fmt_to_ver_bytes('monero') + data.pubkey
+			return CoinAddr(
+				proto = self.proto,
+				addr = self.b58enc( step1 + self.keccak_256(step1).digest()[:4]) )
+
+		@check_data
+		def to_viewkey(self,data):
+			return MoneroViewKey( data.viewkey_bytes.hex() )
+
+	class zcash_z(base):
+
+		@check_data
+		def to_addr(self,data):
+			ret = _b58chk_encode(
+				self.proto.addr_fmt_to_ver_bytes('zcash_z')
+				+ data.pubkey )
+			return CoinAddr( self.proto, ret )
+
+		@check_data
+		def to_viewkey(self,data):
+			ret = _b58chk_encode(
+				self.proto.addr_fmt_to_ver_bytes('viewkey')
+				+ data.viewkey_bytes )
+			return ZcashViewKey( self.proto, ret )

+ 7 - 7
mmgen/addrlist.py

@@ -227,7 +227,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID
 
 		if self.gen_addrs:
 			from .addr import KeyGenerator,AddrGenerator
-			kg = KeyGenerator( self.proto, mmtype )
+			kg = KeyGenerator( self.proto, mmtype.pubkey_type )
 			ag = AddrGenerator( self.proto, mmtype )
 
 		t_addrs,out = ( len(addr_idxs), AddrListData() )
@@ -258,12 +258,12 @@ class AddrList(MMGenObject): # Address info for a single seed ID
 				pubkey_type = mmtype.pubkey_type )
 
 			if self.gen_addrs:
-				pubhex = kg.to_pubhex(e.sec)
-				e.addr = ag.to_addr(pubhex)
+				data = kg.gen_data(e.sec)
+				e.addr = ag.to_addr(data)
 				if gen_viewkey:
-					e.viewkey = ag.to_viewkey(pubhex)
+					e.viewkey = ag.to_viewkey(data)
 				if gen_wallet_passwd:
-					e.wallet_passwd = ag.to_wallet_passwd(e.sec)
+					e.wallet_passwd = self.gen_wallet_passwd(e.sec)
 			elif self.gen_passwds:
 				e.passwd = self.gen_passwd(e.sec) # TODO - own type
 
@@ -356,9 +356,9 @@ class AddrList(MMGenObject): # Address info for a single seed ID
 		def gen_addr(pk,t):
 			at = self.proto.addr_type(t)
 			from .addr import KeyGenerator,AddrGenerator
-			kg = KeyGenerator(self.proto,at)
+			kg = KeyGenerator(self.proto,at.pubkey_type)
 			ag = AddrGenerator(self.proto,at)
-			return ag.to_addr(kg.to_pubhex(pk))
+			return ag.to_addr(kg.gen_data(pk))
 
 		compressed_types = set(self.proto.mmtypes) - {'L','E'}
 		uncompressed_types = set(self.proto.mmtypes) & {'L','E'}

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-13.1.dev002
+13.1.dev003

+ 1 - 0
mmgen/exception.py

@@ -47,6 +47,7 @@ class BaseConversionPadError(Exception):  mmcode = 2
 class TransactionChainMismatch(Exception):mmcode = 2
 class ObjectInitError(Exception):         mmcode = 2
 class ClassFlagsError(Exception):         mmcode = 2
+class ExtensionModuleError(Exception):    mmcode = 2
 
 # 3: yellow hl, 'MMGen Error' + exception + message
 class RPCFailure(Exception):              mmcode = 3

+ 1 - 4
mmgen/globalvars.py

@@ -173,7 +173,7 @@ class GlobalContext(Lockable):
 	required_opts = (
 		'quiet','verbose','debug','outdir','echo_passphrase','passwd_file','stdout',
 		'show_hash_presets','label','keep_passphrase','keep_hash_preset','yes',
-		'brain_params','b16','usr_randchars','coin','bob','alice','key_generator',
+		'brain_params','b16','usr_randchars','coin','bob','alice',
 		'hidden_incog_input_params','in_fmt','hash_preset','seed_len',
 	)
 	incompatible_opts = (
@@ -271,9 +271,6 @@ class GlobalContext(Lockable):
 	aesctr_dfl_iv  = int.to_bytes(1,aesctr_iv_len,'big')
 	hincog_chk_len = 8
 
-	key_generators = ('python-ecdsa','libsecp256k1') # '1','2'
-	key_generator  = 2 # libsecp256k1 is default
-
 	force_standalone_scrypt_module = False
 	# Scrypt params: 'id_num': [N, r, p] (N is an exponent of two)
 	# NB: hashlib.scrypt in Python (>=v3.6) supports max N value of 14.  This means that

+ 11 - 0
mmgen/help.py

@@ -39,6 +39,17 @@ def help_notes_func(proto,po,k):
 
 	class help_notes:
 
+		def coin_id():
+			return proto.coin_id
+
+		def keygen_backends():
+			from .keygen import get_backends
+			from .addr import MMGenAddrType
+			backends = get_backends(
+				MMGenAddrType(proto,po.user_opts.get('type') or proto.dfl_mmtype).pubkey_type
+			)
+			return ' '.join( f'{n}:{k}{" [default]" if n==1 else ""}' for n,k in enumerate(backends,1) )
+
 		def coind_exec():
 			return coind_exec()
 

+ 17 - 16
mmgen/key.py

@@ -22,7 +22,7 @@ key.py: MMGen public and private key objects
 
 from string import ascii_letters,digits
 from .objmethods import Hilite,InitErrors,MMGenObject
-from .obj import ImmutableAttr,get_obj,HexStr
+from .obj import ImmutableAttr,get_obj
 
 class WifKey(str,Hilite,InitErrors):
 	"""
@@ -44,26 +44,26 @@ class WifKey(str,Hilite,InitErrors):
 def is_wif(proto,s):
 	return get_obj( WifKey, proto=proto, wif=s, silent=True, return_bool=True )
 
-class PubKey(HexStr,MMGenObject): # TODO: add some real checks
+class PubKey(bytes,InitErrors,MMGenObject): # TODO: add some real checks
 
-	def __new__(cls,s,privkey):
+	def __new__(cls,s,compressed):
 		try:
-			me = HexStr.__new__(cls,s,case='lower')
-			me.privkey = privkey
-			me.compressed = privkey.compressed
+			assert isinstance(s,bytes)
+			me = bytes.__new__(cls,s)
+			me.compressed = compressed
 			return me
 		except Exception as e:
 			return cls.init_fail(e,s)
 
-class PrivKey(str,Hilite,InitErrors,MMGenObject):
+class PrivKey(bytes,Hilite,InitErrors,MMGenObject):
 	"""
 	Input:   a) raw, non-preprocessed bytes; or b) WIF key.
-	Output:  preprocessed hexadecimal key, plus WIF key in 'wif' attribute
+	Output:  preprocessed key bytes, plus WIF key in 'wif' attribute
 	For coins without a WIF format, 'wif' contains the preprocessed hex.
 	The numeric validity of the resulting key is always checked.
 	"""
 	color = 'red'
-	width = 64
+	width = 32
 	trunc_ok = False
 
 	compressed = ImmutableAttr(bool,typeconv=False)
@@ -78,11 +78,11 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject):
 				assert s == None,"'wif' and key hex args are mutually exclusive"
 				assert set(wif) <= set(ascii_letters+digits),'not an ascii alphanumeric string'
 				k = proto.parse_wif(wif) # raises exception on error
-				me = str.__new__(cls,k.sec.hex())
+				me = bytes.__new__(cls,k.sec)
 				me.compressed = k.compressed
 				me.pubkey_type = k.pubkey_type
 				me.wif = str.__new__(WifKey,wif) # check has been done
-				me.orig_hex = None
+				me.orig_bytes = None
 				if k.sec != proto.preprocess_key(k.sec,k.pubkey_type):
 					from .exception import PrivateKeyError
 					raise PrivateKeyError(
@@ -94,19 +94,20 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject):
 		else:
 			try:
 				assert s,'private key bytes data missing'
+				assert isinstance(s,bytes),'input is not bytes'
 				assert pubkey_type is not None,"'pubkey_type' arg missing"
-				assert len(s) == cls.width // 2, f'key length must be {cls.width // 2} bytes'
+				assert len(s) == cls.width, f'key length must be {cls.width} bytes'
 				if pubkey_type == 'password': # skip WIF creation and pre-processing for passwds
-					me = str.__new__(cls,s.hex())
+					me = bytes.__new__(cls,s)
 				else:
 					assert compressed is not None, "'compressed' arg missing"
 					assert type(compressed) == bool,(
 						f"'compressed' must be of type bool, not {type(compressed).__name__}" )
-					me = str.__new__(cls,proto.preprocess_key(s,pubkey_type).hex())
-					me.wif = WifKey(proto,proto.hex2wif(me,pubkey_type,compressed))
+					me = bytes.__new__( cls, proto.preprocess_key(s,pubkey_type) )
+					me.wif = WifKey( proto, proto.bytes2wif(me,pubkey_type,compressed) )
 					me.compressed = compressed
 				me.pubkey_type = pubkey_type
-				me.orig_hex = s.hex() # save the non-preprocessed key
+				me.orig_bytes = s # save the non-preprocessed key
 				me.proto = proto
 				return me
 			except Exception as e:

+ 239 - 0
mmgen/keygen.py

@@ -0,0 +1,239 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+keygen.py: Public key generation classes for the MMGen suite
+"""
+
+from collections import namedtuple
+from .key import PubKey,PrivKey
+
+keygen_public_data = namedtuple(
+	'keygen_public_data', [
+		'pubkey',
+		'viewkey_bytes',
+		'pubkey_type',
+		'compressed' ])
+
+class keygen_base:
+
+	def gen_data(self,privkey):
+		assert isinstance(privkey,PrivKey)
+		return keygen_public_data(
+			self.to_pubkey(privkey),
+			self.to_viewkey(privkey),
+			privkey.pubkey_type,
+			privkey.compressed )
+
+	def to_viewkey(self,privkey):
+		return None
+
+class keygen_backend:
+
+	class std:
+		backends = ('libsecp256k1','python-ecdsa')
+
+		class libsecp256k1(keygen_base):
+
+			def __init__(self):
+				from .secp256k1 import priv2pub
+				self.priv2pub = priv2pub
+
+			def to_pubkey(self,privkey):
+				return PubKey(
+					s = self.priv2pub( privkey, int(privkey.compressed) ),
+					compressed = privkey.compressed )
+
+			@classmethod
+			def test_avail(cls,silent=False):
+				try:
+					from .secp256k1 import priv2pub
+					if not priv2pub(bytes.fromhex('deadbeef'*8),1):
+						from .exception import ExtensionModuleError
+						raise ExtensionModuleError('Unable to execute priv2pub() from secp256k1 extension module')
+					return True
+				except Exception as e:
+					if not silent:
+						from .util import ymsg
+						ymsg(str(e))
+					return False
+
+		class python_ecdsa(keygen_base):
+
+			def __init__(self):
+				import ecdsa
+				self.ecdsa = ecdsa
+
+			def to_pubkey(self,privkey):
+				"""
+				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 privnum2pubkey(numpriv,compressed=False):
+					pko = self.ecdsa.SigningKey.from_secret_exponent(numpriv,curve=self.ecdsa.SECP256k1)
+					# pubkey = x (32 bytes) + y (32 bytes) (unsigned big-endian)
+					pubkey = pko.get_verifying_key().to_string()
+					if compressed: # discard Y coord, replace with appropriate version byte
+						# even y: <0, odd y: >0 -- https://bitcointalk.org/index.php?topic=129652.0
+						return (b'\x02',b'\x03')[pubkey[-1] & 1] + pubkey[:32]
+					else:
+						return b'\x04' + pubkey
+
+				return PubKey(
+					s = privnum2pubkey( int.from_bytes(privkey,'big'), compressed=privkey.compressed ),
+					compressed = privkey.compressed )
+
+	class monero:
+		backends = ('nacl','ed25519ll_djbec','ed25519')
+
+		class base(keygen_base):
+
+			def __init__(self):
+
+				from .protocol import CoinProtocol
+				self.proto_cls = CoinProtocol.Monero
+
+				from .util import get_keccak
+				self.keccak_256 = get_keccak()
+
+			def to_viewkey(self,privkey):
+				return self.proto_cls.preprocess_key(
+					self.proto_cls,
+					self.keccak_256(privkey).digest(),
+					None )
+
+		class nacl(base):
+
+			def __init__(self):
+				super().__init__()
+				from nacl.bindings import crypto_scalarmult_ed25519_base_noclamp
+				self.scalarmultbase = crypto_scalarmult_ed25519_base_noclamp
+
+			def to_pubkey(self,privkey):
+				return PubKey(
+					self.scalarmultbase( privkey ) +
+					self.scalarmultbase( self.to_viewkey(privkey) ),
+					compressed = privkey.compressed
+				)
+
+		class ed25519(base):
+
+			def __init__(self):
+				super().__init__()
+				from .ed25519 import edwards,encodepoint,B,scalarmult
+				self.edwards     = edwards
+				self.encodepoint = encodepoint
+				self.B           = B
+				self.scalarmult  = scalarmult
+
+			def scalarmultbase(self,privnum):
+				"""
+				Source and license for scalarmultbase function:
+				  https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py
+				Copyright (c) 2014-2016, The Monero Project
+				All rights reserved.
+				"""
+				if privnum == 0:
+					return [0, 1]
+				Q = self.scalarmult(self.B, privnum//2)
+				Q = self.edwards(Q, Q)
+				if privnum & 1:
+					Q = self.edwards(Q, self.B)
+				return Q
+
+			@staticmethod
+			def rev_bytes2int(in_bytes):
+				return int.from_bytes( in_bytes[::-1], 'big' )
+
+			def to_pubkey(self,privkey):
+				return PubKey(
+					self.encodepoint( self.scalarmultbase( self.rev_bytes2int(privkey) )) +
+					self.encodepoint( self.scalarmultbase( self.rev_bytes2int(self.to_viewkey(privkey)) )),
+					compressed = privkey.compressed
+				)
+
+		class ed25519ll_djbec(ed25519):
+
+			def __init__(self):
+				super().__init__()
+				from .ed25519ll_djbec import scalarmult
+				self.scalarmult = scalarmult
+
+	class zcash_z:
+		backends = ('nacl',)
+
+		class nacl(keygen_base):
+
+			def __init__(self):
+				from nacl.bindings import crypto_scalarmult_base
+				self.crypto_scalarmult_base = crypto_scalarmult_base
+				from .sha2 import Sha256
+				self.Sha256 = Sha256
+
+			def zhash256(self,s,t):
+				s = bytearray(s + bytes(32))
+				s[0] |= 0xc0
+				s[32] = t
+				return self.Sha256(s,preprocess=False).digest()
+
+			def to_pubkey(self,privkey):
+				return PubKey(
+					self.zhash256(privkey,0)
+					+ self.crypto_scalarmult_base(self.zhash256(privkey,1)),
+					compressed = privkey.compressed
+				)
+
+			def to_viewkey(self,privkey):
+				vk = bytearray( self.zhash256(privkey,0) + self.zhash256(privkey,1) )
+				vk[32] &= 0xf8
+				vk[63] &= 0x7f
+				vk[63] |= 0x40
+				return vk
+
+def get_backends(pubkey_type):
+	return getattr(keygen_backend,pubkey_type).backends
+
+def _check_backend(backend,pubkey_type,desc='keygen backend'):
+
+	from .util import is_int,qmsg,die
+
+	assert is_int(backend), f'illegal value for {desc} (must be an integer)'
+
+	backends = get_backends(pubkey_type)
+
+	if not (1 <= int(backend) <= len(backends)):
+		die(1,
+			f'{backend}: {desc} out of range\n' +
+			f'Configured backends: ' +
+			' '.join( f'{n}:{k}' for n,k in enumerate(backends,1) )
+		)
+
+	qmsg(f'Using backend {backends[int(backend)-1]!r} for public key generation')
+
+	return True
+
+def check_backend(proto,backend,addr_type):
+
+	from .addr import MMGenAddrType
+	pubkey_type = MMGenAddrType(proto,addr_type or proto.dfl_mmtype).pubkey_type
+
+	return  _check_backend(
+		backend,
+		pubkey_type,
+		desc = '--keygen-backend parameter' )

+ 8 - 9
mmgen/main_addrgen.py

@@ -56,15 +56,13 @@ opts_data = {
 -c, --print-checksum  Print address list checksum and exit
 -d, --outdir=      d  Output files to directory 'd' instead of working dir
 -e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry
--E, --use-old-ed25519 Use original (and slow) ed25519 module for Monero
-                      address generation instead of ed25519ll_djbec
 -i, --in-fmt=      f  Input is from wallet format 'f' (see FMT CODES below)
 -H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
                       'f' at offset 'o' (comma-separated)
 -O, --old-incog-fmt   Specify old-format incognito input
 -k, --use-internal-keccak-module Force use of the internal keccak module
--K, --key-generator=m Use method 'm' for public key generation
-                      Options: {kgs} (default: {kg})
+-K, --keygen-backend=n Use backend 'n' for public key generation.  Options
+                      for {coin_id}: {kgs}
 -l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option
                       is required only for brainwallet and incognito inputs
                       with non-standard (< {g.dfl_seed_len}-bit) seed lengths
@@ -106,11 +104,11 @@ FMT CODES:
 """
 	},
 	'code': {
-		'options': lambda proto,s: s.format(
+		'options': lambda proto,help_notes,s: s.format(
 			seed_lens=', '.join(map(str,g.seed_lens)),
 			dmat="'{}' or '{}'".format(proto.dfl_mmtype,MMGenAddrType.mmtypes[proto.dfl_mmtype].name),
-			kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
-			kg=g.key_generator,
+			kgs=help_notes('keygen_backends'),
+			coin_id=help_notes('coin_id'),
 			pnm=g.proj_name,
 			what=gen_what,
 			g=g,
@@ -142,8 +140,9 @@ addr_type = MMGenAddrType(
 if len(cmd_args) < 1:
 	opts.usage()
 
-if getattr(opt,'use_old_ed25519',False):
-	msg('Using old (slow) ed25519 module by user request')
+if opt.keygen_backend:
+	from .keygen import check_backend
+	check_backend( proto, opt.keygen_backend, opt.type )
 
 idxs = AddrIdxList(fmt_str=cmd_args.pop())
 

+ 7 - 1
mmgen/main_tool.py

@@ -63,6 +63,8 @@ opts_data = {
 -h, --help             Print this help message
 --, --longhelp         Print help message for long options (common options)
 -k, --use-internal-keccak-module Force use of the internal keccak module
+-K, --keygen-backend=n Use backend 'n' for public key generation.  Options
+                       for {coin_id}: {kgs}
 -p, --hash-preset= p   Use the scrypt hash parameters defined by preset 'p'
                        for password hashing (default: '{g.dfl_hash_preset}')
 -P, --passwd-file= f   Get passphrase from file 'f'.
@@ -84,7 +86,11 @@ Type '{pn} help <command>' for help on a particular command
 """
 	},
 	'code': {
-		'options': lambda s: s.format(g=g),
+		'options': lambda s, help_notes: s.format(
+			kgs=help_notes('keygen_backends'),
+			coin_id=help_notes('coin_id'),
+			g=g,
+		),
 		'notes': lambda s: s.format(
 			ch=make_cmd_help(),
 			pn=g.prog_name)

+ 4 - 5
mmgen/main_txbump.py

@@ -51,9 +51,8 @@ opts_data = {
                        is required only for brainwallet and incognito inputs
                        with non-standard (< {g.dfl_seed_len}-bit) seed lengths.
 -k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
--K, --key-generator= m Use method 'm' for public key generation
-                       Options: {kgs}
-                       (default: {kg})
+-K, --keygen-backend=n Use backend 'n' for public key generation.  Options
+                       for {coin_id}: {kgs}
 -M, --mmgen-keys-from-file=f Provide keys for {pnm} addresses in a key-
                        address file (output of '{pnl}-keygen'). Permits
                        online signing without an {pnm} seed source. The
@@ -87,8 +86,8 @@ column below:
 			pnl=g.proj_name.lower(),
 			fu=help_notes('rel_fee_desc'),
 			fl=help_notes('fee_spec_letters'),
-			kgs=' '.join([f'{n}:{k}' for n,k in enumerate(g.key_generators,1)]),
-			kg=g.key_generator,
+			kgs=help_notes('keygen_backends'),
+			coin_id=help_notes('coin_id'),
 			cu=proto.coin),
 		'notes': lambda help_notes,s: s.format(
 			help_notes('fee'),

+ 4 - 5
mmgen/main_txdo.py

@@ -58,9 +58,8 @@ opts_data = {
                        is required only for brainwallet and incognito inputs
                        with non-standard (< {g.dfl_seed_len}-bit) seed lengths.
 -k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
--K, --key-generator= m Use method 'm' for public key generation
-                       Options: {kgs}
-                       (default: {kg})
+-K, --keygen-backend=n Use backend 'n' for public key generation.  Options
+                       for {coin_id}: {kgs}
 -L, --locktime=      t Lock time (block height or unix seconds) (default: 0)
 -m, --minconf=n        Minimum number of confirmations required to spend
                        outputs (default: 1)
@@ -95,14 +94,14 @@ column below:
 	'code': {
 		'options': lambda proto,help_notes,s: s.format(
 			g=g,pnm=g.proj_name,pnl=g.proj_name.lower(),
-			kgs=' '.join([f'{n}:{k}' for n,k in enumerate(g.key_generators,1)]),
+			kgs=help_notes('keygen_backends'),
+			coin_id=help_notes('coin_id'),
 			fu=help_notes('rel_fee_desc'),
 			fl=help_notes('fee_spec_letters'),
 			ss=g.subseeds,
 			ss_max=SubSeedIdxRange.max_idx,
 			fe_all=fmt_list(g.autoset_opts['fee_estimate_mode'].choices,fmt='no_spc'),
 			fe_dfl=g.autoset_opts['fee_estimate_mode'].choices[0],
-			kg=g.key_generator,
 			cu=proto.coin),
 		'notes': lambda help_notes,s: s.format(
 			help_notes('txcreate'),

+ 5 - 5
mmgen/main_txsign.py

@@ -50,8 +50,8 @@ opts_data = {
                       for password hashing (default: '{g.dfl_hash_preset}')
 -z, --show-hash-presets Show information on available hash presets
 -k, --keys-from-file=f Provide additional keys for non-{pnm} addresses
--K, --key-generator=m Use method 'm' for public key generation
-                      Options: {kgs} (default: {kg})
+-K, --keygen-backend=n Use backend 'n' for public key generation.  Options
+                      for {coin_id}: {kgs}
 -M, --mmgen-keys-from-file=f Provide keys for {pnm} addresses in a key-
                       address file (output of '{pnl}-keygen'). Permits
                       online signing without an {pnm} seed source. The
@@ -77,12 +77,12 @@ column below:
 """
 	},
 	'code': {
-		'options': lambda proto,s: s.format(
+		'options': lambda proto,help_notes,s: s.format(
 			g=g,
 			pnm=g.proj_name,
 			pnl=g.proj_name.lower(),
-			kgs=' '.join([f'{n}:{k}' for n,k in enumerate(g.key_generators,1)]),
-			kg=g.key_generator,
+			kgs=help_notes('keygen_backends'),
+			coin_id=help_notes('coin_id'),
 			ss=g.subseeds,
 			ss_max=SubSeedIdxRange.max_idx,
 			cu=proto.coin),

+ 0 - 4
mmgen/opts.py

@@ -544,10 +544,6 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails
 		opt_is_float(val,desc)
 		ymsg(f'Adjusting transaction vsize by a factor of {float(val):1.2f}')
 
-	def chk_key_generator(key,val,desc):
-		opt_compares(val,'<=',len(g.key_generators),desc)
-		opt_compares(val,'>',0,desc)
-
 	def chk_coin(key,val,desc):
 		from .protocol import CoinProtocol
 		opt_is_in_list(val.lower(),CoinProtocol.coins,'coin')

+ 8 - 9
mmgen/passwdlist.py

@@ -185,28 +185,27 @@ class PasswordList(AddrList):
 					default_yes = True ):
 				die(1,'Exiting at user request')
 
-	def gen_passwd(self,hex_sec):
+	def gen_passwd(self,secbytes):
 		assert self.pw_fmt in self.pw_info
 		if self.pw_fmt == 'hex':
 			# take most significant part
-			return hex_sec[:self.pw_len]
+			return secbytes.hex()[:self.pw_len]
 		elif self.pw_fmt == 'bip39':
 			from .bip39 import bip39
-			pw_len_hex = bip39.nwords2seedlen(self.pw_len,in_hex=True)
+			pw_len_bytes = bip39.nwords2seedlen( self.pw_len, in_bytes=True )
 			# take most significant part
-			return ' '.join(bip39.fromhex(hex_sec[:pw_len_hex],wl_id='bip39'))
+			return ' '.join( bip39.fromhex( secbytes[:pw_len_bytes].hex(), wl_id='bip39' ) )
 		elif self.pw_fmt == 'xmrseed':
-			pw_len_hex = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] * 2
-			# take most significant part
+			pw_len_bytes = baseconv.seedlen_map_rev['xmrseed'][self.pw_len]
 			from .protocol import init_proto
 			bytes_preproc = init_proto('xmr').preprocess_key(
-				bytes.fromhex(hex_sec[:pw_len_hex]),
+				secbytes[:pw_len_bytes], # take most significant part
 				None )
 			return ' '.join( baseconv.frombytes( bytes_preproc, wl_id='xmrseed' ) )
 		else:
 			# take least significant part
-			return baseconv.fromhex(
-				hex_sec,
+			return baseconv.frombytes(
+				secbytes,
 				self.pw_fmt,
 				pad = self.pw_len,
 				tostr = True )[-self.pw_len:]

+ 29 - 30
mmgen/protocol.py

@@ -33,14 +33,11 @@ import mmgen.bech32 as bech32
 parsed_wif = namedtuple('parsed_wif',['sec','pubkey_type','compressed'])
 parsed_addr = namedtuple('parsed_addr',['bytes','fmt'])
 
-def hash160(hexnum): # take hex, return hex - OP_HASH160
-	return hashlib.new('ripemd160',hashlib.sha256(bytes.fromhex(hexnum)).digest()).hexdigest()
+def hash160(in_bytes): # OP_HASH160
+	return hashlib.new('ripemd160',hashlib.sha256(in_bytes).digest()).digest()
 
-def hash256(hexnum): # take hex, return hex - OP_HASH256
-	return hashlib.sha256(hashlib.sha256(bytes.fromhex(hexnum)).digest()).hexdigest()
-
-def hash256bytes(bstr): # bytes in, bytes out - OP_HASH256
-	return hashlib.sha256(hashlib.sha256(bstr).digest()).digest()
+def hash256(in_bytes): # OP_HASH256
+	return hashlib.sha256(hashlib.sha256(in_bytes).digest()).digest()
 
 _b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
 
@@ -57,14 +54,14 @@ def _b58chk_encode(in_bytes):
 		while n:
 			yield _b58a[n % 58]
 			n //= 58
-	return ('1' * lzeroes) + ''.join(do_enc(int.from_bytes(in_bytes+hash256bytes(in_bytes)[:4],'big')))[::-1]
+	return ('1' * lzeroes) + ''.join(do_enc(int.from_bytes(in_bytes+hash256(in_bytes)[:4],'big')))[::-1]
 
 def _b58chk_decode(s):
 	lzeroes = len(s) - len(s.lstrip('1'))
 	res = sum(_b58a.index(ch) * 58**n for n,ch in enumerate(s[::-1]))
 	bl = res.bit_length()
 	out = b'\x00' * lzeroes + res.to_bytes(bl//8 + bool(bl%8),'big')
-	if out[-4:] != hash256bytes(out[:-4])[:4]:
+	if out[-4:] != hash256(out[:-4])[:4]:
 		raise ValueError('_b58chk_decode(): incorrect checksum')
 	return out[:-4]
 
@@ -190,6 +187,7 @@ class CoinProtocol(MMGenObject):
 		"""
 		secp256k1_ge = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
 		privkey_len  = 32
+		pubkey_types = ('std',)
 
 		def preprocess_key(self,sec,pubkey_type):
 			# Key must be non-zero and less than group order of secp256k1 curve
@@ -240,13 +238,12 @@ class CoinProtocol(MMGenObject):
 		start_subsidy   = 50
 		ignore_daemon_version = False
 
-		def hex2wif(self,hexpriv,pubkey_type,compressed): # input is preprocessed hex
-			sec = bytes.fromhex(hexpriv)
-			assert len(sec) == self.privkey_len, f'{len(sec)} bytes: incorrect private key length!'
+		def bytes2wif(self,privbytes,pubkey_type,compressed): # input is preprocessed hex
+			assert len(privbytes) == self.privkey_len, f'{len(privbytes)} bytes: incorrect private key length!'
 			assert pubkey_type in self.wif_ver_num, f'{pubkey_type!r}: invalid pubkey_type'
 			return _b58chk_encode(
 				bytes.fromhex(self.wif_ver_num[pubkey_type])
-				+ sec
+				+ privbytes
 				+ (b'',b'\x01')[bool(compressed)])
 
 		def parse_wif(self,wif):
@@ -288,24 +285,24 @@ class CoinProtocol(MMGenObject):
 			return self.parse_addr_bytes(_b58chk_decode(addr))
 
 		def pubhash2addr(self,pubkey_hash,p2sh):
-			assert len(pubkey_hash) == 40, f'{len(pubkey_hash)}: invalid length for pubkey hash'
-			return _b58chk_encode(bytes.fromhex(
-				self.addr_fmt_to_ver_bytes(('p2pkh','p2sh')[p2sh],return_hex=True) + pubkey_hash
-			))
+			assert len(pubkey_hash) == 20, f'{len(pubkey_hash)}: invalid length for pubkey hash'
+			return _b58chk_encode(
+				self.addr_fmt_to_ver_bytes(('p2pkh','p2sh')[p2sh],return_hex=False) + pubkey_hash
+			)
 
 		# Segwit:
-		def pubhex2redeem_script(self,pubhex):
+		def pubkey2redeem_script(self,pubkey):
 			# https://bitcoincore.org/en/segwit_wallet_dev/
 			# The P2SH redeemScript is always 22 bytes. It starts with a OP_0, followed
 			# by a canonical push of the keyhash (i.e. 0x0014{20-byte keyhash})
-			return self.witness_vernum_hex + '14' + hash160(pubhex)
+			return bytes.fromhex(self.witness_vernum_hex + '14') + hash160(pubkey)
 
-		def pubhex2segwitaddr(self,pubhex):
+		def pubkey2segwitaddr(self,pubkey):
 			return self.pubhash2addr(
-				hash160( self.pubhex2redeem_script(pubhex)), p2sh=True )
+				hash160( self.pubkey2redeem_script(pubkey)), p2sh=True )
 
 		def pubhash2bech32addr(self,pubhash):
-			d = list(bytes.fromhex(pubhash))
+			d = list(pubhash)
 			return bech32.bech32_encode(self.bech32_hrp,[self.witness_vernum]+bech32.convertbits(d,8,5))
 
 	class BitcoinTestnet(Bitcoin):
@@ -329,8 +326,8 @@ class CoinProtocol(MMGenObject):
 		max_tx_fee      = BCHAmt('0.1')
 		ignore_daemon_version = False
 
-		def pubhex2redeem_script(self,pubhex): raise NotImplementedError
-		def pubhex2segwitaddr(self,pubhex):    raise NotImplementedError
+		def pubkey2redeem_script(self,pubkey): raise NotImplementedError
+		def pubkey2segwitaddr(self,pubkey):    raise NotImplementedError
 
 	class BitcoinCashTestnet(BitcoinCash):
 		addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' }
@@ -365,10 +362,10 @@ class CoinProtocol(MMGenObject):
 
 	class DummyWIF:
 
-		def hex2wif(self,hexpriv,pubkey_type,compressed):
+		def bytes2wif(self,privbytes,pubkey_type,compressed):
 			assert pubkey_type == self.pubkey_type, f'{pubkey_type}: invalid pubkey_type for {self.name} protocol!'
 			assert compressed == False, f'{self.name} protocol does not support compressed pubkeys!'
-			return hexpriv
+			return privbytes.hex()
 
 		def parse_wif(self,wif):
 			return parsed_wif(
@@ -427,9 +424,9 @@ class CoinProtocol(MMGenObject):
 			return ''.join(addr[i].upper() if int(h[i],16) > 7 else addr[i] for i in range(len(addr)))
 
 		def pubhash2addr(self,pubkey_hash,p2sh):
-			assert len(pubkey_hash) == 40, f'{len(pubkey_hash)}: invalid length for {self.name} pubkey hash'
+			assert len(pubkey_hash) == 20, f'{len(pubkey_hash)}: invalid length for {self.name} pubkey hash'
 			assert not p2sh, f'{self.name} protocol has no P2SH address format'
-			return pubkey_hash
+			return pubkey_hash.hex()
 
 	class EthereumTestnet(Ethereum):
 		chain_names = ['kovan','goerli','rinkeby']
@@ -452,6 +449,7 @@ class CoinProtocol(MMGenObject):
 		base_coin      = 'ZEC'
 		addr_ver_bytes = { '1cb8': 'p2pkh', '1cbd': 'p2sh', '169a': 'zcash_z', 'a8abd3': 'viewkey' }
 		wif_ver_num    = { 'std': '80', 'zcash_z': 'ab36' }
+		pubkey_types   = ('std','zcash_z')
 		mmtypes        = ('L','C','Z')
 		mmcaps         = ('key','addr')
 		dfl_mmtype     = 'L'
@@ -473,9 +471,9 @@ class CoinProtocol(MMGenObject):
 
 		def pubhash2addr(self,pubkey_hash,p2sh):
 			hash_len = len(pubkey_hash)
-			if hash_len == 40:
+			if hash_len == 20:
 				return super().pubhash2addr(pubkey_hash,p2sh)
-			elif hash_len == 128:
+			elif hash_len == 64:
 				raise NotImplementedError('Zcash z-addresses do not support pubhash2addr()')
 			else:
 				raise ValueError(f'{hash_len}: incorrect pubkey_hash length')
@@ -492,6 +490,7 @@ class CoinProtocol(MMGenObject):
 		addr_ver_bytes = { '12': 'monero', '2a': 'monero_sub' }
 		addr_len       = 68
 		wif_ver_num    = {}
+		pubkey_types   = ('monero',)
 		mmtypes        = ('M',)
 		dfl_mmtype     = 'M'
 		pubkey_type    = 'monero' # required by DummyWIF

+ 23 - 21
mmgen/tool.py

@@ -303,7 +303,7 @@ class MMGenToolCmds(metaclass=MMGenToolCmdMeta):
 		else:
 			return gd(
 				at,
-				KeyGenerator(self.proto,at),
+				KeyGenerator(self.proto,at.pubkey_type),
 				AddrGenerator(self.proto,at),
 			)
 
@@ -364,7 +364,7 @@ class MMGenToolCmdUtil(MMGenToolCmds):
 
 	def hash160(self,hexstr:'sstr'):
 		"compute ripemd160(sha256(data)) (convert hex pubkey to hex addr)"
-		return hash160(hexstr)
+		return hash160(bytes.fromhex(hexstr)).hex()
 
 	def hash256(self,string_or_bytes:str,file_input=False,hex_input=False): # TODO: handle stdin
 		"compute sha256(sha256(data)) (double sha256)"
@@ -458,19 +458,19 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 	def randpair(self):
 		"generate a random private key/address pair"
 		gd = self.init_generators()
-		privhex = PrivKey(
+		privkey = PrivKey(
 			self.proto,
 			get_random(32),
 			pubkey_type = gd.at.pubkey_type,
 			compressed  = gd.at.compressed )
-		addr = gd.ag.to_addr(gd.kg.to_pubhex(privhex))
-		return (privhex.wif,addr)
+		addr = gd.ag.to_addr(gd.kg.gen_data(privkey))
+		return ( privkey.wif, addr )
 
 	def wif2hex(self,wifkey:'sstr'):
 		"convert a private key from WIF to hex format"
 		return PrivKey(
 			self.proto,
-			wif = wifkey )
+			wif = wifkey ).hex()
 
 	def hex2wif(self,privhex:'sstr'):
 		"convert a private key from hex to WIF format"
@@ -484,31 +484,31 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 	def wif2addr(self,wifkey:'sstr'):
 		"generate a coin address from a key in WIF format"
 		gd = self.init_generators()
-		privhex = PrivKey(
+		privkey = PrivKey(
 			self.proto,
 			wif = wifkey )
-		addr = gd.ag.to_addr(gd.kg.to_pubhex(privhex))
+		addr = gd.ag.to_addr(gd.kg.gen_data(privkey))
 		return addr
 
 	def wif2redeem_script(self,wifkey:'sstr'): # new
 		"convert a WIF private key to a Segwit P2SH-P2WPKH redeem script"
 		assert self.mmtype.name == 'segwit','This command is meaningful only for --type=segwit'
 		gd = self.init_generators()
-		privhex = PrivKey(
+		privkey = PrivKey(
 			self.proto,
 			wif = wifkey )
-		return gd.ag.to_segwit_redeem_script(gd.kg.to_pubhex(privhex))
+		return gd.ag.to_segwit_redeem_script(gd.kg.gen_data(privkey))
 
 	def wif2segwit_pair(self,wifkey:'sstr'):
 		"generate both a Segwit P2SH-P2WPKH redeem script and address from WIF"
 		assert self.mmtype.name == 'segwit','This command is meaningful only for --type=segwit'
 		gd = self.init_generators()
-		pubhex = gd.kg.to_pubhex(PrivKey(
+		data = gd.kg.gen_data(PrivKey(
 			self.proto,
 			wif = wifkey ))
 		return (
-			gd.ag.to_segwit_redeem_script(pubhex),
-			gd.ag.to_addr(pubhex) )
+			gd.ag.to_segwit_redeem_script(data),
+			gd.ag.to_addr(data) )
 
 	def privhex2addr(self,privhex:'sstr',output_pubhex=False):
 		"generate coin address from raw private key data in hexadecimal format"
@@ -518,8 +518,8 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 			bytes.fromhex(privhex),
 			compressed  = gd.at.compressed,
 			pubkey_type = gd.at.pubkey_type )
-		ph = gd.kg.to_pubhex(pk)
-		return ph if output_pubhex else gd.ag.to_addr(ph)
+		data = gd.kg.gen_data(pk)
+		return data.pubkey.hex() if output_pubhex else gd.ag.to_addr(data)
 
 	def privhex2pubhex(self,privhex:'sstr'): # new
 		"generate a hex public key from a hex private key"
@@ -527,30 +527,32 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 
 	def pubhex2addr(self,pubkeyhex:'sstr'):
 		"convert a hex pubkey to an address"
+		pubkey = bytes.fromhex(pubkeyhex)
 		if self.mmtype.name == 'segwit':
-			return self.proto.pubhex2segwitaddr(pubkeyhex)
+			return self.proto.pubkey2segwitaddr( pubkey )
 		else:
-			return self.pubhash2addr(hash160(pubkeyhex))
+			return self.pubhash2addr( hash160(pubkey).hex() )
 
 	def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new
 		"convert a hex pubkey to a Segwit P2SH-P2WPKH redeem script"
 		assert self.mmtype.name == 'segwit','This command is meaningful only for --type=segwit'
-		return self.proto.pubhex2redeem_script(pubkeyhex)
+		return self.proto.pubkey2redeem_script( bytes.fromhex(pubkeyhex) ).hex()
 
 	def redeem_script2addr(self,redeem_scripthex:'sstr'): # new
 		"convert a Segwit P2SH-P2WPKH redeem script to an address"
 		assert self.mmtype.name == 'segwit', 'This command is meaningful only for --type=segwit'
 		assert redeem_scripthex[:4] == '0014', f'{redeem_scripthex!r}: invalid redeem script'
 		assert len(redeem_scripthex) == 44, f'{len(redeem_scripthex)//2} bytes: invalid redeem script length'
-		return self.pubhash2addr(hash160(redeem_scripthex))
+		return self.pubhash2addr( hash160(bytes.fromhex(redeem_scripthex)).hex() )
 
 	def pubhash2addr(self,pubhashhex:'sstr'):
 		"convert public key hash to address"
+		pubhash = bytes.fromhex(pubhashhex)
 		if self.mmtype.name == 'bech32':
-			return self.proto.pubhash2bech32addr(pubhashhex)
+			return self.proto.pubhash2bech32addr( pubhash )
 		else:
 			gd = self.init_generators('addrtype_only')
-			return self.proto.pubhash2addr(pubhashhex,gd.at.addr_fmt=='p2sh')
+			return self.proto.pubhash2addr( pubhash, gd.at.addr_fmt=='p2sh' )
 
 	def addr2pubhash(self,addr:'sstr'):
 		"convert coin address to public key hash"

+ 4 - 4
mmgen/tx.py

@@ -96,11 +96,11 @@ def addr2scriptPubKey(proto,addr):
 
 def scriptPubKey2addr(proto,s):
 	if len(s) == 50 and s[:6] == '76a914' and s[-4:] == '88ac':
-		return proto.pubhash2addr(s[6:-4],p2sh=False),'p2pkh'
+		return proto.pubhash2addr(bytes.fromhex(s[6:-4]),p2sh=False),'p2pkh'
 	elif len(s) == 46 and s[:4] == 'a914' and s[-2:] == '87':
-		return proto.pubhash2addr(s[4:-2],p2sh=True),'p2sh'
+		return proto.pubhash2addr(bytes.fromhex(s[4:-2]),p2sh=True),'p2sh'
 	elif len(s) == 44 and s[:4] == proto.witness_vernum_hex + '14':
-		return proto.pubhash2bech32addr(s[4:]),'bech32'
+		return proto.pubhash2bech32addr(bytes.fromhex(s[4:])),'bech32'
 	else:
 		raise NotImplementedError(f'Unknown scriptPubKey ({s})')
 
@@ -1260,7 +1260,7 @@ class MMGenTX:
 				e['amount'] = e['amt']
 				del e['amt']
 				if d.mmtype == 'S':
-					e['redeemScript'] = ag.to_segwit_redeem_script(kg.to_pubhex(keydict[d.addr]))
+					e['redeemScript'] = ag.to_segwit_redeem_script(kg.gen_data(keydict[d.addr]))
 				sig_data.append(e)
 
 			msg_r(f'Signing transaction{tx_num_str}...')

+ 22 - 24
test/gentest.py

@@ -155,13 +155,13 @@ class GenTool(object):
 	def run_tool(self,sec):
 		vcoin = 'BTC' if self.proto.coin == 'BCH' else self.proto.coin
 		ret = self.run(sec,vcoin)
-		self.data[sec] = ret._asdict()
+		self.data[sec.hex()] = ret._asdict()
 		return ret
 
 class GenToolEthkey(GenTool):
 	desc = 'ethkey'
 	def run(self,sec,vcoin):
-		o = get_cmd_output(['ethkey','info',sec])
+		o = get_cmd_output(['ethkey','info',sec.hex()])
 		return gtr(o[0].split()[1],o[-1].split()[1],None)
 
 class GenToolKeyconv(GenTool):
@@ -194,10 +194,10 @@ class GenToolPycoin(GenTool):
 			vcoin = ci.external_tests['testnet']['pycoin'][vcoin]
 		network = self.nfnc(vcoin)
 		key = network.keys.private(
-			secret_exponent = int(sec,16),
+			secret_exponent = int(sec.hex(),16),
 			is_compressed = self.addr_type.name != 'legacy' )
 		if key is None:
-			die(1,f'can’t parse {sec}')
+			die(1,f'can’t parse {sec.hex()}')
 		if self.addr_type.name in ('segwit','bech32'):
 			hash160_c = key.hash160(is_compressed=True)
 			if self.addr_type.name == 'segwit':
@@ -221,10 +221,10 @@ class GenToolMoneropy(GenTool):
 		self.mpa = moneropy.account
 
 	def run(self,sec,vcoin):
-		if sec in self.data:
-			return gtr(**self.data[sec])
+		if sec.hex() in self.data:
+			return gtr(**self.data[sec.hex()])
 		else:
-			sk,vk,addr = self.mpa.account_from_spend_key(sec) # VERY slow!
+			sk,vk,addr = self.mpa.account_from_spend_key(sec.hex()) # VERY slow!
 			return gtr(sk,addr,vk)
 
 def find_or_check_tool(proto,addr_type,toolname):
@@ -274,14 +274,14 @@ def do_ab_test(proto,addr_type,kg_b,rounds,backend_num):
 			qmsg_r(f'\rRound {i+1}/{trounds} ')
 			last_t = time.time()
 		sec = PrivKey(proto,in_bytes,compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type)
-		data = kg_a.to_pubhex(sec)
+		data = kg_a.gen_data(sec)
 		ag = AddrGenerator(proto,addr_type)
 		a_addr = ag.to_addr(data)
 		tinfo = (in_bytes,sec,sec.wif,type(kg_a).__name__,type(kg_b).__name__)
 		a_vk = None
 
 		def do_msg():
-			vmsg( fs.format( b=in_bytes.hex(), r=sec, k=sec.wif, v=a_vk, a=a_addr ))
+			vmsg( fs.format( b=in_bytes.hex(), r=sec.hex(), k=sec.wif, v=a_vk, a=a_addr ))
 
 		if isinstance(kg_b,GenTool):
 			def run_tool():
@@ -294,12 +294,12 @@ def do_ab_test(proto,addr_type,kg_b,rounds,backend_num):
 			a_vk = run_tool()
 			do_msg()
 		else:
-			test_equal( 'addresses', a_addr, ag.to_addr(kg_b.to_pubhex(sec)), *tinfo )
+			test_equal( 'addresses', a_addr, ag.to_addr(kg_b.gen_data(sec)), *tinfo )
 			do_msg()
 
 		qmsg_r(f'\rRound {n+1}/{trounds} ')
 
-	kg_a = KeyGenerator(proto,addr_type,backend_num)
+	kg_a = KeyGenerator(proto,addr_type.pubkey_type,backend_num)
 	if type(kg_a) == type(kg_b):
 		rdie(1,'Key generators are the same!')
 
@@ -349,16 +349,13 @@ def do_ab_test(proto,addr_type,kg_b,rounds,backend_num):
 def init_tool(proto,addr_type,toolname):
 	return globals()['GenTool'+capfirst(toolname.replace('-','_'))](proto,addr_type)
 
-def get_backends(proto,foo):
-	return (1,) if isinstance(proto,CoinProtocol.Zcash) else (1,2)
-
 def ab_test(proto,gen_num,rounds,toolname_or_gen2_num):
 
 	addr_type = MMGenAddrType( proto=proto, id_str=opt.type or proto.dfl_mmtype )
 
 	if is_int(toolname_or_gen2_num):
 		assert gen_num != 'all', "'all' must be used only with external tool"
-		tool = KeyGenerator( proto, addr_type, int(toolname_or_gen2_num) )
+		tool = KeyGenerator( proto, addr_type.pubkey_type, int(toolname_or_gen2_num) )
 	else:
 		toolname = find_or_check_tool( proto, addr_type, toolname_or_gen2_num )
 		if toolname == None:
@@ -367,12 +364,12 @@ def ab_test(proto,gen_num,rounds,toolname_or_gen2_num):
 		tool = init_tool( proto, addr_type, toolname )
 
 	if gen_num == 'all': # check all backends against external tool
-		for n in range(len(get_backends(proto,addr_type.pubkey_type))):
+		for n in range(len(get_backends(addr_type.pubkey_type))):
 			do_ab_test( proto, addr_type, tool, rounds, n+1 )
 	else:                # check specific backend against external tool or another backend
-		do_ab_test( proto, addr_type, tool, rounds, int(gen_num) )
+		do_ab_test( proto, addr_type, tool, rounds, gen_num )
 
-def speed_test(proto,addr_type,kg,ag,rounds):
+def speed_test(proto,kg,ag,rounds):
 	qmsg(green('Testing speed of address generator {!r} for coin {}'.format(
 		type(kg).__name__,
 		proto.coin )))
@@ -387,8 +384,8 @@ def speed_test(proto,addr_type,kg,ag,rounds):
 		if time.time() - last_t >= 0.1:
 			qmsg_r(f'\rRound {i+1}/{rounds} ')
 			last_t = time.time()
-		sec = PrivKey( proto, seed+pack('I', i), compressed=addr_type.compressed, pubkey_type=addr_type.pubkey_type )
-		addr = ag.to_addr(kg.to_pubhex(sec))
+		sec = PrivKey( proto, seed+pack('I', i), compressed=ag.compressed, pubkey_type=ag.pubkey_type )
+		addr = ag.to_addr(kg.gen_data(sec))
 		vmsg(f'\nkey:  {sec.wif}\naddr: {addr}\n')
 	qmsg(
 		f'\rRound {i+1}/{rounds} ' +
@@ -415,9 +412,9 @@ def dump_test(proto,kg,ag,filename):
 			b_sec = PrivKey(proto,wif=b_wif)
 		except:
 			die(2,f'\nInvalid {proto.network} WIF address in dump file: {b_wif}')
-		a_addr = ag.to_addr(kg.to_pubhex(b_sec))
+		a_addr = ag.to_addr(kg.gen_data(b_sec))
 		vmsg(f'\nwif: {b_wif}\naddr: {b_addr}\n')
-		tinfo = (b_sec,b_sec,b_wif,type(kg).__name__,filename)
+		tinfo = (b_sec,b_sec.hex(),b_wif,type(kg).__name__,filename)
 		test_equal('addresses',a_addr,b_addr,*tinfo)
 
 	qmsg(green(('\n','')[bool(opt.verbose)] + 'OK'))
@@ -481,10 +478,10 @@ def main():
 		for proto in protos:
 			ab_test( proto, pa.gen_num, pa.rounds, toolname_or_gen2_num=pa.arg )
 	else:
-		kg = KeyGenerator( proto, addr_type, pa.gen_num )
+		kg = KeyGenerator( proto, addr_type.pubkey_type, pa.gen_num )
 		ag = AddrGenerator( proto, addr_type )
 		if pa.test == 'speed':
-			speed_test( proto, addr_type, kg, ag, pa.rounds )
+			speed_test( proto, kg, ag, pa.rounds )
 		elif pa.test == 'dump':
 			dump_test( proto, kg, ag, filename=pa.arg )
 
@@ -499,6 +496,7 @@ from mmgen.protocol import init_proto,init_proto_from_opts,CoinProtocol,init_gen
 from mmgen.altcoin import CoinInfo as ci
 from mmgen.key import PrivKey
 from mmgen.addr import KeyGenerator,AddrGenerator,MMGenAddrType
+from mmgen.keygen import get_backends
 
 sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
 cmd_args = opts.init(opts_data,add_opts=['exact_output'])

+ 3 - 1
test/misc/opts.py

@@ -34,7 +34,9 @@ opts_data = {
 """
 	},
 	'code': {
-		'options': lambda s: s.format(
+		'options': lambda help_notes,s: s.format(
+			kgs=help_notes('keygen_backends'),
+			coin_id=help_notes('coin_id'),
 			g=g,
 		),
 		'notes': lambda s: s.format(nn='a note'),

+ 11 - 6
test/objtest_py_d/ot_btc_mainnet.py

@@ -227,8 +227,13 @@ tests = {
 	},
 	'PubKey': {
 		'arg1': 's',
-		'bad':  ({'s':1,'privkey':False},{'s':'F00BAA12','privkey':False},),
-		'good': ({'s':'deadbeef','privkey':privkey},) # TODO: add real pubkeys
+		'bad':  (
+			{'s':1, 'compressed':True },
+			{'s':'F00BAA12','compressed':False},
+		),
+		'good': ( # TODO: add real pubkeys
+			{'s':bytes.fromhex('deadbeef'),'compressed':True},
+		)
 	},
 	'PrivKey': {
 		'arg1': 'proto',
@@ -246,11 +251,11 @@ tests = {
 		),
 		'good': (
 			{'proto':proto, 'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb',
-			'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'},
+			'ret':bytes.fromhex('e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c')},
 			{'proto':proto, 'wif':'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk',
-			'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'},
-			{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
-			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
+			'ret':bytes.fromhex('08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f')},
+			{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32},
+			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32}
 		)
 	},
 	'AddrListID': { # a rather pointless test, but do it anyway

+ 4 - 4
test/objtest_py_d/ot_btc_testnet.py

@@ -59,11 +59,11 @@ tests = {
 		),
 		'good': (
 			{'proto':proto, 'wif':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6',
-			'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'},
+			'ret':bytes.fromhex('e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c')},
 			{'proto':proto, 'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR',
-			'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'},
-			{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
-			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
+			'ret':bytes.fromhex('08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f')},
+			{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32},
+			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32}
 		),
 	},
 }

+ 4 - 4
test/objtest_py_d/ot_ltc_mainnet.py

@@ -63,11 +63,11 @@ tests = {
 		),
 		'good': (
 			{'proto':proto, 'wif':'6ufJhtQQiRYA3w2QvDuXNXuLgPFp15i3HR1Wp8An2mx1JnhhJAh',
-			'ret':'470a974ffca9fca1299b706b09142077bea3acbab6d6480b87dbba79d5fd279b'},
+			'ret':bytes.fromhex('470a974ffca9fca1299b706b09142077bea3acbab6d6480b87dbba79d5fd279b')},
 			{'proto':proto, 'wif':'T41Fm7J3mtZLKYPMCLVSFARz4QF8nvSDhLAfW97Ds56Zm9hRJgn8',
-			'ret':'1c6feab55a4c3b4ad1823d4ecacd1565c64228c01828cf44fb4db1e2d82c3d56'},
-			{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
-			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
+			'ret':bytes.fromhex('1c6feab55a4c3b4ad1823d4ecacd1565c64228c01828cf44fb4db1e2d82c3d56')},
+			{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32},
+			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32}
 		)
 	},
 }

+ 4 - 4
test/objtest_py_d/ot_ltc_testnet.py

@@ -59,11 +59,11 @@ tests = {
 		),
 		'good': (
 			{'proto':proto, 'wif':'92iqzh6NqiKawyB1ronw66YtEHrU4rxRJ5T4aHniZqvuSVZS21f',
-			'ret':'95b2aa7912550eacdd3844dcc14bee08ce7bc2434ad4858beb136021e945afeb'},
+			'ret':bytes.fromhex('95b2aa7912550eacdd3844dcc14bee08ce7bc2434ad4858beb136021e945afeb')},
 			{'proto':proto, 'wif':'cSaJAXBAm9ooHpVJgoxqjDG3AcareFy29Cz8mhnNTRijjv2HLgta',
-			'ret':'94fa8b90c11fea8fb907c9376b919534b0a75b9a9621edf71a78753544b4101c'},
-			{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
-			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
+			'ret':bytes.fromhex('94fa8b90c11fea8fb907c9376b919534b0a75b9a9621edf71a78753544b4101c')},
+			{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32},
+			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32}
 		)
 	},
 }

+ 41 - 38
test/test-release.sh

@@ -303,34 +303,35 @@ i_alts='Gen-only altcoin'
 s_alts='The following tests will test generation operations for all supported altcoins'
 t_alts="
 	- # speed tests, no verification:
-	- $gentest_py --coin=etc 2 $rounds
-	- $gentest_py --coin=etc --use-internal-keccak-module 2 $rounds_min
-	- $gentest_py --coin=eth 2 $rounds
-	- $gentest_py --coin=eth --use-internal-keccak-module 2 $rounds_min
-	- $gentest_py --coin=xmr 2 $rounds
-	- $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
+	- $gentest_py --coin=etc 1 $rounds
+	- $gentest_py --coin=etc --use-internal-keccak-module 1 $rounds_min
+	- $gentest_py --coin=eth 1 $rounds
+	- $gentest_py --coin=eth --use-internal-keccak-module 1 $rounds_min
+	- $gentest_py --coin=xmr 1 $rounds
+	- $gentest_py --coin=xmr --use-internal-keccak-module 1 $rounds_min
+	- $gentest_py --coin=zec 1 $rounds
+	- $gentest_py --coin=zec --type=zcash_z 1 $rounds_mid
 	- # verification against external libraries and tools:
 	- #   pycoin
-	- $gentest_py --all --type=legacy 2:pycoin $rounds
-	- $gentest_py --all --type=compressed 2:pycoin $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-coins --type=legacy 1:pycoin $rounds
+	- $gentest_py --all-coins --type=compressed 1:pycoin $rounds
+	- $gentest_py --all-coins --type=segwit 1:pycoin $rounds
+	- $gentest_py --all-coins --type=bech32 1:pycoin $rounds
+
+	- $gentest_py --all-coins --type=legacy --testnet=1 1:pycoin $rounds
+	- $gentest_py --all-coins --type=compressed --testnet=1 1:pycoin $rounds
+	- $gentest_py --all-coins --type=segwit --testnet=1 1:pycoin $rounds
+	- $gentest_py --all-coins --type=bech32 --testnet=1 1:pycoin $rounds
 	- #   keyconv
-	- $gentest_py --all --type=legacy 2:keyconv $rounds
-	- $gentest_py --all --type=compressed 2:keyconv $rounds
+	- $gentest_py --all-coins --type=legacy 1:keyconv $rounds
+	- $gentest_py --all-coins --type=compressed 1:keyconv $rounds
 	e #   ethkey
-	e $gentest_py --all 2:ethkey $rounds
+	e $gentest_py --coin=eth 1:ethkey $rounds
+	e $gentest_py --coin=eth --use-internal-keccak-module 2:ethkey $rounds_mid
 	m #   moneropy
-	m $gentest_py --all --coin=xmr 2:moneropy $rounds_min # very slow, be patient!
+	m $gentest_py --coin=xmr all:moneropy $rounds_mid # very slow, please be patient!
 	z #   zcash-mini
-	z $gentest_py --all --coin=zec --type=zcash_z 1:zcash-mini $rounds_mid
+	z $gentest_py --coin=zec --type=zcash_z all:zcash-mini $rounds_mid
 "
 
 [ "$MSYS2" ] && t_alts_skip='m z'  # no moneropy (pysha3), zcash-mini (golang)
@@ -501,23 +502,23 @@ i_gen='Gentest'
 s_gen="The following tests will run '$gentest_py' for BTC and LTC mainnet and testnet"
 t_gen="
 	- # speed tests, no verification:
-	- $gentest_py --coin=btc 2 $rounds
-	- $gentest_py --coin=btc --type=compressed 2 $rounds
-	- $gentest_py --coin=btc --type=segwit 2 $rounds
-	- $gentest_py --coin=btc --type=bech32 2 $rounds
-	- $gentest_py --coin=ltc 2 $rounds
-	- $gentest_py --coin=ltc --type=compressed 2 $rounds
-	- $gentest_py --coin=ltc --type=segwit 2 $rounds
-	- $gentest_py --coin=ltc --type=bech32 2 $rounds
+	- $gentest_py --coin=btc 1 $rounds
+	- $gentest_py --coin=btc --type=compressed 1 $rounds
+	- $gentest_py --coin=btc --type=segwit 1 $rounds
+	- $gentest_py --coin=btc --type=bech32 1 $rounds
+	- $gentest_py --coin=ltc 1 $rounds
+	- $gentest_py --coin=ltc --type=compressed 1 $rounds
+	- $gentest_py --coin=ltc --type=segwit 1 $rounds
+	- $gentest_py --coin=ltc --type=bech32 1 $rounds
 	- # wallet dumps:
-	- $gentest_py 2 $REFDIR/btcwallet.dump
-	- $gentest_py --type=segwit 2 $REFDIR/btcwallet-segwit.dump
-	- $gentest_py --type=bech32 2 $REFDIR/btcwallet-bech32.dump
-	- $gentest_py --testnet=1 2 $REFDIR/btcwallet-testnet.dump
-	- $gentest_py --coin=ltc 2 $REFDIR/litecoin/ltcwallet.dump
-	- $gentest_py --coin=ltc --type=segwit 2 $REFDIR/litecoin/ltcwallet-segwit.dump
-	- $gentest_py --coin=ltc --type=bech32 2 $REFDIR/litecoin/ltcwallet-bech32.dump
-	- $gentest_py --coin=ltc --testnet=1 2 $REFDIR/litecoin/ltcwallet-testnet.dump
+	- $gentest_py --type=compressed 1 $REFDIR/btcwallet.dump
+	- $gentest_py --type=segwit 1 $REFDIR/btcwallet-segwit.dump
+	- $gentest_py --type=bech32 1 $REFDIR/btcwallet-bech32.dump
+	- $gentest_py --type=compressed --testnet=1 1 $REFDIR/btcwallet-testnet.dump
+	- $gentest_py --coin=ltc --type=compressed 1 $REFDIR/litecoin/ltcwallet.dump
+	- $gentest_py --coin=ltc --type=segwit 1 $REFDIR/litecoin/ltcwallet-segwit.dump
+	- $gentest_py --coin=ltc --type=bech32 1 $REFDIR/litecoin/ltcwallet-bech32.dump
+	- $gentest_py --coin=ltc --type=compressed --testnet=1 1 $REFDIR/litecoin/ltcwallet-testnet.dump
 	- # libsecp256k1 vs python-ecdsa:
 	- $gentest_py 1:2 $rounds
 	- $gentest_py --type=segwit 1:2 $rounds
@@ -528,6 +529,8 @@ t_gen="
 	- $gentest_py --coin=ltc --type=segwit 1:2 $rounds
 	- $gentest_py --coin=ltc --testnet=1 1:2 $rounds
 	- $gentest_py --coin=ltc --testnet=1 --type=segwit 1:2 $rounds
+	- # all backends vs pycoin:
+	- $gentest_py all:pycoin $rounds
 "
 f_gen='gentest tests completed'
 

+ 2 - 2
test/test_py_d/ts_main.py

@@ -384,7 +384,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 			rand_coinaddr = AddrGenerator(
 				self.proto,
 				('legacy','compressed')[non_mmgen_input_compressed]
-				).to_addr(KeyGenerator(self.proto,'std').to_pubhex(privkey))
+				).to_addr(KeyGenerator(self.proto,'std').gen_data(privkey))
 			of = joinpath(self.cfgs[non_mmgen_input]['tmpdir'],non_mmgen_fn)
 			write_data_to_file(
 				outfile           = of,
@@ -422,7 +422,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 		t = ('compressed','segwit')['S' in self.proto.mmtypes]
 		from mmgen.addr import KeyGenerator,AddrGenerator
 		rand_coinaddr = AddrGenerator(self.proto,t).to_addr(
-			KeyGenerator(self.proto,'std').to_pubhex(privkey)
+			KeyGenerator(self.proto,'std').gen_data(privkey)
 		)
 
 		# total of two outputs must be < 10 BTC (<1000 LTC)

+ 4 - 4
test/test_py_d/ts_ref_altcoin.py

@@ -53,8 +53,8 @@ class TestSuiteRefAltcoin(TestSuiteRef,TestSuiteBase):
 		('ref_addrfile_gen_zec',  'generate address file (ZEC-T)'),
 		('ref_addrfile_gen_zec_z','generate address file (ZEC-Z)'),
 		('ref_addrfile_gen_xmr',  'generate address file (XMR)'),
-		# we test the old ed25519 library in test-release.sh, so skip this
-#	('ref_addrfile_gen_xmr_old','generate address file (XMR - old (slow) ed25519 library)'),
+		# we test the unoptimized ed25519 mod in unit_tests.py, so skip this
+#		('ref_addrfile_gen_xmr_slow','generate address file (XMR - unoptimized ed25519 module)'),
 
 		('ref_keyaddrfile_gen_eth',  'generate key-address file (ETH)'),
 		('ref_keyaddrfile_gen_etc',  'generate key-address file (ETC)'),
@@ -153,8 +153,8 @@ class TestSuiteRefAltcoin(TestSuiteRef,TestSuiteBase):
 	def ref_addrfile_gen_xmr(self):
 		return self.ref_altcoin_addrgen(coin='XMR',mmtype='monero')
 
-	def ref_addrfile_gen_xmr_old(self):
-		return self.ref_altcoin_addrgen(coin='XMR',mmtype='monero',add_args=['--use-old-ed25519'])
+	def ref_addrfile_gen_xmr_slow(self):
+		return self.ref_altcoin_addrgen(coin='XMR',mmtype='monero',add_args=['--keygen-backend=2'])
 
 	def ref_keyaddrfile_gen_eth(self):
 		return self.ref_altcoin_addrgen(coin='ETH',mmtype='ethereum',gen_what='key')

+ 2 - 2
test/test_py_d/ts_regtest.py

@@ -908,7 +908,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		return self.alice_add_label_badaddr( rt_pw,'Invalid coin address for this chain: ')
 
 	def alice_add_label_badaddr2(self):
-		addr = init_proto(self.proto.coin,network='mainnet').pubhash2addr('00'*20,False) # mainnet zero address
+		addr = init_proto(self.proto.coin,network='mainnet').pubhash2addr(bytes(20),False) # mainnet zero address
 		return self.alice_add_label_badaddr( addr, f'Invalid coin address for this chain: {addr}' )
 
 	def alice_add_label_badaddr3(self):
@@ -916,7 +916,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		return self.alice_add_label_badaddr( addr, f'MMGen address {addr!r} not found in tracking wallet' )
 
 	def alice_add_label_badaddr4(self):
-		addr = self.proto.pubhash2addr('00'*20,False) # regtest (testnet) zero address
+		addr = self.proto.pubhash2addr(bytes(20),False) # regtest (testnet) zero address
 		return self.alice_add_label_badaddr( addr, f'Address {addr!r} not found in tracking wallet' )
 
 	def alice_remove_label1(self):

+ 99 - 0
test/unit_tests_d/ut_gen.py

@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+"""
+test.unit_tests_d.ut_gen: key/address generation unit tests for the MMGen suite
+"""
+
+from mmgen.common import *
+from mmgen.protocol import init_proto
+from mmgen.key import PrivKey
+from mmgen.addr import MMGenAddrType,KeyGenerator,AddrGenerator
+from mmgen.keygen import get_backends
+
+# TODO: add viewkey checks
+vectors = { # from tooltest2
+	'btc': ( (
+		'5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX',
+		'1C5VPtgq9xQ6AcTgMAR3J6GDrs72HC4pS1',
+		'legacy'
+	), (
+		'KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm',
+		'1Kz9fVSUMshzPejpzW9D95kScgA3rY6QxF',
+		'compressed'
+	), (
+		'KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm',
+		'3AhjTiWHhVJAi1s5CfKMcLzYps12x3gZhg',
+		'segwit'
+	), (
+		'KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm',
+		'bc1q6pqnfwwakuuejpm9w52ds342f9d5u36v0qnz7c',
+		'bech32' ),
+	),
+	'eth': ( (
+		'0000000000000000000000000000000000000000000000000000000000000001',
+		'7e5f4552091a69125d5dfcb7b8c2659029395bdf',
+		'ethereum',
+		), ),
+	'xmr': ( (
+		'0000000000000000000000000000000000000000000000000000000000000001',
+		'42nsXK8WbVGTNayQ6Kjw5UdgqbQY5KCCufdxdCgF7NgTfjC69Mna7DJSYyie77hZTQ8H92G2HwgFhgEUYnDzrnLnQdF28r3',
+		'monero',
+		), ),
+	'zec': ( (
+		'SKxny894fJe2rmZjeuoE6GVfNkWoXfPp8337VrLLNWG56FjqVUYR',
+		'zceQDpyNwek7dKqF5ZuFGj7YrNVxh7X1aPkrVxDLVxWSiZAFDEuy5C7XNV8VhyZ3ghTPQ61xjCGiyLT3wqpiN1Yi6mdmaCq',
+		'zcash_z',
+		), ),
+}
+
+def do_test(proto,wif,addr_chk,addr_type,internal_keccak):
+
+	if internal_keccak:
+		opt.use_internal_keccak_module = True
+		add_msg = ' (internal keccak module)'
+	else:
+		add_msg = ''
+
+	at = MMGenAddrType(proto,addr_type)
+	privkey = PrivKey(proto,wif=wif)
+
+	for n,backend in enumerate(get_backends(at.pubkey_type)):
+
+		kg = KeyGenerator(proto,at.pubkey_type,n+1)
+		qmsg(blue(f'  Testing backend {backend!r} for addr type {addr_type!r}{add_msg}'))
+
+		data = kg.gen_data(privkey)
+
+		for k,v in data._asdict().items():
+			if v and k in ('pubkey','viewkey_bytes'):
+				qmsg(f'    {k+":":19} {v.hex()}')
+
+		ag = AddrGenerator(proto,addr_type)
+		addr = ag.to_addr(data)
+		qmsg(f'    addr:               {addr}\n')
+
+		assert addr == addr_chk, f'{addr} != {addr_chk}'
+
+	opt.use_internal_keccak_module = False
+
+def do_tests(coin,internal_keccak=False):
+	proto = init_proto(coin)
+	for wif,addr,addr_type in vectors[coin]:
+		do_test(proto,wif,addr,addr_type,internal_keccak)
+	return True
+
+class unit_tests:
+
+	def btc(self,name,ut):
+		return do_tests('btc')
+
+	def eth(self,name,ut):
+		do_tests('eth')
+		return do_tests('eth',internal_keccak=True)
+
+	def xmr(self,name,ut):
+		if not opt.fast:
+			do_tests('xmr')
+		return do_tests('xmr',internal_keccak=True)
+
+	def zec(self,name,ut):
+		return do_tests('zec')