2018-10-30 16:23:12 +00:00
|
|
|
#!/usr/bin/env python3
|
2015-01-09 21:02:16 +03:00
|
|
|
#
|
|
|
|
|
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
2021-02-19 20:09:06 +03:00
|
|
|
# Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
|
2015-01-09 21:02:16 +03:00
|
|
|
#
|
|
|
|
|
# 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/>.
|
|
|
|
|
|
|
|
|
|
"""
|
2019-03-06 20:58:59 +00:00
|
|
|
common.py: Shared routines and data for the MMGen test suites
|
2015-01-09 21:02:16 +03:00
|
|
|
"""
|
|
|
|
|
|
2019-03-02 18:27:53 +00:00
|
|
|
class TestSuiteException(Exception): pass
|
|
|
|
|
class TestSuiteFatalException(Exception): pass
|
|
|
|
|
|
2020-02-25 16:45:27 +00:00
|
|
|
import os
|
2016-02-28 16:41:43 +03:00
|
|
|
from mmgen.common import *
|
2019-10-16 16:39:04 +00:00
|
|
|
from mmgen.devtools import *
|
2015-01-09 21:02:16 +03:00
|
|
|
|
2020-06-03 12:29:50 +00:00
|
|
|
def strip_ansi_escapes(s):
|
|
|
|
|
import re
|
|
|
|
|
return re.sub('\x1b\[[;0-9]+?m','',s)
|
|
|
|
|
|
2019-05-28 14:49:44 +00:00
|
|
|
ascii_uc = ''.join(map(chr,list(range(65,91)))) # 26 chars
|
|
|
|
|
ascii_lc = ''.join(map(chr,list(range(97,123)))) # 26 chars
|
2019-06-12 14:21:28 +00:00
|
|
|
lat_accent = ''.join(map(chr,list(range(192,383)))) # 191 chars, L,S
|
2019-05-28 14:49:44 +00:00
|
|
|
ru_uc = ''.join(map(chr,list(range(1040,1072)))) # 32 chars
|
|
|
|
|
gr_uc = ''.join(map(chr,list(range(913,930)) + list(range(931,940)))) # 26 chars (930 is ctrl char)
|
2019-06-12 14:21:28 +00:00
|
|
|
gr_uc_w_ctrl = ''.join(map(chr,list(range(913,940)))) # 27 chars, L,C
|
2019-05-28 14:49:44 +00:00
|
|
|
lat_cyr_gr = lat_accent[:130:5] + ru_uc + gr_uc # 84 chars
|
2019-05-29 09:05:05 +00:00
|
|
|
ascii_cyr_gr = ascii_uc + ru_uc + gr_uc # 84 chars
|
2019-05-28 14:49:44 +00:00
|
|
|
|
2019-06-12 14:21:28 +00:00
|
|
|
utf8_text = '[α-$ample UTF-8 text-ω]' * 10 # 230 chars, L,N,P,S,Z
|
2019-05-28 14:49:44 +00:00
|
|
|
utf8_combining = '[α-$ámple UTF-8 téxt-ω]' * 10 # L,N,P,S,Z,M
|
|
|
|
|
utf8_ctrl = '[α-$ample\nUTF-8\ntext-ω]' * 10 # L,N,P,S,Z,C
|
|
|
|
|
|
|
|
|
|
text_jp = '必要なのは、信用ではなく暗号化された証明に基づく電子取引システムであり、これにより希望する二者が信用できる第三者機関を介さずに直接取引できるよう' # 72 chars ('W'ide)
|
|
|
|
|
text_zh = '所以,我們非常需要這樣一種電子支付系統,它基於密碼學原理而不基於信用,使得任何達成一致的雙方,能夠直接進行支付,從而不需要協力廠商仲介的參與。。' # 72 chars ('F'ull + 'W'ide)
|
|
|
|
|
|
|
|
|
|
sample_text = 'The Times 03/Jan/2009 Chancellor on brink of second bailout for banks'
|
new mnemonic entry modes, new 'mn2hex_interactive' tool command
Auto-completion functionality for seed phrase entry provides real benefit to the
user, reducing the number of keystrokes required and permitting quick re-entry
of mistyped words. In addition, unifying the number of keystrokes among words
improves security against acoustic side-channel attacks. To this end, three
new interactive mnemonic entry modes are introduced by this patch.
Each entry mode is optimized for a particular wordlist. The “short” mode, for
example, takes advantage of the fact that each word in the Monero wordlist is
uniquely identifiable by its first three letters. For MMGen’s default Electrum
wordlist, which lacks this unique substring property, the “minimal” mode was
developed to reduce keystrokes to a minimum while retaining the option of
obfuscating entry with pad characters.
Users who prefer not to use auto-completion may specify the “full” mode, which
emulates the previous default behavior.
Overview of the key entry modes:
- 'full' (all wordlists): words are typed in full and entered with the ENTER
or SPACE key, or by exceeding the pad character limit (see below).
- 'short' (BIP39, Monero): words are entered automatically once user types
UNIQ_SS_LEN (see below) valid word letters. 3-letter words in the BIP39
wordlist must be entered with the ENTER or SPACE key, or by exceeding the
pad character limit.
- 'fixed' (BIP39, Electrum): words are entered automatically once user types
UNIQ_SS_LEN characters in total. Words shorter than UNIQ_SS_LEN must be
padded to fit. Thus the total number of characters entered is the same for
all words.
- 'minimal' (Electrum): words are entered automatically once user types the
minimum number of characters required to uniquely identify a word (varies
from word to word). Words that are substrings of other words in the wordlist
must be entered with the ENTER or SPACE key, or by exceeding the pad
character limit. This is the only mode that checks user input letter by
letter.
Pad character limits by mode:
-----------------------------
short: 16
minimal: 16
full: longest_word - word_len
fixed: uniq_ss_len - word_len
Wordlist parameters:
--------------------
Parameter Electrum BIP39 XMRSEED
--------- -------- ----- -------
uniq_ss_len: 10 4 3
shortest_word: 3 3 4
longest word: 12 8 12
optimum mode: minimal fixed short
Default modes for each wordlist may be configured in 'mmgen.cfg' via the
'mnemonic_entry_modes' option.
Usage / testing:
$ mmgen-walletconv -i words
$ mmgen-walletconv -i bip39
$ mmgen-tool mn2hex_interactive fmt=mmgen mn_len=12 print_mn=1
$ mmgen-tool mn2hex_interactive fmt=bip39
$ mmgen-tool mn2hex_interactive fmt=xmrseed
$ test/unit_tests.py mn_entry
$ test/test.py -e input
2020-03-12 17:12:43 +00:00
|
|
|
sample_mn = {
|
|
|
|
|
'mmgen': { # 'able': 0, 'youth': 1625, 'after' == 'afternoon'[:5]
|
|
|
|
|
'mn': 'able cast forgive master funny gaze after afternoon million paint moral youth',
|
|
|
|
|
'hex': '0005685ab4e94cbe3b228cf92112bc5f',
|
|
|
|
|
},
|
|
|
|
|
'bip39': { # len('sun') < uniq_ss_len
|
|
|
|
|
'mn': 'vessel ladder alter error federal sibling chat ability sun glass valve picture',
|
|
|
|
|
'hex': 'f30f8c1da665478f49b001d94c5fc452',
|
|
|
|
|
},
|
|
|
|
|
'xmrseed': {
|
|
|
|
|
'mn': 'viewpoint donuts ardent template unveil agile meant unafraid urgent athlete rustled mime azure jaded hawk baby jagged haystack baby jagged haystack ramped oncoming point template',
|
|
|
|
|
'hex': 'e8164dda6d42bd1e261a3406b2038dcbddadbeefdeadbeefdeadbeefdeadbe0f',
|
|
|
|
|
},
|
|
|
|
|
}
|
2019-05-28 14:49:44 +00:00
|
|
|
|
|
|
|
|
ref_kafile_pass = 'kafile password'
|
|
|
|
|
ref_kafile_hash_preset = '1'
|
|
|
|
|
|
2021-10-02 17:54:12 +00:00
|
|
|
def getrand(n):
|
2021-10-03 17:40:03 +00:00
|
|
|
if g.test_suite_deterministic:
|
|
|
|
|
from mmgen.crypto import fake_urandom
|
|
|
|
|
return fake_urandom(n)
|
|
|
|
|
else:
|
|
|
|
|
return os.urandom(n)
|
2021-09-29 21:17:56 +00:00
|
|
|
|
|
|
|
|
def getrandnum(n):
|
2021-10-02 17:54:12 +00:00
|
|
|
return int(getrand(n).hex(),16)
|
2021-09-29 21:17:56 +00:00
|
|
|
|
|
|
|
|
def getrandhex(n):
|
2021-10-02 17:54:12 +00:00
|
|
|
return getrand(n).hex()
|
2021-09-29 21:17:56 +00:00
|
|
|
|
2019-03-02 18:27:53 +00:00
|
|
|
def getrandnum_range(nbytes,rn_max):
|
|
|
|
|
while True:
|
2021-10-02 17:54:12 +00:00
|
|
|
rn = int(getrand(nbytes).hex(),16)
|
2021-09-29 21:17:56 +00:00
|
|
|
if rn < rn_max:
|
|
|
|
|
return rn
|
2019-03-02 18:27:53 +00:00
|
|
|
|
|
|
|
|
def getrandstr(num_chars,no_space=False):
|
2021-10-02 17:54:11 +00:00
|
|
|
n,m = (94,33) if no_space else (95,32)
|
2021-10-02 17:54:12 +00:00
|
|
|
return ''.join( chr(i % n + m) for i in list(getrand(num_chars)) )
|
2019-03-02 18:27:53 +00:00
|
|
|
|
2020-05-28 09:53:34 +00:00
|
|
|
def get_data_dir():
|
|
|
|
|
return os.path.join('test','data_dir' + ('','-α')[bool(os.getenv('MMGEN_DEBUG_UTF8'))])
|
|
|
|
|
|
2018-05-12 15:26:54 +00:00
|
|
|
# Windows uses non-UTF8 encodings in filesystem, so use raw bytes here
|
2019-03-02 18:27:53 +00:00
|
|
|
def cleandir(d,do_msg=False):
|
2018-10-31 14:16:00 +00:00
|
|
|
d_enc = d.encode()
|
2018-05-12 15:26:54 +00:00
|
|
|
|
|
|
|
|
try: files = os.listdir(d_enc)
|
2015-01-09 21:02:16 +03:00
|
|
|
except: return
|
|
|
|
|
|
2018-05-12 15:26:54 +00:00
|
|
|
from shutil import rmtree
|
2021-09-29 21:17:57 +00:00
|
|
|
if do_msg:
|
|
|
|
|
gmsg(f'Cleaning directory {d!r}')
|
2015-01-09 21:02:16 +03:00
|
|
|
for f in files:
|
2016-12-08 18:02:28 +03:00
|
|
|
try:
|
2018-05-12 15:26:54 +00:00
|
|
|
os.unlink(os.path.join(d_enc,f))
|
2016-12-08 18:02:28 +03:00
|
|
|
except:
|
2019-05-20 15:44:30 +00:00
|
|
|
rmtree(os.path.join(d_enc,f),ignore_errors=True)
|
2015-01-09 21:02:16 +03:00
|
|
|
|
2016-11-21 19:59:03 +03:00
|
|
|
def mk_tmpdir(d):
|
2018-10-30 16:31:14 +00:00
|
|
|
try: os.mkdir(d,0o755)
|
2015-01-09 21:02:16 +03:00
|
|
|
except OSError as e:
|
|
|
|
|
if e.errno != 17: raise
|
2017-07-27 22:55:52 +03:00
|
|
|
else:
|
2021-09-29 21:17:57 +00:00
|
|
|
vmsg(f'Created directory {d!r}')
|
2016-06-25 18:27:45 +03:00
|
|
|
|
2019-03-02 18:27:53 +00:00
|
|
|
def get_tmpfile(cfg,fn):
|
2015-01-09 21:02:16 +03:00
|
|
|
return os.path.join(cfg['tmpdir'],fn)
|
|
|
|
|
|
2019-03-02 18:27:53 +00:00
|
|
|
def write_to_file(fn,data,binary=False):
|
|
|
|
|
write_data_to_file( fn,
|
|
|
|
|
data,
|
2019-03-20 17:10:46 +00:00
|
|
|
quiet = True,
|
2019-03-02 18:27:53 +00:00
|
|
|
binary = binary,
|
|
|
|
|
ignore_opt_outdir = True )
|
|
|
|
|
|
2015-10-25 13:04:30 +03:00
|
|
|
def write_to_tmpfile(cfg,fn,data,binary=False):
|
2019-03-02 18:27:53 +00:00
|
|
|
write_to_file( os.path.join(cfg['tmpdir'],fn), data=data, binary=binary )
|
2015-01-09 21:02:16 +03:00
|
|
|
|
2015-10-25 13:04:30 +03:00
|
|
|
def read_from_file(fn,binary=False):
|
2015-01-09 21:02:16 +03:00
|
|
|
from mmgen.util import get_data_from_file
|
2019-03-20 17:10:46 +00:00
|
|
|
return get_data_from_file(fn,quiet=True,binary=binary)
|
2015-01-09 21:02:16 +03:00
|
|
|
|
2015-10-25 13:04:30 +03:00
|
|
|
def read_from_tmpfile(cfg,fn,binary=False):
|
|
|
|
|
return read_from_file(os.path.join(cfg['tmpdir'],fn),binary=binary)
|
2015-01-09 21:02:16 +03:00
|
|
|
|
2019-03-02 18:27:53 +00:00
|
|
|
def joinpath(*args,**kwargs):
|
|
|
|
|
return os.path.join(*args,**kwargs)
|
|
|
|
|
|
2015-01-09 21:02:16 +03:00
|
|
|
def ok():
|
2016-11-11 16:05:27 +03:00
|
|
|
if opt.profile: return
|
2015-01-10 18:52:30 +03:00
|
|
|
if opt.verbose or opt.exact_output:
|
2018-03-07 09:59:35 +00:00
|
|
|
gmsg('OK')
|
2016-02-28 16:41:43 +03:00
|
|
|
else: msg(' OK')
|
2015-01-09 21:02:16 +03:00
|
|
|
|
2019-03-02 18:27:53 +00:00
|
|
|
def cmp_or_die(s,t,desc=None):
|
|
|
|
|
if s != t:
|
2021-09-29 21:17:57 +00:00
|
|
|
raise TestSuiteFatalException(
|
|
|
|
|
(f'For {desc}:\n' if desc else '') +
|
|
|
|
|
f'ERROR: recoded data:\n{t!r}\ndiffers from original data:\n{s!r}'
|
|
|
|
|
)
|
2018-02-24 14:39:49 +03:00
|
|
|
|
|
|
|
|
def init_coverage():
|
|
|
|
|
coverdir = os.path.join('test','trace')
|
|
|
|
|
acc_file = os.path.join('test','trace.acc')
|
2018-10-30 16:31:14 +00:00
|
|
|
try: os.mkdir(coverdir,0o755)
|
2018-02-24 14:39:49 +03:00
|
|
|
except: pass
|
|
|
|
|
return coverdir,acc_file
|
2019-06-06 10:33:17 +00:00
|
|
|
|
|
|
|
|
devnull_fh = open(('/dev/null','null.out')[g.platform == 'win'],'w')
|
|
|
|
|
def silence():
|
2021-05-14 11:26:32 +00:00
|
|
|
if not (opt.verbose or getattr(opt,'exact_output',None)):
|
2019-06-06 10:33:17 +00:00
|
|
|
g.stdout = g.stderr = devnull_fh
|
|
|
|
|
|
|
|
|
|
def end_silence():
|
2021-05-14 11:26:32 +00:00
|
|
|
if not (opt.verbose or getattr(opt,'exact_output',None)):
|
2019-06-06 10:33:17 +00:00
|
|
|
g.stdout = sys.stdout
|
|
|
|
|
g.stderr = sys.stderr
|
|
|
|
|
|
|
|
|
|
def omsg(s):
|
|
|
|
|
sys.stderr.write(s + '\n')
|
|
|
|
|
def omsg_r(s):
|
|
|
|
|
sys.stderr.write(s)
|
|
|
|
|
sys.stderr.flush()
|
2021-05-14 11:26:32 +00:00
|
|
|
|
2019-06-06 10:33:17 +00:00
|
|
|
def imsg(s):
|
2021-05-14 11:26:32 +00:00
|
|
|
if opt.verbose or getattr(opt,'exact_output',None):
|
2019-06-06 10:33:17 +00:00
|
|
|
omsg(s)
|
|
|
|
|
def imsg_r(s):
|
2021-05-14 11:26:32 +00:00
|
|
|
if opt.verbose or getattr(opt,'exact_output',None):
|
2019-06-06 10:33:17 +00:00
|
|
|
omsg_r(s)
|
2021-05-14 11:26:32 +00:00
|
|
|
|
2019-06-06 10:33:17 +00:00
|
|
|
def iqmsg(s):
|
2021-05-14 11:26:32 +00:00
|
|
|
if not opt.quiet:
|
|
|
|
|
omsg(s)
|
2019-06-06 10:33:17 +00:00
|
|
|
def iqmsg_r(s):
|
2021-05-14 11:26:32 +00:00
|
|
|
if not opt.quiet:
|
|
|
|
|
omsg_r(s)
|
|
|
|
|
|
|
|
|
|
def oqmsg(s):
|
|
|
|
|
if not (opt.verbose or getattr(opt,'exact_output',None)):
|
|
|
|
|
omsg(s)
|
|
|
|
|
def oqmsg_r(s):
|
|
|
|
|
if not (opt.verbose or getattr(opt,'exact_output',None)):
|
|
|
|
|
omsg_r(s)
|
2019-12-07 12:45:04 +00:00
|
|
|
|
2021-10-02 17:54:11 +00:00
|
|
|
def end_msg(t):
|
|
|
|
|
omsg(green(
|
|
|
|
|
'All requested tests finished OK' +
|
|
|
|
|
('' if g.test_suite_deterministic else f', elapsed time: {t//60:02d}:{t%60:02d}')
|
|
|
|
|
))
|
|
|
|
|
|
2020-06-27 10:22:01 +00:00
|
|
|
def start_test_daemons(*network_ids,remove_datadir=False):
|
2020-05-10 13:39:53 +00:00
|
|
|
if not opt.no_daemon_autostart:
|
2020-06-27 10:22:01 +00:00
|
|
|
return test_daemons_ops(*network_ids,op='start',remove_datadir=remove_datadir)
|
2019-12-07 12:45:04 +00:00
|
|
|
|
|
|
|
|
def stop_test_daemons(*network_ids):
|
2020-05-10 13:39:53 +00:00
|
|
|
if not opt.no_daemon_stop:
|
|
|
|
|
return test_daemons_ops(*network_ids,op='stop')
|
2019-12-07 12:45:04 +00:00
|
|
|
|
2020-06-27 10:22:01 +00:00
|
|
|
def restart_test_daemons(*network_ids,remove_datadir=False):
|
2021-07-29 14:20:44 +00:00
|
|
|
if not stop_test_daemons(*network_ids):
|
|
|
|
|
return False
|
2020-06-27 10:22:01 +00:00
|
|
|
return start_test_daemons(*network_ids,remove_datadir=remove_datadir)
|
2020-02-18 20:28:28 +00:00
|
|
|
|
2020-06-27 10:22:01 +00:00
|
|
|
def test_daemons_ops(*network_ids,op,remove_datadir=False):
|
2020-05-10 13:39:53 +00:00
|
|
|
if not opt.no_daemon_autostart:
|
|
|
|
|
from mmgen.daemon import CoinDaemon
|
|
|
|
|
silent = not opt.verbose and not getattr(opt,'exact_output',False)
|
2021-07-29 14:20:44 +00:00
|
|
|
ret = False
|
2020-05-10 13:39:53 +00:00
|
|
|
for network_id in network_ids:
|
2021-08-01 20:54:51 +00:00
|
|
|
d = CoinDaemon(network_id,test_suite=True,daemon_id=g.daemon_id)
|
2020-06-27 10:22:01 +00:00
|
|
|
if remove_datadir:
|
|
|
|
|
d.stop(silent=True)
|
|
|
|
|
d.remove_datadir()
|
2021-07-29 14:20:44 +00:00
|
|
|
ret = d.cmd(op,silent=silent)
|
|
|
|
|
return ret
|