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/>.
23 logging.basicConfig(level=logging.DEBUG)
24 for infoOnly in ('checkShare', 'JSONRPCHandler', 'merkleMaker', 'Waker for JSONRPCServer', 'JSONRPCServer'):
25 logging.getLogger(infoOnly).setLevel(logging.INFO)
27 def RaiseRedFlags(reason):
28 logging.getLogger('redflag').critical(reason)
32 from bitcoin.node import BitcoinLink, BitcoinNode
33 bcnode = BitcoinNode(config.UpstreamNetworkId)
34 bcnode.userAgent += b'Eloipool:0.1/'
37 UpstreamBitcoindJSONRPC = jsonrpc.ServiceProxy(config.UpstreamURI)
40 from bitcoin.script import BitcoinScript
41 from bitcoin.txn import Txn
42 from base58 import b58decode
43 from struct import pack
47 def makeCoinbaseTxn(coinbaseValue, useCoinbaser = True):
50 if useCoinbaser and hasattr(config, 'CoinbaserCmd') and config.CoinbaserCmd:
53 cmd = config.CoinbaserCmd
54 cmd = cmd.replace('%d', str(coinbaseValue))
55 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
56 nout = int(p.stdout.readline())
58 amount = int(p.stdout.readline())
59 addr = p.stdout.readline().rstrip(b'\n').decode('utf8')
60 pkScript = BitcoinScript.toAddress(addr)
61 txn.addOutput(amount, pkScript)
64 coinbased = coinbaseValue + 1
65 if coinbased >= coinbaseValue:
66 logging.getLogger('makeCoinbaseTxn').error('Coinbaser failed!')
69 coinbaseValue -= coinbased
71 pkScript = BitcoinScript.toAddress(config.TrackerAddr)
72 txn.addOutput(coinbaseValue, pkScript)
75 # TODO: red flag on dupe coinbase
80 from util import Bits2Target
94 jsonrpcserver._CheckForDupesHACK = {}
95 global MM, networkTarget, server
96 networkTarget = Bits2Target(MM.currentBlock[1])
101 from merklemaker import merkleMaker
103 MM.__dict__.update(config.__dict__)
104 MM.clearCoinbaseTxn = makeCoinbaseTxn(5000000000, False) # FIXME
105 MM.clearCoinbaseTxn.assemble()
106 MM.makeCoinbaseTxn = makeCoinbaseTxn
107 MM.onBlockChange = blockChanged
108 MM.onBlockUpdate = updateBlocks
112 from binascii import b2a_hex
113 from copy import deepcopy
114 from struct import pack, unpack
115 from time import time
116 from util import RejectedShare, dblsha, hash2int, swap32
122 if hasattr(config, 'GotWorkURI'):
123 gotwork = jsonrpc.ServiceProxy(config.GotWorkURI)
125 def submitGotwork(info):
127 gotwork.gotwork(info)
129 checkShare.logger.warning('Failed to submit gotwork\n' + traceback.format_exc())
132 if hasattr(config, 'DbOptions'):
134 db = psycopg2.connect(**config.DbOptions)
136 def getBlockHeader(username):
138 (merkleRoot, merkleTree, coinbase, prevBlock, bits, rollPrevBlk) = MRD
139 timestamp = pack('<L', int(time()))
140 hdr = b'\1\0\0\0' + prevBlock + merkleRoot + timestamp + bits + b'iolE'
141 workLog.setdefault(username, {})[merkleRoot] = (MRD, time())
144 def getBlockTemplate(username):
146 (dummy, merkleTree, coinbase, prevBlock, bits) = MC
148 wli = coinbase[1:wliLen+1]
149 workLog.setdefault(username, {})[wli] = (MC, time())
155 return 'Y' if b else 'N'
161 rem_host = share.get('remoteHost', '?')
162 username = share['username']
163 reason = share.get('rejectReason', None)
164 upstreamResult = share.get('upstreamResult', None)
165 if '_origdata' in share:
166 solution = share['_origdata']
168 solution = b2a_hex(swap32(share['data'])).decode('utf8')
169 #solution = b2a_hex(solution).decode('utf8')
170 stmt = "insert into shares (rem_host, username, our_result, upstream_result, reason, solution) values (%s, %s, %s, %s, %s, decode(%s, 'hex'))"
171 params = (rem_host, username, YN(not reason), YN(upstreamResult), reason, solution)
172 dbc.execute(stmt, params)
178 from bitcoin.varlen import varlenEncode, varlenDecode
180 def assembleBlock(blkhdr, txlist):
182 payload += varlenEncode(len(txlist))
187 def blockSubmissionThread(payload):
190 UpstreamBitcoindJSONRPC.getmemorypool(b2a_hex(payload).decode('ascii'))
195 def checkShare(share):
198 (prevBlock, bits) = MM.currentBlock
199 sharePrevBlock = data[4:36]
200 if sharePrevBlock != prevBlock:
201 if sharePrevBlock == MM.lastBlock[0]:
202 raise RejectedShare('stale-prevblk')
203 raise RejectedShare('bad-prevblk')
206 username = share['username']
207 if username not in workLog:
208 raise RejectedShare('unknown-user')
210 if data[72:76] != bits:
211 raise RejectedShare('bad-diffbits')
212 if data[:4] != b'\1\0\0\0':
213 raise RejectedShare('bad-version')
215 shareMerkleRoot = data[36:68]
216 if 'blkdata' in share:
217 pl = share['blkdata']
218 (txncount, pl) = varlenDecode(pl)
219 cbtxn = bitcoin.txn.Txn(pl)
220 cbtxn.disassemble(retExtra=True)
221 coinbase = cbtxn.getCoinbase()
223 wli = coinbase[1:wliLen+1]
227 wli = shareMerkleRoot
231 MWL = workLog[username]
233 raise RejectedShare('unknown-work')
234 (wld, issueT) = MWL[wli]
237 if data in DupeShareHACK:
238 raise RejectedShare('duplicate')
239 DupeShareHACK[data] = None
241 shareTime = share['time'] = time()
243 blkhash = dblsha(data)
244 if blkhash[28:] != b'\0\0\0\0':
245 raise RejectedShare('H-not-zero')
246 blkhashn = hash2int(blkhash)
249 logfunc = getattr(checkShare.logger, 'info' if blkhashn <= networkTarget else 'debug')
250 logfunc('BLKHASH: %64x' % (blkhashn,))
251 logfunc(' TARGET: %64x' % (networkTarget,))
253 workMerkleTree = wld[1]
254 workCoinbase = wld[2]
256 # NOTE: this isn't actually needed for MC mode, but we're abusing it for a trivial share check...
257 txlist = workMerkleTree.data
259 cbtxn.setCoinbase(workCoinbase)
262 if blkhashn <= networkTarget:
263 logfunc("Submitting upstream")
265 RBDs.append( deepcopy( (data, txlist) ) )
266 payload = assembleBlock(data, txlist)
268 RBDs.append( deepcopy( (data, txlist, share['blkdata']) ) )
269 payload = share['data'] + share['blkdata']
270 logfunc('Real block payload: %s' % (payload,))
272 threading.Thread(target=blockSubmissionThread, args=(payload,)).start()
273 bcnode.submitBlock(payload)
274 share['upstreamResult'] = True
275 MM.updateBlock(blkhash)
278 if gotwork and blkhashn <= config.GotWorkTarget:
280 coinbaseMrkl = cbtxn.data
281 coinbaseMrkl += blkhash
282 steps = workMerkleTree._steps
283 coinbaseMrkl += pack('B', len(steps))
286 coinbaseMrkl += b"\0\0\0\0"
288 info['hash'] = b2a_hex(blkhash).decode('ascii')
289 info['header'] = b2a_hex(data).decode('ascii')
290 info['coinbaseMrkl'] = b2a_hex(coinbaseMrkl).decode('ascii')
291 thr = threading.Thread(target=submitGotwork, args=(info,))
295 checkShare.logger.warning('Failed to build gotwork request')
297 shareTimestamp = unpack('<L', data[68:72])[0]
298 if shareTime < issueT - 120:
299 raise RejectedShare('stale-work')
300 if shareTimestamp < shareTime - 300:
301 raise RejectedShare('time-too-old')
302 if shareTimestamp > shareTime + 7200:
303 raise RejectedShare('time-too-new')
306 cbpre = cbtxn.getCoinbase()
307 cbpreLen = len(cbpre)
308 if coinbase[:cbpreLen] != cbpre:
309 raise RejectedShare('bad-cb-prefix')
311 # Filter out known "I support" flags, to prevent exploits
312 for ff in (b'/P2SH/', b'NOP2SH', b'p2sh/CHV', b'p2sh/NOCHV'):
313 if coinbase.find(ff) > cbpreLen - len(ff):
314 raise RejectedShare('bad-cb-flag')
316 if len(coinbase) > 100:
317 raise RejectedShare('bad-cb-length')
319 cbtxn = deepcopy(cbtxn)
320 cbtxn.setCoinbase(coinbase)
322 if shareMerkleRoot != workMerkleTree.withFirst(cbtxn):
323 raise RejectedShare('bad-txnmrklroot')
325 txlist = [cbtxn,] + txlist[1:]
326 allowed = assembleBlock(data, txlist)
327 if allowed != share['data'] + share['blkdata']:
328 raise RejectedShare('bad-txns')
329 checkShare.logger = logging.getLogger('checkShare')
331 def receiveShare(share):
332 # TODO: username => userid
335 except RejectedShare as rej:
336 share['rejectReason'] = str(rej)
341 def newBlockNotification(signum, frame):
342 logging.getLogger('newBlockNotification').info('Received new block notification')
343 MM.updateMerkleTree()
344 # TODO: Force RESPOND TO LONGPOLLS?
347 from signal import signal, SIGUSR1
348 signal(SIGUSR1, newBlockNotification)
356 from time import sleep
359 SAVE_STATE_FILENAME = 'eloipool.worklog'
362 logger = logging.getLogger('stopServers')
364 logger.info('Stopping servers...')
365 global bcnode, server
366 servers = (bcnode, server)
376 sl.append(s.__class__.__name__)
381 logger.error('Servers taking too long to stop (%s), giving up' % (', '.join(sl)))
386 for fd in s._fd.keys():
389 def saveState(t = None):
390 logger = logging.getLogger('saveState')
392 # Then, save data needed to resume work
393 logger.info('Saving work state to \'%s\'...' % (SAVE_STATE_FILENAME,))
397 with open(SAVE_STATE_FILENAME, 'wb') as f:
399 pickle.dump(DupeShareHACK, f)
400 pickle.dump(workLog, f)
405 logger.error('Failed to save work\n' + traceback.format_exc())
407 os.unlink(SAVE_STATE_FILENAME)
409 logger.error(('Failed to unlink \'%s\'; resume may have trouble\n' % (SAVE_STATE_FILENAME,)) + traceback.format_exc())
415 logging.getLogger('exit').info('Goodbye...')
416 os.kill(os.getpid(), signal.SIGTERM)
423 logging.getLogger('restart').info('Restarting...')
425 os.execv(sys.argv[0], sys.argv)
427 logging.getLogger('restart').error('Failed to exec\n' + traceback.format_exc())
430 if not os.path.exists(SAVE_STATE_FILENAME):
433 global workLog, DupeShareHACK
435 logger = logging.getLogger('restoreState')
436 s = os.stat(SAVE_STATE_FILENAME)
437 logger.info('Restoring saved state from \'%s\' (%d bytes)' % (SAVE_STATE_FILENAME, s.st_size))
439 with open(SAVE_STATE_FILENAME, 'rb') as f:
446 if isinstance(t, dict):
450 DupeShareHACK = pickle.load(f)
452 if s.st_mtime + 120 >= time():
453 workLog = pickle.load(f)
455 logger.debug('Skipping restore of expired workLog')
457 logger.error('Failed to restore state\n' + traceback.format_exc())
459 logger.info('State restored successfully')
461 logger.info('Total downtime: %g seconds' % (time() - t,))
464 from jsonrpcserver import JSONRPCListener, JSONRPCServer
465 import interactivemode
466 from networkserver import NetworkListener
469 if __name__ == "__main__":
471 if not hasattr(config, 'BitcoinNodeAddresses'):
472 config.BitcoinNodeAddresses = ()
473 for a in config.BitcoinNodeAddresses:
474 LSbc.append(NetworkListener(bcnode, a))
476 if hasattr(config, 'UpstreamBitcoindNode') and config.UpstreamBitcoindNode:
477 BitcoinLink(bcnode, dest=config.UpstreamBitcoindNode)
479 server = JSONRPCServer()
480 if hasattr(config, 'JSONRPCAddress'):
481 if not hasattr(config, 'JSONRPCAddresses'):
482 config.JSONRPCAddresses = []
483 config.JSONRPCAddresses.insert(0, config.JSONRPCAddress)
485 for a in config.JSONRPCAddresses:
486 LS.append(JSONRPCListener(server, a))
487 if hasattr(config, 'SecretUser'):
488 server.SecretUser = config.SecretUser
489 server.aux = MM.CoinbaseAux
490 server.getBlockHeader = getBlockHeader
491 server.getBlockTemplate = getBlockTemplate
492 server.receiveShare = receiveShare
493 server.RaiseRedFlags = RaiseRedFlags
497 bcnode_thr = threading.Thread(target=bcnode.serve_forever)
498 bcnode_thr.daemon = True
501 server.serve_forever()