fix for 26aaf0cc4
[p2pool.git] / p2pool / web.py
1 import cgi
2 import json
3 import os
4 import time
5 import types
6
7 from twisted.internet import reactor, task
8 from twisted.python import log
9 from twisted.web import resource
10
11 from bitcoin import data as bitcoin_data
12 from . import data as p2pool_data, graphs
13 from util import math
14
15 def get_web_root(tracker, current_work, current_work2, get_current_txouts, datadir_path, net, get_stale_counts, my_pubkey_hash, local_rate_monitor, worker_fee, p2p_node, my_share_hashes, recent_blocks, pseudoshare_received):
16     start_time = time.time()
17     
18     web_root = resource.Resource()
19     
20     def get_rate():
21         if tracker.get_height(current_work.value['best_share_hash']) < 720:
22             return json.dumps(None)
23         return json.dumps(p2pool_data.get_pool_attempts_per_second(tracker, current_work.value['best_share_hash'], 720)
24             / (1 - p2pool_data.get_average_stale_prop(tracker, current_work.value['best_share_hash'], 720)))
25     
26     def get_users():
27         height, last = tracker.get_height_and_last(current_work.value['best_share_hash'])
28         weights, total_weight, donation_weight = tracker.get_cumulative_weights(current_work.value['best_share_hash'], min(height, 720), 65535*2**256)
29         res = {}
30         for script in sorted(weights, key=lambda s: weights[s]):
31             res[bitcoin_data.script2_to_human(script, net.PARENT)] = weights[script]/total_weight
32         return json.dumps(res)
33     
34     def get_current_scaled_txouts(scale, trunc=0):
35         txouts = get_current_txouts()
36         total = sum(txouts.itervalues())
37         results = dict((script, value*scale//total) for script, value in txouts.iteritems())
38         if trunc > 0:
39             total_random = 0
40             random_set = set()
41             for s in sorted(results, key=results.__getitem__):
42                 if results[s] >= trunc:
43                     break
44                 total_random += results[s]
45                 random_set.add(s)
46             if total_random:
47                 winner = math.weighted_choice((script, results[script]) for script in random_set)
48                 for script in random_set:
49                     del results[script]
50                 results[winner] = total_random
51         if sum(results.itervalues()) < int(scale):
52             results[math.weighted_choice(results.iteritems())] += int(scale) - sum(results.itervalues())
53         return results
54     
55     def get_current_payouts():
56         return json.dumps(dict((bitcoin_data.script2_to_human(script, net.PARENT), value/1e8) for script, value in get_current_txouts().iteritems()))
57     
58     def get_patron_sendmany(this):
59         try:
60             if '/' in this:
61                 this, trunc = this.split('/', 1)
62             else:
63                 trunc = '0.01'
64             return json.dumps(dict(
65                 (bitcoin_data.script2_to_address(script, net.PARENT), value/1e8)
66                 for script, value in get_current_scaled_txouts(scale=int(float(this)*1e8), trunc=int(float(trunc)*1e8)).iteritems()
67                 if bitcoin_data.script2_to_address(script, net.PARENT) is not None
68             ))
69         except:
70             log.err()
71             return json.dumps(None)
72     
73     def get_global_stats():
74         # averaged over last hour
75         lookbehind = 3600//net.SHARE_PERIOD
76         if tracker.get_height(current_work.value['best_share_hash']) < lookbehind:
77             return None
78         
79         nonstale_hash_rate = p2pool_data.get_pool_attempts_per_second(tracker, current_work.value['best_share_hash'], lookbehind)
80         stale_prop = p2pool_data.get_average_stale_prop(tracker, current_work.value['best_share_hash'], lookbehind)
81         return json.dumps(dict(
82             pool_nonstale_hash_rate=nonstale_hash_rate,
83             pool_hash_rate=nonstale_hash_rate/(1 - stale_prop),
84             pool_stale_prop=stale_prop,
85         ))
86     
87     def get_local_stats():
88         lookbehind = 3600//net.SHARE_PERIOD
89         if tracker.get_height(current_work.value['best_share_hash']) < lookbehind:
90             return None
91         
92         global_stale_prop = p2pool_data.get_average_stale_prop(tracker, current_work.value['best_share_hash'], lookbehind)
93         
94         my_unstale_count = sum(1 for share in tracker.get_chain(current_work.value['best_share_hash'], lookbehind) if share.hash in my_share_hashes)
95         my_orphan_count = sum(1 for share in tracker.get_chain(current_work.value['best_share_hash'], lookbehind) if share.hash in my_share_hashes and share.share_data['stale_info'] == 253)
96         my_doa_count = sum(1 for share in tracker.get_chain(current_work.value['best_share_hash'], lookbehind) if share.hash in my_share_hashes and share.share_data['stale_info'] == 254)
97         my_share_count = my_unstale_count + my_orphan_count + my_doa_count
98         my_stale_count = my_orphan_count + my_doa_count
99         
100         my_stale_prop = my_stale_count/my_share_count if my_share_count != 0 else None
101         
102         my_work = sum(bitcoin_data.target_to_average_attempts(share.target)
103             for share in tracker.get_chain(current_work.value['best_share_hash'], lookbehind - 1)
104             if share.hash in my_share_hashes)
105         actual_time = (tracker.shares[current_work.value['best_share_hash']].timestamp -
106             tracker.shares[tracker.get_nth_parent_hash(current_work.value['best_share_hash'], lookbehind - 1)].timestamp)
107         share_att_s = my_work / actual_time
108         
109         miner_hash_rates = {}
110         miner_dead_hash_rates = {}
111         datums, dt = local_rate_monitor.get_datums_in_last()
112         for datum in datums:
113             miner_hash_rates[datum['user']] = miner_hash_rates.get(datum['user'], 0) + datum['work']/dt
114             if datum['dead']:
115                 miner_dead_hash_rates[datum['user']] = miner_dead_hash_rates.get(datum['user'], 0) + datum['work']/dt
116         
117         return json.dumps(dict(
118             my_hash_rates_in_last_hour=dict(
119                 note="DEPRECATED",
120                 nonstale=share_att_s,
121                 rewarded=share_att_s/(1 - global_stale_prop),
122                 actual=share_att_s/(1 - my_stale_prop) if my_stale_prop is not None else 0, # 0 because we don't have any shares anyway
123             ),
124             my_share_counts_in_last_hour=dict(
125                 shares=my_share_count,
126                 unstale_shares=my_unstale_count,
127                 stale_shares=my_stale_count,
128                 orphan_stale_shares=my_orphan_count,
129                 doa_stale_shares=my_doa_count,
130             ),
131             my_stale_proportions_in_last_hour=dict(
132                 stale=my_stale_prop,
133                 orphan_stale=my_orphan_count/my_share_count if my_share_count != 0 else None,
134                 dead_stale=my_doa_count/my_share_count if my_share_count != 0 else None,
135             ),
136             miner_hash_rates=miner_hash_rates,
137             miner_dead_hash_rates=miner_dead_hash_rates,
138         ))
139     
140     def get_peer_addresses():
141         return ' '.join(peer.transport.getPeer().host + (':' + str(peer.transport.getPeer().port) if peer.transport.getPeer().port != net.P2P_PORT else '') for peer in p2p_node.peers.itervalues())
142     
143     def get_uptime():
144         return json.dumps(time.time() - start_time)
145     
146     class WebInterface(resource.Resource):
147         def __init__(self, func, mime_type, *fields):
148             self.func, self.mime_type, self.fields = func, mime_type, fields
149         
150         def render_GET(self, request):
151             request.setHeader('Content-Type', self.mime_type)
152             request.setHeader('Access-Control-Allow-Origin', '*')
153             return self.func(*(request.args[field][0] for field in self.fields))
154     
155     web_root.putChild('rate', WebInterface(get_rate, 'application/json'))
156     web_root.putChild('users', WebInterface(get_users, 'application/json'))
157     web_root.putChild('fee', WebInterface(lambda: json.dumps(worker_fee), 'application/json'))
158     web_root.putChild('current_payouts', WebInterface(get_current_payouts, 'application/json'))
159     web_root.putChild('patron_sendmany', WebInterface(get_patron_sendmany, 'text/plain', 'total'))
160     web_root.putChild('global_stats', WebInterface(get_global_stats, 'application/json'))
161     web_root.putChild('local_stats', WebInterface(get_local_stats, 'application/json'))
162     web_root.putChild('peer_addresses', WebInterface(get_peer_addresses, 'text/plain'))
163     web_root.putChild('payout_addr', WebInterface(lambda: json.dumps(bitcoin_data.pubkey_hash_to_address(my_pubkey_hash, net.PARENT)), 'application/json'))
164     web_root.putChild('recent_blocks', WebInterface(lambda: json.dumps(recent_blocks), 'application/json'))
165     web_root.putChild('uptime', WebInterface(get_uptime, 'application/json'))
166     
167     try:
168         from . import draw
169         web_root.putChild('chain_img', WebInterface(lambda: draw.get(tracker, current_work.value['best_share_hash']), 'image/png'))
170     except ImportError:
171         print "Install Pygame and PIL to enable visualizations! Visualizations disabled."
172     
173     new_root = resource.Resource()
174     web_root.putChild('web', new_root)
175     
176     stat_log = []
177     if os.path.exists(os.path.join(datadir_path, 'stats')):
178         try:
179             with open(os.path.join(datadir_path, 'stats'), 'rb') as f:
180                 stat_log = json.loads(f.read())
181         except:
182             log.err(None, 'Error loading stats:')
183     def update_stat_log():
184         while stat_log and stat_log[0]['time'] < time.time() - 24*60*60:
185             stat_log.pop(0)
186         
187         lookbehind = 3600//net.SHARE_PERIOD
188         if tracker.get_height(current_work.value['best_share_hash']) < lookbehind:
189             return None
190         
191         global_stale_prop = p2pool_data.get_average_stale_prop(tracker, current_work.value['best_share_hash'], lookbehind)
192         (stale_orphan_shares, stale_doa_shares), shares, _ = get_stale_counts()
193         
194         miner_hash_rates = {}
195         miner_dead_hash_rates = {}
196         datums, dt = local_rate_monitor.get_datums_in_last()
197         for datum in datums:
198             miner_hash_rates[datum['user']] = miner_hash_rates.get(datum['user'], 0) + datum['work']/dt
199             if datum['dead']:
200                 miner_dead_hash_rates[datum['user']] = miner_dead_hash_rates.get(datum['user'], 0) + datum['work']/dt
201         
202         stat_log.append(dict(
203             time=time.time(),
204             pool_hash_rate=p2pool_data.get_pool_attempts_per_second(tracker, current_work.value['best_share_hash'], lookbehind)/(1-global_stale_prop),
205             pool_stale_prop=global_stale_prop,
206             local_hash_rates=miner_hash_rates,
207             local_dead_hash_rates=miner_dead_hash_rates,
208             shares=shares,
209             stale_shares=stale_orphan_shares + stale_doa_shares,
210             stale_shares_breakdown=dict(orphan=stale_orphan_shares, doa=stale_doa_shares),
211             current_payout=get_current_txouts().get(bitcoin_data.pubkey_hash_to_script2(my_pubkey_hash), 0)*1e-8,
212             peers=dict(
213                 incoming=sum(1 for peer in p2p_node.peers.itervalues() if peer.incoming),
214                 outgoing=sum(1 for peer in p2p_node.peers.itervalues() if not peer.incoming),
215             ),
216             attempts_to_share=bitcoin_data.target_to_average_attempts(tracker.shares[current_work.value['best_share_hash']].max_target),
217             attempts_to_block=bitcoin_data.target_to_average_attempts(current_work.value['bits'].target),
218             block_value=current_work2.value['subsidy']*1e-8,
219         ))
220         
221         with open(os.path.join(datadir_path, 'stats'), 'wb') as f:
222             f.write(json.dumps(stat_log))
223     task.LoopingCall(update_stat_log).start(5*60)
224     new_root.putChild('log', WebInterface(lambda: json.dumps(stat_log), 'application/json'))
225     
226     class ShareExplorer(resource.Resource):
227         def __init__(self, share_hash):
228             self.share_hash = share_hash
229         def render_GET(self, request):
230             request.setHeader('Content-Type', 'text/html')
231             if self.share_hash not in tracker.shares:
232                 return 'share not known'
233             share = tracker.shares[self.share_hash]
234             request.write('<h1>Share</h1>')
235             request.write('<p>Previous: <a href="%x">%s</a></p>' % (share.previous_hash, p2pool_data.format_hash(share.previous_hash)))
236             for next in tracker.reverse_shares.get(share.hash, set()):
237                 request.write('<p>Next: <a href="%x">%s</a></p>' % (next, p2pool_data.format_hash(next)))
238             request.write('<p>Verified: %s</p>' % (share.hash in tracker.verified.shares,))
239             request.write('<ul>')
240             for attr in dir(share):
241                 if attr.startswith('_') or attr == 'previous_hash':
242                     continue
243                 value = getattr(share, attr)
244                 if isinstance(value, types.MethodType):
245                     continue
246                 request.write('<li>%s: %s</li>' % (attr, cgi.escape(repr(value))))
247             request.write('</ul>')
248             return ''
249     class Explorer(resource.Resource):
250         def render_GET(self, request):
251             if not request.path.endswith('/'):
252                 request.redirect(request.path + '/')
253                 return ''
254             request.setHeader('Content-Type', 'text/html')
255             request.write('<h1>P2Pool share explorer</h1>')
256             request.write('<h2>Verified heads</h2>')
257             request.write('<ul>')
258             for head in tracker.heads:
259                 request.write('<li><a href="%x">%s%s</a></li>' % (head, p2pool_data.format_hash(head), ' BEST' if head == current_work.value['best_share_hash'] else ''))
260             request.write('</ul>')
261             return ''
262         def getChild(self, child, request):
263             if not child:
264                 return self
265             return ShareExplorer(int(child, 16))
266     new_root.putChild('explorer', Explorer())
267     
268     grapher = graphs.Grapher(os.path.join(datadir_path, 'rrd'))
269     web_root.putChild('graphs', grapher.get_resource())
270     def add_point():
271         if tracker.get_height(current_work.value['best_share_hash']) < 720:
272             return
273         nonstalerate = p2pool_data.get_pool_attempts_per_second(tracker, current_work.value['best_share_hash'], 720)
274         poolrate = nonstalerate / (1 - p2pool_data.get_average_stale_prop(tracker, current_work.value['best_share_hash'], 720))
275         grapher.add_poolrate_point(poolrate, poolrate - nonstalerate)
276     task.LoopingCall(add_point).start(100)
277     @pseudoshare_received.watch
278     def _(work, dead, user):
279         reactor.callLater(1, grapher.add_localrate_point, work, dead)
280         if user is not None:
281             reactor.callLater(1, grapher.add_localminer_point, user, work, dead)
282
283     
284     return web_root