subseed.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  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. subseed.py: Subseed classes and methods for the MMGen suite
  20. """
  21. from .exception import SubSeedNonceRangeExceeded
  22. from .obj import MMGenRange,IndexedDict
  23. from .seed import *
  24. from .crypto import scramble_seed
  25. class SubSeedIdxRange(MMGenRange):
  26. min_idx = 1
  27. max_idx = 1000000
  28. class SubSeedIdx(str,Hilite,InitErrors):
  29. color = 'red'
  30. trunc_ok = False
  31. def __new__(cls,s):
  32. if type(s) == cls:
  33. return s
  34. try:
  35. assert isinstance(s,str),'not a string or string subclass'
  36. idx = s[:-1] if s[-1] in 'SsLl' else s
  37. from .util import is_int
  38. assert is_int(idx),"valid format: an integer, plus optional letter 'S','s','L' or 'l'"
  39. idx = int(idx)
  40. assert idx >= SubSeedIdxRange.min_idx, f'subseed index < {SubSeedIdxRange.min_idx:,}'
  41. assert idx <= SubSeedIdxRange.max_idx, f'subseed index > {SubSeedIdxRange.max_idx:,}'
  42. sstype,ltr = ('short','S') if s[-1] in 'Ss' else ('long','L')
  43. me = str.__new__(cls,str(idx)+ltr)
  44. me.idx = idx
  45. me.type = sstype
  46. return me
  47. except Exception as e:
  48. return cls.init_fail(e,s)
  49. class SubSeed(SeedBase):
  50. idx = ImmutableAttr(int,typeconv=False)
  51. nonce = ImmutableAttr(int,typeconv=False)
  52. ss_idx = ImmutableAttr(SubSeedIdx)
  53. max_nonce = 1000
  54. def __init__(self,parent_list,idx,nonce,length):
  55. self.idx = idx
  56. self.nonce = nonce
  57. self.ss_idx = str(idx) + { 'long': 'L', 'short': 'S' }[length]
  58. self.parent_list = parent_list
  59. SeedBase.__init__(self,seed_bin=type(self).make_subseed_bin(parent_list,idx,nonce,length))
  60. @staticmethod
  61. def make_subseed_bin(parent_list,idx:int,nonce:int,length:str):
  62. seed = parent_list.parent_seed
  63. short = { 'short': True, 'long': False }[length]
  64. # field maximums: idx: 4294967295 (1000000), nonce: 65535 (1000), short: 255 (1)
  65. scramble_key = idx.to_bytes(4,'big') + nonce.to_bytes(2,'big') + short.to_bytes(1,'big')
  66. return scramble_seed(seed.data,scramble_key)[:16 if short else seed.byte_len]
  67. class SubSeedList(MMGenObject):
  68. have_short = True
  69. nonce_start = 0
  70. debug_last_share_sid_len = 3
  71. def __init__(self,parent_seed):
  72. self.member_type = SubSeed
  73. self.parent_seed = parent_seed
  74. self.data = { 'long': IndexedDict(), 'short': IndexedDict() }
  75. def __len__(self):
  76. return len(self.data['long'])
  77. def get_subseed_by_ss_idx(self,ss_idx_in,print_msg=False):
  78. ss_idx = SubSeedIdx(ss_idx_in)
  79. if print_msg:
  80. msg_r('{} {} of {}...'.format(
  81. green('Generating subseed'),
  82. ss_idx.hl(),
  83. self.parent_seed.sid.hl(),
  84. ))
  85. if ss_idx.idx > len(self):
  86. self._generate(ss_idx.idx)
  87. sid = self.data[ss_idx.type].key(ss_idx.idx-1)
  88. idx,nonce = self.data[ss_idx.type][sid]
  89. if idx != ss_idx.idx:
  90. die(3, "{} != {}: self.data[{t!r}].key(i) does not match self.data[{t!r}][i]!".format(
  91. idx,
  92. ss_idx.idx,
  93. t = ss_idx.type ))
  94. if print_msg:
  95. msg(f'\b\b\b => {SeedID.hlc(sid)}')
  96. seed = self.member_type(self,idx,nonce,length=ss_idx.type)
  97. assert seed.sid == sid, f'{seed.sid} != {sid}: Seed ID mismatch!'
  98. return seed
  99. def get_subseed_by_seed_id(self,sid,last_idx=None,print_msg=False):
  100. def get_existing_subseed_by_seed_id(sid):
  101. for k in ('long','short') if self.have_short else ('long',):
  102. if sid in self.data[k]:
  103. idx,nonce = self.data[k][sid]
  104. return self.member_type(self,idx,nonce,length=k)
  105. def do_msg(subseed):
  106. if print_msg:
  107. qmsg('{} {} ({}:{})'.format(
  108. green('Found subseed'),
  109. subseed.sid.hl(),
  110. self.parent_seed.sid.hl(),
  111. subseed.ss_idx.hl(),
  112. ))
  113. if last_idx == None:
  114. last_idx = g.subseeds
  115. subseed = get_existing_subseed_by_seed_id(sid)
  116. if subseed:
  117. do_msg(subseed)
  118. return subseed
  119. if len(self) >= last_idx:
  120. return None
  121. self._generate(last_idx,last_sid=sid)
  122. subseed = get_existing_subseed_by_seed_id(sid)
  123. if subseed:
  124. do_msg(subseed)
  125. return subseed
  126. def _collision_debug_msg(self,sid,idx,nonce,nonce_desc='nonce',debug_last_share=False):
  127. slen = 'short' if sid in self.data['short'] else 'long'
  128. m1 = f'add_subseed(idx={idx},{slen}):'
  129. if sid == self.parent_seed.sid:
  130. m2 = f'collision with parent Seed ID {sid},'
  131. else:
  132. if debug_last_share:
  133. sl = self.debug_last_share_sid_len
  134. colliding_idx = [d[:sl] for d in self.data[slen].keys].index(sid[:sl]) + 1
  135. sid = sid[:sl]
  136. else:
  137. colliding_idx = self.data[slen][sid][0]
  138. m2 = f'collision with ID {sid} (idx={colliding_idx},{slen}),'
  139. msg(f'{m1:30} {m2:46} incrementing {nonce_desc} to {nonce+1}')
  140. def _generate(self,last_idx=None,last_sid=None):
  141. if last_idx == None:
  142. last_idx = g.subseeds
  143. first_idx = len(self) + 1
  144. if first_idx > last_idx:
  145. return None
  146. if last_sid != None:
  147. last_sid = SeedID(sid=last_sid)
  148. def add_subseed(idx,length):
  149. for nonce in range(self.nonce_start,self.member_type.max_nonce+1): # handle SeedID collisions
  150. sid = make_chksum_8(self.member_type.make_subseed_bin(self,idx,nonce,length))
  151. if sid in self.data['long'] or sid in self.data['short'] or sid == self.parent_seed.sid:
  152. if g.debug_subseed: # should get ≈450 collisions for first 1,000,000 subseeds
  153. self._collision_debug_msg(sid,idx,nonce)
  154. else:
  155. self.data[length][sid] = (idx,nonce)
  156. return last_sid == sid
  157. else: # must exit here, as this could leave self.data in inconsistent state
  158. raise SubSeedNonceRangeExceeded('add_subseed(): nonce range exceeded')
  159. for idx in SubSeedIdxRange(first_idx,last_idx).iterate():
  160. match1 = add_subseed(idx,'long')
  161. match2 = add_subseed(idx,'short') if self.have_short else False
  162. if match1 or match2:
  163. break
  164. def format(self,first_idx,last_idx):
  165. r = SubSeedIdxRange(first_idx,last_idx)
  166. if len(self) < last_idx:
  167. self._generate(last_idx)
  168. fs1 = '{:>18} {:>18}\n'
  169. fs2 = '{i:>7}L: {:8} {i:>7}S: {:8}\n'
  170. hdr = f' Parent Seed: {self.parent_seed.sid.hl()} ({self.parent_seed.bitlen} bits)\n\n'
  171. hdr += fs1.format('Long Subseeds','Short Subseeds')
  172. hdr += fs1.format('-------------','--------------')
  173. sl = self.data['long'].keys
  174. ss = self.data['short'].keys
  175. body = (fs2.format( sl[n-1], ss[n-1], i=n ) for n in r.iterate())
  176. return hdr + ''.join(body)