addrlist.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  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. mmtype = mmtype or proto.dfl_mmtype
  143. assert mmtype in MMGenAddrType.mmtypes, f'{mmtype}: mmtype not in {MMGenAddrType.mmtypes!r}'
  144. from .proto.btc import mainnet
  145. self.bitcoin_addrtypes = tuple(
  146. MMGenAddrType(mainnet,key).name for key in mainnet.mmtypes )
  147. if seed and addr_idxs and mmtype: # data from seed + idxs
  148. self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
  149. adata = self.generate(seed,addr_idxs)
  150. do_chksum = True
  151. elif addrfile: # data from MMGen address file
  152. self.infile = addrfile
  153. adata = self.get_file().parse_file(addrfile) # sets self.al_id
  154. do_chksum = True
  155. elif al_id and adata: # data from tracking wallet
  156. self.al_id = al_id
  157. elif addrlist: # data from flat address list
  158. self.al_id = None
  159. from .util import remove_dups
  160. addrlist = remove_dups(addrlist,edesc='address',desc='address list')
  161. adata = AddrListData([AddrListEntry(proto=proto,addr=a) for a in addrlist])
  162. elif keylist: # data from flat key list
  163. self.al_id = None
  164. keylist = remove_dups(keylist,edesc='key',desc='key list',hide=True)
  165. adata = AddrListData([AddrListEntry(proto=proto,sec=PrivKey(proto=proto,wif=k)) for k in keylist])
  166. elif seed or addr_idxs:
  167. die(3,'Must specify both seed and addr indexes')
  168. elif al_id or adata:
  169. die(3,'Must specify both al_id and adata')
  170. else:
  171. die(3,f'Incorrect arguments for {type(self).__name__}')
  172. # al_id,adata now set
  173. self.data = adata
  174. self.num_addrs = len(adata)
  175. self.fmt_data = ''
  176. self.chksum = None
  177. if self.al_id == None:
  178. return
  179. self.id_str = AddrListIDStr(self)
  180. if type(self) == KeyList:
  181. return
  182. if do_chksum and not skip_chksum:
  183. self.chksum = AddrListChksum(self)
  184. self.do_chksum_msg(record=src=='gen')
  185. def do_chksum_msg(self,record):
  186. chk = 'Check this value against your records'
  187. rec = f'Record this checksum: it will be used to verify the {self.desc} file in the future'
  188. qmsg(
  189. f'Checksum for {self.desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' +
  190. (chk,rec)[record] )
  191. def generate(self,seed,addr_idxs):
  192. assert type(addr_idxs) is AddrIdxList
  193. seed = self.scramble_seed(seed.data)
  194. dmsg_sc('seed',seed[:8].hex())
  195. mmtype = self.al_id.mmtype
  196. gen_wallet_passwd = type(self) == KeyAddrList and 'wallet_passwd' in mmtype.extra_attrs
  197. gen_viewkey = type(self) == KeyAddrList and 'viewkey' in mmtype.extra_attrs
  198. if self.gen_addrs:
  199. from .addr import KeyGenerator,AddrGenerator
  200. kg = KeyGenerator( self.proto, mmtype.pubkey_type )
  201. ag = AddrGenerator( self.proto, mmtype )
  202. if self.add_p2pkh:
  203. ag2 = AddrGenerator( self.proto, 'compressed' )
  204. t_addrs,out = ( len(addr_idxs), AddrListData() )
  205. le = self.entry_type
  206. num,pos = (0,0)
  207. from hashlib import sha256,sha512
  208. from .globalvars import g
  209. while pos != t_addrs:
  210. seed = sha512(seed).digest()
  211. num += 1 # round
  212. if num != addr_idxs[pos]:
  213. continue
  214. pos += 1
  215. if not g.debug:
  216. qmsg_r(f'\rGenerating {self.gen_desc} #{num} ({pos} of {t_addrs})')
  217. e = le(proto=self.proto,idx=num)
  218. # Secret key is double sha256 of seed hash round /num/
  219. e.sec = PrivKey(
  220. self.proto,
  221. sha256(sha256(seed).digest()).digest(),
  222. compressed = mmtype.compressed,
  223. pubkey_type = mmtype.pubkey_type )
  224. if self.gen_addrs:
  225. data = kg.gen_data(e.sec)
  226. e.addr = ag.to_addr(data)
  227. if self.add_p2pkh:
  228. e.addr_p2pkh = ag2.to_addr(data)
  229. if gen_viewkey:
  230. e.viewkey = ag.to_viewkey(data)
  231. if gen_wallet_passwd:
  232. e.wallet_passwd = self.gen_wallet_passwd(e.sec)
  233. elif self.gen_passwds:
  234. e.passwd = self.gen_passwd(e.sec) # TODO - own type
  235. out.append(e)
  236. if g.debug_addrlist:
  237. Msg(f'generate():\n{e.pfmt()}')
  238. qmsg('\r{}: {} {}{} generated{}'.format(
  239. self.al_id.hl(),
  240. t_addrs,
  241. self.gen_desc,
  242. suf(t_addrs,self.gen_desc_pl),
  243. ' ' * 15 ))
  244. return out
  245. def gen_wallet_passwd(self,privbytes):
  246. from .proto.common import hash256
  247. return WalletPassword( hash256(privbytes)[:16].hex() )
  248. def check_format(self,addr):
  249. return True # format is checked when added to list entry object
  250. def scramble_seed(self,seed):
  251. is_btcfork = self.proto.base_coin == 'BTC'
  252. if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
  253. dmsg_sc('str','(none)')
  254. return seed
  255. if self.proto.base_coin == 'ETH':
  256. scramble_key = self.proto.coin.lower()
  257. else:
  258. scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
  259. from .crypto import scramble_seed
  260. if self.proto.testnet:
  261. scramble_key += ':' + self.proto.network
  262. dmsg_sc('str',scramble_key)
  263. return scramble_seed(seed,scramble_key.encode())
  264. def idxs(self):
  265. return [e.idx for e in self.data]
  266. def addrs(self):
  267. return [f'{self.al_id.sid}:{e.idx}' for e in self.data]
  268. def addrpairs(self):
  269. return [(e.idx,e.addr) for e in self.data]
  270. def coinaddrs(self):
  271. return [e.addr for e in self.data]
  272. def comments(self):
  273. return [e.label for e in self.data]
  274. def entry(self,idx):
  275. for e in self.data:
  276. if idx == e.idx:
  277. return e
  278. def coinaddr(self,idx):
  279. for e in self.data:
  280. if idx == e.idx:
  281. return e.addr
  282. def comment(self,idx):
  283. for e in self.data:
  284. if idx == e.idx:
  285. return e.label
  286. def set_comment(self,idx,comment):
  287. for e in self.data:
  288. if idx == e.idx:
  289. e.label = comment
  290. def make_reverse_dict_addrlist(self,coinaddrs):
  291. d = MMGenDict()
  292. b = coinaddrs
  293. for e in self.data:
  294. try:
  295. d[b[b.index(e.addr)]] = ( MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.label )
  296. except ValueError:
  297. pass
  298. return d
  299. def add_wifs(self,key_list):
  300. """
  301. Match WIF keys in a flat list to addresses in self by generating all
  302. possible addresses for each key.
  303. """
  304. def gen_addr(pk,t):
  305. at = self.proto.addr_type(t)
  306. from .addr import KeyGenerator,AddrGenerator
  307. kg = KeyGenerator(self.proto,at.pubkey_type)
  308. ag = AddrGenerator(self.proto,at)
  309. return ag.to_addr(kg.gen_data(pk))
  310. compressed_types = set(self.proto.mmtypes) - {'L','E'}
  311. uncompressed_types = set(self.proto.mmtypes) & {'L','E'}
  312. def gen():
  313. for wif in key_list:
  314. pk = PrivKey(proto=self.proto,wif=wif)
  315. for t in (compressed_types if pk.compressed else uncompressed_types):
  316. yield ( gen_addr(pk,t), pk )
  317. addrs4keys = dict(gen())
  318. for d in self.data:
  319. if d.addr in addrs4keys:
  320. d.sec = addrs4keys[d.addr]
  321. def list_missing(self,attr):
  322. return [d.addr for d in self.data if not getattr(d,attr)]
  323. def get_file(self):
  324. import mmgen.addrfile as mod
  325. return getattr( mod, type(self).__name__.replace('List','File') )(self)
  326. class KeyAddrList(AddrList):
  327. desc = 'key-address'
  328. gen_desc = 'key/address pair'
  329. gen_desc_pl = 's'
  330. gen_keys = True
  331. has_keys = True
  332. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  333. class KeyList(KeyAddrList):
  334. desc = 'key'
  335. gen_desc = 'key'
  336. gen_addrs = False