2 # Eloipool - Python Bitcoin pool server
3 # Copyright (C) 2011-2012 Luke Dashjr <luke-jr+eloipool@utopios.org>
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Affero General Public License for more details.
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 if not hasattr(config, 'ServerName'):
21 config.ServerName = 'Unnamed Eloipool'
23 if not hasattr(config, 'ShareTarget'):
24 config.ShareTarget = 0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff
29 if len(logging.root.handlers) == 0:
31 format='%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s',
34 for infoOnly in ('checkShare', 'JSONRPCHandler', 'merkleMaker', 'Waker for JSONRPCServer', 'JSONRPCServer'):
35 logging.getLogger(infoOnly).setLevel(logging.INFO)
37 def RaiseRedFlags(reason):
38 logging.getLogger('redflag').critical(reason)
42 from bitcoin.node import BitcoinLink, BitcoinNode
43 bcnode = BitcoinNode(config.UpstreamNetworkId)
44 bcnode.userAgent += b'Eloipool:0.1/'
47 UpstreamBitcoindJSONRPC = jsonrpc.ServiceProxy(config.UpstreamURI)
50 from bitcoin.script import BitcoinScript
51 from bitcoin.txn import Txn
52 from base58 import b58decode
53 from struct import pack
57 def makeCoinbaseTxn(coinbaseValue, useCoinbaser = True):
60 if useCoinbaser and hasattr(config, 'CoinbaserCmd') and config.CoinbaserCmd:
63 cmd = config.CoinbaserCmd
64 cmd = cmd.replace('%d', str(coinbaseValue))
65 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
66 nout = int(p.stdout.readline())
68 amount = int(p.stdout.readline())
69 addr = p.stdout.readline().rstrip(b'\n').decode('utf8')
70 pkScript = BitcoinScript.toAddress(addr)
71 txn.addOutput(amount, pkScript)
74 coinbased = coinbaseValue + 1
75 if coinbased >= coinbaseValue:
76 logging.getLogger('makeCoinbaseTxn').error('Coinbaser failed!')
79 coinbaseValue -= coinbased
81 pkScript = BitcoinScript.toAddress(config.TrackerAddr)
82 txn.addOutput(coinbaseValue, pkScript)
85 # TODO: red flag on dupe coinbase
89 import jsonrpc_getwork
90 from util import Bits2Target
104 jsonrpc_getwork._CheckForDupesHACK = {}
105 global MM, networkTarget, server
106 bits = MM.currentBlock[2]
110 networkTarget = Bits2Target(bits)
115 from merklemaker import merkleMaker
117 MM.__dict__.update(config.__dict__)
118 MM.clearCoinbaseTxn = makeCoinbaseTxn(5000000000, False) # FIXME
119 MM.clearCoinbaseTxn.assemble()
120 MM.makeCoinbaseTxn = makeCoinbaseTxn
121 MM.onBlockChange = blockChanged
122 MM.onBlockUpdate = updateBlocks
125 from binascii import b2a_hex
126 from copy import deepcopy
127 from math import ceil, log
128 from struct import pack, unpack
130 from time import time
131 from util import RejectedShare, dblsha, hash2int, swap32
136 if hasattr(config, 'GotWorkURI'):
137 gotwork = jsonrpc.ServiceProxy(config.GotWorkURI)
139 if not hasattr(config, 'DynamicTargetting'):
140 config.DynamicTargetting = 0
142 config.DynamicTargetGoal *= 2
144 def submitGotwork(info):
146 gotwork.gotwork(info)
148 checkShare.logger.warning('Failed to submit gotwork\n' + traceback.format_exc())
150 def getTarget(username, now):
151 if not config.DynamicTargetting:
153 if username in userStatus:
154 status = userStatus[username]
156 userStatus[username] = [None, now, 0]
158 (targetIn, lastUpdate, work) = status
159 if work <= config.DynamicTargetGoal:
160 if now < lastUpdate + 120 and (targetIn is None or targetIn >= networkTarget):
164 getTarget.logger.debug("No shares from '%s', resetting to minimum target")
165 userStatus[username] = [None, now, 0]
168 deltaSec = now - lastUpdate
169 target = targetIn or config.ShareTarget
170 target = int(target * config.DynamicTargetGoal * deltaSec / 120 / work)
171 if target >= config.ShareTarget:
174 if target < networkTarget:
175 target = networkTarget
176 if config.DynamicTargetting == 2:
177 # Round target to a power of two :)
178 truebits = log(target, 2)
179 target = 2**ceil(truebits) - 1
180 if target == config.ShareTarget:
182 if target != targetIn:
183 pfx = 'Retargetting %s' % (repr(username),)
184 getTarget.logger.debug("%s from: %064x" % (pfx, targetIn or config.ShareTarget,))
185 getTarget.logger.debug("%s to: %064x" % (pfx, target or config.ShareTarget,))
186 userStatus[username] = [target, now, 0]
188 getTarget.logger = logging.getLogger('getTarget')
190 def RegisterWork(username, wli, wld):
192 target = getTarget(username, now)
193 wld = tuple(wld) + (target,)
194 workLog.setdefault(username, {})[wli] = (wld, now)
195 return target or config.ShareTarget
197 def getBlockHeader(username):
199 (merkleRoot, merkleTree, coinbase, prevBlock, bits, rollPrevBlk) = MRD[:6]
200 timestamp = pack('<L', int(time()))
201 hdr = b'\2\0\0\0' + prevBlock + merkleRoot + timestamp + bits + b'iolE'
202 workLog.setdefault(username, {})[merkleRoot] = (MRD, time())
203 target = RegisterWork(username, merkleRoot, MRD)
204 return (hdr, workLog[username][merkleRoot], target)
206 def getBlockTemplate(username):
208 (dummy, merkleTree, coinbase, prevBlock, bits) = MC[:5]
209 wliPos = coinbase[0] + 2
210 wliLen = coinbase[wliPos - 1]
211 wli = coinbase[wliPos:wliPos+wliLen]
212 target = RegisterWork(username, wli, MC)
213 return (MC, workLog[username][wli], target)
220 from bitcoin.varlen import varlenEncode, varlenDecode
222 def assembleBlock(blkhdr, txlist):
224 payload += varlenEncode(len(txlist))
229 def blockSubmissionThread(payload, blkhash):
230 myblock = (blkhash, payload[4:36])
231 payload = b2a_hex(payload).decode('ascii')
235 rv = UpstreamBitcoindJSONRPC.submitblock(payload)
239 rv = UpstreamBitcoindJSONRPC.getmemorypool(payload)
249 # FIXME: This will show "Method not found" on pre-BIP22 servers
250 RaiseRedFlags(traceback.format_exc())
252 if MM.currentBlock[0] not in myblock:
253 RaiseRedFlags('Giving up on submitting block upstream')
256 # FIXME: The returned value could be a list of multiple responses
257 RaiseRedFlags('Upstream block submission failed: %s' % (rv,))
259 def checkShare(share):
260 shareTime = share['time'] = time()
264 (prevBlock, height, bits) = MM.currentBlock
265 sharePrevBlock = data[4:36]
266 if sharePrevBlock != prevBlock:
267 if sharePrevBlock == MM.lastBlock[0]:
268 raise RejectedShare('stale-prevblk')
269 raise RejectedShare('bad-prevblk')
272 username = share['username']
273 if username not in workLog:
274 raise RejectedShare('unknown-user')
276 if data[72:76] != bits:
277 raise RejectedShare('bad-diffbits')
279 # Note that we should accept miners reducing version to 1 if they don't understand 2 yet
280 # FIXME: When the supermajority is upgraded to version 2, stop accepting 1!
281 if data[1:4] != b'\0\0\0' or data[0] > 2:
282 raise RejectedShare('bad-version')
284 shareMerkleRoot = data[36:68]
285 if 'blkdata' in share:
286 pl = share['blkdata']
287 (txncount, pl) = varlenDecode(pl)
288 cbtxn = bitcoin.txn.Txn(pl)
289 cbtxn.disassemble(retExtra=True)
290 coinbase = cbtxn.getCoinbase()
291 wliPos = coinbase[0] + 2
292 wliLen = coinbase[wliPos - 1]
293 wli = coinbase[wliPos:wliPos+wliLen]
297 wli = shareMerkleRoot
301 MWL = workLog[username]
303 raise RejectedShare('unknown-work')
304 (wld, issueT) = MWL[wli]
307 if data in DupeShareHACK:
308 raise RejectedShare('duplicate')
309 DupeShareHACK[data] = None
311 blkhash = dblsha(data)
312 if blkhash[28:] != b'\0\0\0\0':
313 raise RejectedShare('H-not-zero')
314 blkhashn = hash2int(blkhash)
317 logfunc = getattr(checkShare.logger, 'info' if blkhashn <= networkTarget else 'debug')
318 logfunc('BLKHASH: %64x' % (blkhashn,))
319 logfunc(' TARGET: %64x' % (networkTarget,))
321 workMerkleTree = wld[1]
322 workCoinbase = wld[2]
325 # NOTE: this isn't actually needed for MC mode, but we're abusing it for a trivial share check...
326 txlist = workMerkleTree.data
327 txlist = [deepcopy(txlist[0]),] + txlist[1:]
329 cbtxn.setCoinbase(workCoinbase)
332 if blkhashn <= networkTarget:
333 logfunc("Submitting upstream")
334 RBDs.append( deepcopy( (data, txlist, share.get('blkdata', None), workMerkleTree) ) )
336 payload = assembleBlock(data, txlist)
338 payload = share['data'] + share['blkdata']
339 logfunc('Real block payload: %s' % (b2a_hex(payload).decode('utf8'),))
341 threading.Thread(target=blockSubmissionThread, args=(payload, blkhash)).start()
342 bcnode.submitBlock(payload)
343 share['upstreamResult'] = True
344 MM.updateBlock(blkhash)
347 if gotwork and blkhashn <= config.GotWorkTarget:
349 coinbaseMrkl = cbtxn.data
350 coinbaseMrkl += blkhash
351 steps = workMerkleTree._steps
352 coinbaseMrkl += pack('B', len(steps))
355 coinbaseMrkl += b"\0\0\0\0"
357 info['hash'] = b2a_hex(blkhash).decode('ascii')
358 info['header'] = b2a_hex(data).decode('ascii')
359 info['coinbaseMrkl'] = b2a_hex(coinbaseMrkl).decode('ascii')
360 thr = threading.Thread(target=submitGotwork, args=(info,))
364 checkShare.logger.warning('Failed to build gotwork request')
366 if workTarget is None:
367 workTarget = config.ShareTarget
368 if blkhashn > workTarget:
369 raise RejectedShare('high-hash')
370 share['target'] = workTarget
371 share['_targethex'] = '%064x' % (workTarget,)
373 shareTimestamp = unpack('<L', data[68:72])[0]
374 if shareTime < issueT - 120:
375 raise RejectedShare('stale-work')
376 if shareTimestamp < shareTime - 300:
377 raise RejectedShare('time-too-old')
378 if shareTimestamp > shareTime + 7200:
379 raise RejectedShare('time-too-new')
381 if config.DynamicTargetting and username in userStatus:
382 # NOTE: userStatus[username] only doesn't exist across restarts
383 status = userStatus[username]
384 target = status[0] or config.ShareTarget
385 if target == workTarget:
386 userStatus[username][2] += 1
388 userStatus[username][2] += float(target) / workTarget
391 cbpre = cbtxn.getCoinbase()
392 cbpreLen = len(cbpre)
393 if coinbase[:cbpreLen] != cbpre:
394 raise RejectedShare('bad-cb-prefix')
396 # Filter out known "I support" flags, to prevent exploits
397 for ff in (b'/P2SH/', b'NOP2SH', b'p2sh/CHV', b'p2sh/NOCHV'):
398 if coinbase.find(ff) > max(-1, cbpreLen - len(ff)):
399 raise RejectedShare('bad-cb-flag')
401 if len(coinbase) > 100:
402 raise RejectedShare('bad-cb-length')
404 cbtxn.setCoinbase(coinbase)
406 if shareMerkleRoot != workMerkleTree.withFirst(cbtxn):
407 raise RejectedShare('bad-txnmrklroot')
409 allowed = assembleBlock(data, txlist)
410 if allowed != share['data'] + share['blkdata']:
411 raise RejectedShare('bad-txns')
412 checkShare.logger = logging.getLogger('checkShare')
414 def receiveShare(share):
415 # TODO: username => userid
418 except RejectedShare as rej:
419 share['rejectReason'] = str(rej)
422 if '_origdata' in share:
423 share['solution'] = share['_origdata']
425 share['solution'] = b2a_hex(swap32(share['data'])).decode('utf8')
426 for i in loggersShare:
429 def newBlockNotification():
430 logging.getLogger('newBlockNotification').info('Received new block notification')
431 MM.updateMerkleTree()
432 # TODO: Force RESPOND TO LONGPOLLS?
435 def newBlockNotificationSIGNAL(signum, frame):
436 # Use a new thread, in case the signal handler is called with locks held
437 thr = threading.Thread(target=newBlockNotification, name='newBlockNotification via signal %s' % (signum,))
441 from signal import signal, SIGUSR1
442 signal(SIGUSR1, newBlockNotificationSIGNAL)
450 from time import sleep
453 SAVE_STATE_FILENAME = 'eloipool.worklog'
456 logger = logging.getLogger('stopServers')
458 if hasattr(stopServers, 'already'):
459 logger.debug('Already tried to stop servers before')
461 stopServers.already = True
463 logger.info('Stopping servers...')
464 global bcnode, server
465 servers = (bcnode, server)
472 logger.error('Failed to stop server %s\n%s' % (s, traceback.format_exc()))
478 sl.append(s.__class__.__name__)
483 logger.error('Servers taking too long to stop (%s), giving up' % (', '.join(sl)))
488 for fd in s._fd.keys():
491 def saveState(t = None):
492 logger = logging.getLogger('saveState')
494 # Then, save data needed to resume work
495 logger.info('Saving work state to \'%s\'...' % (SAVE_STATE_FILENAME,))
499 with open(SAVE_STATE_FILENAME, 'wb') as f:
501 pickle.dump(DupeShareHACK, f)
502 pickle.dump(workLog, f)
507 logger.error('Failed to save work\n' + traceback.format_exc())
509 os.unlink(SAVE_STATE_FILENAME)
511 logger.error(('Failed to unlink \'%s\'; resume may have trouble\n' % (SAVE_STATE_FILENAME,)) + traceback.format_exc())
517 logging.getLogger('exit').info('Goodbye...')
518 os.kill(os.getpid(), signal.SIGTERM)
525 logging.getLogger('restart').info('Restarting...')
527 os.execv(sys.argv[0], sys.argv)
529 logging.getLogger('restart').error('Failed to exec\n' + traceback.format_exc())
532 if not os.path.exists(SAVE_STATE_FILENAME):
535 global workLog, DupeShareHACK
537 logger = logging.getLogger('restoreState')
538 s = os.stat(SAVE_STATE_FILENAME)
539 logger.info('Restoring saved state from \'%s\' (%d bytes)' % (SAVE_STATE_FILENAME, s.st_size))
541 with open(SAVE_STATE_FILENAME, 'rb') as f:
545 # Future formats, not supported here
549 # Old format, from 2012-02-02 to 2012-02-03
554 if isinstance(t, dict):
555 # Old format, from 2012-02-03 to 2012-02-03
559 # Current format, from 2012-02-03 onward
560 DupeShareHACK = pickle.load(f)
562 if t + 120 >= time():
563 workLog = pickle.load(f)
565 logger.debug('Skipping restore of expired workLog')
567 logger.error('Failed to restore state\n' + traceback.format_exc())
569 logger.info('State restored successfully')
571 logger.info('Total downtime: %g seconds' % (time() - t,))
574 from jsonrpcserver import JSONRPCListener, JSONRPCServer
575 import interactivemode
576 from networkserver import NetworkListener
581 if __name__ == "__main__":
582 if not hasattr(config, 'ShareLogging'):
583 config.ShareLogging = ()
584 if hasattr(config, 'DbOptions'):
585 logging.getLogger('backwardCompatibility').warn('DbOptions configuration variable is deprecated; upgrade to ShareLogging var before 2013-03-05')
586 config.ShareLogging = list(config.ShareLogging)
587 config.ShareLogging.append( {
589 'engine': 'postgres',
590 'dbopts': config.DbOptions,
591 'statement': "insert into shares (rem_host, username, our_result, upstream_result, reason, solution) values ({Q(remoteHost)}, {username}, {YN(not(rejectReason))}, {YN(upstreamResult)}, {rejectReason}, decode({solution}, 'hex'))",
593 for i in config.ShareLogging:
594 if not hasattr(i, 'keys'):
596 logging.getLogger('backwardCompatibility').warn('Using short-term backward compatibility for ShareLogging[\'%s\']; be sure to update config before 2012-04-04' % (name,))
597 if name == 'postgres':
600 'engine': 'postgres',
601 'dbopts': parameters,
603 elif name == 'logfile':
605 i['thropts'] = parameters
606 if 'filename' in parameters:
607 i['filename'] = parameters['filename']
608 i['thropts'] = dict(i['thropts'])
609 del i['thropts']['filename']
617 fp, pathname, description = imp.find_module(name, sharelogging.__path__)
618 m = imp.load_module(name, fp, pathname, description)
619 lo = getattr(m, name)(**parameters)
620 loggersShare.append(lo.logShare)
622 logging.getLogger('sharelogging').error("Error setting up share logger %s: %s", name, sys.exc_info())
625 if not hasattr(config, 'BitcoinNodeAddresses'):
626 config.BitcoinNodeAddresses = ()
627 for a in config.BitcoinNodeAddresses:
628 LSbc.append(NetworkListener(bcnode, a))
630 if hasattr(config, 'UpstreamBitcoindNode') and config.UpstreamBitcoindNode:
631 BitcoinLink(bcnode, dest=config.UpstreamBitcoindNode)
633 import jsonrpc_getblocktemplate
634 import jsonrpc_getwork
635 import jsonrpc_setworkaux
637 server = JSONRPCServer()
638 if hasattr(config, 'JSONRPCAddress'):
639 logging.getLogger('backwardCompatibility').warn('JSONRPCAddress configuration variable is deprecated; upgrade to JSONRPCAddresses list before 2013-03-05')
640 if not hasattr(config, 'JSONRPCAddresses'):
641 config.JSONRPCAddresses = []
642 config.JSONRPCAddresses.insert(0, config.JSONRPCAddress)
644 for a in config.JSONRPCAddresses:
645 LS.append(JSONRPCListener(server, a))
646 if hasattr(config, 'SecretUser'):
647 server.SecretUser = config.SecretUser
648 server.aux = MM.CoinbaseAux
649 server.getBlockHeader = getBlockHeader
650 server.getBlockTemplate = getBlockTemplate
651 server.receiveShare = receiveShare
652 server.RaiseRedFlags = RaiseRedFlags
653 server.ShareTarget = config.ShareTarget
655 if hasattr(config, 'TrustedForwarders'):
656 server.TrustedForwarders = config.TrustedForwarders
657 server.ServerName = config.ServerName
663 bcnode_thr = threading.Thread(target=bcnode.serve_forever)
664 bcnode_thr.daemon = True
667 server.serve_forever()