gentest.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2020 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. test/gentest.py: Cryptocoin key/address generation tests for the MMGen suite
  20. """
  21. import sys,os
  22. pn = os.path.dirname(sys.argv[0])
  23. os.chdir(os.path.join(pn,os.pardir))
  24. sys.path.__setitem__(0,os.path.abspath(os.curdir))
  25. os.environ['MMGEN_TEST_SUITE'] = '1'
  26. # Import these _after_ local path's been added to sys.path
  27. from mmgen.common import *
  28. rounds = 100
  29. opts_data = {
  30. 'text': {
  31. 'desc': 'Test key/address generation of the MMGen suite in various ways',
  32. 'usage':'[options] [spec] [rounds | dump file]',
  33. 'options': """
  34. -h, --help Print this help message
  35. -a, --all Test all coins supported by specified external tool
  36. -k, --use-internal-keccak-module Force use of the internal keccak module
  37. --, --longhelp Print help message for long options (common options)
  38. -q, --quiet Produce quieter output
  39. -t, --type=t Specify address type (e.g. 'compressed','segwit','zcash_z','bech32')
  40. -v, --verbose Produce more verbose output
  41. """,
  42. 'notes': """
  43. TEST TYPES:
  44. A/B: {prog} A:B [rounds] (compare key generators A and B)
  45. Speed: {prog} A [rounds] (test speed of key generator A)
  46. Compare: {prog} A <dump file> (compare generator A to wallet dump)
  47. where A and B are one of:
  48. '1' - native Python ECDSA library (slow), or
  49. '2' - bitcoincore.org's libsecp256k1 library (default);
  50. or:
  51. B is name of an external tool (see below) or 'ext'.
  52. If B is 'ext', the external tool will be chosen automatically.
  53. EXAMPLES:
  54. Compare addresses generated by native Python ECDSA library and libsecp256k1,
  55. 100 rounds:
  56. $ {prog} 1:2 100
  57. Compare mmgen-secp256k1 Segwit address generation to pycoin library for all
  58. supported coins, 100 rounds:
  59. $ {prog} --all --type=segwit 2:pycoin 100
  60. Compare mmgen-secp256k1 address generation to keyconv tool for all
  61. supported coins, 100 rounds:
  62. $ {prog} --all --type=compressed 2:keyconv 100
  63. Compare mmgen-secp256k1 XMR address generation to configured external tool,
  64. 10 rounds:
  65. $ {prog} --coin=xmr 2:ext 10
  66. Test speed of mmgen-secp256k1 address generation, 10,000 rounds:
  67. $ {prog} 2 10000
  68. Compare mmgen-secp256k1-generated bech32 addrs to {dn} wallet dump:
  69. $ {prog} --type=bech32 2 bech32wallet.dump
  70. Supported external tools:
  71. + ethkey (for ETH,ETC)
  72. https://github.com/paritytech/parity-ethereum
  73. (build with 'cargo build -p ethkey-cli --release')
  74. + zcash-mini (for Zcash Z-addresses)
  75. https://github.com/FiloSottile/zcash-mini
  76. + moneropy (for Monero addresses)
  77. https://github.com/bigreddmachine/MoneroPy
  78. + pycoin (for supported coins)
  79. https://github.com/richardkiss/pycoin
  80. + keyconv (for supported coins)
  81. https://github.com/exploitagency/vanitygen-plus
  82. ('keyconv' does not generate Segwit addresses)
  83. """
  84. },
  85. 'code': {
  86. 'notes': lambda s: s.format(
  87. prog='test/gentest.py',
  88. pnm=g.proj_name,
  89. snum=rounds,
  90. dn=proto.daemon_name)
  91. }
  92. }
  93. sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
  94. cmd_args = opts.init(opts_data,add_opts=['exact_output','use_old_ed25519'])
  95. if not 1 <= len(cmd_args) <= 2:
  96. opts.usage()
  97. from mmgen.protocol import init_proto_from_opts
  98. proto = init_proto_from_opts()
  99. from subprocess import run,PIPE,DEVNULL
  100. def get_cmd_output(cmd,input=None):
  101. return run(cmd,input=input,stdout=PIPE,stderr=DEVNULL).stdout.decode().splitlines()
  102. from collections import namedtuple
  103. gtr = namedtuple('gen_tool_result',['wif','addr','vk'])
  104. class GenTool(object):
  105. def run_tool(self,sec):
  106. vcoin = 'BTC' if proto.coin == 'BCH' else proto.coin
  107. return self.run(sec,vcoin)
  108. class GenToolEthkey(GenTool):
  109. desc = 'ethkey'
  110. def __init__(self):
  111. proto = init_proto('eth')
  112. global addr_type
  113. addr_type = MMGenAddrType(proto,'E')
  114. def run(self,sec,vcoin):
  115. o = get_cmd_output(['ethkey','info',sec])
  116. return gtr(o[0].split()[1],o[-1].split()[1],None)
  117. class GenToolKeyconv(GenTool):
  118. desc = 'keyconv'
  119. def run(self,sec,vcoin):
  120. o = get_cmd_output(['keyconv','-C',vcoin,sec.wif])
  121. return gtr(o[1].split()[1],o[0].split()[1],None)
  122. class GenToolZcash_mini(GenTool):
  123. desc = 'zcash-mini'
  124. def __init__(self):
  125. proto = init_proto('zec')
  126. global addr_type
  127. addr_type = MMGenAddrType(proto,'Z')
  128. def run(self,sec,vcoin):
  129. o = get_cmd_output(['zcash-mini','-key','-simple'],input=(sec.wif+'\n').encode())
  130. return gtr(o[1],o[0],o[-1])
  131. class GenToolPycoin(GenTool):
  132. """
  133. pycoin/networks/all.py pycoin/networks/legacy_networks.py
  134. """
  135. desc = 'pycoin'
  136. def __init__(self):
  137. m = "Unable to import pycoin.networks.registry. Is pycoin installed on your system?"
  138. try:
  139. from pycoin.networks.registry import network_for_netcode
  140. except:
  141. raise ImportError(m)
  142. self.nfnc = network_for_netcode
  143. def run(self,sec,vcoin):
  144. if proto.testnet:
  145. vcoin = ci.external_tests['testnet']['pycoin'][vcoin]
  146. network = self.nfnc(vcoin)
  147. key = network.keys.private(secret_exponent=int(sec,16),is_compressed=addr_type.name != 'legacy')
  148. if key is None:
  149. die(1,"can't parse {}".format(sec))
  150. if addr_type.name in ('segwit','bech32'):
  151. hash160_c = key.hash160(is_compressed=True)
  152. if addr_type.name == 'segwit':
  153. p2sh_script = network.contract.for_p2pkh_wit(hash160_c)
  154. addr = network.address.for_p2s(p2sh_script)
  155. else:
  156. addr = network.address.for_p2pkh_wit(hash160_c)
  157. else:
  158. addr = key.address()
  159. return gtr(key.wif(),addr,None)
  160. class GenToolMoneropy(GenTool):
  161. desc = 'moneropy'
  162. def __init__(self):
  163. m = "Unable to import moneropy. Is moneropy installed on your system?"
  164. try:
  165. import moneropy.account
  166. except:
  167. raise ImportError(m)
  168. self.mpa = moneropy.account
  169. proto = init_proto('xmr')
  170. global addr_type
  171. addr_type = MMGenAddrType(proto,'M')
  172. def run(self,sec,vcoin):
  173. sk_t,vk_t,addr_t = self.mpa.account_from_spend_key(sec) # VERY slow!
  174. return gtr(sk_t,addr_t,vk_t)
  175. def get_tool(arg):
  176. if arg not in ext_progs + ['ext']:
  177. die(1,'{!r}: unsupported tool for network {}'.format(arg,proto.network))
  178. if opt.all:
  179. if arg == 'ext':
  180. die(1,"'--all' must be combined with a specific external testing tool")
  181. return arg
  182. else:
  183. tool = ci.get_test_support(
  184. proto.coin,
  185. addr_type.name,
  186. proto.network,
  187. verbose = not opt.quiet,
  188. tool = arg if arg in ext_progs else None )
  189. if not tool:
  190. sys.exit(2)
  191. if arg in ext_progs and arg != tool:
  192. sys.exit(3)
  193. return tool
  194. def test_equal(desc,a_val,b_val,in_bytes,sec,wif,a_desc,b_desc):
  195. if a_val != b_val:
  196. fs = """
  197. {i:{w}}: {}
  198. {s:{w}}: {}
  199. {W:{w}}: {}
  200. {a:{w}}: {}
  201. {b:{w}}: {}
  202. """
  203. die(3,
  204. red('\nERROR: {} do not match!').format(desc)
  205. + fs.format(
  206. in_bytes.hex(), sec, wif, a_val, b_val,
  207. i='input', s='sec key', W='WIF key', a=a_desc, b=b_desc,
  208. w=max(len(e) for e in (a_desc,b_desc)) + 1
  209. ).rstrip())
  210. def gentool_test(kg_a,kg_b,ag,rounds):
  211. m = "Comparing address generators '{A}' and '{B}' for {N} {c} ({n}), addrtype {a!r}"
  212. e = ci.get_entry(proto.coin,proto.network)
  213. qmsg(green(m.format(
  214. A = kg_a.desc,
  215. B = kg_b.desc,
  216. N = proto.network,
  217. c = proto.coin,
  218. n = e.name if e else '---',
  219. a = addr_type.name )))
  220. global last_t
  221. last_t = time.time()
  222. def do_compare_test(n,trounds,in_bytes):
  223. global last_t
  224. if opt.verbose or time.time() - last_t >= 0.1:
  225. qmsg_r('\rRound {}/{} '.format(i+1,trounds))
  226. last_t = time.time()
  227. sec = PrivKey(proto,in_bytes,compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type)
  228. a_ph = kg_a.to_pubhex(sec)
  229. a_addr = ag.to_addr(a_ph)
  230. a_vk = None
  231. tinfo = (in_bytes,sec,sec.wif,kg_a.desc,kg_b.desc)
  232. if isinstance(kg_b,GenTool):
  233. b = kg_b.run_tool(sec)
  234. test_equal('WIF keys',sec.wif,b.wif,*tinfo)
  235. test_equal('addresses',a_addr,b.addr,*tinfo)
  236. if b.vk:
  237. a_vk = ag.to_viewkey(a_ph)
  238. test_equal('view keys',a_vk,b.vk,*tinfo)
  239. else:
  240. b_addr = ag.to_addr(kg_b.to_pubhex(sec))
  241. test_equal('addresses',a_addr,b_addr,*tinfo)
  242. vmsg(fs.format(b=in_bytes.hex(),k=sec.wif,v=a_vk,a=a_addr))
  243. qmsg_r('\rRound {}/{} '.format(n+1,trounds))
  244. fs = ( '\ninput: {b}\n%-9s {k}\naddr: {a}\n',
  245. '\ninput: {b}\n%-9s {k}\nviewkey: {v}\naddr: {a}\n')[
  246. 'viewkey' in addr_type.extra_attrs] % (addr_type.wif_label + ':')
  247. # test some important private key edge cases:
  248. edgecase_sks = (
  249. bytes([0x00]*31 + [0x01]), # min
  250. bytes([0xff]*32), # max
  251. bytes([0x0f] + [0xff]*31), # same key as above for zcash-z
  252. bytes([0x00]*31 + [0xff]), # monero will reduce
  253. bytes([0xff]*31 + [0x0f]), # monero will not reduce
  254. )
  255. qmsg(purple('edge cases:'))
  256. for i,in_bytes in enumerate(edgecase_sks):
  257. do_compare_test(i,len(edgecase_sks),in_bytes)
  258. qmsg(green('\rOK ' if opt.verbose else 'OK'))
  259. qmsg(purple('random input:'))
  260. for i in range(rounds):
  261. do_compare_test(i,rounds,os.urandom(32))
  262. qmsg(green('\rOK ' if opt.verbose else 'OK'))
  263. def speed_test(kg,ag,rounds):
  264. m = "Testing speed of address generator '{}' for coin {}"
  265. qmsg(green(m.format(kg.desc,proto.coin)))
  266. from struct import pack,unpack
  267. seed = os.urandom(28)
  268. qmsg('Incrementing key with each round')
  269. qmsg('Starting key: {}'.format((seed + pack('I',0)).hex()))
  270. import time
  271. start = last_t = time.time()
  272. for i in range(rounds):
  273. if time.time() - last_t >= 0.1:
  274. qmsg_r('\rRound {}/{} '.format(i+1,rounds))
  275. last_t = time.time()
  276. sec = PrivKey(proto,seed+pack('I',i),compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type)
  277. addr = ag.to_addr(kg.to_pubhex(sec))
  278. vmsg('\nkey: {}\naddr: {}\n'.format(sec.wif,addr))
  279. qmsg_r('\rRound {}/{} '.format(i+1,rounds))
  280. qmsg('\n{} addresses generated in {:.2f} seconds'.format(rounds,time.time()-start))
  281. def dump_test(kg,ag,fh):
  282. dump = [[*(e.split()[0] for e in line.split('addr='))] for line in fh.readlines() if 'addr=' in line]
  283. if not dump:
  284. die(1,'File {!r} appears not to be a wallet dump'.format(fh.name))
  285. m = 'Comparing output of address generator {!r} against wallet dump {!r}'
  286. qmsg(green(m.format(kg.desc,fh.name)))
  287. for count,(b_wif,b_addr) in enumerate(dump,1):
  288. qmsg_r('\rKey {}/{} '.format(count,len(dump)))
  289. try:
  290. b_sec = PrivKey(proto,wif=b_wif)
  291. except:
  292. die(2,'\nInvalid {} WIF address in dump file: {}'.format(proto.network,b_wif))
  293. a_addr = ag.to_addr(kg.to_pubhex(b_sec))
  294. vmsg('\nwif: {}\naddr: {}\n'.format(b_wif,b_addr))
  295. tinfo = (bytes.fromhex(b_sec),b_sec,b_wif,kg.desc,fh.name)
  296. test_equal('addresses',a_addr,b_addr,*tinfo)
  297. qmsg(green(('\n','')[bool(opt.verbose)] + 'OK'))
  298. def init_tool(tname):
  299. return globals()['GenTool'+capfirst(tname.replace('-','_'))]()
  300. def parse_arg1(arg,arg_id):
  301. m1 = 'First argument must be a numeric generator ID or two colon-separated generator IDs'
  302. m2 = 'Second part of first argument must be a numeric generator ID or one of {}'
  303. def check_gen_num(n):
  304. if not (1 <= int(n) <= len(g.key_generators)):
  305. die(1,'{}: invalid generator ID'.format(n))
  306. return int(n)
  307. if arg_id == 'a':
  308. if is_int(arg):
  309. a_num = check_gen_num(arg)
  310. return (KeyGenerator(proto,addr_type,a_num),a_num)
  311. else:
  312. die(1,m1)
  313. elif arg_id == 'b':
  314. if is_int(arg):
  315. return KeyGenerator(proto,addr_type,check_gen_num(arg))
  316. elif arg in ext_progs + ['ext']:
  317. return init_tool(get_tool(arg))
  318. else:
  319. die(1,m2.format(ext_progs))
  320. def parse_arg2():
  321. m = 'Second argument must be dump filename or integer rounds specification'
  322. if len(cmd_args) == 1:
  323. return None
  324. arg = cmd_args[1]
  325. if is_int(arg) and int(arg) > 0:
  326. return int(arg)
  327. try:
  328. return open(arg)
  329. except:
  330. die(1,m)
  331. # begin execution
  332. from mmgen.protocol import init_proto
  333. from mmgen.altcoin import CoinInfo as ci
  334. from mmgen.obj import MMGenAddrType,PrivKey
  335. from mmgen.addr import KeyGenerator,AddrGenerator
  336. addr_type = MMGenAddrType(
  337. proto = proto,
  338. id_str = opt.type or proto.dfl_mmtype )
  339. ext_progs = list(ci.external_tests[proto.network])
  340. arg1 = cmd_args[0].split(':')
  341. if len(arg1) == 1:
  342. a,a_num = parse_arg1(arg1[0],'a')
  343. b = None
  344. elif len(arg1) == 2:
  345. a,a_num = parse_arg1(arg1[0],'a')
  346. b = parse_arg1(arg1[1],'b')
  347. else:
  348. opts.usage()
  349. if type(a) == type(b):
  350. die(1,'Address generators are the same!')
  351. arg2 = parse_arg2()
  352. if not opt.all:
  353. ag = AddrGenerator(proto,addr_type)
  354. if not b and type(arg2) == int:
  355. speed_test(a,ag,arg2)
  356. elif not b and hasattr(arg2,'read'):
  357. dump_test(a,ag,arg2)
  358. elif a and b and type(arg2) == int:
  359. if opt.all:
  360. from mmgen.protocol import CoinProtocol,init_genonly_altcoins
  361. init_genonly_altcoins(testnet=proto.testnet)
  362. for coin in ci.external_tests[proto.network][b.desc]:
  363. if coin.lower() not in CoinProtocol.coins:
  364. # ymsg('Coin {} not configured'.format(coin))
  365. continue
  366. proto = init_proto(coin)
  367. if addr_type not in proto.mmtypes:
  368. continue
  369. # proto has changed, so reinit kg and ag
  370. a = KeyGenerator(proto,addr_type,a_num)
  371. ag = AddrGenerator(proto,addr_type)
  372. b_chk = ci.get_test_support(proto.coin,addr_type.name,proto.network,tool=b.desc,verbose=not opt.quiet)
  373. if b_chk == b.desc:
  374. gentool_test(a,b,ag,arg2)
  375. else:
  376. gentool_test(a,b,ag,arg2)
  377. else:
  378. opts.usage()