1 from __future__ import division
13 if '--iocp' in sys.argv:
14 from twisted.internet import iocpreactor
16 from twisted.internet import defer, reactor, protocol, task
17 from twisted.web import server
18 from twisted.python import log
19 from nattraverso import portmapper, ipdiscover
21 import bitcoin.p2p as bitcoin_p2p, bitcoin.data as bitcoin_data
22 from bitcoin import worker_interface, helper
23 from util import fixargparse, jsonrpc, variable, deferral, math, logging
24 from . import networks, web, work
25 import p2pool, p2pool.data as p2pool_data, p2pool.node as p2pool_node
27 @defer.inlineCallbacks
28 def main(args, net, datadir_path, merged_urls, worker_endpoint):
30 print 'p2pool (version %s)' % (p2pool.__version__,)
33 @defer.inlineCallbacks
35 # connect to bitcoind over bitcoin-p2p
36 print '''Testing bitcoind P2P connection to '%s:%s'...''' % (args.bitcoind_address, args.bitcoind_p2p_port)
37 factory = bitcoin_p2p.ClientFactory(net.PARENT)
38 reactor.connectTCP(args.bitcoind_address, args.bitcoind_p2p_port, factory)
39 yield factory.getProtocol() # waits until handshake is successful
42 defer.returnValue(factory)
44 if args.testnet: # establish p2p connection first if testnet so bitcoind can work without connections
45 factory = yield connect_p2p()
47 # connect to bitcoind over JSON-RPC and do initial getmemorypool
48 url = '%s://%s:%i/' % ('https' if args.bitcoind_rpc_ssl else 'http', args.bitcoind_address, args.bitcoind_rpc_port)
49 print '''Testing bitcoind RPC connection to '%s' with username '%s'...''' % (url, args.bitcoind_rpc_username)
50 bitcoind = jsonrpc.Proxy(url, dict(Authorization='Basic ' + base64.b64encode(args.bitcoind_rpc_username + ':' + args.bitcoind_rpc_password)), timeout=30)
51 yield helper.check(bitcoind, net)
52 temp_work = yield helper.getwork(bitcoind)
54 bitcoind_warning_var = variable.Variable(None)
55 @defer.inlineCallbacks
57 errors = (yield deferral.retry('Error while calling getmininginfo:')(bitcoind.rpc_getmininginfo)())['errors']
58 bitcoind_warning_var.set(errors if errors != '' else None)
60 task.LoopingCall(poll_warnings).start(20*60)
63 print ' Current block hash: %x' % (temp_work['previous_block'],)
64 print ' Current block height: %i' % (temp_work['height'] - 1,)
68 factory = yield connect_p2p()
70 print 'Determining payout address...'
71 if args.pubkey_hash is None:
72 address_path = os.path.join(datadir_path, 'cached_payout_address')
74 if os.path.exists(address_path):
75 with open(address_path, 'rb') as f:
76 address = f.read().strip('\r\n')
77 print ' Loaded cached address: %s...' % (address,)
81 if address is not None:
82 res = yield deferral.retry('Error validating cached address:', 5)(lambda: bitcoind.rpc_validateaddress(address))()
83 if not res['isvalid'] or not res['ismine']:
84 print ' Cached address is either invalid or not controlled by local bitcoind!'
88 print ' Getting payout address from bitcoind...'
89 address = yield deferral.retry('Error getting payout address from bitcoind:', 5)(lambda: bitcoind.rpc_getaccountaddress('p2pool'))()
91 with open(address_path, 'wb') as f:
94 my_pubkey_hash = bitcoin_data.address_to_pubkey_hash(address, net.PARENT)
96 my_pubkey_hash = args.pubkey_hash
97 print ' ...success! Payout address:', bitcoin_data.pubkey_hash_to_address(my_pubkey_hash, net.PARENT)
100 ss = p2pool_data.ShareStore(os.path.join(datadir_path, 'shares.'), net)
102 known_verified = set()
103 print "Loading shares..."
104 for i, (mode, contents) in enumerate(ss.get_shares()):
106 contents.time_seen = 0
107 shares[contents.hash] = contents
108 if len(shares) % 1000 == 0 and shares:
109 print " %i" % (len(shares),)
110 elif mode == 'verified_hash':
111 known_verified.add(contents)
113 raise AssertionError()
114 print " ...done loading %i shares (%i verified)!" % (len(shares), len(known_verified))
118 print 'Initializing work...'
120 node = p2pool_node.Node(factory, bitcoind, shares.values(), known_verified, net)
123 for share_hash in shares:
124 if share_hash not in node.tracker.items:
125 ss.forget_share(share_hash)
126 for share_hash in known_verified:
127 if share_hash not in node.tracker.verified.items:
128 ss.forget_verified_share(share_hash)
129 del shares, known_verified
130 node.tracker.removed.watch(lambda share: ss.forget_share(share.hash))
131 node.tracker.verified.removed.watch(lambda share: ss.forget_verified_share(share.hash))
134 for share in node.tracker.get_chain(node.best_share_var.value, min(node.tracker.get_height(node.best_share_var.value), 2*net.CHAIN_LENGTH)):
136 if share.hash in node.tracker.verified.items:
137 ss.add_verified_hash(share.hash)
138 task.LoopingCall(save_shares).start(60)
144 print 'Joining p2pool network using port %i...' % (args.p2pool_port,)
146 @defer.inlineCallbacks
149 ip, port = x.split(':')
150 defer.returnValue(((yield reactor.resolve(ip)), int(port)))
152 defer.returnValue(((yield reactor.resolve(x)), net.P2P_PORT))
155 if os.path.exists(os.path.join(datadir_path, 'addrs')):
157 with open(os.path.join(datadir_path, 'addrs'), 'rb') as f:
158 addrs.update(dict((tuple(k), v) for k, v in json.loads(f.read())))
160 print >>sys.stderr, 'error parsing addrs'
161 for addr_df in map(parse, net.BOOTSTRAP_ADDRS):
164 if addr not in addrs:
165 addrs[addr] = (0, time.time(), time.time())
169 connect_addrs = set()
170 for addr_df in map(parse, args.p2pool_nodes):
172 connect_addrs.add((yield addr_df))
176 node.p2p_node = p2pool_node.P2PNode(node, args.p2pool_port, args.p2pool_conns, addrs, connect_addrs)
177 node.p2p_node.start()
180 with open(os.path.join(datadir_path, 'addrs'), 'wb') as f:
181 f.write(json.dumps(node.p2p_node.addr_store.items()))
182 task.LoopingCall(save_addrs).start(60)
188 @defer.inlineCallbacks
192 is_lan, lan_ip = yield ipdiscover.get_local_ip()
194 pm = yield portmapper.get_port_mapper()
195 yield pm._upnp.add_port_mapping(lan_ip, args.p2pool_port, args.p2pool_port, 'p2pool', 'TCP')
196 except defer.TimeoutError:
200 log.err(None, 'UPnP error:')
201 yield deferral.sleep(random.expovariate(1/120))
204 # start listening for workers with a JSON-RPC server
206 print 'Listening for workers on %r port %i...' % (worker_endpoint[0], worker_endpoint[1])
208 wb = work.WorkerBridge(node, my_pubkey_hash, args.donation_percentage, merged_urls, args.worker_fee)
209 web_root = web.get_web_root(wb, datadir_path, bitcoind_warning_var)
210 worker_interface.WorkerInterface(wb).attach_to(web_root, get_handler=lambda request: request.redirect('/static/'))
212 deferral.retry('Error binding to worker port:', traceback=False)(reactor.listenTCP)(worker_endpoint[1], server.Site(web_root), interface=worker_endpoint[0])
214 with open(os.path.join(os.path.join(datadir_path, 'ready_flag')), 'wb') as f:
222 print 'Started successfully!'
223 print 'Go to http://127.0.0.1:%i/ to view graphs and statistics!' % (worker_endpoint[1],)
224 if args.donation_percentage > 0.51:
225 print '''Donating %.1f%% of work towards P2Pool's development. Thanks for the tip!''' % (args.donation_percentage,)
226 elif args.donation_percentage < 0.49:
227 print '''Donating %.1f%% of work towards P2Pool's development. Please donate to encourage further development of P2Pool!''' % (args.donation_percentage,)
229 print '''Donating %.1f%% of work towards P2Pool's development. Thank you!''' % (args.donation_percentage,)
230 print 'You can increase this amount with --give-author argument! (or decrease it, if you must)'
234 if hasattr(signal, 'SIGALRM'):
235 signal.signal(signal.SIGALRM, lambda signum, frame: reactor.callFromThread(
236 sys.stderr.write, 'Watchdog timer went off at:\n' + ''.join(traceback.format_stack())
238 signal.siginterrupt(signal.SIGALRM, False)
239 task.LoopingCall(signal.alarm, 30).start(1)
241 if args.irc_announce:
242 from twisted.words.protocols import irc
243 class IRCClient(irc.IRCClient):
244 nickname = 'p2pool%02i' % (random.randrange(100),)
245 channel = net.ANNOUNCE_CHANNEL
246 def lineReceived(self, line):
249 irc.IRCClient.lineReceived(self, line)
251 self.in_channel = False
252 irc.IRCClient.signedOn(self)
253 self.factory.resetDelay()
254 self.join(self.channel)
255 @defer.inlineCallbacks
256 def new_share(share):
257 if not self.in_channel:
259 if share.pow_hash <= share.header['bits'].target and abs(share.timestamp - time.time()) < 10*60:
260 yield deferral.sleep(random.expovariate(1/60))
261 message = '\x02%s BLOCK FOUND by %s! %s%064x' % (net.NAME.upper(), bitcoin_data.script2_to_address(share.new_script, net.PARENT), net.PARENT.BLOCK_EXPLORER_URL_PREFIX, share.header_hash)
262 if all('%x' % (share.header_hash,) not in old_message for old_message in self.recent_messages):
263 self.say(self.channel, message)
264 self._remember_message(message)
265 self.watch_id = node.tracker.verified.added.watch(new_share)
266 self.recent_messages = []
267 def joined(self, channel):
268 self.in_channel = True
269 def left(self, channel):
270 self.in_channel = False
271 def _remember_message(self, message):
272 self.recent_messages.append(message)
273 while len(self.recent_messages) > 100:
274 self.recent_messages.pop(0)
275 def privmsg(self, user, channel, message):
276 if channel == self.channel:
277 self._remember_message(message)
278 def connectionLost(self, reason):
279 node.tracker.verified.added.unwatch(self.watch_id)
280 print 'IRC connection lost:', reason.getErrorMessage()
281 class IRCClientFactory(protocol.ReconnectingClientFactory):
283 reactor.connectTCP("irc.freenode.net", 6667, IRCClientFactory())
285 @defer.inlineCallbacks
290 yield deferral.sleep(3)
292 height = node.tracker.get_height(node.best_share_var.value)
293 this_str = 'P2Pool: %i shares in chain (%i verified/%i total) Peers: %i (%i incoming)' % (
295 len(node.tracker.verified.items),
296 len(node.tracker.items),
297 len(node.p2p_node.peers),
298 sum(1 for peer in node.p2p_node.peers.itervalues() if peer.incoming),
299 ) + (' FDs: %i R/%i W' % (len(reactor.getReaders()), len(reactor.getWriters())) if p2pool.DEBUG else '')
301 datums, dt = wb.local_rate_monitor.get_datums_in_last()
302 my_att_s = sum(datum['work']/dt for datum in datums)
303 this_str += '\n Local: %sH/s in last %s Local dead on arrival: %s Expected time to share: %s' % (
304 math.format(int(my_att_s)),
306 math.format_binomial_conf(sum(1 for datum in datums if datum['dead']), len(datums), 0.95),
307 math.format_dt(2**256 / node.tracker.items[node.best_share_var.value].max_target / my_att_s) if my_att_s and node.best_share_var.value else '???',
311 (stale_orphan_shares, stale_doa_shares), shares, _ = wb.get_stale_counts()
312 stale_prop = p2pool_data.get_average_stale_prop(node.tracker, node.best_share_var.value, min(60*60//net.SHARE_PERIOD, height))
313 real_att_s = p2pool_data.get_pool_attempts_per_second(node.tracker, node.best_share_var.value, min(height - 1, 60*60//net.SHARE_PERIOD)) / (1 - stale_prop)
315 this_str += '\n Shares: %i (%i orphan, %i dead) Stale rate: %s Efficiency: %s Current payout: %.4f %s' % (
316 shares, stale_orphan_shares, stale_doa_shares,
317 math.format_binomial_conf(stale_orphan_shares + stale_doa_shares, shares, 0.95),
318 math.format_binomial_conf(stale_orphan_shares + stale_doa_shares, shares, 0.95, lambda x: (1 - x)/(1 - stale_prop)),
319 node.get_current_txouts().get(bitcoin_data.pubkey_hash_to_script2(my_pubkey_hash), 0)*1e-8, net.PARENT.SYMBOL,
321 this_str += '\n Pool: %sH/s Stale rate: %.1f%% Expected time to block: %s' % (
322 math.format(int(real_att_s)),
324 math.format_dt(2**256 / node.bitcoind_work.value['bits'].target / real_att_s),
327 for warning in p2pool_data.get_warnings(node.tracker, node.best_share_var.value, net, bitcoind_warning_var.value, node.bitcoind_work.value):
328 print >>sys.stderr, '#'*40
329 print >>sys.stderr, '>>> Warning: ' + warning
330 print >>sys.stderr, '#'*40
332 if this_str != last_str or time.time() > last_time + 15:
335 last_time = time.time()
341 log.err(None, 'Fatal error:')
344 realnets = dict((name, net) for name, net in networks.nets.iteritems() if '_testnet' not in name)
346 parser = fixargparse.FixedArgumentParser(description='p2pool (version %s)' % (p2pool.__version__,), fromfile_prefix_chars='@')
347 parser.add_argument('--version', action='version', version=p2pool.__version__)
348 parser.add_argument('--net',
349 help='use specified network (default: bitcoin)',
350 action='store', choices=sorted(realnets), default='bitcoin', dest='net_name')
351 parser.add_argument('--testnet',
352 help='''use the network's testnet''',
353 action='store_const', const=True, default=False, dest='testnet')
354 parser.add_argument('--debug',
355 help='enable debugging mode',
356 action='store_const', const=True, default=False, dest='debug')
357 parser.add_argument('-a', '--address',
358 help='generate payouts to this address (default: <address requested from bitcoind>)',
359 type=str, action='store', default=None, dest='address')
360 parser.add_argument('--datadir',
361 help='store data in this directory (default: <directory run_p2pool.py is in>/data)',
362 type=str, action='store', default=None, dest='datadir')
363 parser.add_argument('--logfile',
364 help='''log to this file (default: data/<NET>/log)''',
365 type=str, action='store', default=None, dest='logfile')
366 parser.add_argument('--merged',
367 help='call getauxblock on this url to get work for merged mining (example: http://ncuser:ncpass@127.0.0.1:10332/)',
368 type=str, action='append', default=[], dest='merged_urls')
369 parser.add_argument('--give-author', metavar='DONATION_PERCENTAGE',
370 help='donate this percentage of work towards the development of p2pool (default: 0.5)',
371 type=float, action='store', default=0.5, dest='donation_percentage')
372 parser.add_argument('--iocp',
373 help='use Windows IOCP API in order to avoid errors due to large number of sockets being open',
374 action='store_true', default=False, dest='iocp')
375 parser.add_argument('--irc-announce',
376 help='announce any blocks found on irc://irc.freenode.net/#p2pool',
377 action='store_true', default=False, dest='irc_announce')
378 parser.add_argument('--no-bugreport',
379 help='disable submitting caught exceptions to the author',
380 action='store_true', default=False, dest='no_bugreport')
382 p2pool_group = parser.add_argument_group('p2pool interface')
383 p2pool_group.add_argument('--p2pool-port', metavar='PORT',
384 help='use port PORT to listen for connections (forward this port from your router!) (default: %s)' % ', '.join('%s:%i' % (name, net.P2P_PORT) for name, net in sorted(realnets.items())),
385 type=int, action='store', default=None, dest='p2pool_port')
386 p2pool_group.add_argument('-n', '--p2pool-node', metavar='ADDR[:PORT]',
387 help='connect to existing p2pool node at ADDR listening on port PORT (defaults to default p2pool P2P port) in addition to builtin addresses',
388 type=str, action='append', default=[], dest='p2pool_nodes')
389 parser.add_argument('--disable-upnp',
390 help='''don't attempt to use UPnP to forward p2pool's P2P port from the Internet to this computer''',
391 action='store_false', default=True, dest='upnp')
392 p2pool_group.add_argument('--max-conns', metavar='CONNS',
393 help='maximum incoming connections (default: 40)',
394 type=int, action='store', default=40, dest='p2pool_conns')
396 worker_group = parser.add_argument_group('worker interface')
397 worker_group.add_argument('-w', '--worker-port', metavar='PORT or ADDR:PORT',
398 help='listen on PORT on interface with ADDR for RPC connections from miners (default: all interfaces, %s)' % ', '.join('%s:%i' % (name, net.WORKER_PORT) for name, net in sorted(realnets.items())),
399 type=str, action='store', default=None, dest='worker_endpoint')
400 worker_group.add_argument('-f', '--fee', metavar='FEE_PERCENTAGE',
401 help='''charge workers mining to their own bitcoin address (by setting their miner's username to a bitcoin address) this percentage fee to mine on your p2pool instance. Amount displayed at http://127.0.0.1:WORKER_PORT/fee (default: 0)''',
402 type=float, action='store', default=0, dest='worker_fee')
404 bitcoind_group = parser.add_argument_group('bitcoind interface')
405 bitcoind_group.add_argument('--bitcoind-address', metavar='BITCOIND_ADDRESS',
406 help='connect to this address (default: 127.0.0.1)',
407 type=str, action='store', default='127.0.0.1', dest='bitcoind_address')
408 bitcoind_group.add_argument('--bitcoind-rpc-port', metavar='BITCOIND_RPC_PORT',
409 help='''connect to JSON-RPC interface at this port (default: %s <read from bitcoin.conf if password not provided>)''' % ', '.join('%s:%i' % (name, net.PARENT.RPC_PORT) for name, net in sorted(realnets.items())),
410 type=int, action='store', default=None, dest='bitcoind_rpc_port')
411 bitcoind_group.add_argument('--bitcoind-rpc-ssl',
412 help='connect to JSON-RPC interface using SSL',
413 action='store_true', default=False, dest='bitcoind_rpc_ssl')
414 bitcoind_group.add_argument('--bitcoind-p2p-port', metavar='BITCOIND_P2P_PORT',
415 help='''connect to P2P interface at this port (default: %s <read from bitcoin.conf if password not provided>)''' % ', '.join('%s:%i' % (name, net.PARENT.P2P_PORT) for name, net in sorted(realnets.items())),
416 type=int, action='store', default=None, dest='bitcoind_p2p_port')
418 bitcoind_group.add_argument(metavar='BITCOIND_RPCUSERPASS',
419 help='bitcoind RPC interface username, then password, space-separated (only one being provided will cause the username to default to being empty, and none will cause P2Pool to read them from bitcoin.conf)',
420 type=str, action='store', default=[], nargs='*', dest='bitcoind_rpc_userpass')
422 args = parser.parse_args()
426 defer.setDebugging(True)
428 net_name = args.net_name + ('_testnet' if args.testnet else '')
429 net = networks.nets[net_name]
431 datadir_path = os.path.join((os.path.join(os.path.dirname(sys.argv[0]), 'data') if args.datadir is None else args.datadir), net_name)
432 if not os.path.exists(datadir_path):
433 os.makedirs(datadir_path)
435 if len(args.bitcoind_rpc_userpass) > 2:
436 parser.error('a maximum of two arguments are allowed')
437 args.bitcoind_rpc_username, args.bitcoind_rpc_password = ([None, None] + args.bitcoind_rpc_userpass)[-2:]
439 if args.bitcoind_rpc_password is None:
440 conf_path = net.PARENT.CONF_FILE_FUNC()
441 if not os.path.exists(conf_path):
442 parser.error('''Bitcoin configuration file not found. Manually enter your RPC password.\r\n'''
443 '''If you actually haven't created a configuration file, you should create one at %s with the text:\r\n'''
446 '''rpcpassword=%x\r\n'''
448 '''Keep that password secret! After creating the file, restart Bitcoin.''' % (conf_path, random.randrange(2**128)))
449 conf = open(conf_path, 'rb').read()
451 for line in conf.splitlines(True):
453 line = line[:line.index('#')]
456 k, v = line.split('=', 1)
457 contents[k.strip()] = v.strip()
458 for conf_name, var_name, var_type in [
459 ('rpcuser', 'bitcoind_rpc_username', str),
460 ('rpcpassword', 'bitcoind_rpc_password', str),
461 ('rpcport', 'bitcoind_rpc_port', int),
462 ('port', 'bitcoind_p2p_port', int),
464 if getattr(args, var_name) is None and conf_name in contents:
465 setattr(args, var_name, var_type(contents[conf_name]))
466 if args.bitcoind_rpc_password is None:
467 parser.error('''Bitcoin configuration file didn't contain an rpcpassword= line! Add one!''')
469 if args.bitcoind_rpc_username is None:
470 args.bitcoind_rpc_username = ''
472 if args.bitcoind_rpc_port is None:
473 args.bitcoind_rpc_port = net.PARENT.RPC_PORT
475 if args.bitcoind_p2p_port is None:
476 args.bitcoind_p2p_port = net.PARENT.P2P_PORT
478 if args.p2pool_port is None:
479 args.p2pool_port = net.P2P_PORT
481 if args.worker_endpoint is None:
482 worker_endpoint = '', net.WORKER_PORT
483 elif ':' not in args.worker_endpoint:
484 worker_endpoint = '', int(args.worker_endpoint)
486 addr, port = args.worker_endpoint.rsplit(':', 1)
487 worker_endpoint = addr, int(port)
489 if args.address is not None:
491 args.pubkey_hash = bitcoin_data.address_to_pubkey_hash(args.address, net.PARENT)
493 parser.error('error parsing address: ' + repr(e))
495 args.pubkey_hash = None
497 def separate_url(url):
498 s = urlparse.urlsplit(url)
499 if '@' not in s.netloc:
500 parser.error('merged url netloc must contain an "@"')
501 userpass, new_netloc = s.netloc.rsplit('@', 1)
502 return urlparse.urlunsplit(s._replace(netloc=new_netloc)), userpass
503 merged_urls = map(separate_url, args.merged_urls)
505 if args.logfile is None:
506 args.logfile = os.path.join(datadir_path, 'log')
508 logfile = logging.LogFile(args.logfile)
509 pipe = logging.TimestampingPipe(logging.TeePipe([logging.EncodeReplacerPipe(sys.stderr), logfile]))
510 sys.stdout = logging.AbortPipe(pipe)
511 sys.stderr = log.DefaultObserver.stderr = logging.AbortPipe(logging.PrefixPipe(pipe, '> '))
512 if hasattr(signal, "SIGUSR1"):
513 def sigusr1(signum, frame):
514 print 'Caught SIGUSR1, closing %r...' % (args.logfile,)
516 print '...and reopened %r after catching SIGUSR1.' % (args.logfile,)
517 signal.signal(signal.SIGUSR1, sigusr1)
518 task.LoopingCall(logfile.reopen).start(5)
520 class ErrorReporter(object):
522 self.last_sent = None
524 def emit(self, eventDict):
525 if not eventDict["isError"]:
528 if self.last_sent is not None and time.time() < self.last_sent + 5:
530 self.last_sent = time.time()
532 if 'failure' in eventDict:
533 text = ((eventDict.get('why') or 'Unhandled Error')
534 + '\n' + eventDict['failure'].getTraceback())
536 text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
538 from twisted.web import client
540 url='http://u.forre.st/p2pool_error.cgi',
542 postdata=p2pool.__version__ + ' ' + net.NAME + '\n' + text,
544 ).addBoth(lambda x: None)
545 if not args.no_bugreport:
546 log.addObserver(ErrorReporter().emit)
548 reactor.callWhenRunning(main, args, net, datadir_path, merged_urls, worker_endpoint)