ts_misc.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  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. ts_misc.py: Miscellaneous test groups for the test.py test suite
  20. """
  21. from mmgen.globalvars import g
  22. from test.common import *
  23. from test.test_py_d.common import *
  24. from test.test_py_d.ts_base import *
  25. from test.test_py_d.ts_main import TestSuiteMain
  26. from mmgen.seed import SeedSource
  27. class TestSuiteHelp(TestSuiteBase):
  28. 'help, info and usage screens'
  29. networks = ('btc','ltc','bch','eth')
  30. tmpdir_nums = []
  31. passthru_opts = ('daemon_data_dir','rpc_port','coin','testnet')
  32. cmd_group = (
  33. ('helpscreens', (1,'help screens', [])),
  34. ('longhelpscreens', (1,'help screens (--longhelp)',[])),
  35. ('opt_show_hash_presets', (1,'info screen (--show-hash-presets)',[])),
  36. ('tool_help', (1,"'mmgen-tool' usage screen",[])),
  37. ('test_help', (1,"'test.py' help screens",[])),
  38. )
  39. def helpscreens(self,
  40. arg = '--help',
  41. scripts = ( 'walletgen','walletconv','walletchk','passchg','subwalletgen',
  42. 'addrgen','keygen','passgen',
  43. 'seedsplit','seedjoin',
  44. 'txcreate','txsign','txsend','txdo','txbump',
  45. 'addrimport','tool','regtest','autosign')):
  46. for s in scripts:
  47. t = self._run_cmd('mmgen-'+s,[arg],extra_desc='(mmgen-{})'.format(s),no_output=True)
  48. return t
  49. def longhelpscreens(self):
  50. return self.helpscreens(arg='--longhelp')
  51. def opt_show_hash_presets(self):
  52. return self.helpscreens(
  53. arg = '--show-hash-presets',
  54. scripts = (
  55. 'walletgen','walletconv','walletchk','passchg','subwalletgen',
  56. 'addrgen','keygen','passgen',
  57. 'txsign','txdo','txbump'))
  58. def _run_cmd( self, cmd_name,
  59. cmd_args = [],
  60. no_msg = False,
  61. extra_desc = '',
  62. cmd_dir = 'cmds',
  63. no_output = False):
  64. t = self.spawn( cmd_name,
  65. args = cmd_args,
  66. no_msg = no_msg,
  67. extra_desc = extra_desc,
  68. cmd_dir = cmd_dir,
  69. no_output = no_output)
  70. t.read()
  71. ret = t.p.wait()
  72. if ret == 0:
  73. msg('OK')
  74. else:
  75. rdie(1,"\n'{}' returned {}".format(self.test_name,ret))
  76. t.skip_ok = True
  77. return t
  78. def tool_help(self):
  79. self._run_cmd('mmgen-tool',['help'],extra_desc="('mmgen-tool help')")
  80. return self._run_cmd('mmgen-tool',['usage'],extra_desc="('mmgen-tool usage')")
  81. def test_help(self):
  82. self._run_cmd('test.py',['-h'],cmd_dir='test')
  83. self._run_cmd('test.py',['-L'],cmd_dir='test',extra_desc='(cmd group list)')
  84. return self._run_cmd('test.py',['-l'],cmd_dir='test',extra_desc='(cmd list)')
  85. class TestSuiteOutput(TestSuiteBase):
  86. 'screen output tests'
  87. networks = ('btc',)
  88. tmpdir_nums = []
  89. cmd_group = (
  90. ('output_gr', (1,"Greek text", [])),
  91. ('output_ru', (1,"Russian text", [])),
  92. ('output_zh', (1,"Chinese text", [])),
  93. ('output_jp', (1,"Japanese text", []))
  94. )
  95. def screen_output(self,lang):
  96. t = self.spawn('test/misc/utf8_output.py',[lang],cmd_dir='.')
  97. t.read()
  98. return t
  99. def output_gr(self): return self.screen_output('gr')
  100. def output_ru(self): return self.screen_output('ru')
  101. def output_zh(self): return self.screen_output('zh')
  102. def output_jp(self): return self.screen_output('jp')
  103. class TestSuiteInput(TestSuiteBase):
  104. 'user input tests'
  105. networks = ('btc',)
  106. tmpdir_nums = []
  107. cmd_group = (
  108. ('password_entry_noecho', (1,"utf8 password entry", [])),
  109. ('password_entry_echo', (1,"utf8 password entry (echoed)", [])),
  110. ('mnemonic_entry_mmgen', (1,"stealth mnemonic entry (MMGen native)", [])),
  111. ('mnemonic_entry_bip39', (1,"stealth mnemonic entry (BIP39)", [])),
  112. ('dieroll_entry', (1,"dieroll entry (base6d)", [])),
  113. ('dieroll_entry_usrrand', (1,"dieroll entry (base6d) with added user entropy", [])),
  114. )
  115. def password_entry(self,prompt,cmd_args):
  116. t = self.spawn('test/misc/password_entry.py',cmd_args,cmd_dir='.')
  117. pw = 'abc-α'
  118. t.expect(prompt,pw)
  119. ret = t.expect_getend('Entered: ')
  120. assert ret == pw,'Password mismatch! {} != {}'.format(ret,pw)
  121. return t
  122. def password_entry_noecho(self):
  123. if self.skip_for_win():
  124. msg('Perform this test by hand on MSWin with non-ASCII password abc-α:')
  125. msg(' test/misc/password_entry.py')
  126. return 'skip' # getpass() can't handle utf8, and pexpect double-escapes utf8, so skip
  127. return self.password_entry('Enter passphrase: ',[])
  128. def password_entry_echo(self):
  129. if self.skip_for_win():
  130. msg('Perform this test by hand on MSWin with non-ASCII password abc-α:')
  131. msg(' test/misc/password_entry.py --echo-passphrase')
  132. return 'skip' # pexpect double-escapes utf8, so skip
  133. return self.password_entry('Enter passphrase (echoed): ',['--echo-passphrase'])
  134. def _user_seed_entry(self,fmt,usr_rand=False,out_fmt=None):
  135. wcls = SeedSource.fmt_code_to_type(fmt)
  136. wf = os.path.join(ref_dir,'FE3C6545.{}'.format(wcls.ext))
  137. if wcls.wclass == 'mnemonic':
  138. mn = read_from_file(wf).strip().split()
  139. mn = ['foo'] + mn[:5] + ['grac','graceful'] + mn[5:]
  140. elif wcls.wclass == 'dieroll':
  141. mn = list(read_from_file(wf).strip().translate(dict((ord(ws),None) for ws in '\t\n ')))
  142. for idx,val in ((5,'x'),(18,'0'),(30,'7'),(44,'9')):
  143. mn.insert(idx,val)
  144. t = self.spawn('mmgen-walletconv',['-r10','-S','-i',fmt,'-o',out_fmt or fmt])
  145. t.expect('{} type: {}'.format(capfirst(wcls.wclass),wcls.mn_type))
  146. t.expect(wcls.choose_seedlen_prompt,'1')
  147. t.expect('(Y/n): ','y')
  148. if wcls.wclass == 'mnemonic':
  149. stealth_mnemonic_entry(t,mn,fmt=fmt)
  150. elif wcls.wclass == 'dieroll':
  151. user_dieroll_entry(t,mn)
  152. if usr_rand:
  153. t.expect(wcls.user_entropy_prompt,'y')
  154. t.usr_rand(10)
  155. else:
  156. t.expect(wcls.user_entropy_prompt,'n')
  157. if not usr_rand:
  158. sid_chk = 'FE3C6545'
  159. sid = t.expect_getend('Valid {} for Seed ID '.format(wcls.desc))[:8]
  160. assert sid == sid_chk,'Seed ID mismatch! {} != {}'.format(sid,sid_chk)
  161. t.expect('to confirm: ','YES\n')
  162. t.read()
  163. return t
  164. def mnemonic_entry_mmgen(self): return self._user_seed_entry('words')
  165. def mnemonic_entry_bip39(self): return self._user_seed_entry('bip39')
  166. def dieroll_entry(self): return self._user_seed_entry('dieroll')
  167. def dieroll_entry_usrrand(self):return self._user_seed_entry('dieroll',usr_rand=True,out_fmt='bip39')
  168. class TestSuiteTool(TestSuiteMain,TestSuiteBase):
  169. "tests for interactive 'mmgen-tool' commands"
  170. networks = ('btc',)
  171. segwit_opts_ok = False
  172. tmpdir_nums = [9]
  173. enc_infn = 'tool_encrypt.in'
  174. cmd_group = (
  175. ('tool_find_incog_data', (9,"'mmgen-tool find_incog_data'", [[[hincog_fn],1],[[incog_id_fn],1]])),
  176. ('tool_rand2file', (9,"'mmgen-tool rand2file'", [])),
  177. ('tool_encrypt', (9,"'mmgen-tool encrypt' (random data)", [])),
  178. ('tool_decrypt', (9,"'mmgen-tool decrypt' (random data)", [[[enc_infn+'.mmenc'],9]])),
  179. ('tool_twview_bad_comment',(9,"'mmgen-tool twview' (with bad comment)", [])),
  180. # ('tool_encrypt_ref', (9,"'mmgen-tool encrypt' (reference text)", [])),
  181. )
  182. def tool_rand2file(self):
  183. outfile = os.path.join(self.tmpdir,'rand2file.out')
  184. from mmgen.tool import MMGenToolCmd
  185. tu = MMGenToolCmd()
  186. for nbytes in ('1','1023','1K','1048575','1M','1048577','123M'):
  187. t = self.spawn( 'mmgen-tool',
  188. ['-d',self.tmpdir,'-r0','rand2file','rand2file.out',nbytes],
  189. extra_desc='({} byte{})'.format(nbytes,suf(tu.bytespec(nbytes)))
  190. )
  191. t.expect('random data written to file')
  192. t.read()
  193. t.p.wait()
  194. t.ok()
  195. t.skip_ok = True
  196. return t
  197. def tool_encrypt(self):
  198. infile = joinpath(self.tmpdir,self.enc_infn)
  199. write_to_file(infile,os.urandom(1033),binary=True)
  200. t = self.spawn('mmgen-tool',['-d',self.tmpdir,self.usr_rand_arg,'encrypt',infile])
  201. t.usr_rand(self.usr_rand_chars)
  202. t.hash_preset('user data','1')
  203. t.passphrase_new('user data',tool_enc_passwd)
  204. t.written_to_file('Encrypted data')
  205. return t
  206. def tool_decrypt(self,f1):
  207. out_fn = 'tool_encrypt.out'
  208. t = self.spawn('mmgen-tool',['-d',self.tmpdir,'decrypt',f1,'outfile='+out_fn,'hash_preset=1'])
  209. t.passphrase('user data',tool_enc_passwd)
  210. t.written_to_file('Decrypted data')
  211. d1 = self.read_from_tmpfile(self.enc_infn,binary=True)
  212. d2 = self.read_from_tmpfile(out_fn,binary=True)
  213. cmp_or_die(d1,d2)
  214. return t
  215. def tool_find_incog_data(self,f1,f2):
  216. i_id = read_from_file(f2).rstrip()
  217. vmsg('Incog ID: {}'.format(cyan(i_id)))
  218. t = self.spawn('mmgen-tool',['-d',self.tmpdir,'find_incog_data',f1,i_id])
  219. o = t.expect_getend('Incog data for ID {} found at offset '.format(i_id))
  220. if not g.platform == 'win':
  221. os.unlink(f1) # causes problems with MSYS2
  222. cmp_or_die(hincog_offset,int(o))
  223. return t
  224. def tool_twview_bad_comment(self): # test correct operation of get_tw_label()
  225. bw_save = os.getenv('MMGEN_BOGUS_WALLET_DATA')
  226. os.environ['MMGEN_BOGUS_WALLET_DATA'] = joinpath(ref_dir,'bad-comment-unspent.json')
  227. t = self.spawn('mmgen-tool',['twview'])
  228. if bw_save:
  229. os.environ['MMGEN_BOGUS_WALLET_DATA'] = bw_save
  230. t.read()
  231. t.req_exit_val = 2
  232. return t
  233. class TestSuiteRefTX(TestSuiteMain,TestSuiteBase):
  234. 'create a reference transaction file (administrative command)'
  235. segwit_opts_ok = False
  236. passthru_opts = ('daemon_data_dir','rpc_port','coin','testnet')
  237. tmpdir_nums = [31,32,33,34]
  238. cmd_group = (
  239. ('ref_tx_addrgen1', (31,'address generation (legacy)', [[[],1]])),
  240. ('ref_tx_addrgen2', (32,'address generation (compressed)', [[[],1]])),
  241. ('ref_tx_addrgen3', (33,'address generation (segwit)', [[[],1]])),
  242. ('ref_tx_addrgen4', (34,'address generation (bech32)', [[[],1]])),
  243. ('ref_tx_txcreate', (31,'transaction creation',
  244. ([['addrs'],31],[['addrs'],32],[['addrs'],33],[['addrs'],34]))),
  245. )
  246. def __init__(self,trunner,cfgs,spawn):
  247. if cfgs:
  248. for n in self.tmpdir_nums:
  249. cfgs[str(n)].update({ 'addr_idx_list': '1-2',
  250. 'segwit': n in (33,34),
  251. 'dep_generators': { 'addrs':'ref_tx_addrgen'+str(n)[-1] }})
  252. return TestSuiteMain.__init__(self,trunner,cfgs,spawn)
  253. def ref_tx_addrgen(self,atype):
  254. if atype not in g.proto.mmtypes: return
  255. t = self.spawn('mmgen-addrgen',['--outdir='+self.tmpdir,'--type='+atype,dfl_words_file,'1-2'])
  256. t.read()
  257. return t
  258. def ref_tx_addrgen1(self): return self.ref_tx_addrgen(atype='L')
  259. def ref_tx_addrgen2(self): return self.ref_tx_addrgen(atype='C')
  260. def ref_tx_addrgen3(self): return self.ref_tx_addrgen(atype='S')
  261. def ref_tx_addrgen4(self): return self.ref_tx_addrgen(atype='B')
  262. def ref_tx_txcreate(self,f1,f2,f3,f4):
  263. sources = ['31','32']
  264. if 'S' in g.proto.mmtypes: sources += ['33']
  265. if 'B' in g.proto.mmtypes: sources += ['34']
  266. return self.txcreate_common(
  267. addrs_per_wallet = 2,
  268. sources = sources,
  269. add_args = ['--locktime=1320969600'],
  270. do_label = True )