123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- #!/usr/bin/env python3
- #
- # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
- # Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- """
- 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 .key import PrivKey
- from .addr import MMGenID,MMGenAddrType,CoinAddr,AddrIdx,AddrListID,ViewKey
- class AddrIdxList(tuple,InitErrors,MMGenObject):
- max_len = 1000000
- def __new__(cls,fmt_str=None,idx_list=None,sep=','):
- try:
- if fmt_str:
- def gen():
- for i in (fmt_str.split(sep)):
- j = [int(x) for x in i.split('-')]
- if len(j) == 1:
- yield j[0]
- elif len(j) == 2:
- if j[0] > j[1]:
- raise ValueError(f'{i}: invalid range')
- for k in range(j[0], j[1] + 1):
- yield k
- else:
- raise ValueError(f'{i}: invalid range')
- idx_list = tuple(gen())
- 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)
- @property
- def id_str(self):
- def gen():
- i_save = self[0]
- yield f'{i_save}'
- in_range = False
- for i in self[1:]:
- if i - i_save == 1:
- in_range = True
- else:
- if in_range:
- in_range = False
- yield f'-{i_save}'
- yield f',{i}'
- i_save = i
- if in_range:
- yield f'-{i_save}'
- return ''.join(gen()) if self else ''
- class AddrListEntryBase(MMGenListItem):
- invalid_attrs = {'proto'}
- def __init__(self,proto,**kwargs):
- self.__dict__['proto'] = proto
- MMGenListItem.__init__(self,**kwargs)
- class AddrListEntry(AddrListEntryBase):
- 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)
- wallet_passwd = ListItemAttr(WalletPassword)
- class AddrListChksum(HiliteStr):
- color = 'pink'
- trunc_ok = False
- 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))
- ) for e in addrlist.data]
- 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):
- idxs = [e.idx for e in addrlist.data]
- prev = idxs[0]
- ret = [prev]
- for i in idxs[1:]:
- if i == prev + 1:
- if i == idxs[-1]:
- ret.extend(['-', i])
- else:
- if prev != ret[-1]:
- ret.extend(['-', prev])
- ret.extend([',', i])
- prev = i
- s = ''.join(map(str,ret))
- if fmt_str:
- ret = fmt_str.format(s)
- else:
- bc = (addrlist.proto.base_coin,addrlist.proto.coin)[addrlist.proto.base_coin=='ETH']
- mt = addrlist.al_id.mmtype
- ret = '{}{}{}[{}]'.format(
- addrlist.al_id.sid,
- ('-'+bc,'')[bc == 'BTC'],
- ('-'+mt,'')[mt in ('L','E')],
- s )
- addrlist.dmsg_sc('id_str',ret[8:].split('[')[0])
- return str.__new__(cls,ret)
- class AddrListData(list,MMGenObject):
- pass
- class AddrList(MMGenObject): # Address info for a single seed ID
- entry_type = AddrListEntry
- main_attr = 'addr'
- desc = 'address'
- gen_desc = 'address'
- gen_desc_pl = 'es'
- gen_addrs = True
- gen_passwds = False
- gen_keys = False
- has_keys = False
- chksum_rec_f = lambda foo,e: ( str(e.idx), e.addr )
- def dmsg_sc(self,desc,data):
- Msg(f'sc_debug_{desc}: {data}')
- def noop(self,desc,data):
- pass
- def __init__(
- self,
- cfg,
- proto,
- addrfile = '',
- al_id = '',
- adata = [],
- seed = '',
- addr_idxs = '',
- src = '',
- addrlist = '',
- keylist = '',
- mmtype = None,
- 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 ):
- self.cfg = cfg
- self.ka_validity_chk = key_address_validity_check
- self.add_p2pkh = add_p2pkh
- self.proto = proto
- do_chksum = False
- if not cfg.debug_addrlist:
- 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) )
- src = 'gen'
- 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
- adata = self.file.parse_file(addrfile) # sets self.al_id
- do_chksum = True
- elif al_id and adata: # data from tracking wallet
- self.al_id = al_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])
- 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])
- elif seed or addr_idxs:
- die(3,'Must specify both seed and addr indexes')
- elif al_id or adata:
- die(3,'Must specify both al_id and adata')
- else:
- die(3,f'Incorrect arguments for {type(self).__name__}')
- # al_id,adata now set
- self.data = adata
- self.num_addrs = len(adata)
- self.fmt_data = ''
- self.chksum = None
- if self.al_id is None:
- return
- 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}')
- self.id_str = AddrListIDStr(self)
- if type(self) is KeyList:
- return
- if do_chksum and not skip_chksum:
- self.chksum = AddrListChksum(self)
- if not skip_chksum_msg:
- self.do_chksum_msg(record=src=='gen')
- 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] )
- def generate(self,seed,addr_idxs):
- seed = self.scramble_seed(seed.data)
- 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
- 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 )
- if self.add_p2pkh:
- ag2 = AddrGenerator( self.cfg, self.proto, 'compressed' )
- from .derive import derive_coin_privkey_bytes
- t_addrs = len(addr_idxs)
- le = self.entry_type
- out = AddrListData()
- CR = '\n' if self.cfg.debug_addrlist else '\r'
- 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})' )
- 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 )
- if self.gen_addrs:
- data = kg.gen_data(e.sec)
- e.addr = ag.to_addr(data)
- if self.add_p2pkh:
- e.addr_p2pkh = ag2.to_addr(data)
- if gen_viewkey:
- 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 )
- elif self.gen_passwds:
- e.passwd = self.gen_passwd(e.sec) # TODO - own type
- out.append(e)
- self.cfg._util.qmsg('{}{}: {} {}{} generated{}'.format(
- CR,
- self.al_id.hl(),
- t_addrs,
- self.gen_desc,
- suf(t_addrs,self.gen_desc_pl),
- ' ' * 15 ))
- return out
- def gen_wallet_passwd(self,privbytes):
- from .proto.btc.common import hash256
- return WalletPassword( hash256(privbytes)[:16].hex() )
- def check_format(self,addr):
- return True # format is checked when added to list entry object
- 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)')
- 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
- 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())
- def idxs(self):
- return [e.idx for e in self.data]
- def addrs(self):
- 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]
- def coinaddrs(self):
- return [e.addr for e in self.data]
- def comments(self):
- return [e.comment for e in self.data]
- def entry(self,idx):
- for e in self.data:
- if idx == e.idx:
- return e
- def coinaddr(self,idx):
- for e in self.data:
- if idx == e.idx:
- return e.addr
- def comment(self,idx):
- for e in self.data:
- if idx == e.idx:
- return e.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):
- 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 )
- except ValueError:
- pass
- return d
- 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):
- 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 )
- return ag.to_addr(kg.gen_data(pk))
- 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)
- for t in (compressed_types if pk.compressed else uncompressed_types):
- yield ( gen_addr(pk,t), pk )
- addrs4keys = dict(gen())
- for d in self.data:
- 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)]
- @property
- def file(self):
- if not hasattr(self,'_file'):
- from . import addrfile
- self._file = getattr( addrfile, type(self).__name__.replace('List','File') )(self)
- return self._file
- class KeyAddrList(AddrList):
- desc = 'key-address'
- gen_desc = 'key/address pair'
- gen_desc_pl = 's'
- gen_keys = True
- has_keys = True
- chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
- class ViewKeyAddrList(KeyAddrList):
- desc = 'viewkey-address'
- gen_desc = 'viewkey/address pair'
- chksum_rec_f = lambda foo,e: ( str(e.idx), e.addr )
- class KeyList(KeyAddrList):
- desc = 'key'
- gen_desc = 'key'
- gen_addrs = False
|