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