working on tracker
[p2pool.git] / p2pool / data.py
1 from __future__ import division
2
3 import itertools
4
5 from bitcoin import data as bitcoin_data
6
7 class CompressedList(bitcoin_data.Type):
8     def __init__(self, inner):
9         self.inner = inner
10     
11     def read(self, file):
12         values = bitcoin_data.ListType(self.inner).read(file)
13         if values != sorted(set(values)):
14             raise ValueError("invalid values")
15         references = bitcoin_data.ListType(bitcoin_data.VarIntType()).read(file)
16         return [values[reference] for reference in references]
17     
18     def write(self, file, item):
19         values = sorted(set(item))
20         values_map = dict((value, i) for i, value in enumerate(values))
21         bitcoin_data.ListType(self.inner).write(file, values)
22         bitcoin_data.ListType(bitcoin_data.VarIntType()).write(file, [values_map[subitem] for subitem in item])
23
24
25 merkle_branch_type = bitcoin_data.ListType(bitcoin_data.ComposedType([
26     ('side', bitcoin_data.StructType('<B')), # enum?
27     ('hash', bitcoin_data.HashType()),
28 ]))
29
30
31 share_data_type = bitcoin_data.ComposedType([
32     ('previous_share_hash', bitcoin_data.PossiblyNone(0, bitcoin_data.HashType())),
33     ('previous_shares_hash', bitcoin_data.HashType()),
34     ('target2', bitcoin_data.FloatingIntegerType()),
35     ('nonce', bitcoin_data.VarStrType()),
36 ])
37
38
39 coinbase_type = bitcoin_data.ComposedType([
40     ('identifier', bitcoin_data.FixedStrType(8)),
41     ('share_data', share_data_type),
42 ])
43
44 share_info_type = bitcoin_data.ComposedType([
45     ('share_data', share_data_type),
46     ('new_script', bitcoin_data.VarStrType()),
47     ('subsidy', bitcoin_data.StructType('<Q')),
48 ])
49
50
51 share1a_type = bitcoin_data.ComposedType([
52     ('header', bitcoin_data.block_header_type), # merkle_header not completely needed
53     ('share_info', share_info_type),
54     ('merkle_branch', merkle_branch_type),
55 ])
56
57 share1b_type = bitcoin_data.ComposedType([
58     ('header', bitcoin_data.block_header_type),
59     ('share_info', share_info_type),
60     ('other_txs', bitcoin_data.ListType(bitcoin_data.tx_type)),
61 ])
62
63 shares_type = CompressedList(bitcoin_data.VarStrType())
64
65 def calculate_merkle_branch(txs, index):
66     hash_list = [(bitcoin_data.tx_type.hash256(tx), i == index, []) for i, tx in enumerate(txs)]
67     
68     while len(hash_list) > 1:
69         hash_list = [
70             (
71                 bitcoin_data.merkle_record_type.hash256(dict(left=left, right=right)),
72                 left_f or right_f,
73                 (left_l if left_f else right_l) + [dict(side=1, hash=right) if left_f else dict(side=0, hash=left)],
74             )
75             for (left, left_f, left_l), (right, right_f, right_l) in
76                 zip(hash_list[::2], hash_list[1::2] + [hash_list[::2][-1]])
77         ]
78     
79     assert hash_list[0][1]
80     assert check_merkle_branch(txs[index], hash_list[0][2]) == hash_list[0][0]
81     
82     return hash_list[0][2]
83
84 def check_merkle_branch(tx, branch):
85     hash_ = bitcoin_data.tx_type.hash256(tx)
86     for step in branch:
87         if not step['side']:
88             hash_ = bitcoin_data.merkle_record_type.hash256(dict(left=step['hash'], right=hash_))
89         else:
90             hash_ = bitcoin_data.merkle_record_type.hash256(dict(left=hash_, right=step['hash']))
91     return hash_
92
93 def gentx_to_share_info(gentx):
94     return dict(
95         share_data=coinbase_type.unpack(gentx['tx_ins'][0]['script'])['share_data'],
96         subsidy=sum(tx_out['value'] for tx_out in gentx['tx_outs']),
97         new_script=gentx['tx_outs'][-1]['script'],
98     )
99
100 def share_info_to_gentx(share_info, chain, net):
101     return generate_transaction(
102         previous_share2=chain.share2s[share_info['share_data']['previous_share_hash']],
103         nonce=share_info['share_data']['nonce'],
104         new_script=share_info['new_script'],
105         subsidy=share_info['subsidy'],
106         net=net,
107     )
108
109 class Share(object):
110     @classmethod
111     def from_block(cls, block):
112         return cls(block['header'], gentx_to_share_info(block['txs'][0]), other_txs=block['txs'][1:])
113     
114     @classmethod
115     def from_share1a(cls, share1a):
116         return cls(**share1a)
117     
118     @classmethod
119     def from_share1b(cls, share1b):
120         return cls(**share1b)
121     
122     def __init__(self, header, share_info, merkle_branch=None, other_txs=None):
123         if merkle_branch is None and other_txs is None:
124             raise ValueError('need either merkle_branch or other_txs')
125         
126         self.header = header
127         self.share_info = share_info
128         self.merkle_branch = merkle_branch
129         self.other_txs = other_txs
130         
131         self.share_data = self.share_info['share_data']
132         self.new_script = self.share_info['new_script']
133         self.subsidy = self.share_info['subsidy']
134         
135         self.previous_share_hash = self.share_data['previous_share_hash']
136         self.previous_shares_hash = self.share_data['previous_shares_hash']
137         self.target2 = self.share_data['target2']
138         
139         self.hash = bitcoin_data.block_header_type.hash256(header)
140         
141         if self.hash > self.target2:
142             raise ValueError('not enough work!')
143     
144     def as_block(self):
145         if self.txs is None:
146             raise ValueError('share does not contain all txs')
147         
148         return dict(header=self.header, txs=self.txs)
149     
150     def as_share1a(self):
151         return dict(header=self.header, share_info=self.share_info, merkle_branch=self.merkle_branch)
152     
153     def as_share1b(self):
154         return dict(header=self.header, share_info=self.share_info, other_txs=self.other_txs)
155     
156     def check(self, tracker, net):
157         gentx = share_info_to_gentx(self.share_info, tracker, net)
158         
159         if self.merkle_branch is not None:
160             if check_merkle_branch(gentx, self.merkle_branch) != self.header['merkle_root']:
161                 raise ValueError("gentx doesn't match header via merkle_branch")
162         
163         if self.other_txs is not None:
164             if bitcoin_data.merkle_hash([gentx] + self.other_txs) != self.header['merkle_root']:
165                 raise ValueError("gentx doesn't match header via other_txs")
166         
167         return Share2(self)
168
169 class Share2(object):
170     '''Share with associated data'''
171     
172     def __init__(self, share):
173         self.share = share
174         
175         self.shared = False
176     
177     def flag_shared(self):
178         self.shared = True
179
180 def generate_transaction(tracker, previous_share_hash, new_script, subsidy, nonce, block_target, net):
181     previous_share2 = tracker.shares[previous_share_hash] if previous_share_hash is not None else None
182     #previous_share2 = chain.shares
183     #previous_shares
184     #shares = 
185     #shares = (previous_share2.shares if previous_share2 is not None else [net.SCRIPT]*net.SPREAD)[1:-1] + [new_script, new_script]
186     
187     chain = list(itertools.islice(tracker.get_chain(previous_share_hash), net.CHAIN_LENGTH))
188     if len(chain) < 100:
189         target2 = bitcoin_data.FloatingIntegerType().truncate_to(2**256//2**32 - 1)
190     else:
191         attempts_per_second = sum(bitcoin_data.target_to_average_attempts(share.target) for share in itertools.islice(chain, 0, max(0, len(chain) - 1)))//(chain[0].timestamp - chain[-1].timestamp)
192         pre_target = 2**256*net.SHARE_PERIOD//attempts_per_second
193         pre_target2 = math.clip(pre_target, (previous_share2.target*9//10, previous_share2.target*11//10))
194         pre_target3 = math.clip(pre_target2, (0, 2**256//2**32 - 1))
195         target2 = bitcoin_data.FloatingIntegerType().truncate_to(pre_target3)
196     
197     
198     attempts_to_block = bitcoin_data.target_to_average_attempts(block_target)
199     total_weight = 0
200     
201     class fake_share(object):
202         script = new_script
203         share = dict(target=target2)
204     
205     dest_weights = {}
206     for i, share in enumerate(itertools.chain([fake_share], itertools.islice(tracker.get_chain(previous_share_hash), net.CHAIN_LENGTH))):
207         weight = bitcoin_data.target_to_average_attempts(share.share['target'])
208         weight = max(weight, attempts_to_block - total_weight)
209         
210         dest_weights[share.script] = dest_weights.get(share.script, 0) + weight
211         total_weight += weight
212         
213         if total_weight == attempts_to_block:
214             break
215     assert total_weight == attempts_to_block or i == net.CHAIN_LENGTH + 1
216     
217     amounts = dict((script, subsidy*(199*weight)//(200*total_weight)) for (script, weight) in dest_weights.iteritems())
218     amounts[net.SCRIPT] = amounts.get(net.SCRIPT, 0) + subsidy*1//200 # prevent fake previous p2pool blocks
219     amounts[net.SCRIPT] = amounts.get(net.SCRIPT, 0) + subsidy - sum(amounts.itervalues()) # collect any extra
220     
221     dests = sorted(amounts.iterkeys(), key=lambda script: (script == new_script, script))
222     assert dests[-1] == new_script
223     
224     previous_shares = [] # XXX
225     
226     return dict(
227         version=1,
228         tx_ins=[dict(
229             previous_output=None,
230             sequence=None,
231             script=coinbase_type.pack(dict(
232                 identifier=net.IDENTIFIER,
233                 share_data=dict(
234                     previous_share_hash=previous_share_hash,
235                     previous_shares_hash=shares_type.hash256(previous_shares),
236                     nonce=nonce,
237                     target2=target2,
238                 ),
239             )),
240         )],
241         tx_outs=[dict(value=amounts[script], script=script) for script in dests if amounts[script]],
242         lock_time=0,
243     )
244
245
246 class Tracker(object):
247     def __init__(self, net):
248         self.net = net
249         
250         self.shares = {} # hash -> share
251         self.reverse_shares = {} # previous_share_hash -> share_hash
252         
253         self.heads = {} # head hash -> tail_hash
254         self.tails = {} # tail hash -> set of head hashes
255         self.heights = {} # share_hash -> height_to, other_share_hash
256     
257     def add_share(self, share):
258         if share.hash in self.shares:
259             return # XXX raise exception?
260         
261         self.shares[share.hash] = share
262         self.reverse_shares.setdefault(share.previous_share_hash, set()).add(share.hash)
263         
264         if share.hash in self.tails:
265             heads = self.tails.pop(share.hash)
266         else:
267             heads = set([share.hash])
268         
269         if share.previous_share_hash in self.heads:
270             tail = self.heads.pop(share.previous_share_hash)
271         else:
272             tail = share.previous_share_hash
273         
274         self.tails.setdefault(tail, set()).update(heads)
275         if share.previous_share_hash in self.tails[tail]:
276             self.tails[tail].remove(share.previous_share_hash)
277         
278         for head in heads:
279             self.heads[head] = tail
280     
281     def get_height_and_last(self, share_hash):
282         height = 0
283         updates = []
284         while True:
285             if share_hash is None or share_hash not in self.shares:
286                 break
287             updates.append((share_hash, height))
288             if share_hash in self.heights:
289                 height_inc, share_hash = self.heights[share_hash]
290             else:
291                 height_inc, share_hash = 1, self.shares[share_hash].previous_share_hash
292             height += height_inc
293         for update_hash, height_then in updates:
294             self.heights[update_hash] = height - height_then, share_hash
295         return height, share_hash
296     
297     def get_chain(self, start):
298         share_hash_to_get = start
299         while share_hash_to_get in self.shares:
300             share = self.shares[share_hash_to_get]
301             yield share
302             share_hash_to_get = share.previous_share_hash
303     
304     '''
305     def get_best_share_hash(self):
306         return max(self.heads, key=self.score_chain)
307     
308     def score_chain(self, start):
309         length = len(self.get_chain(start))
310         
311         score = 0
312         for share in itertools.islice(self.get_chain(start), self.net.CHAIN_LENGTH):
313             score += a
314         
315         return (min(length, 1000), score)
316     '''
317
318 if __name__ == '__main__':
319     class FakeShare(object):
320         def __init__(self, hash, previous_share_hash):
321             self.hash = hash
322             self.previous_share_hash = previous_share_hash
323     
324     t = Tracker()
325     
326     t.add_share(FakeShare(1, 2))
327     print t.heads, t.tails
328     t.add_share(FakeShare(4, 0))
329     print t.heads, t.tails
330     t.add_share(FakeShare(3, 4))
331     print t.heads, t.tails
332     t.add_share(FakeShare(5, 0))
333     print t.heads, t.tails
334     t.add_share(FakeShare(0, 1))
335     print t.heads, t.tails
336     
337     for share_hash in t.shares:
338         print share_hash, t.get_height_and_last(share_hash)
339
340 class OkayTracker(Tracker):
341     def __init__(self, net):
342         Tracker.__init__(self, net)
343     """
344         self.okay_cache = {} # hash -> height
345     
346     def is_okay(self, start, _height_after=0):
347         '''
348         Returns:
349             {'result': 'okay', verified_height: ...} # if share has an okay parent or if share has CHAIN_LENGTH children and CHAIN_LENTH parents that it verified with
350             {'result': 'needs_share', 'share_hash': ...} # if share doesn't have CHAIN_LENGTH parents
351             #{'result': 'needs_share_shares', 'share_hash': ...} # if share has CHAIN_LENGTH children and needs its shares to 
352             {'result': 'not_okay'} # if the share has a not okay parent or if the share has an okay parent and failed validation
353         '''
354         
355         if start in self.okay_cache:
356             return dict(result='okay', verified_height=self.okay_cache['start'])
357         
358         share = self.shares[start]
359         if start not in self.shares:
360             return dict(result='needs_share', share_hash=start)
361         
362         length = len
363         to_end_rev = []
364         for share in itertools.islice(self.get_chain(start), self.net.CHAIN_LENGTH):
365             if share in self.okay_cache:
366                 return validate(share, to_end_rev[::-1])
367             to_end_rev.append(share)
368         # picking up last share from for loop, ew
369         self.okay_cache.add(share)
370         return validate(share, to_end_rev[::-1])
371     """
372     def think(self):
373         desired = set()
374         for head in self.heads:
375             height, last = self.get_height(head)
376             if last is not None and height < 2*self.net.CHAIN_LENGTH:
377                 desired.add(last)
378                 continue
379             first_to_verify = math.nth(self.get_chain(head), self.net.CHAIN_LENGTH - 1)
380             to_verify = []
381             for share_hash in self.get_chain(head):
382                 if share_hash not in self.verified:
383                     to_verify.append(share_hash)
384             for share_hash in reversed(to_verify):
385                 try:
386                     share.check(self, self.net)
387                 except:
388                     print
389                     print "Share check failed:"
390                     traceback.print_exc()
391                     print
392                     break
393             else:
394                 a
395         return desired
396
397 class Chain(object):
398     def __init__(self):
399         pass
400
401 def get_chain_descriptor(tracker, start):
402     for item in tracker.get_chain(self.net.CHAIN_LENGTH):
403         a
404     pass
405
406
407 class Mainnet(bitcoin_data.Mainnet):
408     SHARE_PERIOD = 5 # seconds
409     CHAIN_LENGTH = 24*60*60//5 # shares
410     SPREAD = 3 # blocks
411     SCRIPT = '4104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac'.decode('hex')
412     IDENTIFIER = '7452839666e1f8f8'.decode('hex')
413     PREFIX = '2d4224bf18c87b87'.decode('hex')
414     ADDRS_TABLE = 'addrs'
415     P2P_PORT = 9333
416
417 class Testnet(bitcoin_data.Testnet):
418     SHARE_PERIOD = 5 # seconds
419     CHAIN_LENGTH = 24*60*60//5 # shares
420     SPREAD = 3 # blocks
421     SCRIPT = '410403ad3dee8ab3d8a9ce5dd2abfbe7364ccd9413df1d279bf1a207849310465b0956e5904b1155ecd17574778f9949589ebfd4fb33ce837c241474a225cf08d85dac'.decode('hex')
422     IDENTIFIER = '1ae3479e4eb6700a'.decode('hex')
423     PREFIX = 'd19778c812754854'.decode('hex')
424     ADDRS_TABLE = 'addrs_testnet'
425     P2P_PORT = 19333