Browse Source

modularize coin protocols

- protocols are now in individual modules under `proto`
The MMGen Project 3 years ago
parent
commit
7558c539a1

+ 1 - 1
mmgen/addrgen.py

@@ -20,7 +20,7 @@
 addrgen.py: Address and view key generation classes for the MMGen suite
 """
 
-from .protocol import hash160,_b58chk_encode
+from .proto.btc import hash160,_b58chk_encode
 from .addr import CoinAddr,MMGenAddrType,MoneroViewKey,ZcashViewKey
 
 # decorator for to_addr() and to_viewkey()

+ 3 - 3
mmgen/addrlist.py

@@ -157,9 +157,9 @@ class AddrList(MMGenObject): # Address info for a single seed ID
 		mmtype = mmtype or proto.dfl_mmtype
 		assert mmtype in MMGenAddrType.mmtypes, f'{mmtype}: mmtype not in {MMGenAddrType.mmtypes!r}'
 
-		from .protocol import CoinProtocol
+		from .proto.btc import mainnet
 		self.bitcoin_addrtypes = tuple(
-			MMGenAddrType(CoinProtocol.Bitcoin,key).name for key in CoinProtocol.Bitcoin.mmtypes)
+			MMGenAddrType(mainnet,key).name for key in mainnet.mmtypes )
 
 		if seed and addr_idxs and mmtype: # data from seed + idxs
 			self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
@@ -281,7 +281,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID
 		return out
 
 	def gen_wallet_passwd(self,privbytes):
-		from .protocol import hash256
+		from .proto.btc import hash256
 		return WalletPassword( hash256(privbytes)[:16].hex() )
 
 	def check_format(self,addr):

+ 4 - 3
mmgen/altcoin.py

@@ -556,7 +556,7 @@ class CoinInfo(object):
 			return '1'
 
 		def phash2addr(ver_num,pk_hash):
-			from .protocol import _b58chk_encode
+			from .proto.btc import _b58chk_encode
 			bl = ver_num.bit_length()
 			ver_bytes = int.to_bytes(ver_num,bl//8 + bool(bl%8),'big')
 			return _b58chk_encode(ver_bytes + pk_hash)
@@ -736,6 +736,7 @@ def init_genonly_altcoins(usr_coin=None,testnet=False):
 def create_altcoin_protos(data):
 
 	from .protocol import CoinProtocol
+	from .proto.btc import mainnet
 
 	def make_proto(e,testnet=False):
 
@@ -751,8 +752,8 @@ def create_altcoin_protos(data):
 			CoinProtocol,
 			proto,
 			type(
-				'CoinProtocol.' + proto,
-				(CoinProtocol.Bitcoin,),
+				proto,
+				(mainnet,),
 				{
 					'base_coin': e.symbol,
 					'addr_ver_bytes': dict(

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-13.1.dev011
+13.1.dev012

+ 2 - 2
mmgen/help.py

@@ -158,7 +158,7 @@ one address with no amount on the command line.
 """
 
 		def txsign():
-			from .protocol import CoinProtocol
+			from .proto.btc import mainnet
 			return """
 Transactions may contain both {pnm} or non-{pnm} input addresses.
 
@@ -180,7 +180,7 @@ source.  Therefore, seed files or a key-address file for all {pnm} outputs
 must also be supplied on the command line if the data can't be found in the
 default wallet.
 """.format(
-	wd  = (f'{coind_exec()} wallet dump or ' if isinstance(proto,CoinProtocol.Bitcoin) else ''),
+	wd  = (f'{coind_exec()} wallet dump or ' if isinstance(proto,mainnet) else ''),
 	pnm = g.proj_name,
 	pnu = proto.name,
 	pnl = g.proj_name.lower() )

+ 2 - 2
mmgen/keygen.py

@@ -106,8 +106,8 @@ class keygen_backend:
 
 			def __init__(self):
 
-				from .protocol import CoinProtocol
-				self.proto_cls = CoinProtocol.Monero
+				from .proto.xmr import mainnet
+				self.proto_cls = mainnet
 
 				from .util import get_keccak
 				self.keccak_256 = get_keccak()

+ 0 - 0
mmgen/proto/__init__.py


+ 37 - 0
mmgen/proto/bch.py

@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Bitcoin Cash protocol
+"""
+
+from .btc import mainnet,_finfo
+
+class mainnet(mainnet):
+	is_fork_of      = 'Bitcoin'
+	mmtypes         = ('L','C')
+	sighash_type    = 'ALL|FORKID'
+	forks = [
+		_finfo(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','BTC',False)
+	]
+	caps = ()
+	coin_amt        = 'BCHAmt'
+	max_tx_fee      = '0.1'
+	ignore_daemon_version = False
+
+	def pubkey2redeem_script(self,pubkey): raise NotImplementedError
+	def pubkey2segwitaddr(self,pubkey):    raise NotImplementedError
+
+class testnet(mainnet):
+	addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' }
+	wif_ver_num    = { 'std': 'ef' }
+
+class regtest(testnet):
+	halving_interval = 150

+ 160 - 0
mmgen/proto/btc.py

@@ -0,0 +1,160 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Bitcoin protocol
+"""
+
+from ..protocol import CoinProtocol,parsed_wif,parsed_addr,_finfo,_b58a,_nw
+import hashlib
+
+def hash160(in_bytes): # OP_HASH160
+	return hashlib.new('ripemd160',hashlib.sha256(in_bytes).digest()).digest()
+
+def hash256(in_bytes): # OP_HASH256
+	return hashlib.sha256(hashlib.sha256(in_bytes).digest()).digest()
+
+# From en.bitcoin.it:
+#  The Base58 encoding used is home made, and has some differences.
+#  Especially, leading zeroes are kept as single zeroes when conversion happens.
+# Test: 5JbQQTs3cnoYN9vDYaGY6nhQ1DggVsY4FJNBUfEfpSQqrEp3srk
+# The 'zero address':
+# 1111111111111111111114oLvT2 (pubkeyhash = '\0'*20)
+
+def _b58chk_encode(in_bytes):
+	lzeroes = len(in_bytes) - len(in_bytes.lstrip(b'\x00'))
+	def do_enc(n):
+		while n:
+			yield _b58a[n % 58]
+			n //= 58
+	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:] != hash256(out[:-4])[:4]:
+		raise ValueError('_b58chk_decode(): incorrect checksum')
+	return out[:-4]
+
+class mainnet(CoinProtocol.Secp256k1): # chainparams.cpp
+	"""
+	All Bitcoin code and chain forks inherit from this class
+	"""
+	mod_clsname     = 'Bitcoin'
+	network_names   = _nw('mainnet','testnet','regtest')
+	addr_ver_bytes  = { '00': 'p2pkh', '05': 'p2sh' }
+	addr_len        = 20
+	wif_ver_num     = { 'std': '80' }
+	mmtypes         = ('L','C','S','B')
+	dfl_mmtype      = 'L'
+	coin_amt        = 'BTCAmt'
+	max_tx_fee      = '0.003'
+	sighash_type    = 'ALL'
+	block0          = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
+	forks           = [
+		_finfo(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','BCH',False),
+	]
+	caps            = ('rbf','segwit')
+	mmcaps          = ('key','addr','rpc','tx')
+	base_coin       = 'BTC'
+	base_proto      = 'Bitcoin'
+	# From BIP173: witness version 'n' is stored as 'OP_n'. OP_0 is encoded as 0x00,
+	# but OP_1 through OP_16 are encoded as 0x51 though 0x60 (81 to 96 in decimal).
+	witness_vernum_hex = '00'
+	witness_vernum  = int(witness_vernum_hex,16)
+	bech32_hrp      = 'bc'
+	sign_mode       = 'daemon'
+	avg_bdi         = int(9.7 * 60) # average block discovery interval (historical)
+	halving_interval = 210000
+	max_halvings    = 64
+	start_subsidy   = 50
+	ignore_daemon_version = False
+	max_int         = 0xffffffff
+
+	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])
+			+ privbytes
+			+ (b'',b'\x01')[bool(compressed)])
+
+	def parse_wif(self,wif):
+		key = _b58chk_decode(wif)
+
+		for k,v in self.wif_ver_num.items():
+			v = bytes.fromhex(v)
+			if key[:len(v)] == v:
+				pubkey_type = k
+				key = key[len(v):]
+				break
+		else:
+			raise ValueError('Invalid WIF version number')
+
+		if len(key) == self.privkey_len + 1:
+			assert key[-1] == 0x01, f'{key[-1]!r}: invalid compressed key suffix byte'
+			compressed = True
+		elif len(key) == self.privkey_len:
+			compressed = False
+		else:
+			raise ValueError(f'{len(key)}: invalid key length')
+
+		return parsed_wif(
+			sec         = key[:self.privkey_len],
+			pubkey_type = pubkey_type,
+			compressed  = compressed )
+
+	def parse_addr(self,addr):
+
+		if 'B' in self.mmtypes and addr[:len(self.bech32_hrp)] == self.bech32_hrp:
+			import mmgen.bech32 as bech32
+			ret = bech32.decode(self.bech32_hrp,addr)
+
+			if ret[0] != self.witness_vernum:
+				from ..util import msg
+				msg(f'{ret[0]}: Invalid witness version number')
+				return False
+
+			return parsed_addr( bytes(ret[1]), 'bech32' ) if ret[1] else False
+
+		return self.parse_addr_bytes(_b58chk_decode(addr))
+
+	def pubhash2addr(self,pubkey_hash,p2sh):
+		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 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 bytes.fromhex(self.witness_vernum_hex + '14') + hash160(pubkey)
+
+	def pubkey2segwitaddr(self,pubkey):
+		return self.pubhash2addr(
+			hash160( self.pubkey2redeem_script(pubkey)), p2sh=True )
+
+	def pubhash2bech32addr(self,pubhash):
+		d = list(pubhash)
+		import mmgen.bech32 as bech32
+		return bech32.bech32_encode(self.bech32_hrp,[self.witness_vernum]+bech32.convertbits(d,8,5))
+
+class testnet(mainnet):
+	addr_ver_bytes      = { '6f': 'p2pkh', 'c4': 'p2sh' }
+	wif_ver_num         = { 'std': 'ef' }
+	bech32_hrp          = 'tb'
+
+class regtest(testnet):
+	bech32_hrp          = 'bcrt'
+	halving_interval    = 150

+ 26 - 0
mmgen/proto/etc.py

@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Ethereum Classic protocol
+"""
+
+from .eth import mainnet
+
+class mainnet(mainnet):
+	chain_names = ['classic','ethereum_classic']
+	max_tx_fee  = '0.005'
+	ignore_daemon_version = False
+
+class testnet(mainnet):
+	chain_names = ['morden','morden_testnet','classic-testnet']
+
+class regtest(testnet):
+	chain_names = ['developmentchain']

+ 76 - 0
mmgen/proto/eth.py

@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Ethereum protocol
+"""
+
+from ..protocol import CoinProtocol,_nw,parsed_addr
+from ..util import is_hex_str_lc,Msg
+
+class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Secp256k1):
+
+	network_names = _nw('mainnet','testnet','devnet')
+	addr_len      = 20
+	mmtypes       = ('E',)
+	dfl_mmtype    = 'E'
+	mod_clsname   = 'Ethereum'
+	base_coin     = 'ETH'
+	pubkey_type   = 'std' # required by DummyWIF
+
+	coin_amt      = 'ETHAmt'
+	max_tx_fee    = '0.005'
+	chain_names   = ['ethereum','foundation']
+	sign_mode     = 'standalone'
+	caps          = ('token',)
+	mmcaps        = ('key','addr','rpc','tx')
+	base_proto    = 'Ethereum'
+	avg_bdi       = 15
+	ignore_daemon_version = False
+
+	chain_ids = {
+		1:    'ethereum',         # ethereum mainnet
+		2:    'morden',           # morden testnet (deprecated)
+		3:    'ropsten',          # ropsten testnet
+		4:    'rinkeby',          # rinkeby testnet
+		5:    'goerli',           # goerli testnet
+		42:   'kovan',            # kovan testnet
+		61:   'classic',          # ethereum classic mainnet
+		62:   'morden',           # ethereum classic testnet
+		17:   'developmentchain', # parity dev chain
+		1337: 'developmentchain', # geth dev chain
+	}
+
+	@property
+	def dcoin(self):
+		return self.tokensym or self.coin
+
+	def parse_addr(self,addr):
+		if is_hex_str_lc(addr) and len(addr) == self.addr_len * 2:
+			return parsed_addr( bytes.fromhex(addr), 'ethereum' )
+		if g.debug:
+			Msg(f'Invalid address: {addr}')
+		return False
+
+	@classmethod
+	def checksummed_addr(cls,addr):
+		h = self.keccak_256(addr.encode()).digest().hex()
+		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) == 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.hex()
+
+class testnet(mainnet):
+	chain_names = ['kovan','goerli','rinkeby']
+
+class regtest(testnet):
+	chain_names = ['developmentchain']

+ 39 - 0
mmgen/proto/ltc.py

@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Litecoin protocol
+"""
+
+from .btc import mainnet
+
+class mainnet(mainnet):
+	block0          = '12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2'
+	addr_ver_bytes  = { '30': 'p2pkh', '32': 'p2sh', '05': 'p2sh' } # new p2sh ver 0x32 must come first
+	wif_ver_num     = { 'std': 'b0' }
+	mmtypes         = ('L','C','S','B')
+	coin_amt        = 'LTCAmt'
+	max_tx_fee      = '0.3'
+	base_coin       = 'LTC'
+	forks           = []
+	bech32_hrp      = 'ltc'
+	avg_bdi         = 150
+	halving_interval = 840000
+	ignore_daemon_version = False
+
+class testnet(mainnet):
+	# addr ver nums same as Bitcoin testnet, except for 'p2sh'
+	addr_ver_bytes     = { '6f':'p2pkh', '3a':'p2sh', 'c4':'p2sh' }
+	wif_ver_num        = { 'std': 'ef' } # same as Bitcoin testnet
+	bech32_hrp         = 'tltc'
+
+class regtest(testnet):
+	bech32_hrp         = 'rltc'
+	halving_interval   = 150

+ 62 - 0
mmgen/proto/xmr.py

@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Monero protocol
+"""
+
+from ..protocol import CoinProtocol,_nw
+
+# https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h
+class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Base):
+
+	network_names  = _nw('mainnet','stagenet',None)
+	base_coin      = 'XMR'
+	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
+	avg_bdi        = 120
+	privkey_len    = 32
+	mmcaps         = ('key','addr')
+	ignore_daemon_version = False
+	coin_amt       = 'XMRAmt'
+
+	def preprocess_key(self,sec,pubkey_type): # reduce key
+		from ..ed25519 import l
+		return int.to_bytes(
+			int.from_bytes( sec[::-1], 'big' ) % l,
+			self.privkey_len,
+			'big' )[::-1]
+
+	def parse_addr(self,addr):
+
+		from ..baseconv import baseconv
+
+		def b58dec(addr_str):
+			bc = baseconv('b58')
+			l = len(addr_str)
+			a = b''.join([bc.tobytes( addr_str[i*11:i*11+11], pad=8 ) for i in range(l//11)])
+			b = bc.tobytes( addr_str[-(l%11):], pad=5 )
+			return a + b
+
+		ret = b58dec(addr)
+
+		chk = self.keccak_256(ret[:-4]).digest()[:4]
+
+		assert ret[-4:] == chk, f'{ret[-4:].hex()}: incorrect checksum.  Correct value: {chk.hex()}'
+
+		return self.parse_addr_bytes(ret)
+
+class testnet(mainnet): # use stagenet for testnet
+	addr_ver_bytes = { '18': 'monero', '24': 'monero_sub' } # testnet is ('35','3f')

+ 52 - 0
mmgen/proto/zec.py

@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Zcash protocol
+"""
+
+from .btc import mainnet
+
+class mainnet(mainnet):
+	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'
+	avg_bdi        = 75
+
+	def __init__(self,*args,**kwargs):
+		super().__init__(*args,**kwargs)
+		from ..opts import opt
+		self.coin_id = 'ZEC-Z' if opt.type in ('zcash_z','Z') else 'ZEC-T'
+
+	def get_addr_len(self,addr_fmt):
+		return (20,64)[addr_fmt in ('zcash_z','viewkey')]
+
+	def preprocess_key(self,sec,pubkey_type):
+		if pubkey_type == 'zcash_z': # zero the first four bits
+			return bytes([sec[0] & 0x0f]) + sec[1:]
+		else:
+			return super().preprocess_key(sec,pubkey_type)
+
+	def pubhash2addr(self,pubkey_hash,p2sh):
+		hash_len = len(pubkey_hash)
+		if hash_len == 20:
+			return super().pubhash2addr(pubkey_hash,p2sh)
+		elif hash_len == 64:
+			raise NotImplementedError('Zcash z-addresses do not support pubhash2addr()')
+		else:
+			raise ValueError(f'{hash_len}: incorrect pubkey_hash length')
+
+class testnet(mainnet):
+	wif_ver_num  = { 'std': 'ef', 'zcash_z': 'ac08' }
+	addr_ver_bytes = { '1d25': 'p2pkh', '1cba': 'p2sh', '16b6': 'zcash_z', 'a8ac0c': 'viewkey' }

+ 15 - 352
mmgen/protocol.py

@@ -17,10 +17,9 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-protocol.py: Coin protocol functions, classes and methods
+protocol.py: Coin protocol base classes and initializer
 """
 
-import sys,os,hashlib
 from collections import namedtuple
 
 from .devtools import *
@@ -29,41 +28,11 @@ from .globalvars import g
 parsed_wif = namedtuple('parsed_wif',['sec','pubkey_type','compressed'])
 parsed_addr = namedtuple('parsed_addr',['bytes','fmt'])
 
-def hash160(in_bytes): # OP_HASH160
-	return hashlib.new('ripemd160',hashlib.sha256(in_bytes).digest()).digest()
-
-def hash256(in_bytes): # OP_HASH256
-	return hashlib.sha256(hashlib.sha256(in_bytes).digest()).digest()
-
-_b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
-
-# From en.bitcoin.it:
-#  The Base58 encoding used is home made, and has some differences.
-#  Especially, leading zeroes are kept as single zeroes when conversion happens.
-# Test: 5JbQQTs3cnoYN9vDYaGY6nhQ1DggVsY4FJNBUfEfpSQqrEp3srk
-# The 'zero address':
-# 1111111111111111111114oLvT2 (pubkeyhash = '\0'*20)
-
-def _b58chk_encode(in_bytes):
-	lzeroes = len(in_bytes) - len(in_bytes.lstrip(b'\x00'))
-	def do_enc(n):
-		while n:
-			yield _b58a[n % 58]
-			n //= 58
-	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:] != hash256(out[:-4])[:4]:
-		raise ValueError('_b58chk_decode(): incorrect checksum')
-	return out[:-4]
-
 _finfo = namedtuple('fork_info',['height','hash','name','replayable'])
 _nw = namedtuple('coin_networks',['mainnet','testnet','regtest'])
 
+_b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' # shared by Bitcoin and Monero
+
 class CoinProtocol(MMGenObject):
 
 	proto_info = namedtuple('proto_info',['name','trust_level']) # trust levels: see altcoin.py
@@ -107,7 +76,7 @@ class CoinProtocol(MMGenObject):
 				self.chain_names = [self.network]
 
 			if self.tokensym:
-				assert isinstance(self,CoinProtocol.Ethereum), 'CoinProtocol.Base_chk1'
+				assert self.name.startswith('Ethereum'), 'CoinProtocol.Base_chk1'
 
 			if self.base_coin in ('ETH','XMR'):
 				from .util import get_keccak
@@ -212,168 +181,10 @@ class CoinProtocol(MMGenObject):
 						ymsg(f'Warning: private key is greater than secp256k1 group order!:\n  {hexpriv}')
 					return (pk % self.secp256k1_ge).to_bytes(self.privkey_len,'big')
 
-	class Bitcoin(Secp256k1): # chainparams.cpp
+	class DummyWIF:
 		"""
-		All Bitcoin code and chain forks inherit from this class
+		Ethereum and Monero protocols inherit from this class
 		"""
-		mod_clsname     = 'Bitcoin'
-		network_names   = _nw('mainnet','testnet','regtest')
-		addr_ver_bytes  = { '00': 'p2pkh', '05': 'p2sh' }
-		addr_len        = 20
-		wif_ver_num     = { 'std': '80' }
-		mmtypes         = ('L','C','S','B')
-		dfl_mmtype      = 'L'
-		coin_amt        = 'BTCAmt'
-		max_tx_fee      = '0.003'
-		sighash_type    = 'ALL'
-		block0          = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
-		forks           = [
-			_finfo(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','BCH',False),
-		]
-		caps            = ('rbf','segwit')
-		mmcaps          = ('key','addr','rpc','tx')
-		base_coin       = 'BTC'
-		base_proto      = 'Bitcoin'
-		# From BIP173: witness version 'n' is stored as 'OP_n'. OP_0 is encoded as 0x00,
-		# but OP_1 through OP_16 are encoded as 0x51 though 0x60 (81 to 96 in decimal).
-		witness_vernum_hex = '00'
-		witness_vernum  = int(witness_vernum_hex,16)
-		bech32_hrp      = 'bc'
-		sign_mode       = 'daemon'
-		avg_bdi         = int(9.7 * 60) # average block discovery interval (historical)
-		halving_interval = 210000
-		max_halvings    = 64
-		start_subsidy   = 50
-		ignore_daemon_version = False
-		max_int         = 0xffffffff
-
-		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])
-				+ privbytes
-				+ (b'',b'\x01')[bool(compressed)])
-
-		def parse_wif(self,wif):
-			key = _b58chk_decode(wif)
-
-			for k,v in self.wif_ver_num.items():
-				v = bytes.fromhex(v)
-				if key[:len(v)] == v:
-					pubkey_type = k
-					key = key[len(v):]
-					break
-			else:
-				raise ValueError('Invalid WIF version number')
-
-			if len(key) == self.privkey_len + 1:
-				assert key[-1] == 0x01, f'{key[-1]!r}: invalid compressed key suffix byte'
-				compressed = True
-			elif len(key) == self.privkey_len:
-				compressed = False
-			else:
-				raise ValueError(f'{len(key)}: invalid key length')
-
-			return parsed_wif(
-				sec         = key[:self.privkey_len],
-				pubkey_type = pubkey_type,
-				compressed  = compressed )
-
-		def parse_addr(self,addr):
-
-			if 'B' in self.mmtypes and addr[:len(self.bech32_hrp)] == self.bech32_hrp:
-				import mmgen.bech32 as bech32
-				ret = bech32.decode(self.bech32_hrp,addr)
-
-				if ret[0] != self.witness_vernum:
-					from .util import msg
-					msg(f'{ret[0]}: Invalid witness version number')
-					return False
-
-				return parsed_addr( bytes(ret[1]), 'bech32' ) if ret[1] else False
-
-			return self.parse_addr_bytes(_b58chk_decode(addr))
-
-		def pubhash2addr(self,pubkey_hash,p2sh):
-			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 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 bytes.fromhex(self.witness_vernum_hex + '14') + hash160(pubkey)
-
-		def pubkey2segwitaddr(self,pubkey):
-			return self.pubhash2addr(
-				hash160( self.pubkey2redeem_script(pubkey)), p2sh=True )
-
-		def pubhash2bech32addr(self,pubhash):
-			d = list(pubhash)
-			import mmgen.bech32 as bech32
-			return bech32.bech32_encode(self.bech32_hrp,[self.witness_vernum]+bech32.convertbits(d,8,5))
-
-	class BitcoinTestnet(Bitcoin):
-		addr_ver_bytes      = { '6f': 'p2pkh', 'c4': 'p2sh' }
-		wif_ver_num         = { 'std': 'ef' }
-		bech32_hrp          = 'tb'
-
-	class BitcoinRegtest(BitcoinTestnet):
-		bech32_hrp          = 'bcrt'
-		halving_interval    = 150
-
-	class BitcoinCash(Bitcoin):
-		is_fork_of      = 'Bitcoin'
-		mmtypes         = ('L','C')
-		sighash_type    = 'ALL|FORKID'
-		forks = [
-			_finfo(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','BTC',False)
-		]
-		caps = ()
-		coin_amt        = 'BCHAmt'
-		max_tx_fee      = '0.1'
-		ignore_daemon_version = False
-
-		def pubkey2redeem_script(self,pubkey): raise NotImplementedError
-		def pubkey2segwitaddr(self,pubkey):    raise NotImplementedError
-
-	class BitcoinCashTestnet(BitcoinCash):
-		addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' }
-		wif_ver_num    = { 'std': 'ef' }
-
-	class BitcoinCashRegtest(BitcoinCashTestnet):
-		halving_interval = 150
-
-	class Litecoin(Bitcoin):
-		block0          = '12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2'
-		addr_ver_bytes  = { '30': 'p2pkh', '32': 'p2sh', '05': 'p2sh' } # new p2sh ver 0x32 must come first
-		wif_ver_num     = { 'std': 'b0' }
-		mmtypes         = ('L','C','S','B')
-		coin_amt        = 'LTCAmt'
-		max_tx_fee      = '0.3'
-		base_coin       = 'LTC'
-		forks           = []
-		bech32_hrp      = 'ltc'
-		avg_bdi         = 150
-		halving_interval = 840000
-		ignore_daemon_version = False
-
-	class LitecoinTestnet(Litecoin):
-		# addr ver nums same as Bitcoin testnet, except for 'p2sh'
-		addr_ver_bytes     = { '6f':'p2pkh', '3a':'p2sh', 'c4':'p2sh' }
-		wif_ver_num        = { 'std': 'ef' } # same as Bitcoin testnet
-		bech32_hrp         = 'tltc'
-
-	class LitecoinRegtest(LitecoinTestnet):
-		bech32_hrp         = 'rltc'
-		halving_interval   = 150
-
-	class DummyWIF:
-
 		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!'
@@ -385,163 +196,6 @@ class CoinProtocol(MMGenObject):
 				pubkey_type = self.pubkey_type,
 				compressed  = False )
 
-	class Ethereum(DummyWIF,Secp256k1):
-
-		network_names = _nw('mainnet','testnet','devnet')
-		addr_len      = 20
-		mmtypes       = ('E',)
-		dfl_mmtype    = 'E'
-		mod_clsname   = 'Ethereum'
-		base_coin     = 'ETH'
-		pubkey_type   = 'std' # required by DummyWIF
-
-		coin_amt      = 'ETHAmt'
-		max_tx_fee    = '0.005'
-		chain_names   = ['ethereum','foundation']
-		sign_mode     = 'standalone'
-		caps          = ('token',)
-		mmcaps        = ('key','addr','rpc','tx')
-		base_proto    = 'Ethereum'
-		avg_bdi       = 15
-		ignore_daemon_version = False
-
-		chain_ids = {
-			1:    'ethereum',         # ethereum mainnet
-			2:    'morden',           # morden testnet (deprecated)
-			3:    'ropsten',          # ropsten testnet
-			4:    'rinkeby',          # rinkeby testnet
-			5:    'goerli',           # goerli testnet
-			42:   'kovan',            # kovan testnet
-			61:   'classic',          # ethereum classic mainnet
-			62:   'morden',           # ethereum classic testnet
-			17:   'developmentchain', # parity dev chain
-			1337: 'developmentchain', # geth dev chain
-		}
-
-		@property
-		def dcoin(self):
-			return self.tokensym or self.coin
-
-		def parse_addr(self,addr):
-			from .util import is_hex_str_lc
-			if is_hex_str_lc(addr) and len(addr) == self.addr_len * 2:
-				return parsed_addr( bytes.fromhex(addr), 'ethereum' )
-			if g.debug:
-				from .util import Msg
-				Msg(f'Invalid address: {addr}')
-			return False
-
-		@classmethod
-		def checksummed_addr(cls,addr):
-			h = self.keccak_256(addr.encode()).digest().hex()
-			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) == 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.hex()
-
-	class EthereumTestnet(Ethereum):
-		chain_names = ['kovan','goerli','rinkeby']
-
-	class EthereumRegtest(EthereumTestnet):
-		chain_names = ['developmentchain']
-
-	class EthereumClassic(Ethereum):
-		chain_names = ['classic','ethereum_classic']
-		max_tx_fee  = '0.005'
-		ignore_daemon_version = False
-
-	class EthereumClassicTestnet(EthereumClassic):
-		chain_names = ['morden','morden_testnet','classic-testnet']
-
-	class EthereumClassicRegtest(EthereumClassicTestnet):
-		chain_names = ['developmentchain']
-
-	class Zcash(Bitcoin):
-		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'
-		avg_bdi        = 75
-
-		def __init__(self,*args,**kwargs):
-			super().__init__(*args,**kwargs)
-			from .opts import opt
-			self.coin_id = 'ZEC-Z' if opt.type in ('zcash_z','Z') else 'ZEC-T'
-
-		def get_addr_len(self,addr_fmt):
-			return (20,64)[addr_fmt in ('zcash_z','viewkey')]
-
-		def preprocess_key(self,sec,pubkey_type):
-			if pubkey_type == 'zcash_z': # zero the first four bits
-				return bytes([sec[0] & 0x0f]) + sec[1:]
-			else:
-				return super().preprocess_key(sec,pubkey_type)
-
-		def pubhash2addr(self,pubkey_hash,p2sh):
-			hash_len = len(pubkey_hash)
-			if hash_len == 20:
-				return super().pubhash2addr(pubkey_hash,p2sh)
-			elif hash_len == 64:
-				raise NotImplementedError('Zcash z-addresses do not support pubhash2addr()')
-			else:
-				raise ValueError(f'{hash_len}: incorrect pubkey_hash length')
-
-	class ZcashTestnet(Zcash):
-		wif_ver_num  = { 'std': 'ef', 'zcash_z': 'ac08' }
-		addr_ver_bytes = { '1d25': 'p2pkh', '1cba': 'p2sh', '16b6': 'zcash_z', 'a8ac0c': 'viewkey' }
-
-	# https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h
-	class Monero(DummyWIF,Base):
-
-		network_names  = _nw('mainnet','stagenet',None)
-		base_coin      = 'XMR'
-		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
-		avg_bdi        = 120
-		privkey_len    = 32
-		mmcaps         = ('key','addr')
-		ignore_daemon_version = False
-		coin_amt       = 'XMRAmt'
-
-		def preprocess_key(self,sec,pubkey_type): # reduce key
-			from .ed25519 import l
-			return int.to_bytes(
-				int.from_bytes( sec[::-1], 'big' ) % l,
-				self.privkey_len,
-				'big' )[::-1]
-
-		def parse_addr(self,addr):
-
-			from .baseconv import baseconv
-
-			def b58dec(addr_str):
-				bc = baseconv('b58')
-				l = len(addr_str)
-				a = b''.join([bc.tobytes( addr_str[i*11:i*11+11], pad=8 ) for i in range(l//11)])
-				b = bc.tobytes( addr_str[-(l%11):], pad=5 )
-				return a + b
-
-			ret = b58dec(addr)
-
-			chk = self.keccak_256(ret[:-4]).digest()[:4]
-
-			assert ret[-4:] == chk, f'{ret[-4:].hex()}: incorrect checksum.  Correct value: {chk.hex()}'
-
-			return self.parse_addr_bytes(ret)
-
-	class MoneroTestnet(Monero): # use stagenet for testnet
-		addr_ver_bytes = { '18': 'monero', '24': 'monero_sub' } # testnet is ('35','3f')
-
 def init_proto(coin=None,testnet=False,regtest=False,network=None,network_id=None,tokensym=None,need_amt=False):
 
 	assert type(testnet) == bool, 'init_proto_chk1'
@@ -567,6 +221,14 @@ def init_proto(coin=None,testnet=False,regtest=False,network=None,network_id=Non
 	name = CoinProtocol.coins[coin].name
 	proto_name = name + ('' if network == 'mainnet' else network.capitalize())
 
+	if not hasattr(CoinProtocol,proto_name):
+		import importlib
+		setattr(
+			CoinProtocol,
+			proto_name,
+			getattr(importlib.import_module(f'mmgen.proto.{coin}'),network)
+		)
+
 	return getattr(CoinProtocol,proto_name)(
 		coin      = coin,
 		name      = name,
@@ -622,4 +284,5 @@ def warn_trustlevel(coinsym):
 		return
 
 	if not keypress_confirm(warning,default_yes=True):
+		import sys
 		sys.exit(0)

+ 2 - 2
mmgen/tool/coin.py

@@ -133,7 +133,7 @@ class tool_cmd(tool_cmd_base):
 		if self.mmtype.name == 'segwit':
 			return self.proto.pubkey2segwitaddr( pubkey )
 		else:
-			from ..protocol import hash160
+			from ..proto.btc import hash160
 			return self.pubhash2addr( hash160(pubkey).hex() )
 
 	def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new
@@ -146,7 +146,7 @@ class tool_cmd(tool_cmd_base):
 		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'
-		from ..protocol import hash160
+		from ..proto.btc import hash160
 		return self.pubhash2addr( hash160(bytes.fromhex(redeem_scripthex)).hex() )
 
 	def pubhash2addr(self,pubhashhex:'sstr'):

+ 3 - 3
mmgen/tool/util.py

@@ -78,7 +78,7 @@ class tool_cmd(tool_cmd_base):
 
 	def hash160(self,hexstr:'sstr'):
 		"compute ripemd160(sha256(data)) (convert hex pubkey to hex addr)"
-		from ..protocol import hash160
+		from ..proto.btc import hash160
 		return hash160( bytes.fromhex(hexstr) ).hex()
 
 	def hash256(self,string_or_bytes:str,file_input=False,hex_input=False): # TODO: handle stdin
@@ -143,12 +143,12 @@ class tool_cmd(tool_cmd_base):
 
 	def hextob58chk(self,hexstr:'sstr'):
 		"convert a hexadecimal number to base58-check encoding"
-		from ..protocol import _b58chk_encode
+		from ..proto.btc import _b58chk_encode
 		return _b58chk_encode( bytes.fromhex(hexstr) )
 
 	def b58chktohex(self,b58chk_num:'sstr'):
 		"convert a base58-check encoded number to hexadecimal"
-		from ..protocol import _b58chk_decode
+		from ..proto.btc import _b58chk_decode
 		return _b58chk_decode(b58chk_num).hex()
 
 	def hextob32(self,hexstr:'sstr',pad=0):

+ 2 - 2
mmgen/util.py

@@ -674,8 +674,8 @@ def altcoin_subclass(cls,proto,mod_dir):
 	"""
 	magic module loading and class retrieval
 	"""
-	from .protocol import CoinProtocol
-	if isinstance(proto,CoinProtocol.Bitcoin):
+
+	if proto.base_coin != 'ETH':
 		return cls
 
 	modname = f'mmgen.altcoins.{proto.base_coin.lower()}.{mod_dir}'

+ 1 - 1
mmgen/xmrwallet.py

@@ -26,7 +26,7 @@ from .common import *
 from .objmethods import Hilite,InitErrors
 from .obj import CoinTxID
 from .seed import SeedID
-from .protocol import _b58a,init_proto
+from .protocol import init_proto,_b58a
 from .addr import CoinAddr,AddrIdx
 from .addrlist import KeyAddrList,AddrIdxList
 from .rpc import MoneroRPCClientRaw,MoneroWalletRPCClient,json_encoder

+ 1 - 0
setup.cfg

@@ -42,6 +42,7 @@ install_requires =
 packages =
 	mmgen
 	mmgen.share
+	mmgen.proto
 	mmgen.tool
 	mmgen.altcoins
 	mmgen.altcoins.eth

+ 1 - 1
test/misc/tool_api_test.py

@@ -73,7 +73,7 @@ def run_test():
 	tool.usr_randchars = 0 # setter
 	check_equal(tool.usr_randchars,0)
 
-	check_equal(f'{tool.coin} {tool.proto.cls_name} {tool.addrtype}', 'BTC Bitcoin L' )
+	check_equal(f'{tool.coin} {tool.proto.cls_name} {tool.addrtype}', 'BTC mainnet L' )
 
 	# test vectors from tooltest2.py:
 

+ 1 - 0
test/overlay/__init__.py

@@ -37,6 +37,7 @@ def overlay_setup(repo_root):
 				'mmgen.data',
 				'mmgen.share',
 				'mmgen.tool',
+				'mmgen.proto',
 				'mmgen.altcoins',
 				'mmgen.altcoins.eth',
 				'mmgen.altcoins.eth.pyethereum',