addrlist.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. addrlist.py: Address list classes for the MMGen suite
  20. """
  21. from hashlib import sha256,sha512
  22. from .util import qmsg,qmsg_r,suf,make_chksum_N,Msg
  23. from .objmethods import MMGenObject,Hilite,InitErrors
  24. from .obj import MMGenListItem,ListItemAttr,MMGenDict,WalletPassword
  25. from .seed import SeedID
  26. from .key import PrivKey
  27. from .addr import MMGenID,MMGenAddrType,CoinAddr,AddrIdx,AddrListID,ViewKey
  28. def dmsg_sc(desc,data):
  29. from .globalvars import g
  30. if g.debug_addrlist:
  31. Msg(f'sc_debug_{desc}: {data}')
  32. class AddrIdxList(list,InitErrors,MMGenObject):
  33. max_len = 1000000
  34. def __init__(self,fmt_str=None,idx_list=None,sep=','):
  35. try:
  36. if idx_list:
  37. return list.__init__(self,sorted({AddrIdx(i) for i in idx_list}))
  38. elif fmt_str:
  39. ret = []
  40. for i in (fmt_str.split(sep)):
  41. j = i.split('-')
  42. if len(j) == 1:
  43. idx = AddrIdx(i)
  44. if not idx:
  45. break
  46. ret.append(idx)
  47. elif len(j) == 2:
  48. beg = AddrIdx(j[0])
  49. if not beg:
  50. break
  51. end = AddrIdx(j[1])
  52. if not beg or (end < beg):
  53. break
  54. ret.extend([AddrIdx(x) for x in range(beg,end+1)])
  55. else: break
  56. else:
  57. return list.__init__(self,sorted(set(ret))) # fell off end of loop - success
  58. raise ValueError(f'{i!r}: invalid range')
  59. except Exception as e:
  60. return type(self).init_fail(e,idx_list or fmt_str)
  61. class AddrListEntryBase(MMGenListItem):
  62. invalid_attrs = {'proto'}
  63. def __init__(self,proto,**kwargs):
  64. self.__dict__['proto'] = proto
  65. MMGenListItem.__init__(self,**kwargs)
  66. class AddrListEntry(AddrListEntryBase):
  67. addr = ListItemAttr(CoinAddr,include_proto=True)
  68. idx = ListItemAttr(AddrIdx) # not present in flat addrlists
  69. label = ListItemAttr('TwComment',reassign_ok=True)
  70. sec = ListItemAttr(PrivKey,include_proto=True)
  71. viewkey = ListItemAttr(ViewKey,include_proto=True)
  72. wallet_passwd = ListItemAttr('WalletPassword')
  73. class AddrListChksum(str,Hilite):
  74. color = 'pink'
  75. trunc_ok = False
  76. def __new__(cls,addrlist):
  77. ea = addrlist.al_id.mmtype.extra_attrs or () # add viewkey and passwd to the mix, if present
  78. lines = [' '.join(
  79. addrlist.chksum_rec_f(e) +
  80. tuple(getattr(e,a) for a in ea if getattr(e,a))
  81. ) for e in addrlist.data]
  82. return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True))
  83. class AddrListIDStr(str,Hilite):
  84. color = 'green'
  85. trunc_ok = False
  86. def __new__(cls,addrlist,fmt_str=None):
  87. idxs = [e.idx for e in addrlist.data]
  88. prev = idxs[0]
  89. ret = prev,
  90. for i in idxs[1:]:
  91. if i == prev + 1:
  92. if i == idxs[-1]:
  93. ret += '-', i
  94. else:
  95. if prev != ret[-1]:
  96. ret += '-', prev
  97. ret += ',', i
  98. prev = i
  99. s = ''.join(map(str,ret))
  100. if fmt_str:
  101. ret = fmt_str.format(s)
  102. else:
  103. bc = (addrlist.proto.base_coin,addrlist.proto.coin)[addrlist.proto.base_coin=='ETH']
  104. mt = addrlist.al_id.mmtype
  105. ret = '{}{}{}[{}]'.format(
  106. addrlist.al_id.sid,
  107. ('-'+bc,'')[bc == 'BTC'],
  108. ('-'+mt,'')[mt in ('L','E')],
  109. s )
  110. dmsg_sc('id_str',ret[8:].split('[')[0])
  111. return str.__new__(cls,ret)
  112. class AddrListData(list,MMGenObject):
  113. pass
  114. class AddrList(MMGenObject): # Address info for a single seed ID
  115. entry_type = AddrListEntry
  116. main_attr = 'addr'
  117. desc = 'address'
  118. gen_desc = 'address'
  119. gen_desc_pl = 'es'
  120. gen_addrs = True
  121. gen_passwds = False
  122. gen_keys = False
  123. has_keys = False
  124. chksum_rec_f = lambda foo,e: ( str(e.idx), e.addr )
  125. def __init__(self,proto,
  126. addrfile = '',
  127. al_id = '',
  128. adata = [],
  129. seed = '',
  130. addr_idxs = '',
  131. src = '',
  132. addrlist = '',
  133. keylist = '',
  134. mmtype = None,
  135. skip_key_address_validity_check = False,
  136. skip_chksum = False,
  137. ):
  138. self.skip_ka_check = skip_key_address_validity_check
  139. self.proto = proto
  140. do_chksum = False
  141. mmtype = mmtype or proto.dfl_mmtype
  142. assert mmtype in MMGenAddrType.mmtypes, f'{mmtype}: mmtype not in {MMGenAddrType.mmtypes!r}'
  143. from .protocol import CoinProtocol
  144. self.bitcoin_addrtypes = tuple(
  145. MMGenAddrType(CoinProtocol.Bitcoin,key).name for key in CoinProtocol.Bitcoin.mmtypes)
  146. if seed and addr_idxs and mmtype: # data from seed + idxs
  147. self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
  148. adata = self.generate(seed,addr_idxs)
  149. do_chksum = True
  150. elif addrfile: # data from MMGen address file
  151. self.infile = addrfile
  152. adata = self.get_file().parse_file(addrfile) # sets self.al_id
  153. do_chksum = True
  154. elif al_id and adata: # data from tracking wallet
  155. self.al_id = al_id
  156. elif addrlist: # data from flat address list
  157. self.al_id = None
  158. from .util import remove_dups
  159. addrlist = remove_dups(addrlist,edesc='address',desc='address list')
  160. adata = AddrListData([AddrListEntry(proto=proto,addr=a) for a in addrlist])
  161. elif keylist: # data from flat key list
  162. self.al_id = None
  163. keylist = remove_dups(keylist,edesc='key',desc='key list',hide=True)
  164. adata = AddrListData([AddrListEntry(proto=proto,sec=PrivKey(proto=proto,wif=k)) for k in keylist])
  165. elif seed or addr_idxs:
  166. die(3,'Must specify both seed and addr indexes')
  167. elif al_id or adata:
  168. die(3,'Must specify both al_id and adata')
  169. else:
  170. die(3,f'Incorrect arguments for {type(self).__name__}')
  171. # al_id,adata now set
  172. self.data = adata
  173. self.num_addrs = len(adata)
  174. self.fmt_data = ''
  175. self.chksum = None
  176. if self.al_id == None:
  177. return
  178. self.id_str = AddrListIDStr(self)
  179. if type(self) == KeyList:
  180. return
  181. if do_chksum and not skip_chksum:
  182. self.chksum = AddrListChksum(self)
  183. self.do_chksum_msg(record=src=='gen')
  184. def do_chksum_msg(self,record):
  185. chk = 'Check this value against your records'
  186. rec = f'Record this checksum: it will be used to verify the {self.desc} file in the future'
  187. qmsg(
  188. f'Checksum for {self.desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' +
  189. (chk,rec)[record] )
  190. def generate(self,seed,addr_idxs):
  191. assert type(addr_idxs) is AddrIdxList
  192. seed = self.scramble_seed(seed.data)
  193. dmsg_sc('seed',seed[:8].hex())
  194. mmtype = self.al_id.mmtype
  195. gen_wallet_passwd = type(self) == KeyAddrList and 'wallet_passwd' in mmtype.extra_attrs
  196. gen_viewkey = type(self) == KeyAddrList and 'viewkey' in mmtype.extra_attrs
  197. if self.gen_addrs:
  198. from .addr import KeyGenerator,AddrGenerator
  199. kg = KeyGenerator( self.proto, mmtype.pubkey_type )
  200. ag = AddrGenerator( self.proto, mmtype )
  201. t_addrs,out = ( len(addr_idxs), AddrListData() )
  202. le = self.entry_type
  203. num,pos = (0,0)
  204. from .globalvars import g
  205. while pos != t_addrs:
  206. seed = sha512(seed).digest()
  207. num += 1 # round
  208. if num != addr_idxs[pos]:
  209. continue
  210. pos += 1
  211. if not g.debug:
  212. qmsg_r(f'\rGenerating {self.gen_desc} #{num} ({pos} of {t_addrs})')
  213. e = le(proto=self.proto,idx=num)
  214. # Secret key is double sha256 of seed hash round /num/
  215. e.sec = PrivKey(
  216. self.proto,
  217. sha256(sha256(seed).digest()).digest(),
  218. compressed = mmtype.compressed,
  219. pubkey_type = mmtype.pubkey_type )
  220. if self.gen_addrs:
  221. data = kg.gen_data(e.sec)
  222. e.addr = ag.to_addr(data)
  223. if gen_viewkey:
  224. e.viewkey = ag.to_viewkey(data)
  225. if gen_wallet_passwd:
  226. e.wallet_passwd = self.gen_wallet_passwd(e.sec)
  227. elif self.gen_passwds:
  228. e.passwd = self.gen_passwd(e.sec) # TODO - own type
  229. out.append(e)
  230. if g.debug_addrlist:
  231. Msg(f'generate():\n{e.pfmt()}')
  232. qmsg('\r{}: {} {}{} generated{}'.format(
  233. self.al_id.hl(),
  234. t_addrs,
  235. self.gen_desc,
  236. suf(t_addrs,self.gen_desc_pl),
  237. ' ' * 15 ))
  238. return out
  239. def gen_wallet_passwd(self,privbytes):
  240. from .protocol import hash256
  241. return WalletPassword( hash256(privbytes)[:16].hex() )
  242. def check_format(self,addr):
  243. return True # format is checked when added to list entry object
  244. def scramble_seed(self,seed):
  245. is_btcfork = self.proto.base_coin == 'BTC'
  246. if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
  247. dmsg_sc('str','(none)')
  248. return seed
  249. if self.proto.base_coin == 'ETH':
  250. scramble_key = self.proto.coin.lower()
  251. else:
  252. scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
  253. from .crypto import scramble_seed
  254. if self.proto.testnet:
  255. scramble_key += ':' + self.proto.network
  256. dmsg_sc('str',scramble_key)
  257. return scramble_seed(seed,scramble_key.encode())
  258. def idxs(self):
  259. return [e.idx for e in self.data]
  260. def addrs(self):
  261. return [f'{self.al_id.sid}:{e.idx}' for e in self.data]
  262. def addrpairs(self):
  263. return [(e.idx,e.addr) for e in self.data]
  264. def coinaddrs(self):
  265. return [e.addr for e in self.data]
  266. def comments(self):
  267. return [e.label for e in self.data]
  268. def entry(self,idx):
  269. for e in self.data:
  270. if idx == e.idx:
  271. return e
  272. def coinaddr(self,idx):
  273. for e in self.data:
  274. if idx == e.idx:
  275. return e.addr
  276. def comment(self,idx):
  277. for e in self.data:
  278. if idx == e.idx:
  279. return e.label
  280. def set_comment(self,idx,comment):
  281. for e in self.data:
  282. if idx == e.idx:
  283. e.label = comment
  284. def make_reverse_dict_addrlist(self,coinaddrs):
  285. d = MMGenDict()
  286. b = coinaddrs
  287. for e in self.data:
  288. try:
  289. d[b[b.index(e.addr)]] = ( MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.label )
  290. except ValueError:
  291. pass
  292. return d
  293. def add_wifs(self,key_list):
  294. """
  295. Match WIF keys in a flat list to addresses in self by generating all
  296. possible addresses for each key.
  297. """
  298. def gen_addr(pk,t):
  299. at = self.proto.addr_type(t)
  300. from .addr import KeyGenerator,AddrGenerator
  301. kg = KeyGenerator(self.proto,at.pubkey_type)
  302. ag = AddrGenerator(self.proto,at)
  303. return ag.to_addr(kg.gen_data(pk))
  304. compressed_types = set(self.proto.mmtypes) - {'L','E'}
  305. uncompressed_types = set(self.proto.mmtypes) & {'L','E'}
  306. def gen():
  307. for wif in key_list:
  308. pk = PrivKey(proto=self.proto,wif=wif)
  309. for t in (compressed_types if pk.compressed else uncompressed_types):
  310. yield ( gen_addr(pk,t), pk )
  311. addrs4keys = dict(gen())
  312. for d in self.data:
  313. if d.addr in addrs4keys:
  314. d.sec = addrs4keys[d.addr]
  315. def list_missing(self,attr):
  316. return [d.addr for d in self.data if not getattr(d,attr)]
  317. def get_file(self):
  318. import mmgen.addrfile as mod
  319. return getattr( mod, type(self).__name__.replace('List','File') )(self)
  320. class KeyAddrList(AddrList):
  321. desc = 'key-address'
  322. gen_desc = 'key/address pair'
  323. gen_desc_pl = 's'
  324. gen_keys = True
  325. has_keys = True
  326. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  327. class KeyList(KeyAddrList):
  328. desc = 'key'
  329. gen_desc = 'key'
  330. gen_addrs = False