addrlist.py 12 KB

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