altcointest.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. altcointest.py - Test constants for Bitcoin-derived altcoins
  12. """
  13. import sys
  14. try:
  15. from include import test_init
  16. except ImportError:
  17. from test.include import test_init
  18. from mmgen.cfg import gc, Config
  19. from mmgen.util import msg
  20. from mmgen.altcoin.params import CoinInfo
  21. def test_equal(desc, a, b, *cdata):
  22. if type(a) is int:
  23. a = hex(a)
  24. b = hex(b)
  25. (network, coin, _, b_desc, verbose) = cdata
  26. if verbose:
  27. msg(f' {desc:20}: {a!r}')
  28. if a != b:
  29. raise ValueError(
  30. f'{desc.capitalize()}s for {coin.upper()} {network} do not match:\n CoinInfo: {a}\n {b_desc}: {b}')
  31. class TestCoinInfo(CoinInfo):
  32. # Sources (see CoinInfo) that are in agreement for these coins
  33. # No check for segwit, p2sh check skipped if source doesn't support it
  34. cross_checks = {
  35. '2GIVE': ['wn'],
  36. '42': ['vg', 'wn'],
  37. '611': ['wn'],
  38. 'AC': ['lb', 'vg'],
  39. 'ACOIN': ['wn'],
  40. 'ALF': ['wn'],
  41. 'ANC': ['vg', 'wn'],
  42. 'APEX': ['wn'],
  43. 'ARCO': ['wn'],
  44. 'ARG': ['pc'],
  45. 'AUR': ['vg', 'wn'],
  46. 'BCH': ['wn'],
  47. 'BLK': ['lb', 'vg', 'wn'],
  48. 'BQC': ['vg', 'wn'],
  49. 'BSTY': ['wn'],
  50. 'BTC': ['lb', 'vg', 'wn'],
  51. 'BTCD': ['lb', 'vg', 'wn'],
  52. 'BUCKS': ['wn'],
  53. 'CASH': ['wn'],
  54. 'CBX': ['wn'],
  55. 'CCN': ['lb', 'vg', 'wn'],
  56. 'CDN': ['lb', 'vg', 'wn'],
  57. 'CHC': ['wn'],
  58. 'CLAM': ['lb', 'vg'],
  59. 'CON': ['vg', 'wn'],
  60. 'CPC': ['wn'],
  61. 'DASH': ['lb', 'pc', 'vg', 'wn'],
  62. 'DCR': ['pc'],
  63. 'DFC': ['pc'],
  64. 'DGB': ['lb', 'vg'],
  65. 'DGC': ['lb', 'vg', 'wn'],
  66. 'DOGE': ['lb', 'pc', 'vg', 'wn'],
  67. 'DOGED': ['lb', 'vg', 'wn'],
  68. 'DOPE': ['lb', 'vg'],
  69. 'DVC': ['vg', 'wn'],
  70. 'EFL': ['lb', 'vg', 'wn'],
  71. 'EMC': ['vg'],
  72. 'EMD': ['wn'],
  73. 'ESP': ['wn'],
  74. 'FAI': ['pc'],
  75. 'FC2': ['wn'],
  76. 'FIBRE': ['wn'],
  77. 'FJC': ['wn'],
  78. 'FLO': ['wn'],
  79. 'FLT': ['wn'],
  80. 'FST': ['wn'],
  81. 'FTC': ['lb', 'pc', 'vg', 'wn'],
  82. 'GCR': ['lb', 'vg'],
  83. 'GOOD': ['wn'],
  84. 'GRC': ['vg', 'wn'],
  85. 'GUN': ['vg', 'wn'],
  86. 'HAM': ['vg', 'wn'],
  87. 'HTML5': ['wn'],
  88. 'HYP': ['wn'],
  89. 'ICASH': ['wn'],
  90. 'INFX': ['wn'],
  91. 'IPC': ['wn'],
  92. 'JBS': ['lb', 'pc', 'vg', 'wn'],
  93. 'JUDGE': ['wn'],
  94. 'LANA': ['wn'],
  95. 'LAT': ['wn'],
  96. 'LDOGE': ['wn'],
  97. 'LMC': ['wn'],
  98. 'LTC': ['lb', 'vg', 'wn'],
  99. 'MARS': ['wn'],
  100. 'MEC': ['pc', 'wn'],
  101. 'MINT': ['wn'],
  102. 'MOBI': ['wn'],
  103. 'MONA': ['lb', 'vg'],
  104. 'MOON': ['wn'],
  105. 'MUE': ['lb', 'vg'],
  106. 'MXT': ['wn'],
  107. 'MYR': ['pc'],
  108. 'MYRIAD': ['vg', 'wn'],
  109. 'MZC': ['lb', 'pc', 'vg', 'wn'],
  110. 'NEOS': ['lb', 'vg'],
  111. 'NEVA': ['wn'],
  112. 'NKA': ['wn'],
  113. 'NLG': ['vg', 'wn'],
  114. 'NMC': ['lb', 'vg'],
  115. 'NVC': ['lb', 'vg', 'wn'],
  116. 'OK': ['lb', 'vg'],
  117. 'OMC': ['vg', 'wn'],
  118. 'ONION': ['vg', 'wn'],
  119. 'PART': ['wn'],
  120. 'PINK': ['vg', 'wn'],
  121. 'PIVX': ['wn'],
  122. 'PKB': ['lb', 'vg', 'wn'],
  123. 'PND': ['lb', 'vg', 'wn'],
  124. 'POT': ['lb', 'vg', 'wn'],
  125. 'PPC': ['lb', 'vg', 'wn'],
  126. 'PTC': ['vg', 'wn'],
  127. 'PXC': ['wn'],
  128. 'QRK': ['wn'],
  129. 'RAIN': ['wn'],
  130. 'RBT': ['wn'],
  131. 'RBY': ['lb', 'vg'],
  132. 'RDD': ['vg', 'wn'],
  133. 'RIC': ['pc', 'vg', 'wn'],
  134. 'SDC': ['lb', 'vg'],
  135. 'SIB': ['wn'],
  136. 'SMLY': ['wn'],
  137. 'SONG': ['wn'],
  138. 'SPR': ['vg', 'wn'],
  139. 'START': ['lb', 'vg'],
  140. 'SYS': ['wn'],
  141. 'TAJ': ['wn'],
  142. 'TIT': ['wn'],
  143. 'TPC': ['lb', 'vg'],
  144. 'TRC': ['wn'],
  145. 'TTC': ['wn'],
  146. 'TX': ['wn'],
  147. 'UNO': ['pc', 'vg', 'wn'],
  148. 'VIA': ['lb', 'pc', 'vg', 'wn'],
  149. 'VPN': ['lb', 'vg'],
  150. 'VTC': ['lb', 'vg', 'wn'],
  151. 'WDC': ['vg', 'wn'],
  152. 'WISC': ['wn'],
  153. 'WKC': ['vg', 'wn'],
  154. 'WSX': ['wn'],
  155. 'XCN': ['wn'],
  156. 'XGB': ['wn'],
  157. 'XPM': ['lb', 'vg', 'wn'],
  158. 'XST': ['wn'],
  159. 'XVC': ['wn'],
  160. 'ZET': ['wn'],
  161. 'ZOOM': ['lb', 'vg'],
  162. 'ZRC': ['lb', 'vg']
  163. }
  164. @classmethod
  165. def verify_leading_symbols(cls, quiet=False, verbose=False):
  166. for network in ('mainnet', 'testnet'):
  167. for coin in [e.symbol for e in cls.coin_constants[network]]:
  168. e = cls.get_entry(coin, network)
  169. cdata = (network, coin, e, 'Computed value', verbose)
  170. if not quiet:
  171. msg(f'{coin} {network}')
  172. vn_info = e.p2pkh_info
  173. ret = cls.find_addr_leading_symbol(vn_info[0])
  174. test_equal('P2PKH leading symbol', vn_info[1], ret, *cdata)
  175. vn_info = e.p2sh_info
  176. if vn_info:
  177. ret = cls.find_addr_leading_symbol(vn_info[0])
  178. test_equal('P2SH leading symbol', vn_info[1], ret, *cdata)
  179. @classmethod
  180. def verify_core_coin_data(cls, cfg, quiet=False, verbose=False):
  181. from mmgen.protocol import CoinProtocol, init_proto
  182. for network in ('mainnet', 'testnet'):
  183. for coin in gc.core_coins:
  184. e = cls.get_entry(coin, network)
  185. if e:
  186. proto = init_proto(cfg, coin, network=network)
  187. cdata = (network, coin, e, type(proto).__name__, verbose)
  188. if not quiet:
  189. msg(f'Verifying {coin.upper()} {network}')
  190. if coin != 'bch': # TODO
  191. test_equal('coin name', e.name, proto.name, *cdata)
  192. if e.trust_level != -1:
  193. test_equal('Trust level', e.trust_level, CoinProtocol.coins[coin].trust_level, *cdata)
  194. test_equal(
  195. 'WIF version number',
  196. e.wif_ver_num,
  197. int.from_bytes(proto.wif_ver_bytes['std'], 'big'),
  198. *cdata)
  199. test_equal(
  200. 'P2PKH version number',
  201. e.p2pkh_info[0],
  202. int.from_bytes(proto.addr_fmt_to_ver_bytes['p2pkh'], 'big'),
  203. *cdata)
  204. test_equal(
  205. 'P2SH version number',
  206. e.p2sh_info[0],
  207. int.from_bytes(proto.addr_fmt_to_ver_bytes['p2sh'], 'big'),
  208. *cdata)
  209. # Data is one of the coin_constants lists above. Normalize ints to hex of correct width, add
  210. # missing leading letters, set trust level from external_tests.
  211. # Insert a coin entry from outside source, set version info leading letters to '?' and trust level
  212. # to 0, then run TestCoinInfo.fix_table(data). 'has_segwit' field is updated manually for now.
  213. @classmethod
  214. def fix_table(cls, data):
  215. import re
  216. def myhex(n):
  217. return '0x{:0{}x}'.format(n, 2 if n < 256 else 4)
  218. def fix_ver_info(e, k):
  219. e[k] = list(e[k])
  220. e[k][0] = myhex(e[k][0])
  221. s1 = cls.find_addr_leading_symbol(int(e[k][0][2:], 16))
  222. m = f'Fixing leading address letter for coin {e["symbol"]} ({e[k][1]!r} --> {s1})'
  223. if e[k][1] != '?':
  224. assert s1 == e[k][1], f'First letters do not match! {m}'
  225. else:
  226. msg(m)
  227. e[k][1] = s1
  228. e[k] = tuple(e[k])
  229. old_sym = None
  230. for sym in sorted([e.symbol for e in data]):
  231. if sym == old_sym:
  232. msg(f'{sym!r}: duplicate coin symbol in data!')
  233. sys.exit(2)
  234. old_sym = sym
  235. tt = cls.create_trust_table()
  236. name_w = max(len(e.name) for e in data)
  237. fs = '\t({:%s} {:10} {:7} {:17} {:17} {:6} {}),' % (name_w+3)
  238. for e in data:
  239. e = e._asdict()
  240. e['wif_ver_num'] = myhex(e['wif_ver_num'])
  241. sym, trust = e['symbol'], e['trust_level']
  242. fix_ver_info(e, 'p2pkh_info')
  243. if isinstance(e['p2sh_info'], tuple):
  244. fix_ver_info(e, 'p2sh_info')
  245. for k in e.keys():
  246. e[k] = repr(e[k])
  247. e[k] = re.sub(r"'0x(..)'", r'0x\1', e[k])
  248. e[k] = re.sub(r"'0x(....)'", r'0x\1', e[k])
  249. e[k] = re.sub(r' ', r'', e[k]) + ('', ',')[k != 'trust_level']
  250. if trust != -1:
  251. if sym in tt:
  252. src = tt[sym]
  253. if src != trust:
  254. msg(f'Updating trust for coin {sym!r}: {trust} -> {src}')
  255. e['trust_level'] = src
  256. else:
  257. if trust != 0:
  258. msg(f'Downgrading trust for coin {sym!r}: {trust} -> 0')
  259. e['trust_level'] = 0
  260. if sym in cls.cross_checks:
  261. if int(e['trust_level']) == 0 and len(cls.cross_checks[sym]) > 1:
  262. msg(f'Upgrading trust for coin {sym!r}: {e["trust_level"]} -> 1')
  263. e['trust_level'] = 1
  264. print(fs.format(*e.values()))
  265. msg(f'Processed {len(data)} entries')
  266. @classmethod
  267. def find_addr_leading_symbol(cls, ver_num, verbose=False):
  268. if ver_num == 0:
  269. return '1'
  270. def phash2addr(ver_num, pk_hash):
  271. from mmgen.proto.btc.common import b58chk_encode
  272. bl = ver_num.bit_length()
  273. ver_bytes = int.to_bytes(ver_num, bl//8 + bool(bl%8), 'big')
  274. return b58chk_encode(ver_bytes + pk_hash)
  275. low = phash2addr(ver_num, b'\x00'*20)
  276. high = phash2addr(ver_num, b'\xff'*20)
  277. if verbose:
  278. print('low address: ' + low)
  279. print('high address: ' + high)
  280. l1, h1 = low[0], high[0]
  281. return (l1, h1) if l1 != h1 else l1
  282. @classmethod
  283. def print_symbols(cls, include_names=False, reverse=False):
  284. for e in cls.coin_constants['mainnet']:
  285. if reverse:
  286. print(f'{e.symbol:6} {e.name}')
  287. else:
  288. name_w = max(len(e.name) for e in cls.coin_constants['mainnet'])
  289. print((f'{e.name:{name_w}} ' if include_names else '') + e.symbol)
  290. @classmethod
  291. def create_trust_table(cls):
  292. tt = {}
  293. mn = cls.external_tests['mainnet']
  294. for ext_prog in mn:
  295. assert len(set(mn[ext_prog])) == len(mn[ext_prog]), f'Duplicate entry in {ext_prog!r}!'
  296. for coin in mn[ext_prog]:
  297. if coin in tt:
  298. tt[coin] += 1
  299. else:
  300. tt[coin] = 1
  301. for k in cls.trust_override:
  302. tt[k] = cls.trust_override[k]
  303. return tt
  304. trust_override = {'BTC':3, 'BCH':3, 'LTC':3, 'DASH':1, 'EMC':2}
  305. @classmethod
  306. def get_test_support(cls, coin, addr_type, network, toolname=None, verbose=False):
  307. """
  308. If requested tool supports coin/addr_type/network triplet, return tool name.
  309. If 'tool' is None, return tool that supports coin/addr_type/network triplet.
  310. Return None on failure.
  311. """
  312. all_tools = [toolname] if toolname else list(cls.external_tests[network].keys())
  313. coin = coin.upper()
  314. for tool in all_tools:
  315. if coin in cls.external_tests[network][tool]:
  316. break
  317. else:
  318. if verbose:
  319. m1 = 'Requested tool {t!r} does not support coin {c} on network {n}'
  320. m2 = 'No test tool found for coin {c} on network {n}'
  321. msg((m1 if toolname else m2).format(t=tool, c=coin, n=network))
  322. return None
  323. if addr_type == 'zcash_z':
  324. if toolname in (None, 'zcash-mini'):
  325. return 'zcash-mini'
  326. else:
  327. if verbose:
  328. msg(f"Address type {addr_type!r} supported only by tool 'zcash-mini'")
  329. return None
  330. try:
  331. bl = cls.external_tests_blacklist[addr_type][tool]
  332. except:
  333. pass
  334. else:
  335. if bl is True or coin in bl:
  336. if verbose:
  337. msg(f'Tool {tool!r} blacklisted for coin {coin}, addr_type {addr_type!r}')
  338. return None
  339. if toolname: # skip whitelists
  340. return tool
  341. if addr_type in ('segwit', 'bech32'):
  342. st = cls.external_tests_segwit_whitelist
  343. if addr_type in st and coin in st[addr_type]:
  344. return tool
  345. else:
  346. if verbose:
  347. m1 = 'Requested tool {t!r} does not support coin {c}, addr_type {a!r}, on network {n}'
  348. m2 = 'No test tool found supporting coin {c}, addr_type {a!r}, on network {n}'
  349. msg((m1 if toolname else m2).format(t=tool, c=coin, n=network, a=addr_type))
  350. return None
  351. return tool
  352. external_tests = {
  353. 'mainnet': {
  354. # List in order of preference.
  355. # If 'tool' is not specified, the first tool supporting the coin will be selected.
  356. 'pycoin': (
  357. 'DASH', # only compressed
  358. 'BCH',
  359. 'BTC', 'LTC', 'VIA', 'FTC', 'DOGE', 'MEC',
  360. 'JBS', 'MZC', 'RIC', 'DFC', 'FAI', 'ARG', 'ZEC', 'DCR'),
  361. 'keyconv': ( # broken: PIVX
  362. 'BCH', '42', 'AC', 'AIB', 'ANC', 'ARS', 'ATMOS', 'AUR', 'BLK', 'BQC', 'BTC', 'TEST',
  363. 'BTCD', 'CCC', 'CCN', 'CDN', 'CLAM', 'CNC', 'CNOTE', 'CON', 'CRW', 'DEEPONION', 'DGB',
  364. 'DGC', 'DMD', 'DOGED', 'DOGE', 'DOPE', 'DVC', 'EFL', 'EMC', 'EXCL', 'FAIR', 'FLOZ', 'FTC',
  365. 'GAME', 'GAP', 'GCR', 'GRC', 'GRS', 'GUN', 'HAM', 'HODL', 'IXC', 'JBS', 'LBRY', 'LEAF',
  366. 'LTC', 'MMC', 'MONA', 'MUE', 'MYRIAD', 'MZC', 'NEOS', 'NLG', 'NMC', 'NVC', 'NYAN', 'OK',
  367. 'OMC', 'PIGGY', 'PINK', 'PKB', 'PND', 'POT', 'PPC', 'PTC', 'PTS', 'QTUM', 'RBY', 'RDD',
  368. 'RIC', 'SCA', 'SDC', 'SKC', 'SPR', 'START', 'SXC', 'TPC', 'UIS', 'UNO', 'VIA', 'VPN',
  369. 'VTC', 'WDC', 'WKC', 'WUBS', 'XC', 'XPM', 'YAC', 'ZOOM', 'ZRC'),
  370. 'ethkey': ('ETH', 'ETC'),
  371. 'zcash-mini': ('ZEC',),
  372. 'monero-python': ('XMR',),
  373. },
  374. 'testnet': {
  375. 'pycoin': {
  376. 'DASH':'tDASH', # only compressed
  377. 'BCH':'XTN',
  378. 'BTC':'XTN', 'LTC':'XLT', 'VIA':'TVI', 'FTC':'FTX', 'DOGE':'XDT', 'DCR':'DCRT'
  379. },
  380. 'ethkey': {},
  381. 'keyconv': {}
  382. }
  383. }
  384. external_tests_segwit_whitelist = {
  385. # Whitelists apply to the *first* tool in cls.external_tests supporting the given coin/addr_type.
  386. # They're ignored if specific tool is requested.
  387. 'segwit': ('BTC',), # LTC Segwit broken on pycoin: uses old fmt
  388. 'bech32': ('BTC', 'LTC'),
  389. 'compressed': (
  390. 'BTC', 'LTC', 'VIA', 'FTC', 'DOGE', 'DASH', 'MEC', 'MYR', 'UNO',
  391. 'JBS', 'MZC', 'RIC', 'DFC', 'FAI', 'ARG', 'ZEC', 'DCR', 'ZEC'
  392. ),
  393. }
  394. external_tests_blacklist = {
  395. # Unconditionally block testing of the given coin/addr_type with given tool, or all coins if True
  396. 'legacy': {},
  397. 'segwit': {'keyconv': True},
  398. 'bech32': {'keyconv': True},
  399. }
  400. if __name__ == '__main__':
  401. opts_data = {
  402. 'text': {
  403. 'desc': 'Check altcoin data',
  404. 'usage':'[opts]',
  405. 'options': '-q, --quiet Be quieter\n-v, --verbose Be more verbose'
  406. }
  407. }
  408. cfg = Config(opts_data=opts_data, need_amt=False)
  409. msg('Checking CoinInfo WIF/P2PKH/P2SH version numbers and trust levels against protocol.py')
  410. TestCoinInfo.verify_core_coin_data(cfg, cfg.quiet, cfg.verbose)
  411. msg('Checking CoinInfo address leading symbols')
  412. TestCoinInfo.verify_leading_symbols(cfg.quiet, cfg.verbose)