Browse Source

altcoin.py -> altcoin/params.py, test/altcointest.py

The MMGen Project 1 year ago
parent
commit
525b54af8e
8 changed files with 509 additions and 496 deletions
  1. 0 0
      mmgen/altcoin/__init__.py
  2. 36 489
      mmgen/altcoin/params.py
  3. 3 3
      mmgen/protocol.py
  4. 1 1
      mmgen/tool/api.py
  5. 1 0
      setup.cfg
  6. 465 0
      test/altcointest.py
  7. 2 2
      test/gentest.py
  8. 1 1
      test/test-release.d/cfg.sh

+ 0 - 0
mmgen/altcoin/__init__.py


+ 36 - 489
mmgen/altcoin.py → mmgen/altcoin/params.py

@@ -1,23 +1,15 @@
 #!/usr/bin/env python3
 #
-# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
 # Copyright (C)2013-2023 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/>.
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
 
 """
-altcoin.py - Coin constants for Bitcoin-derived altcoins
+altcoin.py - Constants for Bitcoin-derived altcoins
 """
 
 # Sources:
@@ -38,19 +30,9 @@ altcoin.py - Coin constants for Bitcoin-derived altcoins
 import sys
 from collections import namedtuple
 
-from .cfg import gc,Config
-from .util import msg
-
-def test_equal(desc,a,b,*cdata):
-	if type(a) is int:
-		a = hex(a)
-		b = hex(b)
-	(network,coin,_,b_desc,verbose) = cdata
-	if verbose:
-		msg(f'  {desc:20}: {a!r}')
-	if a != b:
-		raise ValueError(
-			f'{desc.capitalize()}s for {coin.upper()} {network} do not match:\n  CoinInfo: {a}\n  {b_desc}: {b}' )
+from ..cfg import gc
+from ..protocol import CoinProtocol
+from ..proto.btc.params import mainnet
 
 ce = namedtuple('CoinInfoEntry',
 	['name','symbol','wif_ver_num','p2pkh_info','p2sh_info','has_segwit','trust_level'])
@@ -274,196 +256,6 @@ class CoinInfo:
 	('POT',    'https://github.com/potcoin/Potcoin/blob/master/src/base58.h'),
 	)
 
-	# Sources (see above) that are in agreement for these coins
-	# No check for segwit, p2sh check skipped if source doesn't support it
-	cross_checks = {
-		'2GIVE':  ['wn'],
-		'42':     ['vg','wn'],
-		'611':    ['wn'],
-		'AC':     ['lb','vg'],
-		'ACOIN':  ['wn'],
-		'ALF':    ['wn'],
-		'ANC':    ['vg','wn'],
-		'APEX':   ['wn'],
-		'ARCO':   ['wn'],
-		'ARG':    ['pc'],
-		'AUR':    ['vg','wn'],
-		'BCH':    ['wn'],
-		'BLK':    ['lb','vg','wn'],
-		'BQC':    ['vg','wn'],
-		'BSTY':   ['wn'],
-		'BTC':    ['lb','vg','wn'],
-		'BTCD':   ['lb','vg','wn'],
-		'BUCKS':  ['wn'],
-		'CASH':   ['wn'],
-		'CBX':    ['wn'],
-		'CCN':    ['lb','vg','wn'],
-		'CDN':    ['lb','vg','wn'],
-		'CHC':    ['wn'],
-		'CLAM':   ['lb','vg'],
-		'CON':    ['vg','wn'],
-		'CPC':    ['wn'],
-		'DASH':   ['lb','pc','vg','wn'],
-		'DCR':    ['pc'],
-		'DFC':    ['pc'],
-		'DGB':    ['lb','vg'],
-		'DGC':    ['lb','vg','wn'],
-		'DOGE':   ['lb','pc','vg','wn'],
-		'DOGED':  ['lb','vg','wn'],
-		'DOPE':   ['lb','vg'],
-		'DVC':    ['vg','wn'],
-		'EFL':    ['lb','vg','wn'],
-		'EMC':    ['vg'],
-		'EMD':    ['wn'],
-		'ESP':    ['wn'],
-		'FAI':    ['pc'],
-		'FC2':    ['wn'],
-		'FIBRE':  ['wn'],
-		'FJC':    ['wn'],
-		'FLO':    ['wn'],
-		'FLT':    ['wn'],
-		'FST':    ['wn'],
-		'FTC':    ['lb','pc','vg','wn'],
-		'GCR':    ['lb','vg'],
-		'GOOD':   ['wn'],
-		'GRC':    ['vg','wn'],
-		'GUN':    ['vg','wn'],
-		'HAM':    ['vg','wn'],
-		'HTML5':  ['wn'],
-		'HYP':    ['wn'],
-		'ICASH':  ['wn'],
-		'INFX':   ['wn'],
-		'IPC':    ['wn'],
-		'JBS':    ['lb','pc','vg','wn'],
-		'JUDGE':  ['wn'],
-		'LANA':   ['wn'],
-		'LAT':    ['wn'],
-		'LDOGE':  ['wn'],
-		'LMC':    ['wn'],
-		'LTC':    ['lb','vg','wn'],
-		'MARS':   ['wn'],
-		'MEC':    ['pc','wn'],
-		'MINT':   ['wn'],
-		'MOBI':   ['wn'],
-		'MONA':   ['lb','vg'],
-		'MOON':   ['wn'],
-		'MUE':    ['lb','vg'],
-		'MXT':    ['wn'],
-		'MYR':    ['pc'],
-		'MYRIAD': ['vg','wn'],
-		'MZC':    ['lb','pc','vg','wn'],
-		'NEOS':   ['lb','vg'],
-		'NEVA':   ['wn'],
-		'NKA':    ['wn'],
-		'NLG':    ['vg','wn'],
-		'NMC':    ['lb','vg'],
-		'NVC':    ['lb','vg','wn'],
-		'OK':     ['lb','vg'],
-		'OMC':    ['vg','wn'],
-		'ONION':  ['vg','wn'],
-		'PART':   ['wn'],
-		'PINK':   ['vg','wn'],
-		'PIVX':   ['wn'],
-		'PKB':    ['lb','vg','wn'],
-		'PND':    ['lb','vg','wn'],
-		'POT':    ['lb','vg','wn'],
-		'PPC':    ['lb','vg','wn'],
-		'PTC':    ['vg','wn'],
-		'PXC':    ['wn'],
-		'QRK':    ['wn'],
-		'RAIN':   ['wn'],
-		'RBT':    ['wn'],
-		'RBY':    ['lb','vg'],
-		'RDD':    ['vg','wn'],
-		'RIC':    ['pc','vg','wn'],
-		'SDC':    ['lb','vg'],
-		'SIB':    ['wn'],
-		'SMLY':   ['wn'],
-		'SONG':   ['wn'],
-		'SPR':    ['vg','wn'],
-		'START':  ['lb','vg'],
-		'SYS':    ['wn'],
-		'TAJ':    ['wn'],
-		'TIT':    ['wn'],
-		'TPC':    ['lb','vg'],
-		'TRC':    ['wn'],
-		'TTC':    ['wn'],
-		'TX':     ['wn'],
-		'UNO':    ['pc','vg','wn'],
-		'VIA':    ['lb','pc','vg','wn'],
-		'VPN':    ['lb','vg'],
-		'VTC':    ['lb','vg','wn'],
-		'WDC':    ['vg','wn'],
-		'WISC':   ['wn'],
-		'WKC':    ['vg','wn'],
-		'WSX':    ['wn'],
-		'XCN':    ['wn'],
-		'XGB':    ['wn'],
-		'XPM':    ['lb','vg','wn'],
-		'XST':    ['wn'],
-		'XVC':    ['wn'],
-		'ZET':    ['wn'],
-		'ZOOM':   ['lb','vg'],
-		'ZRC':    ['lb','vg']
-	}
-
-	@classmethod
-	def verify_leading_symbols(cls,quiet=False,verbose=False):
-
-		for network in ('mainnet','testnet'):
-			for coin in [e.symbol for e in cls.coin_constants[network]]:
-				e = cls.get_entry(coin,network)
-				cdata = (network,coin,e,'Computed value',verbose)
-
-				if not quiet:
-					msg(f'{coin} {network}')
-
-				vn_info = e.p2pkh_info
-				ret = cls.find_addr_leading_symbol(vn_info[0])
-				test_equal('P2PKH leading symbol',vn_info[1],ret,*cdata)
-
-				vn_info = e.p2sh_info
-				if vn_info:
-					ret = cls.find_addr_leading_symbol(vn_info[0])
-					test_equal('P2SH leading symbol',vn_info[1],ret,*cdata)
-
-	@classmethod
-	def verify_core_coin_data(cls,cfg,quiet=False,verbose=False):
-		from .protocol import CoinProtocol,init_proto
-
-		for network in ('mainnet','testnet'):
-			for coin in gc.core_coins:
-				e = cls.get_entry(coin,network)
-				if e:
-					proto = init_proto( cfg, coin, network=network )
-					cdata = (network,coin,e,type(proto).__name__,verbose)
-					if not quiet:
-						msg(f'Verifying {coin.upper()} {network}')
-
-					if coin != 'bch': # TODO
-						test_equal('coin name',e.name,proto.name,*cdata)
-
-					if e.trust_level != -1:
-						test_equal('Trust level',e.trust_level,CoinProtocol.coins[coin].trust_level,*cdata)
-
-					test_equal(
-						'WIF version number',
-						e.wif_ver_num,
-						int.from_bytes(proto.wif_ver_bytes['std'],'big'),
-						*cdata )
-
-					test_equal(
-						'P2PKH version number',
-						e.p2pkh_info[0],
-						int.from_bytes(proto.addr_fmt_to_ver_bytes['p2pkh'],'big'),
-						*cdata )
-
-					test_equal(
-						'P2SH version number',
-						e.p2sh_info[0],
-						int.from_bytes(proto.addr_fmt_to_ver_bytes['p2sh'],'big'),
-						*cdata )
-
 	@classmethod
 	def get_supported_coins(cls,network):
 		return [e for e in cls.coin_constants[network] if e.trust_level != -1]
@@ -476,225 +268,35 @@ class CoinInfo:
 			return None
 		return cls.coin_constants[network][idx]
 
-	# Data is one of the coin_constants lists above.  Normalize ints to hex of correct width, add
-	# missing leading letters, set trust level from external_tests.
-	# Insert a coin entry from outside source, set version info leading letters to '?' and trust level
-	# to 0, then run CoinInfo.fix_table(data).  'has_segwit' field is updated manually for now.
-	@classmethod
-	def fix_table(cls,data):
-		import re
-
-		def myhex(n):
-			return '0x{:0{}x}'.format(n,2 if n < 256 else 4)
-
-		def fix_ver_info(e,k):
-			e[k] = list(e[k])
-			e[k][0] = myhex(e[k][0])
-			s1 = cls.find_addr_leading_symbol(int(e[k][0][2:],16))
-			m = f'Fixing leading address letter for coin {e["symbol"]} ({e[k][1]!r} --> {s1})'
-			if e[k][1] != '?':
-				assert s1 == e[k][1], f'First letters do not match! {m}'
-			else:
-				msg(m)
-				e[k][1] = s1
-			e[k] = tuple(e[k])
-
-		old_sym = None
-		for sym in sorted([e.symbol for e in data]):
-			if sym == old_sym:
-				msg(f'{sym!r}: duplicate coin symbol in data!')
-				sys.exit(2)
-			old_sym = sym
-
-		tt = cls.create_trust_table()
-
-		name_w = max(len(e.name) for e in data)
-		fs = '\t({:%s} {:10} {:7} {:17} {:17} {:6} {}),' % (name_w+3)
-		for e in data:
-			e = e._asdict()
-			e['wif_ver_num'] = myhex(e['wif_ver_num'])
-			sym,trust = e['symbol'],e['trust_level']
-
-			fix_ver_info(e,'p2pkh_info')
-			if isinstance(e['p2sh_info'],tuple):
-				fix_ver_info(e,'p2sh_info')
-
-			for k in e.keys():
-				e[k] = repr(e[k])
-				e[k] = re.sub(r"'0x(..)'",r'0x\1',e[k])
-				e[k] = re.sub(r"'0x(....)'",r'0x\1',e[k])
-				e[k] = re.sub(r' ',r'',e[k]) + ('',',')[k != 'trust_level']
+def make_proto(e,testnet=False):
 
-			if trust != -1:
-				if sym in tt:
-					src = tt[sym]
-					if src != trust:
-						msg(f'Updating trust for coin {sym!r}: {trust} -> {src}')
-						e['trust_level'] = src
-				else:
-					if trust != 0:
-						msg(f'Downgrading trust for coin {sym!r}: {trust} -> 0')
-						e['trust_level'] = 0
+	proto = ('X_' if e.name[0] in '0123456789' else '') + e.name + ('Testnet' if testnet else '')
 
-				if sym in cls.cross_checks:
-					if int(e['trust_level']) == 0 and len(cls.cross_checks[sym]) > 1:
-						msg(f'Upgrading trust for coin {sym!r}: {e["trust_level"]} -> 1')
-						e['trust_level'] = 1
+	if hasattr(CoinProtocol,proto):
+		return
 
-			print(fs.format(*e.values()))
-		msg(f'Processed {len(data)} entries')
+	def num2hexstr(n):
+		return '{:0{}x}'.format(n,(4,2)[n < 256])
 
-	@classmethod
-	def find_addr_leading_symbol(cls,ver_num,verbose=False):
-
-		if ver_num == 0:
-			return '1'
-
-		def phash2addr(ver_num,pk_hash):
-			from .proto.btc.common 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)
-
-		low = phash2addr(ver_num,b'\x00'*20)
-		high = phash2addr(ver_num,b'\xff'*20)
-
-		if verbose:
-			print('low address:  ' + low)
-			print('high address: ' + high)
-
-		l1,h1 = low[0],high[0]
-		return (l1,h1) if l1 != h1 else l1
-
-	@classmethod
-	def print_symbols(cls,include_names=False,reverse=False):
-		for e in cls.coin_constants['mainnet']:
-			if reverse:
-				print(f'{e.symbol:6} {e.name}')
-			else:
-				name_w = max(len(e.name) for e in cls.coin_constants['mainnet'])
-				print((f'{e.name:{name_w}} ' if include_names else '') + e.symbol)
-
-	@classmethod
-	def create_trust_table(cls):
-		tt = {}
-		mn = cls.external_tests['mainnet']
-		for ext_prog in mn:
-			assert len(set(mn[ext_prog])) == len(mn[ext_prog]), f'Duplicate entry in {ext_prog!r}!'
-			for coin in mn[ext_prog]:
-				if coin in tt:
-					tt[coin] += 1
-				else:
-					tt[coin] = 1
-		for k in cls.trust_override:
-			tt[k] = cls.trust_override[k]
-		return tt
-
-	trust_override = {'BTC':3,'BCH':3,'LTC':3,'DASH':1,'EMC':2}
-
-	@classmethod
-	def get_test_support(cls,coin,addr_type,network,toolname=None,verbose=False):
-		"""
-		If requested tool supports coin/addr_type/network triplet, return tool name.
-		If 'tool' is None, return tool that supports coin/addr_type/network triplet.
-		Return None on failure.
-		"""
-		all_tools = [toolname] if toolname else list(cls.external_tests[network].keys())
-		coin = coin.upper()
-
-		for tool in all_tools:
-			if coin in cls.external_tests[network][tool]:
-				break
-		else:
-			if verbose:
-				m1 = 'Requested tool {t!r} does not support coin {c} on network {n}'
-				m2 = 'No test tool found for coin {c} on network {n}'
-				msg((m1 if toolname else m2).format(t=tool,c=coin,n=network))
-			return None
-
-		if addr_type == 'zcash_z':
-			if toolname in (None,'zcash-mini'):
-				return 'zcash-mini'
-			else:
-				if verbose:
-					msg(f"Address type {addr_type!r} supported only by tool 'zcash-mini'")
-				return None
-
-		try:
-			bl = cls.external_tests_blacklist[addr_type][tool]
-		except:
-			pass
-		else:
-			if bl is True or coin in bl:
-				if verbose:
-					msg(f'Tool {tool!r} blacklisted for coin {coin}, addr_type {addr_type!r}')
-				return None
-
-		if toolname: # skip whitelists
-			return tool
-
-		if addr_type in ('segwit','bech32'):
-			st = cls.external_tests_segwit_whitelist
-			if addr_type in st and coin in st[addr_type]:
-				return tool
-			else:
-				if verbose:
-					m1 = 'Requested tool {t!r} does not support coin {c}, addr_type {a!r}, on network {n}'
-					m2 = 'No test tool found supporting coin {c}, addr_type {a!r}, on network {n}'
-					msg((m1 if toolname else m2).format(t=tool,c=coin,n=network,a=addr_type))
-				return None
-
-		return tool
-
-	external_tests = {
-		'mainnet': {
-			# List in order of preference.
-			# If 'tool' is not specified, the first tool supporting the coin will be selected.
-			'pycoin': (
-				'DASH', # only compressed
-				'BCH',
-				'BTC','LTC','VIA','FTC','DOGE','MEC',
-				'JBS','MZC','RIC','DFC','FAI','ARG','ZEC','DCR'),
-			'keyconv': (
-				'BCH',
-				# broken: PIVX
-				'42','AC','AIB','ANC','ARS','ATMOS','AUR','BLK','BQC','BTC','TEST','BTCD','CCC','CCN','CDN',
-				'CLAM','CNC','CNOTE','CON','CRW','DEEPONION','DGB','DGC','DMD','DOGED','DOGE','DOPE',
-				'DVC','EFL','EMC','EXCL','FAIR','FLOZ','FTC','GAME','GAP','GCR','GRC','GRS','GUN','HAM','HODL',
-				'IXC','JBS','LBRY','LEAF','LTC','MMC','MONA','MUE','MYRIAD','MZC','NEOS','NLG','NMC','NVC',
-				'NYAN','OK','OMC','PIGGY','PINK','PKB','PND','POT','PPC','PTC','PTS','QTUM','RBY','RDD',
-				'RIC','SCA','SDC','SKC','SPR','START','SXC','TPC','UIS','UNO','VIA','VPN','VTC','WDC','WKC',
-				'WUBS', 'XC', 'XPM', 'YAC', 'ZOOM', 'ZRC'),
-			'ethkey': ('ETH','ETC'),
-			'zcash-mini': ('ZEC',),
-			'monero-python': ('XMR',),
-		},
-		'testnet': {
-			'pycoin': {
-				'DASH':'tDASH', # only compressed
-				'BCH':'XTN',
-				'BTC':'XTN','LTC':'XLT','VIA':'TVI','FTC':'FTX','DOGE':'XDT','DCR':'DCRT'
-				},
-			'ethkey': {},
-			'keyconv': {}
-		}
-	}
-	external_tests_segwit_whitelist = {
-		# Whitelists apply to the *first* tool in cls.external_tests supporting the given coin/addr_type.
-		# They're ignored if specific tool is requested.
-		'segwit': ('BTC',), # LTC Segwit broken on pycoin: uses old fmt
-		'bech32': ('BTC','LTC'),
-		'compressed': (
-			'BTC','LTC','VIA','FTC','DOGE','DASH','MEC','MYR','UNO',
-			'JBS','MZC','RIC','DFC','FAI','ARG','ZEC','DCR','ZEC'
-		),
-	}
-	external_tests_blacklist = {
-		# Unconditionally block testing of the given coin/addr_type with given tool, or all coins if True
-		'legacy': {},
-		'segwit': { 'keyconv': True },
-		'bech32': { 'keyconv': True },
-	}
+	setattr(
+		CoinProtocol,
+		proto,
+		type(
+			proto,
+			(mainnet,),
+			{
+				'base_coin': e.symbol,
+				'addr_ver_info': dict(
+					[( num2hexstr(e.p2pkh_info[0]), 'p2pkh' )] +
+					([( num2hexstr(e.p2sh_info[0]), 'p2sh' )] if e.p2sh_info else [])
+				),
+				'wif_ver_num': { 'std': num2hexstr(e.wif_ver_num) },
+				'mmtypes':    ('L','C','S') if e.has_segwit else ('L','C'),
+				'dfl_mmtype': 'L',
+				'mmcaps':     ('key','addr'),
+			},
+		)
+	)
 
 def init_genonly_altcoins(usr_coin=None,testnet=False):
 	"""
@@ -704,6 +306,7 @@ def init_genonly_altcoins(usr_coin=None,testnet=False):
 	If usr_coin is None, initializes all coins for current network with trust level >-1.
 	Returns trust_level of usr_coin, or 0 (untrusted) if usr_coin is None.
 	"""
+
 	data = { 'mainnet': (), 'testnet': () }
 	networks = ['mainnet'] + (['testnet'] if testnet else [])
 	network = 'testnet' if testnet else 'mainnet'
@@ -713,7 +316,6 @@ def init_genonly_altcoins(usr_coin=None,testnet=False):
 			data[network] = CoinInfo.get_supported_coins(network)
 	else:
 		if usr_coin.lower() in gc.core_coins: # core coin, so return immediately
-			from .protocol import CoinProtocol
 			return CoinProtocol.coins[usr_coin.lower()].trust_level
 		for network in networks:
 			data[network] = (CoinInfo.get_entry(usr_coin,network),)
@@ -724,43 +326,6 @@ def init_genonly_altcoins(usr_coin=None,testnet=False):
 		if cinfo.trust_level == -1:
 			raise ValueError(f'{usr_coin.upper()!r}: unsupported (disabled) coin for network {network.upper()}')
 
-	create_altcoin_protos(data)
-
-def create_altcoin_protos(data):
-
-	from .protocol import CoinProtocol
-	from .proto.btc.params import mainnet
-
-	def make_proto(e,testnet=False):
-
-		proto = ('X_' if e.name[0] in '0123456789' else '') + e.name + ('Testnet' if testnet else '')
-
-		if hasattr(CoinProtocol,proto):
-			return
-
-		def num2hexstr(n):
-			return '{:0{}x}'.format(n,(4,2)[n < 256])
-
-		setattr(
-			CoinProtocol,
-			proto,
-			type(
-				proto,
-				(mainnet,),
-				{
-					'base_coin': e.symbol,
-					'addr_ver_info': dict(
-						[( num2hexstr(e.p2pkh_info[0]), 'p2pkh' )] +
-						([( num2hexstr(e.p2sh_info[0]), 'p2sh' )] if e.p2sh_info else [])
-					),
-					'wif_ver_num': { 'std': num2hexstr(e.wif_ver_num) },
-					'mmtypes':    ('L','C','S') if e.has_segwit else ('L','C'),
-					'dfl_mmtype': 'L',
-					'mmcaps':     ('key','addr'),
-				},
-			)
-		)
-
 	for e in data['mainnet']:
 		make_proto(e)
 
@@ -773,21 +338,3 @@ def create_altcoin_protos(data):
 		CoinProtocol.coins[e.symbol.lower()] = CoinProtocol.proto_info(
 			name        = 'X_'+e.name if e.name[0] in '0123456789' else e.name,
 			trust_level = e.trust_level )
-
-if __name__ == '__main__':
-
-	opts_data = {
-		'text': {
-			'desc': 'Check altcoin data',
-			'usage':'[opts]',
-			'options': '-q, --quiet    Be quieter\n-v, --verbose  Be more verbose'
-		}
-	}
-
-	cfg = Config( opts_data=opts_data, need_amt=False )
-
-	msg('Checking CoinInfo WIF/P2PKH/P2SH version numbers and trust levels against protocol.py')
-	CoinInfo.verify_core_coin_data( cfg, cfg.quiet, cfg.verbose )
-
-	msg('Checking CoinInfo address leading symbols')
-	CoinInfo.verify_leading_symbols( cfg.quiet, cfg.verbose )

+ 3 - 3
mmgen/protocol.py

@@ -34,7 +34,7 @@ _nw = namedtuple('coin_networks',['mainnet','testnet','regtest'])
 
 class CoinProtocol(MMGenObject):
 
-	proto_info = namedtuple('proto_info',['name','trust_level']) # trust levels: see altcoin.py
+	proto_info = namedtuple('proto_info',['name','trust_level']) # trust levels: see altcoin/params.py
 
 	# keys are mirrored in gc.core_coins:
 	coins = {
@@ -258,7 +258,7 @@ def init_proto(
 	coin = coin.lower()
 
 	if coin not in CoinProtocol.coins:
-		from .altcoin import init_genonly_altcoins
+		from .altcoin.params import init_genonly_altcoins
 		init_genonly_altcoins( coin, testnet=testnet ) # raises exception on failure
 
 	name = CoinProtocol.coins[coin].name
@@ -298,7 +298,7 @@ def warn_trustlevel(cfg):
 	if coinsym.lower() in CoinProtocol.coins:
 		trust_level = CoinProtocol.coins[coinsym.lower()].trust_level
 	else:
-		from .altcoin import CoinInfo
+		from .altcoin.params import CoinInfo
 		e = CoinInfo.get_entry(coinsym,'mainnet')
 		trust_level = e.trust_level if e else None
 		if trust_level in (None,-1):

+ 1 - 1
mmgen/tool/api.py

@@ -91,7 +91,7 @@ class tool_api(
 	def coins(self):
 		"""The available coins"""
 		from ..protocol import CoinProtocol
-		from ..altcoin import CoinInfo
+		from ..altcoin.params import CoinInfo
 		return sorted(set(
 			[c.upper() for c in CoinProtocol.coins]
 			+ [c.symbol for c in CoinInfo.get_supported_coins(self.proto.network)]

+ 1 - 0
setup.cfg

@@ -52,6 +52,7 @@ install_requires =
 
 packages =
 	mmgen
+	mmgen.altcoin
 	mmgen.contrib
 	mmgen.data
 	mmgen.help

+ 465 - 0
test/altcointest.py

@@ -0,0 +1,465 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2023 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-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+altcointest.py - Test constants for Bitcoin-derived altcoins
+"""
+
+import sys
+
+try:
+	from include import test_init
+except ImportError:
+	from test.include import test_init
+
+from mmgen.cfg import gc,Config
+from mmgen.util import msg
+from mmgen.altcoin.params import CoinInfo
+
+def test_equal(desc,a,b,*cdata):
+	if type(a) is int:
+		a = hex(a)
+		b = hex(b)
+	(network,coin,_,b_desc,verbose) = cdata
+	if verbose:
+		msg(f'  {desc:20}: {a!r}')
+	if a != b:
+		raise ValueError(
+			f'{desc.capitalize()}s for {coin.upper()} {network} do not match:\n  CoinInfo: {a}\n  {b_desc}: {b}' )
+
+class TestCoinInfo(CoinInfo):
+
+	# Sources (see CoinInfo) that are in agreement for these coins
+	# No check for segwit, p2sh check skipped if source doesn't support it
+	cross_checks = {
+		'2GIVE':  ['wn'],
+		'42':     ['vg','wn'],
+		'611':    ['wn'],
+		'AC':     ['lb','vg'],
+		'ACOIN':  ['wn'],
+		'ALF':    ['wn'],
+		'ANC':    ['vg','wn'],
+		'APEX':   ['wn'],
+		'ARCO':   ['wn'],
+		'ARG':    ['pc'],
+		'AUR':    ['vg','wn'],
+		'BCH':    ['wn'],
+		'BLK':    ['lb','vg','wn'],
+		'BQC':    ['vg','wn'],
+		'BSTY':   ['wn'],
+		'BTC':    ['lb','vg','wn'],
+		'BTCD':   ['lb','vg','wn'],
+		'BUCKS':  ['wn'],
+		'CASH':   ['wn'],
+		'CBX':    ['wn'],
+		'CCN':    ['lb','vg','wn'],
+		'CDN':    ['lb','vg','wn'],
+		'CHC':    ['wn'],
+		'CLAM':   ['lb','vg'],
+		'CON':    ['vg','wn'],
+		'CPC':    ['wn'],
+		'DASH':   ['lb','pc','vg','wn'],
+		'DCR':    ['pc'],
+		'DFC':    ['pc'],
+		'DGB':    ['lb','vg'],
+		'DGC':    ['lb','vg','wn'],
+		'DOGE':   ['lb','pc','vg','wn'],
+		'DOGED':  ['lb','vg','wn'],
+		'DOPE':   ['lb','vg'],
+		'DVC':    ['vg','wn'],
+		'EFL':    ['lb','vg','wn'],
+		'EMC':    ['vg'],
+		'EMD':    ['wn'],
+		'ESP':    ['wn'],
+		'FAI':    ['pc'],
+		'FC2':    ['wn'],
+		'FIBRE':  ['wn'],
+		'FJC':    ['wn'],
+		'FLO':    ['wn'],
+		'FLT':    ['wn'],
+		'FST':    ['wn'],
+		'FTC':    ['lb','pc','vg','wn'],
+		'GCR':    ['lb','vg'],
+		'GOOD':   ['wn'],
+		'GRC':    ['vg','wn'],
+		'GUN':    ['vg','wn'],
+		'HAM':    ['vg','wn'],
+		'HTML5':  ['wn'],
+		'HYP':    ['wn'],
+		'ICASH':  ['wn'],
+		'INFX':   ['wn'],
+		'IPC':    ['wn'],
+		'JBS':    ['lb','pc','vg','wn'],
+		'JUDGE':  ['wn'],
+		'LANA':   ['wn'],
+		'LAT':    ['wn'],
+		'LDOGE':  ['wn'],
+		'LMC':    ['wn'],
+		'LTC':    ['lb','vg','wn'],
+		'MARS':   ['wn'],
+		'MEC':    ['pc','wn'],
+		'MINT':   ['wn'],
+		'MOBI':   ['wn'],
+		'MONA':   ['lb','vg'],
+		'MOON':   ['wn'],
+		'MUE':    ['lb','vg'],
+		'MXT':    ['wn'],
+		'MYR':    ['pc'],
+		'MYRIAD': ['vg','wn'],
+		'MZC':    ['lb','pc','vg','wn'],
+		'NEOS':   ['lb','vg'],
+		'NEVA':   ['wn'],
+		'NKA':    ['wn'],
+		'NLG':    ['vg','wn'],
+		'NMC':    ['lb','vg'],
+		'NVC':    ['lb','vg','wn'],
+		'OK':     ['lb','vg'],
+		'OMC':    ['vg','wn'],
+		'ONION':  ['vg','wn'],
+		'PART':   ['wn'],
+		'PINK':   ['vg','wn'],
+		'PIVX':   ['wn'],
+		'PKB':    ['lb','vg','wn'],
+		'PND':    ['lb','vg','wn'],
+		'POT':    ['lb','vg','wn'],
+		'PPC':    ['lb','vg','wn'],
+		'PTC':    ['vg','wn'],
+		'PXC':    ['wn'],
+		'QRK':    ['wn'],
+		'RAIN':   ['wn'],
+		'RBT':    ['wn'],
+		'RBY':    ['lb','vg'],
+		'RDD':    ['vg','wn'],
+		'RIC':    ['pc','vg','wn'],
+		'SDC':    ['lb','vg'],
+		'SIB':    ['wn'],
+		'SMLY':   ['wn'],
+		'SONG':   ['wn'],
+		'SPR':    ['vg','wn'],
+		'START':  ['lb','vg'],
+		'SYS':    ['wn'],
+		'TAJ':    ['wn'],
+		'TIT':    ['wn'],
+		'TPC':    ['lb','vg'],
+		'TRC':    ['wn'],
+		'TTC':    ['wn'],
+		'TX':     ['wn'],
+		'UNO':    ['pc','vg','wn'],
+		'VIA':    ['lb','pc','vg','wn'],
+		'VPN':    ['lb','vg'],
+		'VTC':    ['lb','vg','wn'],
+		'WDC':    ['vg','wn'],
+		'WISC':   ['wn'],
+		'WKC':    ['vg','wn'],
+		'WSX':    ['wn'],
+		'XCN':    ['wn'],
+		'XGB':    ['wn'],
+		'XPM':    ['lb','vg','wn'],
+		'XST':    ['wn'],
+		'XVC':    ['wn'],
+		'ZET':    ['wn'],
+		'ZOOM':   ['lb','vg'],
+		'ZRC':    ['lb','vg']
+	}
+
+	@classmethod
+	def verify_leading_symbols(cls,quiet=False,verbose=False):
+
+		for network in ('mainnet','testnet'):
+			for coin in [e.symbol for e in cls.coin_constants[network]]:
+				e = cls.get_entry(coin,network)
+				cdata = (network,coin,e,'Computed value',verbose)
+
+				if not quiet:
+					msg(f'{coin} {network}')
+
+				vn_info = e.p2pkh_info
+				ret = cls.find_addr_leading_symbol(vn_info[0])
+				test_equal('P2PKH leading symbol',vn_info[1],ret,*cdata)
+
+				vn_info = e.p2sh_info
+				if vn_info:
+					ret = cls.find_addr_leading_symbol(vn_info[0])
+					test_equal('P2SH leading symbol',vn_info[1],ret,*cdata)
+
+	@classmethod
+	def verify_core_coin_data(cls,cfg,quiet=False,verbose=False):
+		from mmgen.protocol import CoinProtocol,init_proto
+
+		for network in ('mainnet','testnet'):
+			for coin in gc.core_coins:
+				e = cls.get_entry(coin,network)
+				if e:
+					proto = init_proto( cfg, coin, network=network )
+					cdata = (network,coin,e,type(proto).__name__,verbose)
+					if not quiet:
+						msg(f'Verifying {coin.upper()} {network}')
+
+					if coin != 'bch': # TODO
+						test_equal('coin name',e.name,proto.name,*cdata)
+
+					if e.trust_level != -1:
+						test_equal('Trust level',e.trust_level,CoinProtocol.coins[coin].trust_level,*cdata)
+
+					test_equal(
+						'WIF version number',
+						e.wif_ver_num,
+						int.from_bytes(proto.wif_ver_bytes['std'],'big'),
+						*cdata )
+
+					test_equal(
+						'P2PKH version number',
+						e.p2pkh_info[0],
+						int.from_bytes(proto.addr_fmt_to_ver_bytes['p2pkh'],'big'),
+						*cdata )
+
+					test_equal(
+						'P2SH version number',
+						e.p2sh_info[0],
+						int.from_bytes(proto.addr_fmt_to_ver_bytes['p2sh'],'big'),
+						*cdata )
+
+	# Data is one of the coin_constants lists above.  Normalize ints to hex of correct width, add
+	# missing leading letters, set trust level from external_tests.
+	# Insert a coin entry from outside source, set version info leading letters to '?' and trust level
+	# to 0, then run TestCoinInfo.fix_table(data).  'has_segwit' field is updated manually for now.
+	@classmethod
+	def fix_table(cls,data):
+		import re
+
+		def myhex(n):
+			return '0x{:0{}x}'.format(n,2 if n < 256 else 4)
+
+		def fix_ver_info(e,k):
+			e[k] = list(e[k])
+			e[k][0] = myhex(e[k][0])
+			s1 = cls.find_addr_leading_symbol(int(e[k][0][2:],16))
+			m = f'Fixing leading address letter for coin {e["symbol"]} ({e[k][1]!r} --> {s1})'
+			if e[k][1] != '?':
+				assert s1 == e[k][1], f'First letters do not match! {m}'
+			else:
+				msg(m)
+				e[k][1] = s1
+			e[k] = tuple(e[k])
+
+		old_sym = None
+		for sym in sorted([e.symbol for e in data]):
+			if sym == old_sym:
+				msg(f'{sym!r}: duplicate coin symbol in data!')
+				sys.exit(2)
+			old_sym = sym
+
+		tt = cls.create_trust_table()
+
+		name_w = max(len(e.name) for e in data)
+		fs = '\t({:%s} {:10} {:7} {:17} {:17} {:6} {}),' % (name_w+3)
+		for e in data:
+			e = e._asdict()
+			e['wif_ver_num'] = myhex(e['wif_ver_num'])
+			sym,trust = e['symbol'],e['trust_level']
+
+			fix_ver_info(e,'p2pkh_info')
+			if isinstance(e['p2sh_info'],tuple):
+				fix_ver_info(e,'p2sh_info')
+
+			for k in e.keys():
+				e[k] = repr(e[k])
+				e[k] = re.sub(r"'0x(..)'",r'0x\1',e[k])
+				e[k] = re.sub(r"'0x(....)'",r'0x\1',e[k])
+				e[k] = re.sub(r' ',r'',e[k]) + ('',',')[k != 'trust_level']
+
+			if trust != -1:
+				if sym in tt:
+					src = tt[sym]
+					if src != trust:
+						msg(f'Updating trust for coin {sym!r}: {trust} -> {src}')
+						e['trust_level'] = src
+				else:
+					if trust != 0:
+						msg(f'Downgrading trust for coin {sym!r}: {trust} -> 0')
+						e['trust_level'] = 0
+
+				if sym in cls.cross_checks:
+					if int(e['trust_level']) == 0 and len(cls.cross_checks[sym]) > 1:
+						msg(f'Upgrading trust for coin {sym!r}: {e["trust_level"]} -> 1')
+						e['trust_level'] = 1
+
+			print(fs.format(*e.values()))
+		msg(f'Processed {len(data)} entries')
+
+	@classmethod
+	def find_addr_leading_symbol(cls,ver_num,verbose=False):
+
+		if ver_num == 0:
+			return '1'
+
+		def phash2addr(ver_num,pk_hash):
+			from mmgen.proto.btc.common 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)
+
+		low = phash2addr(ver_num,b'\x00'*20)
+		high = phash2addr(ver_num,b'\xff'*20)
+
+		if verbose:
+			print('low address:  ' + low)
+			print('high address: ' + high)
+
+		l1,h1 = low[0],high[0]
+		return (l1,h1) if l1 != h1 else l1
+
+	@classmethod
+	def print_symbols(cls,include_names=False,reverse=False):
+		for e in cls.coin_constants['mainnet']:
+			if reverse:
+				print(f'{e.symbol:6} {e.name}')
+			else:
+				name_w = max(len(e.name) for e in cls.coin_constants['mainnet'])
+				print((f'{e.name:{name_w}} ' if include_names else '') + e.symbol)
+
+	@classmethod
+	def create_trust_table(cls):
+		tt = {}
+		mn = cls.external_tests['mainnet']
+		for ext_prog in mn:
+			assert len(set(mn[ext_prog])) == len(mn[ext_prog]), f'Duplicate entry in {ext_prog!r}!'
+			for coin in mn[ext_prog]:
+				if coin in tt:
+					tt[coin] += 1
+				else:
+					tt[coin] = 1
+		for k in cls.trust_override:
+			tt[k] = cls.trust_override[k]
+		return tt
+
+	trust_override = {'BTC':3,'BCH':3,'LTC':3,'DASH':1,'EMC':2}
+
+	@classmethod
+	def get_test_support(cls,coin,addr_type,network,toolname=None,verbose=False):
+		"""
+		If requested tool supports coin/addr_type/network triplet, return tool name.
+		If 'tool' is None, return tool that supports coin/addr_type/network triplet.
+		Return None on failure.
+		"""
+		all_tools = [toolname] if toolname else list(cls.external_tests[network].keys())
+		coin = coin.upper()
+
+		for tool in all_tools:
+			if coin in cls.external_tests[network][tool]:
+				break
+		else:
+			if verbose:
+				m1 = 'Requested tool {t!r} does not support coin {c} on network {n}'
+				m2 = 'No test tool found for coin {c} on network {n}'
+				msg((m1 if toolname else m2).format(t=tool,c=coin,n=network))
+			return None
+
+		if addr_type == 'zcash_z':
+			if toolname in (None,'zcash-mini'):
+				return 'zcash-mini'
+			else:
+				if verbose:
+					msg(f"Address type {addr_type!r} supported only by tool 'zcash-mini'")
+				return None
+
+		try:
+			bl = cls.external_tests_blacklist[addr_type][tool]
+		except:
+			pass
+		else:
+			if bl is True or coin in bl:
+				if verbose:
+					msg(f'Tool {tool!r} blacklisted for coin {coin}, addr_type {addr_type!r}')
+				return None
+
+		if toolname: # skip whitelists
+			return tool
+
+		if addr_type in ('segwit','bech32'):
+			st = cls.external_tests_segwit_whitelist
+			if addr_type in st and coin in st[addr_type]:
+				return tool
+			else:
+				if verbose:
+					m1 = 'Requested tool {t!r} does not support coin {c}, addr_type {a!r}, on network {n}'
+					m2 = 'No test tool found supporting coin {c}, addr_type {a!r}, on network {n}'
+					msg((m1 if toolname else m2).format(t=tool,c=coin,n=network,a=addr_type))
+				return None
+
+		return tool
+
+	external_tests = {
+		'mainnet': {
+			# List in order of preference.
+			# If 'tool' is not specified, the first tool supporting the coin will be selected.
+			'pycoin': (
+				'DASH', # only compressed
+				'BCH',
+				'BTC','LTC','VIA','FTC','DOGE','MEC',
+				'JBS','MZC','RIC','DFC','FAI','ARG','ZEC','DCR'),
+			'keyconv': (
+				'BCH',
+				# broken: PIVX
+				'42','AC','AIB','ANC','ARS','ATMOS','AUR','BLK','BQC','BTC','TEST','BTCD','CCC','CCN','CDN',
+				'CLAM','CNC','CNOTE','CON','CRW','DEEPONION','DGB','DGC','DMD','DOGED','DOGE','DOPE',
+				'DVC','EFL','EMC','EXCL','FAIR','FLOZ','FTC','GAME','GAP','GCR','GRC','GRS','GUN','HAM','HODL',
+				'IXC','JBS','LBRY','LEAF','LTC','MMC','MONA','MUE','MYRIAD','MZC','NEOS','NLG','NMC','NVC',
+				'NYAN','OK','OMC','PIGGY','PINK','PKB','PND','POT','PPC','PTC','PTS','QTUM','RBY','RDD',
+				'RIC','SCA','SDC','SKC','SPR','START','SXC','TPC','UIS','UNO','VIA','VPN','VTC','WDC','WKC',
+				'WUBS', 'XC', 'XPM', 'YAC', 'ZOOM', 'ZRC'),
+			'ethkey': ('ETH','ETC'),
+			'zcash-mini': ('ZEC',),
+			'monero-python': ('XMR',),
+		},
+		'testnet': {
+			'pycoin': {
+				'DASH':'tDASH', # only compressed
+				'BCH':'XTN',
+				'BTC':'XTN','LTC':'XLT','VIA':'TVI','FTC':'FTX','DOGE':'XDT','DCR':'DCRT'
+				},
+			'ethkey': {},
+			'keyconv': {}
+		}
+	}
+	external_tests_segwit_whitelist = {
+		# Whitelists apply to the *first* tool in cls.external_tests supporting the given coin/addr_type.
+		# They're ignored if specific tool is requested.
+		'segwit': ('BTC',), # LTC Segwit broken on pycoin: uses old fmt
+		'bech32': ('BTC','LTC'),
+		'compressed': (
+			'BTC','LTC','VIA','FTC','DOGE','DASH','MEC','MYR','UNO',
+			'JBS','MZC','RIC','DFC','FAI','ARG','ZEC','DCR','ZEC'
+		),
+	}
+	external_tests_blacklist = {
+		# Unconditionally block testing of the given coin/addr_type with given tool, or all coins if True
+		'legacy': {},
+		'segwit': { 'keyconv': True },
+		'bech32': { 'keyconv': True },
+	}
+
+if __name__ == '__main__':
+
+	opts_data = {
+		'text': {
+			'desc': 'Check altcoin data',
+			'usage':'[opts]',
+			'options': '-q, --quiet    Be quieter\n-v, --verbose  Be more verbose'
+		}
+	}
+
+	cfg = Config( opts_data=opts_data, need_amt=False )
+
+	msg('Checking CoinInfo WIF/P2PKH/P2SH version numbers and trust levels against protocol.py')
+	TestCoinInfo.verify_core_coin_data( cfg, cfg.quiet, cfg.verbose )
+
+	msg('Checking CoinInfo address leading symbols')
+	TestCoinInfo.verify_leading_symbols( cfg.quiet, cfg.verbose )

+ 2 - 2
test/gentest.py

@@ -558,8 +558,8 @@ def main():
 from subprocess import run,PIPE,DEVNULL
 from collections import namedtuple
 from mmgen.protocol import init_proto,CoinProtocol
-from mmgen.altcoin import init_genonly_altcoins
-from mmgen.altcoin import CoinInfo as cinfo
+from mmgen.altcoin.params import init_genonly_altcoins
+from test.altcointest import TestCoinInfo as cinfo
 from mmgen.key import PrivKey
 from mmgen.addr import MMGenAddrType
 from mmgen.addrgen import KeyGenerator,AddrGenerator

+ 1 - 1
test/test-release.d/cfg.sh

@@ -33,7 +33,7 @@ init_tests() {
 
 	d_alt="altcoin module"
 	t_alt="
-		- python3 -m mmgen.altcoin $altcoin_mod_opts
+		- python3 -m test.altcointest $altcoin_mod_opts
 	"
 
 	d_obj="data objects"