1 # Eloipool - Python Bitcoin pool server
2 # Copyright (C) 2011-2012 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, UniqueSessionIdManager
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()
55 def sendReply(self, ob):
56 return self.push(json.dumps(ob).encode('ascii') + b"\n")
58 def found_terminator(self):
59 inbuf = b"".join(self.incoming).decode('ascii')
66 rpc = json.loads(inbuf)
70 funcname = '_stratum_%s' % (rpc['method'].replace('.', '_'),)
71 if not hasattr(self, funcname):
73 'error': [-3, "Method '%s' not found" % (rpc['method'],), None],
80 rv = getattr(self, funcname)(*rpc['params'])
81 except StratumError as e:
83 'error': (e.StratumErrNo, e.StratumErrMsg, traceback.format_exc() if e.StratumTB else None),
88 except BaseException as e:
89 fexc = traceback.format_exc()
91 'error': (20, str(e), fexc),
95 if not hasattr(e, 'StratumQuiet'):
96 self.logger.debug(fexc)
106 target = self.server.defaultTarget
107 if len(self.Usernames) == 1:
108 dtarget = self.server.getTarget(next(iter(self.Usernames)), time())
109 if not dtarget is None:
111 bdiff = target2bdiff(target)
112 if self.lastBDiff != bdiff:
115 'method': 'mining.set_difficulty',
120 self.lastBDiff = bdiff
121 self.push(self.server.JobBytes)
122 if len(self.JobTargets) > 4:
123 self.JobTargets.popitem(False)
124 self.JobTargets[self.server.JobId] = target
126 def _stratum_mining_subscribe(self):
127 if not hasattr(self, '_sid'):
128 self._sid = UniqueSessionIdManager.get()
129 xid = struct.pack('=I', self._sid) # NOTE: Assumes sessionids are 4 bytes
130 self.extranonce1 = xid
131 xid = b2a_hex(xid).decode('ascii')
132 self.server._Clients[id(self)] = self
133 self.changeTask(self.sendJob, 0)
136 ['mining.notify', '%s1' % (xid,)],
137 ['mining.set_difficulty', '%s2' % (xid,)],
143 def handle_close(self):
144 if hasattr(self, '_sid'):
145 UniqueSessionIdManager.put(self._sid)
146 delattr(self, '_sid')
148 del self.server._Clients[id(self)]
151 super().handle_close()
153 def _stratum_mining_submit(self, username, jobid, extranonce2, ntime, nonce):
154 if username not in self.Usernames:
155 raise StratumError(24, 'unauthorized-user', False)
157 'username': username,
158 'remoteHost': self.remoteHost,
160 'extranonce1': self.extranonce1,
161 'extranonce2': bytes.fromhex(extranonce2),
162 'ntime': bytes.fromhex(ntime),
163 'nonce': bytes.fromhex(nonce),
165 if jobid in self.JobTargets:
166 share['target'] = self.JobTargets[jobid]
168 self.server.receiveShare(share)
169 except RejectedShare as rej:
171 errno = StratumCodes.get(rej, 20)
172 raise StratumError(errno, rej, False)
175 def checkAuthentication(self, username, password):
178 def _stratum_mining_authorize(self, username, password = None):
180 valid = self.checkAuthentication(username, password)
184 self.Usernames[username] = None
187 def _stratum_mining_get_transactions(self, jobid):
189 (MC, wld) = self.server.getExistingStratumJob(jobid)
190 except KeyError as e:
191 e.StratumQuiet = True
193 (height, merkleTree, cb, prevBlock, bits) = MC[:5]
194 return list(b2a_hex(txn.data).decode('ascii') for txn in merkleTree.data[1:])
196 class StratumServer(networkserver.AsyncSocketServer):
197 logger = logging.getLogger('StratumServer')
202 extranonce1null = struct.pack('=I', 0) # NOTE: Assumes sessionids are 4 bytes
204 def __init__(self, *a, **ka):
205 ka.setdefault('RequestHandlerClass', StratumHandler)
206 super().__init__(*a, **ka)
210 self.JobId = '%d' % (time(),)
211 self.WakeRequest = None
212 self.UpdateTask = None
214 def updateJob(self, wantClear = False):
217 self.rmSchedule(self.UpdateTask)
222 JobId = '%d %d' % (time(), self._JobId)
223 (MC, wld) = self.getStratumJob(JobId, wantClear=wantClear)
224 (height, merkleTree, cb, prevBlock, bits) = MC[:5]
226 if len(cb) > 96 - len(self.extranonce1null) - 4:
227 if not self.rejecting:
228 self.logger.warning('Coinbase too big for stratum: disabling')
229 self.rejecting = True
231 self.UpdateTask = self.schedule(self.updateJob, time() + 10)
234 self.rejecting = False
235 self.logger.info('Coinbase small enough for stratum again: reenabling')
237 txn = deepcopy(merkleTree.data[0])
238 cb += self.extranonce1null + b'Eloi'
241 pos = txn.data.index(cb) + len(cb)
243 steps = list(b2a_hex(h).decode('ascii') for h in merkleTree._steps)
245 self.JobBytes = json.dumps({
247 'method': 'mining.notify',
250 b2a_hex(swap32(prevBlock)).decode('ascii'),
251 b2a_hex(txn.data[:pos - len(self.extranonce1null) - 4]).decode('ascii'),
252 b2a_hex(txn.data[pos:]).decode('ascii'),
255 b2a_hex(bits[::-1]).decode('ascii'),
256 b2a_hex(struct.pack('>L', int(time()))).decode('ascii'),
257 not self.IsJobValid(self.JobId)
259 }).encode('ascii') + b"\n"
265 self.UpdateTask = self.schedule(self.updateJob, time() + 55)
267 def pre_schedule(self):
271 def _wakeNodes(self):
272 self.WakeRequest = None
275 self.logger.debug('Nobody to wake up')
278 self.logger.debug("%d clients to wake up..." % (OC,))
282 for ic in list(C.values()):
287 # Ignore socket errors; let the main event loop take care of them later
290 self.logger.debug('Error sending new job:\n' + traceback.format_exc())
292 self.logger.debug('New job sent to %d clients in %.3f seconds' % (OC, time() - now))
294 def getTarget(*a, **ka):