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 stratum, worker_interface, helper
23 from util import fixargparse, jsonrpc, variable, deferral, math, logging, switchprotocol
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.HTTPProxy(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,
177 port=args.p2pool_port,
178 max_incoming_conns=args.p2pool_conns,
180 connect_addrs=connect_addrs,
181 desired_outgoing_conns=args.p2pool_outgoing_conns,
183 node.p2p_node.start()
186 with open(os.path.join(datadir_path, 'addrs'), 'wb') as f:
187 f.write(json.dumps(node.p2p_node.addr_store.items()))
188 task.LoopingCall(save_addrs).start(60)
194 @defer.inlineCallbacks
198 is_lan, lan_ip = yield ipdiscover.get_local_ip()
200 pm = yield portmapper.get_port_mapper()
201 yield pm._upnp.add_port_mapping(lan_ip, args.p2pool_port, args.p2pool_port, 'p2pool', 'TCP')
202 except defer.TimeoutError:
206 log.err(None, 'UPnP error:')
207 yield deferral.sleep(random.expovariate(1/120))
210 # start listening for workers with a JSON-RPC server
212 print 'Listening for workers on %r port %i...' % (worker_endpoint[0], worker_endpoint[1])
214 wb = work.WorkerBridge(node, my_pubkey_hash, args.donation_percentage, merged_urls, args.worker_fee)
215 web_root = web.get_web_root(wb, datadir_path, bitcoind_warning_var)
216 worker_interface.WorkerInterface(wb).attach_to(web_root, get_handler=lambda request: request.redirect('/static/'))
217 web_serverfactory = server.Site(web_root)
220 serverfactory = switchprotocol.FirstByteSwitchFactory({'{': stratum.StratumServerFactory(wb)}, web_serverfactory)
221 deferral.retry('Error binding to worker port:', traceback=False)(reactor.listenTCP)(worker_endpoint[1], serverfactory, interface=worker_endpoint[0])
223 with open(os.path.join(os.path.join(datadir_path, 'ready_flag')), 'wb') as f:
231 print 'Started successfully!'
232 print 'Go to http://127.0.0.1:%i/ to view graphs and statistics!' % (worker_endpoint[1],)
233 if args.donation_percentage > 1.1:
234 print '''Donating %.1f%% of work towards P2Pool's development. Thanks for the tip!''' % (args.donation_percentage,)
235 elif args.donation_percentage < .9:
236 print '''Donating %.1f%% of work towards P2Pool's development. Please donate to encourage further development of P2Pool!''' % (args.donation_percentage,)
238 print '''Donating %.1f%% of work towards P2Pool's development. Thank you!''' % (args.donation_percentage,)
239 print 'You can increase this amount with --give-author argument! (or decrease it, if you must)'
243 if hasattr(signal, 'SIGALRM'):
244 signal.signal(signal.SIGALRM, lambda signum, frame: reactor.callFromThread(
245 sys.stderr.write, 'Watchdog timer went off at:\n' + ''.join(traceback.format_stack())
247 signal.siginterrupt(signal.SIGALRM, False)
248 task.LoopingCall(signal.alarm, 30).start(1)
250 if args.irc_announce:
251 from twisted.words.protocols import irc
252 class IRCClient(irc.IRCClient):
253 nickname = 'p2pool%02i' % (random.randrange(100),)
254 channel = net.ANNOUNCE_CHANNEL
255 def lineReceived(self, line):
258 irc.IRCClient.lineReceived(self, line)
260 self.in_channel = False
261 irc.IRCClient.signedOn(self)
262 self.factory.resetDelay()
263 self.join(self.channel)
264 @defer.inlineCallbacks
265 def new_share(share):
266 if not self.in_channel:
268 if share.pow_hash <= share.header['bits'].target and abs(share.timestamp - time.time()) < 10*60:
269 yield deferral.sleep(random.expovariate(1/60))
270 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)
271 if all('%x' % (share.header_hash,) not in old_message for old_message in self.recent_messages):
272 self.say(self.channel, message)
273 self._remember_message(message)
274 self.watch_id = node.tracker.verified.added.watch(new_share)
275 self.recent_messages = []
276 def joined(self, channel):
277 self.in_channel = True
278 def left(self, channel):
279 self.in_channel = False
280 def _remember_message(self, message):
281 self.recent_messages.append(message)
282 while len(self.recent_messages) > 100:
283 self.recent_messages.pop(0)
284 def privmsg(self, user, channel, message):
285 if channel == self.channel:
286 self._remember_message(message)
287 def connectionLost(self, reason):
288 node.tracker.verified.added.unwatch(self.watch_id)
289 print 'IRC connection lost:', reason.getErrorMessage()
290 class IRCClientFactory(protocol.ReconnectingClientFactory):
292 reactor.connectTCP("irc.freenode.net", 6667, IRCClientFactory())
294 @defer.inlineCallbacks
299 yield deferral.sleep(3)
301 height = node.tracker.get_height(node.best_share_var.value)
302 this_str = 'P2Pool: %i shares in chain (%i verified/%i total) Peers: %i (%i incoming)' % (
304 len(node.tracker.verified.items),
305 len(node.tracker.items),
306 len(node.p2p_node.peers),
307 sum(1 for peer in node.p2p_node.peers.itervalues() if peer.incoming),
308 ) + (' FDs: %i R/%i W' % (len(reactor.getReaders()), len(reactor.getWriters())) if p2pool.DEBUG else '')
310 datums, dt = wb.local_rate_monitor.get_datums_in_last()
311 my_att_s = sum(datum['work']/dt for datum in datums)
312 this_str += '\n Local: %sH/s in last %s Local dead on arrival: %s Expected time to share: %s' % (
313 math.format(int(my_att_s)),
315 math.format_binomial_conf(sum(1 for datum in datums if datum['dead']), len(datums), 0.95),
316 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 '???',
320 (stale_orphan_shares, stale_doa_shares), shares, _ = wb.get_stale_counts()
321 stale_prop = p2pool_data.get_average_stale_prop(node.tracker, node.best_share_var.value, min(60*60//net.SHARE_PERIOD, height))
322 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)
324 this_str += '\n Shares: %i (%i orphan, %i dead) Stale rate: %s Efficiency: %s Current payout: %.4f %s' % (
325 shares, stale_orphan_shares, stale_doa_shares,
326 math.format_binomial_conf(stale_orphan_shares + stale_doa_shares, shares, 0.95),
327 math.format_binomial_conf(stale_orphan_shares + stale_doa_shares, shares, 0.95, lambda x: (1 - x)/(1 - stale_prop)),
328 node.get_current_txouts().get(bitcoin_data.pubkey_hash_to_script2(my_pubkey_hash), 0)*1e-8, net.PARENT.SYMBOL,
330 this_str += '\n Pool: %sH/s Stale rate: %.1f%% Expected time to block: %s' % (
331 math.format(int(real_att_s)),
333 math.format_dt(2**256 / node.bitcoind_work.value['bits'].target / real_att_s),
336 for warning in p2pool_data.get_warnings(node.tracker, node.best_share_var.value, net, bitcoind_warning_var.value, node.bitcoind_work.value):
337 print >>sys.stderr, '#'*40
338 print >>sys.stderr, '>>> Warning: ' + warning
339 print >>sys.stderr, '#'*40
341 if this_str != last_str or time.time() > last_time + 15:
344 last_time = time.time()
350 log.err(None, 'Fatal error:')
353 realnets = dict((name, net) for name, net in networks.nets.iteritems() if '_testnet' not in name)
355 parser = fixargparse.FixedArgumentParser(description='p2pool (version %s)' % (p2pool.__version__,), fromfile_prefix_chars='@')
356 parser.add_argument('--version', action='version', version=p2pool.__version__)
357 parser.add_argument('--net',
358 help='use specified network (default: bitcoin)',
359 action='store', choices=sorted(realnets), default='bitcoin', dest='net_name')
360 parser.add_argument('--testnet',
361 help='''use the network's testnet''',
362 action='store_const', const=True, default=False, dest='testnet')
363 parser.add_argument('--debug',
364 help='enable debugging mode',
365 action='store_const', const=True, default=False, dest='debug')
366 parser.add_argument('-a', '--address',
367 help='generate payouts to this address (default: <address requested from bitcoind>)',
368 type=str, action='store', default=None, dest='address')
369 parser.add_argument('--datadir',
370 help='store data in this directory (default: <directory run_p2pool.py is in>/data)',
371 type=str, action='store', default=None, dest='datadir')
372 parser.add_argument('--logfile',
373 help='''log to this file (default: data/<NET>/log)''',
374 type=str, action='store', default=None, dest='logfile')
375 parser.add_argument('--merged',
376 help='call getauxblock on this url to get work for merged mining (example: http://ncuser:ncpass@127.0.0.1:10332/)',
377 type=str, action='append', default=[], dest='merged_urls')
378 parser.add_argument('--give-author', metavar='DONATION_PERCENTAGE',
379 help='donate this percentage of work towards the development of p2pool (default: 1.0)',
380 type=float, action='store', default=1.0, dest='donation_percentage')
381 parser.add_argument('--iocp',
382 help='use Windows IOCP API in order to avoid errors due to large number of sockets being open',
383 action='store_true', default=False, dest='iocp')
384 parser.add_argument('--irc-announce',
385 help='announce any blocks found on irc://irc.freenode.net/#p2pool',
386 action='store_true', default=False, dest='irc_announce')
387 parser.add_argument('--no-bugreport',
388 help='disable submitting caught exceptions to the author',
389 action='store_true', default=False, dest='no_bugreport')
391 p2pool_group = parser.add_argument_group('p2pool interface')
392 p2pool_group.add_argument('--p2pool-port', metavar='PORT',
393 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())),
394 type=int, action='store', default=None, dest='p2pool_port')
395 p2pool_group.add_argument('-n', '--p2pool-node', metavar='ADDR[:PORT]',
396 help='connect to existing p2pool node at ADDR listening on port PORT (defaults to default p2pool P2P port) in addition to builtin addresses',
397 type=str, action='append', default=[], dest='p2pool_nodes')
398 parser.add_argument('--disable-upnp',
399 help='''don't attempt to use UPnP to forward p2pool's P2P port from the Internet to this computer''',
400 action='store_false', default=True, dest='upnp')
401 p2pool_group.add_argument('--max-conns', metavar='CONNS',
402 help='maximum incoming connections (default: 40)',
403 type=int, action='store', default=40, dest='p2pool_conns')
404 p2pool_group.add_argument('--outgoing-conns', metavar='CONNS',
405 help='outgoing connections (default: 6)',
406 type=int, action='store', default=6, dest='p2pool_outgoing_conns')
408 worker_group = parser.add_argument_group('worker interface')
409 worker_group.add_argument('-w', '--worker-port', metavar='PORT or ADDR:PORT',
410 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())),
411 type=str, action='store', default=None, dest='worker_endpoint')
412 worker_group.add_argument('-f', '--fee', metavar='FEE_PERCENTAGE',
413 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)''',
414 type=float, action='store', default=0, dest='worker_fee')
416 bitcoind_group = parser.add_argument_group('bitcoind interface')
417 bitcoind_group.add_argument('--bitcoind-address', metavar='BITCOIND_ADDRESS',
418 help='connect to this address (default: 127.0.0.1)',
419 type=str, action='store', default='127.0.0.1', dest='bitcoind_address')
420 bitcoind_group.add_argument('--bitcoind-rpc-port', metavar='BITCOIND_RPC_PORT',
421 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())),
422 type=int, action='store', default=None, dest='bitcoind_rpc_port')
423 bitcoind_group.add_argument('--bitcoind-rpc-ssl',
424 help='connect to JSON-RPC interface using SSL',
425 action='store_true', default=False, dest='bitcoind_rpc_ssl')
426 bitcoind_group.add_argument('--bitcoind-p2p-port', metavar='BITCOIND_P2P_PORT',
427 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())),
428 type=int, action='store', default=None, dest='bitcoind_p2p_port')
430 bitcoind_group.add_argument(metavar='BITCOIND_RPCUSERPASS',
431 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)',
432 type=str, action='store', default=[], nargs='*', dest='bitcoind_rpc_userpass')
434 args = parser.parse_args()
438 defer.setDebugging(True)
442 net_name = args.net_name + ('_testnet' if args.testnet else '')
443 net = networks.nets[net_name]
445 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)
446 if not os.path.exists(datadir_path):
447 os.makedirs(datadir_path)
449 if len(args.bitcoind_rpc_userpass) > 2:
450 parser.error('a maximum of two arguments are allowed')
451 args.bitcoind_rpc_username, args.bitcoind_rpc_password = ([None, None] + args.bitcoind_rpc_userpass)[-2:]
453 if args.bitcoind_rpc_password is None:
454 conf_path = net.PARENT.CONF_FILE_FUNC()
455 if not os.path.exists(conf_path):
456 parser.error('''Bitcoin configuration file not found. Manually enter your RPC password.\r\n'''
457 '''If you actually haven't created a configuration file, you should create one at %s with the text:\r\n'''
460 '''rpcpassword=%x\r\n'''
462 '''Keep that password secret! After creating the file, restart Bitcoin.''' % (conf_path, random.randrange(2**128)))
463 conf = open(conf_path, 'rb').read()
465 for line in conf.splitlines(True):
467 line = line[:line.index('#')]
470 k, v = line.split('=', 1)
471 contents[k.strip()] = v.strip()
472 for conf_name, var_name, var_type in [
473 ('rpcuser', 'bitcoind_rpc_username', str),
474 ('rpcpassword', 'bitcoind_rpc_password', str),
475 ('rpcport', 'bitcoind_rpc_port', int),
476 ('port', 'bitcoind_p2p_port', int),
478 if getattr(args, var_name) is None and conf_name in contents:
479 setattr(args, var_name, var_type(contents[conf_name]))
480 if args.bitcoind_rpc_password is None:
481 parser.error('''Bitcoin configuration file didn't contain an rpcpassword= line! Add one!''')
483 if args.bitcoind_rpc_username is None:
484 args.bitcoind_rpc_username = ''
486 if args.bitcoind_rpc_port is None:
487 args.bitcoind_rpc_port = net.PARENT.RPC_PORT
489 if args.bitcoind_p2p_port is None:
490 args.bitcoind_p2p_port = net.PARENT.P2P_PORT
492 if args.p2pool_port is None:
493 args.p2pool_port = net.P2P_PORT
495 if args.p2pool_outgoing_conns > 10:
496 parser.error('''--outgoing-conns can't be more than 10''')
498 if args.worker_endpoint is None:
499 worker_endpoint = '', net.WORKER_PORT
500 elif ':' not in args.worker_endpoint:
501 worker_endpoint = '', int(args.worker_endpoint)
503 addr, port = args.worker_endpoint.rsplit(':', 1)
504 worker_endpoint = addr, int(port)
506 if args.address is not None:
508 args.pubkey_hash = bitcoin_data.address_to_pubkey_hash(args.address, net.PARENT)
510 parser.error('error parsing address: ' + repr(e))
512 args.pubkey_hash = None
514 def separate_url(url):
515 s = urlparse.urlsplit(url)
516 if '@' not in s.netloc:
517 parser.error('merged url netloc must contain an "@"')
518 userpass, new_netloc = s.netloc.rsplit('@', 1)
519 return urlparse.urlunsplit(s._replace(netloc=new_netloc)), userpass
520 merged_urls = map(separate_url, args.merged_urls)
522 if args.logfile is None:
523 args.logfile = os.path.join(datadir_path, 'log')
525 logfile = logging.LogFile(args.logfile)
526 pipe = logging.TimestampingPipe(logging.TeePipe([logging.EncodeReplacerPipe(sys.stderr), logfile]))
527 sys.stdout = logging.AbortPipe(pipe)
528 sys.stderr = log.DefaultObserver.stderr = logging.AbortPipe(logging.PrefixPipe(pipe, '> '))
529 if hasattr(signal, "SIGUSR1"):
530 def sigusr1(signum, frame):
531 print 'Caught SIGUSR1, closing %r...' % (args.logfile,)
533 print '...and reopened %r after catching SIGUSR1.' % (args.logfile,)
534 signal.signal(signal.SIGUSR1, sigusr1)
535 task.LoopingCall(logfile.reopen).start(5)
537 class ErrorReporter(object):
539 self.last_sent = None
541 def emit(self, eventDict):
542 if not eventDict["isError"]:
545 if self.last_sent is not None and time.time() < self.last_sent + 5:
547 self.last_sent = time.time()
549 if 'failure' in eventDict:
550 text = ((eventDict.get('why') or 'Unhandled Error')
551 + '\n' + eventDict['failure'].getTraceback())
553 text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
555 from twisted.web import client
557 url='http://u.forre.st/p2pool_error.cgi',
559 postdata=p2pool.__version__ + ' ' + net.NAME + '\n' + text,
561 ).addBoth(lambda x: None)
562 if not args.no_bugreport:
563 log.addObserver(ErrorReporter().emit)
565 reactor.callWhenRunning(main, args, net, datadir_path, merged_urls, worker_endpoint)