From 3b0257358b4dde6611f8932930df95d92165dda7 Mon Sep 17 00:00:00 2001 From: philemon Date: Thu, 27 Jul 2017 22:55:52 +0300 Subject: [PATCH] Support for Segwit (P2SH-P2WPKH) addresses: - Generate Segwit addresses by invoking 'mmgen-addrgen' with the '--type segwit' option - Import Segwit addresses into the tracking wallet as usual - Segwit and legacy MMGen addresses are distinguished by 'S' and 'L' identifiers in the tracking wallet and command line Transaction example: mmgen-txcreate F00BAA12:L:21,1.23 F00BAA12:S:1 (spend 1.23 BTC to legacy address 21 of your default wallet (with Seed ID F00BAA12) and send the change to Segwit address 1) Segwit and legacy addresses for a given seed are generated from different sub-seeds so are cryptographically unrelated to each other. Since MMGen's legacy P2PKH addresses are uncompressed, use of the new Segwit addresses significantly reduces transaction size. Until Segwit activation on mainnet, users can try out the new functionality on testnet or in regtest mode. --- data_files/mmgen.cfg | 3 + mmgen/addr.py | 333 +++++----- mmgen/bitcoin.py | 64 +- mmgen/common.py | 14 + mmgen/filename.py | 24 +- mmgen/globalvars.py | 21 +- mmgen/main_addrgen.py | 30 +- mmgen/main_addrimport.py | 66 +- mmgen/main_passgen.py | 5 +- mmgen/main_tool.py | 27 +- mmgen/main_txcreate.py | 2 +- mmgen/main_txdo.py | 2 +- mmgen/main_txsend.py | 12 +- mmgen/main_txsign.py | 2 +- mmgen/main_wallet.py | 13 +- mmgen/obj.py | 288 +++++++-- mmgen/opts.py | 35 +- mmgen/rpc.py | 23 +- mmgen/seed.py | 27 +- mmgen/share/Opts.py | 6 +- mmgen/test.py | 3 +- mmgen/tool.py | 577 +++++++++++------- mmgen/tw.py | 234 ++++--- mmgen/tx.py | 410 ++++++++++--- mmgen/txcreate.py | 169 ++--- mmgen/txsign.py | 91 +-- mmgen/util.py | 55 +- scripts/traceback.py | 20 + test/gentest.py | 36 +- ...8831F3A-S[1,31-33,500-501,1010-1011].addrs | 19 + ...S[1,31-33,500-501,1010-1011].testnet.addrs | 19 + test/test.py | 244 ++++---- test/tooltest.py | 328 +++++----- 33 files changed, 2045 insertions(+), 1157 deletions(-) create mode 100755 scripts/traceback.py create mode 100644 test/ref/98831F3A-S[1,31-33,500-501,1010-1011].addrs create mode 100644 test/ref/98831F3A-S[1,31-33,500-501,1010-1011].testnet.addrs diff --git a/data_files/mmgen.cfg b/data_files/mmgen.cfg index 171a80f5..31e99560 100644 --- a/data_files/mmgen.cfg +++ b/data_files/mmgen.cfg @@ -17,6 +17,9 @@ # Uncomment to force 256-color output when 'color' is true: # force_256_color true +# Uncomment to use regtest mode (this also sets testnet to true): +# regtest true + # Uncomment to use testnet instead of mainnet: # testnet true diff --git a/mmgen/addr.py b/mmgen/addr.py index c857f56a..fd3b9423 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -23,31 +23,17 @@ addr.py: Address generation/display routines for the MMGen suite from hashlib import sha256,sha512 from binascii import hexlify,unhexlify from mmgen.common import * -from mmgen.bitcoin import privnum2addr,hex2wif,wif2hex +from mmgen.bitcoin import hex2wif,wif2hex,wif_is_compressed from mmgen.obj import * from mmgen.tx import * from mmgen.tw import * pnm = g.proj_name -def _test_for_keyconv(silent=False): - no_keyconv_errmsg = """ -Executable '{kconv}' unavailable. Please install '{kconv}' from the {vgen} -package on your system or specify the secp256k1 library. -""".format(kconv=g.keyconv_exec, vgen='vanitygen') - from subprocess import check_output,STDOUT - try: - check_output([g.keyconv_exec, '-G'],stderr=STDOUT) - except: - if not silent: msg(no_keyconv_errmsg.strip()) - return False - return True - def _test_for_secp256k1(silent=False): no_secp256k1_errmsg = """ -secp256k1 library unavailable. Will use '{kconv}', or failing that, the (slow) -native Python ECDSA library for address generation. -""".format(kconv=g.keyconv_exec) +secp256k1 library unavailable. Using (slow) native Python ECDSA library for address generation. +""" try: from mmgen.secp256k1 import priv2pub assert priv2pub(os.urandom(32),1) @@ -56,58 +42,68 @@ native Python ECDSA library for address generation. return False return True -def _wif2addr_python(wif): +def _pubhex2addr(pubhex,mmtype): + if mmtype == 'L': + from mmgen.bitcoin import hexaddr2addr,hash160 + return hexaddr2addr(hash160(pubhex)) + elif mmtype == 'S': + from mmgen.bitcoin import pubhex2segwitaddr + return pubhex2segwitaddr(pubhex) + else: + die(2,"'{}': mmtype unrecognized".format(mmtype)) + +def _privhex2addr_python(privhex,compressed,mmtype): + assert compressed or mmtype != 'S' + from mmgen.bitcoin import privnum2pubhex + pubhex = privnum2pubhex(int(privhex,16),compressed=compressed) + return _pubhex2addr(pubhex,mmtype=mmtype) + +def _privhex2addr_secp256k1(privhex,compressed,mmtype): + assert compressed or mmtype != 'S' + from mmgen.secp256k1 import priv2pub + pubhex = hexlify(priv2pub(unhexlify(privhex),int(compressed))) + return _pubhex2addr(pubhex,mmtype=mmtype) + +def _wif2addr_python(wif,mmtype): privhex = wif2hex(wif) if not privhex: return False - return privnum2addr(int(privhex,16),wif[0] != ('5','9')[g.testnet]) + return _privhex2addr_python(privhex,wif_is_compressed(wif),mmtype=mmtype) -def _wif2addr_keyconv(wif): - if wif[0] == ('5','9')[g.testnet]: - from subprocess import check_output - return check_output(['keyconv', wif]).split()[1] - else: - return _wif2addr_python(wif) +def _wif2addr_secp256k1(wif,mmtype): + privhex = wif2hex(wif) + if not privhex: return False + return _privhex2addr_secp256k1(privhex,wif_is_compressed(wif),mmtype=mmtype) -def _wif2addr_secp256k1(wif): - return _privhex2addr_secp256k1(wif2hex(wif),wif[0] != ('5','9')[g.testnet]) +def keygen_wif2pubhex(wif,selector): + privhex = wif2hex(wif) + if not privhex: return False + if selector == 1: + from mmgen.secp256k1 import priv2pub + return hexlify(priv2pub(unhexlify(privhex),int(wif_is_compressed(wif)))) + elif selector == 0: + from mmgen.bitcoin import privnum2pubhex + return privnum2pubhex(int(privhex,16),compressed=wif_is_compressed(wif)) -def _privhex2addr_python(privhex,compressed=False): - return privnum2addr(int(privhex,16),compressed) - -def _privhex2addr_keyconv(privhex,compressed=False): - if compressed: - return privnum2addr(int(privhex,16),compressed) - else: - from subprocess import check_output - return check_output(['keyconv', hex2wif(privhex,compressed=False)]).split()[1] - -def _privhex2addr_secp256k1(privhex,compressed=False): - from mmgen.secp256k1 import priv2pub - from mmgen.bitcoin import hexaddr2addr,pubhex2hexaddr - pubkey = priv2pub(unhexlify(privhex),int(compressed)) - return hexaddr2addr(pubhex2hexaddr(hexlify(pubkey))) - -def _keygen_selector(generator=None): - if generator: - if generator == 3 and _test_for_secp256k1(): return 2 - elif generator in (2,3) and _test_for_keyconv(): return 1 - else: - if opt.key_generator == 3 and _test_for_secp256k1(): return 2 - elif opt.key_generator in (2,3) and _test_for_keyconv(): return 1 +def keygen_selector(generator=None): + if _test_for_secp256k1() and generator != 1: + if opt.key_generator != 1: + return 1 msg('Using (slow) native Python ECDSA library for address generation') return 0 def get_wif2addr_f(generator=None): - gen = _keygen_selector(generator=generator) - return (_wif2addr_python,_wif2addr_keyconv,_wif2addr_secp256k1)[gen] + gen = keygen_selector(generator=generator) + return (_wif2addr_python,_wif2addr_secp256k1)[gen] def get_privhex2addr_f(generator=None): - gen = _keygen_selector(generator=generator) - return (_privhex2addr_python,_privhex2addr_keyconv,_privhex2addr_secp256k1)[gen] + gen = keygen_selector(generator=generator) + return (_privhex2addr_python,_privhex2addr_secp256k1)[gen] + class AddrListEntry(MMGenListItem): attrs = 'idx','addr','label','wif','sec' idx = MMGenListItemAttr('idx','AddrIdx') + wif = MMGenListItemAttr('wif','WifKey') class AddrListChksum(str,Hilite): color = 'pink' @@ -119,7 +115,7 @@ class AddrListChksum(str,Hilite): # print '[{}]'.format(' '.join(lines)) return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True)) -class AddrListID(unicode,Hilite): +class AddrListIDStr(unicode,Hilite): color = 'green' trunc_ok = False def __new__(cls,addrlist,fmt_str=None): @@ -138,7 +134,15 @@ class AddrListID(unicode,Hilite): ret += ',', i prev = i s = ''.join([unicode(i) for i in ret]) - return unicode.__new__(cls,fmt_str.format(s) if fmt_str else '{}[{}]'.format(addrlist.seed_id,s)) + + if fmt_str: + ret = fmt_str.format(s) + elif addrlist.al_id.mmtype == 'L': + ret = '{}[{}]'.format(addrlist.al_id.sid,s) + else: + ret = '{}-{}[{}]'.format(addrlist.al_id.sid,addrlist.al_id.mmtype,s) + + return unicode.__new__(cls,ret) class AddrList(MMGenObject): # Address info for a single seed ID msgs = { @@ -150,7 +154,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID # A text label of {n} characters or less may be added to the right of each # address, and it will be appended to the bitcoind wallet label upon import. # The label may contain any printable ASCII symbol. -""".strip().format(n=MMGenAddrLabel.max_len,pnm=pnm), +""".strip().format(n=TwComment.max_len,pnm=pnm), 'record_chksum': """ Record this checksum: it will be used to verify the address file in the future """.strip(), @@ -169,40 +173,46 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file gen_keys = False has_keys = False ext = 'addrs' + dfl_mmtype = MMGenAddrType('L') + cook_hash_rounds = 10 # not too many rounds, so hand decoding can still be feasible - def __init__(self,addrfile='',sid='',adata=[],seed='',addr_idxs='',src='', - addrlist='',keylist='',do_chksum=True,chksum_only=False): + def __init__(self,addrfile='',al_id='',adata=[],seed='',addr_idxs='',src='', + addrlist='',keylist='',mmtype=None,do_chksum=True,chksum_only=False): self.update_msgs() + mmtype = mmtype or self.dfl_mmtype + assert mmtype in MMGenAddrType.mmtypes - if addrfile: # data from MMGen address file - (sid,adata) = self.parse_file(addrfile) - elif sid and adata: # data from tracking wallet + if seed and addr_idxs: # data from seed + idxs + self.al_id,src = AddrListID(seed.sid,mmtype),'gen' + adata = self.generate(seed,addr_idxs,compressed=(mmtype=='S')) + elif addrfile: # data from MMGen address file + adata = self.parse_file(addrfile) # sets self.al_id + elif al_id and adata: # data from tracking wallet + self.al_id = al_id do_chksum = False - elif seed and addr_idxs: # data from seed + idxs - sid,src = seed.sid,'gen' - adata = self.generate(seed,addr_idxs) elif addrlist: # data from flat address list - sid = None - adata = [AddrListEntry(addr=a) for a in set(addrlist)] + self.al_id = None + adata = AddrListList([AddrListEntry(addr=a) for a in set(addrlist)]) elif keylist: # data from flat key list - sid,do_chksum = None,False - adata = [AddrListEntry(wif=k) for k in set(keylist)] + self.al_id = None + adata = AddrListList([AddrListEntry(wif=k) for k in set(keylist)]) elif seed or addr_idxs: die(3,'Must specify both seed and addr indexes') - elif sid or adata: - die(3,'Must specify both seed_id and adata') + elif al_id or adata: + die(3,'Must specify both al_id and adata') else: die(3,'Incorrect arguments for %s' % type(self).__name__) - # sid,adata now set - self.seed_id = sid + # al_id,adata now set self.data = adata self.num_addrs = len(adata) self.fmt_data = '' - self.id_str = None self.chksum = None - self.id_str = AddrListID(self) + + if self.al_id == None: return + + self.id_str = AddrListIDStr(self) if type(self) == KeyList: return @@ -221,17 +231,17 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file if k not in self.msgs: self.msgs[k] = AddrList.msgs[k] - def generate(self,seed,addrnums): + def generate(self,seed,addrnums,compressed): assert type(addrnums) is AddrIdxList - self.seed_id = SeedID(seed=seed) - seed = seed.get_data() + assert compressed in (True,False,None) + seed = seed.get_data() seed = self.cook_seed(seed) if self.gen_addrs: - privhex2addr_f = get_privhex2addr_f() + privhex2addr_f = get_privhex2addr_f() # choose internal ECDSA or secp256k1 generator - t_addrs,num,pos,out = len(addrnums),0,0,[] + t_addrs,num,pos,out = len(addrnums),0,0,AddrListList() while pos != t_addrs: seed = sha512(seed).digest() @@ -250,10 +260,10 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file sec = sha256(sha256(seed).digest()).hexdigest() if self.gen_addrs: - e.addr = privhex2addr_f(sec,compressed=False) + e.addr = privhex2addr_f(sec,compressed=compressed,mmtype=self.al_id.mmtype) if self.gen_keys: - e.wif = hex2wif(sec,compressed=False) + e.wif = hex2wif(sec,compressed=compressed) if opt.b16: e.sec = sec if self.gen_passwds: @@ -261,14 +271,31 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file dmsg('Key {:>03}: {}'.format(pos,sec)) out.append(e) + if g.debug: print 'generate():\n', e.pformat() qmsg('\r%s: %s %s%s generated%s' % ( - self.seed_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15)) + self.al_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15)) return out - def chk_addr_or_pw(self,addr): return is_btc_addr(addr) + def is_mainnet(self): + return self.data[0].addr.is_mainnet() - def cook_seed(self,seed): return seed + def is_for_current_chain(self): + return self.data[0].addr.is_for_current_chain() + + def chk_addr_or_pw(self,addr): + return {'L':'p2pkh','S':'p2sh'}[self.al_id.mmtype] == is_btc_addr(addr).addr_fmt + + def cook_seed(self,seed): + if self.al_id.mmtype == 'L': + return seed + else: + from mmgen.crypto import sha256_rounds + import hmac + key = self.al_id.mmtype.name + cseed = hmac.new(seed,key,sha256).digest() + dmsg('Seed: {}\nKey: {}\nCseed: {}\nCseed len: {}'.format(hexlify(seed),key,hexlify(cseed),len(cseed))) + return sha256_rounds(cseed,self.cook_hash_rounds) def encrypt(self,desc='new key list'): from mmgen.crypto import mmgen_encrypt @@ -284,7 +311,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file return [e.idx for e in self.data] def addrs(self): - return ['%s:%s'%(self.seed_id,e.idx) for e in self.data] + return ['%s:%s'%(self.al_id.sid,e.idx) for e in self.data] def addrpairs(self): return [(e.idx,e.addr) for e in self.data] @@ -313,21 +340,18 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file e.label = comment def make_reverse_dict(self,btcaddrs): - d,b = {},btcaddrs + d,b = MMGenDict(),btcaddrs for e in self.data: try: - d[b[b.index(e.addr)]] = ('%s:%s'%(self.seed_id,e.idx),e.label) + d[b[b.index(e.addr)]] = MMGenID('{}:{}'.format(self.al_id,e.idx)),e.label except: pass return d def flat_list(self): class AddrListFlatEntry(AddrListEntry): attrs = 'mmid','addr','wif' - return [AddrListFlatEntry( - mmid='{}:{}'.format(self.seed_id,e.idx), - addr=e.addr, - wif=e.wif) - for e in self.data] + return [AddrListFlatEntry(mmid='{}:{}'.format(self.al_id,e.idx),addr=e.addr,wif=e.wif) + for e in self.data] def remove_dups(self,cmplist,key='wif'): pop_list = [] @@ -338,7 +362,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file pop_list.append(n) for n in reversed(pop_list): self.data.pop(n) if pop_list: - vmsg(self.msgs['removed_dups'] % (len(pop_list),suf(removed,'k'))) + vmsg(self.msgs['removed_dups'] % (len(pop_list),suf(removed,'s'))) def add_wifs(self,al_key): if not al_key: return @@ -355,13 +379,15 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file def get_addrs(self): return self.get('addr') def get_wifs(self): return self.get('wif') + def get_addr_wif_pairs(self): + return [(d.addr,d.wif) for d in self.data if hasattr(d,'wif')] - def generate_addrs(self): + def generate_addrs_from_keylist(self): wif2addr_f = get_wif2addr_f() d = self.data for n,e in enumerate(d,1): qmsg_r('\rGenerating addresses from keylist: %s/%s' % (n,len(d))) - e.addr = wif2addr_f(e.wif) + e.addr = wif2addr_f(e.wif,mmtype='L') # 'L' == p2pkh qmsg('\rGenerated addresses from keylist: %s/%s ' % (n,len(d))) def format(self,enable_comments=False): @@ -385,9 +411,11 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file if type(self) == PasswordList: out.append(u'{} {} {}:{} {{'.format( - self.seed_id,self.pw_id_str,self.pw_fmt,self.pw_len)) + self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len)) + elif self.al_id.mmtype == 'L': + out.append('{} {{'.format(self.al_id.sid)) else: - out.append('{} {{'.format(self.seed_id)) + out.append('{} {} {{'.format(self.al_id.sid,MMGenAddrType.mmtypes[self.al_id.mmtype].upper())) fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx)) for e in self.data: @@ -410,7 +438,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file if self.has_keys and len(lines) % 2: return 'Key-address file has odd number of lines' - ret = [] + ret = AddrListList() while lines: l = lines.pop(0) @@ -444,7 +472,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file llen = len(ret) for n,e in enumerate(ret): msg_r('\rVerifying keys %s/%s' % (n+1,llen)) - if e.addr != wif2addr_f(e.wif): + if e.addr != wif2addr_f(e.wif,mmtype=self.al_id.mmtype): return "Key doesn't match address!\n %s\n %s" % (e.wif,e.addr) msg(' - done') @@ -463,29 +491,44 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file return do_error("Too few lines in address file (%s)" % len(lines)) ls = lines[0].split() - ls_len = (2,4)[type(self)==PasswordList] - if len(ls) != ls_len: + if not 1 < len(ls) < 5: return do_error("Invalid first line for {} file: '{}'".format(self.gen_desc,lines[0])) - if ls[-1] != '{': + if ls.pop() != '{': return do_error("'%s': invalid first line" % ls) if lines[-1] != '}': return do_error("'%s': invalid last line" % lines[-1]) - if not is_mmgen_seed_id(ls[0]): + + sid = ls.pop(0) + if not is_mmgen_seed_id(sid): return do_error("'%s': invalid Seed ID" % ls[0]) - if type(self) == PasswordList: - self.pw_id_str = MMGenPWIDString(ls[1]) - ss = ls[2].split(':') + if type(self) == PasswordList and len(ls) == 2: + ss = ls.pop().split(':') if len(ss) != 2: return do_error("'%s': invalid password length specifier (must contain colon)" % ls[2]) self.set_pw_fmt(ss[0]) self.set_pw_len(ss[1]) + self.pw_id_str = MMGenPWIDString(ls.pop()) + mmtype = MMGenPasswordType('P') + elif len(ls) == 1: + mmtype = ls.pop().lower() + try: + mmtype = MMGenAddrType(mmtype) + except: + return do_error(u"'{}': invalid address type in address file. Must be one of: {}".format( + mmtype,' '.join(MMGenAddrType.mmtypes.values()).upper())) + elif len(ls) == 0: + mmtype = MMGenAddrType('L') + else: + return do_error(u"Invalid first line for {} file: '{}'".format(self.gen_desc,lines[0])) - ret = self.parse_file_body(lines[1:-1]) - if type(ret) != list: - return do_error(ret) + self.al_id = AddrListID(SeedID(sid=sid),mmtype) - return ls[0],ret + data = self.parse_file_body(lines[1:-1]) + if not issubclass(type(data),list): + return do_error(data) + + return data class KeyAddrList(AddrList): data_desc = 'key-address' @@ -525,7 +568,7 @@ class PasswordList(AddrList): # A text label of {n} characters or less may be added to the right of each # password. The label may contain any printable ASCII symbol. # -""".strip().format(n=MMGenAddrLabel.max_len,pnm=pnm), +""".strip().format(n=TwComment.max_len,pnm=pnm), 'record_chksum': """ Record this checksum: it will be used to verify the password file in the future """.strip() @@ -546,7 +589,6 @@ Record this checksum: it will be used to verify the password file in the future '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' } } - cook_hash_rounds = 10 # not too many rounds, so hand decoding can still be feasible def __init__(self,infile=None,seed=None,pw_idxs=None,pw_id_str=None,pw_len=None,pw_fmt=None, chksum_only=False,chk_params_only=False): @@ -554,7 +596,7 @@ Record this checksum: it will be used to verify the password file in the future self.update_msgs() if infile: - (self.seed_id,self.data) = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len + self.data = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len else: for k in seed,pw_idxs: assert chk_params_only or k for k in pw_id_str,pw_fmt: assert k @@ -562,8 +604,8 @@ Record this checksum: it will be used to verify the password file in the future self.set_pw_fmt(pw_fmt) self.set_pw_len(pw_len) if chk_params_only: return - self.seed_id = seed.sid - self.data = self.generate(seed,pw_idxs) + self.al_id = AddrListID(seed.sid,MMGenPasswordType('P')) + self.data = self.generate(seed,pw_idxs,compressed=None) self.num_addrs = len(self.data) self.fmt_data = '' @@ -572,8 +614,8 @@ Record this checksum: it will be used to verify the password file in the future if chksum_only: Msg(self.chksum) else: - self.id_str = AddrListID(self,fmt_str=u'{}-{}-{}-{}[{{}}]'.format( - self.seed_id,self.pw_id_str,self.pw_fmt,self.pw_len)) + fs = u'{}-{}-{}-{}[{{}}]'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len) + self.id_str = AddrListIDStr(self,fs) qmsg(u'Checksum for {} data {}: {}'.format(self.data_desc,self.id_str.hl(),self.chksum.hl())) qmsg(self.msgs[('record_chksum','check_chksum')[bool(infile)]]) @@ -626,6 +668,7 @@ Record this checksum: it will be used to verify the password file in the future from mmgen.crypto import sha256_rounds # Changing either pw_fmt, pw_len or id_str will cause a different, unrelated set of # passwords to be generated: this is what we want + # NB: In original implementation, pw_id_str was 'baseN', not 'bN' fid_str = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str.encode('utf8')) dmsg(u'Full ID string: {}'.format(fid_str.decode('utf8'))) # Original implementation was 'cseed = seed + fid_str'; hmac was not used @@ -646,54 +689,58 @@ re-import your addresses. } def __init__(self,source=None): - self.sids = {} + self.al_ids = {} if source == 'tw': self.add_tw_data() def seed_ids(self): - return self.sids.keys() + return self.al_ids.keys() - def addrlist(self,sid): - # TODO: Validate sid - if sid in self.sids: - return self.sids[sid] + def addrlist(self,al_id): + # TODO: Validate al_id + if al_id in self.al_ids: + return self.al_ids[al_id] def mmaddr2btcaddr(self,mmaddr): + al_id,idx = MMGenID(mmaddr).rsplit(':',1) btcaddr = '' - sid,idx = mmaddr.split(':') - if sid in self.seed_ids(): - btcaddr = self.addrlist(sid).btcaddr(int(idx)) - return btcaddr + if al_id in self.al_ids: + btcaddr = self.addrlist(al_id).btcaddr(int(idx)) + return btcaddr or None + + def btcaddr2mmaddr(self,btcaddr): + d = self.make_reverse_dict([btcaddr]) + return (d.values()[0][0]) if d else None def add_tw_data(self): - vmsg_r('Getting address data from tracking wallet...') + vmsg('Getting address data from tracking wallet') c = bitcoin_connection() accts = c.listaccounts(0,True) data,i = {},0 alists = c.getaddressesbyaccount([[k] for k in accts],batch=True) for acct,addrlist in zip(accts,alists): - maddr,label = parse_tw_acct_label(acct) - if maddr: + l = TwLabel(acct,on_fail='silent') + if l and l.mmid.type == 'mmgen': + obj = l.mmid.obj i += 1 if len(addrlist) != 1: die(2,self.msgs['too_many_acct_addresses'] % acct) - seed_id,idx = maddr.split(':') - if seed_id not in data: - data[seed_id] = [] - data[seed_id].append(AddrListEntry(idx=idx,addr=addrlist[0],label=label)) - vmsg('{n} {pnm} addresses found, {m} accounts total'.format( - n=i,pnm=pnm,m=len(accts))) - for sid in data: - self.add(AddrList(sid=sid,adata=data[sid])) + al_id = AddrListID(SeedID(sid=obj.sid),MMGenAddrType(obj.mmtype)) + if al_id not in data: + data[al_id] = [] + data[al_id].append(AddrListEntry(idx=obj.idx,addr=addrlist[0],label=l.comment)) + vmsg('{n} {pnm} addresses found, {m} accounts total'.format(n=i,pnm=pnm,m=len(accts))) + for al_id in data: + self.add(AddrList(al_id=al_id,adata=AddrListList(sorted(data[al_id],key=lambda a: a.idx)))) def add(self,addrlist): if type(addrlist) == AddrList: - self.sids[addrlist.seed_id] = addrlist + self.al_ids[addrlist.al_id] = addrlist return True else: raise TypeError, 'Error: object %s is not of type AddrList' % repr(addrlist) def make_reverse_dict(self,btcaddrs): - d = {} - for sid in self.sids: - d.update(self.sids[sid].make_reverse_dict(btcaddrs)) + d = MMGenDict() + for al_id in self.al_ids: + d.update(self.al_ids[al_id].make_reverse_dict(btcaddrs)) return d diff --git a/mmgen/bitcoin.py b/mmgen/bitcoin.py index 8517f790..65235461 100755 --- a/mmgen/bitcoin.py +++ b/mmgen/bitcoin.py @@ -53,33 +53,40 @@ b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' from mmgen.globalvars import g -def hash256(hexnum): # take hex, return hex - OP_HASH256 - return sha256(sha256(unhexlify(hexnum)).digest()).hexdigest() - def hash160(hexnum): # take hex, return hex - OP_HASH160 return hashlib_new('ripemd160',sha256(unhexlify(hexnum)).digest()).hexdigest() -pubhex2hexaddr = hash160 +def hash256(hexnum): # take hex, return hex - OP_HASH256 + return sha256(sha256(unhexlify(hexnum)).digest()).hexdigest() + +# devdoc/ref_transactions.md: +btc_ver_nums = { + 'p2pkh': (('00','1'),('6f','mn')), + 'p2sh': (('05','3'),('c4','2')) +} +addr_pfxs = { 'mainnet': '13', 'testnet': 'mn2', 'regtest': 'mn2' } +vnum_all = tuple([k for k,v in btc_ver_nums['p2pkh'] + btc_ver_nums['p2sh']]) def hexaddr2addr(hexaddr,p2sh=False): - # devdoc/ref_transactions.md: - s = ('00','6f','05','c4')[g.testnet+(2*p2sh)] + hexaddr.strip() + s = vnum_all[g.testnet+(2*p2sh)] + hexaddr.strip() lzeroes = (len(s) - len(s.lstrip('0'))) / 2 return ('1' * lzeroes) + _numtob58(int(s+hash256(s)[:8],16)) -def verify_addr(addr,verbose=False,return_hex=False): +def verify_addr(addr,verbose=False,return_hex=False,return_type=False): addr = addr.strip() - for vers_num,ldigit in ('00','1'),('05','3'),('6f','mn'),('c4','2'): - if addr[0] not in ldigit: continue - num = _b58tonum(addr) - if num == False: break - addr_hex = '{:050x}'.format(num) - if addr_hex[:2] != vers_num: continue - if hash256(addr_hex[:42])[:8] == addr_hex[42:]: - return addr_hex[2:42] if return_hex else True - else: - if verbose: Msg("Invalid checksum in address '%s'" % addr) - break + + for k in ('p2pkh','p2sh'): + for ver_num,ldigit in btc_ver_nums[k]: + if addr[0] not in ldigit: continue + num = _b58tonum(addr) + if num == False: break + addr_hex = '{:050x}'.format(num) + if addr_hex[:2] != ver_num: continue + if hash256(addr_hex[:42])[:8] == addr_hex[42:]: + return addr_hex[2:42] if return_hex else k if return_type else True + else: + if verbose: Msg("Invalid checksum in address '%s'" % addr) + break if verbose: Msg("Invalid address '%s'" % addr) return False @@ -97,7 +104,7 @@ def _b58tonum(b58num): b58num = b58num.strip() for i in b58num: if not i in b58a: return False - return sum([b58a.index(n) * (58**i) for i,n in enumerate(list(b58num[::-1]))]) + return sum(b58a.index(n) * (58**i) for i,n in enumerate(list(b58num[::-1]))) # The following are MMGen internal (non-Bitcoin) b58 functions @@ -146,9 +153,11 @@ def b58decode_pad(s): # Compressed address support: +def wif_is_compressed(wif): return wif[0] != ('5','9')[g.testnet] + def wif2hex(wif): wif = wif.strip() - compressed = wif[0] != ('5','9')[g.testnet] + compressed = wif_is_compressed(wif) num = _b58tonum(wif) if num == False: return False key = '{:x}'.format(num) @@ -178,5 +187,16 @@ def privnum2pubhex(numpriv,compressed=False): else: return '04'+pubkey -def privnum2addr(numpriv,compressed=False): - return hexaddr2addr(pubhex2hexaddr(privnum2pubhex(numpriv,compressed))) +def privnum2addr(numpriv,compressed=False,segwit=False): # used only by tool and testsuite + pubhex = privnum2pubhex(numpriv,compressed) + return pubhex2segwitaddr(pubhex) if segwit else hexaddr2addr(hash160(pubhex)) + +# Segwit: +def pubhex2redeem_script(pubhex): + # https://bitcoincore.org/en/segwit_wallet_dev/ + # The P2SH redeemScript is always 22 bytes. It starts with a OP_0, followed + # by a canonical push of the keyhash (i.e. 0x0014{20-byte keyhash}) + return '0014' + hash160(pubhex) + +def pubhex2segwitaddr(pubhex): + return hexaddr2addr(hash160(pubhex2redeem_script(pubhex)),p2sh=True) diff --git a/mmgen/common.py b/mmgen/common.py index a1ac332b..be39fb4b 100755 --- a/mmgen/common.py +++ b/mmgen/common.py @@ -25,3 +25,17 @@ from mmgen.globalvars import g import mmgen.opts as opts from mmgen.opts import opt from mmgen.util import * + +pw_note = """ +For passphrases all combinations of whitespace are equal and leading and +trailing space is ignored. This permits reading passphrase or brainwallet +data from a multi-line file with free spacing and indentation. +""".strip() + +bw_note = """ +BRAINWALLET NOTE: + +To thwart dictionary attacks, it's recommended to use a strong hash preset +with brainwallets. For a brainwallet passphrase to generate the correct +seed, the same seed length and hash preset parameters must always be used. +""".strip() diff --git a/mmgen/filename.py b/mmgen/filename.py index edb94638..5cf53444 100755 --- a/mmgen/filename.py +++ b/mmgen/filename.py @@ -32,11 +32,15 @@ class Filename(MMGenObject): self.basename = os.path.basename(fn) self.ext = get_extension(fn) self.ftype = None # the file's associated class + self.mtime = None + self.ctime = None + self.atime = None from mmgen.seed import SeedSource + from mmgen.tx import MMGenTX if ftype: if type(ftype) == type: - if issubclass(ftype,SeedSource): + if issubclass(ftype,SeedSource) or issubclass(ftype,MMGenTX): self.ftype = ftype # elif: # other MMGen file types else: @@ -44,6 +48,7 @@ class Filename(MMGenObject): else: die(3,"'%s': not a class" % ftype) else: + # TODO: other file types self.ftype = SeedSource.ext_to_type(self.ext) if not self.ftype: die(3,"'%s': not a recognized extension for SeedSource" % self.ext) @@ -64,6 +69,23 @@ class Filename(MMGenObject): os.close(fd) else: self.size = os.stat(fn).st_size + self.mtime = os.stat(fn).st_mtime + self.ctime = os.stat(fn).st_ctime + self.atime = os.stat(fn).st_atime + +class MMGenFileList(list,MMGenObject): + + def __init__(self,fns,ftype): + flist = [Filename(fn,ftype) for fn in fns] + return list.__init__(self,flist) + + def names(self): + return [f.name for f in self] + + def sort_by_age(self,key='mtime',reverse=False): + if key not in ('atime','ctime','mtime'): + die(1,"'{}': illegal sort key".format(key)) + self.sort(key=lambda a: getattr(a,key),reverse=reverse) def find_files_in_dir(ftype,fdir,no_dups=False): if type(ftype) != type: diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index bd9e0927..f1d7d1cd 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -31,13 +31,15 @@ from mmgen.obj import BTCAmt class g(object): + skip_segwit_active_check = bool(os.getenv('MMGEN_TEST_SUITE')) + def die(ev=0,s=''): if s: sys.stderr.write(s+'\n') sys.exit(ev) # Variables - these might be altered at runtime: - version = '0.9.1' - release_date = 'May 2017' + version = '0.9.199' + release_date = 'July 2017' proj_name = 'MMGen' proj_url = 'https://github.com/mmgen/mmgen' @@ -70,6 +72,10 @@ class g(object): color = (False,True)[sys.stdout.isatty()] force_256_color = False testnet = False + regtest = False + chain = None # set by first call to bitcoin_connection() + chains = 'mainnet','testnet','regtest' + bitcoind_version = None # set by first call to bitcoin_connection() rpc_host = '' rpc_port = 0 rpc_user = '' @@ -97,7 +103,7 @@ class g(object): # User opt sets global var: common_opts = ( 'color','no_license','rpc_host','rpc_port','testnet','rpc_user','rpc_password', - 'bitcoin_data_dir','force_256_color' + 'bitcoin_data_dir','force_256_color','regtest' ) required_opts = ( 'quiet','verbose','debug','outdir','echo_passphrase','passwd_file','stdout', @@ -114,7 +120,7 @@ class g(object): cfg_file_opts = ( 'color','debug','hash_preset','http_timeout','no_license','rpc_host','rpc_port', 'quiet','tx_fee_adj','usr_randchars','testnet','rpc_user','rpc_password', - 'bitcoin_data_dir','force_256_color','max_tx_fee' + 'bitcoin_data_dir','force_256_color','max_tx_fee','regtest' ) env_opts = ( 'MMGEN_BOGUS_WALLET_DATA', @@ -127,6 +133,7 @@ class g(object): 'MMGEN_NO_LICENSE', 'MMGEN_RPC_HOST', 'MMGEN_TESTNET' + 'MMGEN_REGTEST' ) min_screen_width = 80 @@ -136,8 +143,6 @@ class g(object): global_sets_opt = ['minconf','seed_len','hash_preset','usr_randchars','debug', 'quiet','tx_confs','tx_fee_adj','key_generator'] - keyconv_exec = 'keyconv' - mins_per_block = 9 passwd_max_tries = 5 @@ -151,8 +156,8 @@ class g(object): aesctr_iv_len = 16 hincog_chk_len = 8 - key_generators = 'python-ecdsa','keyconv','secp256k1' # 1,2,3 - key_generator = 3 # secp256k1 is default + key_generators = 'python-ecdsa','secp256k1' # '1','2' + key_generator = 2 # secp256k1 is default hash_presets = { # Scrypt params: diff --git a/mmgen/main_addrgen.py b/mmgen/main_addrgen.py index 3b174b8f..42a04372 100755 --- a/mmgen/main_addrgen.py +++ b/mmgen/main_addrgen.py @@ -25,18 +25,19 @@ from mmgen.common import * from mmgen.crypto import * from mmgen.addr import * from mmgen.seed import SeedSource +MAT = MMGenAddrType if sys.argv[0].split('-')[-1] == 'keygen': gen_what = 'keys' gen_desc = 'secret keys' opt_filter = None - note2 = 'By default, both addresses and secret keys are generated.\n\n' + note_addrkey = 'By default, both addresses and secret keys are generated.\n\n' else: gen_what = 'addresses' gen_desc = 'addresses' - opt_filter = 'hbcdeiHOKlpzPqrSv-' - note2 = '' -note1 = """ + opt_filter = 'hbcdeiHOKlpzPqrStv-' + note_addrkey = '' +note_secp256k1 = """ If available, the secp256k1 library will be used for address generation. """.strip() @@ -70,6 +71,8 @@ opts_data = { -r, --usr-randchars=n Get 'n' characters of additional randomness from user (min={g.min_urandchars}, max={g.max_urandchars}, default={g.usr_randchars}) -S, --stdout Print {what} to stdout +-t, --type=t Choose address type. Options: see ADDRESS TYPES below + (default: {dmat}) -v, --verbose Produce more verbose output -x, --b16 Print secret keys in hexadecimal too """.format( @@ -77,7 +80,8 @@ opts_data = { pnm=g.proj_name, kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]), kg=g.key_generator, - what=gen_what,g=g + what=gen_what,g=g, + dmat="'{}' or '{}'".format(MAT.dfl_mmtype,MAT.mmtypes[MAT.dfl_mmtype]) ), 'notes': """ @@ -87,26 +91,32 @@ opts_data = { Address indexes are given as a comma-separated list and/or hyphen-separated range(s). -{n2}{n1} +{n_addrkey}{n_secp} +ADDRESS TYPES: + {n_at} NOTES FOR ALL GENERATOR COMMANDS -{o.pw_note} +{pwn} -{o.bw_note} +{bwn} FMT CODES: {f} """.format( - n1=note1,n2=note2, + n_secp=note_secp256k1,n_addrkey=note_addrkey,pwn=pw_note,bwn=bw_note, f='\n '.join(SeedSource.format_fmt_codes().splitlines()), + n_at='\n '.join(["'{}', '{}'".format(k,v) for k,v in MAT.mmtypes.items()]), o=opts ) } cmd_args = opts.init(opts_data,add_opts=['b16'],opt_filter=opt_filter) +errmsg = "'{}': invalid parameter for --type option".format(opt.type) +addr_type = MAT(opt.type or MAT.dfl_mmtype,errmsg=errmsg) + if len(cmd_args) < 1: opts.usage() idxs = AddrIdxList(fmt_str=cmd_args.pop()) @@ -117,7 +127,7 @@ do_license_msg() ss = SeedSource(sf) i = (gen_what=='addresses') or bool(opt.no_addresses)*2 -al = (KeyAddrList,AddrList,KeyList)[i](seed=ss.seed,addr_idxs=idxs) +al = (KeyAddrList,AddrList,KeyList)[i](seed=ss.seed,addr_idxs=idxs,mmtype=addr_type) al.format() if al.gen_addrs and opt.print_checksum: diff --git a/mmgen/main_addrimport.py b/mmgen/main_addrimport.py index d134af8d..f7909b84 100755 --- a/mmgen/main_addrimport.py +++ b/mmgen/main_addrimport.py @@ -24,6 +24,7 @@ import time from mmgen.common import * from mmgen.addr import AddrList,KeyAddrList +from mmgen.obj import TwLabel # In batch mode, bitcoind just rescans each address separately anyway, so make # --batch and --rescan incompatible. @@ -35,6 +36,7 @@ opts_data = { 'options': """ -h, --help Print this help message --, --longhelp Print help message for long options (common options) +-a, --address=a Import the single Bitcoin address 'a' -b, --batch Import all addresses in one RPC call. -l, --addrlist Address source is a flat list of (non-MMGen) Bitcoin addresses -k, --keyaddr-file Address source is a key-address file @@ -53,29 +55,46 @@ The --batch and --rescan options cannot be used together. cmd_args = opts.init(opts_data) +def import_mmgen_list(infile): + al = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](infile) + if al.al_id.mmtype == 'S': + from mmgen.tx import segwit_is_active + if not segwit_is_active(): + rdie(2,'Segwit is not active on this chain. Cannot import Segwit addresses') + return al + +def import_flat_list(lines): + al = AddrList(addrlist=lines) + from mmgen.bitcoin import verify_addr + qmsg_r('Validating addresses...') + for e in al.data: + if not verify_addr(e.addr,verbose=True): + die(2,'\n%s: invalid address' % e.addr) + if e.addr.addr_fmt == 'p2sh': + fs = "\n'{}':\n Non-{} P2SH addresses may not be imported into the tracking wallet" + rdie(2,fs.format(e.addr,g.proj_name)) + return al + if len(cmd_args) == 1: infile = cmd_args[0] check_infile(infile) if opt.addrlist: lines = get_lines_from_file( infile,'non-{pnm} addresses'.format(pnm=g.proj_name),trim_comments=True) - ai = AddrList(addrlist=lines) + al = import_flat_list(lines) else: - ai = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](infile) + al = import_mmgen_list(infile) +elif len(cmd_args) == 0 and opt.address: + al = import_flat_list([opt.address]) + infile = 'command line' else: die(1,""" -You must specify an {pnm} address file (or a list of non-{pnm} addresses -with the '--addrlist' option) +You must specify an {pnm} address file, a single address, or a list of +non-{pnm} addresses with the '--addrlist' option) """.strip().format(pnm=g.proj_name)) -from mmgen.bitcoin import verify_addr -qmsg_r('Validating addresses...') -for e in ai.data: - if not verify_addr(e.addr,verbose=True): - die(2,'%s: invalid address' % e.addr) - -m = (' from Seed ID %s' % ai.seed_id) if ai.seed_id else '' -qmsg('OK. %s addresses%s' % (ai.num_addrs,m)) +m = ' from Seed ID {}'.format(al.al_id.sid) if hasattr(al.al_id,'sid') else '' +qmsg('OK. {} addresses{}'.format(al.num_addrs,m)) if not opt.test: c = bitcoin_connection() @@ -104,8 +123,8 @@ def import_address(addr,label,rescan): global err_flag err_flag = True -w_n_of_m = len(str(ai.num_addrs)) * 2 + 2 -w_mmid = '' if opt.addrlist else len(str(max(ai.idxs()))) + 12 +w_n_of_m = len(str(al.num_addrs)) * 2 + 2 +w_mmid = '' if opt.addrlist else len(str(max(al.idxs()))) + 12 if opt.rescan: import threading @@ -113,19 +132,26 @@ if opt.rescan: else: msg_fmt = '\r%-{}s %-34s %s'.format(w_n_of_m, w_mmid) -msg("Importing %s addresses from '%s'%s" % - (len(ai.data),infile,('',' (batch mode)')[bool(opt.batch)])) +msg("Importing {} address{} from {}{}".format( + len(al.data), suf(al.data,'es'), infile, + ('',' (batch mode)')[bool(opt.batch)] + )) + +if not al.is_for_current_chain(): + die(2,"Address{} not compatible with {} chain!".format((' list','')[bool(opt.address)],g.chain)) arg_list = [] -for n,e in enumerate(ai.data): +for n,e in enumerate(al.data): if e.idx: - label = '%s:%s' % (ai.seed_id,e.idx) + label = '{}:{}'.format(al.al_id,e.idx) if e.label: label += ' ' + e.label m = label else: label = 'btc:{}'.format(e.addr) m = 'non-'+g.proj_name + label = TwLabel(label) + if opt.batch: arg_list.append((e.addr,label,False)) elif opt.rescan: @@ -138,7 +164,7 @@ for n,e in enumerate(ai.data): while True: if t.is_alive(): elapsed = int(time.time() - start) - count = '%s/%s:' % (n+1, ai.num_addrs) + count = '%s/%s:' % (n+1, al.num_addrs) msg_r(msg_fmt % (secs_to_hms(elapsed),count,e.addr,'(%s)' % m)) time.sleep(1) else: @@ -147,7 +173,7 @@ for n,e in enumerate(ai.data): break else: import_address(e.addr,label,False) - count = '%s/%s:' % (n+1, ai.num_addrs) + count = '%s/%s:' % (n+1, al.num_addrs) msg_r(msg_fmt % (count, e.addr, '(%s)' % m)) if err_flag: die(2,'\nImport failed') msg(' - OK') diff --git a/mmgen/main_passgen.py b/mmgen/main_passgen.py index 6c4c324c..1776ff5b 100755 --- a/mmgen/main_passgen.py +++ b/mmgen/main_passgen.py @@ -98,9 +98,9 @@ EXAMPLE: NOTES FOR ALL GENERATOR COMMANDS -{o.pw_note} +{pwn} -{o.bw_note} +{bwn} FMT CODES: {f} @@ -108,6 +108,7 @@ FMT CODES: f='\n '.join(SeedSource.format_fmt_codes().splitlines()), o=opts,g=g,d58=dfl_len['b58'],d32=dfl_len['b32'], ml=MMGenPWIDString.max_len, + pwn=pw_note,bwn=bw_note, fs="', '".join(MMGenPWIDString.forbidden) ) } diff --git a/mmgen/main_tool.py b/mmgen/main_tool.py index 3c7f3690..c8f908fd 100755 --- a/mmgen/main_tool.py +++ b/mmgen/main_tool.py @@ -39,30 +39,23 @@ opts_data = { """.format(g=g), 'notes': """ -COMMANDS:{} + COMMANDS +{} Type '{} help for help on a particular command """.format(tool.cmd_help,g.prog_name) } -cmd_args = opts.init(opts_data, - add_opts=[ - 'hidden_incog_input_params', - 'in_fmt' - ]) +cmd_args = opts.init(opts_data,add_opts=['hidden_incog_input_params','in_fmt']) -if len(cmd_args) < 1: - opts.usage() - sys.exit(1) +if len(cmd_args) < 1: opts.usage() -command = cmd_args.pop(0) +Command = cmd_args.pop(0).capitalize() -if command not in tool.cmd_data: - die(1,"'%s': no such command" % command) +if Command == 'Help' and not cmd_args: tool.usage(None) -if cmd_args and cmd_args[0] == '--help': - tool.tool_usage(g.prog_name, command) - sys.exit() +if Command not in tool.cmd_data: + die(1,"'%s': no such command" % Command.lower()) -args,kwargs = tool.process_args(g.prog_name, command, cmd_args) -ret = tool.__dict__[command](*args,**kwargs) +args,kwargs = tool.process_args(Command,cmd_args) +ret = tool.__dict__[Command](*args,**kwargs) sys.exit(0 if ret in (None,True) else 1) # some commands die, some return False on failure diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index b8957961..80dd73e6 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -51,5 +51,5 @@ opts_data = { cmd_args = opts.init(opts_data) do_license_msg() -tx = txcreate(opt,cmd_args,do_info=opt.info) +tx = txcreate(cmd_args,do_info=opt.info) tx.write_to_file(ask_write=not opt.yes,ask_overwrite=not opt.yes,ask_write_default_yes=False) diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py index 33fd3811..c86bf9d8 100755 --- a/mmgen/main_txdo.py +++ b/mmgen/main_txdo.py @@ -83,7 +83,7 @@ kal = get_keyaddrlist(opt) kl = get_keylist(opt) if kl and kal: kl.remove_dups(kal,key='wif') -tx = txcreate(opt,cmd_args,caller='txdo') +tx = txcreate(cmd_args,caller='txdo') txsign(opt,c,tx,seed_files,kl,kal) tx.write_to_file(ask_write=False) diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index bfa1e53a..726fd907 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -44,17 +44,13 @@ if len(cmd_args) == 1: else: opts.usage() do_license_msg() -tx = MMGenTX(infile) c = bitcoin_connection() - -if not tx.check_signed(c): - die(1,'Transaction is not signed!') - -if tx.btc_txid: - msg('Warning: transaction has already been sent!') - +tx = MMGenTX(infile) # sig check performed here qmsg("Signed transaction file '%s' is valid" % infile) +if not tx.marked_signed(c): + die(1,'Transaction is not signed!') + if not opt.yes: tx.view_with_prompt('View transaction data?') if tx.add_comment(): # edits an existing comment, returns true if changed diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py index 46fdd64b..1d330518 100755 --- a/mmgen/main_txsign.py +++ b/mmgen/main_txsign.py @@ -90,7 +90,7 @@ for tx_num,tx_file in enumerate(tx_files,1): tx_num_str = ' #%s' % tx_num tx = MMGenTX(tx_file) - if tx.check_signed(c): + if tx.marked_signed(): die(1,'Transaction is already signed!') vmsg("Successfully opened transaction file '%s'" % tx_file) diff --git a/mmgen/main_wallet.py b/mmgen/main_wallet.py index 8c9159ee..8970e81b 100755 --- a/mmgen/main_wallet.py +++ b/mmgen/main_wallet.py @@ -30,8 +30,6 @@ usage = '[opts] [infile]' nargs = 1 iaction = 'convert' oaction = 'convert' -bw_note = opts.bw_note -pw_note = opts.pw_note invoked_as = 'passchg' if g.prog_name == 'mmgen-passchg' else g.prog_name.partition('-wallet')[2] @@ -99,14 +97,14 @@ opts_data = { ), 'notes': """ -{pw_note}{bw_note} +{pwn}{bwn} FMT CODES: {f} """.format( f='\n '.join(SeedSource.format_fmt_codes().splitlines()), - pw_note=pw_note, - bw_note=('','\n\n' + bw_note)[bool(bw_note)] + pwn=pw_note, + bwn=('','\n\n' + bw_note)[bool(bw_note)] ) } @@ -125,12 +123,11 @@ if invoked_as in ('conv','passchg'): msg(green('Processing input wallet')+dw_msg) ss_in = None if invoked_as == 'gen' else SeedSource(sf,passchg=(invoked_as=='passchg')) - if invoked_as == 'chk': lbl = ss_in.ssdata.label.hl() if hasattr(ss_in.ssdata,'label') else 'NONE' vmsg('Wallet label: {}'.format(lbl)) # TODO: display creation date - sys.exit() + sys.exit(0) if invoked_as in ('conv','passchg'): msg(green('Processing output wallet')) @@ -141,7 +138,7 @@ if invoked_as == 'gen': qmsg("This wallet's Seed ID: %s" % ss_out.seed.sid.hl()) if invoked_as == 'passchg': - if not (opt.force_update or [k for k in 'passwd','hash_preset','label' + if not (opt.force_update or [k for k in ('passwd','hash_preset','label') if getattr(ss_out.ssdata,k) != getattr(ss_in.ssdata,k)]): die(1,'Password, hash preset and label are unchanged. Taking no action') diff --git a/mmgen/obj.py b/mmgen/obj.py index bd12e1cf..ef3d6768 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -20,6 +20,7 @@ obj.py: MMGen native classes """ +import sys from decimal import * from mmgen.color import * lvl = 0 @@ -27,38 +28,73 @@ lvl = 0 class MMGenObject(object): # Pretty-print any object of type MMGenObject, recursing into sub-objects - WIP - def pprint(self): print self.pformat() +# def pmsg(self): sys.stderr.write(self.pformat()+'\n') +# def pdie(self): sys.stderr.write(self.pformat()+'\n'); sys.exit(0) + def pmsg(self): print(self.pformat()) + def pdie(self): print(self.pformat()); sys.exit(0) def pformat(self,lvl=0): - def do_list(out,e,lvl=0): - add_spc = False - if e and type(e[0]) not in (str,unicode): - out.append('\n') + from decimal import Decimal + scalars = (str,unicode,int,float,Decimal) + def do_list(out,e,lvl=0,is_dict=False): + out.append('\n') for i in e: - if hasattr(i,'pformat'): - out.append('{:>{l}}{}'.format('',i.pformat(lvl=lvl+1),l=(lvl+1)*8)) - elif type(i) in (str,unicode): - add_spc = True - out.append(u' {}'.format(repr(i))) - elif type(i) == list: - out.append(u'{:>{l}}{:16}'.format('','<'+type(i).__name__+'>',l=(lvl*8)+4)) - do_list(out,i,lvl=lvl) + el = i if not is_dict else e[i] + if is_dict: + out.append('{s}{:<{l}}'.format(i,s=' '*(4*lvl+8),l=10,l2=8*(lvl+1)+8)) + if hasattr(el,'pformat'): + out.append('{:>{l}}{}'.format('',el.pformat(lvl=lvl+1),l=(lvl+1)*8)) + elif type(el) in scalars: + if isList(e): + out.append(u'{:>{l}}{:16}\n'.format('',repr(el),l=lvl*8)) + else: + out.append(u' {}'.format(repr(el))) + elif isList(el) or isDict(el): + indent = 1 if is_dict else lvl*8+4 + out.append(u'{:>{l}}{:16}'.format('','<'+type(el).__name__+'>',l=indent)) + if isList(el) and type(el[0]) in scalars: out.append('\n') + do_list(out,el,lvl=lvl+1,is_dict=isDict(el)) else: - out.append(u'{:>{l}}{:16} {}\n'.format('','<'+type(i).__name__+'>',repr(i),l=(lvl*8)+8)) + out.append(u'{:>{l}}{:16} {}\n'.format('','<'+type(el).__name__+'>',repr(el),l=(lvl*8)+8)) + out.append('\n') if not e: out.append('{}\n'.format(repr(e))) - if add_spc: out.append('\n') - out = [] - out.append(u'<{}>\n'.format(type(self).__name__)) - d = self.__dict__ - for k in d: + + from collections import OrderedDict + def isDict(obj): + return issubclass(type(obj),dict) or issubclass(type(obj),OrderedDict) + def isList(obj): + return issubclass(type(obj),list) and type(obj) != OrderedDict + def isScalar(obj): + return any(issubclass(type(obj),t) for t in scalars) + +# print type(self) +# print dir(self) +# print self.__dict__ # *attributes* of object +# print self.__dict__.keys() # *attributes* of object +# print self.keys() + + out = [u'<{}>{}\n'.format(type(self).__name__,' '+repr(self) if isScalar(self) else '')] + if isList(self) or isDict(self): + do_list(out,self,lvl=lvl,is_dict=isDict(self)) + +# print repr(self.__dict__.keys()) + + for k in self.__dict__: + if k in ('_OrderedDict__root', '_OrderedDict__map'): continue # exclude these because of recursion e = getattr(self,k) - if type(e) == list: + if isList(e) or isDict(e): out.append(u'{:>{l}}{:<10} {:16}'.format('',k,'<'+type(e).__name__+'>',l=(lvl*8)+4)) - do_list(out,e,lvl=lvl) + do_list(out,e,lvl=lvl,is_dict=isDict(e)) elif hasattr(e,'pformat') and type(e) != type: out.append(u'{:>{l}}{:10} {}'.format('',k,e.pformat(lvl=lvl+1),l=(lvl*8)+4)) else: - out.append(u'{:>{l}}{:<10} {:16} {}\n'.format('',k,'<'+type(e).__name__+'>',repr(e),l=(lvl*8)+4)) - return ''.join(out) + out.append(u'{:>{l}}{:<10} {:16} {}\n'.format( + '',k,'<'+type(e).__name__+'>',repr(e),l=(lvl*8)+4)) + + import re + return re.sub('\n+','\n',''.join(out)) + +class MMGenList(list,MMGenObject): pass +class MMGenDict(dict,MMGenObject): pass # Descriptor: https://docs.python.org/2/howto/descriptor.html class MMGenListItemAttr(object): @@ -75,10 +111,10 @@ class MMGenListItemAttr(object): class MMGenListItem(MMGenObject): - addr = MMGenListItemAttr('addr','BTCAddr') - amt = MMGenListItemAttr('amt','BTCAmt') - mmid = MMGenListItemAttr('mmid','MMGenID') - label = MMGenListItemAttr('label','MMGenAddrLabel') + addr = MMGenListItemAttr('addr','BTCAddr') + amt = MMGenListItemAttr('amt','BTCAmt') + mmid = MMGenListItemAttr('mmid','MMGenID') + label = MMGenListItemAttr('label','TwComment') attrs = () attrs_priv = () @@ -91,7 +127,8 @@ class MMGenListItem(MMGenObject): "'{}': attribute '{}' in instance of class '{}' cannot be reassigned".format( val,attr,type(self).__name__) - attrs_base = ('attrs','attrs_priv','attrs_reassign','attrs_base','attr_error','set_error','__dict__','pformat') + attrs_base = ('attrs','attrs_priv','attrs_reassign','attrs_base','attr_error','set_error', + '__dict__','pformat','pmsg','pdie') def __init__(self,*args,**kwargs): if args: @@ -133,7 +170,7 @@ class InitErrors(object): @staticmethod def arg_chk(cls,on_fail): - assert on_fail in ('die','return','silent','raise'),"'on_fail' in class %s" % cls.__name__ + assert on_fail in ('die','return','silent','raise'),"arg_chk in class %s" % cls.__name__ @staticmethod def init_fail(m,on_fail,silent=False): @@ -142,8 +179,8 @@ class InitErrors(object): if on_fail == 'die': die(1,m) elif on_fail == 'return': if m: msg(m) - return None - elif on_fail == 'silent': return None + return None # TODO: change to False + elif on_fail == 'silent': return None # same here elif on_fail == 'raise': raise ValueError,m class AddrIdx(int,InitErrors): @@ -167,7 +204,7 @@ class AddrIdx(int,InitErrors): return cls.init_fail(m,on_fail) -class AddrIdxList(list,InitErrors): +class AddrIdxList(list,InitErrors,MMGenObject): max_len = 1000000 @@ -176,7 +213,7 @@ class AddrIdxList(list,InitErrors): assert fmt_str or idx_list if idx_list: # dies on failure - return list.__init__(self,sorted(set([AddrIdx(i) for i in idx_list]))) + return list.__init__(self,sorted(set(AddrIdx(i) for i in idx_list))) elif fmt_str: desc = fmt_str ret,fs = [],"'%s': value cannot be converted to address index" @@ -315,17 +352,19 @@ class BTCAmt(Decimal,Hilite,InitErrors): def __neg__(self,other,context=None): return type(self)(Decimal.__neg__(self,other,context)) -class BTCAddr(str,Hilite,InitErrors): +class BTCAddr(str,Hilite,InitErrors,MMGenObject): color = 'cyan' - width = 34 + width = 35 # max len of testnet p2sh addr def __new__(cls,s,on_fail='die'): cls.arg_chk(cls,on_fail) + m = "'%s': value is not a Bitcoin address" % s me = str.__new__(cls,s) - from mmgen.bitcoin import verify_addr - if type(s) in (str,unicode,BTCAddr) and verify_addr(s): - return me - else: - m = "'%s': value is not a Bitcoin address" % s + from mmgen.bitcoin import verify_addr,addr_pfxs + if type(s) in (str,unicode,BTCAddr): + me.addr_fmt = verify_addr(s,return_type=True) + me.testnet = s[0] in addr_pfxs['testnet'] + if me.addr_fmt: + return me return cls.init_fail(m,on_fail) @classmethod @@ -338,6 +377,21 @@ class BTCAddr(str,Hilite,InitErrors): s = s[:kwargs['width']-2] + '..' return Hilite.fmtc(s,**kwargs) + def is_for_current_chain(self): + from mmgen.globalvars import g + assert g.chain, 'global chain variable unset' + from bitcoin import addr_pfxs + return self[0] in addr_pfxs[g.chain] + + def is_mainnet(self): + from bitcoin import addr_pfxs + return self[0] in addr_pfxs['mainnet'] + + def is_in_tracking_wallet(self): + from mmgen.rpc import bitcoin_connection + d = bitcoin_connection().validateaddress(self) + return d['iswatchonly'] and 'account' in d + class SeedID(str,Hilite,InitErrors): color = 'blue' width = 8 @@ -351,6 +405,7 @@ class SeedID(str,Hilite,InitErrors): if type(seed) == Seed: return str.__new__(cls,make_chksum_8(seed.get_data())) elif sid: + sid = str(sid) from string import hexdigits if len(sid) == cls.width and set(sid) <= set(hexdigits.upper()): return str.__new__(cls,sid) @@ -358,7 +413,7 @@ class SeedID(str,Hilite,InitErrors): m = "'%s': value cannot be converted to SeedID" % str(seed or sid) return cls.init_fail(m,on_fail) -class MMGenID(str,Hilite,InitErrors): +class MMGenID(str,Hilite,InitErrors,MMGenObject): color = 'orange' width = 0 @@ -367,15 +422,83 @@ class MMGenID(str,Hilite,InitErrors): def __new__(cls,s,on_fail='die'): cls.arg_chk(cls,on_fail) s = str(s) - if ':' in s: - a,b = s.split(':',1) - sid = SeedID(sid=a,on_fail='silent') - if sid: - idx = AddrIdx(b,on_fail='silent') - if idx: - return str.__new__(cls,'%s:%s' % (sid,idx)) + try: + ss = s.split(':') + assert len(ss) in (2,3) + sid = SeedID(sid=ss[0],on_fail='silent') + assert sid + idx = AddrIdx(ss[-1],on_fail='silent') + assert idx + t = MMGenAddrType((MMGenAddrType.dfl_mmtype,ss[1])[len(ss) != 2],on_fail='silent') + assert t + me = str.__new__(cls,'{}:{}:{}'.format(sid,t,idx)) + me.sid = sid + me.mmtype = t + me.idx = idx + me.al_id = AddrListID(sid,me.mmtype) # key with colon! + assert me.al_id + me.sort_key = '{}:{}:{:0{w}}'.format(sid,t,idx,w=idx.max_digits) + return me + except: + m = "'%s': value cannot be converted to MMGenID" % s + return cls.init_fail(m,on_fail) - m = "'%s': value cannot be converted to MMGenID" % s +class TwMMGenID(str,Hilite,InitErrors,MMGenObject): + + color = 'orange' + width = 0 + trunc_ok = False + + def __new__(cls,s,on_fail='die'): + cls.arg_chk(cls,on_fail) + obj,sort_key = None,None + try: + obj = MMGenID(s,on_fail='silent') + sort_key,t = obj.sort_key,'mmgen' + except: + try: + assert len(s) > 4 and s[:4] == 'btc:' + obj,sort_key,t = str(s),'z_'+s,'non-mmgen' + except: + pass + + if obj and sort_key: + me = str.__new__(cls,obj) + me.obj = obj + me.sort_key = sort_key + me.type = t + return me + + m = "'{}': value cannot be converted to {}".format(s,cls.__name__) + return cls.init_fail(m,on_fail) + +# contains TwMMGenID,TwComment. Not for display +class TwLabel(str,InitErrors,MMGenObject): + + def __new__(cls,s,on_fail='die'): + cls.arg_chk(cls,on_fail) + try: + ss = s.split(None,1) + me = str.__new__(cls,s) + me.mmid = TwMMGenID(ss[0],on_fail='silent') + assert me.mmid + me.comment = TwComment(ss[1] if len(ss) == 2 else '',on_fail='silent') + assert me.comment != None + return me + except: + m = "'{}': value cannot be converted to {}".format(s,cls.__name__) + return cls.init_fail(m,on_fail) + +class HexStr(str,Hilite,InitErrors): + color = 'red' + trunc_ok = False + def __new__(cls,s,on_fail='die',case='lower'): + assert case in ('upper','lower') + cls.arg_chk(cls,on_fail) + from string import hexdigits + if set(s) <= set(getattr(hexdigits,case)()) and not len(s) % 2: + return str.__new__(cls,s) + m = "'{}': value cannot be converted to {}".format(s,cls.__name__) return cls.init_fail(m,on_fail) class MMGenTxID(str,Hilite,InitErrors): @@ -396,6 +519,65 @@ class BitcoinTxID(MMGenTxID): width = 64 hexcase = 'lower' +class WifKey(str,Hilite,InitErrors): + width = 53 + color = 'blue' + desc = 'WIF key' + def __new__(cls,s,on_fail='die',errmsg=None): + cls.arg_chk(cls,on_fail) + from mmgen.tx import is_wif + if is_wif(s): + me = str.__new__(cls,s) + return me + m = errmsg or "'{}': invalid value for {}".format(s,cls.desc) + return cls.init_fail(m,on_fail) + +class MMGenAddrType(str,Hilite,InitErrors): + width = 1 + trunc_ok = False + color = 'blue' + mmtypes = { + # TODO 'L' is ambiguous: For user, it means MMGen legacy uncompressed address. + # For generator functions, 'L' means any p2pkh address, and 'S' any ps2h address + 'L': 'legacy', + 'S': 'segwit', +# 'l': 'litecoin', +# 'e': 'ethereum', +# 'E': 'ethereum_classic', +# 'm': 'monero', +# 'z': 'zcash', + } + dfl_mmtype = 'L' + def __new__(cls,s,on_fail='die',errmsg=None): + cls.arg_chk(cls,on_fail) + for k,v in cls.mmtypes.items(): + if s in (k,v): + if s == v: s = k + me = str.__new__(cls,s) + me.name = cls.mmtypes[s] + return me + m = errmsg or "'{}': invalid value for {}".format(s,cls.__name__) + return cls.init_fail(m,on_fail) + +class MMGenPasswordType(MMGenAddrType): + mmtypes = { 'P': 'password' } + +class AddrListID(str,Hilite,InitErrors): + width = 10 + trunc_ok = False + color = 'yellow' + def __new__(cls,sid,mmtype,on_fail='die'): + cls.arg_chk(cls,on_fail) + m = "'{}': not a SeedID. Cannot create {}".format(sid,cls.__name__) + if type(sid) == SeedID: + m = "'{}': not an MMGenAddrType object. Cannot create {}".format(mmtype,cls.__name__) + if type(mmtype) in (MMGenAddrType,MMGenPasswordType): + me = str.__new__(cls,sid+':'+mmtype) # colon in key is OK + me.sid = sid + me.mmtype = mmtype + return me + return cls.init_fail(m,on_fail) + class MMGenLabel(unicode,Hilite,InitErrors): color = 'pink' @@ -425,7 +607,7 @@ class MMGenLabel(unicode,Hilite,InitErrors): elif cls.allowed and not set(list(s)).issubset(set(cls.allowed)): m = u"{} '{}' contains non-allowed symbols: {}".format(capfirst(cls.desc),s, ' '.join(set(list(s)) - set(cls.allowed))) - elif cls.forbidden and any([ch in s for ch in cls.forbidden]): + elif cls.forbidden and any(ch in s for ch in cls.forbidden): m = u"{} '{}' contains one of these forbidden symbols: '{}'".format(capfirst(cls.desc),s, "', '".join(cls.forbidden)) else: @@ -437,10 +619,10 @@ class MMGenWalletLabel(MMGenLabel): allowed = [unichr(i+32) for i in range(95)] desc = 'wallet label' -class MMGenAddrLabel(MMGenLabel): +class TwComment(MMGenLabel): max_len = 32 allowed = [unichr(i+32) for i in range(95)] - desc = 'address label' + desc = 'tracking wallet comment' class MMGenTXLabel(MMGenLabel): max_len = 72 @@ -451,3 +633,5 @@ class MMGenPWIDString(MMGenLabel): min_len = 1 desc = 'password ID string' forbidden = list(u' :/\\') + +class AddrListList(list,MMGenObject): pass diff --git a/mmgen/opts.py b/mmgen/opts.py index 48eb1045..4f434e07 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -27,26 +27,6 @@ from mmgen.globalvars import g import mmgen.share.Opts from mmgen.util import * -pw_note = """ -For passphrases all combinations of whitespace are equal and leading and -trailing space is ignored. This permits reading passphrase or brainwallet -data from a multi-line file with free spacing and indentation. -""".strip() - -bw_note = """ -BRAINWALLET NOTE: - -To thwart dictionary attacks, it's recommended to use a strong hash preset -with brainwallets. For a brainwallet passphrase to generate the correct -seed, the same seed length and hash preset parameters must always be used. -""".strip() - -version_info = """ -{pgnm_uc} version {g.version} -Part of the {pnm} suite, a Bitcoin cold-storage solution for the command line. -Copyright (C) {g.Cdates} {g.author} {g.email} -""".format(pnm=g.proj_name, g=g, pgnm_uc=g.prog_name.upper()).strip() - def usage(): Die(2,'USAGE: %s %s' % (g.prog_name, usage_txt)) def die_on_incompatible_opts(incompat_list): @@ -76,6 +56,7 @@ common_opts_data = """ --, --rpc-port=p Communicate with bitcoind listening on port 'p' --, --rpc-user=user Override 'rpcuser' in bitcoin.conf --, --rpc-password=pass Override 'rpcpassword' in bitcoin.conf +--, --regtest=0|1 Disable or enable regtest mode --, --testnet=0|1 Disable or enable testnet --, --skip-cfg-file Skip reading the configuration file --, --version Print version information and exit @@ -179,6 +160,13 @@ def override_from_env(): setattr(g,gname,set_for_type(val,getattr(g,gname),name,invert_bool)) def init(opts_data,add_opts=[],opt_filter=None): + + version_info = """ + {pgnm_uc} version {g.version} + Part of the {pnm} suite, a Bitcoin cold-storage solution for the command line. + Copyright (C) {g.Cdates} {g.author} {g.email} + """.format(pnm=g.proj_name, g=g, pgnm_uc=g.prog_name.upper()).strip() + opts_data['long_options'] = common_opts_data uopts,args,short_opts,long_opts,skipped_opts = \ @@ -218,6 +206,8 @@ def init(opts_data,add_opts=[],opt_filter=None): val = getattr(opt,k) if val != None: setattr(g,k,set_for_type(val,getattr(g,k),'--'+k)) + if g.regtest: g.testnet = True # These are equivalent for now + # Global vars are now final, including g.testnet, so we can set g.data_dir g.data_dir=os.path.normpath(os.path.join(g.data_dir_root,('',g.testnet_name)[g.testnet])) @@ -238,15 +228,16 @@ def init(opts_data,add_opts=[],opt_filter=None): if opt.show_hash_presets: _show_hash_presets() - sys.exit() + sys.exit(0) - if g.debug: opt_postproc_debug() if opt.verbose: opt.quiet = None die_on_incompatible_opts(g.incompatible_opts) opt_postproc_initializations() + if g.debug: opt_postproc_debug() + return args def check_opts(usr_opts): # Returns false if any check fails diff --git a/mmgen/rpc.py b/mmgen/rpc.py index 2b2d2461..6220c264 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -28,8 +28,6 @@ from mmgen.obj import BTCAmt class BitcoinRPCConnection(object): - client_version = 0 - def __init__( self, host=g.rpc_host,port=(8332,18332)[g.testnet], @@ -78,7 +76,7 @@ class BitcoinRPCConnection(object): p = {'method':cmd,'params':args,'id':1} def die_maybe(*args): - if cf['on_fail'] == 'return': + if cf['on_fail'] in ('return','silent'): return 'rpcfail',args else: die(*args[1:]) @@ -89,7 +87,7 @@ class BitcoinRPCConnection(object): class MyJSONEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, BTCAmt): - return (float,str)[caller.client_version>=120000](obj) + return (float,str)[g.bitcoind_version>=120000](obj) return json.JSONEncoder.default(self, obj) # Can't do UTF-8 labels yet: httplib only ascii? @@ -111,8 +109,9 @@ class BitcoinRPCConnection(object): dmsg(' RPC GETRESPONSE data ==> %s\n' % r.__dict__) if r.status != 200: - msg_r(yellow('Bitcoind RPC Error: ')) - msg(red('{} {}'.format(r.status,r.reason))) + if cf['on_fail'] != 'silent': + msg_r(yellow('Bitcoind RPC Error: ')) + msg(red('{} {}'.format(r.status,r.reason))) e1 = r.read() try: e3 = json.loads(e1)['error'] @@ -142,26 +141,30 @@ class BitcoinRPCConnection(object): return ret if cf['batch'] else ret[0] - rpcmethods = ( - 'createrawtransaction', 'backupwallet', + 'createrawtransaction', 'decoderawtransaction', 'disconnectnode', 'estimatefee', 'getaddressesbyaccount', 'getbalance', 'getblock', + 'getblockchaininfo', 'getblockcount', 'getblockhash', - 'getinfo', + 'getmempoolentry', + 'getnetworkinfo', 'getpeerinfo', + 'getrawmempool', + 'getrawtransaction', + 'gettransaction', 'importaddress', 'listaccounts', 'listunspent', 'sendrawtransaction', 'signrawtransaction', - 'getrawmempool', + 'validateaddress', 'walletpassphrase', ) diff --git a/mmgen/seed.py b/mmgen/seed.py index 1e123858..f0adb512 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -159,18 +159,21 @@ class SeedSource(MMGenObject): msg('Trying again...') @classmethod - def get_subclasses(cls): - if not hasattr(cls,'subclasses'): - gl = globals() - setattr(cls,'subclasses', - [gl[k] for k in gl if type(gl[k]) == type and issubclass(gl[k],cls)]) - return cls.subclasses + def get_subclasses_str(cls): # returns name of calling class too + return cls.__name__ + ' ' + ''.join([c.get_subclasses_str() for c in cls.__subclasses__()]) @classmethod - def get_subclasses_str(cls): - def GetSubclassesTree(cls): - return ''.join([c.__name__ +' '+ GetSubclassesTree(c) for c in cls.__subclasses__()]) - return GetSubclassesTree(cls) + def get_subclasses_easy(cls,acc=[]): + return [globals()[c] for c in cls.get_subclasses_str().split()] + + @classmethod + def get_subclasses(cls): # returns calling class too + def GetSubclassesTree(cls,acc): + acc += [cls] + for c in cls.__subclasses__(): GetSubclassesTree(c,acc) + acc = [] + GetSubclassesTree(cls,acc) + return acc @classmethod def get_extensions(cls): @@ -1027,8 +1030,8 @@ harder to find, you're advised to choose a much larger file size than this. msg('File size must be an integer no less than %s' % min_fsize) - from mmgen.tool import rand2file - rand2file(fn, str(fsize)) + from mmgen.tool import Rand2file # threaded routine + Rand2file(fn,str(fsize)) check_offset = False else: die(1,'Exiting at user request') diff --git a/mmgen/share/Opts.py b/mmgen/share/Opts.py index 2df52804..431d5772 100755 --- a/mmgen/share/Opts.py +++ b/mmgen/share/Opts.py @@ -21,7 +21,7 @@ Opts.py: Generic options handling """ import sys, getopt -# from mmgen.util import mdie,die,pp_die,pp_msg # DEBUG +# from mmgen.util import mdie,die,pdie,pmsg # DEBUG def usage(opts_data): print 'USAGE: %s %s' % (opts_data['prog_name'], opts_data['usage']) @@ -54,8 +54,8 @@ def process_opts(argv,opts_data,short_opts,long_opts): opts = {} for opt, arg in cl_opts: - if opt in ('-h','--help'): print_help(opts_data); sys.exit() - elif opt == '--longhelp': print_help(opts_data,longhelp=True); sys.exit() + if opt in ('-h','--help'): print_help(opts_data); sys.exit(0) + elif opt == '--longhelp': print_help(opts_data,longhelp=True); sys.exit(0) elif opt[:2] == '--' and opt[2:] in long_opts: opts[opt[2:].replace('-','_')] = True elif opt[:2] == '--' and opt[2:]+'=' in long_opts: diff --git a/mmgen/test.py b/mmgen/test.py index fc9b95be..8e5a348d 100755 --- a/mmgen/test.py +++ b/mmgen/test.py @@ -48,7 +48,8 @@ def mk_tmpdir(d): try: os.mkdir(d,0755) except OSError as e: if e.errno != 17: raise - else: msg("Created directory '%s'" % d) + else: + qmsg("Created directory '%s'" % d) def mk_tmpdir_path(path,cfg): try: diff --git a/mmgen/tool.py b/mmgen/tool.py index dfe07d3e..ddcf090a 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: UTF-8 -*- # # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution # Copyright (C)2013-2017 Philemon @@ -31,84 +32,99 @@ pnm = g.proj_name from collections import OrderedDict cmd_data = OrderedDict([ - ('help', [' [str]']), - ('usage', [' [str]']), - ('strtob58', [' [str-]','pad [int=0]']), - ('b58tostr', [' [str-]']), - ('hextob58', [' [str-]','pad [int=0]']), - ('b58tohex', [' [str-]','pad [int=0]']), - ('b58randenc', []), - ('b32tohex', [' [str-]','pad [int=0]']), - ('hextob32', [' [str-]','pad [int=0]']), - ('randhex', ['nbytes [int=32]']), - ('id8', [' [str]']), - ('id6', [' [str]']), - ('sha256x2', [' [str]', # TODO handle stdin + ('Help', [' [str]']), + ('Usage', [' [str]']), + ('Strtob58', [' [str-]','pad [int=0]']), + ('B58tostr', [' [str-]']), + ('Hextob58', [' [str-]','pad [int=0]']), + ('B58tohex', [' [str-]','pad [int=0]']), + ('B58randenc', []), + ('B32tohex', [' [str-]','pad [int=0]']), + ('Hextob32', [' [str-]','pad [int=0]']), + ('Randhex', ['nbytes [int=32]']), + ('Id8', [' [str]']), + ('Id6', [' [str]']), + ('Hash160', [' [str-]']), + ('Hash256', [' [str]', # TODO handle stdin 'hex_input [bool=False]','file_input [bool=False]']), - ('str2id6', [' [str-]']), - ('hexdump', [' [str]', 'cols [int=8]', 'line_nums [bool=True]']), - ('unhexdump', [' [str]']), - ('hexreverse', [' [str-]']), - ('hexlify', [' [str-]']), - ('rand2file', [' [str]',' [str]','threads [int=4]','silent [bool=False]']), + ('Str2id6', [' [str-]']), + ('Hexdump', [' [str]', 'cols [int=8]', 'line_nums [bool=True]']), + ('Unhexdump', [' [str]']), + ('Hexreverse', [' [str-]']), + ('Hexlify', [' [str-]']), + ('Rand2file', [' [str]',' [str]','threads [int=4]','silent [bool=False]']), - ('randwif', ['compressed [bool=False]']), - ('randpair', ['compressed [bool=False]']), - ('hex2wif', [' [str-]', 'compressed [bool=False]']), - ('wif2hex', [' [str-]', 'compressed [bool=False]']), - ('wif2addr', [' [str-]', 'compressed [bool=False]']), - ('hexaddr2addr', [' [str-]']), - ('addr2hexaddr', [' [str-]']), - ('pubkey2addr', [' [str-]']), - ('pubkey2hexaddr', [' [str-]']), - ('privhex2addr', [' [str-]','compressed [bool=False]']), + ('Randwif', ['compressed [bool=False]']), + ('Randpair', ['compressed [bool=False]','segwit [bool=False]']), + ('Hex2wif', [' [str-]','compressed [bool=False]']), + ('Wif2hex', [' [str-]']), + ('Wif2addr', [' [str-]','segwit [bool=False]']), + ('Wif2segwit_pair',[' [str-]']), + ('Hexaddr2addr', [' [str-]']), + ('Addr2hexaddr', [' [str-]']), + ('Privhex2addr', [' [str-]','compressed [bool=False]','segwit [bool=False]']), + ('Privhex2pubhex',[' [str-]','compressed [bool=False]']), + ('Pubhex2addr', [' [str-]','p2sh [bool=False]']), # new + ('Pubhex2redeem_script',[' [str-]']), # new + ('Wif2redeem_script', [' [str-]']), # new - ('hex2mn', [' [str-]',"wordlist [str='electrum']"]), - ('mn2hex', [' [str-]', "wordlist [str='electrum']"]), - ('mn_rand128', ["wordlist [str='electrum']"]), - ('mn_rand192', ["wordlist [str='electrum']"]), - ('mn_rand256', ["wordlist [str='electrum']"]), - ('mn_stats', ["wordlist [str='electrum']"]), - ('mn_printlist', ["wordlist [str='electrum']"]), + ('Hex2mn', [' [str-]',"wordlist [str='electrum']"]), + ('Mn2hex', [' [str-]', "wordlist [str='electrum']"]), + ('Mn_rand128', ["wordlist [str='electrum']"]), + ('Mn_rand192', ["wordlist [str='electrum']"]), + ('Mn_rand256', ["wordlist [str='electrum']"]), + ('Mn_stats', ["wordlist [str='electrum']"]), + ('Mn_printlist', ["wordlist [str='electrum']"]), - ('listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']), - ('getbalance', ['minconf [int=1]']), - ('txview', ['<{} TX file> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]']), - ('twview', ["sort [str='age']",'reverse [bool=False]','minconf [int=1]','wide [bool=False]','pager [bool=False]']), + ('Listaddress',['<{} address> [str]'.format(pnm),'minconf [int=1]','pager [bool=False]','showempty [bool=True]''showbtcaddr [bool=True]']), + ('Listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']), + ('Getbalance', ['minconf [int=1]']), + ('Txview', ['<{} TX file(s)> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]',"sort [str='mtime'] (options: 'ctime','atime')",'MARGS']), + ('Twview', ["sort [str='age']",'reverse [bool=False]','show_days [bool=True]','show_mmid [bool=True]','minconf [int=1]','wide [bool=False]','pager [bool=False]']), - ('add_label', ['<{} address> [str]'.format(pnm),'