1 # Eloipool - Python Bitcoin pool server
2 # Copyright (C) 2011-2013 Luke Dashjr <luke-jr+eloipool@utopios.org>
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as
6 # published by the Free Software Foundation, either version 3 of the
7 # License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 from binascii import b2a_hex
19 from copy import deepcopy
27 from util import RejectedShare, swap32, target2bdiff
29 class StratumError(BaseException):
30 def __init__(self, errno, msg, tb = True):
31 self.StratumErrNo = errno
32 self.StratumErrMsg = msg
43 class StratumHandler(networkserver.SocketHandler):
44 logger = logging.getLogger('StratumHandler')
46 def __init__(self, *a, **ka):
47 super().__init__(*a, **ka)
48 self.remoteHost = self.addr[0]
50 self.set_terminator(b"\n")
53 self.JobTargets = collections.OrderedDict()
56 def sendReply(self, ob):
57 return self.push(json.dumps(ob).encode('ascii') + b"\n")
59 def found_terminator(self):
60 inbuf = b"".join(self.incoming).decode('ascii')
67 rpc = json.loads(inbuf)
71 if 'method' not in rpc:
72 # Assume this is a reply to our request
73 funcname = '_stratumreply_%s' % (rpc['id'],)
74 if not hasattr(self, funcname):
77 getattr(self, funcname)(rpc)
78 except BaseException as e:
79 self.logger.debug(traceback.format_exc())
81 funcname = '_stratum_%s' % (rpc['method'].replace('.', '_'),)
82 if not hasattr(self, funcname):
84 'error': [-3, "Method '%s' not found" % (rpc['method'],), None],
91 rv = getattr(self, funcname)(*rpc['params'])
92 except StratumError as e:
94 'error': (e.StratumErrNo, e.StratumErrMsg, traceback.format_exc() if e.StratumTB else None),
99 except BaseException as e:
100 fexc = traceback.format_exc()
102 'error': (20, str(e), fexc),
106 if not hasattr(e, 'StratumQuiet'):
107 self.logger.debug(fexc)
117 target = self.server.defaultTarget
118 if len(self.Usernames) == 1:
119 dtarget = self.server.getTarget(next(iter(self.Usernames)), time())
120 if not dtarget is None:
122 bdiff = target2bdiff(target)
123 if self.lastBDiff != bdiff:
126 'method': 'mining.set_difficulty',
131 self.lastBDiff = bdiff
132 self.push(self.server.JobBytes)
133 if len(self.JobTargets) > 4:
134 self.JobTargets.popitem(False)
135 self.JobTargets[self.server.JobId] = target
137 def requestStratumUA(self):
140 'method': 'client.get_version',
144 def _stratumreply_7(self, rpc):
145 self.UA = rpc.get('result') or rpc
147 def _stratum_mining_subscribe(self):
148 xid = struct.pack('@P', id(self))
149 self.extranonce1 = xid
150 xid = b2a_hex(xid).decode('ascii')
151 self.server._Clients[id(self)] = self
152 self.changeTask(self.sendJob, 0)
155 ['mining.notify', '%s1' % (xid,)],
156 ['mining.set_difficulty', '%s2' % (xid,)],
164 del self.server._Clients[id(self)]
169 def _stratum_mining_submit(self, username, jobid, extranonce2, ntime, nonce):
170 if username not in self.Usernames:
171 raise StratumError(24, 'unauthorized-user', False)
173 'username': username,
174 'remoteHost': self.remoteHost,
176 'extranonce1': self.extranonce1,
177 'extranonce2': bytes.fromhex(extranonce2),
178 'ntime': bytes.fromhex(ntime),
179 'nonce': bytes.fromhex(nonce),
180 'userAgent': self.UA,
181 'submitProtocol': 'stratum',
183 if jobid in self.JobTargets:
184 share['target'] = self.JobTargets[jobid]
186 self.server.receiveShare(share)
187 except RejectedShare as rej:
189 errno = StratumCodes.get(rej, 20)
190 raise StratumError(errno, rej, False)
193 def checkAuthentication(self, username, password):
196 def _stratum_mining_authorize(self, username, password = None):
198 valid = self.checkAuthentication(username, password)
202 self.Usernames[username] = None
203 self.changeTask(self.requestStratumUA, 0)
206 def _stratum_mining_get_transactions(self, jobid):
208 (MC, wld) = self.server.getExistingStratumJob(jobid)
209 except KeyError as e:
210 e.StratumQuiet = True
212 (height, merkleTree, cb, prevBlock, bits) = MC[:5]
213 return list(b2a_hex(txn.data).decode('ascii') for txn in merkleTree.data[1:])
215 class StratumServer(networkserver.AsyncSocketServer):
216 logger = logging.getLogger('StratumServer')
221 extranonce1null = struct.pack('@P', 0)
223 def __init__(self, *a, **ka):
224 ka.setdefault('RequestHandlerClass', StratumHandler)
225 super().__init__(*a, **ka)
229 self.JobId = '%d' % (time(),)
230 self.WakeRequest = None
231 self.UpdateTask = None
233 def updateJob(self, wantClear = False):
236 self.rmSchedule(self.UpdateTask)
241 JobId = '%d %d' % (time(), self._JobId)
242 (MC, wld) = self.getStratumJob(JobId, wantClear=wantClear)
243 (height, merkleTree, cb, prevBlock, bits) = MC[:5]
245 if len(cb) > 96 - len(self.extranonce1null) - 4:
246 if not self.rejecting:
247 self.logger.warning('Coinbase too big for stratum: disabling')
248 self.rejecting = True
250 self.UpdateTask = self.schedule(self.updateJob, time() + 10)
253 self.rejecting = False
254 self.logger.info('Coinbase small enough for stratum again: reenabling')
256 txn = deepcopy(merkleTree.data[0])
257 cb += self.extranonce1null + b'Eloi'
260 pos = txn.data.index(cb) + len(cb)
262 steps = list(b2a_hex(h).decode('ascii') for h in merkleTree._steps)
264 self.JobBytes = json.dumps({
266 'method': 'mining.notify',
269 b2a_hex(swap32(prevBlock)).decode('ascii'),
270 b2a_hex(txn.data[:pos - len(self.extranonce1null) - 4]).decode('ascii'),
271 b2a_hex(txn.data[pos:]).decode('ascii'),
274 b2a_hex(bits[::-1]).decode('ascii'),
275 b2a_hex(struct.pack('>L', int(time()))).decode('ascii'),
276 not self.IsJobValid(self.JobId)
278 }).encode('ascii') + b"\n"
284 self.UpdateTask = self.schedule(self.updateJob, time() + 55)
286 def pre_schedule(self):
290 def _wakeNodes(self):
291 self.WakeRequest = None
294 self.logger.debug('Nobody to wake up')
297 self.logger.debug("%d clients to wake up..." % (OC,))
301 for ic in list(C.values()):
306 # Ignore socket errors; let the main event loop take care of them later
309 self.logger.debug('Error sending new job:\n' + traceback.format_exc())
311 self.logger.debug('New job sent to %d clients in %.3f seconds' % (OC, time() - now))
313 def getTarget(*a, **ka):