addrlist.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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. key_address_validity_check = None, # None=prompt user, True=check without prompt, False=skip check
  135. skip_chksum = False,
  136. add_p2pkh = False,
  137. ):
  138. self.ka_validity_chk = 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. seed = self.scramble_seed(seed.data)
  189. dmsg_sc('seed',seed[:8].hex())
  190. mmtype = self.al_id.mmtype
  191. gen_wallet_passwd = type(self) == KeyAddrList and 'wallet_passwd' in mmtype.extra_attrs
  192. gen_viewkey = type(self) == KeyAddrList and 'viewkey' in mmtype.extra_attrs
  193. if self.gen_addrs:
  194. from .addrgen import KeyGenerator,AddrGenerator
  195. kg = KeyGenerator( self.proto, mmtype.pubkey_type )
  196. ag = AddrGenerator( self.proto, mmtype )
  197. if self.add_p2pkh:
  198. ag2 = AddrGenerator( self.proto, 'compressed' )
  199. from .globalvars import g
  200. from .derive import derive_coin_privkey_bytes
  201. t_addrs = len(addr_idxs)
  202. le = self.entry_type
  203. out = AddrListData()
  204. CR = '\n' if g.debug_addrlist else '\r'
  205. for pk_bytes in derive_coin_privkey_bytes(seed,addr_idxs):
  206. if not g.debug:
  207. qmsg_r(f'{CR}Generating {self.gen_desc} #{pk_bytes.idx} ({pk_bytes.pos} of {t_addrs})')
  208. e = le( proto=self.proto, idx=pk_bytes.idx )
  209. e.sec = PrivKey(
  210. self.proto,
  211. pk_bytes.data,
  212. compressed = mmtype.compressed,
  213. pubkey_type = mmtype.pubkey_type )
  214. if self.gen_addrs:
  215. data = kg.gen_data(e.sec)
  216. e.addr = ag.to_addr(data)
  217. if self.add_p2pkh:
  218. e.addr_p2pkh = ag2.to_addr(data)
  219. if gen_viewkey:
  220. e.viewkey = ag.to_viewkey(data)
  221. if gen_wallet_passwd:
  222. e.wallet_passwd = self.gen_wallet_passwd(e.sec)
  223. elif self.gen_passwds:
  224. e.passwd = self.gen_passwd(e.sec) # TODO - own type
  225. out.append(e)
  226. qmsg('{}{}: {} {}{} generated{}'.format(
  227. CR,
  228. self.al_id.hl(),
  229. t_addrs,
  230. self.gen_desc,
  231. suf(t_addrs,self.gen_desc_pl),
  232. ' ' * 15 ))
  233. return out
  234. def gen_wallet_passwd(self,privbytes):
  235. from .proto.btc.common import hash256
  236. return WalletPassword( hash256(privbytes)[:16].hex() )
  237. def check_format(self,addr):
  238. return True # format is checked when added to list entry object
  239. def scramble_seed(self,seed):
  240. is_btcfork = self.proto.base_coin == 'BTC'
  241. if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
  242. dmsg_sc('str','(none)')
  243. return seed
  244. if self.proto.base_coin == 'ETH':
  245. scramble_key = self.proto.coin.lower()
  246. else:
  247. scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
  248. from .crypto import scramble_seed
  249. if self.proto.testnet:
  250. scramble_key += ':' + self.proto.network
  251. dmsg_sc('str',scramble_key)
  252. return scramble_seed(seed,scramble_key.encode())
  253. def idxs(self):
  254. return [e.idx for e in self.data]
  255. def addrs(self):
  256. return [f'{self.al_id.sid}:{e.idx}' for e in self.data]
  257. def addrpairs(self):
  258. return [(e.idx,e.addr) for e in self.data]
  259. def coinaddrs(self):
  260. return [e.addr for e in self.data]
  261. def comments(self):
  262. return [e.label for e in self.data]
  263. def entry(self,idx):
  264. for e in self.data:
  265. if idx == e.idx:
  266. return e
  267. def coinaddr(self,idx):
  268. for e in self.data:
  269. if idx == e.idx:
  270. return e.addr
  271. def comment(self,idx):
  272. for e in self.data:
  273. if idx == e.idx:
  274. return e.label
  275. def set_comment(self,idx,comment):
  276. for e in self.data:
  277. if idx == e.idx:
  278. e.label = comment
  279. def make_reverse_dict_addrlist(self,coinaddrs):
  280. d = MMGenDict()
  281. b = coinaddrs
  282. for e in self.data:
  283. try:
  284. d[b[b.index(e.addr)]] = ( MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.label )
  285. except ValueError:
  286. pass
  287. return d
  288. def add_wifs(self,key_list):
  289. """
  290. Match WIF keys in a flat list to addresses in self by generating all
  291. possible addresses for each key.
  292. """
  293. def gen_addr(pk,t):
  294. at = self.proto.addr_type(t)
  295. from .addrgen import KeyGenerator,AddrGenerator
  296. kg = KeyGenerator(self.proto,at.pubkey_type)
  297. ag = AddrGenerator(self.proto,at)
  298. return ag.to_addr(kg.gen_data(pk))
  299. compressed_types = set(self.proto.mmtypes) - {'L','E'}
  300. uncompressed_types = set(self.proto.mmtypes) & {'L','E'}
  301. def gen():
  302. for wif in key_list:
  303. pk = PrivKey(proto=self.proto,wif=wif)
  304. for t in (compressed_types if pk.compressed else uncompressed_types):
  305. yield ( gen_addr(pk,t), pk )
  306. addrs4keys = dict(gen())
  307. for d in self.data:
  308. if d.addr in addrs4keys:
  309. d.sec = addrs4keys[d.addr]
  310. def list_missing(self,attr):
  311. return [d.addr for d in self.data if not getattr(d,attr)]
  312. def get_file(self):
  313. import mmgen.addrfile as mod
  314. return getattr( mod, type(self).__name__.replace('List','File') )(self)
  315. class KeyAddrList(AddrList):
  316. desc = 'key-address'
  317. gen_desc = 'key/address pair'
  318. gen_desc_pl = 's'
  319. gen_keys = True
  320. has_keys = True
  321. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  322. class KeyList(KeyAddrList):
  323. desc = 'key'
  324. gen_desc = 'key'
  325. gen_addrs = False