addrlist.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2024 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,HiliteStr,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(HiliteStr):
  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(HiliteStr):
  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.extend(['-', i])
  99. else:
  100. if prev != ret[-1]:
  101. ret.extend(['-', prev])
  102. ret.extend([',', 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. from .util import remove_dups
  177. keylist = remove_dups(keylist,edesc='key',desc='key list',hide=True)
  178. adata = AddrListData([AddrListEntry(proto=proto,sec=PrivKey(proto=proto,wif=k)) for k in keylist])
  179. elif seed or addr_idxs:
  180. die(3,'Must specify both seed and addr indexes')
  181. elif al_id or adata:
  182. die(3,'Must specify both al_id and adata')
  183. else:
  184. die(3,f'Incorrect arguments for {type(self).__name__}')
  185. # al_id,adata now set
  186. self.data = adata
  187. self.num_addrs = len(adata)
  188. self.fmt_data = ''
  189. self.chksum = None
  190. if self.al_id is None:
  191. return
  192. if type(self) is ViewKeyAddrList:
  193. if not 'viewkey' in self.al_id.mmtype.extra_attrs:
  194. die(1,f'viewkeys not supported for address type {self.al_id.mmtype.desc!r}')
  195. self.id_str = AddrListIDStr(self)
  196. if type(self) is KeyList:
  197. return
  198. if do_chksum and not skip_chksum:
  199. self.chksum = AddrListChksum(self)
  200. if not skip_chksum_msg:
  201. self.do_chksum_msg(record=src=='gen')
  202. def do_chksum_msg(self,record):
  203. chk = 'Check this value against your records'
  204. rec = f'Record this checksum: it will be used to verify the {self.desc} file in the future'
  205. self.cfg._util.qmsg(
  206. f'Checksum for {self.desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' +
  207. (chk,rec)[record] )
  208. def generate(self,seed,addr_idxs):
  209. seed = self.scramble_seed(seed.data)
  210. self.dmsg_sc('seed',seed[:8].hex())
  211. mmtype = self.al_id.mmtype
  212. gen_wallet_passwd = type(self) in (KeyAddrList,ViewKeyAddrList) and 'wallet_passwd' in mmtype.extra_attrs
  213. gen_viewkey = type(self) in (KeyAddrList,ViewKeyAddrList) and 'viewkey' in mmtype.extra_attrs
  214. if self.gen_addrs:
  215. from .keygen import KeyGenerator
  216. from .addrgen import AddrGenerator
  217. kg = KeyGenerator( self.cfg, self.proto, mmtype.pubkey_type )
  218. ag = AddrGenerator( self.cfg, self.proto, mmtype )
  219. if self.add_p2pkh:
  220. ag2 = AddrGenerator( self.cfg, self.proto, 'compressed' )
  221. from .derive import derive_coin_privkey_bytes
  222. t_addrs = len(addr_idxs)
  223. le = self.entry_type
  224. out = AddrListData()
  225. CR = '\n' if self.cfg.debug_addrlist else '\r'
  226. for pk_bytes in derive_coin_privkey_bytes(seed,addr_idxs):
  227. if not self.cfg.debug:
  228. self.cfg._util.qmsg_r(
  229. f'{CR}Generating {self.gen_desc} #{pk_bytes.idx} ({pk_bytes.pos} of {t_addrs})' )
  230. e = le( proto=self.proto, idx=pk_bytes.idx )
  231. e.sec = PrivKey(
  232. self.proto,
  233. pk_bytes.data,
  234. compressed = mmtype.compressed,
  235. pubkey_type = mmtype.pubkey_type )
  236. if self.gen_addrs:
  237. data = kg.gen_data(e.sec)
  238. e.addr = ag.to_addr(data)
  239. if self.add_p2pkh:
  240. e.addr_p2pkh = ag2.to_addr(data)
  241. if gen_viewkey:
  242. e.viewkey = ag.to_viewkey(data)
  243. if gen_wallet_passwd:
  244. e.wallet_passwd = self.gen_wallet_passwd(
  245. e.viewkey.encode() if type(self) is ViewKeyAddrList else e.sec )
  246. elif self.gen_passwds:
  247. e.passwd = self.gen_passwd(e.sec) # TODO - own type
  248. out.append(e)
  249. self.cfg._util.qmsg('{}{}: {} {}{} generated{}'.format(
  250. CR,
  251. self.al_id.hl(),
  252. t_addrs,
  253. self.gen_desc,
  254. suf(t_addrs,self.gen_desc_pl),
  255. ' ' * 15 ))
  256. return out
  257. def gen_wallet_passwd(self,privbytes):
  258. from .proto.btc.common import hash256
  259. return WalletPassword( hash256(privbytes)[:16].hex() )
  260. def check_format(self,addr):
  261. return True # format is checked when added to list entry object
  262. def scramble_seed(self,seed):
  263. is_btcfork = self.proto.base_coin == 'BTC'
  264. if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
  265. self.dmsg_sc('str','(none)')
  266. return seed
  267. if self.proto.base_coin == 'ETH':
  268. scramble_key = self.proto.coin.lower()
  269. else:
  270. scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
  271. from .crypto import Crypto
  272. if self.proto.testnet:
  273. scramble_key += ':' + self.proto.network
  274. self.dmsg_sc('str',scramble_key)
  275. return Crypto(self.cfg).scramble_seed(seed,scramble_key.encode())
  276. def idxs(self):
  277. return [e.idx for e in self.data]
  278. def addrs(self):
  279. return [f'{self.al_id.sid}:{e.idx}' for e in self.data]
  280. def addrpairs(self):
  281. return [(e.idx,e.addr) for e in self.data]
  282. def coinaddrs(self):
  283. return [e.addr for e in self.data]
  284. def comments(self):
  285. return [e.comment for e in self.data]
  286. def entry(self,idx):
  287. for e in self.data:
  288. if idx == e.idx:
  289. return e
  290. def coinaddr(self,idx):
  291. for e in self.data:
  292. if idx == e.idx:
  293. return e.addr
  294. def comment(self,idx):
  295. for e in self.data:
  296. if idx == e.idx:
  297. return e.comment
  298. def set_comment(self,idx,comment):
  299. for e in self.data:
  300. if idx == e.idx:
  301. e.comment = comment
  302. def make_reverse_dict_addrlist(self,coinaddrs):
  303. d = MMGenDict()
  304. b = coinaddrs
  305. for e in self.data:
  306. try:
  307. d[b[b.index(e.addr)]] = ( MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.comment )
  308. except ValueError:
  309. pass
  310. return d
  311. def add_wifs(self,key_list):
  312. """
  313. Match WIF keys in a flat list to addresses in self by generating all
  314. possible addresses for each key.
  315. """
  316. def gen_addr(pk,t):
  317. at = self.proto.addr_type(t)
  318. from .keygen import KeyGenerator
  319. from .addrgen import AddrGenerator
  320. kg = KeyGenerator( self.cfg, self.proto, at.pubkey_type )
  321. ag = AddrGenerator( self.cfg, self.proto, at )
  322. return ag.to_addr(kg.gen_data(pk))
  323. compressed_types = set(self.proto.mmtypes) - {'L','E'}
  324. uncompressed_types = set(self.proto.mmtypes) & {'L','E'}
  325. def gen():
  326. for wif in key_list:
  327. pk = PrivKey(proto=self.proto,wif=wif)
  328. for t in (compressed_types if pk.compressed else uncompressed_types):
  329. yield ( gen_addr(pk,t), pk )
  330. addrs4keys = dict(gen())
  331. for d in self.data:
  332. if d.addr in addrs4keys:
  333. d.sec = addrs4keys[d.addr]
  334. def list_missing(self,attr):
  335. return [d.addr for d in self.data if not getattr(d,attr)]
  336. @property
  337. def file(self):
  338. if not hasattr(self,'_file'):
  339. from . import addrfile
  340. self._file = getattr( addrfile, type(self).__name__.replace('List','File') )(self)
  341. return self._file
  342. class KeyAddrList(AddrList):
  343. desc = 'key-address'
  344. gen_desc = 'key/address pair'
  345. gen_desc_pl = 's'
  346. gen_keys = True
  347. has_keys = True
  348. chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif)
  349. class ViewKeyAddrList(KeyAddrList):
  350. desc = 'viewkey-address'
  351. gen_desc = 'viewkey/address pair'
  352. chksum_rec_f = lambda foo,e: ( str(e.idx), e.addr )
  353. class KeyList(KeyAddrList):
  354. desc = 'key'
  355. gen_desc = 'key'
  356. gen_addrs = False