From df0385160b4f2793ecd801cd03ac05aecda1f753 Mon Sep 17 00:00:00 2001 From: MMGen Date: Thu, 28 Dec 2017 16:03:28 +0300 Subject: [PATCH] XMR (Monero) key/address generation support and wallet generating utility - Monero key-address files include spendkey, viewkey and wallet password - Wallet password is first 16 bytes of SHA256x2(spendkey) - Generate Monero wallets either by hand with: 1) `monero-wallet-cli --generate-from-spend-key walletfile`, using the keyaddrfile data, or automatically, using: 2) `mmgen-tool keyaddrlist2monerowallet keyaddrfile` The utility will generate a wallet for each key/address pair in the keyaddrfile and encrypt it using the password. The password is supplied via stdin. - Other feature: 32-byte hexadecimal password generation with `mmgen-passgen --hex` --- MANIFEST.in | 5 +- mmgen/addr.py | 143 +++++++++++++----- mmgen/ed25519.py | 57 +++++++ mmgen/globalvars.py | 7 +- mmgen/main.py | 2 +- mmgen/main_passgen.py | 14 +- mmgen/obj.py | 53 +++++-- mmgen/opts.py | 3 +- mmgen/protocol.py | 73 +++++++-- mmgen/tool.py | 111 ++++++++++++-- mmgen/tx.py | 3 +- mmgen/util.py | 11 +- setup.py | 1 + ...F3A-XMR-M[1,31-33,500-501,1010-1011].addrs | 19 +++ ...R-M[1,31-33,500-501,1010-1011].akeys.mmenc | Bin 0 -> 3200 bytes ...C-Z[1,31-33,500-501,1010-1011].akeys.mmenc | Bin 2841 -> 2865 bytes test/scrambletest.py | 13 +- test/test.py | 30 +++- 18 files changed, 444 insertions(+), 101 deletions(-) create mode 100644 mmgen/ed25519.py create mode 100644 test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].addrs create mode 100644 test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].akeys.mmenc diff --git a/MANIFEST.in b/MANIFEST.in index 96e68f70..19106b1c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,10 +3,11 @@ include doc/wiki/using-mmgen/* include test/*.py include test/ref/* include test/ref/litecoin/* -include test/ref/zcash/* -include test/ref/dash/* include test/ref/ethereum/* include test/ref/ethereum_classic/* +include test/ref/dash/* +include test/ref/zcash/* +include test/ref/monero/* include scripts/bitcoind-walletunlock.py include scripts/compute-file-chksum.py diff --git a/mmgen/addr.py b/mmgen/addr.py index 34d82719..1202cd59 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -40,15 +40,16 @@ class AddrGenerator(MMGenObject): gen_method = addr_type.gen_method else: raise TypeError,'{}: incorrect argument type for {}()'.format(type(addr_type),cls.__name__) - d = { - 'p2pkh': AddrGeneratorP2PKH, - 'segwit': AddrGeneratorSegwit, + gen_methods = { + 'p2pkh': AddrGeneratorP2PKH, + 'segwit': AddrGeneratorSegwit, 'ethereum': AddrGeneratorEthereum, - 'zcash_z': AddrGeneratorZcashZ + 'zcash_z': AddrGeneratorZcashZ, + 'monero': AddrGeneratorMonero } - assert gen_method in d - me = super(cls,cls).__new__(d[gen_method]) - me.desc = d + assert gen_method in gen_methods + me = super(cls,cls).__new__(gen_methods[gen_method]) + me.desc = gen_methods return me class AddrGeneratorP2PKH(AddrGenerator): @@ -113,6 +114,57 @@ class AddrGeneratorZcashZ(AddrGenerator): def to_segwit_redeem_script(self,pubhex): raise NotImplementedError,'Zcash z-addresses incompatible with Segwit' +class AddrGeneratorMonero(AddrGenerator): + + def b58enc(self,addr_str): + enc,l = baseconv.fromhex,len(addr_str) + a = ''.join([enc(addr_str[i*8:i*8+8].encode('hex'),'b58',pad=11,tostr=True) for i in range(l/8)]) + b = enc(addr_str[l-l%8:].encode('hex'),'b58',pad=7,tostr=True) + return a + b + + def to_addr(self,sk_hex): # sk_hex instead of pubhex + + # ed25519ll, a low-level ctypes wrapper for Ed25519 digital signatures by + # Daniel Holth - http://bitbucket.org/dholth/ed25519ll/ + try: + from ed25519ll.djbec import scalarmult,edwards,encodepoint,B + except: + from mmgen.ed25519 import scalarmult,edwards,encodepoint,B + + # Source and license for scalarmultbase function: + # https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py + # Copyright (c) 2014-2016, The Monero Project + # All rights reserved. + def scalarmultbase(e): + if e == 0: return [0, 1] + Q = scalarmult(B, e//2) + Q = edwards(Q, Q) + if e & 1: Q = edwards(Q, B) + return Q + + def hex2int_le(hexstr): + return int(hexstr.decode('hex')[::-1].encode('hex'),16) + + vk_hex = self.to_viewkey(sk_hex) + pk_str = encodepoint(scalarmultbase(hex2int_le(sk_hex))) + pvk_str = encodepoint(scalarmultbase(hex2int_le(vk_hex))) + addr_p1 = g.proto.addr_ver_num['monero'][0].decode('hex') + pk_str + pvk_str + + import sha3 + return CoinAddr(self.b58enc(addr_p1 + sha3.keccak_256(addr_p1).digest()[:4])) + + def to_wallet_passwd(self,sk_hex): + from mmgen.protocol import hash256 + return WalletPassword(hash256(sk_hex)[:32]) + + def to_viewkey(self,sk_hex): + assert len(sk_hex) == 64,'{}: incorrect privkey length'.format(len(sk_hex)) + import sha3 + return MoneroViewKey(g.proto.preprocess_key(sha3.keccak_256(sk_hex.decode('hex')).hexdigest(),None)) + + def to_segwit_redeem_script(self,sk_hex): + raise NotImplementedError,'Monero addresses incompatible with Segwit' + class KeyGenerator(MMGenObject): def __new__(cls,addr_type,generator=None,silent=False): @@ -130,7 +182,7 @@ class KeyGenerator(MMGenObject): else: msg('Using (slow) native Python ECDSA library for address generation') return super(cls,cls).__new__(KeyGeneratorPython) - elif pubkey_type == 'zcash_z': + elif pubkey_type in ('zcash_z','monero'): g.proto.addr_width = 95 me = super(cls,cls).__new__(KeyGeneratorDummy) me.desc = 'mmgen-'+pubkey_type @@ -198,10 +250,11 @@ class KeyGeneratorDummy(KeyGenerator): class AddrListEntry(MMGenListItem): addr = MMGenListItemAttr('addr','CoinAddr') - viewkey = MMGenListItemAttr('viewkey','ZcashViewKey') idx = MMGenListItemAttr('idx','AddrIdx') # not present in flat addrlists label = MMGenListItemAttr('label','TwComment',reassign_ok=True) sec = MMGenListItemAttr('sec',PrivKey,typeconv=False) + viewkey = MMGenListItemAttr('viewkey','ViewKey') + wallet_passwd = MMGenListItemAttr('wallet_passwd','WalletPassword') class PasswordListEntry(MMGenListItem): passwd = MMGenImmutableAttr('passwd',unicode,typeconv=False) # TODO: create Password type @@ -214,7 +267,11 @@ class AddrListChksum(str,Hilite): trunc_ok = False def __new__(cls,addrlist): - lines = [' '.join(addrlist.chksum_rec_f(e)) for e in addrlist.data] + ea = addrlist.al_id.mmtype.extra_attrs # add viewkey and passwd to the mix, if present + lines = [' '.join( + addrlist.chksum_rec_f(e) + + tuple(getattr(e,a) for a in ea if getattr(e,a)) + ) for e in addrlist.data] return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True)) class AddrListIDStr(unicode,Hilite): @@ -341,7 +398,9 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file compressed = self.al_id.mmtype.compressed pubkey_type = self.al_id.mmtype.pubkey_type - has_viewkey = self.al_id.mmtype.has_viewkey + + gen_wallet_passwd = type(self) == KeyAddrList and 'wallet_passwd' in self.al_id.mmtype.extra_attrs + gen_viewkey = type(self) == KeyAddrList and 'viewkey' in self.al_id.mmtype.extra_attrs if self.gen_addrs: kg = KeyGenerator(self.al_id.mmtype) @@ -369,8 +428,10 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file if self.gen_addrs: ph = kg.to_pubhex(e.sec) e.addr = ag.to_addr(ph) - if has_viewkey: + if gen_viewkey: e.viewkey = ag.to_viewkey(ph) + if gen_wallet_passwd: + e.wallet_passwd = ag.to_wallet_passwd(ph) if type(self) == PasswordList: e.passwd = unicode(self.make_passwd(e.sec)) # TODO - own type @@ -502,16 +563,17 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file for e in self.data: c = ' '+e.label if enable_comments and e.label else '' if type(self) == KeyList: - out.append(fs.format(e.idx,'wif: {}'.format(e.sec.wif),c)) + out.append(fs.format(e.idx,'{} {}'.format(self.al_id.mmtype.wif_label,e.sec.wif),c)) elif type(self) == PasswordList: out.append(fs.format(e.idx,e.passwd,c)) else: # First line with idx out.append(fs.format(e.idx,e.addr,c)) if self.has_keys: - if self.al_id.mmtype.has_viewkey: - out.append(fs.format('','view: '+e.viewkey,c)) - if opt.b16: out.append(fs.format('', 'hex: '+e.sec,c)) - out.append(fs.format('','wif: '+e.sec.wif,c)) + if opt.b16: out.append(fs.format('', 'orig_hex: '+e.sec.orig_hex,c)) + out.append(fs.format('','{} {}'.format(self.al_id.mmtype.wif_label,e.sec.wif),c)) + for k in ('viewkey','wallet_passwd'): + v = getattr(e,k) + if v: out.append(fs.format('','{}: {}'.format(k,v),c)) out.append('}') self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n' @@ -521,24 +583,30 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file ret = AddrListList() le = self.entry_type - while lines: - l = lines.pop(0) - d = l.split(None,2) + def get_line(): + ret = lines.pop(0).split(None,2) + if ret[0] == 'orig_hex:': # hacky + return lines.pop(0).split(None,2) + return ret - assert is_mmgen_idx(d[0]),"'%s': invalid address num. in line: '%s'" % (d[0],l) + while lines: + d = get_line() + + assert is_mmgen_idx(d[0]),"'%s': invalid address num. in line: '%s'" % (d[0],' '.join(d)) assert self.check_format(d[1]),"'{}': invalid {}".format(d[1],self.data_desc) if len(d) != 3: d.append('') a = le(**{'idx':int(d[0]),self.main_attr:d[1],'label':d[2]}) - if self.has_keys: - if self.al_id.mmtype.has_viewkey: - d = lines.pop(0).split(None,2) - assert d[0] == 'view:',"Invalid line in file: '{}'".format(' '.join(d)) - a.viewkey = ZcashViewKey(d[1]) - d = lines.pop(0).split(None,2) - assert d[0] == 'wif:',"Invalid line in file: '{}'".format(' '.join(d)) + if self.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd + d = get_line() + assert d[0] == self.al_id.mmtype.wif_label,"Invalid line in file: '{}'".format(' '.join(d)) a.sec = PrivKey(wif=d[1]) + for k,dtype in (('viewkey',ViewKey),('wallet_passwd',WalletPassword)): + if k in self.al_id.mmtype.extra_attrs: + d = get_line() + assert d[0] == k+':',"Invalid line in file: '{}'".format(' '.join(d)) + setattr(a,k,dtype(d[1])) ret.append(a) @@ -571,12 +639,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file if not al_mmtype: mmtype = MMGenAddrType('E' if al_coin in ('ETH','ETC') else 'L',on_fail='raise') else: - try: - mmtype = MMGenAddrType(al_mmtype,on_fail='raise') - except: - raise ValueError,( - u"'{}': invalid address type in address file. Must be one of: {}".format( - mmtype.upper(),' '.join([i['name'].upper() for i in MMGenAddrType.mmtypes.values()]))) + mmtype = MMGenAddrType(al_mmtype,on_fail='raise') from mmgen.protocol import CoinProtocol base_coin = CoinProtocol(al_coin or 'BTC',testnet=False).base_coin @@ -685,7 +748,8 @@ Record this checksum: it will be used to verify the password file in the future pw_fmt = None pw_info = { 'b58': { 'min_len': 8 , 'max_len': 36 ,'dfl_len': 20, 'desc': 'base-58 password' }, - 'b32': { 'min_len': 10 ,'max_len': 42 ,'dfl_len': 24, 'desc': 'base-32 password' } + 'b32': { 'min_len': 10 ,'max_len': 42 ,'dfl_len': 24, 'desc': 'base-32 password' }, + 'hex': { 'min_len': 64 ,'max_len': 64 ,'dfl_len': 64, 'desc': 'raw hex password' } } chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd) @@ -751,11 +815,14 @@ Record this checksum: it will be used to verify the password file in the future def make_passwd(self,hex_sec): assert self.pw_fmt in self.pw_info - # we take least significant part - return ''.join(baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len))[-self.pw_len:] + if self.pw_fmt == 'hex': + return hex_sec + else: + # we take least significant part + return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:] def check_format(self,pw): - if not (is_b58_str,is_b32_str)[self.pw_fmt=='b32'](pw): + if not {'b58':is_b58_str,'b32':is_b32_str,'hex':is_hex_str}[self.pw_fmt](pw): msg('Password is not a valid {} string'.format(self.pw_fmt)) return False if len(pw) != self.pw_len: diff --git a/mmgen/ed25519.py b/mmgen/ed25519.py new file mode 100644 index 00000000..0650959c --- /dev/null +++ b/mmgen/ed25519.py @@ -0,0 +1,57 @@ +# The reference Ed25519 software is in the public domain. +# Source: https://ed25519.cr.yp.to/python/ed25519.py +# Date accessed: 2 Nov. 2016 + +import hashlib + +b = 256 +q = 2**255 - 19 +l = 2**252 + 27742317777372353535851937790883648493 + +def H(m): + return hashlib.sha512(m).digest() + +def expmod(b, e, m): + if e == 0: return 1 + t = expmod(b, e//2, m)**2 % m + if e & 1: t = (t*b) % m + return t + +def inv(x): + return expmod(x, q-2, q) + +d = -121665 * inv(121666) +I = expmod(2, (q-1)//4, q) + +def xrecover(y): + xx = (y*y-1) * inv(d*y*y+1) + x = expmod(xx, (q+3)//8, q) + if (x*x - xx) % q != 0: x = (x*I) % q + if x % 2 != 0: x = q-x + return x + +By = 4 * inv(5) +Bx = xrecover(By) +B = [Bx%q, By%q] + +def edwards(P, Q): + x1 = P[0] + y1 = P[1] + x2 = Q[0] + y2 = Q[1] + x3 = (x1*y2+x2*y1) * inv(1+d*x1*x2*y1*y2) + y3 = (y1*y2+x1*x2) * inv(1-d*x1*x2*y1*y2) + return [x3%q, y3%q] + +def scalarmult(P, e): + if e == 0: return [0, 1] + Q = scalarmult(P, e//2) + Q = edwards(Q, Q) + if e & 1: Q = edwards(Q, P) + return Q + +def encodepoint(P): + x = P[0] + y = P[1] + bits = [(y >> i) & 1 for i in range(b-1)] + [x & 1] + return b''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b//8)]) diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index eadc2c9a..a70225e6 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -44,7 +44,7 @@ class g(object): proj_name = 'MMGen' proj_url = 'https://github.com/mmgen/mmgen' prog_name = os.path.basename(sys.argv[0]) - author = 'Philemon' + author = 'The MMGen Project' email = '' Cdates = '2013-2017' keywords = 'Bitcoin, BTC, cryptocurrency, wallet, cold storage, offline, online, spending, open-source, command-line, Python, Linux, Bitcoin Core, bitcoind, hd, deterministic, hierarchical, secure, anonymous, Electrum, seed, mnemonic, brainwallet, Scrypt, utility, script, scriptable, blockchain, raw, transaction, permissionless, console, terminal, curses, ansi, color, tmux, remote, client, daemon, RPC, json, entropy, xterm, rxvt, PowerShell, MSYS, MinGW, mswin, Armbian, Raspbian, Raspberry Pi, Orange Pi, BCash, BCH, Litecoin, LTC, altcoin, ZEC, Zcash, DASH, ETH, Ethereum, Classic, SHA256Compress' @@ -73,6 +73,7 @@ class g(object): force_256_color = False testnet = False regtest = False + accept_defaults = False chain = None # set by first call to rpc_init() chains = 'mainnet','testnet','regtest' daemon_version = None # set by first call to rpc_init() @@ -108,7 +109,8 @@ class g(object): # User opt sets global var: common_opts = ( 'color','no_license','rpc_host','rpc_port','testnet','rpc_user','rpc_password', - 'daemon_data_dir','force_256_color','regtest','coin','bob','alice' + 'daemon_data_dir','force_256_color','regtest','coin','bob','alice', + 'accept_defaults' ) required_opts = ( 'quiet','verbose','debug','outdir','echo_passphrase','passwd_file','stdout', @@ -116,6 +118,7 @@ class g(object): 'brain_params','b16','usr_randchars','coin','bob','alice','key_generator' ) incompatible_opts = ( + ('base32','hex'), # mmgen-passgen ('bob','alice'), ('quiet','verbose'), ('label','keep_label'), diff --git a/mmgen/main.py b/mmgen/main.py index 281bff0e..e77646cf 100755 --- a/mmgen/main.py +++ b/mmgen/main.py @@ -57,5 +57,5 @@ def launch(what): else: try: m = u'{}\n'.format(e[0]) except: m = u'{!r}\n'.format(e[0]) - sys.stderr.write(m) + sys.stderr.write(u'ERROR: ' + m) sys.exit(2) diff --git a/mmgen/main_passgen.py b/mmgen/main_passgen.py index 8d089c55..9e8fb0ef 100755 --- a/mmgen/main_passgen.py +++ b/mmgen/main_passgen.py @@ -29,7 +29,8 @@ from mmgen.obj import MMGenPWIDString dfl_len = { 'b58': PasswordList.pw_info['b58']['dfl_len'], - 'b32': PasswordList.pw_info['b32']['dfl_len'] + 'b32': PasswordList.pw_info['b32']['dfl_len'], + 'hex': PasswordList.pw_info['hex']['dfl_len'] } opts_data = lambda: { @@ -41,6 +42,7 @@ opts_data = lambda: { -h, --help Print this help message --, --longhelp Print help message for long options (common options) -b, --base32 Generate passwords in Base32 format instead of Base58 +-x, --hex Generate passwords in raw hex format instead of Base58 -d, --outdir= d Output files to directory 'd' instead of working dir -e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry -i, --in-fmt= f Input is from wallet format 'f' (see FMT CODES below) @@ -48,9 +50,9 @@ opts_data = lambda: { 'f' at offset 'o' (comma-separated) -O, --old-incog-fmt Specify old-format incognito input -L, --passwd-len= l Specify length of generated passwords - (default: {d58} chars [base58], {d32} chars [base32]). - An argument of 'h' will generate passwords of half - the default length. + (default: {d58} chars [base58], {d32} chars [base32], + {dhex} chars [hex]). An argument of 'h' will generate + passwords of half the default length. -l, --seed-len= l Specify wallet seed length of 'l' bits. This option is required only for brainwallet and incognito inputs with non-standard (< {g.seed_len}-bit) seed lengths @@ -65,7 +67,7 @@ opts_data = lambda: { -v, --verbose Produce more verbose output """.format( seed_lens=', '.join([str(i) for i in g.seed_lens]), - g=g,pnm=g.proj_name,d58=dfl_len['b58'],d32=dfl_len['b32'], + g=g,pnm=g.proj_name,d58=dfl_len['b58'],d32=dfl_len['b32'],dhex=dfl_len['hex'], kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]) ), 'notes': """ @@ -124,7 +126,7 @@ pw_id_str = cmd_args.pop() sf = get_seed_file(cmd_args,1) -pw_fmt = ('b58','b32')[bool(opt.base32)] +pw_fmt = ('b58','b32','hex')[bool(opt.base32)+2*bool(opt.hex)] pw_len = (opt.passwd_len,dfl_len[pw_fmt]/2)[opt.passwd_len in ('h','H')] diff --git a/mmgen/obj.py b/mmgen/obj.py index 85561fe6..46563a02 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -20,7 +20,7 @@ obj.py: MMGen native classes """ -import sys +import sys,os from decimal import * from mmgen.color import * from string import hexdigits,ascii_letters,digits @@ -32,7 +32,7 @@ def is_coin_addr(s): return CoinAddr(s,on_fail='silent') def is_addrlist_id(s): return AddrListID(s,on_fail='silent') def is_tw_label(s): return TwLabel(s,on_fail='silent') def is_wif(s): return WifKey(s,on_fail='silent') -def is_viewkey(s): return ZcashViewKey(s,on_fail='silent') +def is_viewkey(s): return ViewKey(s,on_fail='silent') class MMGenObject(object): @@ -111,9 +111,11 @@ class InitErrors(object): @staticmethod def init_fail(m,on_fail,silent=False): - if silent: m = '' from mmgen.util import die,msg - if on_fail == 'die': die(1,m) + if silent: m = '' + if os.getenv('MMGEN_TRACEBACK'): + raise ValueError,m + elif on_fail == 'die': die(1,m) elif on_fail == 'return': if m: msg(m) return None # TODO: change to False @@ -404,6 +406,16 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject): d = rpc_init().validateaddress(self) return d['iswatchonly'] and 'account' in d +class ViewKey(object): + def __new__(cls,s,on_fail='die'): + from mmgen.globalvars import g + if g.proto.name == 'zcash': + return ZcashViewKey.__new__(ZcashViewKey,s,on_fail) + elif g.proto.name == 'monero': + return MoneroViewKey.__new__(MoneroViewKey,s,on_fail) + else: + raise ValueError,'{}: protocol does not support view keys'.format(g.proto.name.capitalize()) + class ZcashViewKey(CoinAddr): hex_width = 128 class SeedID(str,Hilite,InitErrors): @@ -513,11 +525,11 @@ class HexStr(str,Hilite,InitErrors): m = "{!r}: value cannot be converted to {} (value is {})" return cls.init_fail(m.format(s,cls.__name__,e[0]),on_fail) -class MMGenTxID(HexStr,Hilite,InitErrors): - color = 'red' - width = 6 +class HexStrWithWidth(HexStr): + color = 'nocolor' trunc_ok = False - hexcase = 'upper' + hexcase = 'lower' + width = None def __new__(cls,s,on_fail='die'): cls.arg_chk(cls,on_fail) try: @@ -528,10 +540,10 @@ class MMGenTxID(HexStr,Hilite,InitErrors): m = "{}\n{!r}: value cannot be converted to {}" return cls.init_fail(m.format(e[0],s,cls.__name__),on_fail) -class CoinTxID(MMGenTxID): - color = 'purple' - width = 64 - hexcase = 'lower' +class MMGenTxID(HexStrWithWidth): color,width,hexcase = 'red',6,'upper' +class MoneroViewKey(HexStrWithWidth): color,width,hexcase = 'cyan',64,'lower' +class WalletPassword(HexStrWithWidth): color,width,hexcase = 'blue',32,'lower' +class CoinTxID(HexStrWithWidth): color,width,hexcase = 'purple',64,'lower' class WifKey(str,Hilite,InitErrors): width = 53 @@ -582,8 +594,9 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject): w2h = g.proto.wif2hex(wif) # raises exception on error me = str.__new__(cls,w2h['hex']) me.compressed = w2h['compressed'] - me.pubkey_type = w2h['pubkey_type'] - me.wif = str.__new__(WifKey,wif) # check has been done + me.pubkey_type = w2h['pubkey_type'] + me.wif = str.__new__(WifKey,wif) # check has been done + me.orig_hex = None return me except Exception as e: fs = "Value {!r} cannot be converted to {} WIF key ({})" @@ -593,6 +606,7 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject): assert s and type(compressed) == bool and pubkey_type,'Incorrect args for PrivKey()' assert len(s) == cls.width / 2,'Key length must be {}'.format(cls.width/2) me = str.__new__(cls,g.proto.preprocess_key(s.encode('hex'),pubkey_type)) + me.orig_hex = s.encode('hex') # save the non-preprocessed key me.compressed = compressed me.pubkey_type = pubkey_type if pubkey_type != 'password': # skip WIF creation for passwds @@ -705,6 +719,12 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject): 'gen_method':'zcash_z', 'addr_fmt':'zcash_z', 'desc':'Zcash z-address' }, + 'M': { 'name':'monero', + 'pubkey_type':'monero', + 'compressed':False, + 'gen_method':'monero', + 'addr_fmt':'monero', + 'desc':'Monero address'} } def __new__(cls,s,on_fail='die',errmsg=None): if type(s) == cls: return s @@ -719,7 +739,10 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject): setattr(me,k,v[k]) assert me in g.proto.mmtypes + ('P',), ( "'{}': invalid address type for {}".format(me.name,g.proto.__name__)) - me.has_viewkey = me.name == 'zcash_z' + me.extra_attrs = [] + if me.name in ('monero','zcash_z'): me.extra_attrs += ['viewkey'] + if me.name == 'monero': me.extra_attrs += ['wallet_passwd'] + me.wif_label = ('wif:','spendkey:')[me.name=='monero'] return me raise ValueError,'not found' except Exception as e: diff --git a/mmgen/opts.py b/mmgen/opts.py index 1093ac04..fa682598 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -181,7 +181,7 @@ Are you sure you want to continue? """.strip().format(g.coin,tl[trust_level],pn=g.proj_name) if os.getenv('MMGEN_TEST_SUITE'): msg(m); return - if not keypress_confirm(m): + if not keypress_confirm(m,default_yes=True): sys.exit(0) def init(opts_f,add_opts=[],opt_filter=None): @@ -191,6 +191,7 @@ def init(opts_f,add_opts=[],opt_filter=None): # most, but not all, of these set the corresponding global var common_opts_data = """ +--, --accept-defaults Accept defaults at all prompts --, --coin=c Choose coin unit. Default: {cu_dfl}. Options: {cu_all} --, --color=0|1 Disable or enable color output --, --force-256-color Force 256-color output when color is enabled diff --git a/mmgen/protocol.py b/mmgen/protocol.py index 120cb318..0dcfae1c 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -251,27 +251,32 @@ class LitecoinTestnetProtocol(LitecoinProtocol): class BitcoinProtocolAddrgen(BitcoinProtocol): mmcaps = ('key','addr') class BitcoinTestnetProtocolAddrgen(BitcoinTestnetProtocol): mmcaps = ('key','addr') -class EthereumProtocol(BitcoinProtocolAddrgen): +class DummyWIF(object): + + @classmethod + def hex2wif(cls,hexpriv,pubkey_type,compressed): + n = cls.name.capitalize() + assert pubkey_type == cls.pubkey_type,'{}: invalid pubkey_type for {}!'.format(pubkey_type,n) + assert compressed == False,'{} does not support compressed pubkeys!'.format(n) + return str(hexpriv) + + @classmethod + def wif2hex(cls,wif): + return { 'hex':str(wif), 'pubkey_type':cls.pubkey_type, 'compressed':False } + +class EthereumProtocol(DummyWIF,BitcoinProtocolAddrgen): addr_width = 40 mmtypes = ('E',) dfl_mmtype = 'E' name = 'ethereum' base_coin = 'ETH' - - @classmethod - def hex2wif(cls,hexpriv,pubkey_type,compressed): - assert compressed == False,'Ethereum does not support compressed pubkeys!' - return str(hexpriv) - - @classmethod - def wif2hex(cls,wif): - return { 'hex':str(wif), 'pubkey_type':'std', 'compressed':False } + pubkey_type = 'std' # required by DummyWIF @classmethod def verify_addr(cls,addr,hex_width,return_dict=False): from mmgen.util import is_hex_str_lc - if is_hex_str_lc(addr) and len(addr) == 40: + if is_hex_str_lc(addr) and len(addr) == cls.addr_width: return { 'hex': addr, 'format': 'ethereum', 'width': cls.addr_width } if return_dict else True if g.debug: Msg("Invalid address '{}'".format(addr)) return False @@ -324,6 +329,50 @@ class ZcashTestnetProtocol(ZcashProtocol): 'zcash_z': ('16b6','??'), 'viewkey': ('0b2a','??') } +# https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h +class MoneroProtocol(DummyWIF,BitcoinProtocolAddrgen): + name = 'monero' + base_coin = 'XMR' + addr_ver_num = { 'monero': ('12','4'), 'monero_sub': ('2a','8') } # 18,42 + wif_ver_num = {} + mmtypes = ('M',) + dfl_mmtype = 'M' + addr_width = 95 + pubkey_type = 'monero' # required by DummyWIF + + @classmethod + def preprocess_key(cls,hexpriv,pubkey_type): # reduce key + try: + from ed25519ll.djbec import l + except: + from mmgen.ed25519 import l + n = int(hexpriv.decode('hex')[::-1].encode('hex'),16) % l + return '{:064x}'.format(n).decode('hex')[::-1].encode('hex') + + @classmethod + def verify_addr(cls,addr,hex_width,return_dict=False): + + def b58dec(addr_str): + from mmgen.util import baseconv + dec,l = baseconv.tohex,len(addr_str) + a = ''.join([dec(addr_str[i*11:i*11+11],'b58',pad=16) for i in range(l/11)]) + b = dec(addr_str[-(l%11):],'b58',pad=10) + return a + b + + from mmgen.util import is_b58_str + assert is_b58_str(addr),'Not valid base-58 string' + assert len(addr) == cls.addr_width,'Incorrect width' + + ret = b58dec(addr) + import sha3 + chk = sha3.keccak_256(ret.decode('hex')[:-4]).hexdigest()[:8] + assert chk == ret[-8:],'Incorrect checksum' + + return { 'hex': ret, 'format': 'monero', 'width': cls.addr_width } if return_dict else True + +class MoneroTestnetProtocol(MoneroProtocol): + addr_ver_num = { 'monero': ('35','4'), 'monero_sub': ('3f','8') } # 53,63 + class CoinProtocol(MMGenObject): coins = { # mainnet testnet trustlevel (None == skip) @@ -333,6 +382,7 @@ class CoinProtocol(MMGenObject): 'eth': (EthereumProtocol,EthereumTestnetProtocol,2), 'etc': (EthereumClassicProtocol,EthereumClassicTestnetProtocol,2), 'zec': (ZcashProtocol,ZcashTestnetProtocol,2), + 'xmr': (MoneroProtocol,MoneroTestnetProtocol,2) } def __new__(cls,coin,testnet): coin = coin.lower() @@ -412,5 +462,6 @@ def make_init_genonly_altcoins_str(data): return out def init_coin(coin): + coin = coin.upper() g.coin = coin g.proto = CoinProtocol(coin,g.testnet) diff --git a/mmgen/tool.py b/mmgen/tool.py index a0f7ca31..652c645a 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -93,7 +93,8 @@ cmd_data = OrderedDict([ ('Encrypt', [' [str]',"outfile [str='']","hash_preset [str='']"]), ('Decrypt', [' [str]',"outfile [str='']","hash_preset [str='']"]), ('Bytespec', [' [str]']), - ('Regtest_setup',[]), + + ('Keyaddrlist2monerowallet',['<{} XMR key-address file> [str]'.format(pnm),'blockheight [int=(current height)]']), ]) def usage(command): @@ -315,9 +316,9 @@ def Mn_rand256(wordlist=dfl_wl_id): do_random_mn(32,wordlist) def Hex2mn(s,wordlist=dfl_wl_id): Msg(' '.join(baseconv.fromhex(s,wordlist))) def Mn2hex(s,wordlist=dfl_wl_id): Msg(baseconv.tohex(s.split(),wordlist)) -def Strtob58(s,pad=None): Msg(''.join(baseconv.fromhex(binascii.hexlify(s),'b58',pad))) -def Hextob58(s,pad=None): Msg(''.join(baseconv.fromhex(s,'b58',pad))) -def Hextob32(s,pad=None): Msg(''.join(baseconv.fromhex(s,'b32',pad))) +def Strtob58(s,pad=None): Msg(baseconv.fromhex(binascii.hexlify(s),'b58',pad,tostr=True)) +def Hextob58(s,pad=None): Msg(baseconv.fromhex(s,'b58',pad,tostr=True)) +def Hextob32(s,pad=None): Msg(baseconv.fromhex(s,'b32',pad,tostr=True)) def B58tostr(s): Msg(binascii.unhexlify(baseconv.tohex(s,'b58'))) def B58tohex(s,pad=None): Msg(baseconv.tohex(s,'b58',pad)) def B32tohex(s,pad=None): Msg(baseconv.tohex(s.upper(),'b32',pad)) @@ -474,12 +475,102 @@ def Rand2file(outfile,nbytes,threads=4,silent=False): def Bytespec(s): Msg(str(parse_nbytes(s))) -def Regtest_setup(): - print 'ok' - return - import subprocess as sp - sp.check_output() - pass +def Keyaddrlist2monerowallet(infile,blockheight=None): + import pexpect + + if blockheight != None and int(blockheight) < 0: blockheight = 0 + + def run_cmd(cmd): + import subprocess as sp + p = sp.Popen(cmd,stdin=sp.PIPE,stdout=sp.PIPE,stderr=sp.PIPE) + return p + + def test_rpc(): + p = run_cmd(['monero-wallet-cli','--version']) + if p.stdout.read()[:6] != 'Monero': + die(1,"Unable to run 'monero-wallet-cli'!") + p = run_cmd(['monerod','status']) + ret = p.stdout.read() + if ret[:7] != 'Height:': + die(1,'Unable to connect to monerod!') + return int(ret[8:].split('/')[0]) + + cur_height = test_rpc() + + from mmgen.protocol import init_coin + init_coin('xmr') + from mmgen.addr import AddrList + al = KeyAddrList(infile) + sid = al.al_id.sid + os.environ['LANG'] = 'C' + + def my_expect(p,m,s,regex=False): + if m: msg_r(' {}...'.format(m)) + ret = (p.expect_exact,p.expect)[regex](s) + if not (ret == 0 or (type(s) == list and ret in (0,1))): + die(2,"Expect failed: '{}' (return value: {})".format(s,ret)) + if m: msg('OK') + return ret + + def my_sendline(p,m,s,usr_ret): + if m: msg_r(' {}...'.format(m)) + ret = p.sendline(s) + if ret != usr_ret: + die(2,"Unable to send line '{}' (return value {})".format(s,ret)) + if m: msg('OK') + vmsg("sendline: '{}' => {}".format(s,ret)) + + def create(): + gmsg('\nCreating {} wallet{}'.format(dl,suf(dl))) + for n,d in enumerate(al.data): + fn = '{}{}-{}-MoneroWallet'.format(('',opt.outdir+'/')[bool(opt.outdir)],sid,d.idx) + gmsg("\nGenerating wallet {}/{} ({})".format(n+1,dl,fn)) + try: os.stat(fn) + except: pass + else: die(1,"Wallet '{}' already exists!".format(fn)) +# pmsg([d.sec,d.wallet_passwd,d.viewkey,d.addr,fn]) + p = pexpect.spawn('monero-wallet-cli --generate-from-spend-key {}'.format(fn)) + my_expect(p,'Awaiting initial prompt','Secret spend key: ') + my_sendline(p,'',d.sec,65) + my_expect(p,'','Enter new wallet password: ') + my_sendline(p,'Sending password',d.wallet_passwd,33) + my_expect(p,'','Confirm password: ') + my_sendline(p,'Sending password again',d.wallet_passwd,33) + my_expect(p,'','of your choice: ') + my_sendline(p,'','1',2) + my_expect(p,'monerod generating wallet','Generated new wallet: ') + my_expect(p,'','\n') + if d.addr not in p.before: + die(3,'Addresses do not match!\n MMGen: {}\n Monero: {}'.format(d.addr,p.before)) + my_expect(p,'','View key: ') + my_expect(p,'','\n') + if d.viewkey not in p.before: + die(3,'View keys do not match!\n MMGen: {}\n Monero: {}'.format(d.viewkey,p.before)) + my_expect(p,'','(YYYY-MM-DD): ') + h = str(blockheight or cur_height-1) + my_sendline(p,'',h,len(h)+1) + ret = my_expect(p,'',['Starting refresh','Still apply restore height? (Y/Yes/N/No): ']) + if ret == 1: + my_sendline(p,'','Y',2) + m = ' Warning: {}: blockheight argument is higher than current blockheight' + ymsg(m.format(blockheight)) + elif blockheight != None: + p.logfile = sys.stderr + my_expect(p,'Syncing wallet','\[wallet.*$',regex=True) + p.logfile = None + my_sendline(p,'Exiting','exit',5) + p.read() + + dl = len(al.data) + try: + create() + gmsg('\n{} wallet{} created'.format(dl,suf(dl))) + except KeyboardInterrupt: + rdie(1,'\nUser interrupt\n') + except EOFError: + rdie(2,'\nEnd of file\n') + except Exception as e: + rdie(1,'Program died: {!r}'.format(e)) # ================ RPC commands ================== # diff --git a/mmgen/tx.py b/mmgen/tx.py index 56f731ac..f78f32b6 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -736,7 +736,8 @@ class MMGenTX(MMGenObject): if self.is_in_mempool(): if status: d = g.rpch.gettransaction(self.coin_txid,on_fail='silent') - r = '{}replaceable'.format(('NOT ','')[d['bip125-replaceable']=='yes']) + brs = 'bip125-replaceable' + r = '{}replaceable'.format(('NOT ','')[brs in d and d[brs]=='yes']) t = d['timereceived'] m = 'Sent {} ({} h/m/s ago)' b = m.format(time.strftime('%c',time.gmtime(t)),secs_to_dhms(int(time.time()-t))) diff --git a/mmgen/util.py b/mmgen/util.py index c1c08a3d..de4f4d5e 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -269,7 +269,7 @@ class baseconv(object): @classmethod def b58encode(cls,s,pad=None): pad = cls.get_pad(s,pad,'en',cls.b58pad_lens,[bytes]) - return ''.join(cls.fromhex(hexlify(s),'b58',pad=pad)) + return cls.fromhex(hexlify(s),'b58',pad=pad,tostr=True) @classmethod def b58decode(cls,s,pad=None): @@ -326,7 +326,7 @@ class baseconv(object): return ('','0')[len(ret) % 2] + ret @classmethod - def fromhex(cls,hexnum,wl_id,pad=None): + def fromhex(cls,hexnum,wl_id,pad=None,tostr=False): hexnum = hexnum.strip() if not is_hex_str(hexnum): @@ -338,7 +338,8 @@ class baseconv(object): while num: ret.append(num % base) num /= base - return [wl[n] for n in [0] * ((pad or 0)-len(ret)) + ret[::-1]] + o = [wl[n] for n in [0] * ((pad or 0)-len(ret)) + ret[::-1]] + return ''.join(o) if tostr else o baseconv.check_wordlists() @@ -715,6 +716,10 @@ def keypress_confirm(prompt,default_yes=False,verbose=False,no_nl=False): p = '{} {}: '.format(prompt,q) nl = ('\n','\r{}\r'.format(' '*len(p)))[no_nl] + if opt.accept_defaults: + msg(p) + return (False,True)[default_yes] + while True: reply = get_char(p).strip('\n\r') if not reply: diff --git a/setup.py b/setup.py index 221d3b25..558db279 100755 --- a/setup.py +++ b/setup.py @@ -114,6 +114,7 @@ setup( 'mmgen.color', 'mmgen.common', 'mmgen.crypto', + 'mmgen.ed25519', 'mmgen.filename', 'mmgen.globalvars', 'mmgen.license', diff --git a/test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].addrs b/test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].addrs new file mode 100644 index 00000000..82f54090 --- /dev/null +++ b/test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].addrs @@ -0,0 +1,19 @@ +# MMGen address file +# +# This file is editable. +# Everything following a hash symbol '#' is a comment and ignored by MMGen. +# A text label of 32 characters or less may be added to the right of each +# address, and it will be appended to the tracking wallet label upon import. +# The label may contain any printable ASCII symbol. +# Address data checksum for 98831F3A-XMR-M[1,31-33,500-501,1010-1011]: 4369 0253 AC2C 0E38 +# Record this value to a secure location. +98831F3A XMR:MONERO { + 1 41tmwZd2CdXEGtWqGY9fH9FVtQM8VxZASYPQ3VJQhFjtGWYzQFuidD21vJYTi2yy3tXRYXTNXBTaYVLav62rwUUpFFyicZU + 31 42R44dGVXu6gspyuWKNieJHmxzpnLxpkVbncKrFs5XniBEifRVYxPpeKJNgGPQkikX8AqjWL1Ysnf5Yc8DALiD6939PJAsb + 32 44mhAiVtVuD5yGbT15BG3M1WmFD5R5dZfbwTfmfVDfneRT9keAcUwgGRvtx32WifQiECKVD1zESYaWqwJf5yh5w71WM6BEE + 33 47UrLDNHrbiF5Ea6jvpPxcRYfW9ZkPRevPJXfWUmtEApb8p5xvp49d5iWQifspQzDzcBSbroBbbMFFJ6TYNcxmrtFviJ84L + 500 428YDXihJ8d8AMSnqtiHbfGt7fA6FsMUEJUCidSB7wN4RjGsMGzC5dBgaKQojevhaxKTZacSmF3xe7wubu9zyZ5b8S79pHG + 501 46nYucqe3BCMoASekNxvVT3BMnKuHsr9A8CwPZ6eAwVSJ9v8am5c6zR8St1Wcg2PPdJ6oL6zDM8zVChF15dZhGcgNEwLg2T + 1010 43tWyaYaMAt5ZaK5n3SVbSJJigpBnWrVXV3iBPJ5PX7UgvPRnzMw6dLXnrogRu1LzAe1aZrYJFHPBWVw1c7CBYMvLMVHf99 + 1011 478e1Nnt14zYhinwFCrr9bFjTc7uLNfudCcvGHsYrBnN1Q4xuGUdwubCEScTYzRzs4113ndcFBJSK1xfkPvUNYFB55i5kqV +} diff --git a/test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].akeys.mmenc b/test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].akeys.mmenc new file mode 100644 index 0000000000000000000000000000000000000000..494996bde08d602a6d5c1facabc4104115557379 GIT binary patch literal 3200 zcmV-`41e?1wA>2^EW^kEEj2n46n6>e;WaVp;1!_HM|BZu`%KjthAF*B)Fkw6It3!F zke`BC{%|dSu1OoaVcnU}9!vcHRYA*P9Y(`64}uD?Ud>bRc7J=c(fhxyQfU+1J{IOr_^WMladST?nI+VYHn+x}J>8wtF#T80 z3R_p@D428_-~+LUVx|k~oj%_U9FKRxNRfyAr%XG8f*}X1U=4?s%V3MjZ?&S#7bgT| zkW>rya=2H-9VKW`X?`Ocm;rH$@VC2${;lCoBF}DTnpjH3XjS zhyarn9{o=3V?ym`*z<`Hs;VmRJus{#2wEw3YJV9fEVB}W7XpwV><&1>v*D6e<+J&p zeF||F2h?Z4cP6cpxCEMEexPw3TY;ntw~X;A)C(jh}1&3W(2c^E4+FPjwByq9&0Tsx?cu8if z(p`@Jsa8_C%iri8Mry3-_d;b46_$+dl_kzg5yg|-2mLD2OQt|sTd7=dRVsu>$=;B9&(d3w!zf7!PrPfH%&qeaWLRkd=wzOdsXjphRgxpJmd9w%2W!y|ckXz7r; z0Q_C7vs-(R>$JiJ*EPI7sZOYoCdj8t@cJ9ak9jvay<$I`wyJB#omf53*Pt(~3P=EQAA8~G9&(jv21k~1 zCqWJl9a?G-gL=#(z1BDO_>17%eQ5{hC&?UKau9yqEU{c+czys3()w6RtoIxs*;@^N zJZfcbE%3d1eBFs%RzWCgRTXmZA>X~OXl?VBsf=>O=iXWb)WoH^))gPU zMI7Ml(oh+tDgNUyCfUiE;-RH(OE2X3vCwC2#K?bnTA)#{wN?^SoQbsJ5S?>jvu^ZX zj=HR#oPs3I=du&9^5nhV8L~$Om!wh(-@LBc2w)%rhIyG;SJVaJa?r&-YIMKTk2{e+2Urer57;RvwyNNDzc-{QVGxS^C zfDSUu@u#b!^a?Z2#BG(UFnnePzB5bP^ACPB25>Z{7Qn>`l5IUim^l*1-gj_ag&ADF zG7{dCncP9^6#<^PHtqEUs4fDq-tIoQA*t4HbV2ZQ(|UjunnMmyq?;(=Qp`^+-%+YJ z{w;bg!aUyF9;$$jTkRi9xKQS->)o>O&Nu7lF-`<)%aw`GDh>U;WgE`RwPxfwTSLtk zhLF$IAXUIORFUg2KmNfh9^AShv-{6g5!%oRc9D`KYz{&z63oQ-;hFpd znQvHz1>}kVXS`E`!pE(&5317tzh~&ShZ8EX6ttcozVcDbc=-`YlMDPR?(H&hOTRIx zt_--+O?I{`2wg7tX+q~$d}>cUMSrJtz9Q>CK5r(a4ZWyb@WNq=gVkrH)FdU@kqYu0 zV-{&QeMT*u`GE*sbZs^U@o>p-eMF$gCuaM&>G?G*WjIf~23BO@5H7K;Zew&by3Gap zx)&~U?gvx3#b~fOuA)$-2Gz(huIz?YV}SPO?Z(b%IH9T>BLOzn5`Ig58r8P(fIj(; zXRFhhe<#&$BNH>7eMvD_S! z)y8-(yJxo8!oEv#^j@In6IXs95M0~Amhk|7!vds##Pa_59bofRf?Ks6RuJ;oChM5# zT!5C0pb_$sx8jyiel%lGp&rhbBzBP#Yi&_!s#mY4K(Rv_nXTpK&aoU)Co+ad2j8ks zk}S4*D29!WGjvAt)aD#F$9PftLLYyB;zU2d-K`k5Nf4N$x${yYr(ua@*+9c> zsX(JF(z|&Qk7$IeKEddr46RZ0MXn}T=^MXsXJ=ahfb%AzT=oaS+_h-o8b0uw_E;=) zX-a1b4Vh0hkmK2lm4u1Fvs$8wx}N@MiFi3zyL+IUk6BCG?qUlWWa{=41?~Pwnplz@ zwD*Kh-#OaDIvX#p-SIB^l(}Q?BG3Qq0>Ng$r@*28w!RD~RGc`KEgpcjh&HI9H&1gP z#MuNNAJjzGIuCJ=TYN*>Uy8ETAIuqVkVB@-GfO)M8w%;BLBQHw(&Oqkzm#3H=$vYQ zHSje6HX-~?fJv2d>p!Z%qhAD>ik_)RHAF_o<{TerjvrBTsSGEdz88uTC%;0r(Z_j9 zXOPs8rzRpH^+)%;yJ}pNSeX12y1?t+L;xQZ)|<=moqb5WxFLHEeoZCQm(+=F0Z&z+ z_yW|~Ssh=PI7-!auqE0tX$eHowf={y`;3lqBBLBn>@YYwfjfhB9)XC*A3lN!88$rr zUN5q;)rUq0mUB`n>bAIPffp>0F2^98g&@tCO^ffvhZ0X9E05S-GzXG^y1VlRj8&9n zQC_c5ah$WsG#=RAWv8c*$Z@7U094Uo4gLNK9E91)~_bD zeC2=li%0?!XA%MGKrkU{hvKO6RTmT#rXEU8Y`5X0qD6I`T*1+ZV+xoIrMorKZ-iLg zF2tTf&A&(c^r=kf9f{Z>Yf~mqWZ(RI^Z1%!>WtAV_LZm7=@zD17MfV!>UOYX?17R3 z>ViUWih;?wFzeWR(q}#kG{KCXM>_-;L*u@(_NCd}k4ii>SkicSjM59GjXECkR|PV3 z7mps9ezE9R)*i_G z6YRD+Q+o(sHjYd-TBj02B(_shTNEju%@dN z2&dxolyA%TcY;~ow$q;rmoT9R995CVzv3G0$0eAT`f;}HY-m3VZouHRUqW10q|$;y zcH(4#n~VCp2}q?3dQE9N2Qr-34WMKQ1)s|DSIZ=reIO-Jaa@DV>1m|=>`DL7S$cGo3kCi< z8A`Gk9sTrCC6oXG2b3oW#mdEV>Sv+#T2Fb&aWG*J$zCqm72%8{o$tr;l0-RJhI^xs z4s&_CSVmf^F&hObbp|F~77>b0@oHd=pA(oZxM9DMOXGJ8q5Tz^*p9loZ9+14q{O|f z^uR2+*085dv%g(wY*ZA*SVDgt9xe&^n**~ zOrmEN9q{TC?I{x|FbobSu&s^#Ai|Aqm!=ett1?a}M{z;oQ(<@wd}UXqqb*AU)w;WR z^D<`&!vTc|{+tt>lgmz1UnacSEOZ6Rd&M{Vx%4#*LdE0g8GTrtEpx2m?@m7uQ z?Xw)Ufmf+0<7~9Q^6Z}=qwiV-Oi+*;8Zv@T}lge;sgpbw-$uCxpPTPBD~B_D!n8%ChlYOrfcFbGH;O zw!b0b`5-dnO38imE>}ZLre~blPYhy0NPYd5W5TCoL2t~AoYOHM4MU*4(9lot${7pz zv93Kb%7hU*hRdGCY+m8zc~tCp$|Sw9zgY8iKI%&wG6=<~_1-ExCuCJ+eYBJk!4d?kft?c1#A0nCBIU=GaQHWCS^PN;{H}J%DnmtMmBsyQZAjT9+d1J zoUPlsdQHaJr8{NxB3yK@xw$HBd!QUSpNruohq*6&N{x6A9)(>w$a(Fv@%B~Z?zsCL zX7Vd8)tGWswe9InCmc>!N-E&G8I@=rwBS2Er*74=Gg->4$Mu>&M|SVI4m!{%BxkTm zc&~XjW>6&H-njs%&p+HJ5t3@|f1EFxe6}g)t6wLGA_D}4lU7E%k?X9~cs(jeU#P(h z^DkxP;|6UO$ExSy@|hUn)O0033!lIIdaNr0#Ji!Xcz{pQG{5be;%L)xruCt=rZ1JK z^%K()%~Zm<_wm3^s{RQ%_03%xlXUB%0H`I>{9M-hC*Uphj>KC9mnE~2qqYLn81Bb7 zKe$?`KRrz*VA9f^=-L!y!0#%@%1Dn_f&RGN^-)p`rIDkUaQ%dp7MdCf!vAjTaDZ!8 z%D5=iHPZZW2E5F0cNOSZdjwwUS?FxXMOG%_6N0l_1+`TE3c9T^YN|zU4*FqpMHugN zTwJz32zQ9o)60+g>Mljc4CIpF-w4}@r+n_nmbMA~nX>YU^bsiDPE&c_cZ8Rxy@0wX zKp;JFj5o;iVCNjFpm}>h$z=ZU+*7`Xq_+Cn{M+v~HH9a6yw+qGC2U1nXsILdKxcU; zmp%xi$TMZBJS}5zvL9>d@PFYhIN)!vE2Op+#4*>DG{Z2t>sb1naglRve{?k|E`8EE zU(b(I?h*B3)+Zo|7MX*elEAgRNnwi+!fcwrMUV=ARt>ZtFmkXt$wM1o{$`7^m9+Bj zv_j?P?$r1az+d_^R^|RqpvxX>cciiJ&*`3Cq)!mr@0J~fhYksklmy?g{`vNEy93>? zqaa;|_H6pTvA1*>ZtIxIlcgKp;8Ja20SU@YK$D)5(l`8LLb^f^dXidiQf_tnzKf6~ zrGNz674oKtF5?p~iWL0&*?GAVsg7Y*Dwj;K^Mah!XH_P#7Orv@7dKHe@9!tu8T8$U zta>G>>HTHO?d$vyV?OL?FBlQuKgeYd_?AN|GWk?HSvxqWm{Bp!MV*;A?oTw9h z^Z&qr?V+bt=+CPUfN6UH7ytQ!M75;I-0}48BCHA&sB`!VX^VVXHP?dmkmv7{+e{L& zO#l{}1?T;b-7ihxUL6B{;jgGg3Aiao=K-}RIcJ-!e;Ze?>Vgdee*{C8%ojcx6kCyo*B$Fo_ zPhBokqap&VKm^}3H&vD#v=L;TJ$$Cg&V-FtAgt3&iE>z>qpiNK(P|cY`Rt8T(odf@ z3*xY2QQflO)+y-u)D3Ks)0l(j^>l-icptANya@+blOn+vB;P^Iw_}*uIXh1 zq7ZNO#|TyHVJKMX1YaMpu&gKv98V<0LG4O64rK-C1O_ZVkUKO8!gT`NngUH@132!} z?BF+vlMDS_!r<5R0H%nCUcyj49elaq{ht-j1_TT)4sfD;Js>IFpdt?T9<1!K?<9Fs zxCDxkWI2!?zeU+{GuVuoSDF@J_xM>R8*RTIRRNR9&UEwzsP%2S>Mdx)0=R{f1-Mg_ z{MO8tr{NbZm2mqoX=aEsoh=LbyTJ1lu}T8Fq}{Vgg4XA%9obB3pQP^$Y~Em`@3?{k PXaxjfazdByY$BKz9}}aR literal 2841 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 diff --git a/test/scrambletest.py b/test/scrambletest.py index 7acb3b91..eac7293d 100755 --- a/test/scrambletest.py +++ b/test/scrambletest.py @@ -63,12 +63,13 @@ test_data = OrderedDict([ ('ltc', ('b11f16632e63ba92','ltc:legacy', '-LTC','LTC', 'LMxB474SVfxeYdqxNrM1WZDZMnifteSMv1')), ('ltc_compressed',('7ccf465d466ee7d3','ltc:compressed', '-LTC-C','LTC:COMPRESSED', 'LdkebBKVXSs6NNoPJWGM8KciDnL8LhXXjb')), ('ltc_segwit', ('9460f5ba15e82768','ltc:segwit', '-LTC-S','LTC:SEGWIT', 'MQrY3vEbqKMBgegXrSaR93R2HoTDE5bKrY')), -('eth', ('213ed116869b19f2','eth', '-ETH', 'ETH','e704b6cfd9f0edb2e6cfbd0c913438d37ede7b35')), -('etc', ('909def37096f5ab8','etc', '-ETC', 'ETC','1a6acbef8c38f52f20d04ecded2992b04d8608d7')), -('dash', ('1319d347b021f952','dash:legacy', '-DASH','DASH','XoK491fppGNZQUUS9uEFkT6L9u8xxVFJNJ')), -('zec', ('0bf9b5b20af7b5a0','zec:legacy', '-ZEC', 'ZEC', 't1URz8BHxV38v3gsaN6oHQNKC16s35R9WkY')), -('zec_zcash_z', ('b15570d033df9b1a','zec:zcash_z', '-ZEC-Z','ZEC:ZCASH_Z','zcLMMsnzfFYZWU4avRBnuc83yh4jTtJXbtP32uWrs3ickzu1krMU4ppZCQPTwwfE9hLnRuFDSYF8VFW13aT9eeQK8aov3Ge')), -('emc', ('7e1a29853d2db875','emc:legacy', '-EMC', 'EMC','EU4L6x2b5QUb2gRQsBAAuB8TuPEwUxCNZU')), +('eth', ('213ed116869b19f2','eth', '-ETH', 'ETH', 'e704b6cfd9f0edb2e6cfbd0c913438d37ede7b35')), +('etc', ('909def37096f5ab8','etc', '-ETC', 'ETC', '1a6acbef8c38f52f20d04ecded2992b04d8608d7')), +('dash', ('1319d347b021f952','dash:legacy', '-DASH', 'DASH','XoK491fppGNZQUUS9uEFkT6L9u8xxVFJNJ')), +('emc', ('7e1a29853d2db875','emc:legacy', '-EMC', 'EMC', 'EU4L6x2b5QUb2gRQsBAAuB8TuPEwUxCNZU')), +('zec', ('0bf9b5b20af7b5a0','zec:legacy', '-ZEC', 'ZEC', 't1URz8BHxV38v3gsaN6oHQNKC16s35R9WkY')), +('zec_zcash_z', ('b15570d033df9b1a','zec:zcash_z', '-ZEC-Z','ZEC:ZCASH_Z','zcLMMsnzfFYZWU4avRBnuc83yh4jTtJXbtP32uWrs3ickzu1krMU4ppZCQPTwwfE9hLnRuFDSYF8VFW13aT9eeQK8aov3Ge')), +('xmr', ('c76af3b088da3364','xmr:monero', '-XMR-M','XMR:MONERO','41tmwZd2CdXEGtWqGY9fH9FVtQM8VxZASYPQ3VJQhFjtGWYzQFuidD21vJYTi2yy3tXRYXTNXBTaYVLav62rwUUpFFyicZU')), ]) def run_tests(): diff --git a/test/test.py b/test/test.py index bdcec197..f72df721 100755 --- a/test/test.py +++ b/test/test.py @@ -327,6 +327,7 @@ cfgs = { }, 'passfile_chk': 'EB29 DC4F 924B 289F', 'passfile32_chk': '37B6 C218 2ABC 7508', + 'passfilehex_chk': '523A F547 0E69 8323', 'wpasswd': 'reference password', 'ref_wallet': 'FE3C6545-D782B529[128,1].mmdat', 'ic_wallet': 'FE3C6545-E29303EA-5E229E30[128,1].mmincog', @@ -378,6 +379,7 @@ cfgs = { }, 'passfile_chk': 'ADEA 0083 094D 489A', 'passfile32_chk': '2A28 C5C7 36EC 217A', + 'passfilehex_chk': 'B11C AC6A 1464 608D', 'wpasswd': 'reference password', 'ref_wallet': '1378FC64-6F0F9BB4[192,1].mmdat', 'ic_wallet': '1378FC64-2907DE97-F980D21F[192,1].mmincog', @@ -429,6 +431,7 @@ cfgs = { }, 'passfile_chk': '2D6D 8FBA 422E 1315', 'passfile32_chk': 'F6C1 CDFB 97D9 FCAE', + 'passfilehex_chk': 'BD4F A0AC 8628 4BE4', 'wpasswd': 'reference password', 'ref_wallet': '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.testnet]), 'ref_addrfile': '98831F3A{}[1,31-33,500-501,1010-1011]{}.addrs', @@ -449,11 +452,13 @@ cfgs = { }, 'ref_addrfile_chksum_zec': '903E 7225 DD86 6E01', 'ref_addrfile_chksum_zec_z': '9C7A 72DC 3D4A B3AF', + 'ref_addrfile_chksum_xmr': '4369 0253 AC2C 0E38', 'ref_addrfile_chksum_dash':'FBC1 6B6A 0988 4403', 'ref_addrfile_chksum_eth': 'E554 076E 7AF6 66A3', 'ref_addrfile_chksum_etc': 'E97A D796 B495 E8BC', 'ref_keyaddrfile_chksum_zec': 'F05A 5A5C 0C8E 2617', - 'ref_keyaddrfile_chksum_zec_z': '220F 5F23 CC9B EC1F', + 'ref_keyaddrfile_chksum_zec_z': '4ADB 5AA4 4590 B60A', + 'ref_keyaddrfile_chksum_xmr': 'E0D7 9612 3D67 404A', 'ref_keyaddrfile_chksum_dash': 'E83D 2C63 FEA2 4142', 'ref_keyaddrfile_chksum_eth': '3635 4DCF B752 8772', 'ref_keyaddrfile_chksum_etc': '9BAC 38E7 5C8E 42E0', @@ -602,6 +607,7 @@ cmd_group['ref'] = ( ('refkeyaddrgen_compressed', (['mmdat',pwfile],'new refwallet key-addr chksum (compressed)')), ('refpasswdgen', (['mmdat',pwfile],'new refwallet passwd file chksum')), ('ref_b32passwdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (base32)')), + ('ref_hexpasswdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (base32)')), ) # misc. saved reference data @@ -715,15 +721,17 @@ cmd_group['altcoin_ref'] = ( ('ref_addrfile_chk_etc', 'reference address file (ETC)'), ('ref_addrfile_chk_dash','reference address file (DASH)'), ('ref_addrfile_chk_zec', 'reference address file (ZEC-T)'), + ('ref_addrfile_chk_xmr', 'reference address file (XMR)'), ('ref_addrfile_chk_zec_z','reference address file (ZEC-Z)'), ('ref_keyaddrfile_chk_eth', 'reference key-address file (ETH)'), ('ref_keyaddrfile_chk_etc', 'reference key-address file (ETC)'), ('ref_keyaddrfile_chk_dash','reference key-address file (DASH)'), ('ref_keyaddrfile_chk_zec', 'reference key-address file (ZEC-T)'), ('ref_keyaddrfile_chk_zec_z','reference key-address file (ZEC-Z)'), + ('ref_keyaddrfile_chk_xmr', 'reference key-address file (XMR)'), ) -# undocumented admin cmds +# undocumented admin cmds - precede with 'admin' cmd_group_admin = OrderedDict() cmd_group_admin['create_ref_tx'] = ( ('ref_tx_setup', 'regtest (Bob and Alice) mode setup'), @@ -1428,8 +1436,9 @@ class MMGenTestSuite(object): chk = t.expect_getend(r'Checksum for {} data .*?: '.format(desc),regex=True) if check_ref: k = 'passfile32_chk' if ftype == 'pass32' \ - else 'passfile_chk' if ftype == 'pass' \ - else '{}file{}_chk'.format(ftype,'_'+mmtype if mmtype else '') + else 'passfilehex_chk' if ftype == 'passhex' \ + else 'passfile_chk' if ftype == 'pass' \ + else '{}file{}_chk'.format(ftype,'_'+mmtype if mmtype else '') chk_ref = cfg[k] if ftype[:4] == 'pass' else cfg[k][fork][g.testnet] refcheck('address data checksum',chk,chk_ref) return @@ -1739,6 +1748,10 @@ class MMGenTestSuite(object): ea = ['--base32','--passwd-len','17'] self.addrgen(name,wf,pf,check_ref=True,ftype='pass32',id_str='фубар@crypto.org',extra_args=ea) + def ref_hexpasswdgen(self,name,wf,pf): + ea = ['--hex'] + self.addrgen(name,wf,pf,check_ref=True,ftype='passhex',id_str='фубар@crypto.org',extra_args=ea) + def txsign_keyaddr(self,name,keyaddr_file,txfile): t = MMGenExpect(name,'mmgen-txsign', ['-d',cfg['tmpdir'],'-M',keyaddr_file,txfile]) t.license() @@ -2060,6 +2073,9 @@ class MMGenTestSuite(object): self.ref_addrfile_chk(name,ftype='addr',coin='ZEC',subdir='zcash',pfx='-ZEC-Z', mmtype='z',add_args=['mmtype=zcash_z']) + def ref_addrfile_chk_xmr(self,name): + self.ref_addrfile_chk(name,ftype='addr',coin='XMR',subdir='monero',pfx='-XMR-M') + def ref_addrfile_chk_dash(self,name): self.ref_addrfile_chk(name,ftype='addr',coin='DASH',subdir='dash',pfx='-DASH-C') @@ -2076,6 +2092,9 @@ class MMGenTestSuite(object): self.ref_addrfile_chk(name,ftype='keyaddr',coin='ZEC',subdir='zcash',pfx='-ZEC-Z', mmtype='z',add_args=['mmtype=zcash_z']) + def ref_keyaddrfile_chk_xmr(self,name): + self.ref_addrfile_chk(name,ftype='keyaddr',coin='XMR',subdir='monero',pfx='-XMR-M') + def ref_keyaddrfile_chk_dash(self,name): self.ref_addrfile_chk(name,ftype='keyaddr',coin='DASH',subdir='dash',pfx='-DASH-C') @@ -2715,7 +2734,8 @@ class MMGenTestSuite(object): 'refaddrgen_compressed', 'refkeyaddrgen_compressed', 'refpasswdgen', - 'ref_b32passwdgen' + 'ref_b32passwdgen', + 'ref_hexpasswdgen' ): for i in ('1','2','3'): locals()[k+i] = locals()[k]