whitespace: top-level modules

This commit is contained in:
The MMGen Project 2024-10-18 10:32:08 +00:00
commit b5600fc563
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
43 changed files with 1962 additions and 1982 deletions

View file

@ -22,29 +22,29 @@ addr: MMGen address-related types
from collections import namedtuple
from .objmethods import HiliteStr,InitErrors,MMGenObject
from .obj import ImmutableAttr,MMGenIdx,get_obj
from .objmethods import HiliteStr, InitErrors, MMGenObject
from .obj import ImmutableAttr, MMGenIdx, get_obj
from .seed import SeedID
from . import color as color_mod
ati = namedtuple('addrtype_info',
['name','pubkey_type','compressed','gen_method','addr_fmt','wif_label','extra_attrs','desc'])
['name', 'pubkey_type', 'compressed', 'gen_method', 'addr_fmt', 'wif_label', 'extra_attrs', 'desc'])
class MMGenAddrType(HiliteStr,InitErrors,MMGenObject):
class MMGenAddrType(HiliteStr, InitErrors, MMGenObject):
width = 1
trunc_ok = False
color = 'blue'
name = ImmutableAttr(str)
pubkey_type = ImmutableAttr(str)
compressed = ImmutableAttr(bool,set_none_ok=True)
gen_method = ImmutableAttr(str,set_none_ok=True)
addr_fmt = ImmutableAttr(str,set_none_ok=True)
wif_label = ImmutableAttr(str,set_none_ok=True)
extra_attrs = ImmutableAttr(tuple,set_none_ok=True)
compressed = ImmutableAttr(bool, set_none_ok=True)
gen_method = ImmutableAttr(str, set_none_ok=True)
addr_fmt = ImmutableAttr(str, set_none_ok=True)
wif_label = ImmutableAttr(str, set_none_ok=True)
extra_attrs = ImmutableAttr(tuple, set_none_ok=True)
desc = ImmutableAttr(str)
pkh_fmts = ('p2pkh','bech32','ethereum')
pkh_fmts = ('p2pkh', 'bech32', 'ethereum')
mmtypes = {
'L': ati('legacy', 'std', False,'p2pkh', 'p2pkh', 'wif', (), 'Legacy uncompressed address'),
'C': ati('compressed','std', True, 'p2pkh', 'p2pkh', 'wif', (), 'Compressed P2PKH address'),
@ -54,34 +54,35 @@ class MMGenAddrType(HiliteStr,InitErrors,MMGenObject):
'Z': ati('zcash_z','zcash_z',False,'zcash_z', 'zcash_z', 'wif', ('viewkey',), 'Zcash z-address'),
'M': ati('monero', 'monero', False,'monero', 'monero', 'spendkey',('viewkey','wallet_passwd'),'Monero address'),
}
def __new__(cls,proto,id_str,errmsg=None):
if isinstance(id_str,cls):
def __new__(cls, proto, id_str, errmsg=None):
if isinstance(id_str, cls):
return id_str
try:
id_str = id_str.replace('-','_')
for k,v in cls.mmtypes.items():
if id_str in (k,v.name):
id_str = id_str.replace('-', '_')
for k, v in cls.mmtypes.items():
if id_str in (k, v.name):
if id_str == v.name:
id_str = k
me = str.__new__(cls,id_str)
me = str.__new__(cls, id_str)
for k in v._fields:
setattr(me,k,getattr(v,k))
setattr(me, k, getattr(v, k))
if me not in proto.mmtypes + ('P',):
raise ValueError(f'{me.name!r}: invalid address type for {proto.name} protocol')
me.proto = proto
return me
raise ValueError(f'{id_str}: unrecognized address type for protocol {proto.name}')
except Exception as e:
return cls.init_fail( e,
return cls.init_fail(
e,
f"{errmsg or ''}{id_str!r}: invalid value for {cls.__name__} ({e!s})",
preformat = True )
preformat = True)
@classmethod
def get_names(cls):
return [v.name for v in cls.mmtypes.values()]
def is_mmgen_addrtype(proto,id_str):
return get_obj( MMGenAddrType, proto=proto, id_str=id_str, silent=True, return_bool=True )
def is_mmgen_addrtype(proto, id_str):
return get_obj(MMGenAddrType, proto=proto, id_str=id_str, silent=True, return_bool=True)
class MMGenPasswordType(MMGenAddrType):
mmtypes = {
@ -92,58 +93,58 @@ class AddrIdx(MMGenIdx):
max_digits = 7
def is_addr_idx(s):
return get_obj( AddrIdx, n=s, silent=True, return_bool=True )
return get_obj(AddrIdx, n=s, silent=True, return_bool=True)
class AddrListID(HiliteStr,InitErrors,MMGenObject):
class AddrListID(HiliteStr, InitErrors, MMGenObject):
width = 10
trunc_ok = False
color = 'yellow'
def __new__(cls,sid=None,mmtype=None,proto=None,id_str=None):
def __new__(cls, sid=None, mmtype=None, proto=None, id_str=None):
try:
if id_str:
a,b = id_str.split(':')
a, b = id_str.split(':')
sid = SeedID(sid=a)
try:
mmtype = MMGenAddrType( proto=proto, id_str=b )
mmtype = MMGenAddrType(proto=proto, id_str=b)
except:
mmtype = MMGenPasswordType( proto=proto, id_str=b )
mmtype = MMGenPasswordType(proto=proto, id_str=b)
else:
assert isinstance(sid,SeedID), f'{sid!r} not a SeedID instance'
if not isinstance(mmtype,(MMGenAddrType,MMGenPasswordType)):
assert isinstance(sid, SeedID), f'{sid!r} not a SeedID instance'
if not isinstance(mmtype, (MMGenAddrType, MMGenPasswordType)):
raise ValueError(f'{mmtype!r}: not an instance of MMGenAddrType or MMGenPasswordType')
me = str.__new__(cls,sid+':'+mmtype)
me = str.__new__(cls, sid+':'+mmtype)
me.sid = sid
me.mmtype = mmtype
return me
except Exception as e:
return cls.init_fail(e, f'sid={sid}, mmtype={mmtype}')
def is_addrlist_id(proto,s):
return get_obj( AddrListID, proto=proto, id_str=s, silent=False, return_bool=True )
def is_addrlist_id(proto, s):
return get_obj(AddrListID, proto=proto, id_str=s, silent=False, return_bool=True)
class MMGenID(HiliteStr,InitErrors,MMGenObject):
class MMGenID(HiliteStr, InitErrors, MMGenObject):
color = 'orange'
width = 0
trunc_ok = False
def __new__(cls,proto,id_str):
def __new__(cls, proto, id_str):
try:
ss = str(id_str).split(':')
assert len(ss) in (2,3),'not 2 or 3 colon-separated items'
t = proto.addr_type((ss[1],proto.dfl_mmtype)[len(ss)==2])
me = str.__new__(cls,f'{ss[0]}:{t}:{ss[-1]}')
assert len(ss) in (2, 3), 'not 2 or 3 colon-separated items'
t = proto.addr_type((ss[1], proto.dfl_mmtype)[len(ss)==2])
me = str.__new__(cls, f'{ss[0]}:{t}:{ss[-1]}')
me.sid = SeedID(sid=ss[0])
me.idx = AddrIdx(ss[-1])
me.mmtype = t
assert t in proto.mmtypes, f'{t}: invalid address type for {proto.cls_name}'
me.al_id = str.__new__(AddrListID,me.sid+':'+me.mmtype) # checks already done
me.al_id = str.__new__(AddrListID, me.sid+':'+me.mmtype) # checks already done
me.sort_key = f'{me.sid}:{me.mmtype}:{me.idx:0{me.idx.max_digits}}'
me.proto = proto
return me
except Exception as e:
return cls.init_fail(e,id_str)
return cls.init_fail(e, id_str)
def is_mmgen_id(proto,s):
return get_obj( MMGenID, proto=proto, id_str=s, silent=True, return_bool=True )
def is_mmgen_id(proto, s):
return get_obj(MMGenID, proto=proto, id_str=s, silent=True, return_bool=True)
class CoinAddr(HiliteStr, InitErrors, MMGenObject):
color = 'cyan'
@ -152,7 +153,7 @@ class CoinAddr(HiliteStr, InitErrors, MMGenObject):
trunc_ok = False
def __new__(cls, proto, addr):
if isinstance(addr,cls):
if isinstance(addr, cls):
return addr
try:
ap = proto.decode_addr(addr)
@ -171,18 +172,18 @@ class CoinAddr(HiliteStr, InitErrors, MMGenObject):
me.proto = proto
return me
except Exception as e:
return cls.init_fail(e,addr,objname=f'{proto.cls_name} address')
return cls.init_fail(e, addr, objname=f'{proto.cls_name} address')
@property
def parsed(self):
if not hasattr(self,'_parsed'):
self._parsed = self.proto.parse_addr(self.ver_bytes,self.bytes,self.addr_fmt)
if not hasattr(self, '_parsed'):
self._parsed = self.proto.parse_addr(self.ver_bytes, self.bytes, self.addr_fmt)
return self._parsed
# reimplement some HiliteStr methods:
@classmethod
def fmtc(cls,s,width,color=False):
return super().fmtc( s=s[:width-2]+'..' if len(s) > width else s, width=width, color=color )
def fmtc(cls, s, width, color=False):
return super().fmtc(s=s[:width-2]+'..' if len(s) > width else s, width=width, color=color)
def fmt(self, view_pref, width, color=False):
s = self.views[view_pref]
@ -191,11 +192,11 @@ class CoinAddr(HiliteStr, InitErrors, MMGenObject):
def hl(self, view_pref, color=True):
return getattr(color_mod, self.color)(self.views[view_pref]) if color else self.views[view_pref]
def is_coin_addr(proto,s):
return get_obj( CoinAddr, proto=proto, addr=s, silent=True, return_bool=True )
def is_coin_addr(proto, s):
return get_obj(CoinAddr, proto=proto, addr=s, silent=True, return_bool=True)
class TokenAddr(CoinAddr):
color = 'blue'
def ViewKey(proto,viewkey_str):
def ViewKey(proto, viewkey_str):
return proto.viewkey(viewkey_str)

View file

@ -21,15 +21,15 @@ addrdata: MMGen AddrData and related classes
"""
from .cfg import gc
from .util import fmt,die
from .util import fmt, die
from .base_obj import AsyncInit
from .obj import MMGenObject,MMGenDict,get_obj
from .addr import MMGenID,AddrListID
from .addrlist import AddrListEntry,AddrListData,AddrList
from .obj import MMGenObject, MMGenDict, get_obj
from .addr import MMGenID, AddrListID
from .addrlist import AddrListEntry, AddrListData, AddrList
class AddrData(MMGenObject):
def __init__(self,proto,*args,**kwargs):
def __init__(self, proto, *args, **kwargs):
self.al_ids = {}
self.proto = proto
self.rpc = None
@ -37,63 +37,63 @@ class AddrData(MMGenObject):
def seed_ids(self):
return list(self.al_ids.keys())
def addrlist(self,al_id):
def addrlist(self, al_id):
# TODO: Validate al_id
if al_id in self.al_ids:
return self.al_ids[al_id]
def mmaddr2coinaddr(self,mmaddr):
al_id,idx = MMGenID(self.proto,mmaddr).rsplit(':',1)
def mmaddr2coinaddr(self, mmaddr):
al_id, idx = MMGenID(self.proto, mmaddr).rsplit(':', 1)
coinaddr = ''
if al_id in self.al_ids:
coinaddr = self.addrlist(al_id).coinaddr(int(idx))
return coinaddr or None
def coinaddr2mmaddr(self,coinaddr):
def coinaddr2mmaddr(self, coinaddr):
d = self.make_reverse_dict([coinaddr])
return (list(d.values())[0][0]) if d else None
def add(self,addrlist):
if isinstance(addrlist,AddrList):
def add(self, addrlist):
if isinstance(addrlist, AddrList):
self.al_ids[addrlist.al_id] = addrlist
return True
else:
raise TypeError(f'Error: object {addrlist!r} is not an instance of AddrList')
def make_reverse_dict(self,coinaddrs):
def make_reverse_dict(self, coinaddrs):
d = MMGenDict()
for al_id in self.al_ids:
d.update(self.al_ids[al_id].make_reverse_dict_addrlist(coinaddrs))
return d
class TwAddrData(AddrData,metaclass=AsyncInit):
class TwAddrData(AddrData, metaclass=AsyncInit):
def __new__(cls,cfg,proto,*args,**kwargs):
return MMGenObject.__new__(proto.base_proto_subclass(cls,'addrdata'))
def __new__(cls, cfg, proto, *args, **kwargs):
return MMGenObject.__new__(proto.base_proto_subclass(cls, 'addrdata'))
async def __init__(self,cfg,proto,twctl=None):
async def __init__(self, cfg, proto, twctl=None):
from .rpc import rpc_init
from .tw.shared import TwLabel
from .seed import SeedID
self.cfg = cfg
self.proto = proto
self.rpc = await rpc_init(cfg,proto)
self.rpc = await rpc_init(cfg, proto)
self.al_ids = {}
twd = await self.get_tw_data(twctl)
out,i = {},0
for acct,addr_array in twd:
l = get_obj(TwLabel,proto=self.proto,text=acct,silent=True)
out, i = {}, 0
for acct, addr_array in twd:
l = get_obj(TwLabel, proto=self.proto, text=acct, silent=True)
if l and l.mmid.type == 'mmgen':
obj = l.mmid.obj
if len(addr_array) != 1:
message = self.msgs['multiple_acct_addrs'].strip().format( acct=acct, proj=gc.proj_name )
die(3, fmt( message, indent=' ' ))
message = self.msgs['multiple_acct_addrs'].strip().format(acct=acct, proj=gc.proj_name)
die(3, fmt(message, indent=' '))
al_id = AddrListID(
sid = SeedID(sid=obj.sid),
mmtype = self.proto.addr_type(obj.mmtype) )
mmtype = self.proto.addr_type(obj.mmtype))
if al_id not in out:
out[al_id] = []
out[al_id].append(AddrListEntry(self.proto,idx=obj.idx,addr=addr_array[0],comment=l.comment))
out[al_id].append(AddrListEntry(self.proto, idx=obj.idx, addr=addr_array[0], comment=l.comment))
i += 1
self.cfg._util.vmsg(f'{i} {gc.proj_name} addresses found, {len(twd)} accounts total')
@ -103,5 +103,5 @@ class TwAddrData(AddrData,metaclass=AsyncInit):
self.cfg,
self.proto,
al_id = al_id,
adata = AddrListData(sorted( out[al_id], key=lambda a: a.idx ))
adata = AddrListData(sorted(out[al_id], key=lambda a: a.idx))
))

View file

@ -21,13 +21,13 @@ addrfile: Address and password file classes for the MMGen suite
"""
from .cfg import gc
from .util import msg,die,capfirst
from .util import msg, die, capfirst
from .protocol import init_proto
from .obj import MMGenObject,TwComment,WalletPassword,MMGenPWIDString
from .seed import SeedID,is_seed_id
from .obj import MMGenObject, TwComment, WalletPassword, MMGenPWIDString
from .seed import SeedID, is_seed_id
from .key import PrivKey
from .addr import ViewKey,AddrListID,MMGenAddrType,MMGenPasswordType,is_addr_idx
from .addrlist import KeyList,AddrListData
from .addr import ViewKey, AddrListID, MMGenAddrType, MMGenPasswordType, is_addr_idx
from .addrlist import KeyList, AddrListData
class AddrFile(MMGenObject):
desc = 'addresses'
@ -43,7 +43,7 @@ class AddrFile(MMGenObject):
# The label may contain any printable ASCII symbol.
"""
def __init__(self,parent):
def __init__(self, parent):
self.parent = parent
self.cfg = parent.cfg
self.infile = None
@ -53,7 +53,7 @@ class AddrFile(MMGenObject):
from .crypto import Crypto
self.fmt_data = Crypto(self.cfg).mmgen_encrypt(
data = self.fmt_data.encode(),
desc = f'new {self.parent.desc} list' )
desc = f'new {self.parent.desc} list')
self.ext += f'.{Crypto.mmenc_ext}'
@property
@ -61,7 +61,7 @@ class AddrFile(MMGenObject):
return '{}{}.{}'.format(
self.parent.id_str,
('.' + self.parent.proto.network) if self.parent.proto.testnet else '',
self.ext )
self.ext)
def write(
self,
@ -92,9 +92,9 @@ class AddrFile(MMGenObject):
)
return self.parent.al_id.sid + (' ' if lbl_p2 else '') + lbl_p2
def format(self,add_comments=False):
def format(self, add_comments=False):
p = self.parent
if p.gen_passwds and p.pw_fmt in ('bip39','xmrseed'):
if p.gen_passwds and p.pw_fmt in ('bip39', 'xmrseed'):
desc_pfx = f'{p.pw_fmt.upper()} '
hdr2 = ''
else:
@ -102,9 +102,9 @@ class AddrFile(MMGenObject):
hdr2 = self.text_label_header
out = [
f'# {gc.proj_name} {desc_pfx}{p.desc} file\n#\n'
+ self.header.strip().format( pnm=gc.proj_name )
+ self.header.strip().format(pnm=gc.proj_name)
+ '\n'
+ hdr2.lstrip().format( n=TwComment.max_screen_width )
+ hdr2.lstrip().format(n=TwComment.max_screen_width)
+ '#\n'
]
@ -113,41 +113,41 @@ class AddrFile(MMGenObject):
out.append('# Record this value to a secure location.\n')
lbl = self.make_label()
self.parent.dmsg_sc('lbl',lbl[9:])
self.parent.dmsg_sc('lbl', lbl[9:])
out.append(f'{lbl} {{')
fs = ' {:<%s} {:<34}{}' % len(str(p.data[-1].idx))
for e in p.data:
c = ' ' + e.comment if add_comments and e.comment else ''
if type(p) is KeyList:
out.append(fs.format( e.idx, f'{p.al_id.mmtype.wif_label}: {e.sec.wif}', c ))
out.append(fs.format(e.idx, f'{p.al_id.mmtype.wif_label}: {e.sec.wif}', c))
elif type(p).__name__ == 'PasswordList':
out.append(fs.format(e.idx,e.passwd,c))
out.append(fs.format(e.idx, e.passwd, c))
else: # First line with idx
out.append(fs.format(e.idx, e.addr.views[e.addr.view_pref], c))
if p.has_keys:
if self.cfg.b16:
out.append(fs.format( '', f'orig_hex: {e.sec.orig_bytes.hex()}', c ))
out.append(fs.format('', f'orig_hex: {e.sec.orig_bytes.hex()}', c))
if type(self) is not ViewKeyAddrFile:
out.append(fs.format( '', f'{p.al_id.mmtype.wif_label}: {e.sec.wif}', c ))
for k in ('viewkey','wallet_passwd'):
v = getattr(e,k)
out.append(fs.format('', f'{p.al_id.mmtype.wif_label}: {e.sec.wif}', c))
for k in ('viewkey', 'wallet_passwd'):
v = getattr(e, k)
if v:
out.append(fs.format( '', f'{k}: {v}', c ))
out.append(fs.format('', f'{k}: {v}', c))
out.append('}')
self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n'
return self.fmt_data
def get_line(self,lines):
ret = lines.pop(0).split(None,2)
def get_line(self, lines):
ret = lines.pop(0).split(None, 2)
self.line_ctr += 1
if ret[0] == 'orig_hex:': # hacky
ret = lines.pop(0).split(None,2)
ret = lines.pop(0).split(None, 2)
self.line_ctr += 1
return ret if len(ret) == 3 else ret + ['']
def parse_file_body(self,lines):
def parse_file_body(self, lines):
p = self.parent
ret = AddrListData()
@ -155,39 +155,39 @@ class AddrFile(MMGenObject):
iifs = "{!r}: invalid identifier [expected '{}:']"
while lines:
idx,addr,comment = self.get_line(lines)
idx, addr, comment = self.get_line(lines)
assert is_addr_idx(idx), f'invalid address index {idx!r}'
p.check_format(addr)
a = le(**{ 'proto': p.proto, 'idx':int(idx), p.main_attr:addr, 'comment':comment })
a = le(**{'proto': p.proto, 'idx':int(idx), p.main_attr:addr, 'comment':comment})
if p.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd
if p.has_keys: # order: wif, (orig_hex), viewkey, wallet_passwd
if type(self) is not ViewKeyAddrFile:
d = self.get_line(lines)
assert d[0] == p.al_id.mmtype.wif_label+':', iifs.format(d[0],p.al_id.mmtype.wif_label)
a.sec = PrivKey(proto=p.proto,wif=d[1])
for k,dtype,add_proto in (
('viewkey',ViewKey,True),
('wallet_passwd',WalletPassword,False) ):
assert d[0] == p.al_id.mmtype.wif_label+':', iifs.format(d[0], p.al_id.mmtype.wif_label)
a.sec = PrivKey(proto=p.proto, wif=d[1])
for k, dtype, add_proto in (
('viewkey', ViewKey, True),
('wallet_passwd', WalletPassword, False)):
if k in p.al_id.mmtype.extra_attrs:
d = self.get_line(lines)
assert d[0] == k+':', iifs.format(d[0],k)
setattr(a,k,dtype( *((p.proto,d[1]) if add_proto else (d[1],)) ) )
assert d[0] == k+':', iifs.format(d[0], k)
setattr(a, k, dtype(*((p.proto, d[1]) if add_proto else (d[1],))))
ret.append(a)
if type(self) is not ViewKeyAddrFile and p.has_keys and p.ka_validity_chk is not False:
def verify_keys():
from .addrgen import KeyGenerator,AddrGenerator
kg = KeyGenerator( self.cfg, p.proto, p.al_id.mmtype.pubkey_type )
ag = AddrGenerator( self.cfg, p.proto, p.al_id.mmtype )
from .addrgen import KeyGenerator, AddrGenerator
kg = KeyGenerator(self.cfg, p.proto, p.al_id.mmtype.pubkey_type)
ag = AddrGenerator(self.cfg, p.proto, p.al_id.mmtype)
llen = len(ret)
qmsg_r = p.cfg._util.qmsg_r
for n,e in enumerate(ret):
for n, e in enumerate(ret):
qmsg_r(f'\rVerifying keys {n+1}/{llen}')
assert e.addr == ag.to_addr(kg.gen_data(e.sec)),(
assert e.addr == ag.to_addr(kg.gen_data(e.sec)), (
f'Key doesn’t match address!\n {e.sec.wif}\n {e.addr}')
p.cfg._util.qmsg(' - done')
@ -195,12 +195,12 @@ class AddrFile(MMGenObject):
verify_keys()
else:
from .ui import keypress_confirm
if keypress_confirm( p.cfg, 'Check key-to-address validity?' ):
if keypress_confirm(p.cfg, 'Check key-to-address validity?'):
verify_keys()
return ret
def parse_file(self,fn,buf=[],exit_on_error=True):
def parse_file(self, fn, buf=[], exit_on_error=True):
def parse_addrfile_label(lbl):
"""
@ -232,7 +232,7 @@ class AddrFile(MMGenObject):
network = 'mainnet'
from .proto.btc.params import mainnet
if lbl in [MMGenAddrType(mainnet,key).name for key in mainnet.mmtypes]:
if lbl in [MMGenAddrType(mainnet, key).name for key in mainnet.mmtypes]:
coin, mmtype_key = ('BTC', lbl)
elif ':' in lbl: # first component is coin, second is mmtype_key
coin, mmtype_key = lbl.split(':')
@ -249,7 +249,7 @@ class AddrFile(MMGenObject):
p = self.parent
from .fileutil import get_lines_from_file
lines = get_lines_from_file( p.cfg, fn, p.desc+' data', trim_comments=True )
lines = get_lines_from_file(p.cfg, fn, p.desc+' data', trim_comments=True)
try:
assert len(lines) >= 3, f'Too few lines in address file ({len(lines)})'
@ -267,15 +267,15 @@ class AddrFile(MMGenObject):
p.set_pw_fmt(ss[0])
p.set_pw_len(ss[1])
p.pw_id_str = MMGenPWIDString(ls.pop())
modname,funcname = p.pw_info[p.pw_fmt].chk_func.split('.')
modname, funcname = p.pw_info[p.pw_fmt].chk_func.split('.')
import importlib
p.chk_func = getattr(importlib.import_module('mmgen.'+modname),funcname)
proto = init_proto( p.cfg, 'btc' ) # FIXME: dummy protocol
mmtype = MMGenPasswordType(proto,'P')
p.chk_func = getattr(importlib.import_module('mmgen.'+modname), funcname)
proto = init_proto(p.cfg, 'btc') # FIXME: dummy protocol
mmtype = MMGenPasswordType(proto, 'P')
elif len(ls) == 1:
proto,mmtype = parse_addrfile_label(ls[0])
proto, mmtype = parse_addrfile_label(ls[0])
elif len(ls) == 0:
proto = init_proto( p.cfg, 'btc' )
proto = init_proto(p.cfg, 'btc')
mmtype = proto.addr_type('L')
else:
raise ValueError(f'{lines[0]}: Invalid first line for {p.gen_desc} file {fn!r}')
@ -289,22 +289,22 @@ class AddrFile(MMGenObject):
raise ValueError(
f'{p.desc} file is '
+ f'{proto.base_coin} {proto.network} but protocol is '
+ f'{p.proto.base_coin} {p.proto.network}' )
+ f'{p.proto.base_coin} {p.proto.network}')
p.base_coin = proto.base_coin
p.network = proto.network
p.al_id = AddrListID( sid=SeedID(sid=sid), mmtype=mmtype )
p.al_id = AddrListID(sid=SeedID(sid=sid), mmtype=mmtype)
data = self.parse_file_body(lines[1:-1])
assert isinstance(data,list),'Invalid file body data'
assert isinstance(data, list), 'Invalid file body data'
except Exception as e:
m = 'Invalid data in {} list file {!r}{} ({!s})'.format(
p.desc,
self.infile,
(f', content line {self.line_ctr}' if self.line_ctr else ''),
e )
e)
if exit_on_error:
die(3,m)
die(3, m)
else:
msg(m)
return False
@ -339,13 +339,13 @@ class PasswordFile(AddrFile):
# password. The label may contain any printable ASCII symbol.
"""
def get_line(self,lines):
def get_line(self, lines):
self.line_ctr += 1
p = self.parent
if p.pw_fmt in ('bip39','xmrseed'):
ret = lines.pop(0).split(None,p.pw_len+1)
if p.pw_fmt in ('bip39', 'xmrseed'):
ret = lines.pop(0).split(None, p.pw_len+1)
if len(ret) > p.pw_len+1:
m1 = f'extraneous text {ret[p.pw_len+1]!r} found after password'
m2 = '[bare comments not allowed in BIP39 password files]'
@ -353,10 +353,10 @@ class PasswordFile(AddrFile):
elif len(ret) < p.pw_len+1:
m = f'invalid password length {len(ret)-1}'
else:
return (ret[0],' '.join(ret[1:p.pw_len+1]),'')
return (ret[0], ' '.join(ret[1:p.pw_len+1]), '')
raise ValueError(m)
else:
ret = lines.pop(0).split(None,2)
ret = lines.pop(0).split(None, 2)
return ret if len(ret) == 3 else ret + ['']
def make_label(self):

View file

@ -24,19 +24,19 @@ from .keygen import KeyGenerator # convenience import
# decorator for to_addr() and to_viewkey()
def check_data(orig_func):
def f(self,data):
def f(self, data):
assert data.pubkey_type == self.pubkey_type, 'addrgen.py:check_data() pubkey_type mismatch'
assert data.compressed == self.compressed,(
assert data.compressed == self.compressed, (
f'addrgen.py:check_data() expected compressed={self.compressed} but got compressed={data.compressed}'
)
return orig_func(self,data)
return orig_func(self, data)
return f
class addr_generator:
class base:
def __init__(self,cfg,proto,addr_type):
def __init__(self, cfg, proto, addr_type):
self.proto = proto
self.pubkey_type = addr_type.pubkey_type
self.compressed = addr_type.compressed
@ -44,12 +44,12 @@ class addr_generator:
class keccak(base):
def __init__(self,cfg,proto,addr_type):
super().__init__(cfg,proto,addr_type)
def __init__(self, cfg, proto, addr_type):
super().__init__(cfg, proto, addr_type)
from .util2 import get_keccak
self.keccak_256 = get_keccak(cfg)
def AddrGenerator(cfg,proto,addr_type):
def AddrGenerator(cfg, proto, addr_type):
"""
factory function returning an address generator for the specified address type
"""
@ -67,7 +67,7 @@ def AddrGenerator(cfg,proto,addr_type):
from .addr import MMGenAddrType
if type(addr_type) is str:
addr_type = MMGenAddrType(proto=proto,id_str=addr_type)
addr_type = MMGenAddrType(proto=proto, id_str=addr_type)
elif type(addr_type) is MMGenAddrType:
assert addr_type in proto.mmtypes, f'{addr_type}: invalid address type for coin {proto.coin}'
else:
@ -76,4 +76,4 @@ def AddrGenerator(cfg,proto,addr_type):
import importlib
return getattr(
importlib.import_module(f'mmgen.proto.{package_map[addr_type.name]}.addrgen'),
addr_type.name )(cfg,proto,addr_type)
addr_type.name)(cfg, proto, addr_type)

View file

@ -20,17 +20,17 @@
addrlist: Address list classes for the MMGen suite
"""
from .util import suf,make_chksum_N,Msg,die
from .objmethods import MMGenObject,HiliteStr,InitErrors
from .obj import MMGenListItem,ListItemAttr,MMGenDict,TwComment,WalletPassword
from .util import suf, make_chksum_N, Msg, die
from .objmethods import MMGenObject, HiliteStr, InitErrors
from .obj import MMGenListItem, ListItemAttr, MMGenDict, TwComment, WalletPassword
from .key import PrivKey
from .addr import MMGenID,MMGenAddrType,CoinAddr,AddrIdx,AddrListID,ViewKey
from .addr import MMGenID, MMGenAddrType, CoinAddr, AddrIdx, AddrListID, ViewKey
class AddrIdxList(tuple,InitErrors,MMGenObject):
class AddrIdxList(tuple, InitErrors, MMGenObject):
max_len = 1000000
def __new__(cls,fmt_str=None,idx_list=None,sep=','):
def __new__(cls, fmt_str=None, idx_list=None, sep=','):
try:
if fmt_str:
def gen():
@ -45,9 +45,9 @@ class AddrIdxList(tuple,InitErrors,MMGenObject):
else:
raise ValueError(f'{i}: invalid range')
idx_list = tuple(gen())
return tuple.__new__(cls,sorted({AddrIdx(i) for i in (idx_list or [])}))
return tuple.__new__(cls, sorted({AddrIdx(i) for i in (idx_list or [])}))
except Exception as e:
return cls.init_fail(e,idx_list or fmt_str)
return cls.init_fail(e, idx_list or fmt_str)
@property
def id_str(self):
@ -74,36 +74,36 @@ class AddrIdxList(tuple,InitErrors,MMGenObject):
class AddrListEntryBase(MMGenListItem):
invalid_attrs = {'proto'}
def __init__(self,proto,**kwargs):
def __init__(self, proto, **kwargs):
self.__dict__['proto'] = proto
MMGenListItem.__init__(self,**kwargs)
MMGenListItem.__init__(self, **kwargs)
class AddrListEntry(AddrListEntryBase):
addr = ListItemAttr(CoinAddr,include_proto=True)
addr_p2pkh = ListItemAttr(CoinAddr,include_proto=True)
addr = ListItemAttr(CoinAddr, include_proto=True)
addr_p2pkh = ListItemAttr(CoinAddr, include_proto=True)
idx = ListItemAttr(AddrIdx) # not present in flat addrlists
comment = ListItemAttr(TwComment,reassign_ok=True)
sec = ListItemAttr(PrivKey,include_proto=True)
viewkey = ListItemAttr(ViewKey,include_proto=True)
comment = ListItemAttr(TwComment, reassign_ok=True)
sec = ListItemAttr(PrivKey, include_proto=True)
viewkey = ListItemAttr(ViewKey, include_proto=True)
wallet_passwd = ListItemAttr(WalletPassword)
class AddrListChksum(HiliteStr):
color = 'pink'
trunc_ok = False
def __new__(cls,addrlist):
def __new__(cls, addrlist):
ea = addrlist.al_id.mmtype.extra_attrs or () # add viewkey and passwd to the mix, if present
lines = [' '.join(
addrlist.chksum_rec_f(e) +
tuple(getattr(e,a) for a in ea if getattr(e,a))
tuple(getattr(e, a) for a in ea if getattr(e, a))
) for e in addrlist.data]
return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True))
return str.__new__(cls, make_chksum_N(' '.join(lines), nchars=16, sep=True))
class AddrListIDStr(HiliteStr):
color = 'green'
trunc_ok = False
def __new__(cls,addrlist,fmt_str=None):
def __new__(cls, addrlist, fmt_str=None):
idxs = [e.idx for e in addrlist.data]
prev = idxs[0]
ret = [prev]
@ -116,7 +116,7 @@ class AddrListIDStr(HiliteStr):
ret.extend(['-', prev])
ret.extend([',', i])
prev = i
s = ''.join(map(str,ret))
s = ''.join(map(str, ret))
if fmt_str:
ret = fmt_str.format(s)
@ -127,14 +127,14 @@ class AddrListIDStr(HiliteStr):
ret = '{}{}{}[{}]'.format(
addrlist.al_id.sid,
(f'-{coin}', '')[coin == 'BTC'],
(f'-{mmtype}', '')[mmtype in ('L','E')],
(f'-{mmtype}', '')[mmtype in ('L', 'E')],
s)
addrlist.dmsg_sc('id_str',ret[8:].split('[')[0])
addrlist.dmsg_sc('id_str', ret[8:].split('[')[0])
return str.__new__(cls,ret)
return str.__new__(cls, ret)
class AddrListData(list,MMGenObject):
class AddrListData(list, MMGenObject):
pass
class AddrList(MMGenObject): # Address info for a single seed ID
@ -149,10 +149,10 @@ class AddrList(MMGenObject): # Address info for a single seed ID
has_keys = False
chksum_rec_f = lambda foo, e: (str(e.idx), e.addr.views[e.addr.view_pref])
def dmsg_sc(self,desc,data):
def dmsg_sc(self, desc, data):
Msg(f'sc_debug_{desc}: {data}')
def noop(self,desc,data):
def noop(self, desc, data):
pass
def __init__(
@ -171,7 +171,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID
key_address_validity_check = None, # None=prompt user, True=check without prompt, False=skip check
skip_chksum = False,
skip_chksum_msg = False,
add_p2pkh = False ):
add_p2pkh = False):
self.cfg = cfg
self.ka_validity_chk = key_address_validity_check
@ -183,9 +183,9 @@ class AddrList(MMGenObject): # Address info for a single seed ID
self.dmsg_sc = self.noop
if seed and addr_idxs: # data from seed + idxs
self.al_id = AddrListID( sid=seed.sid, mmtype=MMGenAddrType(proto, mmtype or proto.dfl_mmtype) )
self.al_id = AddrListID(sid=seed.sid, mmtype=MMGenAddrType(proto, mmtype or proto.dfl_mmtype))
src = 'gen'
adata = self.generate(seed, addr_idxs if isinstance(addr_idxs,AddrIdxList) else AddrIdxList(addr_idxs))
adata = self.generate(seed, addr_idxs if isinstance(addr_idxs, AddrIdxList) else AddrIdxList(addr_idxs))
do_chksum = True
elif addrfile: # data from MMGen address file
self.infile = addrfile
@ -196,21 +196,21 @@ class AddrList(MMGenObject): # Address info for a single seed ID
elif addrlist: # data from flat address list
self.al_id = None
from .util import remove_dups
addrlist = remove_dups(addrlist,edesc='address',desc='address list')
adata = AddrListData([AddrListEntry(proto=proto,addr=a) for a in addrlist])
addrlist = remove_dups(addrlist, edesc='address', desc='address list')
adata = AddrListData([AddrListEntry(proto=proto, addr=a) for a in addrlist])
elif keylist: # data from flat key list
self.al_id = None
from .util import remove_dups
keylist = remove_dups(keylist,edesc='key',desc='key list',hide=True)
adata = AddrListData([AddrListEntry(proto=proto,sec=PrivKey(proto=proto,wif=k)) for k in keylist])
keylist = remove_dups(keylist, edesc='key', desc='key list', hide=True)
adata = AddrListData([AddrListEntry(proto=proto, sec=PrivKey(proto=proto, wif=k)) for k in keylist])
elif seed or addr_idxs:
die(3,'Must specify both seed and addr indexes')
die(3, 'Must specify both seed and addr indexes')
elif al_id or adata:
die(3,'Must specify both al_id and adata')
die(3, 'Must specify both al_id and adata')
else:
die(3,f'Incorrect arguments for {type(self).__name__}')
die(3, f'Incorrect arguments for {type(self).__name__}')
# al_id,adata now set
# al_id, adata now set
self.data = adata
self.num_addrs = len(adata)
self.fmt_data = ''
@ -221,7 +221,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID
if type(self) is ViewKeyAddrList:
if not 'viewkey' in self.al_id.mmtype.extra_attrs:
die(1,f'viewkeys not supported for address type {self.al_id.mmtype.desc!r}')
die(1, f'viewkeys not supported for address type {self.al_id.mmtype.desc!r}')
self.id_str = AddrListIDStr(self)
@ -233,30 +233,30 @@ class AddrList(MMGenObject): # Address info for a single seed ID
if not skip_chksum_msg:
self.do_chksum_msg(record=src=='gen')
def do_chksum_msg(self,record):
def do_chksum_msg(self, record):
chk = 'Check this value against your records'
rec = f'Record this checksum: it will be used to verify the {self.desc} file in the future'
self.cfg._util.qmsg(
f'Checksum for {self.desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' +
(chk,rec)[record] )
(chk, rec)[record])
def generate(self,seed,addr_idxs):
def generate(self, seed, addr_idxs):
seed = self.scramble_seed(seed.data)
self.dmsg_sc('seed',seed[:8].hex())
self.dmsg_sc('seed', seed[:8].hex())
mmtype = self.al_id.mmtype
gen_wallet_passwd = type(self) in (KeyAddrList,ViewKeyAddrList) and 'wallet_passwd' in mmtype.extra_attrs
gen_viewkey = type(self) in (KeyAddrList,ViewKeyAddrList) and 'viewkey' in mmtype.extra_attrs
gen_wallet_passwd = type(self) in (KeyAddrList, ViewKeyAddrList) and 'wallet_passwd' in mmtype.extra_attrs
gen_viewkey = type(self) in (KeyAddrList, ViewKeyAddrList) and 'viewkey' in mmtype.extra_attrs
if self.gen_addrs:
from .keygen import KeyGenerator
from .addrgen import AddrGenerator
kg = KeyGenerator( self.cfg, self.proto, mmtype.pubkey_type )
ag = AddrGenerator( self.cfg, self.proto, mmtype )
kg = KeyGenerator(self.cfg, self.proto, mmtype.pubkey_type)
ag = AddrGenerator(self.cfg, self.proto, mmtype)
if self.add_p2pkh:
ag2 = AddrGenerator( self.cfg, self.proto, 'compressed' )
ag2 = AddrGenerator(self.cfg, self.proto, 'compressed')
from .derive import derive_coin_privkey_bytes
@ -265,19 +265,19 @@ class AddrList(MMGenObject): # Address info for a single seed ID
out = AddrListData()
CR = '\n' if self.cfg.debug_addrlist else '\r'
for pk_bytes in derive_coin_privkey_bytes(seed,addr_idxs):
for pk_bytes in derive_coin_privkey_bytes(seed, addr_idxs):
if not self.cfg.debug:
self.cfg._util.qmsg_r(
f'{CR}Generating {self.gen_desc} #{pk_bytes.idx} ({pk_bytes.pos} of {t_addrs})' )
f'{CR}Generating {self.gen_desc} #{pk_bytes.idx} ({pk_bytes.pos} of {t_addrs})')
e = le( proto=self.proto, idx=pk_bytes.idx )
e = le(proto=self.proto, idx=pk_bytes.idx)
e.sec = PrivKey(
self.proto,
pk_bytes.data,
compressed = mmtype.compressed,
pubkey_type = mmtype.pubkey_type )
pubkey_type = mmtype.pubkey_type)
if self.gen_addrs:
data = kg.gen_data(e.sec)
@ -288,7 +288,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID
e.viewkey = ag.to_viewkey(data)
if gen_wallet_passwd:
e.wallet_passwd = self.gen_wallet_passwd(
e.viewkey.encode() if type(self) is ViewKeyAddrList else e.sec )
e.viewkey.encode() if type(self) is ViewKeyAddrList else e.sec)
elif self.gen_passwds:
e.passwd = self.gen_passwd(e.sec) # TODO - own type
@ -299,32 +299,32 @@ class AddrList(MMGenObject): # Address info for a single seed ID
self.al_id.hl(),
t_addrs,
self.gen_desc,
suf(t_addrs,self.gen_desc_pl),
' ' * 15 ))
suf(t_addrs, self.gen_desc_pl),
' ' * 15))
return out
def gen_wallet_passwd(self,privbytes):
def gen_wallet_passwd(self, privbytes):
from .proto.btc.common import hash256
return WalletPassword( hash256(privbytes)[:16].hex() )
return WalletPassword(hash256(privbytes)[:16].hex())
def check_format(self,addr):
def check_format(self, addr):
return True # format is checked when added to list entry object
def scramble_seed(self,seed):
def scramble_seed(self, seed):
is_btcfork = self.proto.base_coin == 'BTC'
if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
self.dmsg_sc('str','(none)')
self.dmsg_sc('str', '(none)')
return seed
if self.proto.base_coin == 'ETH':
scramble_key = self.proto.coin.lower()
else:
scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
scramble_key = (self.proto.coin.lower()+':', '')[is_btcfork] + self.al_id.mmtype.name
from .crypto import Crypto
if self.proto.testnet:
scramble_key += ':' + self.proto.network
self.dmsg_sc('str',scramble_key)
return Crypto(self.cfg).scramble_seed(seed,scramble_key.encode())
self.dmsg_sc('str', scramble_key)
return Crypto(self.cfg).scramble_seed(seed, scramble_key.encode())
def idxs(self):
return [e.idx for e in self.data]
@ -333,7 +333,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID
return [f'{self.al_id.sid}:{e.idx}' for e in self.data]
def addrpairs(self):
return [(e.idx,e.addr) for e in self.data]
return [(e.idx, e.addr) for e in self.data]
def coinaddrs(self):
return [e.addr for e in self.data]
@ -341,57 +341,57 @@ class AddrList(MMGenObject): # Address info for a single seed ID
def comments(self):
return [e.comment for e in self.data]
def entry(self,idx):
def entry(self, idx):
for e in self.data:
if idx == e.idx:
return e
def coinaddr(self,idx):
def coinaddr(self, idx):
for e in self.data:
if idx == e.idx:
return e.addr
def comment(self,idx):
def comment(self, idx):
for e in self.data:
if idx == e.idx:
return e.comment
def set_comment(self,idx,comment):
def set_comment(self, idx, comment):
for e in self.data:
if idx == e.idx:
e.comment = comment
def make_reverse_dict_addrlist(self,coinaddrs):
def make_reverse_dict_addrlist(self, coinaddrs):
d = MMGenDict()
b = coinaddrs
for e in self.data:
try:
d[b[b.index(e.addr)]] = ( MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.comment )
d[b[b.index(e.addr)]] = (MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.comment)
except ValueError:
pass
return d
def add_wifs(self,key_list):
def add_wifs(self, key_list):
"""
Match WIF keys in a flat list to addresses in self by generating all
possible addresses for each key.
"""
def gen_addr(pk,t):
def gen_addr(pk, t):
at = self.proto.addr_type(t)
from .keygen import KeyGenerator
from .addrgen import AddrGenerator
kg = KeyGenerator( self.cfg, self.proto, at.pubkey_type )
ag = AddrGenerator( self.cfg, self.proto, at )
kg = KeyGenerator(self.cfg, self.proto, at.pubkey_type)
ag = AddrGenerator(self.cfg, self.proto, at)
return ag.to_addr(kg.gen_data(pk))
compressed_types = set(self.proto.mmtypes) - {'L','E'}
uncompressed_types = set(self.proto.mmtypes) & {'L','E'}
compressed_types = set(self.proto.mmtypes) - {'L', 'E'}
uncompressed_types = set(self.proto.mmtypes) & {'L', 'E'}
def gen():
for wif in key_list:
pk = PrivKey(proto=self.proto,wif=wif)
pk = PrivKey(proto=self.proto, wif=wif)
for t in (compressed_types if pk.compressed else uncompressed_types):
yield ( gen_addr(pk,t), pk )
yield (gen_addr(pk, t), pk)
addrs4keys = dict(gen())
@ -399,14 +399,14 @@ class AddrList(MMGenObject): # Address info for a single seed ID
if d.addr in addrs4keys:
d.sec = addrs4keys[d.addr]
def list_missing(self,attr):
return [d.addr for d in self.data if not getattr(d,attr)]
def list_missing(self, attr):
return [d.addr for d in self.data if not getattr(d, attr)]
@property
def file(self):
if not hasattr(self,'_file'):
if not hasattr(self, '_file'):
from . import addrfile
self._file = getattr( addrfile, type(self).__name__.replace('List','File') )(self)
self._file = getattr(addrfile, type(self).__name__.replace('List', 'File'))(self)
return self._file
class KeyAddrList(AddrList):

View file

@ -20,7 +20,7 @@ from subprocess import run, PIPE, DEVNULL
from .cfg import Config
from .util import msg, msg_r, ymsg, rmsg, gmsg, bmsg, die, suf, fmt, fmt_list, is_int, have_sudo, capfirst
from .color import yellow, red, orange, brown, blue
from .wallet import Wallet,get_wallet_cls
from .wallet import Wallet, get_wallet_cls
from .addrlist import AddrIdxList
from .filename import find_file_in_dir
from .ui import keypress_confirm
@ -135,10 +135,10 @@ class Signable:
multiple_ok = True
action_desc = 'signed'
def __init__(self,parent):
def __init__(self, parent):
self.parent = parent
self.cfg = parent.cfg
self.dir = getattr(parent,self.dir_name)
self.dir = getattr(parent, self.dir_name)
self.name = type(self).__name__
@property
@ -152,21 +152,21 @@ class Signable:
@property
def unsigned(self):
return self._unprocessed( '_unsigned', self.rawext, self.sigext )
return self._unprocessed('_unsigned', self.rawext, self.sigext)
@property
def unsubmitted(self):
return self._unprocessed( '_unsubmitted', self.sigext, self.subext )
return self._unprocessed('_unsubmitted', self.sigext, self.subext)
@property
def unsubmitted_raw(self):
return self._unprocessed( '_unsubmitted_raw', self.rawext, self.subext )
return self._unprocessed('_unsubmitted_raw', self.rawext, self.subext)
unsent = unsubmitted
unsent_raw = unsubmitted_raw
def _unprocessed(self,attrname,rawext,sigext):
if not hasattr(self,attrname):
def _unprocessed(self, attrname, rawext, sigext):
if not hasattr(self, attrname):
dirlist = sorted(self.dir.iterdir())
names = {f.name for f in dirlist}
setattr(
@ -174,13 +174,13 @@ class Signable:
attrname,
tuple(f for f in dirlist
if f.name.endswith('.' + rawext)
and f.name[:-len(rawext)] + sigext not in names) )
return getattr(self,attrname)
and f.name[:-len(rawext)] + sigext not in names))
return getattr(self, attrname)
def print_bad_list(self,bad_files):
def print_bad_list(self, bad_files):
msg('\n{a}\n{b}'.format(
a = red(f'Failed {self.desc}s:'),
b = ' {}\n'.format('\n '.join(self.gen_bad_list(sorted(bad_files,key=lambda f: f.name))))
b = ' {}\n'.format('\n '.join(self.gen_bad_list(sorted(bad_files, key=lambda f: f.name))))
))
def die_wrong_num_txs(self, tx_type, msg=None, desc=None, show_dir=False):
@ -255,7 +255,7 @@ class Signable:
dir_name = 'tx_dir'
fail_msg = 'failed to sign'
async def sign(self,f):
async def sign(self, f):
from .tx import UnsignedTX
tx1 = UnsignedTX(
cfg = self.cfg,
@ -263,7 +263,7 @@ class Signable:
automount = self.name=='automount_transaction')
if tx1.proto.sign_mode == 'daemon':
from .rpc import rpc_init
tx1.rpc = await rpc_init( self.cfg, tx1.proto, ignore_wallet=True )
tx1.rpc = await rpc_init(self.cfg, tx1.proto, ignore_wallet=True)
from .tx.sign import txsign
tx2 = await txsign(
cfg_parm = self.cfg,
@ -278,7 +278,7 @@ class Signable:
else:
return False
def print_summary(self,signables):
def print_summary(self, signables):
if self.cfg.full_summary:
bmsg('\nAutosign summary:\n')
@ -289,22 +289,22 @@ class Signable:
for tx in signables:
non_mmgen = [o for o in tx.outputs if not o.mmid]
if non_mmgen:
yield (tx,non_mmgen)
yield (tx, non_mmgen)
body = list(gen())
if body:
bmsg('\nAutosign summary:')
fs = '{} {} {}'
t_wid,a_wid = 6,44
t_wid, a_wid = 6, 44
def gen():
yield fs.format('TX ID ','Non-MMGen outputs'+' '*(a_wid-17),'Amount')
yield fs.format('TX ID ', 'Non-MMGen outputs'+' '*(a_wid-17), 'Amount')
yield fs.format('-'*t_wid, '-'*a_wid, '-'*7)
for tx,non_mmgen in body:
for tx, non_mmgen in body:
for nm in non_mmgen:
yield fs.format(
tx.txid.fmt( width=t_wid, color=True ) if nm is non_mmgen[0] else ' '*t_wid,
tx.txid.fmt(width=t_wid, color=True) if nm is non_mmgen[0] else ' '*t_wid,
nm.addr.fmt(nm.addr.view_pref, width=a_wid, color=True),
nm.amt.hl() + ' ' + yellow(tx.coin))
@ -312,7 +312,7 @@ class Signable:
else:
msg('\nNo non-MMGen outputs')
def gen_bad_list(self,bad_files):
def gen_bad_list(self, bad_files):
for f in bad_files:
yield red(f.name)
@ -326,12 +326,12 @@ class Signable:
class xmr_signable(transaction): # mixin class
def need_daemon_restart(self,m,new_idx):
def need_daemon_restart(self, m, new_idx):
old_idx = self.parent.xmr_cur_wallet_idx
self.parent.xmr_cur_wallet_idx = new_idx
return old_idx != new_idx or m.wd.state != 'ready'
def print_summary(self,signables):
def print_summary(self, signables):
bmsg('\nAutosign summary:')
msg('\n'.join(s.get_info(indent=' ') for s in signables) + self.summary_footer)
@ -342,7 +342,7 @@ class Signable:
multiple_ok = False
summary_footer = ''
async def sign(self,f):
async def sign(self, f):
from . import xmrwallet
from .xmrwallet.file.tx import MoneroMMGenTX
tx1 = MoneroMMGenTX.Completed(self.parent.xmrwallet_cfg, f)
@ -351,7 +351,7 @@ class Signable:
self.parent.xmrwallet_cfg,
infile = str(self.parent.wallet_files[0]), # MMGen wallet file
wallets = str(tx1.src_wallet_idx))
tx2 = await m.main( f, restart_daemon=self.need_daemon_restart(m,tx1.src_wallet_idx) )
tx2 = await m.main(f, restart_daemon=self.need_daemon_restart(m, tx1.src_wallet_idx))
tx2.write(ask_write=False)
return tx2
@ -370,7 +370,7 @@ class Signable:
f for f in super().unsigned
if not json.loads(f.read_text())['MoneroMMGenWalletOutputsFile']['data']['imported'])
async def sign(self,f):
async def sign(self, f):
from . import xmrwallet
wallet_idx = xmrwallet.op_cls('wallet').get_idx_from_fn(f)
m = xmrwallet.op(
@ -378,7 +378,7 @@ class Signable:
self.parent.xmrwallet_cfg,
infile = str(self.parent.wallet_files[0]), # MMGen wallet file
wallets = str(wallet_idx))
obj = await m.main(f, wallet_idx, restart_daemon=self.need_daemon_restart(m,wallet_idx))
obj = await m.main(f, wallet_idx, restart_daemon=self.need_daemon_restart(m, wallet_idx))
obj.write(quiet=not obj.data.sign)
self.action_desc = 'imported and signed' if obj.data.sign else 'imported'
return obj
@ -390,26 +390,26 @@ class Signable:
dir_name = 'msg_dir'
fail_msg = 'failed to sign or signed incompletely'
async def sign(self,f):
from .msg import UnsignedMsg,SignedMsg
m = UnsignedMsg( self.cfg, infile=f )
async def sign(self, f):
from .msg import UnsignedMsg, SignedMsg
m = UnsignedMsg(self.cfg, infile=f)
await m.sign(wallet_files=self.parent.wallet_files[:], passwd_file=str(self.parent.keyfile))
m = SignedMsg( self.cfg, data=m.__dict__ )
m = SignedMsg(self.cfg, data=m.__dict__)
m.write_to_file(
outdir = self.dir.resolve(),
ask_overwrite = False )
ask_overwrite = False)
if m.data.get('failed_sids'):
die('MsgFileFailedSID',f'Failed Seed IDs: {fmt_list(m.data["failed_sids"],fmt="bare")}')
die('MsgFileFailedSID', f'Failed Seed IDs: {fmt_list(m.data["failed_sids"], fmt="bare")}')
return m
def print_summary(self,signables):
def print_summary(self, signables):
gmsg('\nSigned message files:')
for message in signables:
gmsg(' ' + message.signed_filename)
def gen_bad_list(self,bad_files):
def gen_bad_list(self, bad_files):
for f in bad_files:
sigfile = f.parent / ( f.name[:-len(self.rawext)] + self.sigext )
sigfile = f.parent / (f.name[:-len(self.rawext)] + self.sigext)
yield orange(sigfile.name) if sigfile.exists() else red(f.name)
class Autosign:
@ -453,13 +453,13 @@ class Autosign:
def init_fixup(self): # see test/overlay/fakemods/mmgen/autosign.py
pass
def __init__(self,cfg,cmd=None):
def __init__(self, cfg, cmd=None):
if cfg.mnemonic_fmt:
if cfg.mnemonic_fmt not in self.mn_fmts:
die(1,'{!r}: invalid mnemonic format (must be one of: {})'.format(
die(1, '{!r}: invalid mnemonic format (must be one of: {})'.format(
cfg.mnemonic_fmt,
fmt_list( self.mn_fmts, fmt='no_spc' ) ))
fmt_list(self.mn_fmts, fmt='no_spc')))
if sys.platform == 'linux':
self.dfl_mountpoint = f'/mnt/{self.linux_mount_subdir}'
@ -507,11 +507,11 @@ class Autosign:
self.keyfile = self.mountpoint / 'autosign.key'
if any(k in cfg._uopts for k in ('help','longhelp')):
if any(k in cfg._uopts for k in ('help', 'longhelp')):
return
if 'coin' in cfg._uopts:
die(1,'--coin option not supported with this command. Use --coins instead')
die(1, '--coin option not supported with this command. Use --coins instead')
self.coins = cfg.coins.upper().split(',') if cfg.coins else []
@ -539,7 +539,7 @@ class Autosign:
self.dirs |= self.xmr_dirs
self.signables += Signable.xmr_signables
for name,path in self.dirs.items():
for name, path in self.dirs.items():
setattr(self, name, self.mountpoint / path)
self.swap = SwapMgr(self.cfg, ignore_zram=True)
@ -553,28 +553,28 @@ class Autosign:
from .rpc import rpc_init
from .exception import SocketError
try:
await rpc_init( self.cfg, proto, ignore_wallet=True )
await rpc_init(self.cfg, proto, ignore_wallet=True)
except SocketError as e:
from .daemon import CoinDaemon
d = CoinDaemon( self.cfg, proto=proto, test_suite=self.cfg.test_suite )
d = CoinDaemon(self.cfg, proto=proto, test_suite=self.cfg.test_suite)
die(2,
f'\n{e}\nIs the {d.coind_name} daemon ({d.exec_fn}) running '
+ 'and listening on the correct port?' )
+ 'and listening on the correct port?')
@property
def wallet_files(self):
if not hasattr(self,'_wallet_files'):
if not hasattr(self, '_wallet_files'):
try:
dirlist = self.wallet_dir.iterdir()
except:
die(1,f"Cannot open wallet directory '{self.wallet_dir}'. Did you run ‘mmgen-autosign setup’?")
die(1, f"Cannot open wallet directory '{self.wallet_dir}'. Did you run ‘mmgen-autosign setup’?")
self._wallet_files = [f for f in dirlist if f.suffix == '.mmdat']
if not self._wallet_files:
die(1,'No wallet files present!')
die(1, 'No wallet files present!')
return self._wallet_files
@ -595,7 +595,7 @@ class Autosign:
if sys.platform == 'linux' and not self.mountpoint.is_dir():
def do_die(m):
die(1,'\n' + yellow(fmt(m.strip(),indent=' ')))
die(1, '\n' + yellow(fmt(m.strip(), indent=' ')))
if Path(self.old_dfl_mountpoint).is_dir():
do_die(self.old_dfl_mountpoint_errmsg)
else:
@ -607,14 +607,14 @@ class Autosign:
if not silent:
msg(f"Mounting '{self.mountpoint}'")
else:
die(1,f'Unable to mount device {self.dev_label} at {self.mountpoint}')
die(1, f'Unable to mount device {self.dev_label} at {self.mountpoint}')
for dirname in self.dirs:
check_or_create(dirname)
def do_umount(self, silent=False, verbose=False):
if self.mountpoint.is_mount():
run( ['sync'], check=True )
run(['sync'], check=True)
if not silent:
msg(f"Unmounting '{self.mountpoint}'")
redir = None if verbose else DEVNULL
@ -634,8 +634,8 @@ class Autosign:
return not fails
async def sign_all(self,target_name):
target = getattr(Signable,target_name)(self)
async def sign_all(self, target_name):
target = getattr(Signable, target_name)(self)
if target.unsigned:
good = []
bad = []
@ -675,7 +675,7 @@ class Autosign:
self.led.set('busy')
ret = [await self.sign_all(signable) for signable in self.signables]
for val in ret:
if isinstance(val,str):
if isinstance(val, str):
msg(val)
if self.cfg.test_suite_autosign_threaded:
await asyncio.sleep(0.3)
@ -701,15 +701,15 @@ class Autosign:
desc = f"key file '{self.keyfile}'"
msg('Creating ' + desc)
try:
self.keyfile.write_text( os.urandom(32).hex() )
self.keyfile.write_text(os.urandom(32).hex())
self.keyfile.chmod(0o400)
except:
die(2,'Unable to write ' + desc)
die(2, 'Unable to write ' + desc)
msg('Wrote ' + desc)
def gen_key(self,no_unmount=False):
def gen_key(self, no_unmount=False):
if not self.device_inserted:
die(1,'Removable device not present!')
die(1, 'Removable device not present!')
self.do_mount()
self.wipe_encryption_key()
self.create_key()
@ -756,7 +756,7 @@ class Autosign:
try:
self.wallet_dir.stat()
except:
die(2,f"Unable to create wallet directory '{self.wallet_dir}'")
die(2, f"Unable to create wallet directory '{self.wallet_dir}'")
self.gen_key(no_unmount=True)
@ -768,20 +768,20 @@ class Autosign:
remove_wallet_dir()
create_wallet_dir()
wf = find_file_in_dir( get_wallet_cls('mmgen'), self.cfg.data_dir )
wf = find_file_in_dir(get_wallet_cls('mmgen'), self.cfg.data_dir)
if wf and keypress_confirm(
cfg = self.cfg,
prompt = f"Default wallet '{wf}' found.\nUse default wallet for autosigning?",
default_yes = True ):
ss_in = Wallet( Config(), wf )
default_yes = True):
ss_in = Wallet(Config(), wf)
else:
ss_in = Wallet( self.cfg, in_fmt=self.mn_fmts[self.cfg.mnemonic_fmt or self.dfl_mn_fmt] )
ss_out = Wallet( self.cfg, ss=ss_in, passwd_file=str(self.keyfile) )
ss_out.write_to_file( desc='autosign wallet', outdir=self.wallet_dir )
ss_in = Wallet(self.cfg, in_fmt=self.mn_fmts[self.cfg.mnemonic_fmt or self.dfl_mn_fmt])
ss_out = Wallet(self.cfg, ss=ss_in, passwd_file=str(self.keyfile))
ss_out.write_to_file(desc='autosign wallet', outdir=self.wallet_dir)
@property
def xmrwallet_cfg(self):
if not hasattr(self,'_xmrwallet_cfg'):
if not hasattr(self, '_xmrwallet_cfg'):
self._xmrwallet_cfg = Config({
'_clone': self.cfg,
'coin': 'xmr',
@ -820,29 +820,29 @@ class Autosign:
nonlocal count
msg_r('.')
from .fileutil import shred_file
shred_file( f, verbose=self.cfg.verbose )
shred_file(f, verbose=self.cfg.verbose)
count += 1
def clean_dir(s_name):
def clean_files(rawext,sigext):
def clean_files(rawext, sigext):
for f in s.dir.iterdir():
if s.clean_all and (f.name.endswith(f'.{rawext}') or f.name.endswith(f'.{sigext}')):
do_shred(f)
elif f.name.endswith(f'.{sigext}'):
raw = f.parent / ( f.name[:-len(sigext)] + rawext )
raw = f.parent / (f.name[:-len(sigext)] + rawext)
if raw.is_file():
do_shred(raw)
s = getattr(Signable,s_name)(self)
s = getattr(Signable, s_name)(self)
msg_r(f"Cleaning directory '{s.dir}'..")
if s.dir.is_dir():
clean_files( s.rawext, s.sigext )
if hasattr(s,'subext'):
clean_files( s.rawext, s.subext )
clean_files( s.sigext, s.subext )
clean_files(s.rawext, s.sigext)
if hasattr(s, 'subext'):
clean_files(s.rawext, s.subext)
clean_files(s.sigext, s.subext)
msg('done' if s.dir.is_dir() else 'skipped (no dir)')
@ -887,7 +887,7 @@ class Autosign:
msg_r('.')
n += 1
def at_exit(self,exit_val,message=None):
def at_exit(self, exit_val, message=None):
if message:
msg(message)
self.led.stop()
@ -895,16 +895,16 @@ class Autosign:
def init_exit_handler(self):
def handler(arg1,arg2):
self.at_exit(1,'\nCleaning up...')
def handler(arg1, arg2):
self.at_exit(1, '\nCleaning up...')
import signal
signal.signal( signal.SIGTERM, handler )
signal.signal( signal.SIGINT, handler )
signal.signal(signal.SIGTERM, handler)
signal.signal(signal.SIGINT, handler)
def init_led(self):
from .led import LEDControl
self.led = LEDControl(
enabled = self.cfg.led,
simulate = self.cfg.test_suite_autosign_led_simulate )
simulate = self.cfg.test_suite_autosign_led_simulate)
self.led.set('off')

View file

@ -21,14 +21,14 @@ base_obj: base objects with no internal imports for the MMGen suite
"""
class AsyncInit(type):
async def __call__(cls,*args,**kwargs):
instance = cls.__new__(cls,*args,**kwargs)
await type(instance).__init__(instance,*args,**kwargs)
async def __call__(cls, *args, **kwargs):
instance = cls.__new__(cls, *args, **kwargs)
await type(instance).__init__(instance, *args, **kwargs)
return instance
class AttrCtrlMeta(type):
def __call__(cls,*args,**kwargs):
instance = super().__call__(*args,**kwargs)
def __call__(cls, *args, **kwargs):
instance = super().__call__(*args, **kwargs)
if instance._autolock:
instance._lock()
return instance
@ -55,38 +55,38 @@ class AttrCtrl(metaclass=AttrCtrlMeta):
def _lock(self):
self._locked = True
def __getattr__(self,name):
def __getattr__(self, name):
if self._locked and self._default_to_none:
return None
else:
raise AttributeError(f'{type(self).__name__} object has no attribute {name!r}')
def __setattr__(self,name,value):
def __setattr__(self, name, value):
if self._locked:
assert name != '_locked', 'lock can be set only once'
def do_error(name,value,ref_val):
def do_error(name, value, ref_val):
raise AttributeError(
f'{value!r}: invalid value for attribute {name!r}'
+ ' of {} object (must be of type {}, not {})'.format(
type(self).__name__,
type(ref_val).__name__,
type(value).__name__ ) )
type(value).__name__))
if not (name in self.__dict__ or hasattr(type(self),name)):
if not (name in self.__dict__ or hasattr(type(self), name)):
raise AttributeError(f'{type(self).__name__} object has no attribute {name!r}')
ref_val = getattr(type(self),name) if self._use_class_attr else getattr(self,name)
ref_val = getattr(type(self), name) if self._use_class_attr else getattr(self, name)
if (
(name not in self._skip_type_check)
and (ref_val is not None)
and not isinstance(value,type(ref_val))
and not isinstance(value, type(ref_val))
):
do_error(name,value,ref_val)
do_error(name, value, ref_val)
return object.__setattr__(self,name,value)
return object.__setattr__(self, name, value)
def __delattr__(self, name):
if self._locked and not name in self._delete_ok:
@ -107,25 +107,23 @@ class Lockable(AttrCtrl):
_reset_ok = ()
def _lock(self):
for name in ('_set_ok','_reset_ok'):
for attr in getattr(self,name):
assert hasattr(self,attr), (
f'attribute {attr!r} in {name!r} not found in {type(self).__name__} object {id(self)}' )
for name in ('_set_ok', '_reset_ok'):
for attr in getattr(self, name):
assert hasattr(self, attr), (
f'attribute {attr!r} in {name!r} not found in {type(self).__name__} object {id(self)}')
super()._lock()
def __setattr__(self,name,value):
if self._locked and (name in self.__dict__ or hasattr(type(self),name)):
val = getattr(self,name)
def __setattr__(self, name, value):
if self._locked and (name in self.__dict__ or hasattr(type(self), name)):
val = getattr(self, name)
if name not in (self._set_ok + self._reset_ok):
raise AttributeError(f'attribute {name!r} of {type(self).__name__} object is read-only')
elif name not in self._reset_ok:
#print(self.__dict__)
if not (
(val != 0 and not val) or
(self._use_class_attr and name not in self.__dict__) ):
(self._use_class_attr and name not in self.__dict__)):
raise AttributeError(
f'attribute {name!r} of {type(self).__name__} object is already set,'
+ ' and resetting is forbidden' )
# else name is in (_set_ok + _reset_ok) -- allow name to be in both lists
+ ' and resetting is forbidden')
return AttrCtrl.__setattr__(self,name,value)
return AttrCtrl.__setattr__(self, name, value)

View file

@ -32,23 +32,22 @@ def is_b32_str(s):
def is_mmgen_mnemonic(s):
try:
baseconv('mmgen').tobytes(s.split(),pad='seed')
baseconv('mmgen').tobytes(s.split(), pad='seed')
return True
except:
return False
class baseconv:
mn_base = 1626
dt = namedtuple('desc_tuple',['short','long'])
dt = namedtuple('desc_tuple', ['short', 'long'])
constants = {
'desc': {
'b58': dt('base58', 'base58-encoded data'),
'b32': dt('MMGen base32', 'MMGen base32-encoded data created using simple base conversion'),
'b16': dt('hexadecimal string','base16 (hexadecimal) string data'),
'b10': dt('base10 string', 'base10 (decimal) string data'),
'b8': dt('base8 string', 'base8 (octal) string data'),
'b6d': dt('base6d (die roll)', 'base6 data using the digits from one to six'),
# 'tirosh':('Tirosh mnemonic', 'base1626 mnemonic using truncated Tirosh wordlist'), # not used by wallet
'b58': dt('base58', 'base58-encoded data'),
'b32': dt('MMGen base32', 'MMGen base32-encoded data created using simple base conversion'),
'b16': dt('hexadecimal string', 'base16 (hexadecimal) string data'),
'b10': dt('base10 string', 'base10 (decimal) string data'),
'b8': dt('base8 string', 'base8 (octal) string data'),
'b6d': dt('base6d (die roll)', 'base6 data using the digits from one to six'),
'mmgen': dt('MMGen native mnemonic',
'MMGen native mnemonic seed phrase created using old Electrum wordlist and simple base conversion'),
},
@ -68,18 +67,18 @@ class baseconv:
# 'tirosh1633': '1a5faeff' # tirosh list is 1633 words long!
},
'seedlen_map': {
'b58': { 16:22, 24:33, 32:44 },
'b6d': { 16:50, 24:75, 32:100 },
'mmgen': { 16:12, 24:18, 32:24 },
'b58': {16:22, 24:33, 32:44},
'b6d': {16:50, 24:75, 32:100},
'mmgen': {16:12, 24:18, 32:24},
},
'seedlen_map_rev': {
'b58': { 22:16, 33:24, 44:32 },
'b6d': { 50:16, 75:24, 100:32 },
'mmgen': { 12:16, 18:24, 24:32 },
'b58': {22:16, 33:24, 44:32},
'b6d': {50:16, 75:24, 100:32},
'mmgen': {12:16, 18:24, 24:32},
}
}
def __init__(self,wl_id):
def __init__(self, wl_id):
if wl_id == 'mmgen':
from .wordlist.electrum import words
@ -87,9 +86,9 @@ class baseconv:
elif wl_id not in self.constants['digits']:
raise ValueError(f'{wl_id}: unrecognized mnemonic ID')
for k,v in self.constants.items():
for k, v in self.constants.items():
if wl_id in v:
setattr(self,k,v[wl_id])
setattr(self, k, v[wl_id])
self.wl_id = wl_id
@ -98,23 +97,23 @@ class baseconv:
def get_wordlist_chksum(self):
from hashlib import sha256
return sha256( ' '.join(self.digits).encode() ).hexdigest()[:8]
return sha256(' '.join(self.digits).encode()).hexdigest()[:8]
def check_wordlist(self,cfg):
def check_wordlist(self, cfg):
wl = self.digits
ret = f'Wordlist: {self.wl_id}\nLength: {len(wl)} words'
new_chksum = self.get_wordlist_chksum()
cfg._util.compare_chksums( new_chksum, 'generated', self.wl_chksum, 'saved', die_on_fail=True )
cfg._util.compare_chksums(new_chksum, 'generated', self.wl_chksum, 'saved', die_on_fail=True)
if tuple(sorted(wl)) == wl:
return ret + '\nList is sorted'
else:
die(3,'ERROR: List is not sorted!')
die(3, 'ERROR: List is not sorted!')
@staticmethod
def get_pad(pad,seed_pad_func):
def get_pad(pad, seed_pad_func):
"""
'pad' argument to baseconv conversion methods must be either None, 'seed' or an integer.
If None, output of minimum (but never zero) length will be produced.
@ -128,72 +127,72 @@ class baseconv:
elif pad == 'seed':
return seed_pad_func()
else:
die('BaseConversionPadError',f"{pad!r}: illegal value for 'pad' (must be None,'seed' or int)")
die('BaseConversionPadError', f"{pad!r}: illegal value for 'pad' (must be None, 'seed' or int)")
def tohex(self,words_arg,pad=None):
def tohex(self, words_arg, pad=None):
"convert string or list data of instance base to a hexadecimal string"
return self.tobytes(words_arg, pad//2 if type(pad) is int else pad).hex()
def tobytes(self,words_arg,pad=None):
def tobytes(self, words_arg, pad=None):
"convert string or list data of instance base to byte string"
words = words_arg if isinstance(words_arg,(list,tuple)) else tuple(words_arg.strip())
words = words_arg if isinstance(words_arg, (list, tuple)) else tuple(words_arg.strip())
desc = self.desc.short
if len(words) == 0:
die('BaseConversionError',f'empty {desc} data')
die('BaseConversionError', f'empty {desc} data')
def get_seed_pad():
assert hasattr(self,'seedlen_map_rev'), f'seed padding not supported for base {self.wl_id!r}'
assert hasattr(self, 'seedlen_map_rev'), f'seed padding not supported for base {self.wl_id!r}'
d = self.seedlen_map_rev
if not len(words) in d:
die( 'BaseConversionError',
f'{len(words)}: invalid length for seed-padded {desc} data in base conversion' )
die('BaseConversionError',
f'{len(words)}: invalid length for seed-padded {desc} data in base conversion')
return d[len(words)]
pad_val = max(self.get_pad(pad,get_seed_pad),1)
pad_val = max(self.get_pad(pad, get_seed_pad), 1)
wl = self.digits
base = len(wl)
if not set(words) <= set(wl):
die( 'BaseConversionError',
( 'seed data' if pad == 'seed' else f'{words_arg!r}:' ) +
f' not in {desc} format' )
die('BaseConversionError',
('seed data' if pad == 'seed' else f'{words_arg!r}:') +
f' not in {desc} format')
ret = sum(wl.index(words[::-1][i])*(base**i) for i in range(len(words)))
bl = ret.bit_length()
return ret.to_bytes(max(pad_val,bl//8+bool(bl%8)),'big')
return ret.to_bytes(max(pad_val, bl//8+bool(bl%8)), 'big')
def fromhex(self,hexstr,pad=None,tostr=False):
def fromhex(self, hexstr, pad=None, tostr=False):
"convert a hexadecimal string to a list or string data of instance base"
from .util import is_hex_str
if not is_hex_str(hexstr):
die( 'HexadecimalStringError',
( 'seed data' if pad == 'seed' else f'{hexstr!r}:' ) +
' not a hexadecimal string' )
die('HexadecimalStringError',
('seed data' if pad == 'seed' else f'{hexstr!r}:') +
' not a hexadecimal string')
return self.frombytes( bytes.fromhex(hexstr), pad, tostr )
return self.frombytes(bytes.fromhex(hexstr), pad, tostr)
def frombytes(self,bytestr,pad=None,tostr=False):
def frombytes(self, bytestr, pad=None, tostr=False):
"convert byte string to list or string data of instance base"
if not bytestr:
die( 'BaseConversionError', 'empty data not allowed in base conversion' )
die('BaseConversionError', 'empty data not allowed in base conversion')
def get_seed_pad():
assert hasattr(self,'seedlen_map'), f'seed padding not supported for base {self.wl_id!r}'
assert hasattr(self, 'seedlen_map'), f'seed padding not supported for base {self.wl_id!r}'
d = self.seedlen_map
if not len(bytestr) in d:
die( 'SeedLengthError',
f'{len(bytestr)}: invalid byte length for seed data in seed-padded base conversion' )
die('SeedLengthError',
f'{len(bytestr)}: invalid byte length for seed data in seed-padded base conversion')
return d[len(bytestr)]
pad = max(self.get_pad(pad,get_seed_pad),1)
pad = max(self.get_pad(pad, get_seed_pad), 1)
wl = self.digits
def gen():
num = int.from_bytes(bytestr,'big')
num = int.from_bytes(bytestr, 'big')
base = len(wl)
while num:
yield num % base

View file

@ -23,21 +23,21 @@ bip39.py - Data and routines for BIP39 mnemonic seed phrases
from hashlib import sha256
from .baseconv import baseconv
from .util import is_hex_str,die
from .util import is_hex_str, die
def is_bip39_mnemonic(s):
return bool( bip39().tohex(s.split()) )
return bool(bip39().tohex(s.split()))
# implements a subset of the baseconv API
class bip39(baseconv):
desc = baseconv.dt('BIP39 mnemonic', 'BIP39 mnemonic seed phrase')
wl_chksum = 'f18b9a84'
seedlen_map = { 16:12, 24:18, 32:24 }
seedlen_map_rev = { 12:16, 18:24, 24:32 }
seedlen_map = {16:12, 24:18, 32:24}
seedlen_map_rev = {12:16, 18:24, 24:32}
from collections import namedtuple
bc = namedtuple('bip39_constants',['chk_len','mn_len'])
bc = namedtuple('bip39_constants', ['chk_len', 'mn_len'])
# ENT CS MS
constants = {
128: bc(4, 12),
@ -47,21 +47,21 @@ class bip39(baseconv):
256: bc(8, 24),
}
def __init__(self,wl_id='bip39'):
def __init__(self, wl_id='bip39'):
assert wl_id == 'bip39', "initialize with 'bip39' for compatibility with baseconv API"
from .wordlist.bip39 import words
self.digits = words
self.wl_id = 'bip39'
@classmethod
def nwords2seedlen(cls,nwords,in_bytes=False,in_hex=False):
for k,v in cls.constants.items():
def nwords2seedlen(cls, nwords, in_bytes=False, in_hex=False):
for k, v in cls.constants.items():
if v.mn_len == nwords:
return k//8 if in_bytes else k//4 if in_hex else k
die( 'MnemonicError', f'{nwords!r}: invalid word length for BIP39 mnemonic' )
die('MnemonicError', f'{nwords!r}: invalid word length for BIP39 mnemonic')
@classmethod
def seedlen2nwords(cls,seed_len,in_bytes=False,in_hex=False):
def seedlen2nwords(cls, seed_len, in_bytes=False, in_hex=False):
seed_bits = seed_len * 8 if in_bytes else seed_len * 4 if in_hex else seed_len
try:
return cls.constants[seed_bits].mn_len
@ -71,47 +71,47 @@ class bip39(baseconv):
def tohex(self, words_arg, pad=None):
return self.tobytes(words_arg, pad=pad).hex()
def tobytes(self,words_arg,pad=None):
assert isinstance(words_arg,(list,tuple)),'words_arg must be list or tuple'
assert pad in (None,'seed'), f"{pad}: invalid 'pad' argument (must be None or 'seed')"
def tobytes(self, words_arg, pad=None):
assert isinstance(words_arg, (list, tuple)), 'words_arg must be list or tuple'
assert pad in (None, 'seed'), f"{pad}: invalid 'pad' argument (must be None or 'seed')"
wl = self.digits
for n,w in enumerate(words_arg):
for n, w in enumerate(words_arg):
if w not in wl:
die( 'MnemonicError', f'word #{n+1} is not in the BIP39 word list' )
die('MnemonicError', f'word #{n+1} is not in the BIP39 word list')
res = ''.join(f'{wl.index(w):011b}' for w in words_arg)
for k,v in self.constants.items():
for k, v in self.constants.items():
if len(words_arg) == v.mn_len:
bitlen = k
break
else:
die( 'MnemonicError', f'{len(words_arg)}: invalid BIP39 seed phrase length' )
die('MnemonicError', f'{len(words_arg)}: invalid BIP39 seed phrase length')
seed_bin = res[:bitlen]
chk_bin = res[bitlen:]
seed_hex = f'{int(seed_bin,2):0{bitlen//4}x}'
seed_hex = f'{int(seed_bin, 2):0{bitlen//4}x}'
seed_bytes = bytes.fromhex(seed_hex)
chk_len = self.constants[bitlen].chk_len
chk_hex_chk = sha256(seed_bytes).hexdigest()
chk_bin_chk = f'{int(chk_hex_chk,16):0256b}'[:chk_len]
chk_bin_chk = f'{int(chk_hex_chk, 16):0256b}'[:chk_len]
if chk_bin != chk_bin_chk:
die( 'MnemonicError', f'invalid BIP39 seed phrase checksum ({chk_bin} != {chk_bin_chk})' )
die('MnemonicError', f'invalid BIP39 seed phrase checksum ({chk_bin} != {chk_bin_chk})')
return seed_bytes
def fromhex(self, hexstr, pad=None, tostr=False):
assert is_hex_str(hexstr),'seed data not a hexadecimal string'
assert is_hex_str(hexstr), 'seed data not a hexadecimal string'
return self.frombytes(bytes.fromhex(hexstr), pad=pad, tostr=tostr)
def frombytes(self, seed_bytes, pad=None, tostr=False):
assert tostr is False,"'tostr' must be False for 'bip39'"
assert pad in (None,'seed'), f"{pad}: invalid 'pad' argument (must be None or 'seed')"
assert tostr is False, "'tostr' must be False for 'bip39'"
assert pad in (None, 'seed'), f"{pad}: invalid 'pad' argument (must be None or 'seed')"
wl = self.digits
bitlen = len(seed_bytes) * 8
@ -121,14 +121,14 @@ class bip39(baseconv):
chk_hex = sha256(seed_bytes).hexdigest()
seed_bin = f'{int(seed_bytes.hex(),16):0{bitlen}b}'
chk_bin = f'{int(chk_hex,16):0256b}'
seed_bin = f'{int(seed_bytes.hex(), 16):0{bitlen}b}'
chk_bin = f'{int(chk_hex, 16):0256b}'
res = seed_bin + chk_bin
return tuple(wl[int(res[i*11:(i+1)*11],2)] for i in range(c.mn_len))
return tuple(wl[int(res[i*11:(i+1)*11], 2)] for i in range(c.mn_len))
def generate_seed(self,words_arg,passwd=''):
def generate_seed(self, words_arg, passwd=''):
self.tohex(words_arg) # validate

View file

@ -20,15 +20,15 @@
cfg: Configuration classes for the MMGen suite
"""
import sys,os
import sys, os
from collections import namedtuple
from .base_obj import Lockable
def die(*args,**kwargs):
def die(*args, **kwargs):
from .util import die
die(*args,**kwargs)
die(*args, **kwargs)
def die2(exit_val,s):
def die2(exit_val, s):
sys.stderr.write(s+'\n')
sys.exit(exit_val)
@ -86,16 +86,16 @@ class GlobalConstants(Lockable):
cmd_caps = cmd_caps_data.get(prog_id)
if sys.platform not in ('linux', 'win32', 'darwin'):
die2(1,f'{sys.platform!r}: platform not supported by {proj_name}')
die2(1, f'{sys.platform!r}: platform not supported by {proj_name}')
if os.getenv('HOME'): # Linux, MSYS2, or macOS
home_dir = os.getenv('HOME')
elif sys.platform == 'win32': # Windows without MSYS2 - not supported
die2(1,f'$HOME not set! {proj_name} for Windows must be run in MSYS2 environment')
die2(1, f'$HOME not set! {proj_name} for Windows must be run in MSYS2 environment')
else:
die2(2,'$HOME is not set! Unable to determine home directory')
die2(2, '$HOME is not set! Unable to determine home directory')
def get_mmgen_data_file(self,filename,package='mmgen'):
def get_mmgen_data_file(self, filename, package='mmgen'):
"""
this is an expensive import, so do only when required
"""
@ -111,7 +111,7 @@ class GlobalConstants(Lockable):
from importlib.resources import files # Python 3.9 and above
except ImportError:
from importlib_resources import files
return files(package).joinpath('data',filename).read_text()
return files(package).joinpath('data', filename).read_text()
@property
def version(self):
@ -144,7 +144,7 @@ class Config(Lockable):
3 - config file
"""
_autolock = False
_set_ok = ('usr_randchars','_proto')
_set_ok = ('usr_randchars', '_proto')
_reset_ok = ('accept_defaults',)
_delete_ok = ('_opts',)
_use_class_attr = True
@ -210,7 +210,7 @@ class Config(Lockable):
pager = False
columns = 0
color = bool(
( sys.stdout.isatty() and not os.getenv('MMGEN_TEST_SUITE_PEXPECT') ) or
(sys.stdout.isatty() and not os.getenv('MMGEN_TEST_SUITE_PEXPECT')) or
os.getenv('MMGEN_TEST_SUITE_ENABLE_COLOR')
)
@ -256,7 +256,7 @@ class Config(Lockable):
exit_after = ''
resuming = False
skipping_deps = False
test_datadir = os.path.join('test','data_dir' + ('','')[bool(os.getenv('MMGEN_DEBUG_UTF8'))])
test_datadir = os.path.join('test', 'data_dir' + ('', '')[bool(os.getenv('MMGEN_DEBUG_UTF8'))])
mnemonic_entry_modes = {}
@ -273,12 +273,12 @@ class Config(Lockable):
)
_incompatible_opts = (
('help','longhelp'),
('bob','alice','carol'),
('label','keep_label'),
('tx_id','info'),
('tx_id','terse_info'),
('autosign','outdir'),
('help', 'longhelp'),
('bob', 'alice', 'carol'),
('label', 'keep_label'),
('tx_id', 'info'),
('tx_id', 'terse_info'),
('autosign', 'outdir'),
)
_cfg_file_opts = (
@ -319,7 +319,7 @@ class Config(Lockable):
'ltc_ignore_daemon_version',
'xmr_ignore_daemon_version',
'eth_mainnet_chain_names',
'eth_testnet_chain_names' )
'eth_testnet_chain_names')
# Supported environmental vars
# The corresponding attributes (lowercase, without 'mmgen_') must exist in the class.
@ -377,10 +377,10 @@ class Config(Lockable):
'contract_data',
)
# Auto-typechecked and auto-set opts - first value in list is the default
_ov = namedtuple('autoset_opt_info',['type','choices'])
_ov = namedtuple('autoset_opt_info', ['type', 'choices'])
_autoset_opts = {
'fee_estimate_mode': _ov('nocase_pfx', ['conservative','economical']),
'rpc_backend': _ov('nocase_pfx', ['auto','httplib','curl','aiohttp','requests']),
'fee_estimate_mode': _ov('nocase_pfx', ['conservative', 'economical']),
'rpc_backend': _ov('nocase_pfx', ['auto', 'httplib', 'curl', 'aiohttp', 'requests']),
}
_auto_typeset_opts = {
@ -414,13 +414,13 @@ class Config(Lockable):
"""
location of mmgen.cfg
"""
if not hasattr(self,'_data_dir_root'):
if not hasattr(self, '_data_dir_root'):
if self._data_dir_root_override:
self._data_dir_root = os.path.normpath(os.path.abspath(self._data_dir_root_override))
elif self.test_suite:
self._data_dir_root = self.test_datadir
else:
self._data_dir_root = os.path.join(gc.home_dir,'.'+gc.proj_name.lower())
self._data_dir_root = os.path.join(gc.home_dir, '.'+gc.proj_name.lower())
return self._data_dir_root
@property
@ -428,12 +428,12 @@ class Config(Lockable):
"""
location of wallet and other data - same as data_dir_root for mainnet
"""
if not hasattr(self,'_data_dir'):
if not hasattr(self, '_data_dir'):
self._data_dir = os.path.normpath(os.path.join(*{
'regtest': (self.data_dir_root, 'regtest', (self.regtest_user or 'none')),
'testnet': (self.data_dir_root, 'testnet'),
'mainnet': (self.data_dir_root,),
}[self.network] ))
}[self.network]))
return self._data_dir
def __init__(
@ -457,7 +457,7 @@ class Config(Lockable):
if opts_data or parsed_opts or process_opts:
assert cfg is None, (
'Config(): ‘cfg’ cannot be used simultaneously with ' +
'‘opts_data’, ‘parsed_opts’ or ‘process_opts’' )
'‘opts_data’, ‘parsed_opts’ or ‘process_opts’')
from .opts import UserOpts
UserOpts(
cfg = self,
@ -472,26 +472,26 @@ class Config(Lockable):
self._uopts = {}
else:
if '_clone' in cfg:
assert isinstance( cfg['_clone'], Config )
assert isinstance(cfg['_clone'], Config)
self._cloned = cfg['_clone'].__dict__
for k,v in self._cloned.items():
for k, v in self._cloned.items():
if not k.startswith('_'):
setattr(self,k,v)
setattr(self, k, v)
del cfg['_clone']
self._uopts = cfg
self._uopt_desc = 'configuration option'
self._data_dir_root_override = self._cloned.pop(
'_data_dir_root_override',
self._uopts.pop('data_dir',None))
self._uopts.pop('data_dir', None))
if parse_only and not any(k in self._uopts for k in ['help','longhelp']):
if parse_only and not any(k in self._uopts for k in ['help', 'longhelp']):
return
# Step 2: set cfg from user-supplied data, skipping auto opts; set type from corresponding
# class attribute, if it exists:
auto_opts = tuple(self._autoset_opts) + tuple(self._auto_typeset_opts)
for key,val in self._uopts.items():
for key, val in self._uopts.items():
assert key.isascii() and key.isidentifier() and key[0] != '_', (
f'{key!r}: malformed configuration option')
assert key not in self._forbidden_opts, (
@ -524,13 +524,13 @@ class Config(Lockable):
# Step 4: set cfg from cfgfile, skipping already-set opts and auto opts; save set opts and auto
# opts to be set:
# requires ‘data_dir_root’, ‘test_suite_cfgtest’
self._cfgfile_opts = self._set_cfg_from_cfg_file( self._envopts, need_proto )
self._cfgfile_opts = self._set_cfg_from_cfg_file(self._envopts, need_proto)
# Step 5: set autoset opts from user-supplied data, cfgfile data, or default values, in that order:
self._set_autoset_opts( self._cfgfile_opts.autoset )
self._set_autoset_opts(self._cfgfile_opts.autoset)
# Step 6: set auto typeset opts from user-supplied data or cfgfile data, in that order:
self._set_auto_typeset_opts( self._cfgfile_opts.auto_typeset )
self._set_auto_typeset_opts(self._cfgfile_opts.auto_typeset)
if self.regtest or self.bob or self.alice or self.carol or gc.prog_name == f'{gc.proj_id}-regtest':
self.network = 'regtest'
@ -567,7 +567,7 @@ class Config(Lockable):
if need_proto:
from .protocol import init_proto_from_cfg, warn_trustlevel
# requires the default-to-none behavior, so do after the lock:
self._proto = init_proto_from_cfg(self,need_amt=need_amt)
self._proto = init_proto_from_cfg(self, need_amt=need_amt)
if self._opts and not caller_post_init:
self._post_init()
@ -590,41 +590,38 @@ class Config(Lockable):
sys.exit(1) # called only on bad invocation
def _set_cfg_from_env(self):
for name,val in ((k,v) for k,v in os.environ.items() if k.startswith('MMGEN_')):
for name, val in ((k, v) for k, v in os.environ.items() if k.startswith('MMGEN_')):
if name == 'MMGEN_DEBUG_ALL':
continue
if name in self._env_opts:
if val: # ignore empty string values; string value of '0' or 'false' sets variable to False
disable = name.startswith('MMGEN_DISABLE_')
gname = name[(6,14)[disable]:].lower()
gname = name[(6, 14)[disable]:].lower()
if gname in self._uopts: # don’t touch attr if already set by user
continue
if hasattr(self,gname):
if hasattr(self, gname):
setattr(
self,
gname,
conv_type( name, val, getattr(self,gname), 'environment var', invert_bool=disable ))
conv_type(name, val, getattr(self, gname), 'environment var', invert_bool=disable))
yield gname
else:
raise ValueError(f'Name {gname!r} not present in globals')
else:
raise ValueError(f'{name!r} is not a valid MMGen environment variable')
def _set_cfg_from_cfg_file(
self,
env_cfg,
need_proto ):
def _set_cfg_from_cfg_file(self, env_cfg, need_proto):
_ret = namedtuple('cfgfile_opts',['non_auto','autoset','auto_typeset'])
_ret = namedtuple('cfgfile_opts', ['non_auto', 'autoset', 'auto_typeset'])
if not self._use_cfg_file:
return _ret( (), {}, {} )
return _ret((), {}, {})
# check for changes in system template file (term must be initialized)
from .cfgfile import mmgen_cfg_file
mmgen_cfg_file(self,'sample')
mmgen_cfg_file(self, 'sample')
ucfg = mmgen_cfg_file(self,'usr')
ucfg = mmgen_cfg_file(self, 'usr')
self._cfgfile_fn = ucfg.fn
@ -642,9 +639,9 @@ class Config(Lockable):
if ns[0] in gc.core_coins:
if not need_proto:
continue
nse,tn = (
(ns[2:],ns[1]=='testnet') if len(ns) > 2 and ns[1] in ('mainnet','testnet') else
(ns[1:],False)
nse, tn = (
(ns[2:], ns[1]=='testnet') if len(ns) > 2 and ns[1] in ('mainnet', 'testnet') else
(ns[1:], False)
)
# no instance yet, so override _class_ attr:
cls = init_proto(self, ns[0], tn, need_amt=True, return_cls=True)
@ -652,26 +649,26 @@ class Config(Lockable):
else:
cls = self
attr = d.name
refval = getattr(cls,attr)
val = ucfg.parse_value(d.value,refval)
refval = getattr(cls, attr)
val = ucfg.parse_value(d.value, refval)
if not val:
die( 'CfgFileParseError', f'Parse error in file {ucfg.fn!r}, line {d.lineno}' )
val_conv = conv_type( attr, val, refval, 'configuration file option', src=ucfg.fn )
die('CfgFileParseError', f'Parse error in file {ucfg.fn!r}, line {d.lineno}')
val_conv = conv_type(attr, val, refval, 'configuration file option', src=ucfg.fn)
if not attr in already_set:
setattr(cls,attr,val_conv)
setattr(cls, attr, val_conv)
non_auto_opts.append(attr)
elif d.name in self._autoset_opts:
autoset_opts[d.name] = d.value
elif d.name in self._auto_typeset_opts:
auto_typeset_opts[d.name] = d.value
else:
die( 'CfgFileParseError', f'{d.name!r}: unrecognized option in {ucfg.fn!r}, line {d.lineno}' )
die('CfgFileParseError', f'{d.name!r}: unrecognized option in {ucfg.fn!r}, line {d.lineno}')
return _ret( tuple(non_auto_opts), autoset_opts, auto_typeset_opts )
return _ret(tuple(non_auto_opts), autoset_opts, auto_typeset_opts)
def _set_autoset_opts(self,cfgfile_autoset_opts):
def _set_autoset_opts(self, cfgfile_autoset_opts):
def get_autoset_opt(key,val,src):
def get_autoset_opt(key, val, src):
def die_on_err(desc):
from .util import fmt_list
@ -680,11 +677,11 @@ class Config(Lockable):
'{a!r}: invalid {b} (not {c}: {d})'.format(
a = val,
b = {
'cmdline': f'parameter for option --{key.replace("_","-")}',
'cmdline': f'parameter for option --{key.replace("_", "-")}',
'cfgfile': f'value for cfg file option {key!r}'
}[src],
c = desc,
d = fmt_list(data.choices) ))
d = fmt_list(data.choices)))
class opt_type:
@ -703,7 +700,7 @@ class Config(Lockable):
data = self._autoset_opts[key]
return getattr(opt_type,data.type)()
return getattr(opt_type, data.type)()
# Check autoset opts, setting if unset
for key in self._autoset_opts:
@ -711,27 +708,27 @@ class Config(Lockable):
if key in self._cloned:
continue
assert not hasattr(self,key), f'autoset opt {key!r} is already set, but it shouldn’t be!'
assert not hasattr(self, key), f'autoset opt {key!r} is already set, but it shouldn’t be!'
if key in self._uopts:
val,src = (self._uopts[key],'cmdline')
val, src = (self._uopts[key], 'cmdline')
elif key in cfgfile_autoset_opts:
val,src = (cfgfile_autoset_opts[key],'cfgfile')
val, src = (cfgfile_autoset_opts[key], 'cfgfile')
else:
val = None
if val is None:
setattr(self, key, self._autoset_opts[key].choices[0])
else:
setattr(self, key, get_autoset_opt(key,val,src=src))
setattr(self, key, get_autoset_opt(key, val, src=src))
def _set_auto_typeset_opts(self,cfgfile_auto_typeset_opts):
def _set_auto_typeset_opts(self, cfgfile_auto_typeset_opts):
def do_set(key,val,ref_type):
assert not hasattr(self,key), f'{key!r} is in cfg!'
setattr(self,key,None if val is None else ref_type(val))
def do_set(key, val, ref_type):
assert not hasattr(self, key), f'{key!r} is in cfg!'
setattr(self, key, None if val is None else ref_type(val))
for key,ref_type in self._auto_typeset_opts.items():
for key, ref_type in self._auto_typeset_opts.items():
if key in self._uopts:
do_set(key, self._uopts[key], ref_type)
elif key in cfgfile_auto_typeset_opts:
@ -739,18 +736,18 @@ class Config(Lockable):
def _die_on_incompatible_opts(self):
for group in self._incompatible_opts:
bad = [k for k in self.__dict__ if k in group and getattr(self,k) is not None]
bad = [k for k in self.__dict__ if k in group and getattr(self, k) is not None]
if len(bad) > 1:
die(1,'Conflicting options: {}'.format(', '.join(map(fmt_opt,bad))))
die(1, 'Conflicting options: {}'.format(', '.join(map(fmt_opt, bad))))
def _set_quiet(self,val):
def _set_quiet(self, val):
from .util import Util
self.__dict__['quiet'] = val
self.__dict__['_util'] = Util(self) # qmsg, qmsg_r
def check_opts(cfg): # Raises exception if any check fails
from .util import is_int,Msg
from .util import is_int, Msg
def get_desc(desc_pfx=''):
return (
@ -760,18 +757,18 @@ def check_opts(cfg): # Raises exception if any check fails
if name in cfg._uopts and 'command-line' in cfg._uopt_desc else
f'value for configuration option {name!r}'
)
+ ( ' from environment' if name in cfg._envopts else '')
+ (' from environment' if name in cfg._envopts else '')
+ (f' in {cfg._cfgfile_fn!r}' if name in cfg._cfgfile_opts.non_auto else '')
)
def display_opt(name,val='',beg='For selected',end=':\n'):
def display_opt(name, val='', beg='For selected', end=':\n'):
from .util import msg_r
msg_r('{} option {!r}{}'.format(
beg,
f'{fmt_opt(name)}={val}' if val else fmt_opt(name),
end ))
end))
def opt_compares(val,op_str,target):
def opt_compares(val, op_str, target):
import operator
if not {
'<': operator.lt,
@ -779,24 +776,24 @@ def check_opts(cfg): # Raises exception if any check fails
'>': operator.gt,
'>=': operator.ge,
'=': operator.eq,
}[op_str](val,target):
die( 'UserOptError', f'{val}: invalid {get_desc()} (not {op_str} {target})' )
}[op_str](val, target):
die('UserOptError', f'{val}: invalid {get_desc()} (not {op_str} {target})')
def opt_is_int(val,desc_pfx=''):
def opt_is_int(val, desc_pfx=''):
if not is_int(val):
die( 'UserOptError', f'{val!r}: invalid {get_desc(desc_pfx)} (not an integer)' )
die('UserOptError', f'{val!r}: invalid {get_desc(desc_pfx)} (not an integer)')
def opt_is_in_list(val,tlist,desc_pfx=''):
def opt_is_in_list(val, tlist, desc_pfx=''):
if val not in tlist:
q,sep = (('',','),("'","','"))[isinstance(tlist[0],str)]
die( 'UserOptError', '{q}{v}{q}: invalid {w}\nValid choices: {q}{o}{q}'.format(
q, sep = (('', ','), ("'", "','"))[isinstance(tlist[0], str)]
die('UserOptError', '{q}{v}{q}: invalid {w}\nValid choices: {q}{o}{q}'.format(
v = val,
w = get_desc(desc_pfx),
q = q,
o = sep.join(map(str,sorted(tlist))) ))
o = sep.join(map(str, sorted(tlist)))))
def opt_unrecognized():
die( 'UserOptError', f'{val!r}: unrecognized {get_desc()}' )
die('UserOptError', f'{val!r}: unrecognized {get_desc()}')
class check_funcs:
@ -808,32 +805,32 @@ def check_opts(cfg): # Raises exception if any check fails
if name == 'out_fmt':
p = 'hidden_incog_output_params'
if wd.type == 'incog_hidden' and not getattr(cfg,p):
die( 'UserOptError',
if wd.type == 'incog_hidden' and not getattr(cfg, p):
die('UserOptError',
'Hidden incog format output requested. ' +
f'You must supply a file and offset with the {fmt_opt(p)!r} option' )
f'You must supply a file and offset with the {fmt_opt(p)!r} option')
if wd.base_type == 'incog_base' and cfg.old_incog_fmt:
display_opt(name,val,beg='Selected',end=' ')
display_opt('old_incog_fmt',beg='conflicts with',end=':\n')
die( 'UserOptError', 'Export to old incog wallet format unsupported' )
display_opt(name, val, beg='Selected', end=' ')
display_opt('old_incog_fmt', beg='conflicts with', end=':\n')
die('UserOptError', 'Export to old incog wallet format unsupported')
elif wd.type == 'brain':
die( 'UserOptError', 'Output to brainwallet format unsupported' )
die('UserOptError', 'Output to brainwallet format unsupported')
out_fmt = in_fmt
def hidden_incog_input_params():
a = val.rsplit(',',1) # permit comma in filename
a = val.rsplit(',', 1) # permit comma in filename
if len(a) != 2:
display_opt(name,val)
die( 'UserOptError', 'Option requires two comma-separated arguments' )
display_opt(name, val)
die('UserOptError', 'Option requires two comma-separated arguments')
fn,offset = a
fn, offset = a
opt_is_int(offset)
from .fileutil import check_infile,check_outdir,check_outfile
from .fileutil import check_infile, check_outdir, check_outfile
if name == 'hidden_incog_input_params':
check_infile(fn,blkdev_ok=True)
check_infile(fn, blkdev_ok=True)
key2 = 'in_fmt'
else:
try:
@ -843,52 +840,52 @@ def check_opts(cfg): # Raises exception if any check fails
if b:
check_outdir(b)
else:
check_outfile(fn,blkdev_ok=True)
check_outfile(fn, blkdev_ok=True)
key2 = 'out_fmt'
if hasattr(cfg,key2):
val2 = getattr(cfg,key2)
if hasattr(cfg, key2):
val2 = getattr(cfg, key2)
from .wallet import get_wallet_data
wd = get_wallet_data('incog_hidden')
if val2 and val2 not in wd.fmt_codes:
die( 'UserOptError', f'Option conflict:\n {fmt_opt(name)}, with\n {fmt_opt(key2)}={val2}' )
die('UserOptError', f'Option conflict:\n {fmt_opt(name)}, with\n {fmt_opt(key2)}={val2}')
hidden_incog_output_params = hidden_incog_input_params
def subseeds():
from .subseed import SubSeedIdxRange
opt_compares(val,'>=',SubSeedIdxRange.min_idx)
opt_compares(val,'<=',SubSeedIdxRange.max_idx)
opt_compares(val, '>=', SubSeedIdxRange.min_idx)
opt_compares(val, '<=', SubSeedIdxRange.max_idx)
def seed_len():
from .seed import Seed
opt_is_in_list(int(val),Seed.lens)
opt_is_in_list(int(val), Seed.lens)
def hash_preset():
from .crypto import Crypto
opt_is_in_list(val,list(Crypto.hash_presets.keys()))
opt_is_in_list(val, list(Crypto.hash_presets.keys()))
def brain_params():
a = val.split(',')
if len(a) != 2:
display_opt(name,val)
die( 'UserOptError', 'Option requires two comma-separated arguments' )
display_opt(name, val)
die('UserOptError', 'Option requires two comma-separated arguments')
opt_is_int( a[0], desc_pfx='seed length' )
opt_is_int(a[0], desc_pfx='seed length')
from .seed import Seed
opt_is_in_list( int(a[0]), Seed.lens, desc_pfx='seed length' )
opt_is_in_list(int(a[0]), Seed.lens, desc_pfx='seed length')
from .crypto import Crypto
opt_is_in_list( a[1], list(Crypto.hash_presets.keys()), desc_pfx='hash preset' )
opt_is_in_list(a[1], list(Crypto.hash_presets.keys()), desc_pfx='hash preset')
def usr_randchars():
if val != 0:
opt_compares(val,'>=',cfg.min_urandchars)
opt_compares(val,'<=',cfg.max_urandchars)
opt_compares(val, '>=', cfg.min_urandchars)
opt_compares(val, '<=', cfg.max_urandchars)
def tx_confs():
opt_is_int(val)
opt_compares(int(val),'>=',1)
opt_compares(int(val), '>=', 1)
def vsize_adj():
from .util import ymsg
@ -896,19 +893,19 @@ def check_opts(cfg): # Raises exception if any check fails
def daemon_id():
from .daemon import CoinDaemon
opt_is_in_list(val,CoinDaemon.all_daemon_ids())
opt_is_in_list(val, CoinDaemon.all_daemon_ids())
def locktime():
opt_is_int(val)
opt_compares(int(val),'>',0)
opt_compares(int(val), '>', 0)
def columns():
opt_compares(val,'>',10)
opt_compares(val, '>', 10)
# TODO: add checks for token, rbf, tx_fee
check_funcs_names = tuple(check_funcs.__dict__)
for name in tuple(cfg._uopts) + cfg._envopts + cfg._cfgfile_opts.non_auto:
val = getattr(cfg,name)
val = getattr(cfg, name)
if name in cfg._infile_opts:
from .fileutil import check_infile
check_infile(val) # file exists and is readable - dies on error
@ -916,19 +913,19 @@ def check_opts(cfg): # Raises exception if any check fails
from .fileutil import check_outdir
check_outdir(val) # dies on error
elif name in check_funcs_names:
getattr(check_funcs,name)()
getattr(check_funcs, name)()
elif cfg.debug:
Msg(f'check_opts(): No test for config opt {name!r}')
def fmt_opt(o):
return '--' + o.replace('_','-')
return '--' + o.replace('_', '-')
def opt_postproc_debug(cfg):
none_opts = [k for k in dir(cfg) if k[:2] != '__' and getattr(cfg,k) is None]
none_opts = [k for k in dir(cfg) if k[:2] != '__' and getattr(cfg, k) is None]
from .util import Msg
Msg('\n Configuration opts:')
for e in [d for d in dir(cfg) if d[:2] != '__']:
Msg(f' {e:<20}: {getattr(cfg,e)}')
Msg(f' {e:<20}: {getattr(cfg, e)}')
Msg(" Configuration opts set to 'None':")
Msg(' {}\n'.format('\n '.join(none_opts)))
Msg('\n=== end opts.py debug ===\n')
@ -939,21 +936,21 @@ def conv_type(
refval,
desc,
invert_bool = False,
src = None ):
src = None):
def do_fail():
die(1,'{a!r}: invalid value for {b} {c!r}{d} (must be of type {e!r})'.format(
die(1, '{a!r}: invalid value for {b} {c!r}{d} (must be of type {e!r})'.format(
a = val,
b = desc,
c = fmt_opt(name) if 'command-line' in desc else name,
d = f' in {src!r}' if src else '',
e = type(refval).__name__ ))
e = type(refval).__name__))
if type(refval) is bool:
v = str(val).lower()
ret = (
True if v in ('true','yes','1','on') else
False if v in ('false','no','none','0','off','') else
True if v in ('true', 'yes', '1', 'on') else
False if v in ('false', 'no', 'none', '0', 'off', '') else
None
)
return do_fail() if ret is None else (not ret) if invert_bool else ret

View file

@ -20,13 +20,13 @@
cfgfile: API for the MMGen runtime configuration file and related files
"""
import os,re
import os, re
from collections import namedtuple
from .cfg import gc
from .util import msg,ymsg,suf,fmt,fmt_list,oneshot_warning,strip_comment,capfirst,die
from .util import msg, ymsg, suf, fmt, fmt_list, oneshot_warning, strip_comment, capfirst, die
def mmgen_cfg_file(cfg,id_str):
def mmgen_cfg_file(cfg, id_str):
return cfg_file.get_cls_by_id(id_str)(cfg)
class cfg_file:
@ -35,62 +35,62 @@ class cfg_file:
write_ok = False
warn_missing = True
write_metadata = False
line_data = namedtuple('cfgfile_line',['name','value','lineno','chunk'])
line_data = namedtuple('cfgfile_line', ['name', 'value', 'lineno', 'chunk'])
fn_base = 'mmgen.cfg'
class warn_missing_file(oneshot_warning):
color = 'yellow' # has no effect, as color not initialized yet
message = '{} not found at {!r}'
def get_data(self,fn):
def get_data(self, fn):
try:
with open(fn) as fp:
return fp.read().splitlines()
except:
if self.warn_missing:
self.warn_missing_file( div=fn, fmt_args=(self.desc,fn) )
self.warn_missing_file(div=fn, fmt_args=(self.desc, fn))
return ''
def copy_system_data(self,fn):
def copy_system_data(self, fn):
assert self.write_ok, f'writing to file {fn!r} not allowed!'
src = mmgen_cfg_file(self.cfg,'sys')
src = mmgen_cfg_file(self.cfg, 'sys')
if src.data:
data = src.data + src.make_metadata() if self.write_metadata else src.data
try:
with open(fn,'w') as fp:
with open(fn, 'w') as fp:
fp.write('\n'.join(data)+'\n')
os.chmod(fn,0o600)
os.chmod(fn, 0o600)
except:
die(2,f'ERROR: unable to write to {fn!r}')
die(2, f'ERROR: unable to write to {fn!r}')
def parse_value(self,value,refval):
if isinstance(refval,dict):
m = re.fullmatch(r'((\s+\w+:\S+)+)',' '+value) # expect one or more colon-separated values
def parse_value(self, value, refval):
if isinstance(refval, dict):
m = re.fullmatch(r'((\s+\w+:\S+)+)', ' '+value) # expect one or more colon-separated values
if m:
return dict([i.split(':') for i in m[1].split()])
elif isinstance(refval,(list,tuple)):
m = re.fullmatch(r'((\s+\S+)+)',' '+value) # expect single value or list
elif isinstance(refval, (list, tuple)):
m = re.fullmatch(r'((\s+\S+)+)', ' '+value) # expect single value or list
if m:
ret = m[1].split()
return ret if isinstance(refval,list) else tuple(ret)
return ret if isinstance(refval, list) else tuple(ret)
else:
return value
def get_lines(self):
def gen_lines():
for lineno,line in enumerate(self.data,1):
for lineno, line in enumerate(self.data, 1):
line = strip_comment(line)
if line == '':
continue
m = re.fullmatch(r'(\w+)(\s+)(.*)',line)
m = re.fullmatch(r'(\w+)(\s+)(.*)', line)
if m:
yield self.line_data(m[1],m[3],lineno,None)
yield self.line_data(m[1], m[3], lineno, None)
else:
die( 'CfgFileParseError', f'Parse error in file {self.fn!r}, line {lineno}' )
die('CfgFileParseError', f'Parse error in file {self.fn!r}, line {lineno}')
return gen_lines()
@classmethod
def get_cls_by_id(cls,id_str):
def get_cls_by_id(cls, id_str):
d = {
'usr': CfgFileUsr,
'sys': CfgFileSampleSys,
@ -101,13 +101,13 @@ class cfg_file:
class cfg_file_sample(cfg_file):
@classmethod
def cls_make_metadata(cls,data):
def cls_make_metadata(cls, data):
return [f'# Version {cls.cur_ver} {cls.compute_chksum(data)}']
@staticmethod
def compute_chksum(data):
import hashlib
return hashlib.new('ripemd160','\n'.join(data).encode()).hexdigest()
return hashlib.new('ripemd160', '\n'.join(data).encode()).hexdigest()
@property
def computed_chksum(self):
@ -124,19 +124,19 @@ class cfg_file_sample(cfg_file):
- last line is metadata line of the form '# Version VER_NUM HASH'
"""
def process_chunk(chunk,lineno):
m = re.fullmatch(r'(#\s*)(\w+)(\s+)(.*)',chunk[-1])
def process_chunk(chunk, lineno):
m = re.fullmatch(r'(#\s*)(\w+)(\s+)(.*)', chunk[-1])
if m:
return self.line_data(m[2],m[4],lineno,chunk)
return self.line_data(m[2], m[4], lineno, chunk)
else:
die( 'CfgFileParseError', f'Parse error in file {self.fn!r}, line {lineno}' )
die('CfgFileParseError', f'Parse error in file {self.fn!r}, line {lineno}')
def gen_chunks(lines):
hdr = True
chunk = []
in_chunk = False
for lineno,line in enumerate(lines,1):
for lineno, line in enumerate(lines, 1):
if line.startswith('##'):
hdr = False
@ -150,17 +150,17 @@ class cfg_file_sample(cfg_file):
elif line.startswith('#'):
if in_chunk is False:
if chunk:
yield process_chunk(chunk,last_nonblank)
yield process_chunk(chunk, last_nonblank)
chunk = [line]
in_chunk = True
else:
chunk.append(line)
last_nonblank = lineno
else:
die( 'CfgFileParseError', f'Parse error in file {self.fn!r}, line {lineno}' )
die('CfgFileParseError', f'Parse error in file {self.fn!r}, line {lineno}')
if chunk:
yield process_chunk(chunk,last_nonblank)
yield process_chunk(chunk, last_nonblank)
return list(gen_chunks(self.data))
@ -169,9 +169,9 @@ class CfgFileUsr(cfg_file):
warn_missing = False
write_ok = True
def __init__(self,cfg):
def __init__(self, cfg):
self.cfg = cfg
self.fn = os.path.join(cfg.data_dir_root,self.fn_base)
self.fn = os.path.join(cfg.data_dir_root, self.fn_base)
self.data = self.get_data(self.fn)
if not self.data:
self.copy_system_data(self.fn)
@ -180,15 +180,15 @@ class CfgFileSampleSys(cfg_file_sample):
desc = 'system sample configuration file'
test_fn_subdir = 'usr.local.share'
def __init__(self,cfg):
def __init__(self, cfg):
self.cfg = cfg
if self.cfg.test_suite_cfgtest:
self.fn = os.path.join(cfg.data_dir_root,self.test_fn_subdir,self.fn_base)
self.fn = os.path.join(cfg.data_dir_root, self.test_fn_subdir, self.fn_base)
with open(self.fn) as fp:
self.data = fp.read().splitlines()
else:
# self.fn is used for error msgs only, so file need not exist on filesystem
self.fn = os.path.join(os.path.dirname(__file__),'data',self.fn_base)
self.fn = os.path.join(os.path.dirname(__file__), 'data', self.fn_base)
self.data = gc.get_mmgen_data_file(self.fn_base).splitlines()
def make_metadata(self):
@ -204,12 +204,12 @@ class CfgFileSampleUsr(cfg_file_sample):
out_of_date_fs = 'File {!r} is out of date - replacing'
altered_by_user_fs = 'File {!r} was altered by user - replacing'
def __init__(self,cfg):
def __init__(self, cfg):
self.cfg = cfg
self.fn = os.path.join(cfg.data_dir_root,f'{self.fn_base}.sample')
self.fn = os.path.join(cfg.data_dir_root, f'{self.fn_base}.sample')
self.data = self.get_data(self.fn)
src = mmgen_cfg_file(cfg,'sys')
src = mmgen_cfg_file(cfg, 'sys')
if not src.data:
return
@ -217,7 +217,7 @@ class CfgFileSampleUsr(cfg_file_sample):
if self.data:
if self.parse_metadata():
if self.chksum == self.computed_chksum:
diff = self.diff(self.get_lines(),src.get_lines())
diff = self.diff(self.get_lines(), src.get_lines())
if not diff:
return
self.show_changes(diff)
@ -230,14 +230,14 @@ class CfgFileSampleUsr(cfg_file_sample):
def parse_metadata(self):
if self.data:
m = re.match(r'# Version (\d+) ([a-f0-9]{40})$',self.data[-1])
m = re.match(r'# Version (\d+) ([a-f0-9]{40})$', self.data[-1])
if m:
self.ver = m[1]
self.chksum = m[2]
self.data = self.data[:-1] # remove metadata line
return True
def diff(self,a_tup,b_tup): # a=user, b=system
def diff(self, a_tup, b_tup): # a=user, b=system
a = [i.name for i in a_tup]#[3:] # Debug
b = [i.name for i in b_tup]#[:-2] # Debug
removed = set(a) - set(b)
@ -250,34 +250,34 @@ class CfgFileSampleUsr(cfg_file_sample):
else:
return None
def show_changes(self,diff):
def show_changes(self, diff):
ymsg('Warning: configuration file options have changed!\n')
for desc in ('added','removed'):
for desc in ('added', 'removed'):
data = diff[desc]
if data:
opts = fmt_list([i.name for i in data],fmt='bare')
msg(f' The following option{suf(data,verb="has")} been {desc}:\n {opts}\n')
opts = fmt_list([i.name for i in data], fmt='bare')
msg(f' The following option{suf(data, verb="has")} been {desc}:\n {opts}\n')
if desc == 'removed' and data:
uc = mmgen_cfg_file(self.cfg,'usr')
uc = mmgen_cfg_file(self.cfg, 'usr')
usr_names = [i.name for i in uc.get_lines()]
rm_names = [i.name for i in data]
bad = sorted(set(usr_names).intersection(rm_names))
if bad:
m = f"""
The following removed option{suf(bad,verb='is')} set in {uc.fn!r}
The following removed option{suf(bad, verb='is')} set in {uc.fn!r}
and must be deleted or commented out:
{' ' + fmt_list(bad,fmt='bare')}
{' ' + fmt_list(bad, fmt='bare')}
"""
ymsg(fmt(m,indent=' ',strip_char='\t'))
ymsg(fmt(m, indent=' ', strip_char='\t'))
from .ui import keypress_confirm,do_pager
from .ui import keypress_confirm, do_pager
while True:
if not keypress_confirm( self.cfg, self.details_confirm_prompt, no_nl=True ):
if not keypress_confirm(self.cfg, self.details_confirm_prompt, no_nl=True):
return
def get_details():
for desc,data in diff.items():
sep,sep2 = ('\n ','\n\n ')
for desc, data in diff.items():
sep, sep2 = ('\n ', '\n\n ')
if data:
yield (
f'{capfirst(desc)} section{suf(data)}:'

View file

@ -21,27 +21,27 @@ color: color handling for the MMGen suite
"""
_colors = {
'black': ( 232, (30,0) ),
'red': ( 210, (31,1) ),
'green': ( 121, (32,1) ),
'yellow': ( 229, (33,1) ),
'blue': ( 75, (34,1) ),
'magenta': ( 205, (35,1) ),
'cyan': ( 122, (36,1) ),
'black': (232, (30, 0)),
'red': (210, (31, 1)),
'green': (121, (32, 1)),
'yellow': (229, (33, 1)),
'blue': (75, (34, 1)),
'magenta': (205, (35, 1)),
'cyan': (122, (36, 1)),
'gray': ( 246, (30,1) ),
'orange': ( 216, (31,1) ),
'purple': ( 141, (35,1) ),
'pink': ( 218, (35,1) ),
'gray': (246, (30, 1)),
'orange': (216, (31, 1)),
'purple': (141, (35, 1)),
'pink': (218, (35, 1)),
'melon': ( 222, (33,1) ),
'brown': ( 173, (33,0) ),
'grndim': ( 108, (32,0) ),
'melon': (222, (33, 1)),
'brown': (173, (33, 0)),
'grndim': (108, (32, 0)),
'redbg': ( (232,210), (30,101) ),
'grnbg': ( (232,121), (30,102) ),
'yelbg': ( (232,229), (30,103) ),
'blubg': ( (232,75), (30,104) ),
'redbg': ((232, 210), (30, 101)),
'grnbg': ((232, 121), (30, 102)),
'yelbg': ((232, 229), (30, 103)),
'blubg': ((232, 75), (30, 104)),
}
def nocolor(s):
@ -52,16 +52,16 @@ def set_vt100():
import sys
if sys.platform == 'win32':
from subprocess import run
run([],shell=True)
run([], shell=True)
def get_terminfo_colors(term=None):
from subprocess import run,PIPE
cmd = ['infocmp','-0']
from subprocess import run, PIPE
cmd = ['infocmp', '-0']
if term:
cmd.append(term)
try:
cmdout = run(cmd,stdout=PIPE,check=True).stdout.decode()
cmdout = run(cmd, stdout=PIPE, check=True).stdout.decode()
except:
set_vt100()
return None
@ -72,12 +72,12 @@ def get_terminfo_colors(term=None):
if s.isdecimal():
return int(s)
elif s.startswith('0x') and is_hex_str(s[2:]):
return int(s[2:],16)
return int(s[2:], 16)
else:
return None
def init_color(num_colors='auto'):
assert num_colors in ('auto',8,16,256,0)
assert num_colors in ('auto', 8, 16, 256, 0)
import sys
self = sys.modules[__name__]
@ -98,19 +98,19 @@ def init_color(num_colors='auto'):
if num_colors == 0:
ncc = (lambda s: s).__code__
for c in _colors:
getattr(self,c).__code__ = ncc
getattr(self, c).__code__ = ncc
elif num_colors == 256:
for c,e in _colors.items():
for c, e in _colors.items():
start = (
'\033[38;5;{};1m'.format(e[0]) if type(e[0]) == int else
'\033[38;5;{};48;5;{};1m'.format(*e[0]) )
getattr(self,c).__code__ = eval(f'(lambda s: "{start}" + s + "{reset}").__code__')
elif num_colors in (8,16):
for c,e in _colors.items():
'\033[38;5;{};48;5;{};1m'.format(*e[0]))
getattr(self, c).__code__ = eval(f'(lambda s: "{start}" + s + "{reset}").__code__')
elif num_colors in (8, 16):
for c, e in _colors.items():
start = (
'\033[{}m'.format(e[1][0]) if e[1][1] == 0 else
'\033[{};{}m'.format(*e[1]) )
getattr(self,c).__code__ = eval(f'(lambda s: "{start}" + s + "{reset}").__code__')
'\033[{};{}m'.format(*e[1]))
getattr(self, c).__code__ = eval(f'(lambda s: "{start}" + s + "{reset}").__code__')
set_vt100()

View file

@ -24,15 +24,7 @@ import os
from collections import namedtuple
from .cfg import gc
from .util import (
msg,
msg_r,
ymsg,
fmt,
die,
make_chksum_8,
oneshot_warning,
)
from .util import msg, msg_r, ymsg, fmt, die, make_chksum_8, oneshot_warning
class Crypto:
@ -41,7 +33,7 @@ class Crypto:
salt_len = 16
aesctr_iv_len = 16
aesctr_dfl_iv = int.to_bytes(1,aesctr_iv_len,'big')
aesctr_dfl_iv = int.to_bytes(1, aesctr_iv_len, 'big')
hincog_chk_len = 8
mmenc_salt_len = 32
@ -50,7 +42,7 @@ class Crypto:
# Scrypt params: 'id_num': [N, r, p] (N is an exponent of two)
# NB: hashlib.scrypt in Python (>=v3.6) supports max N value of 14. This means that
# for hash presets > 3 the standalone scrypt library must be used!
_hp = namedtuple('scrypt_preset',['N','r','p'])
_hp = namedtuple('scrypt_preset', ['N', 'r', 'p'])
hash_presets = {
'1': _hp(12, 8, 1),
'2': _hp(13, 8, 4),
@ -63,60 +55,60 @@ class Crypto:
class pwfile_reuse_warning(oneshot_warning):
message = 'Reusing passphrase from file {!r} at user request'
def __init__(self,fn):
oneshot_warning.__init__(self,div=fn,fmt_args=[fn],reverse=True)
def __init__(self, fn):
oneshot_warning.__init__(self, div=fn, fmt_args=[fn], reverse=True)
def pwfile_used(self,passwd_file):
if hasattr(self,'_pwfile_used'):
def pwfile_used(self, passwd_file):
if hasattr(self, '_pwfile_used'):
self.pwfile_reuse_warning(passwd_file)
return True
else:
self._pwfile_used = True
return False
def __init__(self,cfg):
def __init__(self, cfg):
self.cfg = cfg
self.util = cfg._util
def get_hash_params(self,hash_preset):
def get_hash_params(self, hash_preset):
if hash_preset in self.hash_presets:
return self.hash_presets[hash_preset] # N,r,p
return self.hash_presets[hash_preset] # N, r, p
else: # Shouldn't be here
die(3,f"{hash_preset}: invalid 'hash_preset' value")
die(3, f"{hash_preset}: invalid 'hash_preset' value")
def sha256_rounds(self,s):
def sha256_rounds(self, s):
from hashlib import sha256
for _ in range(self.scramble_hash_rounds):
s = sha256(s).digest()
return s
def scramble_seed(self,seed,scramble_key):
def scramble_seed(self, seed, scramble_key):
import hmac
step1 = hmac.digest(seed,scramble_key,'sha256')
step1 = hmac.digest(seed, scramble_key, 'sha256')
if self.cfg.debug:
msg(f'Seed: {seed.hex()!r}\nScramble key: {scramble_key}\nScrambled seed: {step1.hex()}\n')
return self.sha256_rounds(step1)
def encrypt_seed(self,data,key,desc='seed'):
return self.encrypt_data(data,key,desc=desc)
def encrypt_seed(self, data, key, desc='seed'):
return self.encrypt_data(data, key, desc=desc)
def decrypt_seed(self,enc_seed,key,seed_id,key_id):
def decrypt_seed(self, enc_seed, key, seed_id, key_id):
self.util.vmsg_r('Checking key...')
chk1 = make_chksum_8(key)
if key_id:
if not self.util.compare_chksums(key_id,'key ID',chk1,'computed'):
if not self.util.compare_chksums(key_id, 'key ID', chk1, 'computed'):
msg('Incorrect passphrase or hash preset')
return False
dec_seed = self.decrypt_data(enc_seed,key,desc='seed')
dec_seed = self.decrypt_data(enc_seed, key, desc='seed')
chk2 = make_chksum_8(dec_seed)
if seed_id:
if self.util.compare_chksums(seed_id,'Seed ID',chk2,'decrypted seed'):
if self.util.compare_chksums(seed_id, 'Seed ID', chk2, 'decrypted seed'):
self.util.qmsg('Passphrase is OK')
else:
if not self.cfg.debug:
msg_r('Checking key ID...')
if self.util.compare_chksums(key_id,'key ID',chk1,'computed'):
if self.util.compare_chksums(key_id, 'key ID', chk1, 'computed'):
msg('Key ID is correct but decryption of seed failed')
else:
msg('Incorrect passphrase or hash preset')
@ -130,26 +122,26 @@ class Crypto:
self,
data,
key,
iv = aesctr_dfl_iv,
desc = 'data',
iv = aesctr_dfl_iv,
desc = 'data',
verify = True,
silent = False ):
silent = False):
from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
if not silent:
self.util.vmsg(f'Encrypting {desc}')
c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
c = Cipher(algorithms.AES(key), modes.CTR(iv), backend=default_backend())
encryptor = c.encryptor()
enc_data = encryptor.update(data) + encryptor.finalize()
if verify:
self.util.vmsg_r(f'Performing a test decryption of the {desc}...')
c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
c = Cipher(algorithms.AES(key), modes.CTR(iv), backend=default_backend())
encryptor = c.encryptor()
dec_data = encryptor.update(enc_data) + encryptor.finalize()
if dec_data != data:
die(2,f'ERROR.\nDecrypted {desc} doesn’t match original {desc}')
die(2, f'ERROR.\nDecrypted {desc} doesn’t match original {desc}')
if not silent:
self.util.vmsg('done')
@ -159,13 +151,13 @@ class Crypto:
self,
enc_data,
key,
iv = aesctr_dfl_iv,
desc = 'data' ):
iv = aesctr_dfl_iv,
desc = 'data'):
from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
self.util.vmsg_r(f'Decrypting {desc} with key...')
c = Cipher(algorithms.AES(key),modes.CTR(iv),backend=default_backend())
c = Cipher(algorithms.AES(key), modes.CTR(iv), backend=default_backend())
encryptor = c.encryptor()
return encryptor.update(enc_data) + encryptor.finalize()
@ -174,13 +166,13 @@ class Crypto:
passwd,
salt,
hash_preset,
buflen = 32 ):
buflen = 32):
# Buflen arg is for brainwallets only, which use this function to generate
# the seed directly.
ps = self.get_hash_params(hash_preset)
if isinstance(passwd,str):
if isinstance(passwd, str):
passwd = passwd.encode()
def do_hashlib_scrypt():
@ -192,7 +184,7 @@ class Crypto:
r = ps.r,
p = ps.p,
maxmem = 0,
dklen = buflen )
dklen = buflen)
def do_standalone_scrypt():
import scrypt
@ -202,7 +194,7 @@ class Crypto:
N = 2**ps.N,
r = ps.r,
p = ps.p,
buflen = buflen )
buflen = buflen)
if int(hash_preset) > 3:
msg_r('Hashing passphrase, please wait...')
@ -210,7 +202,7 @@ class Crypto:
# hashlib.scrypt doesn't support N > 14 (hash preset > 3)
ret = (
do_standalone_scrypt() if ps.N > 14 or self.cfg.force_standalone_scrypt_module else
do_hashlib_scrypt() )
do_hashlib_scrypt())
if int(hash_preset) > 3:
msg_r('\b'*34 + ' '*34 + '\b'*34)
@ -222,19 +214,19 @@ class Crypto:
passwd,
salt,
hash_preset,
desc = 'encryption key',
desc = 'encryption key',
from_what = 'passphrase',
verbose = False ):
verbose = False):
if self.cfg.verbose or verbose:
msg_r(f"Generating {desc}{' from ' + from_what if from_what else ''}...")
key = self.scrypt_hash_passphrase(passwd,salt,hash_preset)
key = self.scrypt_hash_passphrase(passwd, salt, hash_preset)
if self.cfg.verbose or verbose:
msg('done')
self.util.dmsg(f'Key: {key.hex()}')
return key
def _get_random_data_from_user(self,uchars=None,desc='data'):
def _get_random_data_from_user(self, uchars=None, desc='data'):
if uchars is None:
uchars = self.cfg.usr_randchars
@ -260,8 +252,8 @@ class Crypto:
"""
msg(f'Enter {uchars} random symbols' if self.cfg.quiet else
'\n' + fmt(info1,indent=' ') +
'\n' + fmt(info2) )
'\n' + fmt(info1, indent=' ') +
'\n' + fmt(info2))
import time
from .term import get_char_raw
@ -272,7 +264,7 @@ class Crypto:
key_data += get_char_raw(f'\rYou may begin typing. {uchars-i} symbols left: ')
time_data.append(time.time())
msg_r( '\r' if self.cfg.quiet else f'\rThank you. That’s enough.{" "*18}\n\n' )
msg_r('\r' if self.cfg.quiet else f'\rThank you. That’s enough.{" "*18}\n\n')
time_data = [f'{t:.22f}'.rstrip('0') for t in time_data]
@ -287,24 +279,24 @@ class Crypto:
msg(f'USER ENTROPY (user input + keystroke timings):\n{ret}')
from .ui import line_input
line_input( self.cfg, 'User random data successfully acquired. Press ENTER to continue: ' )
line_input(self.cfg, 'User random data successfully acquired. Press ENTER to continue: ')
return ret.encode()
def get_random(self,length):
def get_random(self, length):
os_rand = os.urandom(length)
assert len(os_rand) == length, f'OS random number generator returned {len(os_rand)} (!= {length}) bytes!'
return self.add_user_random(
rand_bytes = os_rand,
desc = 'from your operating system' )
desc = 'from your operating system')
def add_user_random(
self,
rand_bytes,
desc,
urand = {'data':b'', 'counter':0} ):
urand = {'data':b'', 'counter':0}):
assert type(rand_bytes) is bytes, 'add_user_random_chk1'
@ -323,12 +315,12 @@ class Crypto:
import hmac
key = hmac.digest(
urand['data'],
os_rand + int.to_bytes(urand['counter'],8,'big'),
'sha256' )
os_rand + int.to_bytes(urand['counter'], 8, 'big'),
'sha256')
msg(f'Encrypting random data {desc} with ephemeral key #{urand["counter"]}')
return self.encrypt_data( data=rand_bytes, key=key, desc=desc, verify=False, silent=True )
return self.encrypt_data(data=rand_bytes, key=key, desc=desc, verify=False, silent=True)
else:
return rand_bytes
@ -336,15 +328,15 @@ class Crypto:
self,
old_preset = gc.dfl_hash_preset,
data_desc = 'data',
prompt = None ):
prompt = None):
prompt = prompt or (
f'Enter hash preset for {data_desc},\n' +
f'or hit ENTER to accept the default value ({old_preset!r}): ' )
f'Enter hash preset for {data_desc}, \n' +
f'or hit ENTER to accept the default value ({old_preset!r}): ')
from .ui import line_input
while True:
ret = line_input( self.cfg, prompt )
ret = line_input(self.cfg, prompt)
if ret:
if ret in self.hash_presets:
return ret
@ -353,7 +345,7 @@ class Crypto:
else:
return old_preset
def get_new_passphrase(self,data_desc,hash_preset,passwd_file,pw_desc='passphrase'):
def get_new_passphrase(self, data_desc, hash_preset, passwd_file, pw_desc='passphrase'):
message = f"""
You must choose a passphrase to encrypt your {data_desc} with.
A key will be generated from your passphrase using a hash preset of '{hash_preset}'.
@ -366,72 +358,72 @@ class Crypto:
cfg = self.cfg,
infile = passwd_file,
desc = f'{pw_desc} for {data_desc}',
quiet = self.pwfile_used(passwd_file) ))
quiet = self.pwfile_used(passwd_file)))
else:
self.util.qmsg('\n'+fmt(message,indent=' '))
self.util.qmsg('\n'+fmt(message, indent=' '))
from .ui import get_words_from_user
if self.cfg.echo_passphrase:
pw = ' '.join(get_words_from_user( self.cfg, f'Enter {pw_desc} for {data_desc}: ' ))
pw = ' '.join(get_words_from_user(self.cfg, f'Enter {pw_desc} for {data_desc}: '))
else:
for _ in range(gc.passwd_max_tries):
pw = ' '.join(get_words_from_user( self.cfg, f'Enter {pw_desc} for {data_desc}: ' ))
pw_chk = ' '.join(get_words_from_user( self.cfg, f'Repeat {pw_desc}: ' ))
pw = ' '.join(get_words_from_user(self.cfg, f'Enter {pw_desc} for {data_desc}: '))
pw_chk = ' '.join(get_words_from_user(self.cfg, f'Repeat {pw_desc}: '))
self.util.dmsg(f'Passphrases: [{pw}] [{pw_chk}]')
if pw == pw_chk:
self.util.vmsg('Passphrases match')
break
msg('Passphrases do not match. Try again.')
else:
die(2,f'User failed to duplicate passphrase in {gc.passwd_max_tries} attempts')
die(2, f'User failed to duplicate passphrase in {gc.passwd_max_tries} attempts')
if pw == '':
self.util.qmsg('WARNING: Empty passphrase')
return pw
def get_passphrase(self,data_desc,passwd_file,pw_desc='passphrase'):
def get_passphrase(self, data_desc, passwd_file, pw_desc='passphrase'):
if passwd_file:
from .fileutil import get_words_from_file
return ' '.join(get_words_from_file(
cfg = self.cfg,
infile = passwd_file,
desc = f'{pw_desc} for {data_desc}',
quiet = self.pwfile_used(passwd_file) ))
quiet = self.pwfile_used(passwd_file)))
else:
from .ui import get_words_from_user
return ' '.join(get_words_from_user( self.cfg, f'Enter {pw_desc} for {data_desc}: ' ))
return ' '.join(get_words_from_user(self.cfg, f'Enter {pw_desc} for {data_desc}: '))
def mmgen_encrypt(self,data,desc='data',hash_preset=None):
def mmgen_encrypt(self, data, desc='data', hash_preset=None):
salt = self.get_random(self.mmenc_salt_len)
iv = self.get_random(self.aesctr_iv_len)
nonce = self.get_random(self.mmenc_nonce_len)
hp = hash_preset or self.cfg.hash_preset or self.get_hash_preset_from_user(data_desc=desc)
m = ('user-requested','default')[hp=='3']
m = ('user-requested', 'default')[hp=='3']
self.util.vmsg(f'Encrypting {desc}')
self.util.qmsg(f'Using {m} hash preset of {hp!r}')
passwd = self.get_new_passphrase(
data_desc = desc,
hash_preset = hp,
passwd_file = self.cfg.passwd_file )
key = self.make_key(passwd,salt,hp)
passwd_file = self.cfg.passwd_file)
key = self.make_key(passwd, salt, hp)
from hashlib import sha256
enc_d = self.encrypt_data( sha256(nonce+data).digest() + nonce + data, key, iv, desc=desc )
enc_d = self.encrypt_data(sha256(nonce+data).digest() + nonce + data, key, iv, desc=desc)
return salt+iv+enc_d
def mmgen_decrypt(self,data,desc='data',hash_preset=None):
def mmgen_decrypt(self, data, desc='data', hash_preset=None):
self.util.vmsg(f'Preparing to decrypt {desc}')
dstart = self.mmenc_salt_len + self.aesctr_iv_len
salt = data[:self.mmenc_salt_len]
iv = data[self.mmenc_salt_len:dstart]
enc_d = data[dstart:]
hp = hash_preset or self.cfg.hash_preset or self.get_hash_preset_from_user(data_desc=desc)
m = ('user-requested','default')[hp=='3']
m = ('user-requested', 'default')[hp=='3']
self.util.qmsg(f'Using {m} hash preset of {hp!r}')
passwd = self.get_passphrase(
data_desc = desc,
passwd_file = self.cfg.passwd_file )
key = self.make_key(passwd,salt,hp)
dec_d = self.decrypt_data( enc_d, key, iv, desc )
passwd_file = self.cfg.passwd_file)
key = self.make_key(passwd, salt, hp)
dec_d = self.decrypt_data(enc_d, key, iv, desc)
sha256_len = 32
from hashlib import sha256
if dec_d[:sha256_len] == sha256(dec_d[sha256_len:]).digest():
@ -441,9 +433,9 @@ class Crypto:
msg('Incorrect passphrase or hash preset')
return False
def mmgen_decrypt_retry(self,d,desc='data'):
def mmgen_decrypt_retry(self, d, desc='data'):
while True:
d_dec = self.mmgen_decrypt(d,desc)
d_dec = self.mmgen_decrypt(d, desc)
if d_dec:
return d_dec
msg('Trying again...')

View file

@ -20,17 +20,17 @@
daemon: Daemon control interface for the MMGen suite
"""
import sys,os,time,importlib
from subprocess import run,PIPE,CompletedProcess
import sys, os, time, importlib
from subprocess import run, PIPE, CompletedProcess
from collections import namedtuple
from .base_obj import Lockable
from .color import set_vt100
from .util import msg,Msg_r,die,remove_dups,oneshot_warning,fmt_list
from .flags import ClassFlags,ClassOpts
from .util import msg, Msg_r, die, remove_dups, oneshot_warning, fmt_list
from .flags import ClassFlags, ClassOpts
_dd = namedtuple('daemon_data',['coind_name','coind_version','coind_version_str']) # latest tested version
_nw = namedtuple('coin_networks',['mainnet','testnet','regtest'])
_dd = namedtuple('daemon_data', ['coind_name', 'coind_version', 'coind_version_str']) # latest tested version
_nw = namedtuple('coin_networks', ['mainnet', 'testnet', 'regtest'])
class Daemon(Lockable):
@ -47,10 +47,10 @@ class Daemon(Lockable):
private_port = None
avail_opts = ()
avail_flags = () # like opts, but can be set or unset after instantiation
_reset_ok = ('debug','wait','pids')
_reset_ok = ('debug', 'wait', 'pids')
version_info_arg = '--version'
def __init__(self,cfg,opts=None,flags=None):
def __init__(self, cfg, opts=None, flags=None):
self.cfg = cfg
self.platform = sys.platform
@ -58,40 +58,40 @@ class Daemon(Lockable):
self.use_pidfile = False
self.use_threads = True
self.opt = ClassOpts(self,opts)
self.flag = ClassFlags(self,flags)
self.opt = ClassOpts(self, opts)
self.flag = ClassFlags(self, flags)
self.debug = self.debug or cfg.debug_daemon
def exec_cmd_thread(self,cmd):
def exec_cmd_thread(self, cmd):
import threading
tname = ('exec_cmd','exec_cmd_win_console')[self.platform == 'win32' and self.new_console_mswin]
t = threading.Thread(target=getattr(self,tname),args=(cmd,))
tname = ('exec_cmd', 'exec_cmd_win_console')[self.platform == 'win32' and self.new_console_mswin]
t = threading.Thread(target=getattr(self, tname), args=(cmd,))
t.daemon = True
t.start()
if self.platform == 'win32':
Msg_r(' \b') # blocks w/o this...crazy
return True
def exec_cmd_win_console(self,cmd):
from subprocess import Popen,CREATE_NEW_CONSOLE,STARTUPINFO,STARTF_USESHOWWINDOW,SW_HIDE
si = STARTUPINFO(dwFlags=STARTF_USESHOWWINDOW,wShowWindow=SW_HIDE)
p = Popen(cmd,creationflags=CREATE_NEW_CONSOLE,startupinfo=si)
def exec_cmd_win_console(self, cmd):
from subprocess import Popen, CREATE_NEW_CONSOLE, STARTUPINFO, STARTF_USESHOWWINDOW, SW_HIDE
si = STARTUPINFO(dwFlags=STARTF_USESHOWWINDOW, wShowWindow=SW_HIDE)
p = Popen(cmd, creationflags=CREATE_NEW_CONSOLE, startupinfo=si)
p.wait()
def exec_cmd(self,cmd,is_daemon=False,check_retcode=False):
out = (PIPE,None)[is_daemon and self.opt.no_daemonize]
def exec_cmd(self, cmd, is_daemon=False, check_retcode=False):
out = (PIPE, None)[is_daemon and self.opt.no_daemonize]
try:
cp = run(cmd,check=False,stdout=out,stderr=out)
cp = run(cmd, check=False, stdout=out, stderr=out)
except OSError as e:
die( 'MMGenCalledProcessError', f'Error starting executable: {type(e).__name__} [Errno {e.errno}]' )
die('MMGenCalledProcessError', f'Error starting executable: {type(e).__name__} [Errno {e.errno}]')
set_vt100()
if check_retcode and cp.returncode:
die(1,str(cp))
die(1, str(cp))
if self.debug:
print(cp)
return cp
def run_cmd(self,cmd,silent=False,is_daemon=False,check_retcode=False):
def run_cmd(self, cmd, silent=False, is_daemon=False, check_retcode=False):
if self.debug:
msg('\n\n')
@ -100,14 +100,14 @@ class Daemon(Lockable):
msg(f'Starting {self.desc} on port {self.bind_port}')
if self.debug:
msg(f'\nExecuting:\n{fmt_list(cmd,fmt="col",indent=" ")}\n')
msg(f'\nExecuting:\n{fmt_list(cmd, fmt="col", indent=" ")}\n')
if self.use_threads and is_daemon and not self.opt.no_daemonize:
ret = self.exec_cmd_thread(cmd)
else:
ret = self.exec_cmd(cmd,is_daemon,check_retcode)
ret = self.exec_cmd(cmd, is_daemon, check_retcode)
if isinstance(ret,CompletedProcess):
if isinstance(ret, CompletedProcess):
if ret.stdout and (self.debug or not silent):
msg(ret.stdout.decode().rstrip())
if ret.stderr and (self.debug or (ret.returncode and not silent)):
@ -124,7 +124,7 @@ class Daemon(Lockable):
# Assumes only one running instance of given daemon. If multiple daemons are running,
# the first PID in the list is returned and self.pids is set to the PID list.
ss = f'{self.exec_fn}.exe'
cp = self.run_cmd(['ps','-Wl'],silent=True)
cp = self.run_cmd(['ps', '-Wl'], silent=True)
self.pids = ()
# use Windows, not Cygwin, PID
pids = tuple(line.split()[3] for line in cp.stdout.decode().splitlines() if ss in line)
@ -134,10 +134,10 @@ class Daemon(Lockable):
return pids[0]
elif self.platform in ('linux', 'darwin'):
ss = ' '.join(self.start_cmd)
cp = self.run_cmd(['pgrep','-f',ss],silent=True)
cp = self.run_cmd(['pgrep', '-f', ss], silent=True)
if cp.stdout:
return cp.stdout.strip().decode()
die(2,f'{ss!r} not found in process list, cannot determine PID')
die(2, f'{ss!r} not found in process list, cannot determine PID')
@property
def bind_port(self):
@ -147,7 +147,7 @@ class Daemon(Lockable):
def state(self):
if self.debug:
msg(f'Testing port {self.bind_port}')
return 'ready' if self.test_socket('localhost',self.bind_port) else 'stopped'
return 'ready' if self.test_socket('localhost', self.bind_port) else 'stopped'
@property
def start_cmds(self):
@ -156,17 +156,17 @@ class Daemon(Lockable):
@property
def stop_cmd(self):
return (
['kill','-Wf',self.pid] if self.platform == 'win32' else
['kill','-9',self.pid] if self.force_kill else
['kill',self.pid] )
['kill', '-Wf', self.pid] if self.platform == 'win32' else
['kill', '-9', self.pid] if self.force_kill else
['kill', self.pid])
def cmd(self,action,*args,**kwargs):
return getattr(self,action)(*args,**kwargs)
def cmd(self, action, *args, **kwargs):
return getattr(self, action)(*args, **kwargs)
def cli(self,*cmds,silent=False):
return self.run_cmd(self.cli_cmd(*cmds),silent=silent)
def cli(self, *cmds, silent=False):
return self.run_cmd(self.cli_cmd(*cmds), silent=silent)
def state_msg(self,extra_text=None):
def state_msg(self, extra_text=None):
try:
pid = self.pid
except:
@ -176,12 +176,12 @@ class Daemon(Lockable):
f'{self.desc} {extra_text}running',
'pid N/A' if pid is None or self.pids or self.state == 'stopped' else f'pid {pid}',
f'port {self.bind_port}',
w = 60 )
w = 60)
def pre_start(self):
pass
def start(self,quiet=False,silent=False):
def start(self, quiet=False, silent=False):
if self.state == 'ready':
if not (quiet or silent):
msg(self.state_msg(extra_text='already'))
@ -193,24 +193,24 @@ class Daemon(Lockable):
if not silent:
msg(f'Starting {self.desc} on port {self.bind_port}')
ret = self.run_cmd(self.start_cmd,silent=True,is_daemon=True,check_retcode=True)
ret = self.run_cmd(self.start_cmd, silent=True, is_daemon=True, check_retcode=True)
if self.wait:
self.wait_for_state('ready')
return ret
def stop(self,quiet=False,silent=False):
def stop(self, quiet=False, silent=False):
if self.state == 'ready':
if not silent:
msg(f'Stopping {self.desc} on port {self.bind_port}')
if self.force_kill:
run(['sync'])
ret = self.run_cmd(self.stop_cmd,silent=True)
ret = self.run_cmd(self.stop_cmd, silent=True)
if self.pids:
msg('Warning: multiple PIDs [{}] -- we may be stopping the wrong instance'.format(
fmt_list(self.pids,fmt='bare')
fmt_list(self.pids, fmt='bare')
))
if self.wait:
self.wait_for_state('stopped')
@ -221,49 +221,49 @@ class Daemon(Lockable):
msg(f'{self.desc} on port {self.bind_port} not running')
return True
def restart(self,silent=False):
def restart(self, silent=False):
self.stop(silent=silent)
return self.start(silent=silent)
def test_socket(self,host,port,timeout=10):
def test_socket(self, host, port, timeout=10):
import socket
try:
socket.create_connection((host,port),timeout=timeout).close()
socket.create_connection((host, port), timeout=timeout).close()
except:
return False
else:
return True
def wait_for_state(self,req_state):
def wait_for_state(self, req_state):
for _ in range(300):
if self.state == req_state:
return True
time.sleep(0.2)
die(2,f'Wait for state {req_state!r} timeout exceeded for {self.desc} (port {self.bind_port})')
die(2, f'Wait for state {req_state!r} timeout exceeded for {self.desc} (port {self.bind_port})')
@classmethod
def get_exec_version_str(cls):
try:
cp = run([cls.exec_fn,cls.version_info_arg],stdout=PIPE,stderr=PIPE,check=True)
cp = run([cls.exec_fn, cls.version_info_arg], stdout=PIPE, stderr=PIPE, check=True)
except Exception as e:
die(2,f'{e}\nUnable to execute {cls.exec_fn}')
die(2, f'{e}\nUnable to execute {cls.exec_fn}')
if cp.returncode:
die(2,f'Unable to execute {cls.exec_fn}')
die(2, f'Unable to execute {cls.exec_fn}')
else:
res = cp.stdout.decode().splitlines()
return ( res[0] if len(res) == 1 else [s for s in res if 'ersion' in s][0] ).strip()
return (res[0] if len(res) == 1 else [s for s in res if 'ersion' in s][0]).strip()
class RPCDaemon(Daemon):
avail_opts = ('no_daemonize',)
def __init__(self,cfg,opts=None,flags=None):
super().__init__(cfg,opts=opts,flags=flags)
def __init__(self, cfg, opts=None, flags=None):
super().__init__(cfg, opts=opts, flags=flags)
self.desc = '{} {} {}RPC daemon'.format(
self.rpc_type,
getattr(self.proto.network_names,self.proto.network),
'test suite ' if self.test_suite else '' )
getattr(self.proto.network_names, self.proto.network),
'test suite ' if self.test_suite else '')
self._set_ok += ('usr_daemon_args',)
self.usr_daemon_args = []
@ -272,22 +272,22 @@ class RPCDaemon(Daemon):
return [self.exec_fn] + self.daemon_args + self.usr_daemon_args
class CoinDaemon(Daemon):
networks = ('mainnet','testnet','regtest')
networks = ('mainnet', 'testnet', 'regtest')
cfg_file_hdr = ''
avail_flags = ('keep_cfg_file',)
avail_opts = ('no_daemonize','online')
avail_opts = ('no_daemonize', 'online')
testnet_dir = None
test_suite_port_shift = 1237
rpc_user = None
rpc_password = None
_cd = namedtuple('coins_data',['daemon_ids'])
_cd = namedtuple('coins_data', ['daemon_ids'])
coins = {
'BTC': _cd(['bitcoin_core']),
'BCH': _cd(['bitcoin_cash_node']),
'LTC': _cd(['litecoin_core']),
'XMR': _cd(['monero']),
'ETH': _cd(['geth','erigon','openethereum']),
'ETH': _cd(['geth', 'erigon', 'openethereum']),
'ETC': _cd(['parity']),
}
@ -300,7 +300,7 @@ class CoinDaemon(Daemon):
message = 'blacklisted daemon: {!r}'
@classmethod
def get_daemon_ids(cls,cfg,coin):
def get_daemon_ids(cls, cfg, coin):
ret = cls.coins[coin].daemon_ids
if 'erigon' in ret and not cfg.enable_erigon:
@ -310,14 +310,14 @@ class CoinDaemon(Daemon):
def gen():
for daemon_id in ret:
if daemon_id in blacklist:
cls.warn_blacklisted(div=daemon_id,fmt_args=[daemon_id])
cls.warn_blacklisted(div=daemon_id, fmt_args=[daemon_id])
else:
yield daemon_id
ret = list(gen())
return ret
@classmethod
def get_daemon(cls,cfg,coin,daemon_id,proto=None):
def get_daemon(cls, cfg, coin, daemon_id, proto=None):
if proto:
proto_cls = type(proto)
else:
@ -325,17 +325,17 @@ class CoinDaemon(Daemon):
proto_cls = init_proto(cfg, coin, return_cls=True)
return getattr(
importlib.import_module(f'mmgen.proto.{proto_cls.base_proto_coin.lower()}.daemon'),
daemon_id+'_daemon' )
daemon_id+'_daemon')
@classmethod
def get_network_ids(cls,cfg):
def get_network_ids(cls, cfg):
from .protocol import CoinProtocol
def gen():
for coin in cls.coins:
for daemon_id in cls.get_daemon_ids(cfg,coin):
for network in cls.get_daemon( cfg, coin, daemon_id ).networks:
yield CoinProtocol.Base.create_network_id(coin,network)
return remove_dups(list(gen()),quiet=True)
for daemon_id in cls.get_daemon_ids(cfg, coin):
for network in cls.get_daemon(cfg, coin, daemon_id).networks:
yield CoinProtocol.Base.create_network_id(coin, network)
return remove_dups(list(gen()), quiet=True)
def __new__(cls,
cfg,
@ -347,7 +347,7 @@ class CoinDaemon(Daemon):
port_shift = None,
p2p_port = None,
datadir = None,
daemon_id = None ):
daemon_id = None):
assert network_id or proto, 'CoinDaemon_chk1'
assert not (network_id and proto), 'CoinDaemon_chk2'
@ -358,20 +358,20 @@ class CoinDaemon(Daemon):
coin = proto.coin
else:
network_id = network_id.lower()
from .protocol import CoinProtocol,init_proto
proto = init_proto( cfg, network_id=network_id )
coin,network = CoinProtocol.Base.parse_network_id(network_id)
from .protocol import CoinProtocol, init_proto
proto = init_proto(cfg, network_id=network_id)
coin, network = CoinProtocol.Base.parse_network_id(network_id)
coin = coin.upper()
daemon_ids = cls.get_daemon_ids(cfg,coin)
daemon_ids = cls.get_daemon_ids(cfg, coin)
if not daemon_ids:
die(1,f'No configured daemons for coin {coin}!')
die(1, f'No configured daemons for coin {coin}!')
daemon_id = daemon_id or cfg.daemon_id or daemon_ids[0]
if daemon_id not in daemon_ids:
die(1,f'{daemon_id!r}: invalid daemon_id - valid choices: {fmt_list(daemon_ids)}')
die(1, f'{daemon_id!r}: invalid daemon_id - valid choices: {fmt_list(daemon_ids)}')
me = Daemon.__new__(cls.get_daemon( cfg, None, daemon_id, proto=proto ))
me = Daemon.__new__(cls.get_daemon(cfg, None, daemon_id, proto=proto))
assert network in me.networks, f'{network!r}: unsupported network for daemon {daemon_id}'
me.network_id = network_id
@ -392,30 +392,30 @@ class CoinDaemon(Daemon):
port_shift = None,
p2p_port = None,
datadir = None,
daemon_id = None ):
daemon_id = None):
self.test_suite = test_suite
super().__init__(cfg=cfg,opts=opts,flags=flags)
super().__init__(cfg=cfg, opts=opts, flags=flags)
self._set_ok += ('shared_args','usr_coind_args')
self._set_ok += ('shared_args', 'usr_coind_args')
self.shared_args = []
self.usr_coind_args = []
for k,v in self.daemon_data._asdict().items():
setattr(self,k,v)
for k, v in self.daemon_data._asdict().items():
setattr(self, k, v)
self.desc = '{} {} {}daemon'.format(
self.coind_name,
getattr(self.proto.network_names,self.network),
'test suite ' if test_suite else '' )
getattr(self.proto.network_names, self.network),
'test suite ' if test_suite else '')
# user-set values take precedence
self.datadir = os.path.abspath(datadir or cfg.daemon_data_dir or self.init_datadir())
self.non_dfl_datadir = bool(datadir or cfg.daemon_data_dir or test_suite or self.network == 'regtest')
# init_datadir() may have already initialized logdir
self.logdir = os.path.abspath(getattr(self,'logdir',self.datadir))
self.logdir = os.path.abspath(getattr(self, 'logdir', self.datadir))
ps_adj = (port_shift or 0) + (self.test_suite_port_shift if test_suite else 0)
@ -424,10 +424,10 @@ class CoinDaemon(Daemon):
self.p2p_port = (
p2p_port or (
self.get_p2p_port() + ps_adj if self.get_p2p_port() and (test_suite or ps_adj) else None
) if self.network != 'regtest' else None )
) if self.network != 'regtest' else None)
if hasattr(self,'private_ports'):
self.private_port = getattr(self.private_ports,self.network)
if hasattr(self, 'private_ports'):
self.private_port = getattr(self.private_ports, self.network)
# bind_port == self.private_port or self.rpc_port
self.pidfile = f'{self.logdir}/{self.id}-{self.network}-daemon-{self.bind_port}.pid'
@ -437,7 +437,7 @@ class CoinDaemon(Daemon):
def init_datadir(self):
if self.test_suite:
return os.path.join('test','daemons',self.network_id)
return os.path.join('test', 'daemons', self.network_id)
else:
return os.path.join(*self.datadirs[self.platform])
@ -446,7 +446,7 @@ class CoinDaemon(Daemon):
return self.datadir
def get_rpc_port(self):
return getattr(self.rpc_ports,self.network)
return getattr(self.rpc_ports, self.network)
def get_p2p_port(self):
return None
@ -456,27 +456,27 @@ class CoinDaemon(Daemon):
return ([self.exec_fn]
+ self.coind_args
+ self.shared_args
+ self.usr_coind_args )
+ self.usr_coind_args)
def cli_cmd(self,*cmds):
def cli_cmd(self, *cmds):
return ([self.cli_fn]
+ self.shared_args
+ list(cmds) )
+ list(cmds))
def start(self,*args,**kwargs):
def start(self, *args, **kwargs):
assert self.test_suite or self.network == 'regtest', 'start() restricted to test suite and regtest'
return super().start(*args,**kwargs)
return super().start(*args, **kwargs)
def stop(self,*args,**kwargs):
def stop(self, *args, **kwargs):
assert self.test_suite or self.network == 'regtest', 'stop() restricted to test suite and regtest'
return super().stop(*args,**kwargs)
return super().stop(*args, **kwargs)
def pre_start(self):
os.makedirs(self.datadir,exist_ok=True)
os.makedirs(self.datadir, exist_ok=True)
if self.test_suite or self.network == 'regtest':
if self.cfg_file and not self.flag.keep_cfg_file:
with open(f'{self.datadir}/{self.cfg_file}','w') as fp:
with open(f'{self.datadir}/{self.cfg_file}', 'w') as fp:
fp.write(self.cfg_file_hdr)
if self.use_pidfile and os.path.exists(self.pidfile):
@ -491,7 +491,7 @@ class CoinDaemon(Daemon):
run([
('rm' if self.platform == 'win32' else '/bin/rm'),
'-rf',
self.datadir ])
self.datadir])
set_vt100()
else:
msg(f'Cannot remove {self.network_datadir!r} - daemon is not stopped')

View file

@ -21,19 +21,19 @@ derive: coin private key secret derivation for the MMGen suite
"""
from collections import namedtuple
from hashlib import sha512,sha256
from hashlib import sha512, sha256
from .addrlist import AddrIdxList
pk_bytes = namedtuple('coin_privkey_bytes',['idx','pos','data'])
pk_bytes = namedtuple('coin_privkey_bytes', ['idx', 'pos', 'data'])
def derive_coin_privkey_bytes(seed,idxs):
def derive_coin_privkey_bytes(seed, idxs):
assert isinstance(idxs,AddrIdxList), f'{type(idxs)}: idx list not of type AddrIdxList'
assert isinstance(idxs, AddrIdxList), f'{type(idxs)}: idx list not of type AddrIdxList'
t_keys = len(idxs)
pos = 0
for idx in range( 1, AddrIdxList.max_len+1 ): # key/addr indexes begin from one
for idx in range(1, AddrIdxList.max_len + 1): # key/addr indexes begin from one
seed = sha512(seed).digest()
@ -42,7 +42,7 @@ def derive_coin_privkey_bytes(seed,idxs):
pos += 1
# secret is double sha256 of seed hash round /idx/
yield pk_bytes( idx, pos, sha256(sha256(seed).digest()).digest() )
yield pk_bytes(idx, pos, sha256(sha256(seed).digest()).digest())
if pos == t_keys:
break

View file

@ -13,35 +13,35 @@ devinit: Developer tools initialization for the MMGen suite
"""
devtools_funcs = {
'pfmt': lambda *args,**kwargs: devtools_call('pfmt',*args,**kwargs),
'pmsg': lambda *args,**kwargs: devtools_call('pmsg',*args,**kwargs),
'pmsg_r': lambda *args,**kwargs: devtools_call('pmsg_r',*args,**kwargs),
'pdie': lambda *args,**kwargs: devtools_call('pdie',*args,**kwargs),
'pexit': lambda *args,**kwargs: devtools_call('pexit',*args,**kwargs),
'Pmsg': lambda *args,**kwargs: devtools_call('Pmsg',*args,**kwargs),
'Pdie': lambda *args,**kwargs: devtools_call('Pdie',*args,**kwargs),
'Pexit': lambda *args,**kwargs: devtools_call('Pexit',*args,**kwargs),
'print_stack_trace': lambda *args,**kwargs: devtools_call('print_stack_trace',*args,**kwargs),
'get_diff': lambda *args,**kwargs: devtools_call('get_diff',*args,**kwargs),
'print_diff': lambda *args,**kwargs: devtools_call('print_diff',*args,**kwargs),
'get_ndiff': lambda *args,**kwargs: devtools_call('get_ndiff',*args,**kwargs),
'print_ndiff': lambda *args,**kwargs: devtools_call('print_ndiff',*args,**kwargs),
'pfmt': lambda *args, **kwargs: devtools_call('pfmt', *args, **kwargs),
'pmsg': lambda *args, **kwargs: devtools_call('pmsg', *args, **kwargs),
'pmsg_r': lambda *args, **kwargs: devtools_call('pmsg_r', *args, **kwargs),
'pdie': lambda *args, **kwargs: devtools_call('pdie', *args, **kwargs),
'pexit': lambda *args, **kwargs: devtools_call('pexit', *args, **kwargs),
'Pmsg': lambda *args, **kwargs: devtools_call('Pmsg', *args, **kwargs),
'Pdie': lambda *args, **kwargs: devtools_call('Pdie', *args, **kwargs),
'Pexit': lambda *args, **kwargs: devtools_call('Pexit', *args, **kwargs),
'print_stack_trace': lambda *args, **kwargs: devtools_call('print_stack_trace', *args, **kwargs),
'get_diff': lambda *args, **kwargs: devtools_call('get_diff', *args, **kwargs),
'print_diff': lambda *args, **kwargs: devtools_call('print_diff', *args, **kwargs),
'get_ndiff': lambda *args, **kwargs: devtools_call('get_ndiff', *args, **kwargs),
'print_ndiff': lambda *args, **kwargs: devtools_call('print_ndiff', *args, **kwargs),
}
def devtools_call(funcname,*args,**kwargs):
def devtools_call(funcname, *args, **kwargs):
from . import devtools
return getattr(devtools,funcname)(*args,**kwargs)
return getattr(devtools, funcname)(*args, **kwargs)
def MMGenObject_call(methodname,*args,**kwargs):
def MMGenObject_call(methodname, *args, **kwargs):
from .devtools import MMGenObjectMethods
return getattr(MMGenObjectMethods,methodname)(*args,**kwargs)
return getattr(MMGenObjectMethods, methodname)(*args, **kwargs)
class MMGenObjectDevTools:
pmsg = lambda *args,**kwargs: MMGenObject_call('pmsg',*args,**kwargs)
pdie = lambda *args,**kwargs: MMGenObject_call('pdie',*args,**kwargs)
pexit = lambda *args,**kwargs: MMGenObject_call('pexit',*args,**kwargs)
pfmt = lambda *args,**kwargs: MMGenObject_call('pfmt',*args,**kwargs)
pmsg = lambda *args, **kwargs: MMGenObject_call('pmsg', *args, **kwargs)
pdie = lambda *args, **kwargs: MMGenObject_call('pdie', *args, **kwargs)
pexit = lambda *args, **kwargs: MMGenObject_call('pexit', *args, **kwargs)
pfmt = lambda *args, **kwargs: MMGenObject_call('pfmt', *args, **kwargs)
# Check that all immutables have been initialized. Expensive, so do only when testing.
def immutable_attr_init_check(self):
@ -50,22 +50,22 @@ class MMGenObjectDevTools:
for attrname in self.valid_attrs:
for o in (cls,cls.__bases__[0]): # assume there's only one base class
for o in (cls, cls.__bases__[0]): # assume there's only one base class
if attrname in o.__dict__:
attr = o.__dict__[attrname]
break
else:
from .util import die
die(4,f'unable to find descriptor {cls.__name__}.{attrname}')
die(4, f'unable to find descriptor {cls.__name__}.{attrname}')
if type(attr).__name__ == 'ImmutableAttr' and attrname not in self.__dict__:
from .util import die
die(4,f'attribute {attrname!r} of {cls.__name__} has not been initialized in constructor!')
die(4, f'attribute {attrname!r} of {cls.__name__} has not been initialized in constructor!')
def init_dev():
import builtins
# MMGenObject is added to the namespace by objmethods.py, so we must name the builtin differently
# to avoid inadvertently adding MMGenObject to the global namespace here:
setattr(builtins,'MMGenObjectDevTools',MMGenObjectDevTools)
for funcname,func in devtools_funcs.items():
setattr(builtins,funcname,func)
setattr(builtins, 'MMGenObjectDevTools', MMGenObjectDevTools)
for funcname, func in devtools_funcs.items():
setattr(builtins, funcname, func)

View file

@ -16,77 +16,77 @@ import sys
color_funcs = {
name: lambda s, n=n: f'\033[{n};1m{s}\033[0m'
for name,n in (
('red', 31),
('green', 32),
('yellow',33),
('blue', 34),
('purple',35))
for name, n in (
('red', 31),
('green', 32),
('yellow', 33),
('blue', 34),
('purple', 35))
}
def pfmt(*args,color=None):
def pfmt(*args, color=None):
import pprint
ret = pprint.PrettyPrinter(indent=4,width=116).pformat(
args if len(args) > 1 else '' if not args else args[0] )
ret = pprint.PrettyPrinter(indent=4, width=116).pformat(
args if len(args) > 1 else '' if not args else args[0])
return color_funcs[color](ret) if color else ret
def pmsg(*args,color=None):
sys.stderr.write(pfmt(*args,color=color) + '\n')
def pmsg(*args, color=None):
sys.stderr.write(pfmt(*args, color=color) + '\n')
def pmsg_r(*args,color=None):
sys.stderr.write(pfmt(*args,color=color))
def pmsg_r(*args, color=None):
sys.stderr.write(pfmt(*args, color=color))
def pdie(*args,exit_val=1):
pmsg(*args,color='red' if exit_val else None)
def pdie(*args, exit_val=1):
pmsg(*args, color='red' if exit_val else None)
sys.exit(exit_val)
def pexit(*args):
pdie(*args,exit_val=0)
pdie(*args, exit_val=0)
def Pmsg(*args,color=None):
sys.stdout.write(pfmt(*args,color=color) + '\n')
def Pmsg(*args, color=None):
sys.stdout.write(pfmt(*args, color=color) + '\n')
def Pdie(*args,exit_val=1):
Pmsg(*args,color=('yellow' if exit_val == 1 else 'red' if exit_val else None))
def Pdie(*args, exit_val=1):
Pmsg(*args, color=('yellow' if exit_val == 1 else 'red' if exit_val else None))
sys.exit(exit_val)
def Pexit(*args):
Pdie(*args,exit_val=0)
Pdie(*args, exit_val=0)
def print_stack_trace(message=None,fh_list=[],nl='\n',sep='\n ',trim=4):
def print_stack_trace(message=None, fh_list=[], nl='\n', sep='\n ', trim=4):
if not fh_list:
import os
fh_list.append(open(f'devtools.trace.{os.getpid()}','w'))
fh_list.append(open(f'devtools.trace.{os.getpid()}', 'w'))
nl = ''
res = get_stack_trace(message,nl,sep,trim)
res = get_stack_trace(message, nl, sep, trim)
sys.stderr.write(res)
fh_list[0].write(res)
def get_stack_trace(message=None,nl='\n',sep='\n ',trim=3):
def get_stack_trace(message=None, nl='\n', sep='\n ', trim=3):
import os,re,traceback
import os, re, traceback
tb = [t for t in traceback.extract_stack() if t.filename[:1] != '<']
fs = '{}:{}: in {}:\n {}'
out = [
fs.format(
re.sub(r'^\./','',os.path.relpath(t.filename)),
re.sub(r'^\./', '', os.path.relpath(t.filename)),
t.lineno,
(t.name+'()' if t.name[-1] != '>' else t.name),
t.line or '(none)')
for t in (tb[:-trim] if trim else tb) ]
for t in (tb[:-trim] if trim else tb)]
return f'{nl}STACK TRACE {message or "[unnamed]"}:{sep}{sep.join(out)}\n'
def print_diff(*args,**kwargs):
sys.stderr.write(get_diff(*args,**kwargs))
def print_diff(*args, **kwargs):
sys.stderr.write(get_diff(*args, **kwargs))
def get_diff(a,b,a_fn='',b_fn='',from_json=True):
def get_diff(a, b, a_fn='', b_fn='', from_json=True):
if from_json:
import json
a = json.dumps(json.loads(a),indent=4)
b = json.dumps(json.loads(b),indent=4)
a = json.dumps(json.loads(a), indent=4)
b = json.dumps(json.loads(b), indent=4)
from difflib import unified_diff
# chunk headers have trailing newlines, hence the rstrip()
@ -95,70 +95,70 @@ def get_diff(a,b,a_fn='',b_fn='',from_json=True):
a.split('\n'),
b.split('\n'),
a_fn,
b_fn )))
b_fn)))
def print_ndiff(*args,**kwargs):
sys.stderr.write(get_ndiff(*args,**kwargs))
def print_ndiff(*args, **kwargs):
sys.stderr.write(get_ndiff(*args, **kwargs))
def get_ndiff(a,b):
def get_ndiff(a, b):
from difflib import ndiff
return list(ndiff(
a.split('\n'),
b.split('\n') ))
b.split('\n')))
class MMGenObjectMethods: # mixin class for MMGenObject
# Pretty-print an MMGenObject instance, recursing into sub-objects - WIP
def pmsg(self,color=None):
def pmsg(self, color=None):
sys.stdout.write('\n'+self.pfmt(color=color))
def pdie(self,exit_val=1):
def pdie(self, exit_val=1):
self.pmsg(color='red' if exit_val else None)
sys.exit(exit_val)
def pexit(self):
self.pdie(exit_val=0)
def pfmt(self,lvl=0,id_list=[],color=None):
def pfmt(self, lvl=0, id_list=[], color=None):
from decimal import Decimal
scalars = (str,int,float,Decimal)
scalars = (str, int, float, Decimal)
def isDict(obj):
return isinstance(obj,dict)
return isinstance(obj, dict)
def isList(obj):
return isinstance(obj,list)
return isinstance(obj, list)
def isScalar(obj):
return isinstance(obj,scalars)
return isinstance(obj, scalars)
def do_list(out,e,lvl=0,is_dict=False):
def do_list(out, e, lvl=0, is_dict=False):
out.append('\n')
for i in e:
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))
# out.append('{s}{:<{l}}'.format(i, s=' '*(4*lvl+8), l=10, l2=8*(lvl+1)+8))
out.append('{s1}{i}{s2}'.format(
i = i,
s1 = ' ' * (4*lvl+8),
s2 = ' ' * 10 ))
if hasattr(el,'pfmt'):
s2 = ' ' * 10))
if hasattr(el, 'pfmt'):
out.append('{:>{l}}{}'.format(
'',
el.pfmt( lvl=lvl+1, id_list=id_list+[id(self)] ),
l = (lvl+1)*8 ))
elif isinstance(el,scalars):
el.pfmt(lvl=lvl+1, id_list=id_list+[id(self)]),
l = (lvl+1)*8))
elif isinstance(el, scalars):
if isList(e):
out.append( '{:>{l}}{!r:16}\n'.format( '', el, l=lvl*8 ))
out.append('{:>{l}}{!r:16}\n'.format('', el, l=lvl*8))
else:
out.append(f' {el!r}')
elif isList(el) or isDict(el):
indent = 1 if is_dict else lvl*8+4
out.append('{:>{l}}{:16}'.format( '', f'<{type(el).__name__}>', l=indent ))
if isList(el) and isinstance(el[0],scalars):
out.append('{:>{l}}{:16}'.format('', f'<{type(el).__name__}>', l=indent))
if isList(el) and isinstance(el[0], scalars):
out.append('\n')
do_list(out,el,lvl=lvl+1,is_dict=isDict(el))
do_list(out, el, lvl=lvl+1, is_dict=isDict(el))
else:
out.append('{:>{l}}{:16} {!r}\n'.format( '', f'<{type(el).__name__}>', el, l=(lvl*8)+8 ))
out.append('{:>{l}}{:16} {!r}\n'.format('', f'<{type(el).__name__}>', el, l=(lvl*8)+8))
out.append('\n')
if not e:
@ -169,27 +169,27 @@ class MMGenObjectMethods: # mixin class for MMGenObject
if id(self) in id_list:
return out[-1].rstrip() + ' [RECURSION]\n'
if isList(self) or isDict(self):
do_list(out,self,lvl=lvl,is_dict=isDict(self))
do_list(out, self, lvl=lvl, is_dict=isDict(self))
for k in self.__dict__:
e = getattr(self,k)
e = getattr(self, k)
if isList(e) or isDict(e):
out.append('{:>{l}}{:<10} {:16}'.format( '', k, f'<{type(e).__name__}>', l=(lvl*8)+4 ))
do_list(out,e,lvl=lvl,is_dict=isDict(e))
elif hasattr(e,'pfmt') and callable(e.pfmt) and not isinstance(e,type):
out.append('{:>{l}}{:<10} {:16}'.format('', k, f'<{type(e).__name__}>', l=(lvl*8)+4))
do_list(out, e, lvl=lvl, is_dict=isDict(e))
elif hasattr(e, 'pfmt') and callable(e.pfmt) and not isinstance(e, type):
out.append('{:>{l}}{:10} {}'.format(
'',
k,
e.pfmt( lvl=lvl+1, id_list=id_list+[id(self)] ),
l = (lvl*8)+4 ))
e.pfmt(lvl=lvl+1, id_list=id_list+[id(self)]),
l = (lvl*8)+4))
else:
out.append('{:>{l}}{:<10} {:16} {}\n'.format(
'',
k,
f'<{type(e).__name__}>',
repr(e),
l=(lvl*8)+4 ))
l=(lvl*8)+4))
import re
ret = re.sub('\n+','\n',''.join(out))
ret = re.sub('\n+', '\n', ''.join(out))
return color_funcs[color](ret) if color else ret

View file

@ -22,7 +22,7 @@ exception: Exception classes for the MMGen suite
class MMGenError(Exception):
def __init__(self,errno,strerror,stdout):
def __init__(self, errno, strerror, stdout):
self.mmcode = errno
self.stdout = stdout
super().__init__(strerror)

View file

@ -20,12 +20,12 @@
filename: File and MMGenFile classes and methods for the MMGen suite
"""
import sys,os
from .util import die,get_extension
import sys, os
from .util import die, get_extension
class File:
def __init__(self,fn,write=False):
def __init__(self, fn, write=False):
self.name = fn
self.dirname = os.path.dirname(fn)
@ -38,18 +38,18 @@ class File:
try:
st = os.stat(fn)
except:
die( 'FileNotFound', f'{fn!r}: file not found' )
die('FileNotFound', f'{fn!r}: file not found')
import stat
if stat.S_ISBLK(st.st_mode):
if sys.platform in ('win32',):
die(2, 'Access to raw block devices not supported on platform {sys.platform!r}')
mode = (os.O_RDONLY,os.O_RDWR)[bool(write)]
mode = (os.O_RDONLY, os.O_RDWR)[bool(write)]
try:
fd = os.open(fn, mode)
except OSError as e:
if e.errno == 13:
die(2,f'{fn!r}: permission denied')
die(2, f'{fn!r}: permission denied')
# if e.errno != 17: raise
else:
if sys.platform == 'linux':
@ -66,21 +66,21 @@ class File:
class FileList(list):
def __init__(self,fns,write=False):
def __init__(self, fns, write=False):
list.__init__(
self,
[File(fn,write) for fn in fns] )
[File(fn, write) for fn in fns])
def names(self):
return [f.name for f in self]
def sort_by_age(self,key='mtime',reverse=False):
assert key in ('atime','ctime','mtime'), f'{key!r}: invalid sort key'
self.sort( key=lambda a: getattr(a,key), reverse=reverse )
def sort_by_age(self, key='mtime', reverse=False):
assert key in ('atime', 'ctime', 'mtime'), f'{key!r}: invalid sort key'
self.sort(key=lambda a: getattr(a, key), reverse=reverse)
class MMGenFile(File):
def __init__(self,fn,base_class=None,subclass=None,proto=None,write=False):
def __init__(self, fn, base_class=None, subclass=None, proto=None, write=False):
"""
'base_class' - a base class with an 'ext_to_cls' method
'subclass' - a subclass with an 'ext' attribute
@ -91,45 +91,45 @@ class MMGenFile(File):
attribute to True.
"""
super().__init__(fn,write)
super().__init__(fn, write)
assert (subclass or base_class) and not (subclass and base_class), 'MMGenFile chk1'
if not getattr(subclass or base_class,'filename_api',False):
die(3,f'Class {(subclass or base_class).__name__!r} does not support the MMGenFile API')
if not getattr(subclass or base_class, 'filename_api', False):
die(3, f'Class {(subclass or base_class).__name__!r} does not support the MMGenFile API')
if base_class:
subclass = base_class.ext_to_cls( self.ext, proto )
subclass = base_class.ext_to_cls(self.ext, proto)
if not subclass:
die( 'BadFileExtension', f'{self.ext!r}: not a recognized file extension for {base_class}' )
die('BadFileExtension', f'{self.ext!r}: not a recognized file extension for {base_class}')
self.subclass = subclass
class MMGenFileList(FileList):
def __init__(self,fns,base_class,proto=None,write=False):
def __init__(self, fns, base_class, proto=None, write=False):
list.__init__(
self,
[MMGenFile( fn, base_class=base_class, proto=proto, write=write ) for fn in fns] )
[MMGenFile(fn, base_class=base_class, proto=proto, write=write) for fn in fns])
def find_files_in_dir(subclass,fdir,no_dups=False):
def find_files_in_dir(subclass, fdir, no_dups=False):
assert isinstance(subclass,type), f'{subclass}: not a class'
assert isinstance(subclass, type), f'{subclass}: not a class'
if not getattr(subclass,'filename_api',False):
die(3,f'Class {subclass.__name__!r} does not support the MMGenFile API')
if not getattr(subclass, 'filename_api', False):
die(3, f'Class {subclass.__name__!r} does not support the MMGenFile API')
matches = [l for l in os.listdir(fdir) if l.endswith('.'+subclass.ext)]
if no_dups:
if len(matches) == 1:
return os.path.join(fdir,matches[0])
return os.path.join(fdir, matches[0])
elif matches:
die(1,f'ERROR: more than one {subclass.__name__} file in directory {fdir!r}')
die(1, f'ERROR: more than one {subclass.__name__} file in directory {fdir!r}')
else:
return None
else:
return [os.path.join(fdir,m) for m in matches]
return [os.path.join(fdir, m) for m in matches]
def find_file_in_dir(subclass,fdir):
return find_files_in_dir(subclass,fdir,no_dups=True)
def find_file_in_dir(subclass, fdir):
return find_files_in_dir(subclass, fdir, no_dups=True)

View file

@ -22,7 +22,7 @@ fileutil: Routines that read, write, execute or stat files
# 09 Mar 2024: make all utils accept Path instances as arguments
import sys,os
import sys, os
from .color import set_vt100
from .util import (
@ -48,42 +48,42 @@ def check_or_create_dir(path):
str(path)])
set_vt100()
try:
os.makedirs(path,0o700)
os.makedirs(path, 0o700)
except:
die(2, f'ERROR: unable to read or create path ‘{path}')
def check_binary(args):
from subprocess import run,DEVNULL
from subprocess import run, DEVNULL
try:
run(args,stdout=DEVNULL,stderr=DEVNULL,check=True)
run(args, stdout=DEVNULL, stderr=DEVNULL, check=True)
except:
die(2,f'{args[0]!r} binary missing, not in path, or not executable')
die(2, f'{args[0]!r} binary missing, not in path, or not executable')
set_vt100()
def shred_file(fn,verbose=False):
check_binary(['shred','--version'])
def shred_file(fn, verbose=False):
check_binary(['shred', '--version'])
from subprocess import run
run(
['shred','--force','--iterations=30','--zero','--remove=wipesync']
['shred', '--force', '--iterations=30', '--zero', '--remove=wipesync']
+ (['--verbose'] if verbose else [])
+ [str(fn)],
check=True )
check=True)
set_vt100()
def _check_file_type_and_access(fname,ftype,blkdev_ok=False):
def _check_file_type_and_access(fname, ftype, blkdev_ok=False):
import stat
access,op_desc = (
(os.W_OK,'writ') if ftype in ('output file','output directory') else
(os.R_OK,'read') )
access, op_desc = (
(os.W_OK, 'writ') if ftype in ('output file', 'output directory') else
(os.R_OK, 'read'))
if ftype == 'output directory':
ok_types = [(stat.S_ISDIR, 'output directory')]
else:
ok_types = [
(stat.S_ISREG,'regular file'),
(stat.S_ISLNK,'symbolic link')
(stat.S_ISREG, 'regular file'),
(stat.S_ISLNK, 'symbolic link')
]
if blkdev_ok:
if not sys.platform in ('win32',):
@ -98,31 +98,31 @@ def _check_file_type_and_access(fname,ftype,blkdev_ok=False):
if t[0](mode):
break
else:
ok_list = ' or '.join( t[1] for t in ok_types )
ok_list = ' or '.join(t[1] for t in ok_types)
die(1, f'Requested {ftype}{fname}’ is not a {ok_list}')
if not os.access(fname,access):
if not os.access(fname, access):
die(1, f'Requested {ftype}{fname}’ is not {op_desc}able by you')
return True
def check_infile(f,blkdev_ok=False):
return _check_file_type_and_access(f,'input file',blkdev_ok=blkdev_ok)
def check_infile(f, blkdev_ok=False):
return _check_file_type_and_access(f, 'input file', blkdev_ok=blkdev_ok)
def check_outfile(f,blkdev_ok=False):
return _check_file_type_and_access(f,'output file',blkdev_ok=blkdev_ok)
def check_outfile(f, blkdev_ok=False):
return _check_file_type_and_access(f, 'output file', blkdev_ok=blkdev_ok)
def check_outdir(f):
return _check_file_type_and_access(f,'output directory')
return _check_file_type_and_access(f, 'output directory')
def get_seed_file(cfg,nargs,wallets=None,invoked_as=None):
def get_seed_file(cfg, nargs, wallets=None, invoked_as=None):
wallets = wallets or cfg._args
from .filename import find_file_in_dir
from .wallet.mmgen import wallet
wf = find_file_in_dir(wallet,cfg.data_dir)
wf = find_file_in_dir(wallet, cfg.data_dir)
wd_from_opt = bool(cfg.hidden_incog_input_params or cfg.in_fmt) # have wallet data from opt?
@ -138,16 +138,16 @@ def get_seed_file(cfg,nargs,wallets=None,invoked_as=None):
if wallets or wf:
check_infile(wallets[0] if wallets else wf)
return str(wallets[0]) if wallets else (wf,None)[wd_from_opt] # could be a Path instance
return str(wallets[0]) if wallets else (wf, None)[wd_from_opt] # could be a Path instance
def _open_or_die(filename,mode,silent=False):
def _open_or_die(filename, mode, silent=False):
try:
return open(filename,mode)
return open(filename, mode)
except:
if silent:
die(2,'')
die(2, '')
else:
fn = {0:'STDIN',1:'STDOUT',2:'STDERR'}[filename] if isinstance(filename,int) else f'{filename}'
fn = {0:'STDIN', 1:'STDOUT', 2:'STDERR'}[filename] if isinstance(filename, int) else f'{filename}'
desc = 'reading' if 'r' in mode else 'writing'
die(2, f'Unable to open file {fn} for {desc}')
@ -184,13 +184,13 @@ def write_data_to_file(
cfg._util.qmsg('Output to STDOUT requested')
if cfg.stdin_tty:
if no_tty:
die(2,f'Printing {desc} to screen is not allowed')
die(2, f'Printing {desc} to screen is not allowed')
if (ask_tty and not cfg.quiet) or binary:
from .ui import confirm_or_raise
confirm_or_raise(
cfg,
message = '',
action = f'output {desc} to screen' )
action = f'output {desc} to screen')
else:
try:
of = os.readlink(f'/proc/{os.getpid()}/fd/1') # Linux
@ -200,15 +200,15 @@ def write_data_to_file(
if of:
if of[:5] == 'pipe:':
if no_tty:
die(2,f'Writing {desc} to pipe is not allowed')
die(2, f'Writing {desc} to pipe is not allowed')
if ask_tty and not cfg.quiet:
from .ui import confirm_or_raise
confirm_or_raise(
cfg,
message = '',
action = f'output {desc} to pipe' )
action = f'output {desc} to pipe')
msg('')
of2,pd = os.path.relpath(of),os.path.pardir
of2, pd = os.path.relpath(of), os.path.pardir
msg('Redirecting output to file {!r}'.format(of if of2[:len(pd)] == pd else of2))
else:
msg('Redirecting output to file')
@ -216,15 +216,15 @@ def write_data_to_file(
if binary:
if sys.platform == 'win32': # condition on separate line for pylint
import msvcrt
msvcrt.setmode(sys.stdout.fileno(),os.O_BINARY)
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
# MSWin workaround. See msg_r()
try:
sys.stdout.write(data.decode() if isinstance(data,bytes) else data)
sys.stdout.write(data.decode() if isinstance(data, bytes) else data)
except:
os.write(1,data if isinstance(data,bytes) else data.encode())
os.write(1, data if isinstance(data, bytes) else data.encode())
def do_file(outfile,ask_write_prompt):
def do_file(outfile, ask_write_prompt):
if (outdir or (cfg.outdir and not ignore_opt_outdir)) and not os.path.isabs(outfile):
outfile = make_full_path(str(outdir or cfg.outdir), outfile) # outdir could be Path instance
@ -235,8 +235,8 @@ def write_data_to_file(
if not keypress_confirm(
cfg,
ask_write_prompt,
default_yes = ask_write_default_yes ):
die(1,f'{capfirst(desc)} not saved')
default_yes = ask_write_default_yes):
die(1, f'{capfirst(desc)} not saved')
hush = False
if os.path.lexists(outfile) and ask_overwrite:
@ -244,7 +244,7 @@ def write_data_to_file(
confirm_or_raise(
cfg,
message = '',
action = f'File {outfile!r} already exists\nOverwrite?' )
action = f'File {outfile!r} already exists\nOverwrite?')
msg(f'Overwriting file {outfile!r}')
hush = True
@ -253,34 +253,34 @@ def write_data_to_file(
if check_data:
d = ''
try:
with open(outfile,('r','rb')[bool(binary)]) as fp:
with open(outfile, ('r', 'rb')[bool(binary)]) as fp:
d = fp.read()
except:
pass
if d != cmp_data:
die(3,f'{desc} in file {outfile!r} has been altered by some other program! Aborting file write')
die(3, f'{desc} in file {outfile!r} has been altered by some other program! Aborting file write')
# To maintain portability, always open files in binary mode
# If 'binary' option not set, encode/decode data before writing and after reading
try:
with _open_or_die(outfile,'wb') as fp:
with _open_or_die(outfile, 'wb') as fp:
fp.write(data if binary else data.encode())
except:
die(2,f'Failed to write {desc} to file {outfile!r}')
die(2, f'Failed to write {desc} to file {outfile!r}')
if not (hush or quiet):
msg(f'{capfirst(desc)} written to file {outfile!r}')
return True
if cfg.stdout or outfile in ('','-'):
if cfg.stdout or outfile in ('', '-'):
do_stdout()
elif sys.stdin.isatty() and not sys.stdout.isatty():
do_stdout()
else:
do_file(outfile,ask_write_prompt)
do_file(outfile, ask_write_prompt)
def get_words_from_file(cfg,infile,desc,quiet=False):
def get_words_from_file(cfg, infile, desc, quiet=False):
if not quiet:
cfg._util.qmsg(f'Getting {desc} from file ‘{infile}')
@ -291,7 +291,7 @@ def get_words_from_file(cfg,infile,desc,quiet=False):
try:
words = data.decode().split()
except:
die(1,f'{capfirst(desc)} data must be UTF-8 encoded.')
die(1, f'{capfirst(desc)} data must be UTF-8 encoded.')
cfg._util.dmsg('Sanitized input: [{}]'.format(' '.join(words)))
@ -304,7 +304,7 @@ def get_data_from_file(
dash = False,
silent = False,
binary = False,
quiet = False ):
quiet = False):
if not (cfg.quiet or silent or quiet):
cfg._util.qmsg(f'Getting {desc} from file ‘{infile}')
@ -319,8 +319,8 @@ def get_data_from_file(
data = data.decode()
if len(data) == cfg.max_input_size + 1:
die( 'MaxInputSizeExceeded',
f'Too much input data! Max input data size: {cfg.max_input_size} bytes' )
die('MaxInputSizeExceeded',
f'Too much input data! Max input data size: {cfg.max_input_size} bytes')
return data
@ -330,16 +330,16 @@ def get_lines_from_file(
desc = 'data',
trim_comments = False,
quiet = False,
silent = False ):
silent = False):
def decrypt_file_maybe():
data = get_data_from_file( cfg, fn, desc=desc, binary=True, quiet=quiet, silent=silent )
data = get_data_from_file(cfg, fn, desc=desc, binary=True, quiet=quiet, silent=silent)
from .crypto import Crypto
have_enc_ext = get_extension(fn) == Crypto.mmenc_ext
if have_enc_ext or not is_utf8(data):
m = ('Attempting to decrypt','Decrypting')[have_enc_ext]
m = ('Attempting to decrypt', 'Decrypting')[have_enc_ext]
cfg._util.qmsg(f'{m} {desc}{fn}')
data = Crypto(cfg).mmgen_decrypt_retry(data,desc)
data = Crypto(cfg).mmgen_decrypt_retry(data, desc)
return data
lines = decrypt_file_maybe().decode().splitlines()

View file

@ -20,27 +20,27 @@
flags: Class flags and opts for the MMGen suite
"""
from .base_obj import AttrCtrl,Lockable
from .util import fmt_list,die
from .base_obj import AttrCtrl, Lockable
from .util import fmt_list, die
class ClassFlags(AttrCtrl):
_name = 'flags'
_desc = 'flag'
reserved_attrs = ()
def __init__(self,parent,arg):
def __init__(self, parent, arg):
self._parent = parent
self._available = getattr(self._parent,'avail_'+self._name)
self._available = getattr(self._parent, 'avail_'+self._name)
for a in self._available:
if a.startswith('_'):
die( 'ClassFlagsError', f'{a!r}: {self._desc} cannot begin with an underscore' )
die('ClassFlagsError', f'{a!r}: {self._desc} cannot begin with an underscore')
for b in self.reserved_attrs:
if a == b:
die( 'ClassFlagsError', f'{a!r}: {b} is a reserved name for {self._desc}' )
die('ClassFlagsError', f'{a!r}: {b} is a reserved name for {self._desc}')
if arg:
assert type(arg) in (list,tuple), f"{arg!r}: {self._name!r} must be list or tuple"
assert type(arg) in (list, tuple), f"{arg!r}: {self._name!r} must be list or tuple"
else:
arg = []
@ -49,15 +49,15 @@ class ClassFlags(AttrCtrl):
self.not_available_error(e)
for e in self._available:
setattr(self,e,e in arg)
setattr(self, e, e in arg)
def __dir__(self):
return [k for k in self.__dict__ if not k.startswith('_') and not k in self.reserved_attrs]
def __str__(self):
return ' '.join(f'{k}={getattr(self,k)}' for k in dir(self))
return ' '.join(f'{k}={getattr(self, k)}' for k in dir(self))
def __setattr__(self,name,val):
def __setattr__(self, name, val):
if self._locked:
@ -65,23 +65,23 @@ class ClassFlags(AttrCtrl):
self.not_available_error(name)
if self._name == 'flags':
assert isinstance(val,bool), f'{val!r} not boolean'
old_val = getattr(self,name)
assert isinstance(val, bool), f'{val!r} not boolean'
old_val = getattr(self, name)
if val and old_val:
die( 'ClassFlagsError', f'{self._desc} {name!r} already set' )
die('ClassFlagsError', f'{self._desc} {name!r} already set')
if not val and not old_val:
die( 'ClassFlagsError', f'{self._desc} {name!r} not set, so cannot be unset' )
die('ClassFlagsError', f'{self._desc} {name!r} not set, so cannot be unset')
super().__setattr__(name,val)
super().__setattr__(name, val)
def not_available_error(self,name):
die( 'ClassFlagsError', '{!r}: unrecognized {} for {}: (available {}: {})'.format(
def not_available_error(self, name):
die('ClassFlagsError', '{!r}: unrecognized {} for {}: (available {}: {})'.format(
name,
self._desc,
type(self._parent).__name__,
self._name,
fmt_list(self._available,fmt='bare') ))
fmt_list(self._available, fmt='bare')))
class ClassOpts(ClassFlags,Lockable):
class ClassOpts(ClassFlags, Lockable):
_name = 'opts'
_desc = 'opt'

View file

@ -20,41 +20,41 @@
key: MMGen public and private key objects
"""
from .objmethods import HiliteStr,InitErrors,MMGenObject
from .obj import ImmutableAttr,get_obj
from .objmethods import HiliteStr, InitErrors, MMGenObject
from .obj import ImmutableAttr, get_obj
class WifKey(HiliteStr,InitErrors):
class WifKey(HiliteStr, InitErrors):
"""
Initialize a WIF key, checking its well-formedness.
The numeric validity of the private key it encodes is not checked.
"""
width = 53
color = 'blue'
def __new__(cls,proto,wif):
if isinstance(wif,cls):
def __new__(cls, proto, wif):
if isinstance(wif, cls):
return wif
try:
assert wif.isascii() and wif.isalnum(), 'not an ASCII alphanumeric string'
proto.decode_wif(wif) # raises exception on error
return str.__new__(cls,wif)
return str.__new__(cls, wif)
except Exception as e:
return cls.init_fail(e,wif)
return cls.init_fail(e, wif)
def is_wif(proto,s):
return get_obj( WifKey, proto=proto, wif=s, silent=True, return_bool=True )
def is_wif(proto, s):
return get_obj(WifKey, proto=proto, wif=s, silent=True, return_bool=True)
class PubKey(bytes,InitErrors,MMGenObject): # TODO: add some real checks
class PubKey(bytes, InitErrors, MMGenObject): # TODO: add some real checks
def __new__(cls,s,compressed):
def __new__(cls, s, compressed):
try:
assert isinstance(s,bytes)
me = bytes.__new__(cls,s)
assert isinstance(s, bytes)
me = bytes.__new__(cls, s)
me.compressed = compressed
return me
except Exception as e:
return cls.init_fail(e,s)
return cls.init_fail(e, s)
class PrivKey(bytes,InitErrors,MMGenObject):
class PrivKey(bytes, InitErrors, MMGenObject):
"""
Input: a) raw, non-preprocessed bytes; or b) WIF key.
Output: preprocessed key bytes, plus WIF key in 'wif' attribute
@ -65,49 +65,49 @@ class PrivKey(bytes,InitErrors,MMGenObject):
width = 32
trunc_ok = False
compressed = ImmutableAttr(bool,typeconv=False)
wif = ImmutableAttr(WifKey,typeconv=False)
compressed = ImmutableAttr(bool, typeconv=False)
wif = ImmutableAttr(WifKey, typeconv=False)
# initialize with (priv_bin,compressed), WIF or self
def __new__(cls,proto,s=None,compressed=None,wif=None,pubkey_type=None):
if isinstance(s,cls):
# initialize with (priv_bin, compressed), WIF or self
def __new__(cls, proto, s=None, compressed=None, wif=None, pubkey_type=None):
if isinstance(s, cls):
return s
if wif:
try:
assert s is None,"'wif' and key hex args are mutually exclusive"
assert s is None, "'wif' and key hex args are mutually exclusive"
assert wif.isascii() and wif.isalnum(), 'not an ASCII alphanumeric string'
k = proto.decode_wif(wif) # raises exception on error
me = bytes.__new__(cls,k.sec)
me = bytes.__new__(cls, k.sec)
me.compressed = k.compressed
me.pubkey_type = k.pubkey_type
me.wif = str.__new__(WifKey,wif) # check has been done
me.wif = str.__new__(WifKey, wif) # check has been done
me.orig_bytes = None
if k.sec != proto.preprocess_key(k.sec,k.pubkey_type):
if k.sec != proto.preprocess_key(k.sec, k.pubkey_type):
from .util import die
die( 'PrivateKeyError',
f'{proto.cls_name} WIF key {me.wif!r} encodes private key with invalid value {me}' )
die('PrivateKeyError',
f'{proto.cls_name} WIF key {me.wif!r} encodes private key with invalid value {me}')
me.proto = proto
return me
except Exception as e:
return cls.init_fail(e,s,objname=f'{proto.coin} WIF key')
return cls.init_fail(e, s, objname=f'{proto.coin} WIF key')
else:
try:
assert s,'private key bytes data missing'
assert isinstance(s,bytes),'input is not bytes'
assert pubkey_type is not None,"'pubkey_type' arg missing"
assert s, 'private key bytes data missing'
assert isinstance(s, bytes), 'input is not bytes'
assert pubkey_type is not None, "'pubkey_type' arg missing"
assert len(s) == cls.width, f'key length must be {cls.width} bytes'
if pubkey_type == 'password': # skip WIF creation and pre-processing for passwds
me = bytes.__new__(cls,s)
me = bytes.__new__(cls, s)
else:
assert compressed is not None, "'compressed' arg missing"
assert type(compressed) is bool,(
f"'compressed' must be of type bool, not {type(compressed).__name__}" )
me = bytes.__new__( cls, proto.preprocess_key(s,pubkey_type) )
me.wif = WifKey( proto, proto.encode_wif(me,pubkey_type,compressed) )
assert type(compressed) is bool, (
f"'compressed' must be of type bool, not {type(compressed).__name__}")
me = bytes.__new__(cls, proto.preprocess_key(s, pubkey_type))
me.wif = WifKey(proto, proto.encode_wif(me, pubkey_type, compressed))
me.compressed = compressed
me.pubkey_type = pubkey_type
me.orig_bytes = s # save the non-preprocessed key
me.proto = proto
return me
except Exception as e:
return cls.init_fail(e,s)
return cls.init_fail(e, s)

View file

@ -28,39 +28,39 @@ keygen_public_data = namedtuple(
'pubkey',
'viewkey_bytes',
'pubkey_type',
'compressed' ])
'compressed'])
class keygen_base:
def __init__(self,cfg):
def __init__(self, cfg):
if not (self.production_safe or cfg.test_suite):
from .util import die
die(2,
f'Public key generator {type(self).__name__!r} is not safe from timing attacks '
'and may only be used in a testing environment')
def gen_data(self,privkey):
assert isinstance(privkey,PrivKey)
def gen_data(self, privkey):
assert isinstance(privkey, PrivKey)
return keygen_public_data(
self.to_pubkey(privkey),
self.to_viewkey(privkey),
privkey.pubkey_type,
privkey.compressed )
privkey.compressed)
def to_viewkey(self,privkey):
def to_viewkey(self, privkey):
return None
@classmethod
def get_clsname(cls,cfg,silent=False):
def get_clsname(cls, cfg, silent=False):
return cls.__name__
backend_data = {
'std': {
'backends': ('libsecp256k1','python-ecdsa'),
'backends': ('libsecp256k1', 'python-ecdsa'),
'package': 'secp256k1',
},
'monero': {
'backends': ('nacl','ed25519ll-djbec','ed25519'),
'backends': ('nacl', 'ed25519ll-djbec', 'ed25519'),
'package': 'xmr',
},
'zcash_z': {
@ -76,11 +76,11 @@ def get_pubkey_type_cls(pubkey_type):
import importlib
return getattr(
importlib.import_module(f'mmgen.proto.{backend_data[pubkey_type]["package"]}.keygen'),
'backend' )
'backend')
def _check_backend(cfg,backend,pubkey_type,desc='keygen backend'):
def _check_backend(cfg, backend, pubkey_type, desc='keygen backend'):
from .util import is_int,die
from .util import is_int, die
assert is_int(backend), f'illegal value for {desc} (must be an integer)'
@ -90,25 +90,21 @@ def _check_backend(cfg,backend,pubkey_type,desc='keygen backend'):
die(1,
f'{backend}: {desc} out of range\n' +
'Configured backends: ' +
' '.join( f'{n}:{k}' for n,k in enumerate(backends,1) )
' '.join(f'{n}:{k}' for n, k in enumerate(backends, 1))
)
cfg._util.qmsg(f'Using backend {backends[int(backend)-1]!r} for public key generation')
return True
def check_backend(cfg,proto,backend,addr_type):
def check_backend(cfg, proto, backend, addr_type):
from .addr import MMGenAddrType
pubkey_type = MMGenAddrType(proto,addr_type or proto.dfl_mmtype).pubkey_type
pubkey_type = MMGenAddrType(proto, addr_type or proto.dfl_mmtype).pubkey_type
return _check_backend(
cfg,
backend,
pubkey_type,
desc = '--keygen-backend parameter' )
return _check_backend(cfg, backend, pubkey_type, desc='--keygen-backend parameter')
def KeyGenerator(cfg,proto,pubkey_type,backend=None,silent=False):
def KeyGenerator(cfg, proto, pubkey_type, backend=None, silent=False):
"""
factory function returning a key generator backend for the specified pubkey type
"""
@ -119,13 +115,13 @@ def KeyGenerator(cfg,proto,pubkey_type,backend=None,silent=False):
backend = backend or cfg.keygen_backend
if backend:
_check_backend( cfg, backend, pubkey_type )
_check_backend(cfg, backend, pubkey_type)
backend_id = backend_data[pubkey_type]['backends'][int(backend) - 1 if backend else 0]
backend_clsname = getattr(
pubkey_type_cls,
backend_id.replace('-','_')
).get_clsname(cfg,silent=silent)
backend_id.replace('-', '_')
).get_clsname(cfg, silent=silent)
return getattr(pubkey_type_cls,backend_clsname)(cfg)
return getattr(pubkey_type_cls, backend_clsname)(cfg)

View file

@ -29,31 +29,31 @@ from .color import blue, orange
class LEDControl:
binfo = namedtuple('board_info',['name','status','trigger','trigger_states'])
binfo = namedtuple('board_info', ['name', 'status', 'trigger', 'trigger_states'])
boards = {
'raspi_pi': binfo(
name = 'Raspberry Pi',
status = '/sys/class/leds/led0/brightness',
trigger = '/sys/class/leds/led0/trigger',
trigger_states = ('none','mmc0') ),
trigger_states = ('none', 'mmc0')),
'orange_pi': binfo(
name = 'Orange Pi (Armbian)',
status = '/sys/class/leds/orangepi:red:status/brightness',
trigger = None,
trigger_states = None ),
trigger_states = None),
'rock_pi': binfo(
name = 'Rock Pi (Armbian)',
status = '/sys/class/leds/status/brightness',
trigger = '/sys/class/leds/status/trigger',
trigger_states = ('none','heartbeat') ),
trigger_states = ('none', 'heartbeat')),
'dummy': binfo(
name = 'Fake',
status = '/tmp/led_status',
trigger = '/tmp/led_trigger',
trigger_states = ('none','original_value') ),
trigger_states = ('none', 'original_value')),
}
def __init__(self,enabled,simulate=False,debug=False):
def __init__(self, enabled, simulate=False, debug=False):
self.enabled = enabled
self.debug = debug or simulate
@ -65,7 +65,7 @@ class LEDControl:
self.ev = threading.Event()
self.led_thread = None
for board_id,board in self.boards.items():
for board_id, board in self.boards.items():
if board_id == 'dummy' and not simulate:
continue
try:
@ -75,20 +75,20 @@ class LEDControl:
else:
break
else:
die( 'NoLEDSupport', 'Control files not found! LED control not supported on this system' )
die('NoLEDSupport', 'Control files not found! LED control not supported on this system')
msg(f'{board.name} board detected')
if self.debug:
msg(f'\n Status file: {board.status}\n Trigger file: {board.trigger}')
def check_access(fn,desc,init_val=None):
def check_access(fn, desc, init_val=None):
def write_init_val(init_val):
if not init_val:
with open(fn) as fp:
init_val = fp.read().strip()
with open(fn,'w') as fp:
with open(fn, 'w') as fp:
fp.write(f'{init_val}\n')
try:
@ -107,58 +107,58 @@ class LEDControl:
))
sys.exit(1)
check_access(board.status,desc='status LED control')
check_access(board.status, desc='status LED control')
if board.trigger:
check_access(board.trigger,desc='LED trigger',init_val=board.trigger_states[0])
check_access(board.trigger, desc='LED trigger', init_val=board.trigger_states[0])
self.board = board
@classmethod
def create_dummy_control_files(cls):
db = cls.boards['dummy']
with open(db.status,'w') as fp:
with open(db.status, 'w') as fp:
fp.write('0\n')
with open(db.trigger,'w') as fp:
with open(db.trigger, 'w') as fp:
fp.write(db.trigger_states[1]+'\n')
def noop(self,*args,**kwargs):
def noop(self, *args, **kwargs):
pass
def ev_sleep(self,secs):
def ev_sleep(self, secs):
self.ev.wait(secs)
return self.ev.is_set()
def led_loop(self,on_secs,off_secs):
def led_loop(self, on_secs, off_secs):
if self.debug:
msg(f'led_loop({on_secs},{off_secs})')
msg(f'led_loop({on_secs}, {off_secs})')
if not on_secs:
with open(self.board.status,'w') as fp:
with open(self.board.status, 'w') as fp:
fp.write('0\n')
while True:
if self.ev_sleep(3600):
return
while True:
for s_time,val in ((on_secs,255),(off_secs,0)):
for s_time, val in ((on_secs, 255), (off_secs, 0)):
if self.debug:
msg_r(('^','+')[bool(val)])
with open(self.board.status,'w') as fp:
msg_r(('^', '+')[bool(val)])
with open(self.board.status, 'w') as fp:
fp.write(f'{val}\n')
if self.ev_sleep(s_time):
if self.debug:
msg('\n')
return
def set(self,state):
lt = namedtuple('led_timings',['on_secs','off_secs'])
def set(self, state):
lt = namedtuple('led_timings', ['on_secs', 'off_secs'])
timings = {
'off': lt( 0, 0 ),
'standby': lt( 2.2, 0.2 ),
'busy': lt( 0.06, 0.06 ),
'error': lt( 0.5, 0.5 ) }
'off': lt(0, 0),
'standby': lt(2.2, 0.2),
'busy': lt(0.06, 0.06),
'error': lt(0.5, 0.5)}
if self.led_thread:
self.ev.set()
@ -186,5 +186,5 @@ class LEDControl:
msg('Stopping LED')
if self.board.trigger:
with open(self.board.trigger,'w') as fp:
with open(self.board.trigger, 'w') as fp:
fp.write(self.board.trigger_states[1]+'\n')

View file

@ -20,15 +20,15 @@
main: Script launcher for the MMGen Project
"""
import sys,os
import sys, os
def launch(*, mod=None, func=None, fqmod=None, package='mmgen'):
if sys.platform in ('linux', 'darwin') and sys.stdin.isatty():
import termios,atexit
import termios, atexit
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
atexit.register(lambda: termios.tcsetattr(fd,termios.TCSADRAIN,old))
atexit.register(lambda: termios.tcsetattr(fd, termios.TCSADRAIN, old))
try:
__import__(f'{package}.main_{mod}') if mod else func() if func else __import__(fqmod)
@ -47,9 +47,9 @@ def launch(*, mod=None, func=None, fqmod=None, package='mmgen'):
errmsg = repr(e.args[0]) if e.args else repr(e)
from collections import namedtuple
from .color import nocolor,yellow,red
from .color import nocolor, yellow, red
_o = namedtuple('exit_data',['color','exit_val','fs'])
_o = namedtuple('exit_data', ['color', 'exit_val', 'fs'])
d = {
0: _o(nocolor, 0, '{message}'),
1: _o(nocolor, 1, '{message}'),
@ -57,14 +57,14 @@ def launch(*, mod=None, func=None, fqmod=None, package='mmgen'):
3: _o(yellow, 3, '\nMMGen Error ({name}):\n{message}'),
4: _o(red, 4, '\nMMGen Fatal Error ({name}):\n{message}'),
'x': _o(yellow, 5, '\nMMGen Unhandled Exception ({name}): {e}'),
}[getattr(e,'mmcode','x')]
}[getattr(e, 'mmcode', 'x')]
(sys.stdout if getattr(e,'stdout',None) else sys.stderr).write(
(sys.stdout if getattr(e, 'stdout', None) else sys.stderr).write(
d.color(d.fs.format(
name = type(e).__name__,
message = errmsg.strip() or e,
e = e))
+ '\n' )
+ '\n')
if os.getenv('MMGEN_EXEC_WRAPPER') or os.getenv('MMGEN_TRACEBACK'):
raise

View file

@ -20,10 +20,10 @@
mn_entry.py - Mnemonic user entry methods for the MMGen suite
"""
import sys,time
import sys, time
from .util import msg,msg_r,fmt,fmt_list,capfirst,die,ascii_lowercase
from .term import get_char,get_char_raw
from .util import msg, msg_r, fmt, fmt_list, capfirst, die, ascii_lowercase
from .term import get_char, get_char_raw
from .color import cyan
_return_chars = '\n\r '
@ -43,19 +43,19 @@ class MnEntryMode:
pad characters per word are permitted.
"""
def __init__(self,mne):
def __init__(self, mne):
self.pad_max_info = ' ' + self.pad_max_info.lstrip() if self.pad_max else '\n'
self.mne = mne
def get_char(self,s):
def get_char(self, s):
did_erase = False
while True:
ch = get_char_raw('',num_bytes=1)
ch = get_char_raw('', num_bytes=1)
if s and ch in _erase_chars:
s = s[:-1]
did_erase = True
else:
return (ch,s,did_erase)
return (ch, s, did_erase)
class MnEntryModeFull(MnEntryMode):
name = 'Full'
@ -73,10 +73,10 @@ class MnEntryModeFull(MnEntryMode):
def ss_len(self):
return self.mne.longest_word
def get_word(self,mne):
s,pad = ('', 0)
def get_word(self, mne):
s, pad = ('', 0)
while True:
ch,s,_ = self.get_char(s)
ch, s, _ = self.get_char(s)
if ch in _return_chars:
if s:
break
@ -87,7 +87,7 @@ class MnEntryModeFull(MnEntryMode):
if pad + len(s) > self.ss_len:
break
return mne.idx(s,'full')
return mne.idx(s, 'full')
class MnEntryModeShort(MnEntryMode):
name = 'Short'
@ -104,7 +104,7 @@ class MnEntryModeShort(MnEntryMode):
"""
pad_max = 16
def __init__(self,mne):
def __init__(self, mne):
if mne.wl_id == 'bip39':
self.prompt_info += ' ' + self.prompt_info_bip39_add.strip()
super().__init__(mne)
@ -113,10 +113,10 @@ class MnEntryModeShort(MnEntryMode):
def ss_len(self):
return self.mne.uniq_ss_len
def get_word(self,mne):
s,pad = ('', 0)
def get_word(self, mne):
s, pad = ('', 0)
while True:
ch,s,_ = self.get_char(s)
ch, s, _ = self.get_char(s)
if ch in _return_chars:
if s:
break
@ -129,7 +129,7 @@ class MnEntryModeShort(MnEntryMode):
if pad > self.pad_max:
break
return mne.idx(s,'short')
return mne.idx(s, 'short')
class MnEntryModeFixed(MnEntryMode):
name = 'Fixed'
@ -140,14 +140,14 @@ class MnEntryModeFixed(MnEntryMode):
prompt_info = """
Each word is entered automatically once exactly {ssl} characters are typed.
"""
prompt_info_add = ( """
prompt_info_add = ("""
Words shorter than {ssl} letters must be padded to fit.
""", """
{sw}-letter words must be padded with one pad character.
""" )
""")
pad_max = None
def __init__(self,mne):
def __init__(self, mne):
self.len_diff = mne.uniq_ss_len - mne.shortest_word
self.prompt_info += self.prompt_info_add[self.len_diff==1].lstrip()
super().__init__(mne)
@ -156,23 +156,23 @@ class MnEntryModeFixed(MnEntryMode):
def ss_len(self):
return self.mne.uniq_ss_len
def get_word(self,mne):
s,pad = ('', 0)
def get_word(self, mne):
s, pad = ('', 0)
while True:
ch,s,_ = self.get_char(s)
ch, s, _ = self.get_char(s)
if ch in _return_chars:
if s:
break
elif ch in ascii_lowercase:
s += ch
if len(s) + pad == self.ss_len:
return mne.idx(s,'short')
return mne.idx(s, 'short')
else:
pad += 1
if pad > self.len_diff:
return None
if len(s) + pad == self.ss_len:
return mne.idx(s,'short')
return mne.idx(s, 'short')
class MnEntryModeMinimal(MnEntryMode):
name = 'Minimal'
@ -191,26 +191,26 @@ class MnEntryModeMinimal(MnEntryMode):
pad_max = 16
ss_len = None
def get_word(self,mne):
s,pad = ('', 0)
lo,hi = (0, len(mne.wl) - 1)
def get_word(self, mne):
s, pad = ('', 0)
lo, hi = (0, len(mne.wl) - 1)
while True:
ch,s,did_erase = self.get_char(s)
ch, s, did_erase = self.get_char(s)
if did_erase:
lo,hi = (0, len(mne.wl) - 1)
lo, hi = (0, len(mne.wl) - 1)
if ch in _return_chars:
if s:
return mne.idx(s,'full',lo_idx=lo,hi_idx=hi)
return mne.idx(s, 'full', lo_idx=lo, hi_idx=hi)
elif ch in ascii_lowercase:
s += ch
ret = mne.idx(s,'minimal',lo_idx=lo,hi_idx=hi)
if not isinstance(ret,tuple):
ret = mne.idx(s, 'minimal', lo_idx=lo, hi_idx=hi)
if not isinstance(ret, tuple):
return ret
lo,hi = ret
lo, hi = ret
else:
pad += 1
if pad > self.pad_max:
return mne.idx(s,'full',lo_idx=lo,hi_idx=hi)
return mne.idx(s, 'full', lo_idx=lo, hi_idx=hi)
class MnemonicEntry:
@ -225,13 +225,13 @@ class MnemonicEntry:
acters may be typed before, after, or in the middle of words.
""",
}
word_prompt = ('Enter word #{}: ','Incorrect entry. Repeat word #{}: ')
word_prompt = ('Enter word #{}: ', 'Incorrect entry. Repeat word #{}: ')
usr_dfl_entry_mode = None
_lw = None
_sw = None
_usl = None
def __init__(self,cfg):
def __init__(self, cfg):
self.cfg = cfg
self.set_dfl_entry_mode()
@ -252,7 +252,7 @@ class MnemonicEntry:
if not self._usl:
usl = 0
for i in range(len(self.wl)-1):
w1,w2 = self.wl[i],self.wl[i+1]
w1, w2 = self.wl[i], self.wl[i+1]
while True:
if w1[:usl] == w2[:usl]:
usl += 1
@ -261,7 +261,7 @@ class MnemonicEntry:
self._usl = usl
return self._usl
def idx(self,w,entry_mode,lo_idx=None,hi_idx=None):
def idx(self, w, entry_mode, lo_idx=None, hi_idx=None):
"""
Return values:
- all modes:
@ -285,9 +285,9 @@ class MnemonicEntry:
if cur_w == w:
if entry_mode == 'minimal':
if idx > 0 and self.wl[idx-1][:len(w)] == w:
return (lo,hi)
return (lo, hi)
elif idx < last_idx and self.wl[idx+1][:len(w)] == w:
return (lo,hi)
return (lo, hi)
return idx
elif hi <= lo:
return None
@ -296,17 +296,17 @@ class MnemonicEntry:
else:
lo = idx + 1
def get_cls_by_entry_mode(self,entry_mode):
def get_cls_by_entry_mode(self, entry_mode):
return getattr(sys.modules[__name__], 'MnEntryMode' + capfirst(entry_mode))
def choose_entry_mode(self):
msg('Choose an entry mode:\n')
em_objs = [self.get_cls_by_entry_mode(entry_mode)(self) for entry_mode in self.entry_modes]
for n,mode in enumerate(em_objs,1):
for n, mode in enumerate(em_objs, 1):
msg(' {}) {:8} {}'.format(
n,
mode.name + ':',
fmt(mode.choose_info,' '*14).lstrip().format(usl=self.uniq_ss_len),
fmt(mode.choose_info, ' '*14).lstrip().format(usl=self.uniq_ss_len),
))
prompt = f'Type a number, or hit ENTER for the default ({capfirst(self.dfl_entry_mode)}): '
erase = '\r' + ' ' * (len(prompt)+19) + '\r'
@ -315,7 +315,7 @@ class MnemonicEntry:
if uret == '':
msg_r(erase)
return self.get_cls_by_entry_mode(self.dfl_entry_mode)(self)
elif uret in [str(i) for i in range(1,len(em_objs)+1)]:
elif uret in [str(i) for i in range(1, len(em_objs)+1)]:
msg_r(erase)
return em_objs[int(uret)-1]
else:
@ -323,7 +323,7 @@ class MnemonicEntry:
time.sleep(self.cfg.err_disp_timeout)
msg_r(erase)
def get_mnemonic_from_user(self,mn_len,validate=True):
def get_mnemonic_from_user(self, mn_len, validate=True):
mll = list(self.bconv.seedlen_map_rev)
assert mn_len in mll, f'{mn_len}: invalid mnemonic length (must be one of {mll})'
@ -349,8 +349,8 @@ class MnemonicEntry:
sw = self.shortest_word,
))
clear_line = '\n' if self.cfg.test_suite else '{r}{s}{r}'.format(r='\r',s=' '*40)
idx,idxs = 1,[] # initialize idx to a non-None value
clear_line = '\n' if self.cfg.test_suite else '{r}{s}{r}'.format(r='\r', s=' '*40)
idx, idxs = 1, [] # initialize idx to a non-None value
while len(idxs) < mn_len:
msg_r(self.word_prompt[idx is None].format(len(idxs)+1))
@ -369,7 +369,7 @@ class MnemonicEntry:
return ' '.join(words)
@classmethod
def get_cls_by_wordlist(cls,wl):
def get_cls_by_wordlist(cls, wl):
d = {
'mmgen': MnemonicEntryMMGen,
'bip39': MnemonicEntryBIP39,
@ -385,7 +385,7 @@ class MnemonicEntry:
In addition to setting the default entry mode for the current wordlist, checks validity
of all user-configured entry modes
"""
for k,v in self.cfg.mnemonic_entry_modes.items():
for k, v in self.cfg.mnemonic_entry_modes.items():
cls = self.get_cls_by_wordlist(k)
if v not in cls.entry_modes:
errmsg = f"""
@ -393,37 +393,37 @@ class MnemonicEntry:
Entry mode {v!r} not recognized for wordlist {k!r}:
Valid choices: {fmt_list(cls.entry_modes)}
"""
die(2, '\n' + fmt(errmsg,indent=' '))
die(2, '\n' + fmt(errmsg, indent=' '))
if cls == type(self):
self.usr_dfl_entry_mode = v
class MnemonicEntryMMGen(MnemonicEntry):
wl_id = 'mmgen'
modname = 'baseconv'
entry_modes = ('full','minimal','fixed')
entry_modes = ('full', 'minimal', 'fixed')
dfl_entry_mode = 'minimal'
has_chksum = False
class MnemonicEntryBIP39(MnemonicEntry):
wl_id = 'bip39'
modname = 'bip39'
entry_modes = ('full','short','fixed')
entry_modes = ('full', 'short', 'fixed')
dfl_entry_mode = 'fixed'
has_chksum = True
class MnemonicEntryMonero(MnemonicEntry):
wl_id = 'xmrseed'
modname = 'xmrseed'
entry_modes = ('full','short')
entry_modes = ('full', 'short')
dfl_entry_mode = 'short'
has_chksum = True
def mn_entry(cfg,wl_id,entry_mode=None):
def mn_entry(cfg, wl_id, entry_mode=None):
if wl_id == 'words':
wl_id = 'mmgen'
me = MnemonicEntry.get_cls_by_wordlist(wl_id)(cfg)
import importlib
me.bconv = getattr(importlib.import_module(f'mmgen.{me.modname}'),me.modname)(wl_id)
me.bconv = getattr(importlib.import_module(f'mmgen.{me.modname}'), me.modname)(wl_id)
me.wl = me.bconv.digits
if entry_mode:
me.em = getattr(sys.modules[__name__], 'MnEntryMode' + capfirst(entry_mode))(me)

View file

@ -12,40 +12,40 @@
msg: base message signing classes
"""
import os,importlib,json
import os, importlib, json
from .cfg import gc
from .objmethods import MMGenObject,HiliteStr,InitErrors
from .util import msg,die,make_chksum_6,fmt_list,remove_dups
from .color import red,orange,grnbg
from .objmethods import MMGenObject, HiliteStr, InitErrors
from .util import msg, die, make_chksum_6, fmt_list, remove_dups
from .color import red, orange, grnbg
from .protocol import init_proto
from .fileutil import get_data_from_file,write_data_to_file
from .addr import MMGenID,CoinAddr
from .fileutil import get_data_from_file, write_data_to_file
from .addr import MMGenID, CoinAddr
class MMGenIDRange(HiliteStr,InitErrors,MMGenObject):
class MMGenIDRange(HiliteStr, InitErrors, MMGenObject):
"""
closely based on MMGenID
"""
color = 'orange'
width = 0
trunc_ok = False
def __new__(cls,proto,id_str):
def __new__(cls, proto, id_str):
from .addrlist import AddrIdxList
from .addr import AddrListID
from .seed import SeedID
try:
ss = str(id_str).split(':')
assert len(ss) in (2,3),'not 2 or 3 colon-separated items'
t = proto.addr_type((ss[1],proto.dfl_mmtype)[len(ss)==2])
me = str.__new__(cls,'{}:{}:{}'.format(ss[0],t,ss[-1]))
assert len(ss) in (2, 3), 'not 2 or 3 colon-separated items'
t = proto.addr_type((ss[1], proto.dfl_mmtype)[len(ss)==2])
me = str.__new__(cls, '{}:{}:{}'.format(ss[0], t, ss[-1]))
me.sid = SeedID(sid=ss[0])
me.idxlist = AddrIdxList(ss[-1])
me.mmtype = t
assert t in proto.mmtypes, f'{t}: invalid address type for {proto.cls_name}'
me.al_id = str.__new__(AddrListID,me.sid+':'+me.mmtype) # checks already done
me.al_id = str.__new__(AddrListID, me.sid+':'+me.mmtype) # checks already done
me.proto = proto
return me
except Exception as e:
return cls.init_fail(e,id_str)
return cls.init_fail(e, id_str)
class coin_msg:
@ -53,7 +53,7 @@ class coin_msg:
ext = 'rawmsg.json'
signed = False
chksum_keys = ('addrlists','message','msghash_type','network')
chksum_keys = ('addrlists', 'message', 'msghash_type', 'network')
@property
def desc(self):
@ -70,11 +70,11 @@ class coin_msg:
@property
def filename_stem(self):
coin,network = self.data['network'].split('_')
coin, network = self.data['network'].split('_')
return '{}[{}]{}'.format(
self.chksum.upper(),
coin.upper(),
('' if network == 'mainnet' else '.'+network) )
('' if network == 'mainnet' else '.'+network))
@property
def filename(self):
@ -85,13 +85,13 @@ class coin_msg:
return f'{self.filename_stem}.{coin_msg.signed.ext}'
@staticmethod
def get_proto_from_file(cfg,filename):
def get_proto_from_file(cfg, filename):
data = json.loads(get_data_from_file(cfg, filename))
network_id = data['metadata']['network'] if 'metadata' in data else data['network'].lower()
coin,network = network_id.split('_')
coin, network = network_id.split('_')
return init_proto(cfg=cfg, coin=coin, network=network)
def write_to_file(self,outdir=None,ask_overwrite=False):
def write_to_file(self, outdir=None, ask_overwrite=False):
data = {
'id': f'{gc.proj_name} {self.desc}',
'metadata': self.data,
@ -100,23 +100,23 @@ class coin_msg:
write_data_to_file(
cfg = self.cfg,
outfile = os.path.join(outdir or '',self.filename),
data = json.dumps(data,sort_keys=True,indent=4),
outfile = os.path.join(outdir or '', self.filename),
data = json.dumps(data, sort_keys=True, indent=4),
desc = self.desc,
ask_overwrite = ask_overwrite )
ask_overwrite = ask_overwrite)
class new(base):
def __init__(self,message,addrlists,msghash_type,*args,**kwargs):
def __init__(self, message, addrlists, msghash_type, *args, **kwargs):
msghash_type = msghash_type or self.msg_cls.msghash_types[0]
if msghash_type not in self.msg_cls.msghash_types:
die(2,f'msghash_type {msghash_type!r} not supported for {self.proto.base_proto} protocol')
die(2, f'msghash_type {msghash_type!r} not supported for {self.proto.base_proto} protocol')
self.data = {
'network': '{}_{}'.format( self.proto.coin.lower(), self.proto.network ),
'addrlists': [MMGenIDRange(self.proto,i) for i in addrlists.split()],
'network': '{}_{}'.format(self.proto.coin.lower(), self.proto.network),
'addrlists': [MMGenIDRange(self.proto, i) for i in addrlists.split()],
'message': message,
'msghash_type': msghash_type,
}
@ -124,7 +124,7 @@ class coin_msg:
class completed(base):
def __init__(self,data,infile,*args,**kwargs):
def __init__(self, data, infile, *args, **kwargs):
if data:
self.__dict__ = data
@ -133,14 +133,14 @@ class coin_msg:
self.data = get_data_from_file(
cfg = self.cfg,
infile = infile,
desc = self.desc )
desc = self.desc)
d = json.loads(self.data)
self.data = d['metadata']
self.sigs = d['signatures']
self.addrlists = [MMGenIDRange(self.proto,i) for i in self.data['addrlists']]
self.addrlists = [MMGenIDRange(self.proto, i) for i in self.data['addrlists']]
def format(self,req_addr=None):
def format(self, req_addr=None):
labels = {
'addr': 'address:',
@ -152,37 +152,37 @@ class coin_msg:
def gen_entry(e):
for k in labels:
if e.get(k):
yield fs_sig.format( labels[k], e[k] )
yield fs_sig.format(labels[k], e[k])
def gen_all():
for k,v in hdr_data.items():
yield fs_hdr.format( v[0], v[1](self.data[k]) )
for k, v in hdr_data.items():
yield fs_hdr.format(v[0], v[1](self.data[k]))
if self.sigs:
yield ''
yield 'Signatures:'
for n,(k,v) in enumerate(self.sigs.items()):
for n, (k, v) in enumerate(self.sigs.items()):
yield ''
yield f'{n+1:>3}) {k}'
yield from gen_entry(v)
def gen_single():
for k,v in hdr_data.items():
yield fs_hdr.format( v[0], v[1](self.data[k]) )
for k, v in hdr_data.items():
yield fs_hdr.format(v[0], v[1](self.data[k]))
if self.sigs:
yield 'Signature data:'
k = (
CoinAddr(self.proto,req_addr) if type(self).__name__ == 'exported_sigs' else
MMGenID(self.proto,req_addr) )
CoinAddr(self.proto, req_addr) if type(self).__name__ == 'exported_sigs' else
MMGenID(self.proto, req_addr))
if k not in self.sigs:
die(1,f'{k}: address not found in signature data')
die(1, f'{k}: address not found in signature data')
yield from gen_entry(self.sigs[k])
hdr_data = {
'message': ('Message:', grnbg ),
'network': ('Network:', lambda v: v.replace('_',' ').upper() ),
'msghash_type': ('Message Hash Type:', lambda v: v ),
'addrlists': ('Address Ranges:', lambda v: fmt_list(v,fmt='bare') ),
'failed_sids': ('Failed Seed IDs:', lambda v: red(fmt_list(v,fmt='bare')) ),
'message': ('Message:', grnbg),
'network': ('Network:', lambda v: v.replace('_', ' ').upper()),
'msghash_type': ('Message Hash Type:', lambda v: v),
'addrlists': ('Address Ranges:', lambda v: fmt_list(v, fmt='bare')),
'failed_sids': ('Failed Seed IDs:', lambda v: red(fmt_list(v, fmt='bare'))),
}
if len(self.msg_cls.msghash_types) == 1:
@ -212,7 +212,7 @@ class coin_msg:
from .addrlist import KeyAddrList
async def sign_list(al_in,seed):
async def sign_list(al_in, seed):
al = KeyAddrList(
cfg = self.cfg,
proto = self.proto,
@ -220,13 +220,13 @@ class coin_msg:
addr_idxs = al_in.idxlist,
mmtype = al_in.mmtype,
skip_chksum = True,
add_p2pkh = al_in.mmtype in ('S','B') )
add_p2pkh = al_in.mmtype in ('S', 'B'))
for e in al.data:
sig = await self.do_sign(
wif = e.sec.wif,
message = self.data['message'],
msghash_type = self.data['msghash_type'] )
msghash_type = self.data['msghash_type'])
mmid = f'{al_in.sid}:{al_in.mmtype}:{e.idx}'
data = {
@ -234,7 +234,7 @@ class coin_msg:
'sig': sig,
}
if self.msg_cls.include_pubhash:
data.update({ 'pubhash': self.proto.decode_addr(e.addr_p2pkh or e.addr).bytes.hex() })
data.update({'pubhash': self.proto.decode_addr(e.addr_p2pkh or e.addr).bytes.hex()})
if e.addr_p2pkh:
data.update({'addr_p2pkh': e.addr_p2pkh})
@ -243,7 +243,7 @@ class coin_msg:
if self.proto.sign_mode == 'daemon':
from .rpc import rpc_init
self.rpc = await rpc_init( self.cfg, self.proto, ignore_wallet=True )
self.rpc = await rpc_init(self.cfg, self.proto, ignore_wallet=True)
from .wallet import Wallet
wallet_seeds = [Wallet(cfg=self.cfg, fn=fn, passwd_file=passwd_file).seed for fn in wallet_files]
@ -261,7 +261,7 @@ class coin_msg:
# Then subseeds:
for sid in need_sids:
for seed in wallet_seeds:
subseed = seed.subseeds.get_subseed_by_seed_id(sid,print_msg=True)
subseed = seed.subseeds.get_subseed_by_seed_id(sid, print_msg=True)
if subseed:
saved_seeds.append(subseed)
need_sids.remove(sid)
@ -270,11 +270,11 @@ class coin_msg:
for al in self.addrlists:
for seed in saved_seeds:
if al.sid == seed.sid:
await sign_list(al,seed)
await sign_list(al, seed)
break
if need_sids:
msg('Failed Seed IDs: {}'.format(orange(fmt_list(need_sids,fmt='bare'))))
msg('Failed Seed IDs: {}'.format(orange(fmt_list(need_sids, fmt='bare'))))
self.data['failed_sids'] = need_sids
@ -285,45 +285,45 @@ class coin_msg:
class signed_online(signed):
def get_sigs(self,addr):
def get_sigs(self, addr):
if addr:
req_addr = (
CoinAddr(self.proto,addr) if type(self).__name__ == 'exported_sigs' else
MMGenID(self.proto,addr) )
sigs = {k:v for k,v in self.sigs.items() if k == req_addr}
CoinAddr(self.proto, addr) if type(self).__name__ == 'exported_sigs' else
MMGenID(self.proto, addr))
sigs = {k:v for k, v in self.sigs.items() if k == req_addr}
else:
sigs = self.sigs
if not sigs:
die(1,'No signatures')
die(1, 'No signatures')
return sigs
async def verify(self,addr=None):
async def verify(self, addr=None):
sigs = self.get_sigs(addr)
if self.proto.sign_mode == 'daemon':
from .rpc import rpc_init
self.rpc = await rpc_init( self.cfg, self.proto, ignore_wallet=True )
self.rpc = await rpc_init(self.cfg, self.proto, ignore_wallet=True)
for k,v in sigs.items():
for k, v in sigs.items():
ret = await self.do_verify(
addr = v.get('addr_p2pkh') or v['addr'],
sig = v['sig'],
message = self.data['message'],
msghash_type = self.data['msghash_type'] )
msghash_type = self.data['msghash_type'])
if not ret:
die(3,f'Invalid signature for address {k} ({v["addr"]})')
die(3, f'Invalid signature for address {k} ({v["addr"]})')
return len(sigs)
def get_json_for_export(self,addr=None):
sigs = list( self.get_sigs(addr).values() )
def get_json_for_export(self, addr=None):
sigs = list(self.get_sigs(addr).values())
pfx = self.msg_cls.sigdata_pfx
if pfx:
sigs = [{k:pfx+v for k,v in e.items()} for e in sigs]
sigs = [{k:pfx+v for k, v in e.items()} for e in sigs]
return json.dumps(
{
'message': self.data['message'],
@ -337,23 +337,23 @@ class coin_msg:
class exported_sigs(signed_online):
def __init__(self,infile,*args,**kwargs):
def __init__(self, infile, *args, **kwargs):
self.data = json.loads(
get_data_from_file(
cfg = self.cfg,
infile = infile,
desc = self.desc )
desc = self.desc)
)
pfx = self.msg_cls.sigdata_pfx
self.sigs = {sig_data['addr']:sig_data for sig_data in (
[{k:v[len(pfx):] for k,v in e.items()} for e in self.data['signatures']]
[{k:v[len(pfx):] for k, v in e.items()} for e in self.data['signatures']]
if pfx else
self.data['signatures']
)}
def _get_obj(clsname,cfg,*args,coin=None,network='mainnet',infile=None,data=None,**kwargs):
def _get_obj(clsname, cfg, *args, coin=None, network='mainnet', infile=None, data=None, **kwargs):
assert not args, 'msg:_get_obj(): only keyword args allowed'
@ -364,27 +364,27 @@ def _get_obj(clsname,cfg,*args,coin=None,network='mainnet',infile=None,data=None
proto = (
data['proto'] if data else
init_proto( cfg=cfg, coin=coin, network=network ) if coin else
coin_msg.base.get_proto_from_file(cfg,infile) )
init_proto(cfg=cfg, coin=coin, network=network) if coin else
coin_msg.base.get_proto_from_file(cfg, infile))
try:
msg_cls = getattr(
importlib.import_module(f'mmgen.proto.{proto.base_proto_coin.lower()}.msg'),
'coin_msg' )
'coin_msg')
except:
die(1,f'Message signing operations not supported for {proto.base_proto} protocol')
die(1, f'Message signing operations not supported for {proto.base_proto} protocol')
me = MMGenObject.__new__(getattr( msg_cls, clsname, getattr(coin_msg,clsname) ))
me = MMGenObject.__new__(getattr(msg_cls, clsname, getattr(coin_msg, clsname)))
me.msg_cls = msg_cls
me.cfg = cfg
me.proto = proto
me.__init__(infile=infile,data=data,*args,**kwargs)
me.__init__(infile=infile, data=data, *args, **kwargs)
return me
def _get(clsname):
return lambda *args,**kwargs: _get_obj(clsname,*args,**kwargs)
return lambda *args, **kwargs: _get_obj(clsname, *args, **kwargs)
NewMsg = _get('new')
CompletedMsg = _get('completed')

View file

@ -22,9 +22,9 @@ obj: MMGen native classes
import unicodedata
from .objmethods import MMGenObject,Hilite,HiliteStr,InitErrors
from .objmethods import MMGenObject, Hilite, HiliteStr, InitErrors
def get_obj(objname,*args,**kwargs):
def get_obj(objname, *args, **kwargs):
"""
Wrapper for data objects
- If the object throws an exception on instantiation, return False, otherwise return the object.
@ -34,7 +34,7 @@ def get_obj(objname,*args,**kwargs):
"""
assert not args, 'get_obj_chk1'
silent,return_bool = (False,False)
silent, return_bool = (False, False)
if 'silent' in kwargs:
silent = kwargs['silent']
del kwargs['silent']
@ -55,44 +55,44 @@ def get_obj(objname,*args,**kwargs):
# dict that keeps a list of keys for efficient lookup by index
class IndexedDict(dict):
def __init__(self,*args,**kwargs):
def __init__(self, *args, **kwargs):
if args or kwargs:
self.die('initializing values via constructor')
self.__keylist = []
dict.__init__(self,*args,**kwargs)
dict.__init__(self, *args, **kwargs)
def __setitem__(self,key,value):
def __setitem__(self, key, value):
if key in self:
self.die('reassignment to existing key')
self.__keylist.append(key)
return dict.__setitem__(self,key,value)
return dict.__setitem__(self, key, value)
@property
def keys(self):
return self.__keylist
def key(self,idx):
def key(self, idx):
return self.__keylist[idx]
def __delitem__(self,*args):
def __delitem__(self, *args):
self.die('item deletion')
def move_to_end(self,*args):
def move_to_end(self, *args):
self.die('item moving')
def clear(self,*args):
def clear(self, *args):
self.die('clearing')
def update(self,*args):
def update(self, *args):
self.die('updating')
def die(self,desc):
def die(self, desc):
raise NotImplementedError(f'{desc} not implemented for type {type(self).__name__}')
class MMGenList(list,MMGenObject):
class MMGenList(list, MMGenObject):
pass
class MMGenDict(dict,MMGenObject):
class MMGenDict(dict, MMGenObject):
pass
class ImmutableAttr: # Descriptor
@ -100,53 +100,53 @@ class ImmutableAttr: # Descriptor
For attributes that are always present in the data instance
Reassignment and deletion forbidden
"""
ok_dtypes = (type,type(None),type(lambda:0))
ok_dtypes = (type, type(None), type(lambda:0))
def __init__(self,dtype,typeconv=True,set_none_ok=False,include_proto=False):
def __init__(self, dtype, typeconv=True, set_none_ok=False, include_proto=False):
self.set_none_ok = set_none_ok
self.typeconv = typeconv
assert isinstance(dtype,self.ok_dtypes), 'ImmutableAttr_check1'
assert isinstance(dtype, self.ok_dtypes), 'ImmutableAttr_check1'
if include_proto:
assert typeconv, 'ImmutableAttr_check2'
if set_none_ok:
assert typeconv and not isinstance(dtype,str), 'ImmutableAttr_check3'
assert typeconv and not isinstance(dtype, str), 'ImmutableAttr_check3'
if typeconv:
# convert this attribute's type
if set_none_ok:
self.conv = lambda instance,value: None if value is None else dtype(value)
self.conv = lambda instance, value: None if value is None else dtype(value)
elif include_proto:
self.conv = lambda instance,value: dtype(instance.proto,value)
self.conv = lambda instance, value: dtype(instance.proto, value)
else:
self.conv = lambda instance,value: dtype(value)
self.conv = lambda instance, value: dtype(value)
else:
# check this attribute's type
def assign_with_check(instance,value):
def assign_with_check(instance, value):
if type(value) is dtype:
return value
raise TypeError('Attribute {!r} of {} instance must of type {}'.format(
self.name,
type(instance).__name__,
dtype ))
dtype))
self.conv = assign_with_check
def __set_name__(self,owner,name):
def __set_name__(self, owner, name):
self.name = name
def __get__(self,instance,owner):
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def setattr_condition(self,instance):
def setattr_condition(self, instance):
'forbid all reassignment'
return not self.name in instance.__dict__
def __set__(self,instance,value):
def __set__(self, instance, value):
if not self.setattr_condition(instance):
raise AttributeError(f'Attribute {self.name!r} of {type(instance)} instance cannot be reassigned')
instance.__dict__[self.name] = self.conv(instance,value)
instance.__dict__[self.name] = self.conv(instance, value)
def __delete__(self,instance):
def __delete__(self, instance):
raise AttributeError(
f'Attribute {self.name!r} of {type(instance).__name__} instance cannot be deleted')
@ -155,27 +155,27 @@ class ListItemAttr(ImmutableAttr):
For attributes that might not be present in the data instance
Reassignment or deletion allowed if specified
"""
def __init__(self,dtype,typeconv=True,include_proto=False,reassign_ok=False,delete_ok=False):
def __init__(self, dtype, typeconv=True, include_proto=False, reassign_ok=False, delete_ok=False):
self.reassign_ok = reassign_ok
self.delete_ok = delete_ok
ImmutableAttr.__init__(self,dtype,typeconv=typeconv,include_proto=include_proto)
ImmutableAttr.__init__(self, dtype, typeconv=typeconv, include_proto=include_proto)
def __get__(self,instance,owner):
def __get__(self, instance, owner):
"return None if attribute doesn't exist"
try:
return instance.__dict__[self.name]
except:
return None
def setattr_condition(self,instance):
return getattr(instance,self.name) is None or self.reassign_ok
def setattr_condition(self, instance):
return getattr(instance, self.name) is None or self.reassign_ok
def __delete__(self,instance):
def __delete__(self, instance):
if self.delete_ok:
if self.name in instance.__dict__:
del instance.__dict__[self.name]
else:
ImmutableAttr.__delete__(self,instance)
ImmutableAttr.__delete__(self, instance)
class MMGenListItem(MMGenObject):
valid_attrs = set()
@ -189,7 +189,7 @@ class MMGenListItem(MMGenObject):
'immutable_attr_init_check',
}
def __init__(self,*args,**kwargs):
def __init__(self, *args, **kwargs):
# generate valid_attrs, or use the class valid_attrs if set
self.__dict__['valid_attrs'] = self.valid_attrs or (
{e for e in dir(self) if e[0] != '_'}
@ -200,49 +200,49 @@ class MMGenListItem(MMGenObject):
if args:
raise ValueError(f'Non-keyword args not allowed in {type(self).__name__!r} constructor')
for k,v in kwargs.items():
for k, v in kwargs.items():
if v is not None:
setattr(self,k,v)
setattr(self, k, v)
# Require all immutables to be initialized. Check performed only when testing.
self.immutable_attr_init_check()
# allow only valid attributes to be set
def __setattr__(self,name,value):
def __setattr__(self, name, value):
if name not in self.valid_attrs:
raise AttributeError(f'{name!r}: no such attribute in class {type(self)}')
return object.__setattr__(self,name,value)
return object.__setattr__(self, name, value)
def _asdict(self):
return dict((k,v) for k,v in self.__dict__.items() if k in self.valid_attrs)
return dict((k, v) for k, v in self.__dict__.items() if k in self.valid_attrs)
class MMGenRange(tuple,InitErrors,MMGenObject):
class MMGenRange(tuple, InitErrors, MMGenObject):
min_idx = None
max_idx = None
def __new__(cls,*args):
def __new__(cls, *args):
try:
if len(args) == 1:
s = args[0]
if isinstance(s,cls):
if isinstance(s, cls):
return s
assert isinstance(s,str),'not a string or string subclass'
ss = s.split('-',1)
assert isinstance(s, str), 'not a string or string subclass'
ss = s.split('-', 1)
first = int(ss[0])
last = int(ss.pop())
else:
s = repr(args) # needed if exception occurs
assert len(args) == 2,'one format string arg or two start,stop args required'
first,last = args
assert len(args) == 2, 'one format string arg or two start, stop args required'
first, last = args
assert first <= last, 'start of range greater than end of range'
if cls.min_idx is not None:
assert first >= cls.min_idx, f'start of range < {cls.min_idx:,}'
if cls.max_idx is not None:
assert last <= cls.max_idx, f'end of range > {cls.max_idx:,}'
return tuple.__new__(cls,(first,last))
return tuple.__new__(cls, (first, last))
except Exception as e:
return cls.init_fail(e,s)
return cls.init_fail(e, s)
@property
def first(self):
@ -253,23 +253,23 @@ class MMGenRange(tuple,InitErrors,MMGenObject):
return self[1]
def iterate(self):
return range(self[0],self[1]+1)
return range(self[0], self[1]+1)
@property
def items(self):
return list(self.iterate())
class Int(int,Hilite,InitErrors):
class Int(int, Hilite, InitErrors):
min_val = None
max_val = None
max_digits = None
color = 'red'
def __new__(cls,n,base=10):
if isinstance(n,cls):
def __new__(cls, n, base=10):
if isinstance(n, cls):
return n
try:
me = int.__new__(cls,str(n),base)
me = int.__new__(cls, str(n), base)
if cls.min_val is not None:
assert me >= cls.min_val, f'is less than cls.min_val ({cls.min_val})'
if cls.max_val is not None:
@ -278,16 +278,16 @@ class Int(int,Hilite,InitErrors):
assert len(str(me)) <= cls.max_digits, f'has more than {cls.max_digits} digits'
return me
except Exception as e:
return cls.init_fail(e,n)
return cls.init_fail(e, n)
@classmethod
def fmtc(cls,s,width,color=False):
def fmtc(cls, s, width, color=False):
return super().fmtc(str(s), width=width, color=color)
def fmt(self,width,color=False):
def fmt(self, width, color=False):
return super().fmtc(str(self), width=width, color=color)
def hl(self,**kwargs):
def hl(self, **kwargs):
return super().colorize(str(self), **kwargs)
class NonNegativeInt(Int):
@ -302,44 +302,44 @@ class ETHNonce(Int):
class Str(HiliteStr):
pass
class HexStr(HiliteStr,InitErrors):
class HexStr(HiliteStr, InitErrors):
color = 'red'
width = None
hexcase = 'lower'
trunc_ok = False
def __new__(cls,s,case=None):
if isinstance(s,cls):
def __new__(cls, s, case=None):
if isinstance(s, cls):
return s
if case is None:
case = cls.hexcase
from .util import hexdigits_lc,hexdigits_uc
from .util import hexdigits_lc, hexdigits_uc
try:
assert isinstance(s,str),'not a string or string subclass'
assert case in ('upper','lower'), f'{case!r} incorrect case specifier'
assert isinstance(s, str), 'not a string or string subclass'
assert case in ('upper', 'lower'), f'{case!r} incorrect case specifier'
assert set(s) <= set(hexdigits_lc if case == 'lower' else hexdigits_uc), (
f'not {case}case hexadecimal symbols' )
assert not len(s) % 2,'odd-length string'
f'not {case}case hexadecimal symbols')
assert not len(s) % 2, 'odd-length string'
if cls.width:
assert len(s) == cls.width, f'Value is not {cls.width} characters wide'
return str.__new__(cls,s)
return str.__new__(cls, s)
except Exception as e:
return cls.init_fail(e,s)
return cls.init_fail(e, s)
def truncate(self,width,color=True):
def truncate(self, width, color=True):
return self.colorize(
self if width >= self.width else self[:width-2] + '..',
color = color )
color = color)
class CoinTxID(HexStr):
color,width,hexcase = ('purple',64,'lower')
color, width, hexcase = ('purple', 64, 'lower')
class WalletPassword(HexStr):
color,width,hexcase = ('blue',32,'lower')
color, width, hexcase = ('blue', 32, 'lower')
class MMGenTxID(HexStr):
color,width,hexcase = ('red',6,'upper')
color, width, hexcase = ('red', 6, 'upper')
class MMGenLabel(HiliteStr,InitErrors):
class MMGenLabel(HiliteStr, InitErrors):
color = 'pink'
allowed = []
forbidden = []
@ -348,26 +348,26 @@ class MMGenLabel(HiliteStr,InitErrors):
min_len = 0
max_screen_width = 0 # if != 0, overrides max_len
desc = 'label'
def __new__(cls,s):
if isinstance(s,cls):
def __new__(cls, s):
if isinstance(s, cls):
return s
try:
s = s.strip()
if not cls.allowed:
for ch in s:
# Allow: (L)etter,(N)umber,(P)unctuation,(S)ymbol,(Z)space
# Disallow: (C)ontrol,(M)combining
# Allow: (L)etter, (N)umber, (P)unctuation, (S)ymbol, (Z)space
# Disallow: (C)ontrol, (M)combining
# Combining characters create width formatting issues, so disallow them for now
if unicodedata.category(ch)[0] in ('C','M'):
if unicodedata.category(ch)[0] in ('C', 'M'):
raise ValueError(
'{!a}: {} characters not allowed'.format(
ch,
{ 'C':'control', 'M':'combining' }[unicodedata.category(ch)[0]] ))
{'C':'control', 'M':'combining'}[unicodedata.category(ch)[0]]))
me = str.__new__(cls,s)
me = str.__new__(cls, s)
if cls.max_screen_width:
me.screen_width = len(s) + len([1 for ch in s if unicodedata.east_asian_width(ch) in ('F','W')])
me.screen_width = len(s) + len([1 for ch in s if unicodedata.east_asian_width(ch) in ('F', 'W')])
assert me.screen_width <= cls.max_screen_width, f'too wide (>{cls.max_screen_width} screen width)'
else:
assert len(s) <= cls.max_len, f'too long (>{cls.max_len} symbols)'
@ -385,7 +385,7 @@ class MMGenLabel(HiliteStr,InitErrors):
return me
except Exception as e:
return cls.init_fail(e,s)
return cls.init_fail(e, s)
class MMGenWalletLabel(MMGenLabel):
max_len = 48

View file

@ -31,13 +31,13 @@ else:
def immutable_attr_init_check(self):
pass
def truncate_str(s,width): # width = screen width
def truncate_str(s, width): # width = screen width
wide_count = 0
for n,ch in enumerate(s,1):
wide_count += unicodedata.east_asian_width(ch) in ('F','W')
for n, ch in enumerate(s, 1):
wide_count += unicodedata.east_asian_width(ch) in ('F', 'W')
if n + wide_count > width:
return s[:n-1] + ('',' ')[
unicodedata.east_asian_width(ch) in ('F','W')
return s[:n-1] + ('', ' ')[
unicodedata.east_asian_width(ch) in ('F', 'W')
and n + wide_count == width + 1]
raise ValueError('string requires no truncating')
@ -49,34 +49,34 @@ class Hilite:
# class method equivalent of fmt()
@classmethod
def fmtc( cls, s, width, color=False ):
def fmtc(cls, s, width, color=False):
if len(s) > width:
assert cls.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= width of string"
return cls.colorize( s[:width].ljust(width), color=color )
return cls.colorize(s[:width].ljust(width), color=color)
else:
return cls.colorize( s.ljust(width), color=color )
return cls.colorize(s.ljust(width), color=color)
@classmethod
def hlc(cls,s,color=True):
return getattr( color_mod, cls.color )(s) if color else s
def hlc(cls, s, color=True):
return getattr(color_mod, cls.color)(s) if color else s
@classmethod
def colorize(cls,s,color=True):
return getattr( color_mod, cls.color )(s) if color else s
def colorize(cls, s, color=True):
return getattr(color_mod, cls.color)(s) if color else s
@classmethod
def colorize2(cls,s,color=True,color_override=''):
return getattr( color_mod, color_override or cls.color )(s) if color else s
def colorize2(cls, s, color=True, color_override=''):
return getattr(color_mod, color_override or cls.color)(s) if color else s
class HiliteStr(str,Hilite):
class HiliteStr(str, Hilite):
# supports single-width characters only
def fmt( self, width, color=False ):
def fmt(self, width, color=False):
if len(self) > width:
assert self.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= width of string"
return self.colorize( self[:width].ljust(width), color=color )
return self.colorize(self[:width].ljust(width), color=color)
else:
return self.colorize( self.ljust(width), color=color )
return self.colorize(self.ljust(width), color=color)
# an alternative to fmt(), with double-width char support and other features
def fmt2(
@ -87,46 +87,46 @@ class HiliteStr(str,Hilite):
nullrepl = '',
append_chars = '', # single-width chars only
append_color = False,
color_override = '' ):
color_override = ''):
if self == '':
return getattr( color_mod, self.color )(nullrepl.ljust(width)) if color else nullrepl.ljust(width)
return getattr(color_mod, self.color)(nullrepl.ljust(width)) if color else nullrepl.ljust(width)
s_wide_count = len(['' for ch in self if unicodedata.east_asian_width(ch) in ('F','W')])
s_wide_count = len(['' for ch in self if unicodedata.east_asian_width(ch) in ('F', 'W')])
a,b = encl or ('','')
a, b = encl or ('', '')
add_len = len(append_chars) + len(encl)
if len(self) + s_wide_count + add_len > width:
assert self.trunc_ok, "If 'trunc_ok' is false, 'width' must be >= screen width of string"
s = a + (truncate_str(self,width-add_len) if s_wide_count else self[:width-add_len]) + b
s = a + (truncate_str(self, width-add_len) if s_wide_count else self[:width-add_len]) + b
else:
s = a + self + b
if append_chars:
return (
self.colorize(s,color=color)
self.colorize(s, color=color)
+ self.colorize2(
append_chars.ljust(width-len(s)-s_wide_count),
color_override = append_color ))
color_override = append_color))
else:
return self.colorize2( s.ljust(width-s_wide_count), color=color, color_override=color_override )
return self.colorize2(s.ljust(width-s_wide_count), color=color, color_override=color_override)
def hl(self,color=True):
return getattr( color_mod, self.color )(self) if color else self
def hl(self, color=True):
return getattr(color_mod, self.color)(self) if color else self
# an alternative to hl(), with enclosure and color override
# can be called as an unbound method with class as first argument
def hl2(self,s=None,color=True,encl='',color_override=''):
def hl2(self, s=None, color=True, encl='', color_override=''):
if encl:
return self.colorize2( encl[0]+(s or self)+encl[1], color=color, color_override=color_override )
return self.colorize2(encl[0]+(s or self)+encl[1], color=color, color_override=color_override)
else:
return self.colorize2( (s or self), color=color, color_override=color_override )
return self.colorize2((s or self), color=color, color_override=color_override)
class InitErrors:
@classmethod
def init_fail(cls,e,m,e2=None,m2=None,objname=None,preformat=False):
def init_fail(cls, e, m, e2=None, m2=None, objname=None, preformat=False):
def get_errmsg():
ret = m if preformat else (
@ -134,18 +134,18 @@ class InitErrors:
m,
(objname or cls.__name__),
(f'({e2!s}) ' if e2 else ''),
e ))
e))
return f'{m2!r}\n{ret}' if m2 else ret
if hasattr(cls,'passthru_excs') and type(e).__name__ in cls.passthru_excs:
if hasattr(cls, 'passthru_excs') and type(e).__name__ in cls.passthru_excs:
raise e
from .util import die
die(getattr(cls,'exc','ObjectInitError'), get_errmsg())
die(getattr(cls, 'exc', 'ObjectInitError'), get_errmsg())
@classmethod
def method_not_implemented(cls):
import traceback
raise NotImplementedError(
'method {}() not implemented for class {!r}'.format(
traceback.extract_stack()[-2].name, cls.__name__) )
traceback.extract_stack()[-2].name, cls.__name__))

View file

@ -134,7 +134,7 @@ def parse_opts(opts_data, opt_filter, global_opts_data, global_opts_filter):
return namedtuple('parsed_cmd_opts', ['user_opts', 'cmd_args', 'opts'])(
uopts, # dict
uargs, # list, callers can pop
tuple(v.name for k,v in opts if len(k) > 1)
tuple(v.name for k, v in opts if len(k) > 1)
)
def opt_preproc_debug(po):
@ -145,10 +145,10 @@ def opt_preproc_debug(po):
('Cmd args', po.cmd_args, False),
('Opts', po.opts, True),
)
from .util import Msg,fmt_list
from .util import Msg, fmt_list
Msg('\n=== opts.py debug ===')
for label,data,pretty in d:
Msg(' {:<20}: {}'.format(label,'\n' + fmt_list(data,fmt='col',indent=' '*8) if pretty else data))
for label, data, pretty in d:
Msg(' {:<20}: {}'.format(label, '\n' + fmt_list(data, fmt='col', indent=' '*8) if pretty else data))
opts_data_dfl = {
'text': {
@ -202,7 +202,7 @@ class Opts:
cfg._uopts = uopts = po.user_opts
if init_opts: # initialize user opts to given value
for uopt,val in init_opts.items():
for uopt, val in init_opts.items():
if uopt not in uopts:
uopts[uopt] = val

View file

@ -22,10 +22,10 @@ passwdlist: Password list class for the MMGen suite
from collections import namedtuple
from .util import ymsg,is_int,die
from .obj import ImmutableAttr,ListItemAttr,MMGenPWIDString,TwComment
from .util import ymsg, is_int, die
from .obj import ImmutableAttr, ListItemAttr, MMGenPWIDString, TwComment
from .key import PrivKey
from .addr import MMGenPasswordType,AddrIdx,AddrListID
from .addr import MMGenPasswordType, AddrIdx, AddrListID
from .addrlist import (
AddrListChksum,
AddrListIDStr,
@ -34,10 +34,10 @@ from .addrlist import (
)
class PasswordListEntry(AddrListEntryBase):
passwd = ListItemAttr(str,typeconv=False) # TODO: create Password type
passwd = ListItemAttr(str, typeconv=False) # TODO: create Password type
idx = ImmutableAttr(AddrIdx)
comment = ListItemAttr(TwComment,reassign_ok=True)
sec = ListItemAttr(PrivKey,include_proto=True)
comment = ListItemAttr(TwComment, reassign_ok=True)
sec = ListItemAttr(PrivKey, include_proto=True)
class PasswordList(AddrList):
entry_type = PasswordListEntry
@ -50,17 +50,17 @@ class PasswordList(AddrList):
gen_passwds = True
pw_len = None
dfl_pw_fmt = 'b58'
pwinfo = namedtuple('passwd_info',['min_len','max_len','dfl_len','valid_lens','desc','chk_func'])
pwinfo = namedtuple('passwd_info', ['min_len', 'max_len', 'dfl_len', 'valid_lens', 'desc', 'chk_func'])
pw_info = {
# 32**25 < 2**128 < 32**26
'b32': pwinfo(10, 42 ,24, None, 'base32 password', 'baseconv.is_b32_str'),
'b32': pwinfo(10, 42 , 24, None, 'base32 password', 'baseconv.is_b32_str'),
# 58**21 < 2**128 < 58**22
'b58': pwinfo(8, 36 ,20, None, 'base58 password', 'baseconv.is_b58_str'),
'bip39': pwinfo(12, 24 ,24, [12,18,24],'BIP39 mnemonic', 'bip39.is_bip39_mnemonic'),
'xmrseed': pwinfo(25, 25, 25, [25], 'Monero new-style mnemonic','xmrseed.is_xmrseed'),
'hex': pwinfo(32, 64 ,64, [32,48,64],'hexadecimal password', 'util.is_hex_str'),
'b58': pwinfo(8, 36 , 20, None, 'base58 password', 'baseconv.is_b58_str'),
'bip39': pwinfo(12, 24 , 24, [12, 18, 24], 'BIP39 mnemonic', 'bip39.is_bip39_mnemonic'),
'xmrseed': pwinfo(25, 25, 25, [25], 'Monero new-style mnemonic', 'xmrseed.is_xmrseed'),
'hex': pwinfo(32, 64 , 64, [32, 48, 64], 'hexadecimal password', 'util.is_hex_str'),
}
chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
chksum_rec_f = lambda foo, e: (str(e.idx), e.passwd)
feature_warn_fs = 'WARNING: {!r} is a potentially dangerous feature. Use at your own risk!'
hex2bip39 = False
@ -76,7 +76,7 @@ class PasswordList(AddrList):
pw_len = None,
pw_fmt = None,
chk_params_only = False,
skip_chksum_msg = False ):
skip_chksum_msg = False):
self.cfg = cfg
self.proto = proto # proto is ignored
@ -90,7 +90,7 @@ class PasswordList(AddrList):
self.data = self.file.parse_file(infile)
else:
if not chk_params_only:
for k in (seed,pw_idxs):
for k in (seed, pw_idxs):
assert k
self.pw_id_str = MMGenPWIDString(pw_id_str)
self.set_pw_fmt(pw_fmt)
@ -100,20 +100,20 @@ class PasswordList(AddrList):
if self.hex2bip39:
ymsg(self.feature_warn_fs.format(pw_fmt))
self.set_pw_len_vs_seed_len(seed) # sets self.bip39, self.xmrseed, self.xmrproto self.baseconv
self.al_id = AddrListID( sid=seed.sid, mmtype=MMGenPasswordType(self.proto,'P') )
self.data = self.generate(seed,pw_idxs)
self.al_id = AddrListID(sid=seed.sid, mmtype=MMGenPasswordType(self.proto, 'P'))
self.data = self.generate(seed, pw_idxs)
self.num_addrs = len(self.data)
self.fmt_data = ''
self.chksum = AddrListChksum(self)
fs = f'{self.al_id.sid}-{self.pw_id_str}-{self.pw_fmt_disp}-{self.pw_len}[{{}}]'
self.id_str = AddrListIDStr(self,fs)
self.id_str = AddrListIDStr(self, fs)
if not skip_chksum_msg:
self.do_chksum_msg(record=not infile)
def set_pw_fmt(self,pw_fmt):
def set_pw_fmt(self, pw_fmt):
if pw_fmt == 'hex2bip39':
self.hex2bip39 = True
self.pw_fmt = 'bip39'
@ -122,12 +122,12 @@ class PasswordList(AddrList):
self.pw_fmt = pw_fmt
self.pw_fmt_disp = pw_fmt
if self.pw_fmt not in self.pw_info:
die( 'InvalidPasswdFormat',
f'{self.pw_fmt!r}: invalid password format. Valid formats: {", ".join(self.pw_info)}' )
die('InvalidPasswdFormat',
f'{self.pw_fmt!r}: invalid password format. Valid formats: {", ".join(self.pw_info)}')
def chk_pw_len(self,passwd=None):
def chk_pw_len(self, passwd=None):
if passwd is None:
assert self.pw_len,'either passwd or pw_len must be set'
assert self.pw_len, 'either passwd or pw_len must be set'
pw_len = self.pw_len
fs = '{l}: invalid user-requested length for {b} ({c}{m})'
else:
@ -136,13 +136,13 @@ class PasswordList(AddrList):
d = self.pw_info[self.pw_fmt]
if d.valid_lens:
if pw_len not in d.valid_lens:
die(2, fs.format( l=pw_len, b=d.desc, c='not one of ', m=d.valid_lens, pw=passwd ))
die(2, fs.format(l=pw_len, b=d.desc, c='not one of ', m=d.valid_lens, pw=passwd))
elif pw_len > d.max_len:
die(2, fs.format( l=pw_len, b=d.desc, c='>', m=d.max_len, pw=passwd ))
die(2, fs.format(l=pw_len, b=d.desc, c='>', m=d.max_len, pw=passwd))
elif pw_len < d.min_len:
die(2, fs.format( l=pw_len, b=d.desc, c='<', m=d.min_len, pw=passwd ))
die(2, fs.format(l=pw_len, b=d.desc, c='<', m=d.min_len, pw=passwd))
def set_pw_len(self,pw_len):
def set_pw_len(self, pw_len):
d = self.pw_info[self.pw_fmt]
if pw_len is None:
@ -150,11 +150,11 @@ class PasswordList(AddrList):
return
if not is_int(pw_len):
die(2,f'{pw_len!r}: invalid user-requested password length (not an integer)')
die(2, f'{pw_len!r}: invalid user-requested password length (not an integer)')
self.pw_len = int(pw_len)
self.chk_pw_len()
def set_pw_len_vs_seed_len(self,seed):
def set_pw_len_vs_seed_len(self, seed):
pf = self.pw_fmt
if pf == 'hex':
pw_bytes = self.pw_len // 2
@ -162,83 +162,83 @@ class PasswordList(AddrList):
elif pf == 'bip39':
from .bip39 import bip39
self.bip39 = bip39()
pw_bytes = bip39.nwords2seedlen(self.pw_len,in_bytes=True)
good_pw_len = bip39.seedlen2nwords(seed.byte_len,in_bytes=True)
pw_bytes = bip39.nwords2seedlen(self.pw_len, in_bytes=True)
good_pw_len = bip39.seedlen2nwords(seed.byte_len, in_bytes=True)
elif pf == 'xmrseed':
from .xmrseed import xmrseed
from .protocol import init_proto
self.xmrseed = xmrseed()
self.xmrproto = init_proto( self.cfg, 'xmr' )
self.xmrproto = init_proto(self.cfg, 'xmr')
pw_bytes = xmrseed().seedlen_map_rev[self.pw_len]
try:
good_pw_len = xmrseed().seedlen_map[seed.byte_len]
except:
die(1,f'{seed.byte_len*8}: unsupported seed length for Monero new-style mnemonic')
elif pf in ('b32','b58'):
die(1, f'{seed.byte_len*8}: unsupported seed length for Monero new-style mnemonic')
elif pf in ('b32', 'b58'):
pw_int = (32 if pf == 'b32' else 58) ** self.pw_len
pw_bytes = pw_int.bit_length() // 8
from .baseconv import baseconv
self.baseconv = baseconv(self.pw_fmt)
good_pw_len = len( baseconv(pf).frombytes(b'\xff'*seed.byte_len) )
good_pw_len = len(baseconv(pf).frombytes(b'\xff'*seed.byte_len))
else:
raise NotImplementedError(f'{pf!r}: unknown password format')
if pw_bytes > seed.byte_len:
die(1,
f'Cannot generate passwords with more entropy than underlying seed! ({len(seed.data)*8} bits)\n' +
(f'Re-run the command with --passwd-len={good_pw_len}' if pf in ('bip39','hex') else
(f'Re-run the command with --passwd-len={good_pw_len}' if pf in ('bip39', 'hex') else
'Re-run the command, specifying a password length of {} or less')
)
if pf in ('bip39','hex') and pw_bytes < seed.byte_len:
if pf in ('bip39', 'hex') and pw_bytes < seed.byte_len:
from .ui import keypress_confirm
if not keypress_confirm(
self.cfg,
f'WARNING: requested {self.pw_info[pf].desc} length has less entropy ' +
'than underlying seed!\nIs this what you want?',
default_yes = True ):
die(1,'Exiting at user request')
default_yes = True):
die(1, 'Exiting at user request')
def gen_passwd(self,secbytes):
def gen_passwd(self, secbytes):
assert self.pw_fmt in self.pw_info
if self.pw_fmt == 'hex':
# take most significant part
return secbytes.hex()[:self.pw_len]
elif self.pw_fmt == 'bip39':
pw_len_bytes = self.bip39.nwords2seedlen( self.pw_len, in_bytes=True )
pw_len_bytes = self.bip39.nwords2seedlen(self.pw_len, in_bytes=True)
# take most significant part
return ' '.join( self.bip39.fromhex(secbytes[:pw_len_bytes].hex()) )
return ' '.join(self.bip39.fromhex(secbytes[:pw_len_bytes].hex()))
elif self.pw_fmt == 'xmrseed':
pw_len_bytes = self.xmrseed.seedlen_map_rev[self.pw_len]
bytes_preproc = self.xmrproto.preprocess_key(
secbytes[:pw_len_bytes], # take most significant part
None )
return ' '.join( self.xmrseed.frombytes(bytes_preproc) )
None)
return ' '.join(self.xmrseed.frombytes(bytes_preproc))
else:
# take least significant part
return self.baseconv.frombytes(
secbytes,
pad = self.pw_len,
tostr = True )[-self.pw_len:]
tostr = True)[-self.pw_len:]
def check_format(self,pw):
def check_format(self, pw):
if not self.chk_func(pw):
raise ValueError(f'Password is not valid {self.pw_info[self.pw_fmt].desc} data')
pwlen = len(pw.split()) if self.pw_fmt in ('bip39','xmrseed') else len(pw)
pwlen = len(pw.split()) if self.pw_fmt in ('bip39', 'xmrseed') else len(pw)
if pwlen != self.pw_len:
raise ValueError(f'Password has incorrect length ({pwlen} != {self.pw_len})')
return True
def scramble_seed(self,seed):
def scramble_seed(self, seed):
# Changing either pw_fmt or pw_len 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'
scramble_key = f'{self.pw_fmt}:{self.pw_len}:{self.pw_id_str}'
if self.hex2bip39:
pwlen = self.bip39.nwords2seedlen(self.pw_len,in_hex=True)
pwlen = self.bip39.nwords2seedlen(self.pw_len, in_hex=True)
scramble_key = f'hex:{pwlen}:{self.pw_id_str}'
self.dmsg_sc('str',scramble_key)
self.dmsg_sc('str', scramble_key)
from .crypto import Crypto
return Crypto(self.cfg).scramble_seed(seed,scramble_key.encode())
return Crypto(self.cfg).scramble_seed(seed, scramble_key.encode())

View file

@ -25,17 +25,17 @@ from collections import namedtuple
from .cfg import gc
from .objmethods import MMGenObject
decoded_wif = namedtuple('decoded_wif',['sec','pubkey_type','compressed'])
decoded_wif = namedtuple('decoded_wif', ['sec', 'pubkey_type', 'compressed'])
decoded_addr = namedtuple('decoded_addr', ['bytes', 'ver_bytes', 'fmt'])
decoded_addr_multiview = namedtuple('mv_decoded_addr', ['bytes', 'ver_bytes', 'fmt', 'addr', 'views', 'view_pref'])
parsed_addr = namedtuple('parsed_addr',['ver_bytes','data'])
parsed_addr = namedtuple('parsed_addr', ['ver_bytes', 'data'])
_finfo = namedtuple('fork_info',['height','hash','name','replayable'])
_nw = namedtuple('coin_networks',['mainnet','testnet','regtest'])
_finfo = namedtuple('fork_info', ['height', 'hash', 'name', 'replayable'])
_nw = namedtuple('coin_networks', ['mainnet', 'testnet', 'regtest'])
class CoinProtocol(MMGenObject):
proto_info = namedtuple('proto_info',['name','trust_level']) # trust levels: see altcoin/params.py
proto_info = namedtuple('proto_info', ['name', 'trust_level']) # trust levels: see altcoin/params.py
# keys are mirrored in gc.core_coins:
coins = {
@ -54,10 +54,10 @@ class CoinProtocol(MMGenObject):
base_coin = None
is_fork_of = None
chain_names = None
networks = ('mainnet','testnet','regtest')
networks = ('mainnet', 'testnet', 'regtest')
decimal_prec = 28
def __init__(self,cfg,coin,name,network,tokensym=None,need_amt=False):
def __init__(self, cfg, coin, name, network, tokensym=None, need_amt=False):
self.cfg = cfg
self.coin = coin.upper()
self.coin_id = self.coin
@ -65,24 +65,24 @@ class CoinProtocol(MMGenObject):
self.network = network
self.tokensym = tokensym
self.cls_name = type(self).__name__
self.testnet = network in ('testnet','regtest')
self.testnet = network in ('testnet', 'regtest')
self.regtest = network == 'regtest'
self.networks = tuple(k for k,v in self.network_names._asdict().items() if v)
self.networks = tuple(k for k, v in self.network_names._asdict().items() if v)
self.network_id = coin.lower() + {
'mainnet': '',
'testnet': '_tn',
'regtest': '_rt',
}[network]
if hasattr(self,'wif_ver_num'):
self.wif_ver_bytes = {k:bytes.fromhex(v) for k,v in self.wif_ver_num.items()}
self.wif_ver_bytes_to_pubkey_type = {v:k for k,v in self.wif_ver_bytes.items()}
if hasattr(self, 'wif_ver_num'):
self.wif_ver_bytes = {k:bytes.fromhex(v) for k, v in self.wif_ver_num.items()}
self.wif_ver_bytes_to_pubkey_type = {v:k for k, v in self.wif_ver_bytes.items()}
vbs = list(self.wif_ver_bytes.values())
self.wif_ver_bytes_len = len(vbs[0]) if len(set(len(b) for b in vbs)) == 1 else None
if hasattr(self,'addr_ver_info'):
self.addr_ver_bytes = {bytes.fromhex(k):v for k,v in self.addr_ver_info.items()}
self.addr_fmt_to_ver_bytes = {v:k for k,v in self.addr_ver_bytes.items()}
if hasattr(self, 'addr_ver_info'):
self.addr_ver_bytes = {bytes.fromhex(k):v for k, v in self.addr_ver_info.items()}
self.addr_fmt_to_ver_bytes = {v:k for k, v in self.addr_ver_bytes.items()}
self.addr_ver_bytes_len = len(list(self.addr_ver_bytes)[0])
if gc.cmd_caps:
@ -100,15 +100,15 @@ class CoinProtocol(MMGenObject):
if self.tokensym:
assert self.name.startswith('Ethereum'), 'CoinProtocol.Base_chk1'
if self.base_coin in ('ETH','XMR'):
if self.base_coin in ('ETH', 'XMR'):
from .util2 import get_keccak
self.keccak_256 = get_keccak(cfg)
if need_amt:
from . import amt
from decimal import getcontext
self.coin_amt = getattr(amt,self.coin_amt)
self.max_tx_fee = self.coin_amt(self.max_tx_fee) if hasattr(self,'max_tx_fee') else None
self.coin_amt = getattr(amt, self.coin_amt)
self.max_tx_fee = self.coin_amt(self.max_tx_fee) if hasattr(self, 'max_tx_fee') else None
getcontext().prec = self.decimal_prec
else:
self.coin_amt = None
@ -119,7 +119,7 @@ class CoinProtocol(MMGenObject):
return self.coin
@classmethod
def chain_name_to_network(cls,cfg,coin,chain_name):
def chain_name_to_network(cls, cfg, coin, chain_name):
"""
The generic networks 'mainnet', 'testnet' and 'regtest' are required for all coins
that support transaction operations.
@ -128,8 +128,8 @@ class CoinProtocol(MMGenObject):
the attribute 'chain_name' is used, while 'network' retains the generic name.
For Bitcoin and Bitcoin forks, 'network' and 'chain_name' are equivalent.
"""
for network in ('mainnet','testnet','regtest'):
proto = init_proto( cfg, coin, network=network )
for network in ('mainnet', 'testnet', 'regtest'):
proto = init_proto(cfg, coin, network=network)
for proto_chain_name in proto.chain_names:
if chain_name == proto_chain_name:
return network
@ -137,22 +137,22 @@ class CoinProtocol(MMGenObject):
@staticmethod
def parse_network_id(network_id):
nid = namedtuple('parsed_network_id',['coin','network'])
nid = namedtuple('parsed_network_id', ['coin', 'network'])
if network_id.endswith('_tn'):
return nid(network_id[:-3],'testnet')
return nid(network_id[:-3], 'testnet')
elif network_id.endswith('_rt'):
return nid(network_id[:-3],'regtest')
return nid(network_id[:-3], 'regtest')
else:
return nid(network_id,'mainnet')
return nid(network_id, 'mainnet')
@staticmethod
def create_network_id(coin,network):
return coin.lower() + { 'mainnet':'', 'testnet':'_tn', 'regtest':'_rt' }[network]
def create_network_id(coin, network):
return coin.lower() + {'mainnet':'', 'testnet':'_tn', 'regtest':'_rt'}[network]
def cap(self,s):
def cap(self, s):
return s in self.caps
def get_addr_len(self,addr_fmt):
def get_addr_len(self, addr_fmt):
return self.addr_len
def decode_addr_bytes(self, addr_bytes):
@ -162,18 +162,18 @@ class CoinProtocol(MMGenObject):
addr_bytes[:vlen],
self.addr_ver_bytes[addr_bytes[:vlen]])
def coin_addr(self,addr):
def coin_addr(self, addr):
from .addr import CoinAddr
return CoinAddr( proto=self, addr=addr )
return CoinAddr(proto=self, addr=addr)
def addr_type(self,id_str):
def addr_type(self, id_str):
from .addr import MMGenAddrType
return MMGenAddrType( proto=self, id_str=id_str )
return MMGenAddrType(proto=self, id_str=id_str)
def viewkey(self,viewkey_str):
def viewkey(self, viewkey_str):
raise NotImplementedError(f'{self.name} protocol does not support view keys')
def base_proto_subclass(self,cls,modname,sub_clsname=None):
def base_proto_subclass(self, cls, modname, sub_clsname=None):
"""
magic module loading and class selection
"""
@ -182,13 +182,13 @@ class CoinProtocol(MMGenObject):
clsname = (
self.mod_clsname
+ ('Token' if self.tokensym else '')
+ cls.__name__ )
+ cls.__name__)
import importlib
if sub_clsname:
return getattr(getattr(importlib.import_module(modpath),clsname),sub_clsname)
return getattr(getattr(importlib.import_module(modpath), clsname), sub_clsname)
else:
return getattr(importlib.import_module(modpath),clsname)
return getattr(importlib.import_module(modpath), clsname)
class Secp256k1(Base):
@ -199,42 +199,42 @@ class CoinProtocol(MMGenObject):
privkey_len = 32
pubkey_types = ('std',)
def parse_addr(self,ver_bytes,addr_bytes,fmt):
def parse_addr(self, ver_bytes, addr_bytes, fmt):
return parsed_addr(
ver_bytes = ver_bytes,
data = addr_bytes,
)
def preprocess_key(self,sec,pubkey_type):
def preprocess_key(self, sec, pubkey_type):
# Key must be non-zero and less than group order of secp256k1 curve
if 0 < int.from_bytes(sec,'big') < self.secp256k1_group_order:
if 0 < int.from_bytes(sec, 'big') < self.secp256k1_group_order:
return sec
else: # chance of this is less than 1 in 2^127
from .util import die,ymsg
pk = int.from_bytes(sec,'big')
from .util import die, ymsg
pk = int.from_bytes(sec, 'big')
if pk == 0: # chance of this is 1 in 2^256
die(4,'Private key is zero!')
die(4, 'Private key is zero!')
elif pk == self.secp256k1_group_order: # ditto
die(4,'Private key == secp256k1_group_order!')
die(4, 'Private key == secp256k1_group_order!')
else: # return key mod group order as the key
if not self.cfg.test_suite:
ymsg(f'Warning: private key is greater than secp256k1 group order!:\n {sec.hex()}')
return (pk % self.secp256k1_group_order).to_bytes(self.privkey_len,'big')
return (pk % self.secp256k1_group_order).to_bytes(self.privkey_len, 'big')
class DummyWIF:
"""
Ethereum and Monero protocols inherit from this class
"""
def encode_wif(self,privbytes,pubkey_type,compressed):
def encode_wif(self, privbytes, pubkey_type, compressed):
assert pubkey_type == self.pubkey_type, f'{pubkey_type}: invalid pubkey_type for {self.name} protocol!'
assert compressed is False, f'{self.name} protocol does not support compressed pubkeys!'
return privbytes.hex()
def decode_wif(self,wif):
def decode_wif(self, wif):
return decoded_wif(
sec = bytes.fromhex(wif),
pubkey_type = self.pubkey_type,
compressed = False )
compressed = False)
def init_proto(
cfg,
@ -253,7 +253,7 @@ def init_proto(
assert not (coin and network_id), 'init_proto_chk4'
if network_id:
coin,network = CoinProtocol.Base.parse_network_id(network_id)
coin, network = CoinProtocol.Base.parse_network_id(network_id)
elif network:
assert network in CoinProtocol.Base.networks, f'init_proto_chk5 - {network!r}: invalid network'
assert testnet is False, 'init_proto_chk6'
@ -265,37 +265,37 @@ def init_proto(
if coin not in CoinProtocol.coins:
from .altcoin.params import init_genonly_altcoins
init_genonly_altcoins( coin, testnet=testnet ) # raises exception on failure
init_genonly_altcoins(coin, testnet=testnet) # raises exception on failure
name = CoinProtocol.coins[coin].name
proto_name = name + ('' if network == 'mainnet' else network.capitalize())
if not hasattr(CoinProtocol,proto_name):
if not hasattr(CoinProtocol, proto_name):
import importlib
setattr(
CoinProtocol,
proto_name,
getattr(importlib.import_module(f'mmgen.proto.{coin}.params'),network)
getattr(importlib.import_module(f'mmgen.proto.{coin}.params'), network)
)
if return_cls:
return getattr(CoinProtocol,proto_name)
return getattr(CoinProtocol, proto_name)
return getattr(CoinProtocol,proto_name)(
return getattr(CoinProtocol, proto_name)(
cfg = cfg,
coin = coin,
name = name,
network = network,
tokensym = tokensym,
need_amt = need_amt )
need_amt = need_amt)
def init_proto_from_cfg(cfg,need_amt):
def init_proto_from_cfg(cfg, need_amt):
return init_proto(
cfg = cfg,
coin = cfg.coin,
network = cfg.network,
tokensym = cfg.token,
need_amt = need_amt )
need_amt = need_amt)
def warn_trustlevel(cfg):
@ -305,11 +305,11 @@ def warn_trustlevel(cfg):
trust_level = CoinProtocol.coins[coinsym.lower()].trust_level
else:
from .altcoin.params import CoinInfo
e = CoinInfo.get_entry(coinsym,'mainnet')
e = CoinInfo.get_entry(coinsym, 'mainnet')
trust_level = e.trust_level if e else None
if trust_level in (None,-1):
if trust_level in (None, -1):
from .util import die
die(1,f'Coin {coinsym} is not supported by {gc.proj_name}')
die(1, f'Coin {coinsym} is not supported by {gc.proj_name}')
if trust_level > 3:
return
@ -322,7 +322,7 @@ def warn_trustlevel(cfg):
"""
from .util import fmt
from .color import red,yellow,green
from .color import red, yellow, green
warning = fmt(m).strip().format(
c = coinsym.upper(),
@ -332,13 +332,13 @@ def warn_trustlevel(cfg):
2: yellow('MEDIUM'),
3: green('OK'),
}[trust_level],
p = gc.proj_name )
p = gc.proj_name)
if cfg.test_suite:
cfg._util.qmsg(warning)
return
from .ui import keypress_confirm
if not keypress_confirm( cfg, warning, default_yes=True ):
if not keypress_confirm(cfg, warning, default_yes=True):
import sys
sys.exit(0)

View file

@ -17,40 +17,40 @@ class PythonVersion(str):
major = 0
minor = 0
def __new__(cls,arg=None):
if isinstance(arg,PythonVersion):
def __new__(cls, arg=None):
if isinstance(arg, PythonVersion):
return arg
if arg:
major,minor = arg.split('.')
major, minor = arg.split('.')
else:
import platform
major,minor = platform.python_version_tuple()[:2]
me = str.__new__( cls, f'{major}.{minor}' )
major, minor = platform.python_version_tuple()[:2]
me = str.__new__(cls, f'{major}.{minor}')
me.major = int(major)
me.minor = int(minor)
return me
def __lt__(self,other):
def __lt__(self, other):
other = type(self)(other)
return self.major < other.major or (self.major == other.major and self.minor < other.minor)
def __le__(self,other):
def __le__(self, other):
other = type(self)(other)
return self.major < other.major or (self.major == other.major and self.minor <= other.minor)
def __eq__(self,other):
def __eq__(self, other):
other = type(self)(other)
return self.major == other.major and self.minor == other.minor
def __ne__(self,other):
def __ne__(self, other):
other = type(self)(other)
return not (self.major == other.major and self.minor == other.minor)
def __gt__(self,other):
def __gt__(self, other):
other = type(self)(other)
return self.major > other.major or (self.major == other.major and self.minor > other.minor)
def __ge__(self,other):
def __ge__(self, other):
other = type(self)(other)
return self.major > other.major or (self.major == other.major and self.minor >= other.minor)

View file

@ -20,43 +20,43 @@
rpc: Cryptocoin RPC library for the MMGen suite
"""
import sys,re,base64,json,asyncio,importlib
import sys, re, base64, json, asyncio, importlib
from decimal import Decimal
from collections import namedtuple
from .util import msg,ymsg,die,fmt,fmt_list,pp_fmt,oneshot_warning
from .util import msg, ymsg, die, fmt, fmt_list, pp_fmt, oneshot_warning
from .base_obj import AsyncInit
from .obj import NonNegativeInt
from .objmethods import HiliteStr,InitErrors,MMGenObject
from .objmethods import HiliteStr, InitErrors, MMGenObject
auth_data = namedtuple('rpc_auth_data',['user','passwd'])
auth_data = namedtuple('rpc_auth_data', ['user', 'passwd'])
def dmsg_rpc(fs,data=None,is_json=False):
def dmsg_rpc(fs, data=None, is_json=False):
msg(
fs if data is None else
fs.format(pp_fmt(json.loads(data) if is_json else data))
)
def dmsg_rpc_backend(host_url,host_path,payload):
def dmsg_rpc_backend(host_url, host_path, payload):
msg(
f'\n RPC URL: {host_url}{host_path}' +
'\n RPC PAYLOAD data (httplib) ==>' +
f'\n{pp_fmt(payload)}\n' )
f'\n{pp_fmt(payload)}\n')
def noop(*args,**kwargs):
def noop(*args, **kwargs):
pass
class IPPort(HiliteStr,InitErrors):
class IPPort(HiliteStr, InitErrors):
color = 'yellow'
width = 0
trunc_ok = False
min_len = 9 # 0.0.0.0:0
max_len = 21 # 255.255.255.255:65535
def __new__(cls,s):
if isinstance(s,cls):
def __new__(cls, s):
if isinstance(s, cls):
return s
try:
m = re.fullmatch(r'{q}\.{q}\.{q}\.{q}:(\d{{1,10}})'.format(q=r'([0-9]{1,3})'),s)
m = re.fullmatch(r'{q}\.{q}\.{q}\.{q}:(\d{{1,10}})'.format(q=r'([0-9]{1,3})'), s)
assert m is not None, f'{s!r}: invalid IP:HOST specifier'
for e in m.groups():
if len(e) != 1 and e[0] == '0':
@ -65,13 +65,13 @@ class IPPort(HiliteStr,InitErrors):
for e in res[:4]:
assert e <= 255, f'{e}: dotted decimal element > 255'
assert res[4] <= 65535, f'{res[4]}: port number > 65535'
me = str.__new__(cls,s)
me = str.__new__(cls, s)
me.ip = '{}.{}.{}.{}'.format(*res)
me.ip_num = sum( res[i] * ( 2 ** (-(i-3)*8) ) for i in range(4) )
me.ip_num = sum(res[i] * (2 ** (-(i-3)*8)) for i in range(4))
me.port = res[4]
return me
except Exception as e:
return cls.init_fail(e,s)
return cls.init_fail(e, s)
class json_encoder(json.JSONEncoder):
def default(self, o):
@ -84,7 +84,7 @@ class RPCBackends:
class base:
def __init__(self,caller):
def __init__(self, caller):
self.cfg = caller.cfg
self.host = caller.host
self.port = caller.port
@ -95,7 +95,7 @@ class RPCBackends:
self.name = type(self).__name__
self.caller = caller
class aiohttp(base,metaclass=AsyncInit):
class aiohttp(base, metaclass=AsyncInit):
"""
Contrary to the requests library, aiohttp wont read environment variables by
default. But you can do so by passing trust_env=True into aiohttp.ClientSession
@ -108,61 +108,61 @@ class RPCBackends:
self.session.detach()
del self.session
async def __init__(self,caller):
async def __init__(self, caller):
super().__init__(caller)
import aiohttp
self.connector = aiohttp.TCPConnector(limit_per_host=self.cfg.aiohttp_rpc_queue_len)
self.session = aiohttp.ClientSession(
headers = { 'Content-Type': 'application/json' },
headers = {'Content-Type': 'application/json'},
connector = self.connector,
)
if caller.auth_type == 'basic':
self.auth = aiohttp.BasicAuth(*caller.auth,encoding='UTF-8')
self.auth = aiohttp.BasicAuth(*caller.auth, encoding='UTF-8')
else:
self.auth = None
async def run(self,payload,timeout,host_path):
dmsg_rpc_backend(self.host_url,host_path,payload)
async def run(self, payload, timeout, host_path):
dmsg_rpc_backend(self.host_url, host_path, payload)
async with self.session.post(
url = self.host_url + host_path,
auth = self.auth,
data = json.dumps(payload,cls=json_encoder),
data = json.dumps(payload, cls=json_encoder),
timeout = timeout or self.timeout,
) as res:
return (await res.text(),res.status)
return (await res.text(), res.status)
class requests(base):
def __del__(self):
self.session.close()
def __init__(self,caller):
def __init__(self, caller):
super().__init__(caller)
import requests,urllib3
import requests, urllib3
urllib3.disable_warnings()
self.session = requests.Session()
self.session.trust_env = False # ignore *_PROXY environment vars
self.session.headers = caller.http_hdrs
if caller.auth_type:
auth = 'HTTP' + caller.auth_type.capitalize() + 'Auth'
self.session.auth = getattr(requests.auth,auth)(*caller.auth)
self.session.auth = getattr(requests.auth, auth)(*caller.auth)
if self.proxy: # used only by XMR for now: requires pysocks package
self.session.proxies.update({
'http': f'socks5h://{self.proxy}',
'https': f'socks5h://{self.proxy}'
})
async def run(self,*args,**kwargs):
return self.run_noasync(*args,**kwargs)
async def run(self, *args, **kwargs):
return self.run_noasync(*args, **kwargs)
def run_noasync(self,payload,timeout,host_path):
dmsg_rpc_backend(self.host_url,host_path,payload)
def run_noasync(self, payload, timeout, host_path):
dmsg_rpc_backend(self.host_url, host_path, payload)
res = self.session.post(
url = self.host_url + host_path,
data = json.dumps(payload,cls=json_encoder),
data = json.dumps(payload, cls=json_encoder),
timeout = timeout or self.timeout,
verify = False )
return (res.content,res.status_code)
verify = False)
return (res.content, res.status_code)
class httplib(base):
"""
@ -171,22 +171,22 @@ class RPCBackends:
def __del__(self):
self.session.close()
def __init__(self,caller):
def __init__(self, caller):
super().__init__(caller)
import http.client
self.session = http.client.HTTPConnection(caller.host,caller.port,caller.timeout)
self.session = http.client.HTTPConnection(caller.host, caller.port, caller.timeout)
if caller.auth_type == 'basic':
auth_str = f'{caller.auth.user}:{caller.auth.passwd}'
auth_str_b64 = 'Basic ' + base64.b64encode(auth_str.encode()).decode()
self.http_hdrs.update({ 'Host': self.host, 'Authorization': auth_str_b64 })
self.http_hdrs.update({'Host': self.host, 'Authorization': auth_str_b64})
dmsg_rpc(f' RPC AUTHORIZATION data ==> raw: [{auth_str}]\n{"":>31}enc: [{auth_str_b64}]\n')
async def run(self,payload,timeout,host_path):
dmsg_rpc_backend(self.host_url,host_path,payload)
async def run(self, payload, timeout, host_path):
dmsg_rpc_backend(self.host_url, host_path, payload)
if timeout:
import http.client
s = http.client.HTTPConnection(self.host,self.port,timeout)
s = http.client.HTTPConnection(self.host, self.port, timeout)
else:
s = self.session
@ -194,25 +194,25 @@ class RPCBackends:
s.request(
method = 'POST',
url = host_path,
body = json.dumps(payload,cls=json_encoder),
headers = self.http_hdrs )
body = json.dumps(payload, cls=json_encoder),
headers = self.http_hdrs)
r = s.getresponse() # => http.client.HTTPResponse instance
except Exception as e:
die( 'RPCFailure', str(e) )
die('RPCFailure', str(e))
if timeout:
ret = ( r.read(), r.status )
ret = (r.read(), r.status)
s.close()
return ret
else:
return ( r.read(), r.status )
return (r.read(), r.status)
class curl(base):
def __init__(self,caller):
def __init__(self, caller):
def gen_opts():
for k,v in caller.http_hdrs.items():
for k, v in caller.http_hdrs.items():
yield from ('--header', f'{k}: {v}')
if caller.auth_type:
# Authentication with curl is insecure, as it exposes the user's credentials
@ -227,12 +227,12 @@ class RPCBackends:
self.exec_opts = list(gen_opts()) + ['--silent']
self.arg_max = 8192 # set way below system ARG_MAX, just to be safe
async def run(self,payload,timeout,host_path):
data = json.dumps(payload,cls=json_encoder)
async def run(self, payload, timeout, host_path):
data = json.dumps(payload, cls=json_encoder)
if len(data) > self.arg_max:
ymsg('Warning: Curl data payload length exceeded - falling back on httplib')
return RPCBackends.httplib(self.caller).run(payload,timeout,host_path)
dmsg_rpc_backend(self.host_url,host_path,payload)
return RPCBackends.httplib(self.caller).run(payload, timeout, host_path)
dmsg_rpc_backend(self.host_url, host_path, payload)
exec_cmd = [
'curl',
'--proxy', f'socks5h://{self.proxy}' if self.proxy else '',
@ -241,13 +241,13 @@ class RPCBackends:
'--data-binary', data
] + self.exec_opts + [self.host_url + host_path]
dmsg_rpc(' RPC curl exec data ==>\n{}\n',exec_cmd)
dmsg_rpc(' RPC curl exec data ==>\n{}\n', exec_cmd)
from subprocess import run,PIPE
from subprocess import run, PIPE
from .color import set_vt100
res = run(exec_cmd,stdout=PIPE,check=True,text=True).stdout
res = run(exec_cmd, stdout=PIPE, check=True, text=True).stdout
set_vt100()
return (res[:-3],int(res[-3:]))
return (res[:-3], int(res[-3:]))
class RPCClient(MMGenObject):
@ -256,7 +256,7 @@ class RPCClient(MMGenObject):
network_proto = 'http'
proxy = None
def __init__(self,cfg,host,port,test_connection=True):
def __init__(self, cfg, host, port, test_connection=True):
self.cfg = cfg
self.name = type(self).__name__
@ -265,7 +265,7 @@ class RPCClient(MMGenObject):
if sys.platform == 'win32' and host == 'localhost':
host = '127.0.0.1'
global dmsg_rpc,dmsg_rpc_backend
global dmsg_rpc, dmsg_rpc_backend
if not self.cfg.debug_rpc:
dmsg_rpc = dmsg_rpc_backend = noop
@ -275,18 +275,18 @@ class RPCClient(MMGenObject):
if test_connection:
import socket
try:
socket.create_connection((host,port),timeout=1).close()
socket.create_connection((host, port), timeout=1).close()
except:
die( 'SocketError', f'Unable to connect to {host}:{port}' )
die('SocketError', f'Unable to connect to {host}:{port}')
self.http_hdrs = { 'Content-Type': 'application/json' }
self.http_hdrs = {'Content-Type': 'application/json'}
self.host_url = f'{self.network_proto}://{host}:{port}'
self.host = host
self.port = port
self.timeout = self.cfg.http_timeout
self.auth = None
def _get_backend(self,backend):
def _get_backend(self, backend):
backend_id = backend or self.cfg.rpc_backend
if backend_id == 'auto':
return {
@ -295,12 +295,12 @@ class RPCClient(MMGenObject):
'win32': RPCBackends.requests
}[sys.platform](self)
else:
return getattr(RPCBackends,backend_id)(self)
return getattr(RPCBackends, backend_id)(self)
def set_backend(self,backend=None):
def set_backend(self, backend=None):
self.backend = self._get_backend(backend)
async def set_backend_async(self,backend=None):
async def set_backend_async(self, backend=None):
ret = self._get_backend(backend)
self.backend = (await ret) if type(ret).__name__ == 'coroutine' else ret
@ -308,17 +308,17 @@ class RPCClient(MMGenObject):
# - positional params are passed to the daemon, 'timeout' and 'wallet' kwargs to the backend
# - 'wallet' kwarg is used only by regtest
async def call(self,method,*params,timeout=None,wallet=None):
async def call(self, method, *params, timeout=None, wallet=None):
"""
default call: call with param list unrolled, exactly as with cli
"""
return self.process_http_resp(await self.backend.run(
payload = {'id': 1, 'jsonrpc': '2.0', 'method': method, 'params': params },
payload = {'id': 1, 'jsonrpc': '2.0', 'method': method, 'params': params},
timeout = timeout,
host_path = self.make_host_path(wallet)
))
async def batch_call(self,method,param_list,timeout=None,wallet=None):
async def batch_call(self, method, param_list, timeout=None, wallet=None):
"""
Make a single call with a list of tuples as first argument
For RPC calls that return a list of results
@ -328,17 +328,17 @@ class RPCClient(MMGenObject):
'id': n,
'jsonrpc': '2.0',
'method': method,
'params': params } for n,params in enumerate(param_list,1) ],
'params': params} for n, params in enumerate(param_list, 1)],
timeout = timeout,
host_path = self.make_host_path(wallet)
),batch=True)
), batch=True)
async def gathered_call(self,method,args_list,timeout=None,wallet=None):
async def gathered_call(self, method, args_list, timeout=None, wallet=None):
"""
Perform multiple RPC calls, returning results in a list
Can be called two ways:
1) method = methodname, args_list = [args_tuple1, args_tuple2,...]
2) method = None, args_list = [(methodname1,args_tuple1), (methodname2,args_tuple2), ...]
2) method = None, args_list = [(methodname1, args_tuple1), (methodname2, args_tuple2), ...]
"""
cmd_list = args_list if method is None else tuple(zip([method] * len(args_list), args_list))
@ -348,10 +348,10 @@ class RPCClient(MMGenObject):
while cur_pos < len(cmd_list):
tasks = [self.backend.run(
payload = {'id': n, 'jsonrpc': '2.0', 'method': method, 'params': params },
payload = {'id': n, 'jsonrpc': '2.0', 'method': method, 'params': params},
timeout = timeout,
host_path = self.make_host_path(wallet)
) for n,(method,params) in enumerate(cmd_list[cur_pos:chunk_size+cur_pos],1)]
) for n, (method, params) in enumerate(cmd_list[cur_pos:chunk_size+cur_pos], 1)]
ret.extend(await asyncio.gather(*tasks))
cur_pos += chunk_size
@ -362,22 +362,22 @@ class RPCClient(MMGenObject):
# - remaining kwargs are passed to CallSigs method
# - CallSigs method returns method and positional params for Call method
def icall(self,method,**kwargs):
timeout = kwargs.pop('timeout',None)
wallet = kwargs.pop('wallet',None)
def icall(self, method, **kwargs):
timeout = kwargs.pop('timeout', None)
wallet = kwargs.pop('wallet', None)
return self.call(
*getattr(self.call_sigs,method)(**kwargs),
*getattr(self.call_sigs, method)(**kwargs),
timeout = timeout,
wallet = wallet )
wallet = wallet)
def gathered_icall(self,method,args_list,timeout=None,wallet=None):
def gathered_icall(self, method, args_list, timeout=None, wallet=None):
return self.gathered_call(
method,
[getattr(self.call_sigs,method)(*a)[1:] for a in args_list],
[getattr(self.call_sigs, method)(*a)[1:] for a in args_list],
timeout = timeout,
wallet = wallet )
wallet = wallet)
def process_http_resp(self,run_ret,batch=False,json_rpc=True):
def process_http_resp(self, run_ret, batch=False, json_rpc=True):
def float_parser(n):
return n
@ -385,22 +385,22 @@ class RPCClient(MMGenObject):
text, status = run_ret
if status == 200:
dmsg_rpc(' RPC RESPONSE data ==>\n{}\n',text,is_json=True)
dmsg_rpc(' RPC RESPONSE data ==>\n{}\n', text, is_json=True)
m = None
if batch:
return [r['result'] for r in json.loads(text,parse_float=float_parser)]
return [r['result'] for r in json.loads(text, parse_float=float_parser)]
else:
try:
if json_rpc:
ret = json.loads(text,parse_float=float_parser)['result']
if isinstance(ret,list) and ret and type(ret[0]) == dict and 'success' in ret[0]:
ret = json.loads(text, parse_float=float_parser)['result']
if isinstance(ret, list) and ret and type(ret[0]) == dict and 'success' in ret[0]:
for res in ret:
if not res['success']:
m = str(res['error'])
assert False
return ret
else:
return json.loads(text,parse_float=float_parser)
return json.loads(text, parse_float=float_parser)
except:
if not m:
t = json.loads(text)
@ -414,7 +414,7 @@ class RPCClient(MMGenObject):
die('RPCFailure', m)
else:
import http
m,s = ( '', http.HTTPStatus(status) )
m, s = ('', http.HTTPStatus(status))
if text:
try:
m = json.loads(text)['error']['message']
@ -423,9 +423,9 @@ class RPCClient(MMGenObject):
m = text.decode()
except:
m = text
die( 'RPCFailure', f'{s.value} {s.name}: {m}' )
die('RPCFailure', f'{s.value} {s.name}: {m}')
async def stop_daemon(self,quiet=False,silent=False):
async def stop_daemon(self, quiet=False, silent=False):
if self.daemon.state == 'ready':
if not (quiet or silent):
msg(f'Stopping {self.daemon.desc} on port {self.daemon.bind_port}')
@ -438,24 +438,24 @@ class RPCClient(MMGenObject):
msg(f'{self.daemon.desc} on port {self.daemon.bind_port} not running')
return True
def start_daemon(self,silent=False):
def start_daemon(self, silent=False):
return self.daemon.start(silent=silent)
async def restart_daemon(self,quiet=False,silent=False):
await self.stop_daemon(quiet=quiet,silent=silent)
async def restart_daemon(self, quiet=False, silent=False):
await self.stop_daemon(quiet=quiet, silent=silent)
return self.daemon.start(silent=silent)
def handle_unsupported_daemon_version(self,name,warn_only):
def handle_unsupported_daemon_version(self, name, warn_only):
class daemon_version_warning(oneshot_warning):
color = 'yellow'
message = 'ignoring unsupported {} daemon version at user request'
if warn_only:
daemon_version_warning(div=name,fmt_args=[self.daemon.coind_name])
daemon_version_warning(div=name, fmt_args=[self.daemon.coind_name])
else:
name = self.daemon.coind_name
die(2,'\n'+fmt(f"""
die(2, '\n'+fmt(f"""
The running {name} daemon has version {self.daemon_version_str}.
This version of MMGen is tested only on {name} v{self.daemon.coind_version_str} and below.
@ -463,7 +463,7 @@ class RPCClient(MMGenObject):
Alternatively, you may invoke the command with the --ignore-daemon-version
option, in which case you proceed at your own risk.
""",indent=' '))
""", indent=' '))
async def rpc_init(
cfg,
@ -471,36 +471,36 @@ async def rpc_init(
backend = None,
daemon = None,
ignore_daemon_version = False,
ignore_wallet = False ):
ignore_wallet = False):
proto = proto or cfg._proto
if not 'rpc_init' in proto.mmcaps:
die(1,f'rpc_init() not supported for {proto.name} protocol!')
die(1, f'rpc_init() not supported for {proto.name} protocol!')
cls = getattr(
importlib.import_module(f'mmgen.proto.{proto.base_proto_coin.lower()}.rpc'),
proto.base_proto + 'RPCClient' )
proto.base_proto + 'RPCClient')
from .daemon import CoinDaemon
rpc = await cls(
cfg = cfg,
proto = proto,
daemon = daemon or CoinDaemon(cfg,proto=proto,test_suite=cfg.test_suite),
daemon = daemon or CoinDaemon(cfg, proto=proto, test_suite=cfg.test_suite),
backend = backend or cfg.rpc_backend,
ignore_wallet = ignore_wallet )
ignore_wallet = ignore_wallet)
if rpc.daemon_version > rpc.daemon.coind_version:
rpc.handle_unsupported_daemon_version(
proto.name,
ignore_daemon_version or proto.ignore_daemon_version or cfg.ignore_daemon_version )
ignore_daemon_version or proto.ignore_daemon_version or cfg.ignore_daemon_version)
if rpc.chain not in proto.chain_names:
die( 'RPCChainMismatch', '\n' + fmt(f"""
die('RPCChainMismatch', '\n' + fmt(f"""
Protocol: {proto.cls_name}
Valid chain names: {fmt_list(proto.chain_names,fmt='bare')}
Valid chain names: {fmt_list(proto.chain_names, fmt='bare')}
RPC client chain: {rpc.chain}
""",indent=' ').rstrip() )
""", indent=' ').rstrip())
rpc.blockcount = NonNegativeInt(rpc.blockcount)

View file

@ -20,41 +20,41 @@
seed: Seed-related classes and methods for the MMGen suite
"""
from .util import make_chksum_8,hexdigits_uc,die
from .objmethods import HiliteStr,InitErrors,MMGenObject
from .obj import ImmutableAttr,get_obj
from .util import make_chksum_8, hexdigits_uc, die
from .objmethods import HiliteStr, InitErrors, MMGenObject
from .obj import ImmutableAttr, get_obj
class SeedID(HiliteStr,InitErrors):
class SeedID(HiliteStr, InitErrors):
color = 'blue'
width = 8
trunc_ok = False
def __new__(cls,seed=None,sid=None):
if isinstance(sid,cls):
def __new__(cls, seed=None, sid=None):
if isinstance(sid, cls):
return sid
try:
if seed:
assert isinstance(seed,SeedBase),'not a subclass of SeedBase'
return str.__new__(cls,make_chksum_8(seed.data))
assert isinstance(seed, SeedBase), 'not a subclass of SeedBase'
return str.__new__(cls, make_chksum_8(seed.data))
elif sid:
assert set(sid) <= set(hexdigits_uc), 'not uppercase hex digits'
assert len(sid) == cls.width, f'not {cls.width} characters wide'
return str.__new__(cls,sid)
return str.__new__(cls, sid)
raise ValueError('no arguments provided')
except Exception as e:
return cls.init_fail(e,seed or sid)
return cls.init_fail(e, seed or sid)
def is_seed_id(s):
return get_obj( SeedID, sid=s, silent=True, return_bool=True )
return get_obj(SeedID, sid=s, silent=True, return_bool=True)
class SeedBase(MMGenObject):
lens = ( 128, 192, 256 )
lens = (128, 192, 256)
dfl_len = 256
data = ImmutableAttr(bytes,typeconv=False)
sid = ImmutableAttr(SeedID,typeconv=False)
data = ImmutableAttr(bytes, typeconv=False)
sid = ImmutableAttr(SeedID, typeconv=False)
def __init__(self,cfg,seed_bin=None,nSubseeds=None):
def __init__(self, cfg, seed_bin=None, nSubseeds=None):
if not seed_bin:
from .crypto import Crypto
@ -62,7 +62,7 @@ class SeedBase(MMGenObject):
# Truncate random data for smaller seed lengths
seed_bin = sha256(Crypto(cfg).get_random(1033)).digest()[:(cfg.seed_len or self.dfl_len)//8]
elif len(seed_bin)*8 not in self.lens:
die(3,f'{len(seed_bin)*8}: invalid seed bit length')
die(3, f'{len(seed_bin)*8}: invalid seed bit length')
self.cfg = cfg
self.data = seed_bin
@ -85,24 +85,24 @@ class Seed(SeedBase):
@property
def subseeds(self):
if not hasattr(self,'_subseeds'):
if not hasattr(self, '_subseeds'):
from .subseed import SubSeedList
self._subseeds = SubSeedList(
self,
length = self.nSubseeds or self.cfg.subseeds )
length = self.nSubseeds or self.cfg.subseeds)
return self._subseeds
def subseed(self,*args,**kwargs):
return self.subseeds.get_subseed_by_ss_idx(*args,**kwargs)
def subseed(self, *args, **kwargs):
return self.subseeds.get_subseed_by_ss_idx(*args, **kwargs)
def subseed_by_seed_id(self,*args,**kwargs):
return self.subseeds.get_subseed_by_seed_id(*args,**kwargs)
def subseed_by_seed_id(self, *args, **kwargs):
return self.subseeds.get_subseed_by_seed_id(*args, **kwargs)
def split(self,*args,**kwargs):
def split(self, *args, **kwargs):
from .seedsplit import SeedShareList
return SeedShareList(self,*args,**kwargs)
return SeedShareList(self, *args, **kwargs)
@staticmethod
def join_shares(*args,**kwargs):
def join_shares(*args, **kwargs):
from .seedsplit import join_shares
return join_shares(*args,**kwargs)
return join_shares(*args, **kwargs)

View file

@ -21,11 +21,11 @@ seedsplit: Seed split classes and methods for the MMGen suite
"""
from .color import yellow
from .util import msg,die
from .objmethods import MMGenObject,HiliteStr,InitErrors
from .obj import ImmutableAttr,MMGenPWIDString,MMGenIdx,get_obj,IndexedDict
from .seed import Seed,SeedBase
from .subseed import SubSeedList,SubSeedIdx,SubSeed
from .util import msg, die
from .objmethods import MMGenObject, HiliteStr, InitErrors
from .obj import ImmutableAttr, MMGenPWIDString, MMGenIdx, get_obj, IndexedDict
from .seed import Seed, SeedBase
from .subseed import SubSeedList, SubSeedIdx, SubSeed
from .crypto import Crypto
class SeedShareIdx(MMGenIdx):
@ -37,26 +37,26 @@ class SeedShareCount(SeedShareIdx):
class MasterShareIdx(MMGenIdx):
max_val = 1024
class SeedSplitSpecifier(HiliteStr,InitErrors,MMGenObject):
class SeedSplitSpecifier(HiliteStr, InitErrors, MMGenObject):
color = 'red'
def __new__(cls,s):
if isinstance(s,cls):
def __new__(cls, s):
if isinstance(s, cls):
return s
try:
arr = s.split(':')
assert len(arr) in (2,3), 'cannot be parsed'
a,b,c = arr if len(arr) == 3 else ['default'] + arr
me = str.__new__(cls,s)
assert len(arr) in (2, 3), 'cannot be parsed'
a, b, c = arr if len(arr) == 3 else ['default'] + arr
me = str.__new__(cls, s)
me.id = SeedSplitIDString(a)
me.idx = SeedShareIdx(b)
me.count = SeedShareCount(c)
assert me.idx <= me.count, 'share index greater than share count'
return me
except Exception as e:
return cls.init_fail(e,s)
return cls.init_fail(e, s)
def is_seed_split_specifier(s):
return get_obj( SeedSplitSpecifier, s=s, silent=True, return_bool=True )
return get_obj(SeedSplitSpecifier, s=s, silent=True, return_bool=True)
class SeedSplitIDString(MMGenPWIDString):
desc = 'seed split ID string'
@ -68,7 +68,7 @@ class SeedShareList(SubSeedList):
count = ImmutableAttr(SeedShareCount)
id_str = ImmutableAttr(SeedSplitIDString)
def __init__(self,parent_seed,count,id_str=None,master_idx=None,debug_last_share=False):
def __init__(self, parent_seed, count, id_str=None, master_idx=None, debug_last_share=False):
self.member_type = SeedShare
self.parent_seed = parent_seed
self.id_str = id_str or 'default'
@ -77,13 +77,13 @@ class SeedShareList(SubSeedList):
def make_master_share():
for nonce in range(SeedShare.max_nonce+1):
ms = SeedShareMaster(self,master_idx,nonce)
ms = SeedShareMaster(self, master_idx, nonce)
if ms.sid == parent_seed.sid:
if parent_seed.cfg.debug_subseed:
msg(f'master_share seed ID collision with parent seed, incrementing nonce to {nonce+1}')
else:
return ms
die( 'SubSeedNonceRangeExceeded', 'nonce range exceeded' )
die('SubSeedNonceRangeExceeded', 'nonce range exceeded')
def last_share_debug(last_share):
if not debug_last_share:
@ -98,29 +98,29 @@ class SeedShareList(SubSeedList):
for nonce in range(SeedShare.max_nonce+1):
self.nonce_start = nonce
self.data = { 'long': IndexedDict(), 'short': IndexedDict() } # 'short' is required as a placeholder
self.data = {'long': IndexedDict(), 'short': IndexedDict()} # 'short' is required as a placeholder
if self.master_share:
self.data['long'][self.master_share.sid] = (1,self.master_share.nonce)
self.data['long'][self.master_share.sid] = (1, self.master_share.nonce)
self._generate(count-1)
self.last_share = ls = SeedShareLast(self)
if last_share_debug(ls) or ls.sid in self.data['long'] or ls.sid == parent_seed.sid:
# collision: throw out entire split list and redo with new start nonce
if parent_seed.cfg.debug_subseed:
self._collision_debug_msg(ls.sid,count,nonce,'nonce_start',debug_last_share)
self._collision_debug_msg(ls.sid, count, nonce, 'nonce_start', debug_last_share)
else:
self.data['long'][ls.sid] = (count,nonce)
self.data['long'][ls.sid] = (count, nonce)
break
else:
die( 'SubSeedNonceRangeExceeded', 'nonce range exceeded' )
die('SubSeedNonceRangeExceeded', 'nonce range exceeded')
if parent_seed.cfg.debug_subseed:
A = parent_seed.data
B = self.join().data
assert A == B, f'Data mismatch!\noriginal seed: {A!r}\nrejoined seed: {B!r}'
def get_share_by_idx(self,idx,base_seed=False):
def get_share_by_idx(self, idx, base_seed=False):
if idx < 1 or idx > self.count:
die( 'RangeError', f'{idx}: share index out of range' )
die('RangeError', f'{idx}: share index out of range')
elif idx == self.count:
return self.last_share
elif self.master_share and idx == 1:
@ -129,7 +129,7 @@ class SeedShareList(SubSeedList):
ss_idx = SubSeedIdx(str(idx) + 'L')
return self.get_subseed_by_ss_idx(ss_idx)
def get_share_by_seed_id(self,sid,base_seed=False):
def get_share_by_seed_id(self, sid, base_seed=False):
if sid == self.data['long'].key(self.count-1):
return self.last_share
elif self.master_share and sid == self.data['long'].key(0):
@ -140,26 +140,26 @@ class SeedShareList(SubSeedList):
def join(self):
return Seed.join_shares(
self.parent_seed.cfg,
[self.get_share_by_idx(i+1) for i in range(len(self))] )
[self.get_share_by_idx(i+1) for i in range(len(self))])
def format(self):
assert self.split_type == 'N-of-N'
fs1 = ' {}\n'
fs2 = '{i:>5}: {}\n'
mfs1,mfs2,midx,msid = ('','','','')
mfs1, mfs2, midx, msid = ('', '', '', '')
if self.master_share:
mfs1,mfs2 = (' with master share #{} ({})',' (master share #{})')
midx,msid = (self.master_share.idx,self.master_share.sid)
mfs1, mfs2 = (' with master share #{} ({})', ' (master share #{})')
midx, msid = (self.master_share.idx, self.master_share.sid)
hdr = ' {} {} ({} bits)\n'.format('Seed:',self.parent_seed.sid.hl(),self.parent_seed.bitlen)
hdr += ' {} {c}-of-{c} (XOR){m}\n'.format('Split Type:',c=self.count,m=mfs1.format(midx,msid))
hdr += ' {} {}\n\n'.format('ID String:',self.id_str.hl())
hdr = ' {} {} ({} bits)\n'.format('Seed:', self.parent_seed.sid.hl(), self.parent_seed.bitlen)
hdr += ' {} {c}-of-{c} (XOR){m}\n'.format('Split Type:', c=self.count, m=mfs1.format(midx, msid))
hdr += ' {} {}\n\n'.format('ID String:', self.id_str.hl())
hdr += fs1.format('Shares')
hdr += fs1.format('------')
sl = self.data['long'].keys
body1 = fs2.format(sl[0]+mfs2.format(midx),i=1)
body = (fs2.format(sl[n],i=n+1) for n in range(1,len(self)))
body1 = fs2.format(sl[0]+mfs2.format(midx), i=1)
body = (fs2.format(sl[n], i=n+1) for n in range(1, len(self)))
return hdr + body1 + ''.join(body)
@ -181,103 +181,100 @@ class SeedShareBase(MMGenObject):
def desc(self):
return self.get_desc()
def get_desc(self,ui=False):
def get_desc(self, ui=False):
pl = self.parent_list
mss = f', with master share #{pl.master_share.idx}' if pl.master_share else ''
if ui:
m = ( yellow("(share {} of {} of ")
m = (yellow("(share {} of {} of ")
+ pl.parent_seed.sid.hl()
+ yellow(', split id ')
+ pl.id_str.hl2(encl='‘’')
+ yellow('{})') )
+ yellow('{})'))
else:
m = "share {} of {} of " + pl.parent_seed.sid + ", split id '" + pl.id_str + "'{}"
return m.format(self.idx,pl.count,mss)
return m.format(self.idx, pl.count, mss)
class SeedShare(SeedShareBase,SubSeed):
class SeedShare(SeedShareBase, SubSeed):
@staticmethod
def make_subseed_bin(parent_list,idx:int,nonce:int,length:str):
def make_subseed_bin(parent_list, idx:int, nonce:int, length:str):
seed = parent_list.parent_seed
assert parent_list.have_short is False
assert length == 'long'
# field maximums: id_str: none (256 chars), count: 65535 (1024), idx: 65535 (1024), nonce: 65535 (1000)
scramble_key = (
f'{parent_list.split_type}:{parent_list.id_str}:'.encode() +
parent_list.count.to_bytes(2,'big') +
idx.to_bytes(2,'big') +
nonce.to_bytes(2,'big')
parent_list.count.to_bytes(2, 'big') +
idx.to_bytes(2, 'big') +
nonce.to_bytes(2, 'big')
)
if parent_list.master_share:
scramble_key += (
b':master:' +
parent_list.master_share.idx.to_bytes(2,'big')
parent_list.master_share.idx.to_bytes(2, 'big')
)
return Crypto(parent_list.parent_seed.cfg).scramble_seed(seed.data,scramble_key)[:seed.byte_len]
return Crypto(parent_list.parent_seed.cfg).scramble_seed(seed.data, scramble_key)[:seed.byte_len]
class SeedShareLast(SeedShareBase,SeedBase):
class SeedShareLast(SeedShareBase, SeedBase):
idx = ImmutableAttr(SeedShareIdx)
nonce = 0
def __init__(self,parent_list):
def __init__(self, parent_list):
self.idx = parent_list.count
self.parent_list = parent_list
SeedBase.__init__(
self,
parent_list.parent_seed.cfg,
seed_bin=self.make_subseed_bin(parent_list) )
seed_bin=self.make_subseed_bin(parent_list))
@staticmethod
def make_subseed_bin(parent_list):
seed_list = (parent_list.get_share_by_idx(i+1) for i in range(len(parent_list)))
seed = parent_list.parent_seed
ret = int(seed.data.hex(),16)
ret = int(seed.data.hex(), 16)
for ss in seed_list:
ret ^= int(ss.data.hex(),16)
ret ^= int(ss.data.hex(), 16)
return ret.to_bytes(seed.byte_len,'big')
return ret.to_bytes(seed.byte_len, 'big')
class SeedShareMaster(SeedBase,SeedShareBase):
class SeedShareMaster(SeedBase, SeedShareBase):
idx = ImmutableAttr(MasterShareIdx)
nonce = ImmutableAttr(int,typeconv=False)
nonce = ImmutableAttr(int, typeconv=False)
def __init__(self,parent_list,idx,nonce):
def __init__(self, parent_list, idx, nonce):
self.idx = idx
self.nonce = nonce
self.parent_list = parent_list
self.cfg = parent_list.parent_seed.cfg
SeedBase.__init__( self, self.cfg, self.make_base_seed_bin() )
SeedBase.__init__(self, self.cfg, self.make_base_seed_bin())
self.derived_seed = SeedBase(
self.cfg,
self.make_derived_seed_bin( parent_list.id_str, parent_list.count )
self.make_derived_seed_bin(parent_list.id_str, parent_list.count)
)
@property
def fn_stem(self):
return '{}-MASTER{}[{}]'.format(
self.parent_list.parent_seed.sid,
self.idx,
self.sid )
return '{}-MASTER{}[{}]'.format(self.parent_list.parent_seed.sid, self.idx, self.sid)
def make_base_seed_bin(self):
seed = self.parent_list.parent_seed
# field maximums: idx: 65535 (1024)
scramble_key = b'master_share:' + self.idx.to_bytes(2,'big') + self.nonce.to_bytes(2,'big')
return Crypto(self.cfg).scramble_seed(seed.data,scramble_key)[:seed.byte_len]
scramble_key = b'master_share:' + self.idx.to_bytes(2, 'big') + self.nonce.to_bytes(2, 'big')
return Crypto(self.cfg).scramble_seed(seed.data, scramble_key)[:seed.byte_len]
# Don't bother with avoiding seed ID collision here, as sid of derived seed is not used
# by user as an identifier
def make_derived_seed_bin(self,id_str,count):
def make_derived_seed_bin(self, id_str, count):
# field maximums: id_str: none (256 chars), count: 65535 (1024)
scramble_key = id_str.encode() + b':' + count.to_bytes(2,'big')
return Crypto(self.cfg).scramble_seed(self.data,scramble_key)[:self.byte_len]
scramble_key = id_str.encode() + b':' + count.to_bytes(2, 'big')
return Crypto(self.cfg).scramble_seed(self.data, scramble_key)[:self.byte_len]
def get_desc(self,ui=False):
def get_desc(self, ui=False):
psid = self.parent_list.parent_seed.sid
mss = f'master share #{self.idx} of '
return yellow('(' + mss) + psid.hl() + yellow(')') if ui else mss + psid
@ -287,33 +284,33 @@ class SeedShareMasterJoining(SeedShareMaster):
id_str = ImmutableAttr(SeedSplitIDString)
count = ImmutableAttr(SeedShareCount)
def __init__(self,cfg,idx,base_seed,id_str,count):
def __init__(self, cfg, idx, base_seed, id_str, count):
SeedBase.__init__( self, cfg, seed_bin=base_seed.data )
SeedBase.__init__(self, cfg, seed_bin=base_seed.data)
self.cfg = cfg
self.id_str = id_str or 'default'
self.count = count
self.derived_seed = SeedBase( cfg, self.make_derived_seed_bin(self.id_str,self.count) )
self.derived_seed = SeedBase(cfg, self.make_derived_seed_bin(self.id_str, self.count))
def join_shares(
cfg,
seed_list,
master_idx = None,
id_str = None ):
id_str = None):
if not hasattr(seed_list,'__next__'): # seed_list can be iterator or iterable
if not hasattr(seed_list, '__next__'): # seed_list can be iterator or iterable
seed_list = iter(seed_list)
class d:
byte_len,ret,count = None,0,0
byte_len, ret, count = None, 0, 0
def add_share(ss):
if d.byte_len:
assert ss.byte_len == d.byte_len, f'Seed length mismatch! {ss.byte_len} != {d.byte_len}'
else:
d.byte_len = ss.byte_len
d.ret ^= int(ss.data.hex(),16)
d.ret ^= int(ss.data.hex(), 16)
d.count += 1
if master_idx:
@ -323,8 +320,8 @@ def join_shares(
add_share(ss)
if master_idx:
add_share(SeedShareMasterJoining( cfg, master_idx, master_share, id_str, d.count+1 ).derived_seed)
add_share(SeedShareMasterJoining(cfg, master_idx, master_share, id_str, d.count+1).derived_seed)
SeedShareCount(d.count) # check that d.count is in valid range
return Seed( cfg, seed_bin=d.ret.to_bytes(d.byte_len,'big') )
return Seed(cfg, seed_bin=d.ret.to_bytes(d.byte_len, 'big'))

View file

@ -27,7 +27,7 @@ sha2: A non-optimized but very compact implementation of the SHA2 hash
# implementation must not be used for anything but testing and study. Test with
# the test/hashfunc.py script in the MMGen repository.
from struct import pack,unpack
from struct import pack, unpack
class Sha2:
'Implementation based on the pseudocode at https://en.wikipedia.org/wiki/SHA-2'
@ -40,7 +40,7 @@ class Sha2:
def nextPrime(n=2):
while True:
for factor in range(2,int(sqrt(n))+1):
for factor in range(2, int(sqrt(n))+1):
if n % factor == 0:
break
else:
@ -56,9 +56,9 @@ class Sha2:
return int((n - int(n)) * fb_mul)
if cls.use_gmp:
from gmpy2 import context,set_context,sqrt,cbrt
from gmpy2 import context, set_context, sqrt, cbrt
# context() parameters are platform-dependent!
set_context(context(precision=75,round=1)) # OK for gmp 6.1.2 / gmpy 2.1.0
set_context(context(precision=75, round=1)) # OK for gmp 6.1.2 / gmpy 2.1.0
else:
cbrt = lambda n: pow(n, 1 / 3)
@ -68,9 +68,9 @@ class Sha2:
# First wordBits bits of the fractional parts of the cube roots of the first nRounds primes
cls.K = tuple(getFractionalBits(cbrt(n)) for n in primes)
def __init__(self,message,preprocess=True):
def __init__(self, message, preprocess=True):
'Use preprocess=False for Sha256Compress'
assert isinstance(message,(bytes,bytearray,list)),'message must be of type bytes, bytearray or list'
assert isinstance(message, (bytes, bytearray, list)), 'message must be of type bytes, bytearray or list'
if not self.K:
type(self).initConstants()
self.H = list(self.H_init)
@ -100,25 +100,25 @@ class Sha2:
def bytesToWords(self):
ws = self.wordSize
assert len(self.M) % ws == 0
self.M = tuple(unpack(self.word_fmt,self.M[i*ws:ws+(i*ws)])[0] for i in range(len(self.M) // ws))
self.M = tuple(unpack(self.word_fmt, self.M[i*ws:ws+(i*ws)])[0] for i in range(len(self.M) // ws))
def digest(self):
return b''.join((pack(self.word_fmt,w) for w in self.H))
return b''.join((pack(self.word_fmt, w) for w in self.H))
def hexdigest(self):
return self.digest().hex()
def compute(self):
for i in range(0,len(self.M),16):
for i in range(0, len(self.M), 16):
self.processBlock(i)
def processBlock(self,offset):
def processBlock(self, offset):
'Process a blkSize-byte chunk of the message'
def rrotate(a,b):
def rrotate(a, b):
return ((a << self.wordBits-b) & self.wordMask) | (a >> b)
def addm(a,b):
def addm(a, b):
return (a + b) & self.wordMask
# Copy chunk into first 16 words of message schedule array
@ -126,46 +126,46 @@ class Sha2:
self.W[i] = self.M[offset + i]
# Extend the first 16 words into the remaining nRounds words of message schedule array
for i in range(16,self.nRounds):
for i in range(16, self.nRounds):
g0 = self.W[i-15]
gamma0 = rrotate(g0,self.g0r1) ^ rrotate(g0,self.g0r2) ^ (g0 >> self.g0r3)
gamma0 = rrotate(g0, self.g0r1) ^ rrotate(g0, self.g0r2) ^ (g0 >> self.g0r3)
g1 = self.W[i-2]
gamma1 = rrotate(g1,self.g1r1) ^ rrotate(g1,self.g1r2) ^ (g1 >> self.g1r3)
self.W[i] = addm(addm(addm(gamma0,self.W[i-7]),gamma1),self.W[i-16])
gamma1 = rrotate(g1, self.g1r1) ^ rrotate(g1, self.g1r2) ^ (g1 >> self.g1r3)
self.W[i] = addm(addm(addm(gamma0, self.W[i-7]), gamma1), self.W[i-16])
# Initialize working variables from current hash state
a,b,c,d,e,f,g,h = self.H
a, b, c, d, e, f, g, h = self.H
# Compression function main loop
for i in range(self.nRounds):
ch = (e & f) ^ (~e & g)
maj = (a & b) ^ (a & c) ^ (b & c)
sigma0 = rrotate(a,self.s0r1) ^ rrotate(a,self.s0r2) ^ rrotate(a,self.s0r3)
sigma1 = rrotate(e,self.s1r1) ^ rrotate(e,self.s1r2) ^ rrotate(e,self.s1r3)
sigma0 = rrotate(a, self.s0r1) ^ rrotate(a, self.s0r2) ^ rrotate(a, self.s0r3)
sigma1 = rrotate(e, self.s1r1) ^ rrotate(e, self.s1r2) ^ rrotate(e, self.s1r3)
t1 = addm(addm(addm(addm(h,sigma1),ch),self.K[i]),self.W[i])
t2 = addm(sigma0,maj)
t1 = addm(addm(addm(addm(h, sigma1), ch), self.K[i]), self.W[i])
t2 = addm(sigma0, maj)
h = g
g = f
f = e
e = addm(d,t1)
e = addm(d, t1)
d = c
c = b
b = a
a = addm(t1,t2)
a = addm(t1, t2)
# Save hash state
for n,v in enumerate((a,b,c,d,e,f,g,h)):
self.H[n] = addm(self.H[n],v)
for n, v in enumerate((a, b, c, d, e, f, g, h)):
self.H[n] = addm(self.H[n], v)
class Sha256(Sha2):
use_gmp = False
g0r1,g0r2,g0r3 = (7,18,3)
g1r1,g1r2,g1r3 = (17,19,10)
s0r1,s0r2,s0r3 = (2,13,22)
s1r1,s1r2,s1r3 = (6,11,25)
g0r1, g0r2, g0r3 = (7, 18, 3)
g1r1, g1r2, g1r3 = (17, 19, 10)
s0r1, s0r2, s0r3 = (2, 13, 22)
s1r1, s1r2, s1r3 = (6, 11, 25)
blkSize = 64
nRounds = 64
wordSize = 4
@ -174,7 +174,7 @@ class Sha256(Sha2):
word_fmt = '>I'
def pack_msglen(self):
return pack('>Q',len(self.M)*8)
return pack('>Q', len(self.M)*8)
class Sha512(Sha2):
"""
@ -190,10 +190,10 @@ class Sha512(Sha2):
- the shift and rotate amounts used are different.
"""
use_gmp = True
g0r1,g0r2,g0r3 = (1,8,7)
g1r1,g1r2,g1r3 = (19,61,6)
s0r1,s0r2,s0r3 = (28,34,39)
s1r1,s1r2,s1r3 = (14,18,41)
g0r1, g0r2, g0r3 = (1, 8, 7)
g1r1, g1r2, g1r3 = (19, 61, 6)
s0r1, s0r2, s0r3 = (28, 34, 39)
s1r1, s1r2, s1r3 = (14, 18, 41)
blkSize = 128
nRounds = 80
wordSize = 8

View file

@ -21,64 +21,64 @@ subseed: Subseed classes and methods for the MMGen suite
"""
from .color import green
from .util import msg_r,msg,die,make_chksum_8
from .objmethods import MMGenObject,HiliteStr,InitErrors
from .obj import MMGenRange,IndexedDict,ImmutableAttr
from .seed import SeedBase,SeedID
from .util import msg_r, msg, die, make_chksum_8
from .objmethods import MMGenObject, HiliteStr, InitErrors
from .obj import MMGenRange, IndexedDict, ImmutableAttr
from .seed import SeedBase, SeedID
class SubSeedIdxRange(MMGenRange):
min_idx = 1
max_idx = 1000000
class SubSeedIdx(HiliteStr,InitErrors):
class SubSeedIdx(HiliteStr, InitErrors):
color = 'red'
trunc_ok = False
def __new__(cls,s):
if isinstance(s,cls):
def __new__(cls, s):
if isinstance(s, cls):
return s
try:
assert isinstance(s,str),'not a string or string subclass'
assert isinstance(s, str), 'not a string or string subclass'
idx = s[:-1] if s[-1] in 'SsLl' else s
from .util import is_int
assert is_int(idx),"valid format: an integer, plus optional letter 'S','s','L' or 'l'"
assert is_int(idx), "valid format: an integer, plus optional letter 'S', 's', 'L' or 'l'"
idx = int(idx)
assert idx >= SubSeedIdxRange.min_idx, f'subseed index < {SubSeedIdxRange.min_idx:,}'
assert idx <= SubSeedIdxRange.max_idx, f'subseed index > {SubSeedIdxRange.max_idx:,}'
sstype,ltr = ('short','S') if s[-1] in 'Ss' else ('long','L')
me = str.__new__(cls,str(idx)+ltr)
sstype, ltr = ('short', 'S') if s[-1] in 'Ss' else ('long', 'L')
me = str.__new__(cls, str(idx)+ltr)
me.idx = idx
me.type = sstype
return me
except Exception as e:
return cls.init_fail(e,s)
return cls.init_fail(e, s)
class SubSeed(SeedBase):
idx = ImmutableAttr(int,typeconv=False)
nonce = ImmutableAttr(int,typeconv=False)
idx = ImmutableAttr(int, typeconv=False)
nonce = ImmutableAttr(int, typeconv=False)
ss_idx = ImmutableAttr(SubSeedIdx)
max_nonce = 1000
def __init__(self,parent_list,idx,nonce,length):
def __init__(self, parent_list, idx, nonce, length):
self.idx = idx
self.nonce = nonce
self.ss_idx = str(idx) + { 'long': 'L', 'short': 'S' }[length]
self.ss_idx = str(idx) + {'long': 'L', 'short': 'S'}[length]
self.parent_list = parent_list
SeedBase.__init__(
self,
parent_list.parent_seed.cfg,
seed_bin=self.make_subseed_bin( parent_list, idx, nonce, length ))
seed_bin=self.make_subseed_bin(parent_list, idx, nonce, length))
@staticmethod
def make_subseed_bin(parent_list,idx:int,nonce:int,length:str):
def make_subseed_bin(parent_list, idx:int, nonce:int, length:str):
seed = parent_list.parent_seed
short = { 'short': True, 'long': False }[length]
short = {'short': True, 'long': False}[length]
# field maximums: idx: 4294967295 (1000000), nonce: 65535 (1000), short: 255 (1)
scramble_key = idx.to_bytes(4,'big') + nonce.to_bytes(2,'big') + short.to_bytes(1,'big')
scramble_key = idx.to_bytes(4, 'big') + nonce.to_bytes(2, 'big') + short.to_bytes(1, 'big')
from .crypto import Crypto
return Crypto(parent_list.parent_seed.cfg).scramble_seed(
seed.data,scramble_key)[:16 if short else seed.byte_len]
seed.data, scramble_key)[:16 if short else seed.byte_len]
class SubSeedList(MMGenObject):
have_short = True
@ -86,16 +86,16 @@ class SubSeedList(MMGenObject):
debug_last_share_sid_len = 3
dfl_len = 100
def __init__(self,parent_seed,length=None):
def __init__(self, parent_seed, length=None):
self.member_type = SubSeed
self.parent_seed = parent_seed
self.data = { 'long': IndexedDict(), 'short': IndexedDict() }
self.data = {'long': IndexedDict(), 'short': IndexedDict()}
self.len = length or self.dfl_len
def __len__(self):
return len(self.data['long'])
def get_subseed_by_ss_idx(self,ss_idx_in,print_msg=False):
def get_subseed_by_ss_idx(self, ss_idx_in, print_msg=False):
ss_idx = SubSeedIdx(ss_idx_in)
if print_msg:
msg_r('{} {} of {}...'.format(
@ -108,27 +108,27 @@ class SubSeedList(MMGenObject):
self._generate(ss_idx.idx)
sid = self.data[ss_idx.type].key(ss_idx.idx-1)
idx,nonce = self.data[ss_idx.type][sid]
idx, nonce = self.data[ss_idx.type][sid]
if idx != ss_idx.idx:
die(3, "{} != {}: self.data[{t!r}].key(i) does not match self.data[{t!r}][i]!".format(
idx,
ss_idx.idx,
t = ss_idx.type ))
t = ss_idx.type))
if print_msg:
msg(f'\b\b\b => {SeedID.hlc(sid)}')
seed = self.member_type(self,idx,nonce,length=ss_idx.type)
seed = self.member_type(self, idx, nonce, length=ss_idx.type)
assert seed.sid == sid, f'{seed.sid} != {sid}: Seed ID mismatch!'
return seed
def get_subseed_by_seed_id(self,sid,last_idx=None,print_msg=False):
def get_subseed_by_seed_id(self, sid, last_idx=None, print_msg=False):
def get_existing_subseed_by_seed_id(sid):
for k in ('long','short') if self.have_short else ('long',):
for k in ('long', 'short') if self.have_short else ('long',):
if sid in self.data[k]:
idx,nonce = self.data[k][sid]
return self.member_type(self,idx,nonce,length=k)
idx, nonce = self.data[k][sid]
return self.member_type(self, idx, nonce, length=k)
def do_msg(subseed):
if print_msg:
@ -150,14 +150,14 @@ class SubSeedList(MMGenObject):
if len(self) >= last_idx:
return None
self._generate(last_idx,last_sid=sid)
self._generate(last_idx, last_sid=sid)
subseed = get_existing_subseed_by_seed_id(sid)
if subseed:
do_msg(subseed)
return subseed
def _collision_debug_msg(self,sid,idx,nonce,nonce_desc='nonce',debug_last_share=False):
def _collision_debug_msg(self, sid, idx, nonce, nonce_desc='nonce', debug_last_share=False):
slen = 'short' if sid in self.data['short'] else 'long'
m1 = f'add_subseed(idx={idx},{slen}):'
if sid == self.parent_seed.sid:
@ -172,7 +172,7 @@ class SubSeedList(MMGenObject):
m2 = f'collision with ID {sid} (idx={colliding_idx},{slen}),'
msg(f'{m1:30} {m2:46} incrementing {nonce_desc} to {nonce+1}')
def _generate(self,last_idx=None,last_sid=None):
def _generate(self, last_idx=None, last_sid=None):
if last_idx is None:
last_idx = self.len
@ -185,27 +185,27 @@ class SubSeedList(MMGenObject):
if last_sid is not None:
last_sid = SeedID(sid=last_sid)
def add_subseed(idx,length):
for nonce in range(self.nonce_start,self.member_type.max_nonce+1): # handle SeedID collisions
sid = make_chksum_8(self.member_type.make_subseed_bin(self,idx,nonce,length))
def add_subseed(idx, length):
for nonce in range(self.nonce_start, self.member_type.max_nonce+1): # handle SeedID collisions
sid = make_chksum_8(self.member_type.make_subseed_bin(self, idx, nonce, length))
if sid in self.data['long'] or sid in self.data['short'] or sid == self.parent_seed.sid:
if self.parent_seed.cfg.debug_subseed: # should get ≈450 collisions for first 1,000,000 subseeds
self._collision_debug_msg(sid,idx,nonce)
self._collision_debug_msg(sid, idx, nonce)
else:
self.data[length][sid] = (idx,nonce)
self.data[length][sid] = (idx, nonce)
return last_sid == sid
# must exit here, as this could leave self.data in inconsistent state
die('SubSeedNonceRangeExceeded', 'add_subseed(): nonce range exceeded')
for idx in SubSeedIdxRange(first_idx,last_idx).iterate():
match1 = add_subseed(idx,'long')
match2 = add_subseed(idx,'short') if self.have_short else False
for idx in SubSeedIdxRange(first_idx, last_idx).iterate():
match1 = add_subseed(idx, 'long')
match2 = add_subseed(idx, 'short') if self.have_short else False
if match1 or match2:
break
def format(self,first_idx,last_idx):
def format(self, first_idx, last_idx):
r = SubSeedIdxRange(first_idx,last_idx)
r = SubSeedIdxRange(first_idx, last_idx)
if len(self) < last_idx:
self._generate(last_idx)
@ -214,11 +214,11 @@ class SubSeedList(MMGenObject):
fs2 = '{i:>7}L: {:8} {i:>7}S: {:8}\n'
hdr = f' Parent Seed: {self.parent_seed.sid.hl()} ({self.parent_seed.bitlen} bits)\n\n'
hdr += fs1.format('Long Subseeds','Short Subseeds')
hdr += fs1.format('-------------','--------------')
hdr += fs1.format('Long Subseeds', 'Short Subseeds')
hdr += fs1.format('-------------', '--------------')
sl = self.data['long'].keys
ss = self.data['short'].keys
body = (fs2.format( sl[n-1], ss[n-1], i=n ) for n in r.iterate())
body = (fs2.format(sl[n-1], ss[n-1], i=n) for n in r.iterate())
return hdr + ''.join(body)

View file

@ -22,26 +22,26 @@ term: Terminal classes for the MMGen suite
# TODO: reimplement as instance instead of class
import sys,os,time
import sys, os, time
from collections import namedtuple
from .util import msg,msg_r,die
from .util import msg, msg_r, die
if sys.platform in ('linux', 'darwin'):
import tty,termios
import tty, termios
from select import select
hold_protect_timeout = 2 if sys.platform == 'darwin' else 0.3
elif sys.platform == 'win32':
try:
import msvcrt
except:
die(2,'Unable to set terminal mode')
die(2, 'Unable to set terminal mode')
if not sys.stdin.isatty():
msvcrt.setmode(sys.stdin.fileno(),os.O_BINARY)
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
else:
die(2,f'{sys.platform!r}: unsupported platform')
die(2, f'{sys.platform!r}: unsupported platform')
_term_dimensions = namedtuple('terminal_dimensions',['width','height'])
_term_dimensions = namedtuple('terminal_dimensions', ['width', 'height'])
class MMGenTerm:
@ -50,11 +50,11 @@ class MMGenTerm:
pass
@classmethod
def init(cls,noecho=False):
def init(cls, noecho=False):
pass
@classmethod
def set(cls,*args,**kwargs):
def set(cls, *args, **kwargs):
pass
@classmethod
@ -69,34 +69,34 @@ class MMGenTermLinux(MMGenTerm):
@classmethod
def register_cleanup(cls):
if not hasattr(cls,'cleanup_registered'):
if not hasattr(cls, 'cleanup_registered'):
import atexit
atexit.register(
lambda: termios.tcsetattr(
cls.stdin_fd,
termios.TCSADRAIN,
cls.orig_term) )
cls.orig_term))
cls.cleanup_registered = True
@classmethod
def reset(cls):
termios.tcsetattr( cls.stdin_fd, termios.TCSANOW, cls.orig_term )
termios.tcsetattr(cls.stdin_fd, termios.TCSANOW, cls.orig_term)
cls.cur_term = cls.orig_term
@classmethod
def set(cls,setting):
def set(cls, setting):
d = {
'echo': lambda t: t[:3] + [t[3] | (termios.ECHO | termios.ECHONL)] + t[4:], # echo input chars
'noecho': lambda t: t[:3] + [t[3] & ~(termios.ECHO | termios.ECHONL)] + t[4:], # don’t echo input chars
}
termios.tcsetattr( cls.stdin_fd, termios.TCSANOW, d[setting](cls.cur_term) )
termios.tcsetattr(cls.stdin_fd, termios.TCSANOW, d[setting](cls.cur_term))
cls.cur_term = termios.tcgetattr(cls.stdin_fd)
@classmethod
def init(cls,noecho=False):
def init(cls, noecho=False):
cls.stdin_fd = sys.stdin.fileno()
cls.cur_term = termios.tcgetattr(cls.stdin_fd)
if not hasattr(cls,'orig_term'):
if not hasattr(cls, 'orig_term'):
cls.orig_term = cls.cur_term
if noecho:
cls.set('noecho')
@ -109,9 +109,9 @@ class MMGenTermLinux(MMGenTerm):
try:
ret = (
int(os.environ['COLUMNS']),
int(os.environ['LINES']) )
int(os.environ['LINES']))
except:
ret = (80,25)
ret = (80, 25)
return _term_dimensions(*ret)
@classmethod
@ -128,7 +128,7 @@ class MMGenTermLinux(MMGenTerm):
break
@classmethod
def get_char(cls,prompt='',immed_chars='',prehold_protect=True,num_bytes=5):
def get_char(cls, prompt='', immed_chars='', prehold_protect=True, num_bytes=5):
"""
Use os.read(), not file.read(), to get a variable number of bytes without blocking.
Request 5 bytes to cover escape sequences generated by F1, F2, .. Fn keys (5 bytes)
@ -142,7 +142,7 @@ class MMGenTermLinux(MMGenTerm):
while True:
# Protect against held-down key before read()
key = select([sys.stdin], [], [], timeout)[0]
s = os.read(cls.stdin_fd,num_bytes).decode()
s = os.read(cls.stdin_fd, num_bytes).decode()
if prehold_protect and key:
continue
if s in immed_chars:
@ -155,10 +155,10 @@ class MMGenTermLinux(MMGenTerm):
return s
@classmethod
def get_char_raw(cls,prompt='',num_bytes=5,**kwargs):
def get_char_raw(cls, prompt='', num_bytes=5, **kwargs):
tty.setcbreak(cls.stdin_fd)
msg_r(prompt)
s = os.read(cls.stdin_fd,num_bytes).decode()
s = os.read(cls.stdin_fd, num_bytes).decode()
termios.tcsetattr(cls.stdin_fd, termios.TCSADRAIN, cls.cur_term)
return s
@ -169,11 +169,11 @@ class MMGenTermLinuxStub(MMGenTermLinux):
pass
@classmethod
def init(cls,noecho=False):
def init(cls, noecho=False):
cls.stdin_fd = sys.stdin.fileno()
@classmethod
def set(cls,*args,**kwargs):
def set(cls, *args, **kwargs):
pass
@classmethod
@ -181,9 +181,9 @@ class MMGenTermLinuxStub(MMGenTermLinux):
pass
@classmethod
def get_char(cls,prompt='',immed_chars='',prehold_protect=None,num_bytes=5):
def get_char(cls, prompt='', immed_chars='', prehold_protect=None, num_bytes=5):
msg_r(prompt)
return os.read(0,num_bytes).decode()
return os.read(0, num_bytes).decode()
get_char_raw = get_char
@ -196,26 +196,26 @@ class MMGenTermMSWin(MMGenTerm):
@classmethod
def get_terminal_size(cls):
import struct
x,y = 0,0
x, y = 0, 0
try:
from ctypes import windll,create_string_buffer
from ctypes import windll, create_string_buffer
# handles - stdin: -10, stdout: -11, stderr: -12
csbi = create_string_buffer(22)
h = windll.kernel32.GetStdHandle(-12)
res = windll.kernel32.GetConsoleScreenBufferInfo(h,csbi)
res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
assert res, 'failed to get console screen buffer info'
left,top,right,bottom = struct.unpack('hhhhHhhhhhh', csbi.raw)[5:9]
left, top, right, bottom = struct.unpack('hhhhHhhhhhh', csbi.raw)[5:9]
x = right - left + 1
y = bottom - top + 1
except:
pass
if x and y:
return _term_dimensions(x,y)
return _term_dimensions(x, y)
else:
from .color import yellow
msg(yellow('Warning: could not get terminal size. Using fallback dimensions.'))
return _term_dimensions(80,25)
return _term_dimensions(80, 25)
@classmethod
def kb_hold_protect(cls):
@ -230,7 +230,7 @@ class MMGenTermMSWin(MMGenTerm):
return
@classmethod
def get_char(cls,prompt='',immed_chars='',prehold_protect=True,num_bytes=None):
def get_char(cls, prompt='', immed_chars='', prehold_protect=True, num_bytes=None):
"""
always return a single character, ignore num_bytes
first character of 2-character sequence returned by F1-F12 keys is discarded
@ -253,13 +253,13 @@ class MMGenTermMSWin(MMGenTerm):
return ch
@classmethod
def get_char_raw(cls,prompt='',num_bytes=None,**kwargs):
def get_char_raw(cls, prompt='', num_bytes=None, **kwargs):
"""
return single ASCII char or 2-char escape sequence, ignoring num_bytes
"""
msg_r(prompt)
ret = msvcrt.getch()
if ret in (b'\x00',b'\xe0'): # first byte of 2-byte escape sequence
if ret in (b'\x00', b'\xe0'): # first byte of 2-byte escape sequence
return chr(ret[0]) + chr(msvcrt.getch()[0])
if ret == b'\x03':
raise KeyboardInterrupt
@ -268,7 +268,7 @@ class MMGenTermMSWin(MMGenTerm):
class MMGenTermMSWinStub(MMGenTermMSWin):
@classmethod
def get_char(cls,prompt='',immed_chars='',prehold_protect=None,num_bytes=None):
def get_char(cls, prompt='', immed_chars='', prehold_protect=None, num_bytes=None):
"""
Use stdin to allow UTF-8 and emulate the one-character behavior of MMGenTermMSWin
"""
@ -289,7 +289,7 @@ def get_term():
'win32': (MMGenTermMSWin if sys.stdin.isatty() else MMGenTermMSWinStub),
}[sys.platform]
def init_term(cfg,noecho=False):
def init_term(cfg, noecho=False):
term = get_term()

View file

@ -12,11 +12,11 @@
ui: Interactive user interface functions for the MMGen suite
"""
import sys,os
import sys, os
from .util import msg,msg_r,Msg,die
from .util import msg, msg_r, Msg, die
def confirm_or_raise(cfg,message,action,expect='YES',exit_msg='Exiting at user request'):
def confirm_or_raise(cfg, message, action, expect='YES', exit_msg='Exiting at user request'):
if message:
msg(message)
if line_input(
@ -24,27 +24,27 @@ def confirm_or_raise(cfg,message,action,expect='YES',exit_msg='Exiting at user r
(f'{action} ' if action[0].isupper() else f'Are you sure you want to {action}?\n') +
f'Type uppercase {expect!r} to confirm: '
).strip() != expect:
die( 'UserNonConfirmation', exit_msg )
die('UserNonConfirmation', exit_msg)
def get_words_from_user(cfg,prompt):
words = line_input( cfg, prompt, echo=cfg.echo_passphrase ).split()
def get_words_from_user(cfg, prompt):
words = line_input(cfg, prompt, echo=cfg.echo_passphrase).split()
if cfg.debug:
msg('Sanitized input: [{}]'.format(' '.join(words)))
return words
def get_data_from_user(cfg,desc='data'): # user input MUST be UTF-8
data = line_input( cfg, f'Enter {desc}: ', echo=cfg.echo_passphrase )
def get_data_from_user(cfg, desc='data'): # user input MUST be UTF-8
data = line_input(cfg, f'Enter {desc}: ', echo=cfg.echo_passphrase)
if cfg.debug:
msg(f'User input: [{data}]')
return data
def line_input(cfg,prompt,echo=True,insert_txt='',hold_protect=True):
def line_input(cfg, prompt, echo=True, insert_txt='', hold_protect=True):
"""
multi-line prompts OK
one-line prompts must begin at beginning of line
empty prompts forbidden due to interactions with readline
"""
assert prompt,'calling line_input() with an empty prompt forbidden'
assert prompt, 'calling line_input() with an empty prompt forbidden'
def get_readline():
try:
@ -60,7 +60,7 @@ def line_input(cfg,prompt,echo=True,insert_txt='',hold_protect=True):
if cfg.test_suite_popen_spawn:
msg(prompt)
sys.stderr.flush() # required by older Pythons (e.g. v3.7)
reply = os.read(0,4096).decode().rstrip('\n') # strip NL to mimic behavior of input()
reply = os.read(0, 4096).decode().rstrip('\n') # strip NL to mimic behavior of input()
elif not sys.stdin.isatty():
msg_r(prompt)
reply = input('')
@ -86,10 +86,10 @@ def keypress_confirm(
default_yes = False,
verbose = False,
no_nl = False,
complete_prompt = False ):
complete_prompt = False):
if not complete_prompt:
prompt = '{} {}: '.format( prompt, '(Y/n)' if default_yes else '(y/N)' )
prompt = '{} {}: '.format(prompt, '(Y/n)' if default_yes else '(y/N)')
nl = f'\r{" "*len(prompt)}\r' if no_nl else '\n'
@ -99,7 +99,7 @@ def keypress_confirm(
from .term import get_char
while True:
reply = get_char(prompt,immed_chars='yYnN').strip('\n\r')
reply = get_char(prompt, immed_chars='yYnN').strip('\n\r')
if not reply:
msg_r(nl)
return default_yes
@ -111,7 +111,7 @@ def keypress_confirm(
def do_pager(text):
pagers = ['less','more']
pagers = ['less', 'more']
end_msg = '\n(end of text)\n\n'
os.environ['LESS'] = '--jump-target=2 --shift=4 --tabs=4 --RAW-CONTROL-CHARS --chop-long-lines'
@ -123,7 +123,7 @@ def do_pager(text):
for pager in pagers:
try:
m = text + ('' if pager == 'less' else end_msg)
run([pager],input=m.encode(),check=True)
run([pager], input=m.encode(), check=True)
msg_r('\r')
except:
pass
@ -133,7 +133,7 @@ def do_pager(text):
Msg(text+end_msg)
set_vt100()
def do_license_msg(cfg,immed=False):
def do_license_msg(cfg, immed=False):
if cfg.quiet or cfg.no_license or cfg.yes or not cfg.stdin_tty:
return
@ -145,7 +145,7 @@ def do_license_msg(cfg,immed=False):
from .term import get_char
prompt = "Press 'w' for conditions and warranty info, or 'c' to continue: "
while True:
reply = get_char(prompt, immed_chars=('','wc')[bool(immed)])
reply = get_char(prompt, immed_chars=('', 'wc')[bool(immed)])
if reply == 'w':
do_pager(gpl.conditions)
elif reply == 'c':

View file

@ -20,9 +20,9 @@
util: Frequently-used variables, classes and utility functions for the MMGen suite
"""
import sys,os,time,re
import sys, os, time, re
from .color import red,yellow,green,blue,purple
from .color import red, yellow, green, blue, purple
from .cfg import gv
ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
@ -32,12 +32,12 @@ hexdigits = '0123456789abcdefABCDEF'
hexdigits_uc = '0123456789ABCDEF'
hexdigits_lc = '0123456789abcdef'
def noop(*args,**kwargs):
def noop(*args, **kwargs):
pass
class Util:
def __init__(self,cfg):
def __init__(self, cfg):
self.cfg = cfg
@ -75,9 +75,9 @@ class Util:
if not chk1 == chk2:
fs = "{} ERROR: {} checksum ({}) doesn't match {} checksum ({})"
m = fs.format((hdr+':\n ' if hdr else 'CHECKSUM'),desc2,chk2,desc1,chk1)
m = fs.format((hdr+':\n ' if hdr else 'CHECKSUM'), desc2, chk2, desc1, chk1)
if die_on_fail:
die(3,m)
die(3, m)
else:
if verbose or self.cfg.verbose:
msg(m)
@ -90,7 +90,7 @@ class Util:
def compare_or_die(self, val1, desc1, val2, desc2, e='Error'):
if val1 != val2:
die(3,f"{e}: {desc2} ({val2}) doesn't match {desc1} ({val1})")
die(3, f"{e}: {desc2} ({val2}) doesn't match {desc1} ({val1})")
if self.cfg.debug:
msg(f'{capfirst(desc2)} OK ({val2})')
return True
@ -101,7 +101,7 @@ if sys.platform == 'win32':
gv.stderr.write(s)
gv.stderr.flush()
except:
os.write(2,s.encode())
os.write(2, s.encode())
def msg(s):
msg_r(s + '\n')
@ -111,7 +111,7 @@ if sys.platform == 'win32':
gv.stdout.write(s)
gv.stdout.flush()
except:
os.write(1,s.encode())
os.write(1, s.encode())
def Msg(s):
Msg_r(s + '\n')
@ -156,73 +156,73 @@ def mdie(*args):
mmsg(*args)
sys.exit(0)
def die(ev,s='',stdout=False):
if isinstance(ev,int):
from .exception import MMGenSystemExit,MMGenError
def die(ev, s='', stdout=False):
if isinstance(ev, int):
from .exception import MMGenSystemExit, MMGenError
if ev <= 2:
raise MMGenSystemExit(ev,s,stdout)
raise MMGenSystemExit(ev, s, stdout)
else:
raise MMGenError(ev,s,stdout)
elif isinstance(ev,str):
raise MMGenError(ev, s, stdout)
elif isinstance(ev, str):
from . import exception
raise getattr(exception,ev)(s)
raise getattr(exception, ev)(s)
else:
raise ValueError(f'{ev}: exit value must be string or int instance')
def Die(ev=0,s=''):
die(ev=ev,s=s,stdout=True)
def Die(ev=0, s=''):
die(ev=ev, s=s, stdout=True)
def pp_fmt(d):
import pprint
return pprint.PrettyPrinter(indent=4,compact=False).pformat(d)
return pprint.PrettyPrinter(indent=4, compact=False).pformat(d)
def pp_msg(d):
msg(pp_fmt(d))
def indent(s,indent=' ',append='\n'):
def indent(s, indent=' ', append='\n'):
"indent multiple lines of text with specified string"
return indent + ('\n'+indent).join(s.strip().splitlines()) + append
def fmt(s,indent='',strip_char=None,append='\n'):
def fmt(s, indent='', strip_char=None, append='\n'):
"de-indent multiple lines of text, or indent with specified string"
return indent + ('\n'+indent).join([l.lstrip(strip_char) for l in s.strip().splitlines()]) + append
def fmt_list(iterable,fmt='dfl',indent='',conv=None):
def fmt_list(iterable, fmt='dfl', indent='', conv=None):
"pretty-format a list"
_conv,sep,lq,rq = {
'dfl': ( str, ", ", "'", "'"),
'utf8': ( str, ", ", "", ""),
'bare': ( repr, " ", "", ""),
'barest': ( str, " ", "", ""),
'fancy': ( str, " ", "", ""),
'no_quotes': ( str, ", ", "", ""),
'compact': ( str, ",", "", ""),
'no_spc': ( str, ",", "'", "'"),
'min': ( str, ",", "", ""),
'repr': ( repr, ", ", "", ""),
'csv': ( repr, ",", "", ""),
'col': ( str, "\n", "", ""),
_conv, sep, lq, rq = {
'dfl': (str, ", ", "'", "'"),
'utf8': (str, ", ", "", ""),
'bare': (repr, " ", "", ""),
'barest': (str, " ", "", ""),
'fancy': (str, " ", "", ""),
'no_quotes': (str, ", ", "", ""),
'compact': (str, ",", "", ""),
'no_spc': (str, ",", "'", "'"),
'min': (str, ",", "", ""),
'repr': (repr, ", ", "", ""),
'csv': (repr, ",", "", ""),
'col': (str, "\n", "", ""),
}[fmt]
conv = conv or _conv
return indent + (sep+indent).join(lq+conv(e)+rq for e in iterable)
def fmt_dict(mapping,fmt='dfl',kconv=None,vconv=None):
def fmt_dict(mapping, fmt='dfl', kconv=None, vconv=None):
"pretty-format a dict"
kc,vc,sep,fs = {
'dfl': ( str, str, ", ", "'{}' ({})" ),
'dfl_compact': ( str, str, " ", "{} ({})" ),
'square': ( str, str, ", ", "'{}' [{}]" ),
'square_compact':( str, str, " ", "{} [{}]" ),
'equal': ( str, str, ", ", "'{}'={}" ),
'equal_spaced': ( str, str, ", ", "'{}' = {}" ),
'equal_compact': ( str, str, " ", "{}={}" ),
'kwargs': ( str, repr, ", ", "{}={}" ),
'colon': ( str, repr, ", ", "{}:{}" ),
'colon_compact': ( str, str, " ", "{}:{}" ),
kc, vc, sep, fs = {
'dfl': (str, str, ", ", "'{}' ({})"),
'dfl_compact': (str, str, " ", "{} ({})"),
'square': (str, str, ", ", "'{}' [{}]"),
'square_compact':(str, str, " ", "{} [{}]"),
'equal': (str, str, ", ", "'{}'={}"),
'equal_spaced': (str, str, ", ", "'{}' = {}"),
'equal_compact': (str, str, " ", "{}={}"),
'kwargs': (str, repr, ", ", "{}={}"),
'colon': (str, repr, ", ", "{}:{}"),
'colon_compact': (str, str, " ", "{}:{}"),
}[fmt]
kconv = kconv or kc
vconv = vconv or vc
return sep.join(fs.format(kconv(k),vconv(v)) for k,v in mapping.items())
return sep.join(fs.format(kconv(k), vconv(v)) for k, v in mapping.items())
def list_gen(*data):
"""
@ -231,10 +231,10 @@ def list_gen(*data):
elements of the sublist are included in the result. Otherwise the sublist is skipped.
- If a sublist contains only one element, the condition defaults to true.
"""
assert type(data) in (list,tuple), f'{type(data).__name__} not in (list,tuple)'
assert type(data) in (list, tuple), f'{type(data).__name__} not in (list, tuple)'
def gen():
for d in data:
assert isinstance(d,list), f'{type(d).__name__} != list'
assert isinstance(d, list), f'{type(d).__name__} != list'
if len(d) == 1:
yield d[0]
elif d[-1]:
@ -242,7 +242,7 @@ def list_gen(*data):
yield d[idx]
return list(gen())
def remove_dups(iterable,edesc='element',desc='list',quiet=False,hide=False):
def remove_dups(iterable, edesc='element', desc='list', quiet=False, hide=False):
"""
Remove duplicate occurrences of iterable elements, preserving first occurrence
If iterable is a generator, return a list, else type(iterable)
@ -256,44 +256,44 @@ def remove_dups(iterable,edesc='element',desc='list',quiet=False,hide=False):
ret.append(e)
return ret if type(iterable).__name__ == 'generator' else type(iterable)(ret)
def contains_any(target_list,source_list):
return any(map(target_list.count,source_list))
def contains_any(target_list, source_list):
return any(map(target_list.count, source_list))
def suf(arg,suf_type='s',verb='none'):
def suf(arg, suf_type='s', verb='none'):
suf_types = {
'none': {
's': ('s', ''),
'es': ('es', ''),
'ies': ('ies','y'),
's': ('s', ''),
'es': ('es', ''),
'ies': ('ies', 'y'),
},
'is': {
's': ('s are', ' is'),
'es': ('es are', ' is'),
'ies': ('ies are','y is'),
's': ('s are', ' is'),
'es': ('es are', ' is'),
'ies': ('ies are', 'y is'),
},
'has': {
's': ('s have', ' has'),
'es': ('es have', ' has'),
'ies': ('ies have','y has'),
's': ('s have', ' has'),
'es': ('es have', ' has'),
'ies': ('ies have', 'y has'),
},
}
if isinstance(arg,int):
if isinstance(arg, int):
n = arg
elif isinstance(arg,(list,tuple,set,dict)):
elif isinstance(arg, (list, tuple, set, dict)):
n = len(arg)
else:
die(2,f'{arg}: invalid parameter for suf()')
die(2, f'{arg}: invalid parameter for suf()')
return suf_types[verb][suf_type][n == 1]
def get_extension(fn):
return os.path.splitext(fn)[1][1:]
def remove_extension(fn,ext):
a,b = os.path.splitext(fn)
def remove_extension(fn, ext):
a, b = os.path.splitext(fn)
return a if b[1:] == ext else fn
def make_chksum_N(s,nchars,sep=False,rounds=2,upper=True):
if isinstance(s,str):
def make_chksum_N(s, nchars, sep=False, rounds=2, upper=True):
if isinstance(s, str):
s = s.encode()
from hashlib import sha256
for i in range(rounds):
@ -301,28 +301,28 @@ def make_chksum_N(s,nchars,sep=False,rounds=2,upper=True):
ret = s.hex()[:nchars]
if sep:
assert 4 <= nchars <= 64 and (not nchars % 4), 'illegal ‘nchars’ value'
ret = ' '.join( ret[i:i+4] for i in range(0,nchars,4) )
ret = ' '.join(ret[i:i+4] for i in range(0, nchars, 4))
else:
assert 4 <= nchars <= 64, 'illegal ‘nchars’ value'
return ret.upper() if upper else ret
def make_chksum_8(s,sep=False):
def make_chksum_8(s, sep=False):
from .obj import HexStr
from hashlib import sha256
s = HexStr(sha256(sha256(s).digest()).hexdigest()[:8].upper(),case='upper')
return '{} {}'.format(s[:4],s[4:]) if sep else s
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):
from .obj import HexStr
from hashlib import sha256
if isinstance(s,str):
if isinstance(s, str):
s = s.encode()
return HexStr(sha256(s).hexdigest()[:6])
def is_chksum_6(s):
return len(s) == 6 and set(s) <= set(hexdigits_lc)
def split_into_cols(col_wid,s):
def split_into_cols(col_wid, s):
return ' '.join([s[col_wid*i:col_wid*(i+1)] for i in range(len(s)//col_wid+1)]).rstrip()
def capfirst(s): # different from str.capitalize() - doesn't downcase any uc in string
@ -332,20 +332,20 @@ def decode_timestamp(s):
# tz_save = open('/etc/timezone').read().rstrip()
os.environ['TZ'] = 'UTC'
# os.environ['TZ'] = tz_save
return int(time.mktime( time.strptime(s,'%Y%m%d_%H%M%S') ))
return int(time.mktime(time.strptime(s, '%Y%m%d_%H%M%S')))
def make_timestamp(secs=None):
return '{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}'.format(*time.gmtime(
int(secs) if secs is not None else time.time() )[:6])
int(secs) if secs is not None else time.time())[:6])
def make_timestr(secs=None):
return '{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}'.format(*time.gmtime(
int(secs) if secs is not None else time.time() )[:6])
int(secs) if secs is not None else time.time())[:6])
def secs_to_dhms(secs):
hrs = secs // 3600
return '{}{:02d}:{:02d}:{:02d} h/m/s'.format(
('{} day{}, '.format(hrs//24,suf(hrs//24)) if hrs > 24 else ''),
('{} day{}, '.format(hrs//24, suf(hrs//24)) if hrs > 24 else ''),
hrs % 24,
(secs // 60) % 60,
secs % 60
@ -360,9 +360,9 @@ def secs_to_ms(secs):
def is_int(s): # actually is_nonnegative_int()
return set(str(s)) <= set(digits)
def check_int_between(val,imin,imax,desc):
def check_int_between(val, imin, imax, desc):
if not imin <= int(val) <= imax:
die(1,f'{val}: invalid value for {desc} (must be between {imin} and {imax})')
die(1, f'{val}: invalid value for {desc} (must be between {imin} and {imax})')
return int(val)
def is_hex_str(s):
@ -379,36 +379,36 @@ def is_utf8(s):
else:
return True
def remove_whitespace(s,ws='\t\r\n '):
return s.translate(dict((ord(e),None) for e in ws))
def remove_whitespace(s, ws='\t\r\n '):
return s.translate(dict((ord(e), None) for e in ws))
def strip_comment(line):
return re.sub('#.*','',line).rstrip()
return re.sub('#.*', '', line).rstrip()
def strip_comments(lines):
pat = re.compile('#.*')
return [m for m in [pat.sub('',l).rstrip() for l in lines] if m != '']
return [m for m in [pat.sub('', l).rstrip() for l in lines] if m != '']
def make_full_path(outdir,outfile):
def make_full_path(outdir, outfile):
return os.path.normpath(os.path.join(outdir, os.path.basename(outfile)))
class oneshot_warning:
color = 'nocolor'
def __init__(self,div=None,fmt_args=[],reverse=False):
self.do(type(self),div,fmt_args,reverse)
def __init__(self, div=None, fmt_args=[], reverse=False):
self.do(type(self), div, fmt_args, reverse)
def do(self,wcls,div,fmt_args,reverse):
def do(self, wcls, div, fmt_args, reverse):
def do_warning():
from . import color
msg(getattr(color, getattr(wcls,'color'))('WARNING: ' + getattr(wcls,'message').format(*fmt_args)))
msg(getattr(color, getattr(wcls, 'color'))('WARNING: ' + getattr(wcls, 'message').format(*fmt_args)))
if not hasattr(wcls,'data'):
setattr(wcls,'data',[])
if not hasattr(wcls, 'data'):
setattr(wcls, 'data', [])
data = getattr(wcls,'data')
data = getattr(wcls, 'data')
condition = (div in data) if reverse else (not div in data)
if not div in data:
@ -422,10 +422,10 @@ class oneshot_warning:
class oneshot_warning_group(oneshot_warning):
def __init__(self,wcls,div=None,fmt_args=[],reverse=False):
self.do(getattr(self,wcls),div,fmt_args,reverse)
def __init__(self, wcls, div=None, fmt_args=[], reverse=False):
self.do(getattr(self, wcls), div, fmt_args, reverse)
def get_subclasses(cls,names=False):
def get_subclasses(cls, names=False):
def gen(cls):
for i in cls.__subclasses__():
yield i
@ -442,11 +442,11 @@ def wrap_ripemd160(called=[]):
import hashlib
hashlib.new('ripemd160')
except ValueError:
def hashlib_new_wrapper(name,*args,**kwargs):
def hashlib_new_wrapper(name, *args, **kwargs):
if name == 'ripemd160':
return ripemd160(*args,**kwargs)
return ripemd160(*args, **kwargs)
else:
return hashlib_new(name,*args,**kwargs)
return hashlib_new(name, *args, **kwargs)
from .contrib.ripemd160 import ripemd160
hashlib_new = hashlib.new
hashlib.new = hashlib_new_wrapper
@ -454,7 +454,7 @@ def wrap_ripemd160(called=[]):
def exit_if_mswin(feature):
if sys.platform == 'win32':
die(2, capfirst(feature) + ' not supported on the MSWin / MSYS2 platform' )
die(2, capfirst(feature) + ' not supported on the MSWin / MSYS2 platform')
def have_sudo(silent=False):
from subprocess import run, DEVNULL

View file

@ -12,19 +12,19 @@
util2: Less frequently-used variables, classes and utility functions for the MMGen suite
"""
import sys,re,time
from .util import msg,suf,hexdigits,die
import sys, re, time
from .util import msg, suf, hexdigits, die
def die_wait(delay,ev=0,s=''):
assert isinstance(delay,int)
assert isinstance(ev,int)
def die_wait(delay, ev=0, s=''):
assert isinstance(delay, int)
assert isinstance(ev, int)
if s:
msg(s)
time.sleep(delay)
sys.exit(ev)
def die_pause(ev=0,s=''):
assert isinstance(ev,int)
def die_pause(ev=0, s=''):
assert isinstance(ev, int)
if s:
msg(s)
input('Press ENTER to exit')
@ -39,12 +39,12 @@ def load_cryptodomex():
try:
import Crypto # cryptodome
except ImportError:
die(2,'Unable to import either the ‘pycryptodomex’ or ‘pycryptodome’ package')
die(2, 'Unable to import either the ‘pycryptodomex’ or ‘pycryptodome’ package')
else:
sys.modules['Cryptodome'] = Crypto
# called with no arguments by pyethereum.utils:
def get_keccak(cfg=None,cached_ret=[]):
def get_keccak(cfg=None, cached_ret=[]):
if not cached_ret:
if cfg and cfg.use_internal_keccak_module:
@ -61,7 +61,7 @@ def get_keccak(cfg=None,cached_ret=[]):
die('MMGenImportError',
'Please install the ‘pycryptodome’ or ‘pycryptodomex’ package on your system')
def keccak_256(data):
return keccak.new(data=data,digest_bytes=32)
return keccak.new(data=data, digest_bytes=32)
cached_ret.append(keccak_256)
return cached_ret[0]
@ -87,13 +87,13 @@ bytespec_map = (
('E', 1152921504606846976),
)
def int2bytespec(n,spec,fmt,print_sym=True,strip=False,add_space=False):
def int2bytespec(n, spec, fmt, print_sym=True, strip=False, add_space=False):
def spec2int(spec):
for k,v in bytespec_map:
for k, v in bytespec_map:
if k == spec:
return v
die(1,f'{spec!r}: unrecognized bytespec')
die(1, f'{spec!r}: unrecognized bytespec')
ret = f'{n/spec2int(spec):{fmt}f}'
if strip:
@ -101,17 +101,17 @@ def int2bytespec(n,spec,fmt,print_sym=True,strip=False,add_space=False):
return (
ret
+ ('0' if ret.endswith('.') else '')
+ ((' ' if add_space else '') + spec if print_sym else '') )
+ ((' ' if add_space else '') + spec if print_sym else ''))
else:
return (
ret
+ ((' ' if add_space else '') + spec if print_sym else '') )
+ ((' ' if add_space else '') + spec if print_sym else ''))
def parse_bytespec(nbytes):
m = re.match(r'([0123456789.]+)(.*)',nbytes)
m = re.match(r'([0123456789.]+)(.*)', nbytes)
if m:
if m.group(2):
for k,v in bytespec_map:
for k, v in bytespec_map:
if k == m.group(2):
from decimal import Decimal
return int(Decimal(m.group(1)) * v)
@ -121,9 +121,9 @@ def parse_bytespec(nbytes):
else:
return int(nbytes)
die(1,f'{nbytes!r}: invalid byte specifier')
die(1, f'{nbytes!r}: invalid byte specifier')
def format_elapsed_days_hr(t,now=None,cached={}):
def format_elapsed_days_hr(t, now=None, cached={}):
e = int((now or time.time()) - t)
if not e in cached:
days = abs(e) // 86400
@ -155,7 +155,7 @@ def format_elapsed_hr(t, now=None, cached={}, rel_now=True, show_secs=False):
cached[key] = ' '.join(f'{n} {desc}{suf(n)}' for desc, n in data if n) + add_suffix()
return cached[key]
def pretty_format(s,width=80,pfx=''):
def pretty_format(s, width=80, pfx=''):
out = []
while s:
if len(s) <= width:
@ -166,8 +166,8 @@ def pretty_format(s,width=80,pfx=''):
s = s[i+1:]
return pfx + ('\n'+pfx).join(out)
def block_format(data,gw=2,cols=8,line_nums=None,data_is_hex=False):
assert line_nums in (None,'hex','dec'),"'line_nums' must be one of None, 'hex' or 'dec'"
def block_format(data, gw=2, cols=8, line_nums=None, data_is_hex=False):
assert line_nums in (None, 'hex', 'dec'), "'line_nums' must be one of None, 'hex' or 'dec'"
ln_fs = '{:06x}: ' if line_nums == 'hex' else '{:06}: '
bytes_per_chunk = gw
if data_is_hex:
@ -180,12 +180,12 @@ def block_format(data,gw=2,cols=8,line_nums=None,data_is_hex=False):
for i in range(nchunks)
).rstrip() + '\n'
def pretty_hexdump(data,gw=2,cols=8,line_nums=None):
return block_format(data.hex(),gw,cols,line_nums,data_is_hex=True)
def pretty_hexdump(data, gw=2, cols=8, line_nums=None):
return block_format(data.hex(), gw, cols, line_nums, data_is_hex=True)
def decode_pretty_hexdump(data):
pat = re.compile(fr'^[{hexdigits}]+:\s+')
lines = [pat.sub('',line) for line in data.splitlines()]
lines = [pat.sub('', line) for line in data.splitlines()]
try:
return bytes.fromhex(''.join((''.join(lines).split())))
except:

View file

@ -24,17 +24,17 @@ from .baseconv import baseconv
from .util import die
def is_xmrseed(s):
return bool( xmrseed().tobytes(s.split()) )
return bool(xmrseed().tobytes(s.split()))
# implements a subset of the baseconv API
class xmrseed(baseconv):
desc = baseconv.dt('Monero mnemonic', 'Monero new-style mnemonic seed phrase')
wl_chksum = '3c381ebb'
seedlen_map = { 32:25 }
seedlen_map_rev = { 25:32 }
seedlen_map = {32: 25}
seedlen_map_rev = {25: 32}
def __init__(self,wl_id='xmrseed'):
def __init__(self, wl_id='xmrseed'):
assert wl_id == 'xmrseed', "initialize with 'xmrseed' for compatibility with baseconv API"
from .wordlist.monero import words
self.digits = words
@ -46,9 +46,9 @@ class xmrseed(baseconv):
wstr = ''.join(word[:3] for word in words)
return words[crc32(wstr.encode()) % len(words)]
def tobytes(self,words_arg,pad=None):
def tobytes(self, words_arg, pad=None):
assert isinstance(words_arg,(list,tuple)),'words must be list or tuple'
assert isinstance(words_arg, (list, tuple)), 'words must be list or tuple'
assert pad is None, f"{pad}: invalid 'pad' argument (must be None)"
words = words_arg
@ -58,26 +58,26 @@ class xmrseed(baseconv):
base = len(wl)
if not set(words) <= set(wl):
die( 'MnemonicError', f'{words!r}: not in {desc} format' )
die('MnemonicError', f'{words!r}: not in {desc} format')
if len(words) not in self.seedlen_map_rev:
die( 'MnemonicError', f'{len(words)}: invalid seed phrase length for {desc}' )
die('MnemonicError', f'{len(words)}: invalid seed phrase length for {desc}')
z = self.monero_mn_checksum(words[:-1])
if z != words[-1]:
die( 'MnemonicError', f'invalid {desc} checksum' )
die('MnemonicError', f'invalid {desc} checksum')
words = tuple(words[:-1])
def gen():
for i in range(len(words)//3):
w1,w2,w3 = [wl.index(w) for w in words[3*i:3*i+3]]
w1, w2, w3 = [wl.index(w) for w in words[3*i:3*i+3]]
x = w1 + base*((w2-w1)%base) + base*base*((w3-w2)%base)
yield x.to_bytes(4,'big')[::-1]
yield x.to_bytes(4, 'big')[::-1]
return b''.join(gen())
def frombytes(self,bytestr,pad=None,tostr=False):
def frombytes(self, bytestr, pad=None, tostr=False):
assert pad is None, f"{pad}: invalid 'pad' argument (must be None)"
desc = self.desc.short
@ -85,19 +85,19 @@ class xmrseed(baseconv):
base = len(wl)
if len(bytestr) not in self.seedlen_map:
die( 'SeedLengthError', f'{len(bytestr)}: invalid seed byte length for {desc}' )
die('SeedLengthError', f'{len(bytestr)}: invalid seed byte length for {desc}')
def num2base_monero(num):
w1 = num % base
w2 = (num//base + w1) % base
w3 = (num//base//base + w2) % base
return ( wl[w1], wl[w2], wl[w3] )
return (wl[w1], wl[w2], wl[w3])
def gen():
for i in range(len(bytestr)//4):
yield from num2base_monero(int.from_bytes( bytestr[i*4:i*4+4][::-1], 'big' ))
yield from num2base_monero(int.from_bytes(bytestr[i*4:i*4+4][::-1], 'big'))
o = list(gen())
o.append( self.monero_mn_checksum(o) )
o.append(self.monero_mn_checksum(o))
return ' '.join(o) if tostr else tuple(o)