Browse Source

Remove dependencies on pysha3 and ethereum modules

- pysha3: Monero and Ethereum use the old pre-SHA3 version of the keccak
  hash function, which is not supported by hashlib.sha3.

  The pysha3 package supports the function but is not portable.

  Therefore, use the pure-Python implementation mmgen.keccak as a fallback,
  making the pysha3 package optional.

  Use of mmgen.keccak may be forced with --use-internal-keccak-module

  mmgen.keccak was ported from the Python 2 implementation at
  https://github.com/ctz/keccak

- ethereum (pyethereum): Installation of this package presents numerous
  problems due to poor maintenance and many superfluous dependencies.

  Therefore, use stripped-down and modified local versions of
  ethereum.transactions and ethereum.utils as fallbacks, making the ethereum
  package optional.

  The local pyethereum.utils uses mmgen.keccak as a fallback for pysha3's
  keccak implementation.
MMGen 6 years ago
parent
commit
a7126ede03

+ 50 - 21
mmgen/addr.py

@@ -78,14 +78,27 @@ class AddrGeneratorBech32(AddrGenerator):
 		raise NotImplementedError('Segwit redeem script not supported by this address type')
 
 class AddrGeneratorEthereum(AddrGenerator):
+
+	def __init__(self,addr_type):
+
+		try:
+			assert not g.use_internal_keccak_module
+			from sha3 import keccak_256
+		except:
+			from mmgen.keccak import keccak_256
+		self.keccak_256 = keccak_256
+
+		from mmgen.protocol import hash256
+		self.hash256 = hash256
+
+		return AddrGenerator.__init__(addr_type)
+
 	def to_addr(self,pubhex):
 		assert type(pubhex) == PubKey
-		import sha3
-		return CoinAddr(sha3.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:])
+		return CoinAddr(self.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:])
 
 	def to_wallet_passwd(self,sk_hex):
-		from mmgen.protocol import hash256
-		return WalletPassword(hash256(sk_hex)[:32])
+		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')
@@ -132,6 +145,31 @@ class AddrGeneratorZcashZ(AddrGenerator):
 
 class AddrGeneratorMonero(AddrGenerator):
 
+	def __init__(self,addr_type):
+
+		try:
+			assert not g.use_internal_keccak_module
+			from sha3 import keccak_256
+		except:
+			from mmgen.keccak import keccak_256
+		self.keccak_256 = keccak_256
+
+		from mmgen.protocol import hash256
+		self.hash256 = hash256
+
+		if opt.use_old_ed25519:
+			from mmgen.ed25519 import edwards,encodepoint,B,scalarmult
+		else:
+			from mmgen.ed25519ll_djbec import scalarmult
+			from mmgen.ed25519 import edwards,encodepoint,B
+
+		self.edwards     = edwards
+		self.encodepoint = encodepoint
+		self.scalarmult  = scalarmult
+		self.B           = B
+
+		return AddrGenerator.__init__(addr_type)
+
 	def b58enc(self,addr_bytes):
 		enc = baseconv.fromhex
 		l = len(addr_bytes)
@@ -141,42 +179,33 @@ class AddrGeneratorMonero(AddrGenerator):
 
 	def to_addr(self,sk_hex): # sk_hex instead of pubhex
 
-		if opt.use_old_ed25519:
-			from mmgen.ed25519 import edwards,encodepoint,B,scalarmult
-		else:
-			from mmgen.ed25519ll_djbec import scalarmult
-			from mmgen.ed25519 import edwards,encodepoint,B
-
 		# 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 = scalarmult(B, e//2)
-			Q = edwards(Q, Q)
-			if e & 1: Q = edwards(Q, B)
+			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  = encodepoint(scalarmultbase(hex2int_le(sk_hex)))
-		pvk_str = encodepoint(scalarmultbase(hex2int_le(vk_hex)))
+		pk_str  = self.encodepoint(scalarmultbase(hex2int_le(sk_hex)))
+		pvk_str = self.encodepoint(scalarmultbase(hex2int_le(vk_hex)))
 		addr_p1 = bytes.fromhex(g.proto.addr_ver_num['monero'][0]) + pk_str + pvk_str
 
-		import sha3
-		return CoinAddr(self.b58enc(addr_p1 + sha3.keccak_256(addr_p1).digest()[:4]))
+		return CoinAddr(self.b58enc(addr_p1 + self.keccak_256(addr_p1).digest()[:4]))
 
 	def to_wallet_passwd(self,sk_hex):
-		from mmgen.protocol import hash256
-		return WalletPassword(hash256(sk_hex)[:32])
+		return WalletPassword(self.hash256(sk_hex)[:32])
 
 	def to_viewkey(self,sk_hex):
 		assert len(sk_hex) == 64,'{}: incorrect privkey length'.format(len(sk_hex))
-		import sha3
-		return MoneroViewKey(g.proto.preprocess_key(sha3.keccak_256(bytes.fromhex(sk_hex)).hexdigest(),None))
+		return MoneroViewKey(g.proto.preprocess_key(self.keccak_256(bytes.fromhex(sk_hex)).hexdigest(),None))
 
 	def to_segwit_redeem_script(self,sk_hex):
 		raise NotImplementedError('Monero addresses incompatible with Segwit')

+ 11 - 3
mmgen/altcoins/eth/contract.py

@@ -20,14 +20,19 @@
 altcoins.eth.contract: Ethereum contract and token classes for the MMGen suite
 """
 
-from sha3 import keccak_256
 from decimal import Decimal
 import rlp
 
 from mmgen.globalvars import g
 from mmgen.common import *
 from mmgen.obj import MMGenObject,TokenAddr,CoinTxID,ETHAmt
-from mmgen.util import msg,msg_r,pmsg,pdie
+from mmgen.util import msg,pmsg
+
+try:
+	assert not g.use_internal_keccak_module
+	from sha3 import keccak_256
+except:
+	from mmgen.keccak import keccak_256
 
 def parse_abi(s):
 	return [s[:8]] + [s[8+x*64:8+(x+1)*64] for x in range(len(s[8:])//64)]
@@ -103,7 +108,10 @@ class Token(MMGenObject): # ERC20
 				'data':    bytes.fromhex(data) }
 
 	def txsign(self,tx_in,key,from_addr,chain_id=None):
-		from ethereum.transactions import Transaction
+
+		try: from ethereum.transactions import Transaction
+		except: from mmgen.altcoins.eth.pyethereum.transactions import Transaction
+
 		if chain_id is None:
 			chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in g.rpch.caps]
 			chain_id = int(g.rpch.request(chain_id_method),16)

+ 0 - 0
mmgen/altcoins/eth/pyethereum/__init__.py


+ 215 - 0
mmgen/altcoins/eth/pyethereum/transactions.py

@@ -0,0 +1,215 @@
+#
+# Adapted from: # https://github.com/ethereum/pyethereum/blob/master/ethereum/transactions.py
+#
+import rlp
+from rlp.sedes import big_endian_int,binary
+from mmgen.altcoins.eth.pyethereum.utils import (
+		str_to_bytes,encode_hex,ascii_chr,big_endian_to_int,TT256,mk_contract_address,
+		ecsign,ecrecover_to_pub,normalize_key )
+import mmgen.altcoins.eth.pyethereum.utils as utils
+
+class InvalidTransaction(Exception): pass
+class opcodes(object):
+	GTXCOST = 21000       # TX BASE GAS COST
+	GTXDATAZERO = 4       # TX DATA ZERO BYTE GAS COST
+	GTXDATANONZERO = 68   # TX DATA NON ZERO BYTE GAS COST
+
+# in the yellow paper it is specified that s should be smaller than
+# secpk1n (eq.205)
+secpk1n = 115792089237316195423570985008687907852837564279074904382605163141518161494337
+null_address = b'\xff' * 20
+
+class Transaction(rlp.Serializable):
+
+	"""
+	A transaction is stored as:
+	[nonce, gasprice, startgas, to, value, data, v, r, s]
+
+	nonce is the number of transactions already sent by that account, encoded
+	in binary form (eg.  0 -> '', 7 -> '\x07', 1000 -> '\x03\xd8').
+
+	(v,r,s) is the raw Electrum-style signature of the transaction without the
+	signature made with the private key corresponding to the sending account,
+	with 0 <= v <= 3. From an Electrum-style signature (65 bytes) it is
+	possible to extract the public key, and thereby the address, directly.
+
+	A valid transaction is one where:
+	(i) the signature is well-formed (ie. 0 <= v <= 3, 0 <= r < P, 0 <= s < N,
+		0 <= r < P - N if v >= 2), and
+	(ii) the sending account has enough funds to pay the fee and the value.
+	"""
+
+	fields = [
+		('nonce', big_endian_int),
+		('gasprice', big_endian_int),
+		('startgas', big_endian_int),
+		('to', utils.address),
+		('value', big_endian_int),
+		('data', binary),
+		('v', big_endian_int),
+		('r', big_endian_int),
+		('s', big_endian_int),
+	]
+
+	_sender = None
+
+	def __init__(self, nonce, gasprice, startgas, to, value, data, v=0, r=0, s=0):
+		# self.data = None
+
+		to = utils.normalize_address(to, allow_blank=True)
+
+		super(
+			Transaction,
+			self).__init__(
+			nonce,
+			gasprice,
+			startgas,
+			to,
+			value,
+			data,
+			v,
+			r,
+			s)
+
+		if self.gasprice >= TT256 or self.startgas >= TT256 or \
+				self.value >= TT256 or self.nonce >= TT256:
+			raise InvalidTransaction("Values way too high!")
+
+	@property
+	def sender(self):
+		if not self._sender:
+			# Determine sender
+			if self.r == 0 and self.s == 0:
+				self._sender = null_address
+			else:
+				if self.v in (27, 28):
+					vee = self.v
+					sighash = utils.sha3(rlp.encode(unsigned_tx_from_tx(self), UnsignedTransaction))
+
+				elif self.v >= 37:
+					vee = self.v - self.network_id * 2 - 8
+					assert vee in (27, 28)
+					rlpdata = rlp.encode(rlp.infer_sedes(self).serialize(self)[:-3] + [self.network_id, '', ''])
+					sighash = utils.sha3(rlpdata)
+				else:
+					raise InvalidTransaction("Invalid V value")
+				if self.r >= secpk1n or self.s >= secpk1n or self.r == 0 or self.s == 0:
+					raise InvalidTransaction("Invalid signature values!")
+				pub = ecrecover_to_pub(sighash, vee, self.r, self.s)
+				if pub == b'\x00' * 64:
+					raise InvalidTransaction(
+						"Invalid signature (zero privkey cannot sign)")
+				self._sender = utils.sha3(pub)[-20:]
+		return self._sender
+
+	@property
+	def network_id(self):
+		if self.r == 0 and self.s == 0:
+			return self.v
+		elif self.v in (27, 28):
+			return None
+		else:
+			return ((self.v - 1) // 2) - 17
+
+	@sender.setter
+	def sender(self, value):
+		self._sender = value
+
+	def sign(self, key, network_id=None):
+		"""Sign this transaction with a private key.
+
+		A potentially already existing signature would be overridden.
+		"""
+		if network_id is None:
+			rawhash = utils.sha3(rlp.encode(unsigned_tx_from_tx(self), UnsignedTransaction))
+		else:
+			assert 1 <= network_id < 2**63 - 18
+			rlpdata = rlp.encode(rlp.infer_sedes(self).serialize(self)[:-3] + [network_id, b'', b''])
+			rawhash = utils.sha3(rlpdata)
+
+		key = normalize_key(key)
+
+		v, r, s = ecsign(rawhash, key)
+		if network_id is not None:
+			v += 8 + network_id * 2
+
+		ret = self.copy(
+			v=v, r=r, s=s
+		)
+		ret._sender = utils.privtoaddr(key)
+		return ret
+
+	@property
+	def hash(self):
+		return utils.sha3(rlp.encode(self))
+
+	def to_dict(self):
+		d = {}
+		for name, _ in self.__class__._meta.fields:
+			d[name] = getattr(self, name)
+			if name in ('to', 'data'):
+				d[name] = '0x' + encode_hex(d[name])
+		d['sender'] = '0x' + encode_hex(self.sender)
+		d['hash'] = '0x' + encode_hex(self.hash)
+		return d
+
+	@property
+	def intrinsic_gas_used(self):
+		num_zero_bytes = str_to_bytes(self.data).count(ascii_chr(0))
+		num_non_zero_bytes = len(self.data) - num_zero_bytes
+		return (opcodes.GTXCOST
+				#         + (0 if self.to else opcodes.CREATE[3])
+				+ opcodes.GTXDATAZERO * num_zero_bytes
+				+ opcodes.GTXDATANONZERO * num_non_zero_bytes)
+
+	@property
+	def creates(self):
+		"returns the address of a contract created by this tx"
+		if self.to in (b'', '\0' * 20):
+			return mk_contract_address(self.sender, self.nonce)
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and self.hash == other.hash
+
+	def __lt__(self, other):
+		return isinstance(other, self.__class__) and self.hash < other.hash
+
+	def __hash__(self):
+		return utils.big_endian_to_int(self.hash)
+
+	def __ne__(self, other):
+		return not self.__eq__(other)
+
+	def __repr__(self):
+		return '<Transaction(%s)>' % encode_hex(self.hash)[:4]
+
+	def __structlog__(self):
+		return encode_hex(self.hash)
+
+	# This method should be called for block numbers >= HOMESTEAD_FORK_BLKNUM only.
+	# The >= operator is replaced by > because the integer division N/2 always produces the value
+	# which is by 0.5 less than the real N/2
+	def check_low_s_metropolis(self):
+		if self.s > secpk1n // 2:
+			raise InvalidTransaction("Invalid signature S value!")
+
+	def check_low_s_homestead(self):
+		if self.s > secpk1n // 2 or self.s == 0:
+			raise InvalidTransaction("Invalid signature S value!")
+
+
+class UnsignedTransaction(rlp.Serializable):
+	fields = [
+		(field, sedes) for field, sedes in Transaction._meta.fields
+		if field not in "vrs"
+	]
+
+def unsigned_tx_from_tx(tx):
+	return UnsignedTransaction(
+		nonce=tx.nonce,
+		gasprice=tx.gasprice,
+		startgas=tx.startgas,
+		to=tx.to,
+		value=tx.value,
+		data=tx.data,
+	)

+ 397 - 0
mmgen/altcoins/eth/pyethereum/utils.py

@@ -0,0 +1,397 @@
+#
+# Adapted from: https://github.com/ethereum/pyethereum/blob/master/ethereum/utils.py
+#
+
+from py_ecc.secp256k1 import privtopub, ecdsa_raw_sign, ecdsa_raw_recover
+import rlp
+from rlp.sedes import big_endian_int, BigEndianInt, Binary
+from eth_utils import encode_hex as encode_hex_0x
+from eth_utils import decode_hex, int_to_big_endian, big_endian_to_int
+from rlp.utils import ALL_BYTES
+
+from mmgen.globalvars import g
+try:
+	assert not g.use_internal_keccak_module
+	import sha3 as _sha3
+	def sha3_256(x): return _sha3.keccak_256(x).digest()
+except:
+	from mmgen.keccak import keccak_256
+	def sha3_256(x):
+		return keccak_256(x).digest()
+
+
+class Memoize:
+	def __init__(self, fn):
+		self.fn = fn
+		self.memo = {}
+	def __call__(self, *args):
+		if args not in self.memo:
+			self.memo[args] = self.fn(*args)
+		return self.memo[args]
+
+TT256 = 2 ** 256
+TT256M1 = 2 ** 256 - 1
+TT255 = 2 ** 255
+SECP256K1P = 2**256 - 4294968273
+
+def is_numeric(x): return isinstance(x, int)
+
+def is_string(x): return isinstance(x, bytes)
+
+def to_string(value):
+	if isinstance(value, bytes):
+		return value
+	if isinstance(value, str):
+		return bytes(value, 'utf-8')
+	if isinstance(value, int):
+		return bytes(str(value), 'utf-8')
+
+def int_to_bytes(value):
+	if isinstance(value, bytes):
+		return value
+	return int_to_big_endian(value)
+
+def to_string_for_regexp(value):
+	return str(to_string(value), 'utf-8')
+unicode = str
+
+def bytearray_to_bytestr(value):
+	return bytes(value)
+
+def encode_int32(v):
+	return v.to_bytes(32, byteorder='big')
+
+def bytes_to_int(value):
+	return int.from_bytes(value, byteorder='big')
+
+def str_to_bytes(value):
+	if isinstance(value, bytearray):
+		value = bytes(value)
+	if isinstance(value, bytes):
+		return value
+	return bytes(value, 'utf-8')
+
+def ascii_chr(n):
+	return ALL_BYTES[n]
+
+def encode_hex(n):
+	if isinstance(n, str):
+		return encode_hex(n.encode('ascii'))
+	return encode_hex_0x(n)[2:]
+
+def ecrecover_to_pub(rawhash, v, r, s):
+	result = ecdsa_raw_recover(rawhash, (v, r, s))
+	if result:
+		x, y = result
+		pub = encode_int32(x) + encode_int32(y)
+	else:
+		raise ValueError('Invalid VRS')
+	assert len(pub) == 64
+	return pub
+
+
+def ecsign(rawhash, key):
+	return ecdsa_raw_sign(rawhash, key)
+
+
+def mk_contract_address(sender, nonce):
+	return sha3(rlp.encode([normalize_address(sender), nonce]))[12:]
+
+
+def mk_metropolis_contract_address(sender, initcode):
+	return sha3(normalize_address(sender) + initcode)[12:]
+
+
+def safe_ord(value):
+	if isinstance(value, int):
+		return value
+	else:
+		return ord(value)
+
+# decorator
+
+
+def flatten(li):
+	o = []
+	for l in li:
+		o.extend(l)
+	return o
+
+
+def bytearray_to_int(arr):
+	o = 0
+	for a in arr:
+		o = (o << 8) + a
+	return o
+
+
+def int_to_32bytearray(i):
+	o = [0] * 32
+	for x in range(32):
+		o[31 - x] = i & 0xff
+		i >>= 8
+	return o
+
+# sha3_count = [0]
+
+
+def sha3(seed):
+	return sha3_256(to_string(seed))
+
+
+assert encode_hex(
+	sha3(b'')) == 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'
+
+
+@Memoize
+def privtoaddr(k):
+	k = normalize_key(k)
+	x, y = privtopub(k)
+	return sha3(encode_int32(x) + encode_int32(y))[12:]
+
+
+def normalize_address(x, allow_blank=False):
+	if is_numeric(x):
+		return int_to_addr(x)
+	if allow_blank and x in {'', b''}:
+		return b''
+	if len(x) in (42, 50) and x[:2] in {'0x', b'0x'}:
+		x = x[2:]
+	if len(x) in (40, 48):
+		x = decode_hex(x)
+	if len(x) == 24:
+		assert len(x) == 24 and sha3(x[:20])[:4] == x[-4:]
+		x = x[:20]
+	if len(x) != 20:
+		raise Exception("Invalid address format: %r" % x)
+	return x
+
+
+def normalize_key(key):
+	if is_numeric(key):
+		o = encode_int32(key)
+	elif len(key) == 32:
+		o = key
+	elif len(key) == 64:
+		o = decode_hex(key)
+	elif len(key) == 66 and key[:2] == '0x':
+		o = decode_hex(key[2:])
+	else:
+		raise Exception("Invalid key format: %r" % key)
+	if o == b'\x00' * 32:
+		raise Exception("Zero privkey invalid")
+	return o
+
+
+def zpad(x, l):
+	""" Left zero pad value `x` at least to length `l`.
+
+	>>> zpad('', 1)
+	'\x00'
+	>>> zpad('\xca\xfe', 4)
+	'\x00\x00\xca\xfe'
+	>>> zpad('\xff', 1)
+	'\xff'
+	>>> zpad('\xca\xfe', 2)
+	'\xca\xfe'
+	"""
+	return b'\x00' * max(0, l - len(x)) + x
+
+
+def rzpad(value, total_length):
+	""" Right zero pad value `x` at least to length `l`.
+
+	>>> zpad('', 1)
+	'\x00'
+	>>> zpad('\xca\xfe', 4)
+	'\xca\xfe\x00\x00'
+	>>> zpad('\xff', 1)
+	'\xff'
+	>>> zpad('\xca\xfe', 2)
+	'\xca\xfe'
+	"""
+	return value + b'\x00' * max(0, total_length - len(value))
+
+
+def int_to_addr(x):
+	o = [b''] * 20
+	for i in range(20):
+		o[19 - i] = ascii_chr(x & 0xff)
+		x >>= 8
+	return b''.join(o)
+
+
+def coerce_addr_to_bin(x):
+	if is_numeric(x):
+		return encode_hex(zpad(big_endian_int.serialize(x), 20))
+	elif len(x) == 40 or len(x) == 0:
+		return decode_hex(x)
+	else:
+		return zpad(x, 20)[-20:]
+
+
+def coerce_addr_to_hex(x):
+	if is_numeric(x):
+		return encode_hex(zpad(big_endian_int.serialize(x), 20))
+	elif len(x) == 40 or len(x) == 0:
+		return x
+	else:
+		return encode_hex(zpad(x, 20)[-20:])
+
+
+def coerce_to_int(x):
+	if is_numeric(x):
+		return x
+	elif len(x) == 40:
+		return big_endian_to_int(decode_hex(x))
+	else:
+		return big_endian_to_int(x)
+
+
+def coerce_to_bytes(x):
+	if is_numeric(x):
+		return big_endian_int.serialize(x)
+	elif len(x) == 40:
+		return decode_hex(x)
+	else:
+		return x
+
+
+def parse_int_or_hex(s):
+	if is_numeric(s):
+		return s
+	elif s[:2] in (b'0x', '0x'):
+		s = to_string(s)
+		tail = (b'0' if len(s) % 2 else b'') + s[2:]
+		return big_endian_to_int(decode_hex(tail))
+	else:
+		return int(s)
+
+
+def ceil32(x):
+	return x if x % 32 == 0 else x + 32 - (x % 32)
+
+
+def to_signed(i):
+	return i if i < TT255 else i - TT256
+
+
+def sha3rlp(x):
+	return sha3(rlp.encode(x))
+
+
+# Format encoders/decoders for bin, addr, int
+
+
+def decode_bin(v):
+	"""decodes a bytearray from serialization"""
+	if not is_string(v):
+		raise Exception("Value must be binary, not RLP array")
+	return v
+
+
+def decode_addr(v):
+	"""decodes an address from serialization"""
+	if len(v) not in [0, 20]:
+		raise Exception("Serialized addresses must be empty or 20 bytes long!")
+	return encode_hex(v)
+
+
+def decode_int(v):
+	"""decodes and integer from serialization"""
+	if len(v) > 0 and (v[0] == b'\x00' or v[0] == 0):
+		raise Exception("No leading zero bytes allowed for integers")
+	return big_endian_to_int(v)
+
+
+def decode_int256(v):
+	return big_endian_to_int(v)
+
+
+def encode_bin(v):
+	"""encodes a bytearray into serialization"""
+	return v
+
+
+def encode_root(v):
+	"""encodes a trie root into serialization"""
+	return v
+
+
+def encode_int(v):
+	"""encodes an integer into serialization"""
+	if not is_numeric(v) or v < 0 or v >= TT256:
+		raise Exception("Integer invalid or out of range: %r" % v)
+	return int_to_big_endian(v)
+
+
+def encode_int256(v):
+	return zpad(int_to_big_endian(v), 256)
+
+
+def scan_bin(v):
+	if v[:2] in ('0x', b'0x'):
+		return decode_hex(v[2:])
+	else:
+		return decode_hex(v)
+
+
+def scan_int(v):
+	if v[:2] in ('0x', b'0x'):
+		return big_endian_to_int(decode_hex(v[2:]))
+	else:
+		return int(v)
+
+
+def int_to_hex(x):
+	o = encode_hex(encode_int(x))
+	return '0x' + (o[1:] if (len(o) > 0 and o[0] == b'0') else o)
+
+
+def remove_0x_head(s):
+	return s[2:] if s[:2] in (b'0x', '0x') else s
+
+
+def parse_as_bin(s):
+	return decode_hex(s[2:] if s[:2] == '0x' else s)
+
+
+def parse_as_int(s):
+	return s if is_numeric(s) else int(
+		'0' + s[2:], 16) if s[:2] == '0x' else int(s)
+
+
+def dump_state(trie):
+	res = ''
+	for k, v in list(trie.to_dict().items()):
+		res += '%r:%r\n' % (encode_hex(k), encode_hex(v))
+	return res
+
+
+class Denoms():
+
+	def __init__(self):
+		self.wei = 1
+		self.babbage = 10 ** 3
+		self.ada = 10 ** 3
+		self.kwei = 10 ** 6
+		self.lovelace = 10 ** 6
+		self.mwei = 10 ** 6
+		self.shannon = 10 ** 9
+		self.gwei = 10 ** 9
+		self.szabo = 10 ** 12
+		self.finney = 10 ** 15
+		self.mether = 10 ** 15
+		self.ether = 10 ** 18
+		self.turing = 2 ** 256 - 1
+
+
+denoms = Denoms()
+
+
+address = Binary.fixed_length(20, allow_empty=True)
+int20 = BigEndianInt(20)
+int32 = BigEndianInt(32)
+int256 = BigEndianInt(256)
+hash32 = Binary.fixed_length(32)
+trie_root = Binary.fixed_length(32, allow_empty=True)

+ 7 - 2
mmgen/altcoins/eth/tx.py

@@ -90,7 +90,10 @@ class EthereumMMGenTX(MMGenTX):
 	# hex data if signed, json if unsigned: see create_raw()
 	def check_txfile_hex_data(self):
 		if self.check_sigs():
-			from ethereum.transactions import Transaction
+
+			try: from ethereum.transactions import Transaction
+			except: from mmgen.altcoins.eth.pyethereum.transactions import Transaction
+
 			import rlp
 			etx = rlp.decode(bytes.fromhex(self.hex),Transaction)
 			d = etx.to_dict() # ==> hex values have '0x' prefix, 0 is '0x'
@@ -284,7 +287,9 @@ class EthereumMMGenTX(MMGenTX):
 				'nonce':    d['nonce'],
 				'data':     bytes.fromhex(d['data'])}
 
-		from ethereum.transactions import Transaction
+		try: from ethereum.transactions import Transaction
+		except: from mmgen.altcoins.eth.pyethereum.transactions import Transaction
+
 		etx = Transaction(**d_in).sign(wif,d['chainId'])
 		assert etx.sender.hex() == d['from'],(
 			'Sender address recovered from signature does not match true sender')

+ 14 - 5
mmgen/globalvars.py

@@ -79,8 +79,12 @@ class g(object):
 	testnet              = False
 	regtest              = False
 	accept_defaults      = False
+	use_internal_keccak_module = False
+
 	chain                = None # set by first call to rpc_init()
 	chains               = 'mainnet','testnet','regtest'
+
+	# rpc:
 	rpc_host             = ''
 	rpc_port             = 0
 	rpc_user             = ''
@@ -88,6 +92,7 @@ class g(object):
 	rpc_fail_on_command  = ''
 	rpch                 = None # global RPC handle
 
+	# regtest:
 	bob                  = False
 	alice                = False
 
@@ -117,12 +122,20 @@ class g(object):
 	data_dir_root,data_dir,cfg_file = None,None,None
 	daemon_data_dir = '' # set by user or protocol
 
-	# User opt sets global var:
+	# global var sets user opt:
+	global_sets_opt = ( 'minconf','seed_len','hash_preset','usr_randchars','debug',
+						'quiet','tx_confs','tx_fee_adj','key_generator' )
+
+	# user opt sets global var:
+	opt_sets_global = ( 'use_internal_keccak_module', )
+
+	# 'long' opts - opt sets global var
 	common_opts = (
 		'color','no_license','rpc_host','rpc_port','testnet','rpc_user','rpc_password',
 		'daemon_data_dir','force_256_color','regtest','coin','bob','alice',
 		'accept_defaults','token'
 	)
+	# opts initialized to None by opts.init() if not set by user
 	required_opts = (
 		'quiet','verbose','debug','outdir','echo_passphrase','passwd_file','stdout',
 		'show_hash_presets','label','keep_passphrase','keep_hash_preset','yes',
@@ -179,10 +192,6 @@ class g(object):
 	max_tx_file_size = 100000
 	max_input_size   = 1024 * 1024
 
-	# Global var sets user opt:
-	global_sets_opt = ['minconf','seed_len','hash_preset','usr_randchars','debug',
-						'quiet','tx_confs','tx_fee_adj','key_generator']
-
 	passwd_max_tries = 5
 
 	max_urandchars = 80

+ 310 - 0
mmgen/keccak.py

@@ -0,0 +1,310 @@
+#!/usr/bin/env python3
+#
+# Python 2 code from here: https://github.com/ctz/keccak
+# Ported to python3 by the MMGen Project
+#
+# This is the old, pre-SHA3 version of Keccak used by Ethereum, which is not supported
+# by hashlib.sha3
+
+from math import log
+from operator import xor
+from copy import deepcopy
+from functools import reduce
+
+# The Keccak-f round constants.
+RoundConstants = [
+	0x0000000000000001,   0x0000000000008082,   0x800000000000808A,   0x8000000080008000,
+	0x000000000000808B,   0x0000000080000001,   0x8000000080008081,   0x8000000000008009,
+	0x000000000000008A,   0x0000000000000088,   0x0000000080008009,   0x000000008000000A,
+	0x000000008000808B,   0x800000000000008B,   0x8000000000008089,   0x8000000000008003,
+	0x8000000000008002,   0x8000000000000080,   0x000000000000800A,   0x800000008000000A,
+	0x8000000080008081,   0x8000000000008080,   0x0000000080000001,   0x8000000080008008
+]
+
+RotationConstants = [
+	[  0,  1, 62, 28, 27, ],
+	[ 36, 44,  6, 55, 20, ],
+	[  3, 10, 43, 25, 39, ],
+	[ 41, 45, 15, 21,  8, ],
+	[ 18,  2, 61, 56, 14, ]
+]
+
+Masks = [(1 << i) - 1 for i in range(65)]
+
+def bits2bytes(x):
+	return (int(x) + 7) // 8
+
+def rol(value, left, bits):
+	"""
+	Circularly rotate 'value' to the left,
+	treating it as a quantity of the given size in bits.
+	"""
+	top = value >> (bits - left)
+	bot = (value & Masks[bits - left]) << left
+	return bot | top
+
+def ror(value, right, bits):
+	"""
+	Circularly rotate 'value' to the right,
+	treating it as a quantity of the given size in bits.
+	"""
+	top = value >> right
+	bot = (value & Masks[right]) << (bits - right)
+	return bot | top
+
+def multirate_padding(used_bytes, align_bytes):
+	"""
+	The Keccak padding function.
+	"""
+	padlen = align_bytes - used_bytes
+	if padlen == 0:
+		padlen = align_bytes
+	# note: padding done in 'internal bit ordering', wherein LSB is leftmost
+	if padlen == 1:
+		return [0x81]
+	else:
+		return [0x01] + ([0x00] * (padlen - 2)) + [0x80]
+
+def keccak_f(state):
+	"""
+	This is Keccak-f permutation.  It operates on and
+	mutates the passed-in KeccakState.  It returns nothing.
+	"""
+	def round(A, RC):
+		W, H = state.W, state.H
+		rangeW, rangeH = state.rangeW, state.rangeH
+		lanew = state.lanew
+		zero = state.zero
+
+		# theta
+		C = [reduce(xor, A[x]) for x in rangeW]
+		D = [0] * W
+		for x in rangeW:
+			D[x] = C[(x - 1) % W] ^ rol(C[(x + 1) % W], 1, lanew)
+			for y in rangeH:
+				A[x][y] ^= D[x]
+
+		# rho and pi
+		B = zero()
+		for x in rangeW:
+			for y in rangeH:
+				B[y % W][(2 * x + 3 * y) % H] = rol(A[x][y], RotationConstants[y][x], lanew)
+
+		# chi
+		for x in rangeW:
+			for y in rangeH:
+				A[x][y] = B[x][y] ^ ((~ B[(x + 1) % W][y]) & B[(x + 2) % W][y])
+
+		# iota
+		A[0][0] ^= RC
+
+	l = int(log(state.lanew, 2))
+	nr = 12 + 2 * l
+
+	for ir in range(nr):
+		round(state.s, RoundConstants[ir])
+
+class KeccakState(object):
+	"""
+	A keccak state container.
+
+	The state is stored as a 5x5 table of integers.
+	"""
+	W = 5
+	H = 5
+
+	rangeW = range(W)
+	rangeH = range(H)
+
+	@staticmethod
+	def zero():
+		"""
+		Returns an zero state table.
+		"""
+		return [[0] * KeccakState.W for x in KeccakState.rangeH]
+
+	@staticmethod
+	def format(st):
+		"""
+		Formats the given state as hex, in natural byte order.
+		"""
+		rows = []
+		def fmt(x): return '%016x' % x
+		for y in KeccakState.rangeH:
+			row = []
+			for x in rangeW:
+				row.append(fmt(st[x][y]))
+			rows.append(' '.join(row))
+		return '\n'.join(rows)
+
+	@staticmethod
+	def lane2bytes(s, w):
+		"""
+		Converts the lane s to a sequence of byte values,
+		assuming a lane is w bits.
+		"""
+		return [(s >> b) & 0xff for b in range(0, w, 8)]
+
+	@staticmethod
+	def bytes2lane(bb):
+		"""
+		Converts a sequence of byte values to a lane.
+		"""
+		r = 0
+		for b in reversed(bb):
+			r = r << 8 | b
+		return r
+
+	def __init__(self, bitrate, b):
+		self.bitrate = bitrate
+		self.b = b
+
+		# only byte-aligned
+		assert self.bitrate % 8 == 0
+		self.bitrate_bytes = bits2bytes(self.bitrate)
+
+		assert self.b % 25 == 0
+		self.lanew = self.b // 25
+
+		self.s = KeccakState.zero()
+
+	def __str__(self):
+		return KeccakState.format(self.s)
+
+	def absorb(self, bb):
+		"""
+		Mixes in the given bitrate-length string to the state.
+		"""
+		assert len(bb) == self.bitrate_bytes
+
+		bb += [0] * bits2bytes(self.b - self.bitrate)
+		i = 0
+
+		for y in self.rangeH:
+			for x in self.rangeW:
+				self.s[x][y] ^= KeccakState.bytes2lane(bb[i:i + 8])
+				i += 8
+
+	def squeeze(self):
+		"""
+		Returns the bitrate-length prefix of the state to be output.
+		"""
+		return self.get_bytes()[:self.bitrate_bytes]
+
+	def get_bytes(self):
+		"""
+		Convert whole state to a byte string.
+		"""
+		out = [0] * bits2bytes(self.b)
+		i = 0
+		for y in self.rangeH:
+			for x in self.rangeW:
+					v = KeccakState.lane2bytes(self.s[x][y], self.lanew)
+					out[i:i+8] = v
+					i += 8
+		return out
+
+	def set_bytes(self, bb):
+		"""
+		Set whole state from byte string, which is assumed
+		to be the correct length.
+		"""
+		i = 0
+		for y in self.rangeH:
+			for x in self.rangeW:
+				self.s[x][y] = KeccakState.bytes2lane(bb[i:i+8])
+				i += 8
+
+class KeccakSponge(object):
+	def __init__(self, bitrate, width, padfn, permfn):
+		self.state = KeccakState(bitrate, width)
+		self.padfn = padfn
+		self.permfn = permfn
+		self.buffer = []
+
+	def copy(self):
+		return deepcopy(self)
+
+	def absorb_block(self, bb):
+		assert len(bb) == self.state.bitrate_bytes
+		self.state.absorb(bb)
+		self.permfn(self.state)
+
+	def absorb(self, s):
+		self.buffer += bytes(s)
+
+		while len(self.buffer) >= self.state.bitrate_bytes:
+			self.absorb_block(self.buffer[:self.state.bitrate_bytes])
+			self.buffer = self.buffer[self.state.bitrate_bytes:]
+
+	def absorb_final(self):
+		padded = self.buffer + self.padfn(len(self.buffer), self.state.bitrate_bytes)
+		self.absorb_block(padded)
+		self.buffer = []
+
+	def squeeze_once(self):
+		rc = self.state.squeeze()
+		self.permfn(self.state)
+		return rc
+
+	def squeeze(self, l):
+		Z = self.squeeze_once()
+		while len(Z) < l:
+			Z += self.squeeze_once()
+		return Z[:l]
+
+class KeccakHash(object):
+	"""
+	The Keccak hash function, with a hashlib-compatible interface.
+	"""
+	def __init__(self, bitrate_bits, capacity_bits, output_bits):
+		# our in-absorption sponge. this is never given padding
+		assert bitrate_bits + capacity_bits in (25, 50, 100, 200, 400, 800, 1600)
+		self.sponge = KeccakSponge(bitrate_bits, bitrate_bits + capacity_bits,
+								   multirate_padding,
+								   keccak_f)
+
+		# hashlib interface members
+		assert output_bits % 8 == 0
+		self.digest_size = bits2bytes(output_bits)
+		self.block_size = bits2bytes(bitrate_bits)
+
+	def __repr__(self):
+		inf = (self.sponge.state.bitrate,
+			   self.sponge.state.b - self.sponge.state.bitrate,
+			   self.digest_size * 8)
+		return '<KeccakHash with r=%d, c=%d, image=%d>' % inf
+
+	def copy(self):
+		return deepcopy(self)
+
+	def update(self, s):
+		self.sponge.absorb(s)
+
+	def digest(self):
+		finalised = self.sponge.copy()
+		finalised.absorb_final()
+		digest = finalised.squeeze(self.digest_size)
+		return bytes(digest)
+
+	def hexdigest(self):
+		return self.digest().hex()
+
+	@staticmethod
+	def preset(bitrate_bits, capacity_bits, output_bits):
+		"""
+		Returns a factory function for the given bitrate, sponge capacity and output length.
+		The function accepts an optional initial input, ala hashlib.
+		"""
+		def create(initial_input = None):
+			h = KeccakHash(bitrate_bits, capacity_bits, output_bits)
+			if initial_input is not None:
+				h.update(initial_input)
+			return h
+		return create
+
+# SHA3 parameter presets
+keccak_224 = KeccakHash.preset(1152, 448, 224)
+keccak_256 = KeccakHash.preset(1088, 512, 256)
+keccak_384 = KeccakHash.preset(832, 768, 384)
+keccak_512 = KeccakHash.preset(576, 1024, 512)

+ 2 - 1
mmgen/main_addrgen.py

@@ -35,7 +35,7 @@ if sys.argv[0].split('-')[-1] == 'keygen':
 else:
 	gen_what = 'addresses'
 	gen_desc = 'addresses'
-	opt_filter = 'hbcdeEiHOKlpzPqrStv-'
+	opt_filter = 'hbcdeEiHOkKlpzPqrStv-'
 	note_addrkey = ''
 note_secp256k1 = """
 If available, the secp256k1 library will be used for address generation.
@@ -59,6 +59,7 @@ opts_data = lambda: {
 -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})
 -l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option

+ 1 - 0
mmgen/main_tool.py

@@ -60,6 +60,7 @@ opts_data = lambda: {
 -d, --outdir=       d Specify an alternate directory 'd' for output
 -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
 -p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
                       for password hashing (default: '{g.hash_preset}')
 -P, --passwd-file= f  Get passphrase from file 'f'.

+ 1 - 0
mmgen/main_txsign.py

@@ -35,6 +35,7 @@ opts_data = lambda: {
 -d, --outdir=      d  Specify an alternate directory 'd' for output
 -D, --tx-id           Display transaction ID and exit
 -e, --echo-passphrase Print passphrase to screen when typing it
+-E, --use-internal-keccak-module Force use of the internal keccak module
 -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)

+ 8 - 5
mmgen/opts.py

@@ -228,8 +228,9 @@ def init(opts_f,add_opts=[],opt_filter=None,parse_only=False):
 	usage_txt = opts_data['usage']
 
 	# Transfer uopts into opt, setting program's opts + required opts to None if not set by user
-	for o in tuple([s.rstrip('=') for s in long_opts] + add_opts + skipped_opts) + \
-				g.required_opts + g.common_opts:
+	for o in  ( tuple([s.rstrip('=') for s in long_opts] + add_opts + skipped_opts)
+				+ g.required_opts
+				+ g.common_opts ):
 		setattr(opt,o,uopts[o] if o in uopts else None)
 
 	if opt.version: Die(0,"""
@@ -255,9 +256,11 @@ def init(opts_f,add_opts=[],opt_filter=None,parse_only=False):
 	override_from_env()
 
 	# User opt sets global var - do these here, before opt is set from g.global_sets_opt
-	for k in g.common_opts:
-		val = getattr(opt,k)
-		if val != None: setattr(g,k,set_for_type(val,getattr(g,k),'--'+k))
+	for k in (g.common_opts + g.opt_sets_global):
+		if hasattr(opt,k):
+			val = getattr(opt,k)
+			if val != None:
+				setattr(g,k,set_for_type(val,getattr(g,k),'--'+k))
 
 	if g.regtest: g.testnet = True # These are equivalent for now
 

+ 8 - 2
mmgen/protocol.py

@@ -415,8 +415,14 @@ class MoneroProtocol(DummyWIF,BitcoinProtocolAddrgen):
 		assert len(addr) == cls.addr_width,'Incorrect width'
 
 		ret = b58dec(addr)
-		import sha3
-		chk = sha3.keccak_256(bytes.fromhex(ret)[:-4]).hexdigest()[:8]
+
+		try:
+			assert not g.use_internal_keccak_module
+			from sha3 import keccak_256
+		except:
+			from mmgen.keccak import keccak_256
+
+		chk = keccak_256(bytes.fromhex(ret)[:-4]).hexdigest()[:8]
 		assert chk == ret[-8:],'{}: incorrect checksum.  Correct value: {}'.format(ret[-8:],chk)
 
 		return { 'hex': ret, 'format': 'monero' } if return_dict else True

+ 16 - 11
scripts/test-release.sh

@@ -19,10 +19,10 @@ scrambletest_py='test/scrambletest.py'
 mmgen_tool='cmds/mmgen-tool'
 mmgen_keygen='cmds/mmgen-keygen'
 python='python3'
-rounds=100 rounds_mid=250 rounds_max=500
+rounds=100 rounds_min=20 rounds_mid=250 rounds_max=500
 monero_addrs='3,99,2,22-24,101-104'
 
-dfl_tests='obj unit sha2 alts monero eth autosign btc btc_tn btc_rt bch bch_rt ltc ltc_tn ltc_rt tool tool2 gen'
+dfl_tests='obj unit hash alts monero eth autosign btc btc_tn btc_rt bch bch_rt ltc ltc_tn ltc_rt tool tool2 gen'
 add_tests='autosign_minimal autosign_live'
 
 PROGNAME=$(basename $0)
@@ -47,7 +47,7 @@ do
 		echo   "  AVAILABLE TESTS:"
 		echo   "     obj      - data objects"
 		echo   "     unit     - unit tests"
-		echo   "     sha2     - MMGen sha2 implementation"
+		echo   "     hash     - internal hash function implementations"
 		echo   "     alts     - operations for all supported gen-only altcoins"
 		echo   "     monero   - operations for Monero"
 		echo   "     eth      - operations for Ethereum"
@@ -78,7 +78,7 @@ do
 		gentest_py="$python $gentest_py"
 		mmgen_tool="$python $mmgen_tool"
 		mmgen_keygen="$python $mmgen_keygen" ;&
-	f)  rounds=10 rounds_mid=25 rounds_max=50 monero_addrs='3,23' ;;
+	f)  rounds=10 rounds_min=3 rounds_mid=25 rounds_max=50 monero_addrs='3,23' ;;
 	i)  INSTALL=1 ;;
 	I)  INSTALL_ONLY=1 ;;
 	l)  echo -e "Default tests:\n  $dfl_tests"
@@ -169,12 +169,13 @@ s_unit='Running unit'
 t_unit=("$unit_tests_py")
 f_unit='Unit tests run complete'
 
-i_sha2='MMGen SHA2 implementation'
-s_sha2='Testing SHA2 implementation'
-t_sha2=(
-	"$python test/sha2test.py sha256 $rounds_max"
-	"$python test/sha2test.py sha512 $rounds_max")
-f_sha2='SHA2 test complete'
+i_hash='Internal hash function implementations'
+s_hash='Testing internal hash function implementations'
+t_hash=(
+	"$python test/hashfunc.py sha256 $rounds_max"
+	"$python test/hashfunc.py sha512 $rounds_max"
+	"$python test/hashfunc.py keccak $rounds_max")
+f_hash='Hash function tests complete'
 
 i_alts='Gen-only altcoin'
 s_alts='The following tests will test generation operations for all supported altcoins'
@@ -190,7 +191,11 @@ t_alts=(
 	"$gentest_py --coin=ltc --type=segwit 2 $rounds"
 	"$gentest_py --coin=ltc --type=bech32 2 $rounds"
 	"$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"
 
@@ -228,7 +233,7 @@ s_monero='Testing key-address file generation and wallet creation and sync opera
 s_monero='The monerod (mainnet) daemon must be running for the following tests'
 t_monero=(
 "mmgen-walletgen -q -r0 -p1 -Llabel --outdir $TMPDIR -o words"
-"$mmgen_keygen -q --accept-defaults --outdir $TMPDIR --coin=xmr $TMPDIR/*.mmwords $monero_addrs"
+"$mmgen_keygen -q --accept-defaults --use-internal-keccak-module --outdir $TMPDIR --coin=xmr $TMPDIR/*.mmwords $monero_addrs"
 'cs1=$(mmgen-tool -q --accept-defaults --coin=xmr keyaddrfile_chksum $TMPDIR/*-XMR*.akeys)'
 "$mmgen_keygen -q --use-old-ed25519 --accept-defaults --outdir $TMPDIR --coin=xmr $TMPDIR/*.mmwords $monero_addrs"
 'cs2=$(mmgen-tool -q --accept-defaults --coin=xmr keyaddrfile_chksum $TMPDIR/*-XMR*.akeys)'

+ 4 - 0
setup.py

@@ -128,6 +128,7 @@ setup(
 			'mmgen.exception',
 			'mmgen.filename',
 			'mmgen.globalvars',
+			'mmgen.keccak',
 			'mmgen.license',
 			'mmgen.mn_electrum',
 			'mmgen.mn_tirosh',
@@ -151,6 +152,9 @@ setup(
 			'mmgen.altcoins.eth.obj',
 			'mmgen.altcoins.eth.tx',
 			'mmgen.altcoins.eth.tw',
+			'mmgen.altcoins.eth.pyethereum.__init__',
+			'mmgen.altcoins.eth.pyethereum.transactions',
+			'mmgen.altcoins.eth.pyethereum.utils',
 
 			'mmgen.main',
 			'mmgen.main_addrgen',

+ 2 - 1
test/gentest.py

@@ -37,6 +37,7 @@ opts_data = lambda: {
 	'options': """
 -h, --help       Print this help message
 -a, --all        Test all supported coins for external generator 'ext'
+-k, --use-internal-keccak-module Force use of the internal keccak module
 --, --longhelp   Print help message for long options (common options)
 -q, --quiet      Produce quieter output
 -t, --type=t     Specify address type (valid options: 'compressed','segwit','zcash_z')
@@ -122,7 +123,7 @@ def init_external_prog():
 		addr_type = MMGenAddrType('Z')
 	elif test_support('pyethereum'):
 		try:
-			import ethereum.utils as eth
+			import mmgen.altcoins.eth.pyethereum.utils as eth
 		except:
 			raise ImportError("Unable to import 'ethereum' module. Is pyethereum installed?")
 		ext_sec2addr = pyethereum_sec2addr

+ 37 - 23
test/sha2test.py → test/hashfunc.py

@@ -16,22 +16,21 @@
 # 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/sha2test.py: Test MMGen's SHA256 and SHA512 implementations
+test/hashfunc.py: Test internal implementations of SHA256, SHA512 and Keccak256
 """
 
 import sys,os
-from mmgen.sha2 import Sha256,Sha512
 from mmgen.util import die
 
 assert len(sys.argv) in (2,3),"Test takes 1 or 2 arguments: test name, plus optional rounds count"
 test = sys.argv[1].capitalize()
-assert test in ('Sha256','Sha512'), "Valid choices for test are 'sha256' or 'sha512'"
+assert test in ('Sha256','Sha512','Keccak'), "Valid choices for test are 'sha256','sha512' or 'keccak'"
 random_rounds = int(sys.argv[2]) if len(sys.argv) == 3 else 500
 
 def msg(s): sys.stderr.write(s)
 def green(s): return '\033[32;1m' + s + '\033[0m'
 
-class TestSha2(object):
+class TestHashFunc(object):
 
 	def test_constants(self):
 		msg('Testing generated constants: ')
@@ -45,8 +44,7 @@ class TestSha2(object):
 		msg('OK\n')
 
 	def compare_hashes(self,dlen,data):
-		import hashlib
-		sha2_ref = getattr(hashlib,self.desc)(data).hexdigest()
+		sha2_ref = getattr(self.hashlib,self.desc)(data).hexdigest()
 		ret = self.t_cls(data).hexdigest()
 		if ret != sha2_ref:
 			m ='\nHashes do not match!\nReference {d}: {}\nMMGen {d}:     {}'
@@ -81,9 +79,40 @@ class TestSha2(object):
 			self.compare_hashes(dlen,os.urandom(dlen))
 		msg('OK\n')
 
+class TestKeccak(TestHashFunc):
+	desc = 'keccak_256'
+	def __init__(self):
+		from mmgen.keccak import keccak_256
+		import sha3
+		self.t_cls = keccak_256
+		self.hashlib = sha3
+
+	def test_constants(self): pass
+
+class TestSha2(TestHashFunc):
+
+	def __init__(self):
+		from mmgen.sha2 import Sha256,Sha512
+		import hashlib
+		self.t_cls = { 'sha256':Sha256, 'sha512':Sha512 }[self.desc]
+		self.hashlib = hashlib
+
+class TestSha256(TestSha2):
+	desc = 'sha256'
+	H_ref = (
+		0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 )
+	K_ref = (
+		0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
+		0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
+		0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
+		0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
+		0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
+		0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
+		0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
+		0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 )
+
 class TestSha512(TestSha2):
 	desc = 'sha512'
-	t_cls = Sha512
 	H_ref = (
 		0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1,
 		0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179 )
@@ -105,23 +134,8 @@ class TestSha512(TestSha2):
 		0x113f9804bef90dae, 0x1b710b35131c471b, 0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc,
 		0x431d67c49c100d4c, 0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817 )
 
-class TestSha256(TestSha2):
-	desc = 'sha256'
-	t_cls = Sha256
-	H_ref = (
-		0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 )
-	K_ref = (
-		0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
-		0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
-		0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
-		0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
-		0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
-		0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
-		0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
-		0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 )
-
-msg(green('Testing MMGen implementation of {}\n'.format(test)))
 t = globals()['Test'+test]()
+msg(green('Testing internal implementation of {}\n'.format(t.desc)))
 t.test_constants()
 t.test_ref()
 t.test_random(random_rounds)