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.
This commit is contained in:
philemon 2017-07-27 22:55:52 +03:00
commit 3b0257358b
Signed by untrusted user who does not match committer: mmgen
GPG key ID: 62DBE9E5212F05BE
33 changed files with 2038 additions and 1150 deletions

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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')

View file

@ -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)
)
}

View file

@ -39,30 +39,23 @@ opts_data = {
""".format(g=g),
'notes': """
COMMANDS:{}
COMMANDS
{}
Type '{} help <command> 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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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')

View file

@ -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

View file

@ -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

View file

@ -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',
)

View file

@ -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')

View file

@ -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:

View file

@ -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:

View file

@ -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 <mmgen-py@yandex.com>
@ -31,84 +32,99 @@ pnm = g.proj_name
from collections import OrderedDict
cmd_data = OrderedDict([
('help', ['<tool command> [str]']),
('usage', ['<tool command> [str]']),
('strtob58', ['<string> [str-]','pad [int=0]']),
('b58tostr', ['<b58 number> [str-]']),
('hextob58', ['<hex number> [str-]','pad [int=0]']),
('b58tohex', ['<b58 number> [str-]','pad [int=0]']),
('b58randenc', []),
('b32tohex', ['<b32 num> [str-]','pad [int=0]']),
('hextob32', ['<hex num> [str-]','pad [int=0]']),
('randhex', ['nbytes [int=32]']),
('id8', ['<infile> [str]']),
('id6', ['<infile> [str]']),
('sha256x2', ['<str, hexstr or filename> [str]', # TODO handle stdin
('Help', ['<tool command> [str]']),
('Usage', ['<tool command> [str]']),
('Strtob58', ['<string> [str-]','pad [int=0]']),
('B58tostr', ['<b58 number> [str-]']),
('Hextob58', ['<hex number> [str-]','pad [int=0]']),
('B58tohex', ['<b58 number> [str-]','pad [int=0]']),
('B58randenc', []),
('B32tohex', ['<b32 num> [str-]','pad [int=0]']),
('Hextob32', ['<hex num> [str-]','pad [int=0]']),
('Randhex', ['nbytes [int=32]']),
('Id8', ['<infile> [str]']),
('Id6', ['<infile> [str]']),
('Hash160', ['<hexadecimal string> [str-]']),
('Hash256', ['<str, hexstr or filename> [str]', # TODO handle stdin
'hex_input [bool=False]','file_input [bool=False]']),
('str2id6', ['<string (spaces are ignored)> [str-]']),
('hexdump', ['<infile> [str]', 'cols [int=8]', 'line_nums [bool=True]']),
('unhexdump', ['<infile> [str]']),
('hexreverse', ['<hexadecimal string> [str-]']),
('hexlify', ['<string> [str-]']),
('rand2file', ['<outfile> [str]','<nbytes> [str]','threads [int=4]','silent [bool=False]']),
('Str2id6', ['<string (spaces are ignored)> [str-]']),
('Hexdump', ['<infile> [str]', 'cols [int=8]', 'line_nums [bool=True]']),
('Unhexdump', ['<infile> [str]']),
('Hexreverse', ['<hexadecimal string> [str-]']),
('Hexlify', ['<string> [str-]']),
('Rand2file', ['<outfile> [str]','<nbytes> [str]','threads [int=4]','silent [bool=False]']),
('randwif', ['compressed [bool=False]']),
('randpair', ['compressed [bool=False]']),
('hex2wif', ['<private key in hex format> [str-]', 'compressed [bool=False]']),
('wif2hex', ['<wif> [str-]', 'compressed [bool=False]']),
('wif2addr', ['<wif> [str-]', 'compressed [bool=False]']),
('hexaddr2addr', ['<btc address in hex format> [str-]']),
('addr2hexaddr', ['<btc address> [str-]']),
('pubkey2addr', ['<public key in hex format> [str-]']),
('pubkey2hexaddr', ['<public key in hex format> [str-]']),
('privhex2addr', ['<private key in hex format> [str-]','compressed [bool=False]']),
('Randwif', ['compressed [bool=False]']),
('Randpair', ['compressed [bool=False]','segwit [bool=False]']),
('Hex2wif', ['<private key in hex format> [str-]','compressed [bool=False]']),
('Wif2hex', ['<wif> [str-]']),
('Wif2addr', ['<wif> [str-]','segwit [bool=False]']),
('Wif2segwit_pair',['<wif> [str-]']),
('Hexaddr2addr', ['<btc address in hex format> [str-]']),
('Addr2hexaddr', ['<btc address> [str-]']),
('Privhex2addr', ['<private key in hex format> [str-]','compressed [bool=False]','segwit [bool=False]']),
('Privhex2pubhex',['<private key in hex format> [str-]','compressed [bool=False]']),
('Pubhex2addr', ['<public key in hex format> [str-]','p2sh [bool=False]']), # new
('Pubhex2redeem_script',['<public key in hex format> [str-]']), # new
('Wif2redeem_script', ['<private key in WIF format> [str-]']), # new
('hex2mn', ['<hexadecimal string> [str-]',"wordlist [str='electrum']"]),
('mn2hex', ['<mnemonic> [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', ['<hexadecimal string> [str-]',"wordlist [str='electrum']"]),
('Mn2hex', ['<mnemonic> [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),'<label> [str]']),
('remove_label', ['<{} address> [str]'.format(pnm)]),
('addrfile_chksum', ['<{} addr file> [str]'.format(pnm)]),
('keyaddrfile_chksum', ['<{} addr file> [str]'.format(pnm)]),
('passwdfile_chksum', ['<{} password file> [str]'.format(pnm)]),
('find_incog_data', ['<file or device name> [str]','<Incog ID> [str]','keep_searching [bool=False]']),
('Add_label', ['<{} address> [str]'.format(pnm),'<label> [str]']),
('Remove_label', ['<{} address> [str]'.format(pnm)]),
('Addrfile_chksum', ['<{} addr file> [str]'.format(pnm)]),
('Keyaddrfile_chksum', ['<{} addr file> [str]'.format(pnm)]),
('Passwdfile_chksum', ['<{} password file> [str]'.format(pnm)]),
('Find_incog_data', ['<file or device name> [str]','<Incog ID> [str]','keep_searching [bool=False]']),
('encrypt', ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
('decrypt', ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
('bytespec', ['<bytespec> [str]']),
('Encrypt', ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
('Decrypt', ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
('Bytespec', ['<bytespec> [str]']),
])
cmd_help = """
Bitcoin address/key operations (compressed public keys supported):
addr2hexaddr - convert Bitcoin address from base58 to hex format
hex2wif - convert a private key from hex to WIF format
hexaddr2addr - convert Bitcoin address from hex to base58 format
privhex2addr - generate Bitcoin address from private key in hex format
pubkey2addr - convert Bitcoin public key to address
pubkey2hexaddr - convert Bitcoin public key to address in hex format
randpair - generate a random private key/address pair
randwif - generate a random private key in WIF format
wif2addr - generate a Bitcoin address from a key in WIF format
wif2hex - convert a private key from WIF to hex format
stdin_msg = """
To force a command to read from STDIN in place of its first argument (for
supported commands), use '-' as the first argument.
""".strip()
Wallet/TX operations (bitcoind must be running):
cmd_help = """
Bitcoin address/key operations (compressed public keys supported):
addr2hexaddr - convert Bitcoin address from base58 to hex format
hex2wif - convert a private key from hex to WIF format
hexaddr2addr - convert Bitcoin address from hex to base58 format
privhex2addr - generate Bitcoin address from private key in hex format
privhex2pubhex - generate a hex public key from a hex private key
pubhex2addr - convert a hex pubkey to an address
pubhex2redeem_script - convert a hex pubkey to a witness redeem script
wif2redeem_script - convert a WIF private key to a witness redeem script
wif2segwit_pair - generate both a Segwit redeem script and address from WIF
pubkey2addr - convert Bitcoin public key to address
randpair - generate a random private key/address pair
randwif - generate a random private key in WIF format
wif2addr - generate a Bitcoin address from a key in WIF format
wif2hex - convert a private key from WIF to hex format
Wallet/TX operations (bitcoind must be running):
getbalance - like 'bitcoin-cli getbalance' but shows confirmed/unconfirmed,
spendable/unspendable balances for individual {pnm} wallets
listaddress - list the specified {pnm} address and its balance
listaddresses - list {pnm} addresses and their balances
txview - show raw/signed {pnm} transaction in human-readable form
twview - view tracking wallet
General utilities:
General utilities:
hexdump - encode data into formatted hexadecimal form (file or stdin)
unhexdump - decode formatted hexadecimal data (file or stdin)
bytespec - convert a byte specifier such as '1GB' into an integer
@ -116,7 +132,8 @@ cmd_help = """
hexreverse - reverse bytes of a hexadecimal string
rand2file - write 'n' bytes of random data to specified file
randhex - print 'n' bytes (default 32) of random data in hex format
sha256x2 - compute a double sha256 hash of data
hash256 - compute sha256(sha256(data)) (double sha256)
hash160 - compute ripemd160(sha256(data)) (converts hexpubkey to hexaddr)
b58randenc - generate a random 32-byte number and convert it to base 58
b58tostr - convert a base 58 number to a string
strtob58 - convert a string to base 58
@ -125,7 +142,7 @@ cmd_help = """
b32tohex - convert a base 32 number to hexadecimal
hextob32 - convert a hexadecimal number to base 32
File encryption:
File encryption:
encrypt - encrypt a file
decrypt - decrypt a file
{pnm} encryption suite:
@ -133,7 +150,7 @@ cmd_help = """
* Enc: AES256_CTR, 16-byte rand IV, sha256 hash + 32-byte nonce + data
* The encrypted file is indistinguishable from random data
{pnm}-specific operations:
{pnm}-specific operations:
add_label - add descriptive label for {pnm} address in tracking wallet
remove_label - remove descriptive label for {pnm} address in tracking wallet
addrfile_chksum - compute checksum for {pnm} address file
@ -144,7 +161,7 @@ cmd_help = """
id8 - generate 8-character {pnm} ID for a file (or stdin)
str2id6 - generate 6-character {pnm} ID for a string, ignoring spaces
Mnemonic operations (choose 'electrum' (default), 'tirosh' or 'all'
Mnemonic operations (choose 'electrum' (default), 'tirosh' or 'all'
wordlists):
mn_rand128 - generate random 128-bit mnemonic
mn_rand192 - generate random 192-bit mnemonic
@ -156,63 +173,97 @@ cmd_help = """
IMPORTANT NOTE: Though {pnm} mnemonics use the Electrum wordlist, they're
computed using a different algorithm and are NOT Electrum-compatible!
""".format(pnm=pnm)
def tool_usage(prog_name, command):
if command in cmd_data:
{sm}
""".format(pnm=pnm,sm='\n '.join(stdin_msg.split('\n')))
def usage(command):
for v in cmd_data.values():
if v and v[0][-2:] == '-]':
v[0] = v[0][:-2] + ' or STDIN]'
if 'MARGS' in v: v.remove('MARGS')
if not command:
Msg('Usage information for mmgen-tool commands:')
for k,v in cmd_data.items():
Msg(' {:18} {}'.format(k.lower(),' '.join(v)))
Msg('\n '+'\n '.join(stdin_msg.split('\n')))
sys.exit(0)
Command = command.capitalize()
if Command in cmd_data:
import re
for line in cmd_help.split('\n'):
if ' ' + command in line:
if re.match(r'\s+{}\s+'.format(command),line):
c,h = line.split('-',1)
Msg('MMGEN-TOOL {}: {}'.format(c.strip().upper(),h.strip()))
cd = cmd_data[command]
if cd and cd[0][-2:] == '-]':
cd[0] = cd[0][:-2] + ' or STDIN]'
msg('USAGE: %s %s %s' % (prog_name, command, ' '.join(cd)))
cd = cmd_data[Command]
msg('USAGE: %s %s %s' % (g.prog_name, command, ' '.join(cd)))
else:
msg("'%s': no such tool command" % command)
sys.exit(1)
def process_args(prog_name, command, cmd_args):
Help = usage
def process_args(command,cmd_args):
if 'MARGS' in cmd_data[command]:
cmd_data[command].remove('MARGS')
margs = True
else:
margs = False
c_args = [[i.split(' [')[0],i.split(' [')[1][:-1]]
for i in cmd_data[command] if '=' not in i]
c_kwargs = dict([[
i.split(' [')[0],
[i.split(' [')[1].split('=')[0], i.split(' [')[1].split('=')[1][:-1]]
] for i in cmd_data[command] if '=' in i])
u_args = [a for a in cmd_args[:len(c_args)]]
if c_args and c_args[0][1][-1] == '-':
c_args[0][1] = c_args[0][1][:-1] # [str-] -> [str]
# If we're reading from a pipe, make the input the first argument
if len(u_args) < len(c_kwargs) + len(c_args):
if not sys.stdin.isatty():
u_args = [sys.stdin.read()] + u_args
if not margs:
u_args = [a for a in cmd_args[:len(c_args)]]
if len(u_args) < len(c_args):
m1 = 'Command requires exactly %s non-keyword argument%s'
msg(m1 % (len(c_args),suf(c_args,'k')))
tool_usage(prog_name,command)
if c_args and c_args[0][1][-1] == '-':
c_args[0][1] = c_args[0][1][:-1] # [str-] -> [str]
# If we're reading from a pipe, replace '-' with output of previous command
if u_args and u_args[0] == '-':
if not sys.stdin.isatty():
u_args[0] = sys.stdin.read().strip()
if not u_args[0]:
die(2,'{}: ERROR: no output from previous command in pipe'.format(command.lower()))
if not margs and len(u_args) < len(c_args):
m1 = 'Command requires exactly %s non-keyword argument%s'
msg(m1 % (len(c_args),suf(c_args,'s')))
usage(command)
# print u_args
extra_args = len(cmd_args) - len(c_args)
u_kwargs = {}
if extra_args > 0:
if margs:
t = [a.split('=') for a in cmd_args if '=' in a]
tk = [a[0] for a in t]
tk_bad = [a for a in tk if a not in c_kwargs]
if set(tk_bad) != set(tk[:len(tk_bad)]):
die(1,"'{}': illegal keyword argument".format(tk_bad[-1]))
u_kwargs = dict(t[len(tk_bad):])
u_args = cmd_args[:-len(u_kwargs) or None]
elif extra_args > 0:
u_kwargs = dict([a.split('=') for a in cmd_args[len(c_args):] if '=' in a])
if len(u_kwargs) != extra_args:
msg('Command requires exactly %s non-keyword argument%s'
% (len(c_args),suf(c_args,'k')))
tool_usage(prog_name,command)
% (len(c_args),suf(c_args,'s')))
usage(command)
if len(u_kwargs) > len(c_kwargs):
msg('Command requires exactly %s keyword argument%s'
% (len(c_kwargs),suf(c_kwargs,'k')))
tool_usage(prog_name,command)
% (len(c_kwargs),suf(c_kwargs,'s')))
usage(command)
# mdie(c_args,c_kwargs,u_args,u_kwargs)
for k in u_kwargs:
if k not in c_kwargs:
msg("'%s': invalid keyword argument" % k)
tool_usage(prog_name,command)
usage(command)
def conv_type(arg,arg_name,arg_type):
if arg_type == 'str': arg_type = 'unicode'
@ -221,17 +272,19 @@ def process_args(prog_name, command, cmd_args):
elif arg.lower() in ('false','no','0','off'): arg = False
else:
msg("'%s': invalid boolean value for keyword argument" % arg)
tool_usage(prog_name,command)
usage(command)
try:
return __builtins__[arg_type](arg)
except:
die(1,"'%s': Invalid argument for argument %s ('%s' required)" % \
(arg, arg_name, arg_type))
args = [conv_type(u_args[i],c_args[i][0],c_args[i][1]) for i in range(len(c_args))]
if margs:
args = [conv_type(u_args[i],c_args[0][0],c_args[0][1]) for i in range(len(u_args))]
else:
args = [conv_type(u_args[i],c_args[i][0],c_args[i][1]) for i in range(len(c_args))]
kwargs = dict([(k,conv_type(u_kwargs[k],k,c_kwargs[k][0])) for k in u_kwargs])
# mdie(args,kwargs)
return args,kwargs
# Individual cmd_data
@ -252,53 +305,86 @@ def print_convert_results(indata,enc,dec,dtype):
if error:
die(3,"Error! Recoded data doesn't match input!")
def usage(cmd):
tool_usage(g.prog_name, cmd)
help = usage
def hexdump(infile, cols=8, line_nums=True):
def Hexdump(infile, cols=8, line_nums=True):
Msg(pretty_hexdump(
get_data_from_file(infile,dash=True,silent=True,binary=True),
cols=cols,line_nums=line_nums))
def unhexdump(infile):
def Unhexdump(infile):
if g.platform == 'win':
import msvcrt
msvcrt.setmode(sys.stdout.fileno(),os.O_BINARY)
sys.stdout.write(decode_pretty_hexdump(
get_data_from_file(infile,dash=True,silent=True)))
def b58randenc():
def B58randenc():
r = get_random(32)
enc = mmb.b58encode(r)
dec = mmb.b58decode(enc)
print_convert_results(r,enc,dec,'str')
def randhex(nbytes='32'):
def Randhex(nbytes='32'):
Msg(ba.hexlify(get_random(int(nbytes))))
def randwif(compressed=False):
def Randwif(compressed=False):
r_hex = ba.hexlify(get_random(32))
enc = mmb.hex2wif(r_hex,compressed)
dec = mmb.wif2hex(enc)
dec = wif2hex(enc)
print_convert_results(r_hex,enc,dec,'hex')
def randpair(compressed=False):
def Randpair(compressed=False,segwit=False):
if segwit: compressed = True
r_hex = ba.hexlify(get_random(32))
wif = mmb.hex2wif(r_hex,compressed)
addr = mmb.privnum2addr(int(r_hex,16),compressed)
addr = mmb.privnum2addr(int(r_hex,16),compressed,segwit=segwit)
Vmsg('Key (hex): %s' % r_hex)
Vmsg_r('Key (WIF): '); Msg(wif)
Vmsg_r('Addr: '); Msg(addr)
def wif2addr(wif,compressed=False):
s_enc = mmb.wif2hex(wif)
if s_enc == False:
die(1,'Invalid address')
addr = mmb.privnum2addr(int(s_enc,16),compressed)
def Wif2addr(wif,segwit=False):
compressed = mmb.wif_is_compressed(wif)
if segwit and not compressed:
die(1,'Segwit address cannot be generated from uncompressed WIF')
privhex = wif2hex(wif)
addr = mmb.privnum2addr(int(privhex,16),compressed,segwit=segwit)
Vmsg_r('Addr: '); Msg(addr)
def Wif2segwit_pair(wif):
if not mmb.wif_is_compressed(wif):
die(1,'Segwit address cannot be generated from uncompressed WIF')
privhex = wif2hex(wif)
pubhex = mmb.privnum2pubhex(int(privhex,16),compressed=True)
rs = mmb.pubhex2redeem_script(pubhex)
addr = mmb.hexaddr2addr(mmb.hash160(rs),p2sh=True)
addr_chk = mmb.privnum2addr(int(privhex,16),compressed=True,segwit=True)
assert addr == addr_chk
Msg('{}\n{}'.format(rs,addr))
def Hexaddr2addr(hexaddr): Msg(mmb.hexaddr2addr(hexaddr))
def Addr2hexaddr(addr): Msg(mmb.verify_addr(addr,return_hex=True))
def Hash160(pubkeyhex): Msg(mmb.hash160(pubkeyhex))
def Pubhex2addr(pubkeyhex,p2sh=False): Msg(mmb.hexaddr2addr(mmb.hash160(pubkeyhex),p2sh=p2sh))
def Wif2hex(wif): Msg(wif2hex(wif))
def Hex2wif(hexpriv,compressed=False):
Msg(mmb.hex2wif(hexpriv,compressed))
def Privhex2addr(privhex,compressed=False,segwit=False):
if segwit and not compressed:
die(1,'Segwit address can be generated only from a compressed pubkey')
Msg(mmb.privnum2addr(int(privhex,16),compressed,segwit=segwit))
def Privhex2pubhex(privhex,compressed=False): # new
Msg(mmb.privnum2pubhex(int(privhex,16),compressed))
def Pubhex2redeem_script(pubhex): # new
Msg(mmb.pubhex2redeem_script(pubhex))
def Wif2redeem_script(wif): # new
if not mmb.wif_is_compressed(wif):
die(1,'Witness redeem script cannot be generated from uncompressed WIF')
pubhex = mmb.privnum2pubhex(int(wif2hex(wif),16),compressed=True)
Msg(mmb.pubhex2redeem_script(pubhex))
def wif2hex(wif): # wrapper
ret = mmb.wif2hex(wif)
return ret or die(1,'{}: Invalid WIF'.format(wif))
wordlists = 'electrum','tirosh'
dfl_wl_id = 'electrum'
@ -311,123 +397,157 @@ def do_random_mn(nbytes,wordlist):
mn = baseconv.fromhex(hexrand,wl_id)
Msg(' '.join(mn))
def mn_rand128(wordlist=dfl_wl_id): do_random_mn(16,wordlist)
def mn_rand192(wordlist=dfl_wl_id): do_random_mn(24,wordlist)
def mn_rand256(wordlist=dfl_wl_id): do_random_mn(32,wordlist)
def Mn_rand128(wordlist=dfl_wl_id): do_random_mn(16,wordlist)
def Mn_rand192(wordlist=dfl_wl_id): do_random_mn(24,wordlist)
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 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(ba.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 b58tostr(s): Msg(ba.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))
def Strtob58(s,pad=None): Msg(''.join(baseconv.fromhex(ba.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 B58tostr(s): Msg(ba.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))
from mmgen.seed import Mnemonic
def mn_stats(wordlist=dfl_wl_id):
def Mn_stats(wordlist=dfl_wl_id):
wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist))
baseconv.check_wordlist(wordlist)
def mn_printlist(wordlist=dfl_wl_id):
def Mn_printlist(wordlist=dfl_wl_id):
wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist))
Msg('\n'.join(baseconv.digits[wordlist]))
def id8(infile):
def Id8(infile):
Msg(make_chksum_8(
get_data_from_file(infile,dash=True,silent=True,binary=True)
))
def id6(infile):
def Id6(infile):
Msg(make_chksum_6(
get_data_from_file(infile,dash=True,silent=True,binary=True)
))
def str2id6(s): # retain ignoring of space for backwards compat
def Str2id6(s): # retain ignoring of space for backwards compat
Msg(make_chksum_6(''.join(s.split())))
# List MMGen addresses and their balances:
def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=False):
def Listaddress(addr,minconf=1,pager=False,showempty=True,showbtcaddr=True):
return Listaddresses(addrs=addr,minconf=minconf,pager=pager,showempty=showempty,showbtcaddrs=showbtcaddr)
# TODO - move some or all of this code to AddrList
usr_addr_list = []
if addrs:
sid_s,idxs = split2(addrs,':')
sid = SeedID(sid=sid_s)
usr_addr_list = ['{}:{}'.format(sid,a) for a in AddrIdxList(idxs)]
# List MMGen addresses and their balances. TODO: move this code to AddrList
def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=False):
c = bitcoin_connection()
addrs = {} # reusing variable name!
def check_dup_mmid(accts):
help_msg = """
Your tracking wallet is corrupted or has been altered by a non-{pnm} program.
You might be able to salvage your wallet by determining which of the offending
addresses doesn't belong to {pnm} ID {mid} and then typing:
bitcoin-cli importaddress <offending address> "" false
"""
m_prev = None
for m in sorted([l.mmid for l in accts]):
if m == m_prev:
msg('Duplicate MMGen ID ({}) discovered in tracking wallet!\n'.format(m))
bad_accts = MMGenList([l for l in accts if l.mmid == m])
msg(' Affected Bitcoin RPC accounts:\n {}\n'.format('\n '.join(bad_accts)))
bad_addrs = [a[0] for a in c.getaddressesbyaccount([[a] for a in bad_accts],batch=True)]
if len(set(bad_addrs)) != 1:
msg(' Offending addresses:\n {}'.format('\n '.join(bad_addrs)))
msg(help_msg.format(mid=m,pnm=pnm))
die(3,red('Exiting on error'))
m_prev = m
usr_addr_list = []
if addrs:
a = addrs.rsplit(':',1)
if len(a) != 2:
m = "'{}': invalid address list argument (must be in form <seed ID>:[<type>:]<idx list>)"
die(1,m.format(addrs))
usr_addr_list = [MMGenID('{}:{}'.format(a[0],i)) for i in AddrIdxList(a[1])]
class TwAddrList(dict,MMGenObject): pass
addrs = TwAddrList() # reusing name!
total = BTCAmt('0')
for d in c.listunspent(0):
mmaddr,comment = split2(d['account'])
if usr_addr_list and (mmaddr not in usr_addr_list): continue
if (mmaddr[:4] == 'btc:' or is_mmgen_id(mmaddr)) and d['confirmations'] >= minconf:
key = mmaddr.replace(':','_')
if key in addrs:
if addrs[key][2] != d['address']:
if not 'account' in d: continue # skip coinbase outputs with missing account
if d['confirmations'] < minconf: continue
label = TwLabel(d['account'],on_fail='silent')
if label:
if usr_addr_list and (label.mmid not in usr_addr_list): continue
if label.mmid in addrs:
if addrs[label.mmid]['addr'] != d['address']:
die(2,'duplicate BTC address ({}) for this MMGen address! ({})'.format(
(d['address'], addrs[key][2])))
(d['address'], addrs[label.mmid]['addr'])))
else:
addrs[key] = [BTCAmt('0'),MMGenAddrLabel(comment),BTCAddr(d['address'])]
addrs[key][0] += d['amount']
addrs[label.mmid] = { 'amt':BTCAmt('0'), 'lbl':label, 'addr':BTCAddr(d['address']) }
addrs[label.mmid]['amt'] += d['amount']
total += d['amount']
# We use listaccounts only for empty addresses, as it shows false positive balances
if showempty:
accts = c.listaccounts(0,True) # minconf,watchonly
save_a = []
for acct in accts:
mmaddr,comment = split2(acct)
if usr_addr_list and (mmaddr not in usr_addr_list): continue
if mmaddr[:4] == 'btc:' or is_mmgen_id(mmaddr):
key = mmaddr.replace(':','_')
if key not in addrs:
if showbtcaddrs: save_a.append([acct])
addrs[key] = [BTCAmt('0'),MMGenAddrLabel(comment),'']
for acct,addr in zip(save_a,c.getaddressesbyaccount(save_a,batch=True)):
if len(addr) != 1:
die(2,"Account '%s' has more or less than one BTC address!" % addr)
key = split2(acct[0])[0].replace(':','_')
addrs[key][2] = BTCAddr(addr[0])
# args: minconf,watchonly
accts = MMGenList([b for b in [TwLabel(a,on_fail='silent') for a in c.listaccounts(0,True)] if b])
check_dup_mmid(accts)
acct_addrs = c.getaddressesbyaccount([[a] for a in accts],batch=True)
assert len(accts) == len(acct_addrs), 'listaccounts() and getaddressesbyaccount() not of same length'
for a in acct_addrs:
if len(a) != 1:
die(2,"'{}': more than one BTC address in account!".format(a))
for label,addr in zip(accts,[b[0] for b in acct_addrs]):
if usr_addr_list and (label.mmid not in usr_addr_list): continue
if label.mmid not in addrs:
addrs[label.mmid] = { 'amt':BTCAmt('0'), 'lbl':label, 'addr':'' }
if showbtcaddrs:
addrs[label.mmid]['addr'] = BTCAddr(addr)
if not addrs:
die(0,('No addresses with balances!','No tracked addresses!')[showempty])
die(0,('No tracked addresses with balances!','No tracked addresses!')[showempty])
fs = ('{mid} {lbl} {amt}','{mid} {addr} {lbl} {amt}')[showbtcaddrs]
max_mmid_len = max([len(k) for k in addrs if k[:4] != 'btc_'] or [10])
max_lbl_len = max(len(addrs[k][1]) for k in addrs) or 7
out = [fs.format(
out = ([],[green('Chain: {}'.format(g.chain.upper()))])[g.chain in ('testnet','regtest')]
fs = ('{mid} {cmt} {amt}','{mid} {addr} {cmt} {amt}')[showbtcaddrs]
mmaddrs = [k for k in addrs.keys() if k.type == 'mmgen']
max_mmid_len = max(len(k) for k in mmaddrs) + 2 if mmaddrs else 10
max_cmt_len = max(max(len(addrs[k]['lbl'].comment) for k in addrs),7)
out += [fs.format(
mid=MMGenID.fmtc('MMGenID',width=max_mmid_len),
addr=BTCAddr.fmtc('ADDRESS'),
lbl=MMGenAddrLabel.fmtc('COMMENT',width=max_lbl_len),
cmt=TwComment.fmtc('COMMENT',width=max_cmt_len),
amt='BALANCE'
)]
old_sid = ''
def s_mmgen(k):
s = k.split('_')
a,b = s if len(s) == 2 else (k,'')
return '{}_{:>0{w}}'.format(a,b,w=AddrIdx.max_digits+9)
for k in sorted(addrs,key=s_mmgen):
if old_sid and old_sid != k.split('_')[0]: out.append('')
old_sid = k.split('_')[0]
m = 'non-'+g.proj_name if k[:4] == 'btc_' else k.replace('_',':')
al_id_save = None
for mmid in sorted(addrs,key=lambda j: j.sort_key):
if mmid.type == 'mmgen':
if al_id_save and al_id_save != mmid.obj.al_id:
out.append('')
al_id_save = mmid.obj.al_id
mmid_disp = mmid
else:
if al_id_save:
out.append('')
al_id_save = None
mmid_disp = mmid.type
out.append(fs.format(
mid = MMGenID.fmtc(m,width=max_mmid_len,color=True),
addr=(addrs[k][2].fmt(color=True) if showbtcaddrs else None),
lbl=addrs[k][1].fmt(width=max_lbl_len,color=True,nullrepl='-'),
amt=addrs[k][0].fmt('3.0',color=True)))
mid = MMGenID.fmtc(mmid_disp,width=max_mmid_len,color=True),
addr=(addrs[mmid]['addr'].fmt(color=True) if showbtcaddrs else None),
cmt=addrs[mmid]['lbl'].comment.fmt(width=max_cmt_len,color=True,nullrepl='-'),
amt=addrs[mmid]['amt'].fmt('3.0',color=True)))
out.append('\nTOTAL: %s BTC' % total.hl(color=True))
o = '\n'.join(out)
if pager: do_pager(o)
else: Msg(o)
def getbalance(minconf=1):
return do_pager(o) if pager else Msg(o)
def Getbalance(minconf=1):
accts = {}
for d in bitcoin_connection().listunspent(0):
ma = split2(d['account'])[0]
ma = split2(d['account'] if 'account' in d else '')[0] # include coinbase outputs if spendable
keys = ['TOTAL']
if d['spendable']: keys += ['SPENDABLE']
if is_mmgen_id(ma): keys += [ma.split(':')[0]]
@ -445,77 +565,62 @@ def getbalance(minconf=1):
*[s.ljust(16) for s in ' Unconfirmed',' <%s %s'%(mc,lbl),' >=%s %s'%(mc,lbl)]))
for key in sorted(accts.keys()):
Msg(fs.format(key+':', *[a.fmt(color=True,suf=' BTC') for a in accts[key]]))
if 'SPENDABLE' in accts:
Msg(red('Warning: this wallet contains PRIVATE KEYS for the SPENDABLE balance!'))
def txview(infile,pager=False,terse=False):
c = bitcoin_connection()
tx = MMGenTX(infile)
tx.view(pager,pause=False,terse=terse)
def Txview(*infiles,**kwargs):
from mmgen.filename import MMGenFileList
pager = 'pager' in kwargs and kwargs['pager']
terse = 'terse' in kwargs and kwargs['terse']
sort_key = kwargs['sort'] if 'sort' in kwargs else 'mtime'
flist = MMGenFileList(infiles,ftype=MMGenTX)
flist.sort_by_age(key=sort_key) # in-place sort
from mmgen.term import get_terminal_size
sep = u''*get_terminal_size()[0]+'\n'
out = sep.join([MMGenTX(fn).format_view(terse=terse) for fn in flist.names()])
(Msg,do_pager)[pager](out.rstrip())
def twview(pager=False,reverse=False,wide=False,minconf=1,sort='age'):
def Twview(pager=False,reverse=False,wide=False,minconf=1,sort='age',show_days=True,show_mmid=True):
from mmgen.tw import MMGenTrackingWallet
tw = MMGenTrackingWallet(minconf=minconf)
tw.do_sort(sort,reverse=reverse)
tw.show_days = show_days
tw.show_mmid = show_mmid
out = tw.format_for_printing(color=True) if wide else tw.format_for_display()
do_pager(out) if pager else sys.stdout.write(out)
(Msg_r,do_pager)[pager](out)
def add_label(mmaddr,label):
def Add_label(mmaddr,label):
from mmgen.tw import MMGenTrackingWallet
if MMGenTrackingWallet.add_label(mmaddr,label): # returns on failure
s = '{pnm} address {a} in tracking wallet'.format(a=mmaddr,pnm=pnm)
if label: msg("Added label '{}' for {}".format(label,s))
else: msg('Removed label for {}'.format(s))
else:
die(1,'Label could not be %s' % ('removed','added')[bool(label)])
MMGenTrackingWallet.add_label(mmaddr,label) # dies on failure
def remove_label(mmaddr): add_label(mmaddr,'')
def Remove_label(mmaddr): Add_label(mmaddr,'')
def addrfile_chksum(infile):
def Addrfile_chksum(infile):
from mmgen.addr import AddrList
AddrList(infile,chksum_only=True)
def keyaddrfile_chksum(infile):
def Keyaddrfile_chksum(infile):
from mmgen.addr import KeyAddrList
KeyAddrList(infile,chksum_only=True)
def passwdfile_chksum(infile):
def Passwdfile_chksum(infile):
from mmgen.addr import PasswordList
PasswordList(infile=infile,chksum_only=True)
def hexreverse(s):
def Hexreverse(s):
Msg(ba.hexlify(ba.unhexlify(s.strip())[::-1]))
def hexlify(s):
def Hexlify(s):
Msg(ba.hexlify(s))
def sha256x2(s, file_input=False, hex_input=False):
def Hash256(s, file_input=False, hex_input=False):
from hashlib import sha256
if file_input: b = get_data_from_file(s,binary=True)
elif hex_input: b = decode_pretty_hexdump(s)
else: b = s
Msg(sha256(sha256(b).digest()).hexdigest())
def hexaddr2addr(hexaddr):
Msg(mmb.hexaddr2addr(hexaddr))
def addr2hexaddr(addr):
Msg(mmb.verify_addr(addr,return_hex=True))
def pubkey2hexaddr(pubkeyhex):
Msg(mmb.pubhex2hexaddr(pubkeyhex))
def pubkey2addr(pubkeyhex):
Msg(mmb.hexaddr2addr(mmb.pubhex2hexaddr(pubkeyhex)))
def privhex2addr(privkeyhex,compressed=False):
Msg(mmb.privnum2addr(int(privkeyhex,16),compressed))
def wif2hex(wif,compressed=False):
Msg(mmb.wif2hex(wif))
def hex2wif(hexpriv,compressed=False):
Msg(mmb.hex2wif(hexpriv,compressed))
def encrypt(infile,outfile='',hash_preset=''):
def Encrypt(infile,outfile='',hash_preset=''):
data = get_data_from_file(infile,'data for encryption',binary=True)
enc_d = mmgen_encrypt(data,'user data',hash_preset)
if not outfile:
@ -523,7 +628,7 @@ def encrypt(infile,outfile='',hash_preset=''):
write_data_to_file(outfile,enc_d,'encrypted data',binary=True)
def decrypt(infile,outfile='',hash_preset=''):
def Decrypt(infile,outfile='',hash_preset=''):
enc_d = get_data_from_file(infile,'encrypted data',binary=True)
while True:
dec_d = mmgen_decrypt(enc_d,'user data',hash_preset)
@ -537,7 +642,7 @@ def decrypt(infile,outfile='',hash_preset=''):
write_data_to_file(outfile,dec_d,'decrypted data',binary=True)
def find_incog_data(filename,iv_id,keep_searching=False):
def Find_incog_data(filename,iv_id,keep_searching=False):
ivsize,bsize,mod = g.aesctr_iv_len,4096,4096*8
n,carry = 0,' '*ivsize
flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
@ -554,7 +659,7 @@ def find_incog_data(filename,iv_id,keep_searching=False):
if n+i < ivsize: continue
msg('\rIncog data for ID %s found at offset %s' %
(iv_id,n+i-ivsize))
if not keep_searching: sys.exit()
if not keep_searching: sys.exit(0)
carry = d[len(d)-ivsize:]
n += bsize
if not n % mod: msg_r('\rSearched: %s bytes' % n)
@ -562,7 +667,7 @@ def find_incog_data(filename,iv_id,keep_searching=False):
msg('')
os.close(f)
def rand2file(outfile, nbytes, threads=4, silent=False):
def Rand2file(outfile, nbytes, threads=4, silent=False):
nbytes = parse_nbytes(nbytes)
from Crypto import Random
rh = Random.new()
@ -620,4 +725,4 @@ def rand2file(outfile, nbytes, threads=4, silent=False):
q2.join()
f.close()
def bytespec(s): Msg(str(parse_nbytes(s)))
def Bytespec(s): Msg(str(parse_nbytes(s)))

View file

@ -34,22 +34,25 @@ def parse_tw_acct_label(s):
a2 = ret[1] if len(ret) == 2 else None
return a1,a2
class MMGenTWOutput(MMGenListItem):
attrs_reassign = 'label','skip'
attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','days','skip'
label = MMGenListItemAttr('label','MMGenAddrLabel')
class MMGenTrackingWallet(MMGenObject):
class MMGenTwOutputList(list,MMGenObject): pass
class MMGenTwOutput(MMGenListItem):
twmmid = MMGenListItemAttr('twmmid','TwMMGenID')
txid = MMGenListItemAttr('txid','BitcoinTxID')
attrs_reassign = 'label','skip'
attrs = 'txid','vout','amt','label','twmmid','addr','confs','scriptPubKey','days','skip'
wmsg = {
'no_spendable_outputs': """
No spendable outputs found! Import addresses with balances into your
watch-only wallet using '{}-addrimport' and then re-run this program.
""".strip().format(g.proj_name)
""".strip().format(g.proj_name.lower())
}
sort_keys = 'addr','age','amt','txid','mmid'
def __init__(self,minconf=1):
self.unspent = []
self.unspent = self.MMGenTwOutputList()
self.fmt_display = ''
self.fmt_print = ''
self.cols = None
@ -58,57 +61,62 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
self.show_days = True
self.show_mmid = True
self.minconf = minconf
self.get_data()
self.get_unspent_data()
self.sort_key = 'age'
self.do_sort()
self.total = self.get_total_btc()
def get_total_btc(self):
return sum([i.amt for i in self.unspent])
return sum(i.amt for i in self.unspent)
def get_data(self):
def get_unspent_data(self):
if g.bogus_wallet_data: # for debugging purposes only
us_rpc = eval(get_data_from_file(g.bogus_wallet_data))
else:
us_rpc = bitcoin_connection().listunspent(self.minconf)
# write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
# sys.exit()
# sys.exit(0)
if not us_rpc: die(0,self.wmsg['no_spendable_outputs'])
mm_rpc = self.MMGenTwOutputList()
for o in us_rpc:
o['mmid'],o['label'] = parse_tw_acct_label(o['account']) if 'account' in o else ('','')
o['days'] = int(o['confirmations'] * g.mins_per_block / (60*24))
o['amt'] = o['amount'] # TODO
o['addr'] = o['address']
o['confs'] = o['confirmations']
self.unspent = [MMGenTWOutput(**dict([(k,v) for k,v in o.items() if k in MMGenTWOutput.attrs and o[k] not in (None,'')])) for o in us_rpc]
# die(1,''.join([pp_format(i)+'\n' for i in us_rpc]))
# die(1,''.join([str(i)+'\n' for i in self.unspent]))
if not 'account' in o: continue # coinbase outputs have no account field
l = TwLabel(o['account'],on_fail='silent')
if l:
o.update({
'twmmid': l.mmid,
'label': l.comment,
'days': int(o['confirmations'] * g.mins_per_block / (60*24)),
'amt': o['amount'], # TODO
'addr': o['address'],
'confs': o['confirmations']
})
mm_rpc.append(o)
self.unspent = self.MMGenTwOutputList([self.MMGenTwOutput(**dict([(k,v) for k,v in o.items() if k in self.MMGenTwOutput.attrs])) for o in mm_rpc])
for u in self.unspent:
if u.label == None: u.label = ''
if not self.unspent:
die(1,'No tracked unspent outputs in tracking wallet!')
def s_addr(self,i): return i.addr
def s_age(self,i): return 0 - i.confs
def s_amt(self,i): return i.amt
def s_txid(self,i): return '%s %03s' % (i.txid,i.vout)
def s_mmid(self,i):
if i.mmid:
return '{}:{:>0{w}}'.format(
*i.mmid.split(':'), w=AddrIdx.max_digits)
else: return 'G' + (i.label or '')
def do_sort(self,key=None,reverse=None):
if not key: key = self.sort_key
assert key
def do_sort(self,key=None,reverse=False):
sort_funcs = {
'addr': lambda i: i.addr,
'age': lambda i: 0 - i.confs,
'amt': lambda i: i.amt,
'txid': lambda i: '%s %03s' % (i.txid,i.vout),
'mmid': lambda i: i.twmmid.sort_key
}
key = key or self.sort_key
if key not in sort_funcs:
die(1,"'{}': invalid sort key. Valid options: {}".format(key,' '.join(sort_funcs.keys())))
self.sort_key = key
if key not in self.sort_keys:
fs = "'{}': invalid sort key. Valid keys: [{}]"
die(2,fs.format(key,' '.join(self.sort_keys)))
if reverse == None: reverse = self.reverse
self.unspent.sort(key=getattr(self,'s_'+key),reverse=reverse)
assert type(reverse) == bool
self.unspent.sort(key=sort_funcs[key],reverse=reverse or self.reverse)
def sort_info(self,include_group=True):
ret = ([],['Reverse'])[self.reverse]
ret.append(capfirst(self.sort_key).replace('Mmid','MMGenID'))
if include_group and self.group and (self.sort_key in ('addr','txid','mmid')):
ret.append(capfirst(self.sort_key).replace('Twmmid','MMGenID'))
if include_group and self.group and (self.sort_key in ('addr','txid','twmmid')):
ret.append('Grouped')
return ret
@ -126,42 +134,42 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
msg(self.format_for_display())
def format_for_display(self):
unsp = [MMGenTWOutput(**i.__dict__) for i in self.unspent]
unsp = self.unspent
# unsp.pdie()
self.set_term_columns()
for i in unsp:
if i.label == None: i.label = ''
i.skip = ''
mmid_w = max(len(i.mmid or '') for i in unsp) or 10
max_acct_len = max([len((i.mmid or '')+i.label)+1 for i in unsp])
addr_w = min(34+((1+max_acct_len) if self.show_mmid else 0),self.cols-46) + 6
acct_w = min(max_acct_len, max(24,int(addr_w-10)))
# Field widths
min_mmid_w = 12 # DEADBEEF:S:1
mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in unsp) or min_mmid_w
max_acct_w = max(len(i.label) for i in unsp) + mmid_w + 1
addr_w = min(35+(0,1+max_acct_w)[self.show_mmid],self.cols-45)
acct_w = min(max_acct_w, max(24,int(addr_w-10)))
btaddr_w = addr_w - acct_w - 1
label_w = acct_w - mmid_w - 1
tx_w = max(11,min(64, self.cols-addr_w-32))
txdots = ('','...')[tx_w < 64]
fs = ' %-4s %-' + str(tx_w) + 's %-2s %s %s %s'
table_hdr = fs % ('Num',
'TX id'.ljust(tx_w - 5) + ' Vout',
'',
BTCAddr.fmtc('Address',width=addr_w+1),
'Amt(BTC) ',
('Conf.','Age(d)')[self.show_days])
fs = ' %-4s %-{}s %-2s %s %s %s'.format(tx_w)
if self.group and (self.sort_key in ('addr','txid','mmid')):
for i in unsp: i.skip = None
if self.group and (self.sort_key in ('addr','txid','twmmid')):
for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
for k in ('addr','txid','mmid'):
for k in ('addr','txid','twmmid'):
if self.sort_key == k and getattr(a,k) == getattr(b,k):
b.skip = (k,'addr')[k=='mmid']
b.skip = (k,'addr')[k=='twmmid']
hdr_fmt = 'UNSPENT OUTPUTS (sort order: %s) Total BTC: %s'
out = [hdr_fmt % (' '.join(self.sort_info()), self.total.hl()), table_hdr]
out = [hdr_fmt % (' '.join(self.sort_info()), self.total.hl())]
if g.chain in ('testnet','regtest'):
out += [green('Chain: {}'.format(g.chain.upper()))]
af = BTCAddr.fmtc('Address',width=addr_w+1)
cf = ('Conf.','Age(d)')[self.show_days]
out += [fs % ('Num','TX id'.ljust(tx_w - 5) + ' Vout','',af,'Amt(BTC) ',cf)]
for n,i in enumerate(unsp):
addr_dots = '|' + '.'*33
mmid_disp = MMGenID.fmtc('.'*mmid_w if i.skip=='addr'
else i.mmid or 'Non-{}'.format(g.proj_name),width=mmid_w,color=True)
else i.twmmid if i.twmmid.type=='mmgen'
else 'Non-{}'.format(g.proj_name),width=mmid_w,color=True)
if self.show_mmid:
addr_out = '%s %s' % (
type(i.addr).fmtc(addr_dots,width=btaddr_w,color=True) if i.skip == 'addr' \
@ -180,6 +188,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
i.days if self.show_days else i.confs))
self.fmt_display = '\n'.join(out) + '\n'
# unsp.pdie()
return self.fmt_display
def format_for_printing(self,color=False):
@ -190,13 +199,14 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
max_lbl_len = max([len(i.label) for i in self.unspent if i.label] or [1])
for n,i in enumerate(self.unspent):
addr = '=' if i.skip == 'addr' and self.group else i.addr.fmt(color=color)
tx = ' ' * 63 + '=' if i.skip == 'txid' and self.group else str(i.txid)
addr = '|'+'.' * 34 if i.skip == 'addr' and self.group else i.addr.fmt(color=color)
tx = '|'+'.' * 63 if i.skip == 'txid' and self.group else str(i.txid)
s = fs % (str(n+1)+')', tx+','+str(i.vout),addr,
MMGenID.fmtc(i.mmid or 'Non-{}'.format(g.proj_name),width=14,color=color),
MMGenID.fmtc(i.twmmid if i.twmmid.type=='mmgen'
else 'Non-{}'.format(g.proj_name),width=14,color=color),
i.amt.fmt(color=color),i.confs,i.days,
i.label.hl(color=color) if i.label else
MMGenAddrLabel.fmtc('',color=color,nullrepl='-',width=max_lbl_len))
TwComment.fmtc('',color=color,nullrepl='-',width=max_lbl_len))
out.append(s.rstrip())
fs = 'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n'
@ -232,14 +242,15 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
"Removing label for address #%s. Is this what you want?" % n):
return n,s
elif s:
if MMGenAddrLabel(s,on_fail='return'):
if TwComment(s,on_fail='return'):
return n,s
def view_and_sort(self):
def view_and_sort(self,tx):
txos = 'Total to spend, excluding fees: {} BTC\n\n'.format(tx.sum_outputs().hl()) if tx.outputs else ''
prompt = """
Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
{}Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
""".strip()
""".format(txos).strip()
self.display()
msg(prompt)
@ -257,8 +268,8 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
idx,lbl = self.get_idx_and_label_from_user()
if idx:
e = self.unspent[idx-1]
if type(self).add_label(e.mmid,lbl,addr=e.addr):
self.get_data()
if type(self).add_label(e.twmmid,lbl,addr=e.addr):
self.get_unspent_data()
self.do_sort()
msg('%s\n%s\n%s' % (self.fmt_display,prompt,p))
else:
@ -291,36 +302,71 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
# returns on failure
@classmethod
def add_label(cls,arg1,label='',addr=None):
def add_label(cls,arg1,label='',addr=None,silent=False):
from mmgen.tx import is_mmgen_id,is_btc_addr
mmaddr,btcaddr = None,None
if is_btc_addr(addr or arg1):
btcaddr = BTCAddr(addr or arg1,on_fail='return')
if is_mmgen_id(arg1):
mmaddr = MMGenID(arg1)
elif is_btc_addr(arg1): # called from 'mmgen-tool add_label'
addr = arg1
mmaddr = 'btc:'+arg1
elif not arg1 and is_btc_addr(addr): # called from view_and_sort(), non-MMGen addr
mmaddr = 'btc:'+addr
else:
die(3,'{}: not a BTC address or {} ID'.format(arg1,g.proj_name))
mmaddr = TwMMGenID(arg1)
if addr:
if not BTCAddr(addr,on_fail='return'): return False
else:
if not btcaddr and not mmaddr:
msg("Address '{}' invalid or not found in tracking wallet".format(addr or arg1))
return False
if not btcaddr:
from mmgen.addr import AddrData
addr = AddrData(source='tw').mmaddr2btcaddr(mmaddr)
if not addr:
msg('{} address {} not found in tracking wallet'.format(g.proj_name,mmaddr))
return False
btcaddr = AddrData(source='tw').mmaddr2btcaddr(mmaddr)
label = MMGenAddrLabel(label,on_fail='return')
if not label and label != '': return False
if not btcaddr:
msg("{} address '{}' not found in tracking wallet".format(g.proj_name,mmaddr))
return False
# Checked that the user isn't importing a random address
if not btcaddr.is_in_tracking_wallet():
msg("Address '{}' not in tracking wallet".format(btcaddr))
return False
c = bitcoin_connection()
if not btcaddr.is_for_current_chain():
msg("Address '{}' not valid for chain {}".format(btcaddr,g.chain.upper()))
return False
# Allow for the possibility that BTC addr of MMGen addr was entered.
# Do reverse lookup, so that MMGen addr will not be marked as non-MMGen.
if not mmaddr:
from mmgen.addr import AddrData
ad = AddrData(source='tw')
mmaddr = ad.btcaddr2mmaddr(btcaddr)
if not mmaddr: mmaddr = 'btc:'+btcaddr
mmaddr = TwMMGenID(mmaddr)
cmt = TwComment(label,on_fail='return')
if cmt in (False,None): return False
lbl = TwLabel(mmaddr + ('',' '+cmt)[bool(cmt)]) # label is ASCII for now
# NOTE: this works because importaddress() removes the old account before
# associating the new account with the address.
# Will be replaced by setlabel() with new RPC label API
# RPC args: addr,label,rescan[=true],p2sh[=none]
ret = c.importaddress(btcaddr,lbl,False,on_fail='return')
acct = mmaddr + (' ' + label if label else '') # label is ASCII for now
# return on failure - args: addr,label,rescan,p2sh
ret = bitcoin_connection().importaddress(addr,acct,False,on_fail='return')
from mmgen.rpc import rpc_error,rpc_errmsg
if rpc_error(ret): msg('From bitcoind: ' + rpc_errmsg(ret))
return not rpc_error(ret)
if rpc_error(ret):
msg('From bitcoind: ' + rpc_errmsg(ret))
if not silent:
msg('Label could not be {}'.format(('removed','added')[bool(label)]))
return False
else:
m = mmaddr.type.replace('mmg','MMG')
a = mmaddr.replace('btc:','')
s = '{} address {} in tracking wallet'.format(m,a)
if label: msg("Added label '{}' to {}".format(label,s))
else: msg('Removed label from {}'.format(s))
return True
@classmethod
def remove_label(cls,mmaddr): cls.add_label(mmaddr,'')

View file

@ -17,7 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
tx.py: Bitcoin transaction routines
tx.py: Transaction routines for the MMGen suite
"""
import sys,os
@ -30,24 +30,97 @@ def is_mmgen_seed_id(s): return SeedID(sid=s,on_fail='silent')
def is_mmgen_idx(s): return AddrIdx(s,on_fail='silent')
def is_mmgen_id(s): return MMGenID(s,on_fail='silent')
def is_btc_addr(s): return BTCAddr(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):
if s == '': return False
from mmgen.bitcoin import wif2hex
return bool(wif2hex(s))
class MMGenTxInputOldFmt(MMGenListItem): # for converting old tx files only
tr = {'amount':'amt', 'address':'addr', 'confirmations':'confs','comment':'label'}
attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','wif'
attrs_priv = 'tr',
def segwit_is_active(exit_on_error=False):
d = bitcoin_connection().getblockchaininfo()
if d['chain'] == 'regtest' or d['bip9_softforks']['segwit']['status'] == 'active':
return True
if g.skip_segwit_active_check: return True
if exit_on_error:
die(2,'Segwit not active on this chain. Exiting')
else:
return False
class MMGenTxInput(MMGenListItem):
attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','have_wif','sequence'
label = MMGenListItemAttr('label','MMGenAddrLabel')
def bytes2int(hex_bytes):
r = hexlify(unhexlify(hex_bytes)[::-1])
if r[0] in '89abcdef':
die(3,"{}: Negative values not permitted in transaction!".format(hex_bytes))
return int(r,16)
class MMGenTxOutput(MMGenListItem):
attrs = 'txid','vout','amt','label','mmid','addr','have_wif','is_chg'
label = MMGenListItemAttr('label','MMGenAddrLabel')
def bytes2btc(hex_bytes):
return bytes2int(hex_bytes) * g.satoshi
from collections import OrderedDict
class DeserializedTX(OrderedDict,MMGenObject): # need to add MMGen types
def __init__(self,txhex):
tx = list(unhexlify(txhex))
tx_copy = tx[:]
def hshift(l,n,reverse=False):
ret = l[:n]
del l[:n]
return hexlify(''.join(ret[::-1] if reverse else ret))
# https://bitcoin.org/en/developer-reference#compactsize-unsigned-integers
# For example, the number 515 is encoded as 0xfd0302.
def readVInt(l):
s = int(hexlify(l[0]),16)
bytes_len = 1 if s < 0xfd else 2 if s == 0xfd else 4 if s == 0xfe else 8
if bytes_len != 1: del l[0]
ret = int(hexlify(''.join(l[:bytes_len][::-1])),16)
del l[:bytes_len]
return ret
d = { 'version': bytes2int(hshift(tx,4)) }
has_witness = (False,True)[hexlify(tx[0])=='00']
if has_witness:
u = hshift(tx,2)[2:]
if u != '01':
die(2,"'{}': Illegal value for flag in transaction!".format(u))
del tx_copy[-len(tx)-2:-len(tx)]
d['num_txins'] = readVInt(tx)
d['txins'] = MMGenList([OrderedDict((
('txid', hshift(tx,32,reverse=True)),
('vout', bytes2int(hshift(tx,4))),
('scriptSig', hshift(tx,readVInt(tx))),
('nSeq', hshift(tx,4,reverse=True))
)) for i in range(d['num_txins'])])
d['num_txouts'] = readVInt(tx)
d['txouts'] = MMGenList([OrderedDict((
('amount', bytes2btc(hshift(tx,8))),
('scriptPubKey', hshift(tx,readVInt(tx)))
)) for i in range(d['num_txouts'])])
d['witness_size'] = 0
if has_witness:
# https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
# A non-witness program (defined hereinafter) txin MUST be associated with an empty
# witness field, represented by a 0x00.
del tx_copy[-len(tx):-4]
wd,tx = tx[:-4],tx[-4:]
d['witness_size'] = len(wd) + 2 # add marker and flag
for i in range(len(d['txins'])):
if hexlify(wd[0]) == '00':
hshift(wd,1)
continue
d['txins'][i]['witness'] = [hshift(wd,readVInt(wd)) for item in range(readVInt(wd))]
if wd:
die(3,'More witness data than inputs with witnesses!')
d['lock_time'] = bytes2int(hshift(tx,4))
d['txid'] = hexlify(sha256(sha256(''.join(tx_copy)).digest()).digest()[::-1])
keys = 'txid','version','lock_time','witness_size','num_txins','txins','num_txouts','txouts'
return OrderedDict.__init__(self, ((k,d[k]) for k in keys))
class MMGenTX(MMGenObject):
ext = 'rawtx'
@ -56,11 +129,25 @@ class MMGenTX(MMGenObject):
txid_ext = 'txid'
desc = 'transaction'
class MMGenTxInput(MMGenListItem):
attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','have_wif','sequence'
txid = MMGenListItemAttr('txid','BitcoinTxID')
scriptPubKey = MMGenListItemAttr('scriptPubKey','HexStr')
class MMGenTxOutput(MMGenListItem):
attrs = 'txid','vout','amt','label','mmid','addr','have_wif','is_chg'
class MMGenTxInputOldFmt(MMGenListItem): # for converting old tx files only
tr = {'amount':'amt', 'address':'addr', 'confirmations':'confs','comment':'label'}
attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','wif'
attrs_priv = 'tr',
class MMGenTxInputList(list,MMGenObject): pass
class MMGenTxOutputList(list,MMGenObject): pass
def __init__(self,filename=None):
self.inputs = []
self.inputs_enc = []
self.outputs = []
self.outputs_enc = []
self.inputs = self.MMGenTxInputList()
self.outputs = self.MMGenTxOutputList()
self.send_amt = BTCAmt('0') # total amt minus change
self.hex = '' # raw serialized hex transaction
self.label = MMGenTXLabel('')
@ -70,13 +157,21 @@ class MMGenTX(MMGenObject):
self.chksum = ''
self.fmt_data = ''
self.blockcount = 0
self.chain = None
if filename:
if get_extension(filename) == self.sig_ext:
self.mark_signed()
self.parse_tx_file(filename)
self.check_sigs() # marks the tx as signed
# repeat with sign and send, because bitcoind could be restarted
self.die_if_incorrect_chain()
def die_if_incorrect_chain(self):
if self.chain and g.chain and self.chain != g.chain:
die(2,'Transaction is for {}, but current chain is {}!'.format(self.chain,g.chain))
def add_output(self,btcaddr,amt,is_chg=None):
self.outputs.append(MMGenTxOutput(addr=btcaddr,amt=amt,is_chg=is_chg))
self.outputs.append(self.MMGenTxOutput(addr=btcaddr,amt=amt,is_chg=is_chg))
def get_chg_output_idx(self):
for i in range(len(self.outputs)):
@ -87,7 +182,7 @@ class MMGenTX(MMGenObject):
def update_output_amt(self,idx,amt):
o = self.outputs[idx].__dict__
o['amt'] = amt
self.outputs[idx] = MMGenTxOutput(**o)
self.outputs[idx] = self.MMGenTxOutput(**o)
def del_output(self,idx):
self.outputs.pop(idx)
@ -95,7 +190,7 @@ class MMGenTX(MMGenObject):
def sum_outputs(self,exclude=None):
olist = self.outputs if exclude == None else \
self.outputs[:exclude] + self.outputs[exclude+1:]
return BTCAmt(sum([e.amt for e in olist]))
return BTCAmt(sum(e.amt for e in olist))
def add_mmaddrs_to_outputs(self,ad_w,ad_f):
a = [e.addr for e in self.outputs]
@ -107,7 +202,7 @@ class MMGenTX(MMGenObject):
if f: e.label = f
# def encode_io(self,desc):
# tr = getattr((MMGenTxOutput,MMGenTxInput)[desc=='inputs'],'tr')
# tr = getattr((self.MMGenTxOutput,self.MMGenTxInput)[desc=='inputs'],'tr')
# tr_rev = dict([(v,k) for k,v in tr.items()])
# return [dict([(tr_rev[e] if e in tr_rev else e,getattr(d,e)) for e in d.__dict__])
# for d in getattr(self,desc)]
@ -140,24 +235,89 @@ class MMGenTX(MMGenObject):
def edit_comment(self):
return self.add_comment(self)
# https://bitcoin.stackexchange.com/questions/1195/
# how-to-calculate-transaction-size-before-sending
def has_segwit_inputs(self):
return any(i.mmid and i.mmid.mmtype == 'S' for i in self.inputs)
# https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending
# 180: uncompressed, 148: compressed
def get_size(self):
def estimate_size_old(self):
if not self.inputs or not self.outputs: return None
return len(self.inputs)*180 + len(self.outputs)*34 + 10
# https://bitcoincore.org/en/segwit_wallet_dev/
# vsize: 3 times of the size with original serialization, plus the size with new
# serialization, divide the result by 4 and round up to the next integer.
# TODO: results differ slightly from actual transaction size
def estimate_vsize(self):
if not self.inputs or not self.outputs: return None
sig_size = 72 # sig in DER format
pubkey_size = { 'compressed':33, 'uncompressed':65 }
outpoint_size = 36 # txid + vout
def get_inputs_size():
segwit_isize = outpoint_size + 1 + 23 + 4 # (txid,vout) [scriptSig size] scriptSig nSeq # = 64
# txid vout [scriptSig size] scriptSig (<sig> <pubkey>) nSeq
legacy_isize = outpoint_size + 1 + 2 + sig_size + pubkey_size['uncompressed'] + 4 # = 180
compressed_isize = outpoint_size + 1 + 2 + sig_size + pubkey_size['compressed'] + 4 # = 148
ret = sum((legacy_isize,segwit_isize)[i.mmid.mmtype=='S'] for i in self.inputs if i.mmid)
# assume all non-MMGen pubkeys are compressed (we have no way of knowing
# until we see the key). TODO: add user option to specify this?
return ret + sum(compressed_isize for i in self.inputs if not i.mmid)
def get_outputs_size():
return sum((34,32)[o.addr.addr_fmt=='p2sh'] for o in self.outputs)
# https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
# The witness is a serialization of all witness data of the transaction. Each txin is
# associated with a witness field. A witness field starts with a var_int to indicate the
# number of stack items for the txin. It is followed by stack items, with each item starts
# with a var_int to indicate the length. Witness data is NOT script.
# A non-witness program txin MUST be associated with an empty witness field, represented
# by a 0x00. If all txins are not witness program, a transaction's wtxid is equal to its txid.
def get_witness_size():
if not self.has_segwit_inputs(): return 0
wf_size = 1 + 1 + sig_size + 1 + pubkey_size['compressed'] # vInt vInt sig vInt pubkey = 108
return sum((1,wf_size)[bool(i.mmid) and i.mmid.mmtype=='S'] for i in self.inputs)
isize = get_inputs_size()
osize = get_outputs_size()
wsize = get_witness_size()
# pmsg([i.mmid and i.mmid.mmtype == 'S' for i in self.inputs])
# pmsg([i.mmid for i in self.inputs])
# pmsg([i.mmid for i in self.outputs])
# pmsg('isize',isize)
# pmsg('osize',osize)
# pmsg('wsize',wsize)
# TODO: compute real varInt sizes instead of assuming 1 byte
# old serialization: [nVersion] [vInt][txins][vInt][txouts] [nLockTime]
old_size = 4 + 1 + isize + 1 + osize + 4
# new serialization: [nVersion][marker][flag][vInt][txins][vInt][txouts][witness][nLockTime]
new_size = 4 + 1 + 1 + 1 + isize + 1 + osize + wsize + 4 \
if wsize else old_size
ret = (old_size * 3 + new_size) / 4
# pmsg('old_size',old_size) # This should be equal to the size of serialized signed tx
# pmsg('ret',ret)
# pmsg('estimate_size_old',self.estimate_size_old())
return ret
estimate_size = estimate_vsize
def get_fee(self):
return self.sum_inputs() - self.sum_outputs()
def btc2spb(self,btc_fee):
return int(btc_fee/g.satoshi/self.get_size())
return int(btc_fee/g.satoshi/self.estimate_size())
def get_relay_fee(self):
assert self.get_size()
kb_fee = BTCAmt(bitcoin_connection().getinfo()['relayfee'])
assert self.estimate_size()
kb_fee = BTCAmt(bitcoin_connection().getnetworkinfo()['relayfee'])
vmsg('Relay fee: {} BTC/kB'.format(kb_fee))
return kb_fee * self.get_size() / 1024
return kb_fee * self.estimate_size() / 1024
def convert_fee_spec(self,tx_fee,tx_size,on_fail='throw'):
if BTCAmt(tx_fee,on_fail='silent'):
@ -174,7 +334,7 @@ class MMGenTX(MMGenObject):
assert False, "'{}': invalid tx-fee argument".format(tx_fee)
def get_usr_fee(self,tx_fee,desc='Missing description'):
btc_fee = self.convert_fee_spec(tx_fee,self.get_size(),on_fail='return')
btc_fee = self.convert_fee_spec(tx_fee,self.estimate_size(),on_fail='return')
if btc_fee == None:
msg("'{}': cannot convert satoshis-per-byte to BTC because transaction size is unknown".format(tx_fee))
assert False # because we shouldn't be calling this if tx size is unknown
@ -215,28 +375,35 @@ class MMGenTX(MMGenObject):
if hasattr(e,attr): delattr(e,attr)
def decode_io(self,desc,data):
io = (MMGenTxOutput,MMGenTxInput)[desc=='inputs']
return [io(**dict([(k,d[k]) for k in io.attrs
if k in d and d[k] not in ('',None)])) for d in data]
io,il = (
(self.MMGenTxOutput,self.MMGenTxOutputList),
(self.MMGenTxInput,self.MMGenTxInputList)
)[desc=='inputs']
return il([io(**dict([(k,d[k]) for k in io.attrs
if k in d and d[k] not in ('',None)])) for d in data])
def decode_io_oldfmt(self,data):
io = MMGenTxInputOldFmt
io = self.MMGenTxInputOldFmt
tr_rev = dict([(v,k) for k,v in io.tr.items()])
copy_keys = [tr_rev[k] if k in tr_rev else k for k in io.attrs]
return [io(**dict([(io.tr[k] if k in io.tr else k,d[k])
for k in copy_keys if k in d and d[k] != ''])) for d in data]
def copy_inputs_from_tw(self,data):
self.inputs = self.decode_io('inputs',[e.__dict__ for e in data])
def copy_inputs_from_tw(self,tw_unspent_data):
txi,self.inputs = self.MMGenTxInput,self.MMGenTxInputList()
for d in tw_unspent_data:
t = txi(**dict([(attr,getattr(d,attr)) for attr in d.__dict__ if attr in txi.attrs]))
if d.twmmid.type == 'mmgen': t.mmid = d.twmmid # twmmid -> mmid
self.inputs.append(t)
def get_input_sids(self):
return set([e.mmid[:8] for e in self.inputs if e.mmid])
return set(e.mmid.sid for e in self.inputs if e.mmid)
def get_output_sids(self):
return set([e.mmid[:8] for e in self.outputs if e.mmid])
return set(e.mmid.sid for e in self.outputs if e.mmid)
def sum_inputs(self):
return sum([e.amt for e in self.inputs])
return sum(e.amt for e in self.inputs)
def add_timestamp(self):
self.timestamp = make_timestamp()
@ -247,7 +414,8 @@ class MMGenTX(MMGenObject):
def format(self):
from mmgen.bitcoin import b58encode
lines = [
'{} {} {} {}'.format(
'{} {} {} {} {}'.format(
self.chain.upper() if self.chain else 'Unknown',
self.txid,
self.send_amt,
self.timestamp,
@ -266,68 +434,136 @@ class MMGenTX(MMGenObject):
self.fmt_data = '\n'.join([self.chksum] + lines)+'\n'
def get_non_mmaddrs(self,desc):
return list(set([i.addr for i in getattr(self,desc) if not i.mmid]))
return list(set(i.addr for i in getattr(self,desc) if not i.mmid))
# return true or false, don't exit
def sign(self,c,tx_num_str,keys):
self.die_if_incorrect_chain()
if not keys:
msg('No keys. Cannot sign!')
return False
qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'k')))
dmsg('Keys:\n %s' % '\n '.join(keys))
qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'s')))
sig_data = [dict([(k,getattr(d,k)) for k in 'txid','vout','scriptPubKey'])
for d in self.inputs]
dmsg('Sig data:\n%s' % pp_format(sig_data))
dmsg('Raw hex:\n%s' % self.hex)
sig_data = []
for d in self.inputs:
e = dict([(k,getattr(d,k)) for k in 'txid','vout','scriptPubKey','amt'])
e['amount'] = e['amt']
del e['amt']
wif = keys[d.addr]
if d.mmid and d.mmid.mmtype == 'S':
from mmgen.bitcoin import pubhex2redeem_script
from mmgen.addr import keygen_wif2pubhex,keygen_selector
pubhex = keygen_wif2pubhex(wif,keygen_selector())
e['redeemScript'] = pubhex2redeem_script(pubhex)
sig_data.append(e)
from mmgen.bitcoin import hash256
msg_r('Signing transaction{}...'.format(tx_num_str))
sig_tx = c.signrawtransaction(self.hex,sig_data,keys)
# sighashtype defaults to 'ALL'
sig_tx = c.signrawtransaction(self.hex,sig_data,keys.values())
if sig_tx['complete']:
msg('OK')
self.hex = sig_tx['hex']
self.mark_signed()
vmsg('Signed transaction size: {}'.format(len(self.hex)/2))
dt = DeserializedTX(self.hex)
txid = dt['txid']
self.check_sigs(dt)
assert txid == c.decoderawtransaction(self.hex)['txid'], 'txid mismatch (after signing)'
self.btc_txid = BitcoinTxID(txid,on_fail='return')
msg('OK')
return True
else:
msg('failed\nBitcoind returned the following errors:')
pp_msg(sig_tx['errors'])
pmsg(sig_tx['errors'])
return False
def mark_raw(self):
self.desc = 'transaction'
self.ext = self.raw_ext
def mark_signed(self):
def mark_signed(self): # called ONLY by check_sigs()
self.desc = 'signed transaction'
self.ext = self.sig_ext
def is_signed(self,color=False):
def marked_signed(self,color=False):
ret = self.desc == 'signed transaction'
return (red,green)[ret](str(ret)) if color else ret
def check_signed(self,c):
d = c.decoderawtransaction(self.hex)
ret = bool(d['vin'][0]['scriptSig']['hex'])
if ret: self.mark_signed()
return ret
def check_sigs(self,deserial_tx=None): # return False if no sigs, die on error
txins = (deserial_tx or DeserializedTX(self.hex))['txins']
has_ss = any(ti['scriptSig'] for ti in txins)
has_witness = any('witness' in ti and ti['witness'] for ti in txins)
if not (has_ss or has_witness):
return False
for ti in txins:
if ti['scriptSig'][:6] == '160014' and len(ti['scriptSig']) == 46: # P2SH-P2WPKH
assert 'witness' in ti, 'missing witness'
assert type(ti['witness']) == list and len(ti['witness']) == 2, 'malformed witness'
assert len(ti['witness'][1]) == 66, 'incorrect witness pubkey length'
elif ti['scriptSig'] == '': # native P2WPKH
die(3,('TX has missing signature','Native P2WPKH not implemented')['witness' in ti])
else: # non-witness
assert not 'witness' in ti, 'non-witness input has witness'
# sig_size 72 (DER format), pubkey_size 'compressed':33, 'uncompressed':65
assert (200 < len(ti['scriptSig']) < 300), 'malformed scriptSig' # VERY rough check
self.mark_signed()
return True
def has_segwit_outputs(self):
return any(o.mmid and o.mmid.mmtype == 'S' for o in self.outputs)
def is_in_mempool(self,c):
return 'size' in c.getmempoolentry(self.btc_txid,on_fail='silent')
def is_in_wallet(self,c):
ret = c.gettransaction(self.btc_txid,on_fail='silent')
return 'confirmations' in ret and ret['confirmations'] > 0
def is_replaced(self,c):
if self.is_in_mempool(c): return False
ret = c.gettransaction(self.btc_txid,on_fail='silent')
if not 'bip125-replaceable' in ret or not 'confirmations' in ret or ret['confirmations'] > 0:
return False
return -ret['confirmations'] + 1 # 1: replacement in mempool, 2: replacement confirmed
def is_in_utxos(self,c):
return 'txid' in c.getrawtransaction(self.btc_txid,True,on_fail='silent')
def send(self,c,prompt_user=True):
self.die_if_incorrect_chain()
bogus_send = os.getenv('MMGEN_BOGUS_SEND')
if self.has_segwit_outputs() and not segwit_is_active() and not bogus_send:
m = 'Transaction has MMGen Segwit outputs, but this blockchain does not support Segwit'
die(2,m+' at the current height')
if self.get_fee() > g.max_tx_fee:
die(2,'Transaction fee ({}) greater than max_tx_fee ({})!'.format(self.get_fee(),g.max_tx_fee))
if self.is_in_mempool(c):
msg('Warning: transaction is in mempool!')
elif self.is_in_wallet(c):
die(1,'Transaction has been confirmed!')
elif self.is_in_utxos(c):
die(2,red('ERROR: transaction is in the blockchain (but not in the tracking wallet)!'))
ret = self.is_replaced(c) # 1: replacement in mempool, 2: replacement confirmed
if ret:
die(1,'Transaction has been replaced'+('',', and the replacement TX is confirmed')[ret==2]+'!')
if prompt_user:
m1 = ("Once this transaction is sent, there's no taking it back!",'')[bool(opt.quiet)]
m2 = 'broadcast this transaction to the network'
m2 = 'broadcast this transaction to the {} network'.format(g.chain.upper())
m3 = ('YES, I REALLY WANT TO DO THIS','YES')[bool(opt.quiet or opt.yes)]
confirm_or_exit(m1,m2,m3)
msg('Sending transaction')
if os.getenv('MMGEN_BOGUS_SEND'):
if bogus_send:
ret = 'deadbeef' * 8
m = 'BOGUS transaction NOT sent: %s'
else:
@ -335,15 +571,15 @@ class MMGenTX(MMGenObject):
m = 'Transaction sent: %s'
if ret:
self.btc_txid = BitcoinTxID(ret,on_fail='return')
if self.btc_txid:
self.desc = 'sent transaction'
msg(m % self.btc_txid.hl())
self.add_timestamp()
self.add_blockcount(c)
return True
if not bogus_send:
assert ret == self.btc_txid, 'txid mismatch (after sending)'
self.desc = 'sent transaction'
msg(m % self.btc_txid.hl())
self.add_timestamp()
self.add_blockcount(c)
return True
# rpc implementation exits on failure, so we won't get here
# rpc call exits on failure, so we won't get here
msg('Sending of transaction {} failed'.format(self.txid))
return False
@ -375,7 +611,7 @@ class MMGenTX(MMGenObject):
o = self.format_view(terse=terse).encode('utf8')
if pager: do_pager(o)
else:
sys.stdout.write(o)
Msg_r(o)
from mmgen.term import get_char
if pause:
get_char('Press any key to continue: ')
@ -397,6 +633,7 @@ class MMGenTX(MMGenObject):
self.inputs[0].sequence = g.max_int - 2
def format_view(self,terse=False):
# self.pdie()
try:
blockcount = bitcoin_connection().getblockcount()
except:
@ -408,9 +645,10 @@ class MMGenTX(MMGenObject):
)[bool(terse)]
out = hdr_fs.format(self.txid.hl(),self.send_amt.hl(),self.timestamp,
self.is_rbf(color=True),self.is_signed(color=True))
self.is_rbf(color=True),self.marked_signed(color=True))
enl = ('\n','')[bool(terse)]
if self.chain in ('testnet','regtest'): out += green('Chain: {}\n'.format(self.chain.upper()))
if self.btc_txid: out += 'Bitcoin TxID: {}\n'.format(self.btc_txid.hl())
out += enl
@ -418,9 +656,8 @@ class MMGenTX(MMGenObject):
out += 'Comment: %s\n%s' % (self.label.hl(),enl)
out += 'Inputs:\n' + enl
nonmm_str = '(non-{pnm} address){s}'.format(pnm=g.proj_name,s=('',' ')[terse])
# for i in self.inputs: print i #DEBUG
for n,e in enumerate(self.inputs):
nonmm_str = '(non-{pnm} address){s} '.format(pnm=g.proj_name,s=('',' ')[terse])
for n,e in enumerate(sorted(self.inputs,key=lambda o: o.mmid.sort_key if o.mmid else o.addr)):
if blockcount:
confs = e.confs + blockcount - self.blockcount
days = int(confs * g.mins_per_block / (60*24))
@ -440,7 +677,7 @@ class MMGenTX(MMGenObject):
out += '\n'
out += 'Outputs:\n' + enl
for n,e in enumerate(self.outputs):
for n,e in enumerate(sorted(self.outputs,key=lambda o: o.mmid.sort_key if o.mmid else o.addr)):
if e.mmid:
app=('',' (chg)')[bool(e.is_chg and terse)]
mmid_fmt = e.mmid.fmt(width=len(nonmm_str),encl='()',color=True,
@ -474,7 +711,11 @@ class MMGenTX(MMGenObject):
)
if opt.verbose:
ts = len(self.hex)/2 if self.hex else 'unknown'
out += 'Transaction size: estimated - {}, actual - {}\n'.format(self.get_size(),ts)
out += 'Transaction size: Vsize={} Actual={}'.format(self.estimate_size(),ts)
if self.marked_signed():
ws = DeserializedTX(self.hex)['witness_size']
out += ' Base={} Witness={}'.format(ts-ws,ws)
out += '\n'
# TX label might contain non-ascii chars
return out
@ -489,7 +730,7 @@ class MMGenTX(MMGenObject):
if len(tx_data) < 5: do_err('number of lines')
self.chksum = tx_data.pop(0)
self.chksum = HexStr(tx_data.pop(0))
if self.chksum != make_chksum_6(' '.join(tx_data)):
do_err('checksum')
@ -517,12 +758,17 @@ class MMGenTX(MMGenObject):
else:
do_err('number of lines')
if len(metadata.split()) != 4: do_err('metadata')
metadata = metadata.split()
if len(metadata) not in (4,5): do_err('metadata')
if len(metadata) == 5:
t = metadata.pop(0)
self.chain = (t.lower(),None)[t=='Unknown']
self.txid,send_amt,self.timestamp,blockcount = metadata.split()
self.txid,send_amt,self.timestamp,blockcount = metadata
self.txid = MMGenTxID(self.txid)
self.send_amt = BTCAmt(send_amt)
self.blockcount = int(blockcount)
self.hex = HexStr(self.hex)
try: unhexlify(self.hex)
except: do_err('hex data')
@ -530,6 +776,9 @@ class MMGenTX(MMGenObject):
try: self.inputs = self.decode_io('inputs',eval(inputs_data))
except: do_err('inputs data')
if not self.chain and not self.inputs[0].addr.testnet:
self.chain = 'mainnet'
try: self.outputs = self.decode_io('outputs',eval(outputs_data))
except: do_err('btc-to-mmgen address map data')
@ -545,9 +794,9 @@ class MMGenBumpTX(MMGenTX):
if not self.is_rbf():
die(1,"Transaction '{}' is not replaceable (RBF)".format(self.txid))
# If sending, require tx to have been signed and broadcast
# If sending, require tx to have been signed
if send:
if not self.is_signed():
if not self.marked_signed():
die(1,"File '{}' is not a signed {} transaction file".format(filename,g.proj_name))
if not self.btc_txid:
die(1,"Transaction '{}' was not broadcast to the network".format(self.txid,g.proj_name))
@ -560,7 +809,8 @@ class MMGenBumpTX(MMGenTX):
init_reply = opt.output_to_reduce
while True:
if init_reply == None:
reply = my_raw_input('Which output do you wish to deduct the fee from? ')
m = 'Choose an output to deduct the fee from (Hit ENTER for the change output): '
reply = my_raw_input(m) or 'c'
else:
reply,init_reply = init_reply,None
if chg_idx == None and not is_int(reply):

View file

@ -121,7 +121,10 @@ def mmaddr2baddr(c,mmaddr,ad_w,ad_f):
return BTCAddr(btc_addr)
def get_fee_from_estimate_or_usr(tx,c,estimate_fail_msg_shown=[]):
def get_fee_from_estimate_or_user(tx,estimate_fail_msg_shown=[]):
c = bitcoin_connection()
if opt.tx_fee:
desc = 'User-selected'
start_fee = opt.tx_fee
@ -134,14 +137,86 @@ def get_fee_from_estimate_or_usr(tx,c,estimate_fail_msg_shown=[]):
estimate_fail_msg_shown.append(True)
start_fee = None
else:
start_fee = BTCAmt(ret) * opt.tx_fee_adj * tx.get_size() / 1024
start_fee = BTCAmt(ret) * opt.tx_fee_adj * tx.estimate_size() / 1024
if opt.verbose:
msg('{} fee ({} confs): {} BTC/kB'.format(desc,opt.tx_confs,ret))
msg('TX size (estimated): {}'.format(tx.get_size()))
msg('TX size (estimated): {}'.format(tx.estimate_size()))
return tx.get_usr_fee_interactive(start_fee,desc=desc)
def txcreate(opt,cmd_args,do_info=False,caller='txcreate'):
def get_outputs_from_cmdline(cmd_args,tx):
from mmgen.addr import AddrList,AddrData
addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext]
cmd_args = set(cmd_args) - set(addrfiles)
ad_f = AddrData()
for a in addrfiles:
check_infile(a)
ad_f.add(AddrList(a))
ad_w = AddrData(source='tw')
for a in cmd_args:
if ',' in a:
a1,a2 = a.split(',',1)
if is_mmgen_id(a1) or is_btc_addr(a1):
btc_addr = mmaddr2baddr(c,a1,ad_w,ad_f) if is_mmgen_id(a1) else BTCAddr(a1)
tx.add_output(btc_addr,BTCAmt(a2))
else:
die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
elif is_mmgen_id(a) or is_btc_addr(a):
if tx.get_chg_output_idx() != None:
die(2,'ERROR: More than one change address listed on command line')
btc_addr = mmaddr2baddr(c,a,ad_w,ad_f) if is_mmgen_id(a) else BTCAddr(a)
tx.add_output(btc_addr,BTCAmt('0'),is_chg=True)
else:
die(2,'%s: unrecognized argument' % a)
if not tx.outputs:
die(2,'At least one output must be specified on the command line')
if tx.get_chg_output_idx() == None:
die(2,('ERROR: No change output specified',wmsg['no_change_output'])[len(tx.outputs) == 1])
tx.add_mmaddrs_to_outputs(ad_w,ad_f)
if not segwit_is_active() and tx.has_segwit_outputs():
fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain'
rdie(2,fs.format(g.proj_name))
def get_inputs_from_user(tw,tx,caller):
while True:
m = 'Enter a range or space-separated list of outputs to spend: '
sel_nums = select_unspent(tw.unspent,m)
msg('Selected output%s: %s' % (suf(sel_nums,'s'),' '.join(str(i) for i in sel_nums)))
sel_unspent = tw.MMGenTwOutputList([tw.unspent[i-1] for i in sel_nums])
t_inputs = sum(s.amt for s in sel_unspent)
if t_inputs < tx.send_amt:
msg(wmsg['not_enough_btc'] % (tx.send_amt - t_inputs))
continue
non_mmaddrs = [i for i in sel_unspent if i.twmmid.type == 'non-mmgen']
if non_mmaddrs and caller != 'txdo':
msg(wmsg['non_mmgen_inputs'] % ', '.join(set(sorted([a.addr.hl() for a in non_mmaddrs]))))
if not keypress_confirm('Accept?'):
continue
tx.copy_inputs_from_tw(sel_unspent) # makes tx.inputs
change_amt = tx.sum_inputs() - tx.send_amt - get_fee_from_estimate_or_user(tx)
if change_amt >= 0:
p = 'Transaction produces %s BTC in change' % change_amt.hl()
if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
if opt.yes: msg(p)
return change_amt
else:
msg(wmsg['not_enough_btc'] % abs(change_amt))
def txcreate(cmd_args,do_info=False,caller='txcreate'):
tx = MMGenTX()
@ -149,92 +224,29 @@ def txcreate(opt,cmd_args,do_info=False,caller='txcreate'):
c = bitcoin_connection()
if not do_info:
from mmgen.addr import AddrList,AddrData
addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext]
cmd_args = set(cmd_args) - set(addrfiles)
ad_f = AddrData()
for a in addrfiles:
check_infile(a)
ad_f.add(AddrList(a))
ad_w = AddrData(source='tw')
for a in cmd_args:
if ',' in a:
a1,a2 = a.split(',',1)
if is_mmgen_id(a1) or is_btc_addr(a1):
btc_addr = mmaddr2baddr(c,a1,ad_w,ad_f) if is_mmgen_id(a1) else BTCAddr(a1)
tx.add_output(btc_addr,BTCAmt(a2))
else:
die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
elif is_mmgen_id(a) or is_btc_addr(a):
if tx.get_chg_output_idx() != None:
die(2,'ERROR: More than one change address listed on command line')
btc_addr = mmaddr2baddr(c,a,ad_w,ad_f) if is_mmgen_id(a) else BTCAddr(a)
tx.add_output(btc_addr,BTCAmt('0'),is_chg=True)
else:
die(2,'%s: unrecognized argument' % a)
if not tx.outputs:
die(2,'At least one output must be specified on the command line')
if tx.get_chg_output_idx() == None:
die(2,('ERROR: No change output specified',wmsg['no_change_output'])[len(tx.outputs) == 1])
if not do_info: get_outputs_from_cmdline(cmd_args,tx)
tw = MMGenTrackingWallet(minconf=opt.minconf)
tw.view_and_sort()
tw.view_and_sort(tx)
tw.display_total()
if do_info: sys.exit()
if do_info: sys.exit(0)
tx.send_amt = tx.sum_outputs()
msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt.hl())[bool(tx.send_amt)])
while True:
sel_nums = select_unspent(tw.unspent,
'Enter a range or space-separated list of outputs to spend: ')
msg('Selected output%s: %s' % (
('s','')[len(sel_nums)==1],
' '.join(str(i) for i in sel_nums)
))
change_amt = get_inputs_from_user(tw,tx,caller)
sel_unspent = [tw.unspent[i-1] for i in sel_nums]
t_inputs = sum(s.amt for s in sel_unspent)
if t_inputs < tx.send_amt:
msg(wmsg['not_enough_btc'] % (tx.send_amt - t_inputs))
continue
non_mmaddrs = [i for i in sel_unspent if i.mmid == None]
if non_mmaddrs and caller != 'txdo':
msg(wmsg['non_mmgen_inputs'] % ', '.join(set(sorted([a.addr.hl() for a in non_mmaddrs]))))
if not keypress_confirm('Accept?'):
continue
tx.copy_inputs_from_tw(sel_unspent) # makes tx.inputs
if opt.rbf: tx.signal_for_rbf() # only after we have inputs
change_amt = tx.sum_inputs() - tx.send_amt - get_fee_from_estimate_or_usr(tx,c)
if change_amt >= 0:
p = 'Transaction produces %s BTC in change' % change_amt.hl()
if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
if opt.yes: msg(p)
break
else:
msg(wmsg['not_enough_btc'] % abs(change_amt))
if opt.rbf: tx.signal_for_rbf() # only after we have inputs
chg_idx = tx.get_chg_output_idx()
if change_amt > 0:
tx.update_output_amt(chg_idx,BTCAmt(change_amt))
else:
if change_amt == 0:
msg('Warning: Change address will be deleted as transaction produces no change')
tx.del_output(chg_idx)
else:
tx.update_output_amt(chg_idx,BTCAmt(change_amt))
if not tx.send_amt:
tx.send_amt = change_amt
@ -244,11 +256,12 @@ def txcreate(opt,cmd_args,do_info=False,caller='txcreate'):
if not opt.yes:
tx.add_comment() # edits an existing comment
tx.create_raw(c) # creates tx.hex, tx.txid
tx.add_mmaddrs_to_outputs(ad_w,ad_f)
tx.add_timestamp()
tx.add_blockcount(c)
tx.chain = g.chain
assert tx.get_fee() <= g.max_tx_fee
assert tx.sum_inputs() - tx.sum_outputs() <= g.max_tx_fee
qmsg('Transaction successfully created')

View file

@ -68,34 +68,37 @@ ERROR: a key file must be supplied for the following non-{pnm} address%s:\n %
saved_seeds = {}
def get_seed_for_seed_id(seed_id,infiles,saved_seeds):
def get_seed_for_seed_id(sid,infiles,saved_seeds):
if seed_id in saved_seeds:
return saved_seeds[seed_id]
if sid in saved_seeds:
return saved_seeds[sid]
while True:
if infiles:
ss = SeedSource(infiles.pop(0),ignore_in_fmt=True)
elif opt.in_fmt:
qmsg('Need seed data for Seed ID %s' % seed_id)
qmsg('Need seed data for Seed ID %s' % sid)
ss = SeedSource()
msg('User input produced Seed ID %s' % ss.seed.sid)
else:
die(2,'ERROR: No seed source found for Seed ID: %s' % seed_id)
die(2,'ERROR: No seed source found for Seed ID: %s' % sid)
saved_seeds[ss.seed.sid] = ss.seed
if ss.seed.sid == seed_id: return ss.seed
if ss.seed.sid == sid: return ss.seed
def generate_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds):
seed_ids = set([i[:8] for i in mmgen_addrs])
vmsg('Need seed%s: %s' % (suf(seed_ids,'k'),' '.join(seed_ids)))
d = []
sids = set(i.sid for i in mmgen_addrs)
vmsg('Need seed%s: %s' % (suf(sids,'s'),' '.join(sids)))
d = AddrListList()
from mmgen.addr import KeyAddrList
for seed_id in seed_ids:
for sid in sids:
# Returns only if seed is found
seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds)
addr_idxs = AddrIdxList(idx_list=[int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id])
d += KeyAddrList(seed=seed,addr_idxs=addr_idxs,do_chksum=False).flat_list()
seed = get_seed_for_seed_id(sid,infiles,saved_seeds)
for t in MMGenAddrType.mmtypes:
idx_list = [i.idx for i in mmgen_addrs if i.sid == sid and i.mmtype == t]
if idx_list:
addr_idxs = AddrIdxList(idx_list=idx_list)
d += KeyAddrList(seed=seed,addr_idxs=addr_idxs,do_chksum=False,mmtype=MMGenAddrType(t)).flat_list()
return d
def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
@ -113,54 +116,57 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
if f.addr == e.addr:
e.have_wif = True
if src == 'inputs':
new_keys.append(f.wif)
new_keys.append((f.addr,f.wif))
else:
die(3,wmsg['mapping_error'] % (m1,f.mmid,f.addr,'tx file:',e.mmid,e.addr))
if new_keys:
vmsg('Added %s wif key%s from %s' % (len(new_keys),suf(new_keys,'k'),desc))
vmsg('Added %s wif key%s from %s' % (len(new_keys),suf(new_keys,'s'),desc))
return new_keys
def get_tx_files(opt,args): # strips found args
def is_tx(i): return get_extension(i) == MMGenTX.raw_ext
ret = [args.pop(i) for i in range(len(args)-1,-1,-1) if is_tx(args[i])]
if not ret:
die(1,'You must specify a raw transaction file!')
return list(reversed(ret))
def _pop_and_return(args,cmplist): # strips found args
return list(reversed([args.pop(args.index(a)) for a in reversed(args) if get_extension(a) in cmplist]))
def get_seed_files(opt,args): # strips found args
def is_seed(i): return get_extension(i) in SeedSource.get_extensions()
ret = [args.pop(i) for i in range(len(args)-1,-1,-1) if is_seed(args[i])]
from mmgen.filename import find_file_in_dir
wf = find_file_in_dir(Wallet,g.data_dir)
if wf: ret.append(wf)
if not (ret or opt.mmgen_keys_from_file or opt.keys_from_file): # or opt.use_wallet_dat):
die(1,'You must specify a seed or key source!')
return list(reversed(ret))
def get_keyaddrlist(opt):
ret = None
if opt.mmgen_keys_from_file:
ret = KeyAddrList(opt.mmgen_keys_from_file)
def get_tx_files(opt,args):
ret = _pop_and_return(args,[MMGenTX.raw_ext])
if not ret: die(1,'You must specify a raw transaction file!')
return ret
def get_seed_files(opt,args):
# favor unencrypted seed sources first, as they don't require passwords
u,e = SeedSourceUnenc,SeedSourceEnc
ret = _pop_and_return(args,u.get_extensions())
from mmgen.filename import find_file_in_dir
wf = find_file_in_dir(Wallet,g.data_dir) # Make this the first encrypted ss in the list
if wf: ret.append(wf)
ret += _pop_and_return(args,e.get_extensions())
if not (ret or opt.mmgen_keys_from_file or opt.keys_from_file): # or opt.use_wallet_dat
die(1,'You must specify a seed or key source!')
return ret
def get_keyaddrlist(opt):
if opt.mmgen_keys_from_file:
return KeyAddrList(opt.mmgen_keys_from_file)
return None
def get_keylist(opt):
ret = None
if opt.keys_from_file:
l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True)
ret = KeyAddrList(keylist=[m.split()[0] for m in l]) # accept bitcoind wallet dumps
ret.generate_addrs()
return ret
ret.generate_addrs_from_keylist()
return ret
return None
def txsign(opt,c,tx,seed_files,kl,kal,tx_num_str=''):
# Start
keys = []
# tx.pmsg()
non_mm_addrs = tx.get_non_mmaddrs('inputs')
if non_mm_addrs:
tmp = KeyAddrList(addrlist=non_mm_addrs,do_chksum=False)
tmp.add_wifs(kl)
m = tmp.list_missing('wif')
if m: die(2,wmsg['missing_keys_error'] % (suf(m,'es'),'\n '.join(m)))
keys += tmp.get_wifs()
keys += tmp.get_addr_wif_pairs()
if opt.mmgen_keys_from_file:
keys += add_keys(tx,'inputs',keyaddr_list=kal)
@ -174,10 +180,9 @@ def txsign(opt,c,tx,seed_files,kl,kal,tx_num_str=''):
extra_sids = set(saved_seeds) - tx.get_input_sids() - tx.get_output_sids()
if extra_sids:
msg('Unused Seed ID%s: %s' %
(suf(extra_sids,'k'),' '.join(extra_sids)))
msg('Unused Seed ID{}: {}'.format(suf(extra_sids,'s'),' '.join(extra_sids)))
if tx.sign(c,tx_num_str,keys):
if tx.sign(c,tx_num_str,dict(keys)):
return tx
else:
die(3,'failed\nSome keys were missing. Transaction %scould not be signed.' % tx_num_str)
die(3,'failed\nSome keys were missing. Transaction {}could not be signed.'.format(tx_num_str))

View file

@ -35,7 +35,7 @@ def msgred(s): msg(red(s))
def mmsg(*args):
for d in args: Msg(repr(d))
def mdie(*args):
mmsg(*args); sys.exit()
mmsg(*args); sys.exit(0)
def die_wait(delay,ev=0,s=''):
assert type(delay) == int
@ -57,11 +57,18 @@ def Die(ev=0,s=''):
if s: Msg(s)
sys.exit(ev)
def pp_format(d):
def rdie(ev=0,s=''): die(ev,red(s))
def ydie(ev=0,s=''): die(ev,yellow(s))
def pformat(d):
import pprint
return pprint.PrettyPrinter(indent=4).pformat(d)
def pp_die(d): die(1,pp_format(d))
def pp_msg(d): msg(pp_format(d))
def pmsg(*args):
if not args: return
Msg(pformat(args if len(args) > 1 else args[0]))
def pdie(*args):
if not args: sys.exit(1)
Die(1,(pformat(args if len(args) > 1 else args[0])))
def set_for_type(val,refval,desc,invert_bool=False,src=None):
src_str = (''," in '{}'".format(src))[bool(src)]
@ -132,17 +139,16 @@ def dmsg(s):
if opt.debug: msg(s)
def suf(arg,suf_type):
suf_types = { 's': ('s',''), 'es': ('es','') }
assert suf_type in suf_types
t = type(arg)
if t == int:
n = arg
elif t in (list,tuple,set,dict):
elif any(issubclass(t,c) for c in (list,tuple,set,dict)):
n = len(arg)
else:
msg('%s: invalid parameter' % arg)
return ''
if suf_type in ('a','es'): return ('es','')[n == 1]
if suf_type in ('k','s'): return ('s','')[n == 1]
die(2,'%s: invalid parameter for suf()' % arg)
return suf_types[suf_type][n==1]
def get_extension(f):
a,b = os.path.splitext(f)
@ -159,9 +165,12 @@ def make_chksum_N(s,nchars,sep=False):
return sep.join([s[i*4:i*4+4] for i in range(nchars/4)])
def make_chksum_8(s,sep=False):
s = sha256(sha256(s).digest()).hexdigest()[:8].upper()
from mmgen.obj import HexStr
s = HexStr(sha256(sha256(s).digest()).hexdigest()[:8].upper(),case='upper')
return '{} {}'.format(s[:4],s[4:]) if sep else s
def make_chksum_6(s): return sha256(s).hexdigest()[:6]
def make_chksum_6(s):
from mmgen.obj import HexStr
return HexStr(sha256(s).hexdigest()[:6])
def is_chksum_6(s): return len(s) == 6 and is_hex_str_lc(s)
def make_iv_chksum(s): return sha256(s).hexdigest()[:8].upper()
@ -700,12 +709,14 @@ def do_pager(text):
pagers,shell = ['less','more'],False
# --- Non-MSYS Windows code deleted ---
# raw, chop, scroll right 1 char, disable buggy line chopping for Windows
os.environ['LESS'] = (('-RS -#1'),('-cR -#1'))[g.platform=='win']
# raw, chop, horiz scroll 8 chars, disable buggy line chopping in MSYS
os.environ['LESS'] = (('--shift 8 -RS'),('-cR -#1'))[g.platform=='win']
if 'PAGER' in os.environ and os.environ['PAGER'] != pagers[0]:
pagers = [os.environ['PAGER']] + pagers
text = text.encode('utf8')
for pager in pagers:
end = ('\n(end of text)\n','')[pager=='less']
try:
@ -771,5 +782,19 @@ def bitcoin_connection():
g.rpc_password or cfg['rpcpassword'],
auth_cookie=get_bitcoind_auth_cookie())
# do an RPC call so we exit immediately if we can't connect
c.client_version = int(c.getinfo()['version'])
if not g.bitcoind_version:
g.bitcoind_version = int(c.getnetworkinfo()['version'])
g.chain = c.getblockchaininfo()['chain']
if g.chain != 'regtest': g.chain += 'net'
assert g.chain in g.chains
err = None
if g.regtest and g.chain != 'regtest':
err = '--regtest option'
elif g.testnet and g.chain == 'mainnet':
err = '--testnet option'
# we won't actually get here, as connect will fail first
elif (not g.testnet) and g.chain != 'mainnet':
err = 'mainnet'
if err:
die(1,'{} selected but chain is {}'.format(err,g.chain))
return c

20
scripts/traceback.py Executable file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env python
import sys,traceback,os
sys.path.insert(0,'.')
if 'TMUX' in os.environ: del os.environ['TMUX']
f = open('my.err','w')
try:
sys.argv.pop(0)
execfile(sys.argv[0])
except SystemExit:
sys.exit(int(str(sys.exc_info()[1])))
except:
l = traceback.format_exception(*sys.exc_info())
exc = l.pop()
def red(s): return '{e}[31;1m{}{e}[0m'.format(s,e='\033')
def yellow(s): return '{e}[33;1m{}{e}[0m'.format(s,e='\033')
sys.stdout.write('{}{} {}'.format(yellow(''.join(l)),red(exc),str(e[1])))
traceback.print_exc(file=f)

View file

@ -29,7 +29,7 @@ from binascii import hexlify
# Import these _after_ local path's been added to sys.path
from mmgen.common import *
from mmgen.bitcoin import hex2wif,privnum2addr
from mmgen.bitcoin import hex2wif
rounds = 100
opts_data = {
@ -39,6 +39,7 @@ opts_data = {
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-q, --quiet Produce quieter output
-s, --segwit Generate Segwit (P2SH-P2WPKH) addresses
-v, --verbose Produce more verbose output
""",
'notes': """
@ -48,15 +49,14 @@ opts_data = {
Compare: {prog} a <dump file> (compare output of a key generator against wallet dump)
where a and b are one of:
'1' - native Python ecdsa library (very slow)
'2' - 'keyconv' utility from the 'vanitygen' package (old default)
'3' - bitcoincore.org's secp256k1 library (default from v0.8.6)
'2' - bitcoincore.org's secp256k1 library (default from v0.8.6)
EXAMPLES:
{prog} 2:3 1000
(compare output of 'keyconv' with secp256k1 library, 1000 rounds)
{prog} 3 1000
{prog} 1:2 100
(compare output of native Python ECDSA with secp256k1 library, 100 rounds)
{prog} 2 1000
(test speed of secp256k1 library address generation, 1000 rounds)
{prog} 3 my.dump
{prog} 2 my.dump
(compare addrs generated with secp256k1 library to bitcoind wallet dump)
""".format(prog='gentest.py',pnm=g.proj_name,snum=rounds)
}
@ -105,7 +105,7 @@ else:
die(1,"%s: invalid generator IDs" % cmd_args[0])
def match_error(sec,wif,a_addr,b_addr,a,b):
m = ['','py-ecdsa','keyconv','secp256k1','dump']
m = ['','py-ecdsa','secp256k1','dump']
qmsg_r(red('\nERROR: Addresses do not match!'))
die(3,"""
sec key : {}
@ -114,13 +114,16 @@ def match_error(sec,wif,a_addr,b_addr,a,b):
{b:10}: {}
""".format(sec,wif,a_addr,b_addr,pnm=g.proj_name,a=m[a],b=m[b]).rstrip())
# Begin execution
mmtype = ('L','S')[bool(opt.segwit)]
compressed = True
if a and b:
m = "Comparing address generators '{}' and '{}'"
qmsg(green(m.format(g.key_generators[a-1],g.key_generators[b-1])))
from mmgen.addr import get_privhex2addr_f
gen_a = get_privhex2addr_f(generator=a)
gen_b = get_privhex2addr_f(generator=b)
compressed = False
last_t = time.time()
for i in range(rounds):
@ -129,12 +132,12 @@ if a and b:
last_t = time.time()
sec = hexlify(os.urandom(32))
wif = hex2wif(sec,compressed=compressed)
a_addr = gen_a(sec,compressed)
b_addr = gen_b(sec,compressed)
a_addr = gen_a(sec,compressed,mmtype=mmtype)
b_addr = gen_b(sec,compressed,mmtype=mmtype)
vmsg('\nkey: %s\naddr: %s\n' % (wif,a_addr))
if a_addr != b_addr:
match_error(sec,wif,a_addr,b_addr,a,b)
if a != 2 and b != 2:
if not opt.segwit:
compressed = not compressed
qmsg_r('\rRound %s/%s ' % (i+1,rounds))
@ -143,12 +146,11 @@ elif a and not fh:
m = "Testing speed of address generator '{}'"
qmsg(green(m.format(g.key_generators[a-1])))
from mmgen.addr import get_privhex2addr_f
gen_a = get_privhex2addr_f(generator=a)
gen = get_privhex2addr_f(generator=a)
from struct import pack,unpack
seed = os.urandom(28)
print 'Incrementing key with each round'
print 'Starting key:', hexlify(seed+pack('I',0))
compressed = False
import time
start = last_t = time.time()
@ -158,9 +160,9 @@ elif a and not fh:
last_t = time.time()
sec = hexlify(seed+pack('I',i))
wif = hex2wif(sec,compressed=compressed)
a_addr = gen_a(sec,compressed)
a_addr = gen(sec,compressed,mmtype=mmtype)
vmsg('\nkey: %s\naddr: %s\n' % (wif,a_addr))
if a != 2:
if not opt.segwit:
compressed = not compressed
qmsg_r('\rRound %s/%s ' % (i+1,rounds))
@ -179,7 +181,7 @@ elif a and dump:
if sec == False:
die(2,'\nInvalid {}net WIF address in dump file: {}'.format(('main','test')[g.testnet],wif))
compressed = wif[0] != ('5','9')[g.testnet]
b_addr = gen_a(sec,compressed)
b_addr = gen_a(sec,compressed,'L')
if a_addr != b_addr:
match_error(sec,wif,a_addr,b_addr,1 if compressed and a==2 else a,4)
qmsg(green(('\n','')[bool(opt.verbose)] + 'OK'))

View file

@ -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 bitcoind wallet label upon import.
# The label may contain any printable ASCII symbol.
# Address data checksum for 98831F3A-S[1,31-33,500-501,1010-1011]: 06C1 9C87 F25C 4EE6
# Record this value to a secure location.
98831F3A SEGWIT {
1 36TvVzU5mxSjJ3D9qKAmYzCV7iUqtTDezF
31 33BhCyZJeXt2EUitVQUCKSN8hutAL3jnDh
32 37GSqHBi7yKUYN1y5WGwcVeKgxAkhNTJBs
33 3CWwH3GZrWh2BfbVjaMChHnuamSc4uxLxK
500 35QxzdbJTErNtxYaQJQvfhv3qiG876bhHA
501 3P1r25Ch3rp7CUTw9uoBxoZsDQ9smGQ8iZ
1010 37h2StmReijH9j3kd1AVMU1vfXe5HwMowX
1011 37UkuGYko84PeTE46qXLDWKKvGhMM82tpu
}

View file

@ -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 bitcoind wallet label upon import.
# The label may contain any printable ASCII symbol.
# Address data checksum for 98831F3A-S[1,31-33,500-501,1010-1011]: 58D1 7B6C E9F9 9C14
# Record this value to a secure location.
98831F3A SEGWIT {
1 2Mx28ZjQ7PQx5VpqhWSneAwBkL4h1dsq6cY
31 2MtjuGiVLFzPNSGMSAY64wPMPvG6L8zYx4e
32 2Mxpeu27jjRppk9eWkdtpESdauJNvQnxHw6
33 2N459LnCbTyCNPTE3Qhy5KEnAo7emrkhMpq
500 2MvyB4NXL4hMj6kB85S2oHeuK44UHtYTjtF
501 2NEa45p8ifKKTQG6Uq3R4akZ8RkN3arccQY
1010 2MyFEWdhTGBEdMWgJJ8nMyR1BssrF8oR48c
1011 2My2xy1UnQaZjrErbmy9CqTJb8cuXDzyKiH
}

View file

@ -20,25 +20,8 @@
test/test.py: Test suite for the MMGen suite
"""
import sys,os
def run_in_tb():
fn = sys.argv[0]
source = open(fn)
try:
exec source in {'inside_tb':1}
except SystemExit:
pass
except:
def color(s): return '\033[36;1m' + s + '\033[0m'
e = sys.exc_info()
sys.stdout.write(color('\nTest script returned: %s\n' % (e[0].__name__)))
if not 'inside_tb' in globals() and 'MMGEN_TEST_TRACEBACK' in os.environ:
run_in_tb()
sys.exit()
pn = os.path.dirname(sys.argv[0])
os.chdir(os.path.join(pn,os.pardir))
sys.path.__setitem__(0,os.path.abspath(os.curdir))
@ -138,6 +121,8 @@ opts_data = {
-D, --direct-exec Bypass pexpect and execute a command directly (for
debugging only)
-e, --exact-output Show the exact output of the MMGen script(s) being run
-g, --segwit Generate and use Segwit addresses
-G, --segwit-random Generate and use a random mix of Segwit and Legacy addrs
-l, --list-cmds List and describe the commands in the test suite
-L, --log Log commands to file {lf}
-n, --names Display command names instead of descriptions
@ -167,6 +152,11 @@ cmd_args = opts.init(opts_data)
tn_desc = ('','.testnet')[g.testnet]
def randbool():
return hexlify(os.urandom(1))[1] in '12345678'
def get_segwit_val():
return randbool() if opt.segwit_random else True if opt.segwit else False
cfgs = {
'15': {
'tmpdir': os.path.join('test','tmp15'),
@ -181,6 +171,7 @@ cfgs = {
'mmseed': 'export_seed_dfl_wallet',
'del_dw_run': 'delete_dfl_wallet',
},
'segwit': get_segwit_val()
},
'16': {
'tmpdir': os.path.join('test','tmp16'),
@ -189,6 +180,7 @@ cfgs = {
'dep_generators': {
pwfile: 'passchg_dfl_wallet',
},
'segwit': get_segwit_val()
},
'1': {
'tmpdir': os.path.join('test','tmp1'),
@ -211,6 +203,7 @@ cfgs = {
incog_id_fn: 'export_incog_hidden',
'akeys.mmenc': 'keyaddrgen'
},
'segwit': get_segwit_val()
},
'2': {
'tmpdir': os.path.join('test','tmp2'),
@ -224,6 +217,7 @@ cfgs = {
'sigtx': 'txsign2',
'mmwords': 'export_mnemonic2',
},
'segwit': get_segwit_val()
},
'3': {
'tmpdir': os.path.join('test','tmp3'),
@ -235,6 +229,7 @@ cfgs = {
'rawtx': 'txcreate3',
'sigtx': 'txsign3'
},
'segwit': get_segwit_val()
},
'4': {
'tmpdir': os.path.join('test','tmp4'),
@ -251,6 +246,7 @@ cfgs = {
},
'bw_filename': 'brainwallet.mmbrain',
'bw_params': '192,1',
'segwit': get_segwit_val()
},
'14': {
'kapasswd': 'Maxwell',
@ -263,6 +259,7 @@ cfgs = {
'addrs': 'addrgen14',
'akeys.mmenc': 'keyaddrgen14',
},
'segwit': get_segwit_val()
},
'5': {
'tmpdir': os.path.join('test','tmp5'),
@ -272,14 +269,15 @@ cfgs = {
'mmdat': 'passchg',
pwfile: 'passchg',
},
'segwit': get_segwit_val()
},
'6': {
'name': 'reference wallet check (128-bit)',
'seed_len': 128,
'seed_id': 'FE3C6545',
'ref_bw_seed_id': '33F10310',
'addrfile_chk': ('B230 7526 638F 38CB','B64D 7327 EF2A 60FE')[g.testnet],
'keyaddrfile_chk': ('CF83 32FB 8A8B 08E2','FEBF 7878 97BB CC35')[g.testnet],
'addrfile_chk': ('B230 7526 638F 38CB','B64D 7327 EF2A 60FE','9914 6D10 2307 F348','7DBF 441F E188 8B37'),
'keyaddrfile_chk':('CF83 32FB 8A8B 08E2','FEBF 7878 97BB CC35','C13B F717 D4E8 CF59','4DB5 BAF0 45B7 6E81'),
'passfile_chk': 'EB29 DC4F 924B 289F',
'passfile32_chk': '37B6 C218 2ABC 7508',
'wpasswd': 'reference password',
@ -300,15 +298,15 @@ cfgs = {
'addrs': 'refaddrgen1',
'akeys.mmenc': 'refkeyaddrgen1'
},
'segwit': get_segwit_val()
},
'7': {
'name': 'reference wallet check (192-bit)',
'seed_len': 192,
'seed_id': '1378FC64',
'ref_bw_seed_id': 'CE918388',
'addrfile_chk': ('8C17 A5FA 0470 6E89','0A59 C8CD 9439 8B81')[g.testnet],
'keyaddrfile_chk': ('9648 5132 B98E 3AD9','2F72 C83F 44C5 0FAC')[g.testnet],
'addrfile_chk': ('8C17 A5FA 0470 6E89','0A59 C8CD 9439 8B81','91C4 0414 89E4 2089','3BA6 7494 8E2B 858D'),
'keyaddrfile_chk': ('9648 5132 B98E 3AD9','2F72 C83F 44C5 0FAC','C98B DF08 A3D5 204B','25F2 AEB6 AAAC 8BBE'),
'passfile_chk': 'ADEA 0083 094D 489A',
'passfile32_chk': '2A28 C5C7 36EC 217A',
'wpasswd': 'reference password',
@ -329,23 +327,25 @@ cfgs = {
'addrs': 'refaddrgen2',
'akeys.mmenc': 'refkeyaddrgen2'
},
'segwit': get_segwit_val()
},
'8': {
'name': 'reference wallet check (256-bit)',
'seed_len': 256,
'seed_id': '98831F3A',
'ref_bw_seed_id': 'B48CD7FC',
'addrfile_chk': ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E')[g.testnet],
'keyaddrfile_chk': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2')[g.testnet],
'addrfile_chk': ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E','06C1 9C87 F25C 4EE6','58D1 7B6C E9F9 9C14'),
'keyaddrfile_chk': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2','A447 12C2 DD14 5A9B','0690 460D A600 D315'),
'passfile_chk': '2D6D 8FBA 422E 1315',
'passfile32_chk': 'F6C1 CDFB 97D9 FCAE',
'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'.format(tn_desc),
'ref_segwitaddrfile':'98831F3A-S[1,31-33,500-501,1010-1011]{}.addrs'.format(tn_desc),
'ref_keyaddrfile': '98831F3A[1,31-33,500-501,1010-1011]{}.akeys.mmenc'.format(tn_desc),
'ref_passwdfile': '98831F3A-фубар@crypto.org-b58-20[1,4,9-11,1100].pws',
'ref_addrfile_chksum': ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E')[g.testnet],
'ref_segwitaddrfile_chksum':('06C1 9C87 F25C 4EE6','58D1 7B6C E9F9 9C14')[g.testnet],
'ref_keyaddrfile_chksum': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2')[g.testnet],
'ref_passwdfile_chksum': 'A983 DAB9 5514 27FB',
# 'ref_fake_unspent_data':'98831F3A_unspent.json',
@ -367,6 +367,7 @@ cfgs = {
'addrs': 'refaddrgen3',
'akeys.mmenc': 'refkeyaddrgen3'
},
'segwit': get_segwit_val()
},
'9': {
'tmpdir': os.path.join('test','tmp9'),
@ -489,6 +490,7 @@ cmd_group['ref'] = (
# misc. saved reference data
cmd_group['ref_other'] = (
('ref_addrfile_chk', 'saved reference address file'),
('ref_segwitaddrfile_chk','saved reference address file (segwit)'),
('ref_keyaddrfile_chk','saved reference key-address file'),
('ref_passwdfile_chk', 'saved reference password file'),
# Create the fake inputs:
@ -609,7 +611,7 @@ del cmd_group
add_spawn_args = ' '.join(['{} {}'.format(
'--'+k.replace('_','-'),
getattr(opt,k) if getattr(opt,k) != True else ''
) for k in 'testnet','rpc_host' if getattr(opt,k)]).split()
) for k in 'testnet','rpc_host','rpc_port','regtest' if getattr(opt,k)]).split()
add_spawn_args += ['--data-dir',data_dir]
if opt.profile: opt.names = True
@ -631,6 +633,8 @@ os.environ['MMGEN_NO_LICENSE'] = '1'
os.environ['MMGEN_MIN_URANDCHARS'] = '3'
os.environ['MMGEN_BOGUS_SEND'] = '1'
def get_segwit_arg(cfg): return ([],['--type','segwit'])[cfg['segwit']]
# Tell spawned programs they're running in the test suite
os.environ['MMGEN_TEST_SUITE'] = '1'
@ -692,7 +696,7 @@ if opt.list_cmds:
w = max([len(i) for i in utils])
for cmd in sorted(utils):
Msg(fs.format(cmd,utils[cmd],w=w))
sys.exit()
sys.exit(0)
import time,re
if g.platform == 'linux':
@ -718,7 +722,7 @@ else: # Windows
m3 = '.\nControl values should be checked against the program output.\nContinue?'
if not keypress_confirm(green(m1)+grnbg(m2)+green(m3),default_yes=True):
errmsg('Exiting at user request')
sys.exit()
sys.exit(0)
def my_send(p,t,delay=send_delay,s=False):
if delay: time.sleep(delay)
@ -770,8 +774,10 @@ def get_file_with_ext(ext,mydir,delete=True,no_dot=False):
if len(flist) > 1:
if delete:
if not opt.quiet:
msg("Multiple *.%s files in '%s' - deleting" % (ext,mydir))
for f in flist: os.unlink(f)
msg("Multiple *.{} files in '{}' - deleting".format(ext,mydir))
for f in flist:
msg(f)
os.unlink(f)
return False
else:
return flist[0]
@ -845,6 +851,7 @@ class MMGenExpect(object):
else:
if opt.traceback:
cmd,args = tb_cmd,[cmd]+args
cmd_str = tb_cmd + ' ' + cmd_str
if use_popen_spawn:
self.p = PopenSpawn(cmd_str)
else:
@ -854,7 +861,7 @@ class MMGenExpect(object):
def ok(self,exit_val=0):
ret = self.p.wait()
if ret != exit_val:
die(1,red('Program exited with value {}'.format(ret)))
die(1,red('test.py: spawned program exited with value {}'.format(ret)))
if opt.profile: return
if opt.verbose or opt.exact_output:
sys.stderr.write(green('OK\n'))
@ -863,7 +870,7 @@ class MMGenExpect(object):
def cmp_or_die(self,s,t,skip_ok=False,exit_val=0):
ret = self.p.wait()
if ret != exit_val:
die(1,red('Program exited with value {}'.format(ret)))
die(1,red('test.py: spawned program exited with value {}'.format(ret)))
if s == t:
if not skip_ok: ok()
else:
@ -977,16 +984,17 @@ class MMGenExpect(object):
from mmgen.obj import BTCAmt
from mmgen.bitcoin import verify_addr
def create_fake_unspent_entry(address,sid=None,idx=None,lbl=None,non_mmgen=None):
def create_fake_unspent_entry(btcaddr,al_id=None,idx=None,lbl=None,non_mmgen=False,segwit=False):
if lbl: lbl = ' ' + lbl
spk1,spk2 = (('76a914','88ac'),('a914','87'))[segwit and btcaddr.addr_fmt=='p2sh']
return {
'account': (non_mmgen or ('%s:%s%s' % (sid,idx,lbl))).decode('utf8'),
'account': 'btc:{}'.format(btcaddr) if non_mmgen else (u'{}:{}{}'.format(al_id,idx,lbl.decode('utf8'))),
'vout': int(getrandnum(4) % 8),
'txid': hexlify(os.urandom(32)).decode('utf8'),
'amount': BTCAmt('%s.%s' % (10+(getrandnum(4) % 40), getrandnum(4) % 100000000)),
'address': address,
'address': btcaddr,
'spendable': False,
'scriptPubKey': ('76a914'+verify_addr(address,return_hex=True)+'88ac'),
'scriptPubKey': (spk1+verify_addr(btcaddr,return_hex=True)+spk2),
'confirmations': getrandnum(4) % 50000
}
@ -1013,35 +1021,82 @@ labels = [
"Carl's capital",
]
label_iter = None
def create_fake_unspent_data(adata,unspent_data_file,tx_data,non_mmgen_input=''):
def create_fake_unspent_data(adata,tx_data,non_mmgen_input=''):
out = []
for s in tx_data:
sid = tx_data[s]['sid']
a = adata.addrlist(sid)
for n,(idx,btcaddr) in enumerate(a.addrpairs(),1):
for d in tx_data.values():
al = adata.addrlist(d['al_id'])
for n,(idx,btcaddr) in enumerate(al.addrpairs()):
while True:
try: lbl = next(label_iter)
except: label_iter = iter(labels)
else: break
out.append(create_fake_unspent_entry(btcaddr,sid,idx,lbl))
if n == 1: # create a duplicate address. This means addrs_per_wallet += 1
out.append(create_fake_unspent_entry(btcaddr,sid,idx,lbl))
out.append(create_fake_unspent_entry(btcaddr,d['al_id'],idx,lbl,segwit=d['segwit']))
if n == 0: # create a duplicate address. This means addrs_per_wallet += 1
out.append(create_fake_unspent_entry(btcaddr,d['al_id'],idx,lbl,segwit=d['segwit']))
if non_mmgen_input:
from mmgen.bitcoin import privnum2addr,hex2wif
privnum = getrandnum(32)
btcaddr = privnum2addr(privnum,compressed=True)
from mmgen.bitcoin import privnum2addr,hex2wif
from mmgen.obj import BTCAddr
btcaddr = BTCAddr(privnum2addr(privnum,compressed=True))
of = os.path.join(cfgs[non_mmgen_input]['tmpdir'],non_mmgen_fn)
wif = hex2wif('{:064x}'.format(privnum),compressed=True)
# Msg(yellow(wif + ' ' + btcaddr))
write_data_to_file(of,wif+'\n','compressed bitcoin key',silent=True)
out.append(create_fake_unspent_entry(btcaddr,non_mmgen=True,segwit=False))
out.append(create_fake_unspent_entry(btcaddr,non_mmgen='Non-MMGen address'))
# msg('\n'.join([repr(o) for o in out])); sys.exit(0)
return out
# msg('\n'.join([repr(o) for o in out])); sys.exit()
write_data_to_file(unspent_data_file,repr(out),'Unspent outputs',silent=True)
def write_fake_data_to_file(d):
unspent_data_file = os.path.join(cfg['tmpdir'],'unspent.json')
write_data_to_file(unspent_data_file,d,'Unspent outputs',silent=True)
os.environ['MMGEN_BOGUS_WALLET_DATA'] = unspent_data_file
bwd_msg = 'MMGEN_BOGUS_WALLET_DATA=%s' % unspent_data_file
if opt.print_cmdline: msg(bwd_msg)
if opt.log: log_fd.write(bwd_msg + ' ')
if opt.verbose or opt.exact_output:
sys.stderr.write("Fake transaction wallet data written to file '%s'\n" % unspent_data_file)
def create_tx_data(sources):
from mmgen.addr import AddrList,AddrData,AddrIdxList
tx_data,ad = {},AddrData()
for s in sources:
afile = get_file_with_ext('addrs',cfgs[s]['tmpdir'])
al = AddrList(afile)
ad.add(al)
aix = AddrIdxList(fmt_str=cfgs[s]['addr_idx_list'])
if len(aix) != addrs_per_wallet:
errmsg(red('Address index list length != %s: %s' %
(addrs_per_wallet,repr(aix))))
sys.exit(0)
tx_data[s] = {
'addrfile': afile,
'chk': al.chksum,
'al_id': al.al_id,
'addr_idxs': aix[-2:],
'segwit': cfgs[s]['segwit']
}
return ad,tx_data
def make_txcreate_cmdline(tx_data):
from mmgen.bitcoin import privnum2addr
btcaddr = privnum2addr(getrandnum(32),compressed=True)
cmd_args = ['-d',cfg['tmpdir']]
for num in tx_data:
s = tx_data[num]
cmd_args += [
'{}:{},{}'.format(s['al_id'],s['addr_idxs'][0],cfgs[num]['amts'][0]),
]
# + one change address and one BTC address
if num is tx_data.keys()[-1]:
cmd_args += ['{}:{}'.format(s['al_id'],s['addr_idxs'][1])]
cmd_args += ['{},{}'.format(btcaddr,cfgs[num]['amts'][1])]
return cmd_args + [tx_data[num]['addrfile'] for num in tx_data]
def add_comments_to_addr_file(addrfile,outfile):
silence()
@ -1073,7 +1128,7 @@ def do_between():
if opt.verbose or opt.exact_output: sys.stderr.write('\n')
else:
errmsg('Exiting at user request')
sys.exit()
sys.exit(0)
elif opt.verbose or opt.exact_output:
sys.stderr.write('\n')
@ -1370,7 +1425,7 @@ class MMGenTestSuite(object):
def addrgen(self,name,wf,pf=None,check_ref=False,ftype='addr',id_str=None,extra_args=[]):
ftype,chkfile = ((ftype,'{}file_chk'.format(ftype)),('pass','passfile32_chk'))[ftype=='pass32']
add_args = extra_args + ([],['-q'] + ([],['-P',pf])[bool(pf)])[ia]
add_args = extra_args + ([],['-q'] + ([],['-P',pf])[bool(pf)])[ia] + (get_segwit_arg(cfg),[])[ftype[:4]=='pass']
dlist = [id_str] if id_str else []
t = MMGenExpect(name,'mmgen-{}gen'.format(ftype), add_args +
['-d',cfg['tmpdir']] + ([],[wf])[bool(wf)] + dlist + [cfg['{}_idx_list'.format(ftype)]])
@ -1381,7 +1436,8 @@ class MMGenTestSuite(object):
desc = ('address','password')[ftype=='pass']
chk = t.expect_getend(r'Checksum for {} data .*?: '.format(desc),regex=True)
if check_ref:
refcheck('address data checksum',chk,cfg[chkfile])
c = (cfg[chkfile][g.testnet + 2*cfg['segwit']],cfg[chkfile])[ftype=='pass']
refcheck('address data checksum',chk,c)
return
t.written_to_file('Addresses',oo=True)
t.ok()
@ -1400,7 +1456,6 @@ class MMGenTestSuite(object):
t = MMGenExpect(name,'mmgen-addrimport', add_args + [outfile])
if ia: return
t.expect_getend(r'Checksum for address data .*\[.*\]: ',regex=True)
t.expect_getend('Validating addresses...OK. ')
t.expect("Type uppercase 'YES' to confirm: ",'\n')
vmsg('This is a simulation, so no addresses were actually imported into the tracking\nwallet')
t.ok(exit_val=1)
@ -1408,50 +1463,14 @@ class MMGenTestSuite(object):
def txcreate_common(self,name,sources=['1'],non_mmgen_input='',do_label=False,txdo_args=[],add_args=[]):
if opt.verbose or opt.exact_output:
sys.stderr.write(green('Generating fake tracking wallet info\n'))
silence()
from mmgen.addr import AddrList,AddrData,AddrIdxList
tx_data,ad = {},AddrData()
for s in sources:
afile = get_file_with_ext('addrs',cfgs[s]['tmpdir'])
ai = AddrList(afile)
ad.add(ai)
aix = AddrIdxList(fmt_str=cfgs[s]['addr_idx_list'])
if len(aix) != addrs_per_wallet:
errmsg(red('Address index list length != %s: %s' %
(addrs_per_wallet,repr(aix))))
sys.exit()
tx_data[s] = {
'addrfile': afile,
'chk': ai.chksum,
'sid': ai.seed_id,
'addr_idxs': aix[-2:],
}
unspent_data_file = os.path.join(cfg['tmpdir'],'unspent.json')
create_fake_unspent_data(ad,unspent_data_file,tx_data,non_mmgen_input)
if opt.verbose or opt.exact_output:
sys.stderr.write("Fake transaction wallet data written to file '%s'\n" % unspent_data_file)
# make the command line
from mmgen.bitcoin import privnum2addr
btcaddr = privnum2addr(getrandnum(32),compressed=True)
cmd_args = ['-d',cfg['tmpdir']]
for num in tx_data:
s = tx_data[num]
cmd_args += [
'%s:%s,%s' % (s['sid'],s['addr_idxs'][0],cfgs[num]['amts'][0]),
]
# + one BTC address
# + one change address and one BTC address
if num is tx_data.keys()[-1]:
cmd_args += ['%s:%s' % (s['sid'],s['addr_idxs'][1])]
cmd_args += ['%s,%s' % (btcaddr,cfgs[num]['amts'][1])]
for num in tx_data: cmd_args += [tx_data[num]['addrfile']]
os.environ['MMGEN_BOGUS_WALLET_DATA'] = unspent_data_file
ad,tx_data = create_tx_data(sources)
dfake = create_fake_unspent_data(ad,tx_data,non_mmgen_input)
write_fake_data_to_file(repr(dfake))
cmd_args = make_txcreate_cmdline(tx_data)
end_silence()
if opt.verbose or opt.exact_output: sys.stderr.write('\n')
if ia:
@ -1459,9 +1478,6 @@ class MMGenTestSuite(object):
m = '\nAnswer the interactive prompts as follows:\n' + \
" 'y', 'y', 'q', '1-9'<ENTER>, ENTER, ENTER, ENTER, ENTER, 'y'"
msg(grnbg(m))
bwd_msg = 'MMGEN_BOGUS_WALLET_DATA=%s' % unspent_data_file
if opt.print_cmdline: msg(bwd_msg)
if opt.log: log_fd.write(bwd_msg + ' ')
t = MMGenExpect(name,'mmgen-'+('txcreate','txdo')[bool(txdo_args)],['--rbf','-f',tx_fee] + add_args + cmd_args + txdo_args)
if ia: return
t.license()
@ -1516,7 +1532,7 @@ class MMGenTestSuite(object):
t.hash_preset('key-address data','1')
t.passphrase('key-address data',cfgs['14']['kapasswd'])
t.expect('Check key-to-address validity? (y/N): ','y')
t.expect('Which output do you wish to deduct the fee from? ','1\n')
t.expect('deduct the fee from (Hit ENTER for the change output): ','1\n')
# Fee must be > tx_fee + network relay fee (currently 0.00001)
t.expect('OK? (Y/n): ','\n')
t.expect('Enter transaction fee: ','124s\n')
@ -1567,13 +1583,12 @@ class MMGenTestSuite(object):
if txdo_handle: return
if save:
self.txsign_end(t,has_label=has_label)
exit_val = 0
t.ok()
else:
cprompt = ('Add a comment to transaction','Edit transaction comment')[has_label]
t.expect('%s? (y/N): ' % cprompt,'\n')
t.expect('Save signed transaction? (Y/n): ','n')
exit_val = 1
t.ok(exit_val=exit_val)
t.ok(exit_val=1)
def txsign_dfl_wallet(self,name,txfile,pf='',save=True,has_label=False):
return self.txsign(name,txfile,wf=None,pf=pf,save=save,has_label=has_label)
@ -1586,7 +1601,7 @@ class MMGenTestSuite(object):
t.license()
t.tx_view()
t.expect('Add a comment to transaction? (y/N): ','\n')
t.expect('broadcast this transaction to the network?')
t.expect('Are you sure you want to broadcast this')
m = 'YES, I REALLY WANT TO DO THIS'
t.expect("'%s' to confirm: " % m,m+'\n')
t.expect('BOGUS transaction NOT sent')
@ -1655,8 +1670,8 @@ class MMGenTestSuite(object):
def addrgen_seed(self,name,wf,foo,desc='seed data',in_fmt='seed'):
stdout = (False,True)[desc=='seed data'] #capture output to screen once
add_arg = ([],['-S'])[bool(stdout)]
t = MMGenExpect(name,'mmgen-addrgen', add_arg +
add_args = ([],['-S'])[bool(stdout)] + get_segwit_arg(cfg)
t = MMGenExpect(name,'mmgen-addrgen', add_args +
['-i'+in_fmt,'-d',cfg['tmpdir'],wf,cfg['addr_idx_list']])
t.license()
t.expect_getend('Valid %s for Seed ID ' % desc)
@ -1677,7 +1692,7 @@ class MMGenTestSuite(object):
self.addrgen_seed(name,wf,foo,desc='mnemonic data',in_fmt='words')
def addrgen_incog(self,name,wf=[],foo='',in_fmt='i',desc='incognito data',args=[]):
t = MMGenExpect(name,'mmgen-addrgen', args+['-i'+in_fmt,'-d',cfg['tmpdir']]+
t = MMGenExpect(name,'mmgen-addrgen', args + get_segwit_arg(cfg) + ['-i'+in_fmt,'-d',cfg['tmpdir']]+
([],[wf])[bool(wf)] + [cfg['addr_idx_list']])
t.license()
t.expect_getend('Incog Wallet ID: ')
@ -1698,7 +1713,7 @@ class MMGenTestSuite(object):
args=['-H','%s,%s'%(rf,hincog_offset),'-l',str(hincog_seedlen)])
def keyaddrgen(self,name,wf,pf=None,check_ref=False):
args = ['-d',cfg['tmpdir'],usr_rand_arg,wf,cfg['addr_idx_list']]
args = get_segwit_arg(cfg) + ['-d',cfg['tmpdir'],usr_rand_arg,wf,cfg['addr_idx_list']]
if ia:
m = "\nAnswer 'n' at the interactive prompt"
msg(grnbg(m))
@ -1709,7 +1724,7 @@ class MMGenTestSuite(object):
t.passphrase('MMGen wallet',cfg['wpasswd'])
chk = t.expect_getend(r'Checksum for key-address data .*?: ',regex=True)
if check_ref:
refcheck('key-address data checksum',chk,cfg['keyaddrfile_chk'])
refcheck('key-address data checksum',chk,cfg['keyaddrfile_chk'][g.testnet + 2*cfg['segwit']])
return
t.expect('Encrypt key list? (y/N): ','y')
t.usr_rand(usr_rand_chars)
@ -2025,7 +2040,7 @@ class MMGenTestSuite(object):
aa = ['-P',get_tmpfile_fn(cfg,pfn)]
else:
aa = []
t = MMGenExpect(name,'mmgen-tool',aa+[ftype+'file_chksum',wf])
t = MMGenExpect(name,'mmgen-tool',aa+[ftype.replace('segwit','')+'file_chksum',wf])
if ia:
k = 'ref_%saddrfile_chksum' % ('','key')[ftype == 'keyaddr']
m = grnbg('Checksum should be:')
@ -2046,6 +2061,9 @@ class MMGenTestSuite(object):
def ref_passwdfile_chk(self,name):
self.ref_addrfile_chk(name,ftype='passwd')
def ref_segwitaddrfile_chk(self,name):
self.ref_addrfile_chk(name,ftype='segwitaddr')
# def txcreate8(self,name,addrfile):
# self.txcreate_common(name,sources=['8'])
@ -2253,7 +2271,7 @@ try:
for arg in cmd_args:
if arg in utils:
globals()[arg](cmd_args[cmd_args.index(arg)+1:])
sys.exit()
sys.exit(0)
elif 'info_'+arg in cmd_data:
dirs = cmd_data['info_'+arg][1]
if dirs: clean(dirs)
@ -2276,7 +2294,11 @@ try:
if cmd is not cmd_data.keys()[-1]: do_between()
except KeyboardInterrupt:
die(1,'\nExiting at user request')
raise
except opt.traceback and Exception:
with open('my.err') as f:
t = f.readlines()
if t: msg_r('\n'+yellow(''.join(t[:-1]))+red(t[-1]))
die(1,blue('Test script exited with error'))
except:
sys.stderr = stderr_save
raise

View file

@ -20,7 +20,7 @@
test/tooltest.py: Tests for the 'mmgen-tool' utility
"""
import sys,os
import sys,os,subprocess
pn = os.path.dirname(sys.argv[0])
os.chdir(os.path.join(pn,os.pardir))
sys.path.__setitem__(0,os.path.abspath(os.curdir))
@ -33,52 +33,60 @@ cmd_data = OrderedDict([
('util', {
'desc': 'base conversion, hashing and file utilities',
'cmd_data': OrderedDict([
('strtob58', ()),
('b58tostr', ('strtob58','io')),
('hextob58', ()),
('b58tohex', ('hextob58','io')),
('b58randenc', ()),
('hextob32', ()),
('b32tohex', ('hextob32','io')),
('randhex', ()),
('id8', ()),
('id6', ()),
('str2id6', ()),
('sha256x2', ()),
('hexreverse', ()),
('hexlify', ()),
('hexdump', ()),
('unhexdump', ('hexdump','io')),
('rand2file', ()),
('Strtob58', ()),
('B58tostr', ('Strtob58','io')),
('Hextob58', ()),
('B58tohex', ('Hextob58','io')),
('B58randenc', ()),
('Hextob32', ()),
('B32tohex', ('Hextob32','io')),
('Randhex', ()),
('Id6', ()),
('Id8', ()),
('Str2id6', ()),
('Hash160', ()),
('Hash256', ()),
('Hexreverse', ()),
('Hexlify', ()),
('Hexdump', ()),
('Unhexdump', ('Hexdump','io')),
('Rand2file', ()),
])
}
),
('bitcoin', {
'desc': 'Bitcoin address/key commands',
'cmd_data': OrderedDict([
('randwif', ()),
('randpair', ()),
('wif2addr', ('randpair','o2')),
('wif2hex', ('randpair','o2')),
('privhex2addr', ('wif2hex','o2')), # wif from randpair o2
('hex2wif', ('wif2hex','io2')),
('addr2hexaddr', ('randpair','o2')),
('hexaddr2addr', ('addr2hexaddr','io2')),
# ('pubkey2addr', ['<public key in hex format> [str]']),
# ('pubkey2hexaddr', ['<public key in hex format> [str]']),
('Randwif', ()),
('Randpair', ()), # create 3 pairs: uncomp,comp,segwit
('Wif2addr', ('Randpair','o3')),
('Wif2hex', ('Randpair','o3')),
('Privhex2pubhex', ('Wif2hex','o3')),
('Pubhex2addr', ('Privhex2pubhex','o3')),
('Pubhex2redeem_script', ('Privhex2pubhex','o3')),
('Wif2redeem_script', ('Randpair','o3')),
('Wif2segwit_pair', ('Randpair','o2')),
('Privhex2addr', ('Wif2hex','o3')), # compare with output of Randpair
('Hex2wif', ('Wif2hex','io2')),
('Addr2hexaddr', ('Randpair','o2')),
('Hexaddr2addr', ('Addr2hexaddr','io2')),
('Pipetest', ('Randpair','o3')),
])
}
),
('mnemonic', {
'desc': 'mnemonic commands',
'cmd_data': OrderedDict([
('hex2mn', ()),
('mn2hex', ('hex2mn','io3')),
('mn_rand128', ()),
('mn_rand192', ()),
('mn_rand256', ()),
('mn_stats', ()),
('mn_printlist', ()),
('Hex2mn', ()),
('Mn2hex', ('Hex2mn','io3')),
('Mn_rand128', ()),
('Mn_rand192', ()),
('Mn_rand256', ()),
('Mn_stats', ()),
('Mn_printlist', ()),
])
}
),
@ -86,11 +94,11 @@ cmd_data = OrderedDict([
'desc': 'Bitcoind RPC commands',
'cmd_data': OrderedDict([
# ('keyaddrfile_chksum', ()), # interactive
('addrfile_chksum', ()),
('getbalance', ()),
('listaddresses', ()),
('twview', ()),
('txview', ()),
('Addrfile_chksum', ()),
('Getbalance', ()),
('Listaddresses', ()),
('Twview', ()),
('Txview', ()),
])
}
),
@ -113,7 +121,8 @@ opts_data = {
'options': """
-h, --help Print this help message
--, --longhelp Print help message for long options (common options)
-l, --list-cmds List and describe the tests and commands in the test suite
-l, --list-cmds List and describe the tests and commands in this test suite
-L, --list-names List the names of all tested 'mmgen-tool' commands
-s, --system Test scripts and modules installed on system rather than
those in the repo root
-v, --verbose Produce more verbose output
@ -127,10 +136,11 @@ If no command is given, the whole suite of tests is run.
sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
cmd_args = opts.init(opts_data,add_opts=['exact_output','profile'])
spawn_cmd = ['python',os.path.join(os.curdir,'mmgen-tool') if not opt.system else 'mmgen-tool']
add_spawn_args = ' '.join(['{} {}'.format(
'--'+k.replace('_','-'),
getattr(opt,k) if getattr(opt,k) != True else ''
) for k in 'testnet','rpc_host' if getattr(opt,k)]).split()
) for k in 'testnet','rpc_host','regtest' if getattr(opt,k)]).split()
add_spawn_args += [ '--data-dir', cfg['tmpdir']] # ignore ~/.mmgen
if opt.system: sys.path.pop(0)
@ -143,12 +153,29 @@ if opt.list_cmds:
Msg(fs.format(cmd,cmd_data[cmd]['desc'],w=w))
Msg('\nAvailable utilities:')
Msg(fs.format('clean','Clean the tmp directory',w=w))
sys.exit()
sys.exit(0)
if opt.list_names:
acc = []
for v in cmd_data.values():
acc += v['cmd_data'].keys()
tc = sorted(c.lower() for c in acc)
msg('{}\n{}'.format(green('Tested commands:'),'\n'.join(tc)))
import mmgen.tool
tested_in_tool = ('Encrypt','Decrypt','Find_incog_data','Keyaddrfile_chksum','Passwdfile_chksum')
ignore = ('Help','Usage')
uc = sorted(c.lower() for c in set(mmgen.tool.cmd_data.keys()) - set(acc) - set(ignore) - set(tested_in_tool))
msg('\n{}\n{}'.format(yellow('Untested commands:'),'\n'.join(uc)))
die()
import binascii
from mmgen.test import *
from mmgen.tx import is_wif,is_btc_addr
msg_w = 35
def test_msg(m):
m2 = 'Testing {}'.format(m)
msg_r(green(m2+'\n') if opt.verbose else '{:{w}}'.format(m2,w=msg_w+8))
class MMGenToolTestSuite(object):
def __init__(self):
@ -179,35 +206,28 @@ class MMGenToolTestSuite(object):
for cmd in cdata: self.do_cmd(cmd,cdata[cmd])
def do_cmd(self,cmd,cdata):
fns = self.gen_deps_for_cmd(cmd,cdata)
file_list = [os.path.join(cfg['tmpdir'],fn) for fn in fns]
self.__class__.__dict__[cmd](*([self,cmd] + file_list))
def run_cmd(self,name,tool_args,kwargs='',extra_msg='',silent=False,strip=True):
mmgen_tool = 'mmgen-tool'
if not opt.system:
mmgen_tool = os.path.join(os.curdir,mmgen_tool)
sys_cmd = ['python',mmgen_tool] + add_spawn_args + ['-r0','-d',cfg['tmpdir'],name] + tool_args + kwargs.split()
if extra_msg: extra_msg = '(%s)' % extra_msg
full_name = ' '.join([name]+kwargs.split()+extra_msg.split())
sys_cmd = (
spawn_cmd +
add_spawn_args +
['-r0','-d',cfg['tmpdir'],name.lower()] +
tool_args +
kwargs.split()
)
if extra_msg: extra_msg = '({})'.format(extra_msg)
full_name = ' '.join([name.lower()]+kwargs.split()+extra_msg.split())
if not silent:
if opt.verbose:
sys.stderr.write(green('Testing %s\nExecuting ' % full_name))
sys.stderr.write('%s\n' % cyan(repr(sys_cmd)))
sys.stderr.write(green('Testing {}\nExecuting '.format(full_name)))
sys.stderr.write(cyan(' '.join(sys_cmd)+'\n'))
else:
msg_r('Testing %-31s%s' % (full_name+':',''))
msg_r('Testing {:{w}}'.format(full_name+':',w=msg_w))
import subprocess
p = subprocess.Popen(
sys_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
p = subprocess.Popen(sys_cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
a,b = p.communicate()
retcode = p.wait()
if retcode != 0:
@ -234,18 +254,19 @@ class MMGenToolTestSuite(object):
vmsg('Out: ' + repr(ret))
return ret
def run_cmd_out(self,name,carg=None,Return=False,kwargs='',fn_idx='',extra_msg='',literal=False,chkdata=''):
def run_cmd_out(self,name,carg=None,Return=False,kwargs='',fn_idx='',extra_msg='',literal=False,chkdata='',hush=False):
if carg: write_to_tmpfile(cfg,'%s%s.in' % (name,fn_idx),carg+'\n')
ret = self.run_cmd(name,([],[carg])[bool(carg)],kwargs=kwargs,extra_msg=extra_msg)
if carg: vmsg('In: ' + repr(carg))
vmsg('Out: ' + (repr(ret),ret)[literal])
vmsg('Out: ' + (repr(ret),ret.decode('utf8'))[literal])
if ret or ret == '':
write_to_tmpfile(cfg,'%s%s.out' % (name,fn_idx),ret+'\n')
if chkdata:
cmp_or_die(ret,chkdata)
return
if Return: return ret
else: ok()
else:
if not hush: ok()
else:
die(3,red("Error for command '%s'" % name))
@ -259,115 +280,141 @@ class MMGenToolTestSuite(object):
ok()
vmsg('Returned: %s' % ret)
def str2id6(self,name):
# Util
def Strtob58(self,name): self.run_cmd_out(name,getrandstr(16))
def B58tostr(self,name,f1,f2): self.run_cmd_chk(name,f1,f2)
def Hextob58(self,name): self.run_cmd_out(name,getrandhex(32))
def B58tohex(self,name,f1,f2): self.run_cmd_chk(name,f1,f2)
def B58randenc(self,name):
ret = self.run_cmd_out(name,Return=True)
ok_or_die(ret,is_b58_str,'base 58 string')
def Hextob32(self,name): self.run_cmd_out(name,getrandhex(24))
def B32tohex(self,name,f1,f2): self.run_cmd_chk(name,f1,f2)
def Randhex(self,name):
ret = self.run_cmd_out(name,Return=True)
ok_or_die(ret,binascii.unhexlify,'hex string')
def Id6(self,name): self.run_cmd_randinput(name)
def Id8(self,name): self.run_cmd_randinput(name)
def Str2id6(self,name):
s = getrandstr(120,no_space=True)
s2 = ' %s %s %s %s %s ' % (s[:3],s[3:9],s[9:29],s[29:50],s[50:120])
ret1 = self.run_cmd(name,[s],extra_msg='unspaced input'); ok()
ret2 = self.run_cmd(name,[s2],extra_msg='spaced input')
cmp_or_die(ret1,ret2)
vmsg('Returned: %s' % ret1)
def mn_rand128(self,name):
self.run_cmd_out(name)
def mn_rand192(self,name):
self.run_cmd_out(name)
def mn_rand256(self,name):
self.run_cmd_out(name)
def mn_stats(self,name):
self.run_cmd_out(name)
def mn_printlist(self,name):
self.run_cmd(name,[])
ok()
def id6(self,name): self.run_cmd_randinput(name)
def id8(self,name): self.run_cmd_randinput(name)
def hexdump(self,name): self.run_cmd_randinput(name,strip=False)
def unhexdump(self,name,fn1,fn2):
def Hash160(self,name): self.run_cmd_out(name,getrandhex(16))
def Hash256(self,name): self.run_cmd_out(name,getrandstr(16))
def Hexreverse(self,name): self.run_cmd_out(name,getrandhex(24))
def Hexlify(self,name): self.run_cmd_out(name,getrandstr(24))
def Hexdump(self,name): self.run_cmd_randinput(name,strip=False)
def Unhexdump(self,name,fn1,fn2):
ret = self.run_cmd(name,[fn2],strip=False)
orig = read_from_file(fn1,binary=True)
cmp_or_die(orig,ret)
def rand2file(self,name):
def Rand2file(self,name):
of = name + '.out'
dlen = 1024
self.run_cmd(name,[of,str(1024),'threads=4','silent=1'],strip=False)
d = read_from_tmpfile(cfg,of,binary=True)
cmp_or_die(dlen,len(d))
def strtob58(self,name): self.run_cmd_out(name,getrandstr(16))
def sha256x2(self,name): self.run_cmd_out(name,getrandstr(16))
def hexreverse(self,name): self.run_cmd_out(name,getrandhex(24))
def hexlify(self,name): self.run_cmd_out(name,getrandstr(24))
def b58tostr(self,name,f1,f2): self.run_cmd_chk(name,f1,f2)
def hextob58(self,name): self.run_cmd_out(name,getrandhex(32))
def b58tohex(self,name,f1,f2): self.run_cmd_chk(name,f1,f2)
def hextob32(self,name): self.run_cmd_out(name,getrandhex(24))
def b32tohex(self,name,f1,f2): self.run_cmd_chk(name,f1,f2)
def b58randenc(self,name):
ret = self.run_cmd_out(name,Return=True)
ok_or_die(ret,is_b58_str,'base 58 string')
def randhex(self,name):
ret = self.run_cmd_out(name,Return=True)
ok_or_die(ret,binascii.unhexlify,'hex string')
def randwif(self,name):
# Bitcoin
def Randwif(self,name):
for n,k in enumerate(['','compressed=1']):
ret = self.run_cmd_out(name,kwargs=k,Return=True,fn_idx=n+1)
ok_or_die(ret,is_wif,'WIF key')
def randpair(self,name):
for n,k in enumerate(['','compressed=1']):
def Randpair(self,name):
for n,k in enumerate(['','compressed=1','segwit=1 compressed=1']):
wif,addr = self.run_cmd_out(name,kwargs=k,Return=True,fn_idx=n+1).split()
ok_or_die(wif,is_wif,'WIF key',skip_ok=True)
ok_or_die(addr,is_btc_addr,'Bitcoin address')
def hex2wif(self,name,f1,f2,f3,f4):
def Wif2addr(self,name,f1,f2,f3):
for n,f,k,m in (1,f1,'',''),(2,f2,'','compressed'),(3,f3,'segwit=1','compressed'):
wif = read_from_file(f).split()[0]
self.run_cmd_out(name,wif,kwargs=k,fn_idx=n,extra_msg=m)
def Wif2hex(self,name,f1,f2,f3):
for n,f,m in (1,f1,''),(2,f2,'compressed'),(3,f3,'compressed for segwit'):
wif = read_from_file(f).split()[0]
self.run_cmd_out(name,wif,fn_idx=n,extra_msg=m)
def Privhex2addr(self,name,f1,f2,f3):
keys = [read_from_file(f).rstrip() for f in f1,f2,f3]
for n,k in enumerate(('','compressed=1','compressed=1 segwit=1')):
ret = self.run_cmd(name,[keys[n]],kwargs=k).rstrip()
iaddr = read_from_tmpfile(cfg,'Randpair{}.out'.format(n+1)).split()[-1]
cmp_or_die(iaddr,ret)
def Hex2wif(self,name,f1,f2,f3,f4):
for n,fi,fo,k in (1,f1,f2,''),(2,f3,f4,'compressed=1'):
ret = self.run_cmd_chk(name,fi,fo,kwargs=k)
def wif2hex(self,name,f1,f2):
for n,f,k in (1,f1,''),(2,f2,'compressed=1'):
wif = read_from_file(f).split()[0]
self.run_cmd_out(name,wif,kwargs=k,fn_idx=n)
def wif2addr(self,name,f1,f2):
for n,f,k in (1,f1,''),(2,f2,'compressed=1'):
wif = read_from_file(f).split()[0]
self.run_cmd_out(name,wif,kwargs=k,fn_idx=n)
def addr2hexaddr(self,name,f1,f2):
def Addr2hexaddr(self,name,f1,f2):
for n,f,m in (1,f1,''),(2,f2,'from compressed'):
addr = read_from_file(f).split()[-1]
self.run_cmd_out(name,addr,fn_idx=n,extra_msg=m)
def hexaddr2addr(self,name,f1,f2,f3,f4):
def Hexaddr2addr(self,name,f1,f2,f3,f4):
for n,fi,fo,m in (1,f1,f2,''),(2,f3,f4,'from compressed'):
self.run_cmd_chk(name,fi,fo,extra_msg=m)
def privhex2addr(self,name,f1,f2):
key1 = read_from_file(f1).rstrip()
key2 = read_from_file(f2).rstrip()
for n,args in enumerate([[key1],[key2,'compressed=1']]):
ret = self.run_cmd(name,args).rstrip()
iaddr = read_from_tmpfile(cfg,'randpair%s.out' % (n+1)).split()[-1]
cmp_or_die(iaddr,ret)
def hex2mn(self,name):
def Privhex2pubhex(self,name,f1,f2,f3): # from hex2wif
addr = read_from_file(f3).strip()
self.run_cmd_out(name,addr,kwargs='compressed=1',fn_idx=3)
def Pubhex2redeem_script(self,name,f1,f2,f3): # from above
addr = read_from_file(f3).strip()
self.run_cmd_out(name,addr,fn_idx=3)
rs = read_from_tmpfile(cfg,name+'3.out').strip()
self.run_cmd_out('pubhex2addr',rs,kwargs='p2sh=1',fn_idx=3,hush=True)
addr1 = read_from_tmpfile(cfg,'pubhex2addr3.out').strip()
addr2 = read_from_tmpfile(cfg,'Randpair3.out').split()[1]
cmp_or_die(addr1,addr2)
def Wif2redeem_script(self,name,f1,f2,f3): # compare output with above
wif = read_from_file(f3).split()[0]
ret1 = self.run_cmd_out(name,wif,fn_idx=3,Return=True)
ret2 = read_from_tmpfile(cfg,'Pubhex2redeem_script3.out').strip()
cmp_or_die(ret1,ret2)
def Wif2segwit_pair(self,name,f1,f2): # does its own checking, so just run
wif = read_from_file(f2).split()[0]
self.run_cmd_out(name,wif,fn_idx=2)
def Pubhex2addr(self,name,f1,f2,f3):
addr = read_from_file(f3).strip()
self.run_cmd_out(name,addr,fn_idx=3)
def Pipetest(self,name,f1,f2,f3):
test_msg('command piping')
wif = read_from_file(f3).split()[0]
cmd = '{tc} {sa} wif2hex {wif} | {tc} privhex2pubhex - compressed=1 | {tc} pubhex2redeem_script - | {tc} {sa} pubhex2addr - p2sh=1'.format(wif=wif,sa=' '.join(add_spawn_args),tc=' '.join(spawn_cmd))
p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,shell=True)
res = p.stdout.read().strip()
addr = read_from_tmpfile(cfg,'Wif2addr3.out').strip()
cmp_or_die(res,addr)
# Mnemonic
def Hex2mn(self,name):
for n,size,m in(1,16,'128-bit'),(2,24,'192-bit'),(3,32,'256-bit'):
hexnum = getrandhex(size)
self.run_cmd_out(name,hexnum,fn_idx=n,extra_msg=m)
def mn2hex(self,name,f1,f2,f3,f4,f5,f6):
def Mn2hex(self,name,f1,f2,f3,f4,f5,f6):
for f_i,f_o,m in (f1,f2,'128-bit'),(f3,f4,'192-bit'),(f5,f6,'256-bit'):
self.run_cmd_chk(name,f_i,f_o,extra_msg=m)
def Mn_rand128(self,name): self.run_cmd_out(name)
def Mn_rand192(self,name): self.run_cmd_out(name)
def Mn_rand256(self,name): self.run_cmd_out(name)
def Mn_stats(self,name): self.run_cmd_out(name)
def Mn_printlist(self,name):
self.run_cmd(name,[])
ok()
def getbalance(self,name):
self.run_cmd_out(name,literal=True)
def listaddresses(self,name):
self.run_cmd_out(name,literal=True)
def twview(self,name):
self.run_cmd_out(name,literal=True)
def txview(self,name):
fn = os.path.join(cfg['refdir'],cfg['txfile'])
self.run_cmd_out(name,fn,literal=True)
def addrfile_chksum(self,name):
# RPC
def Addrfile_chksum(self,name):
fn = os.path.join(cfg['refdir'],cfg['addrfile'])
self.run_cmd_out(name,fn,literal=True,chkdata=cfg['addrfile_chk'])
def Getbalance(self,name):
self.run_cmd_out(name,literal=True)
def Listaddresses(self,name):
self.run_cmd_out(name,literal=True)
def Twview(self,name):
self.run_cmd_out(name,literal=True)
def Txview(self,name):
fn = os.path.join(cfg['refdir'],cfg['txfile'])
self.run_cmd_out(name,fn,literal=True)
# main()
import time
@ -378,14 +425,13 @@ mk_tmpdir(cfg['tmpdir'])
if cmd_args:
if len(cmd_args) != 1:
die(1,'Only one command may be specified')
cmd = cmd_args[0]
if cmd in cmd_data:
msg('Running tests for %s:' % cmd_data[cmd]['desc'])
ts.do_cmds(cmd)
elif cmd == 'clean':
cleandir(cfg['tmpdir'])
sys.exit()
sys.exit(0)
else:
die(1,"'%s': unrecognized command" % cmd)
else: