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
18 from copy import deepcopy
26 from util import RejectedShare, swap32
28 class StratumError(BaseException):
29 def __init__(self, errno, msg):
30 self.StratumErrNo = errno
31 self.StratumErrMsg = msg
41 class StratumHandler(networkserver.SocketHandler):
42 logger = logging.getLogger('StratumHandler')
44 def __init__(self, *a, **ka):
45 super().__init__(*a, **ka)
46 self.remoteHost = self.addr[0]
48 self.set_terminator(b"\n")
51 def sendReply(self, ob):
52 return self.push(json.dumps(ob).encode('ascii') + b"\n")
54 def found_terminator(self):
55 inbuf = b"".join(self.incoming).decode('ascii')
61 rpc = json.loads(inbuf)
62 funcname = '_stratum_%s' % (rpc['method'].replace('.', '_'),)
63 if not hasattr(self, funcname):
65 'error': [-3, "Method '%s' not found" % (rpc['method'],), None],
72 rv = getattr(self, funcname)(*rpc['params'])
73 except StratumError as e:
75 'error': (e.StratumErrNo, e.StratumErrMsg, None),
91 self.push(self.server.JobBytes)
93 def _stratum_mining_subscribe(self):
94 xid = struct.pack('@P', id(self))
95 self.extranonce1 = xid
96 xid = b2a_hex(xid).decode('ascii')
97 self.server._Clients[id(self)] = self
98 self.changeTask(self.sendJob, 0)
101 ['mining.notify', '%s1' % (xid,)],
102 ['mining.set_difficulty', '%s2' % (xid,)],
108 def handle_close(self):
110 del self.server._Clients[id(self)]
113 super().handle_close()
115 def _stratum_mining_submit(self, username, jobid, extranonce2, ntime, nonce):
116 if username not in self.Usernames:
119 'username': username,
120 'remoteHost': self.remoteHost,
122 'extranonce1': self.extranonce1,
123 'extranonce2': bytes.fromhex(extranonce2),
124 'ntime': bytes.fromhex(ntime),
125 'nonce': bytes.fromhex(nonce),
128 self.server.receiveShare(share)
129 except RejectedShare as rej:
131 errno = StratumCodes.get(rej, 20)
132 raise StratumError(errno, rej)
135 def checkAuthentication(self, username, password):
138 def _stratum_mining_authorize(self, username, password = None):
140 valid = self.checkAuthentication(username, password)
144 self.Usernames[username] = None
147 class StratumServer(networkserver.AsyncSocketServer):
148 logger = logging.getLogger('StratumServer')
152 extranonce1null = struct.pack('@P', 0)
154 def __init__(self, *a, **ka):
155 ka.setdefault('RequestHandlerClass', StratumHandler)
156 super().__init__(*a, **ka)
160 self.JobId = '%d' % (time(),)
161 self.WakeRequest = None
163 def updateJob(self, wantClear = False):
165 JobId = '%d %d' % (time(), self._JobId)
166 (MC, wld) = self.getStratumJob(JobId, wantClear=wantClear)
167 (height, merkleTree, cb, prevBlock, bits) = MC[:5]
169 if len(cb) > 96 - len(self.extranonce1null) - 4:
170 self.logger.warning('Coinbase too big for Stratum: GIVING CLIENTS INVALID JOBS')
171 # TODO: shutdown stratum
172 # TODO: restart automatically when coinbase works?
174 txn = deepcopy(merkleTree.data[0])
175 cb += self.extranonce1null + b'Eloi'
178 pos = txn.data.index(cb) + len(cb)
180 steps = list(b2a_hex(h).decode('ascii') for h in merkleTree._steps)
182 self.JobBytes = json.dumps({
184 'method': 'mining.notify',
187 b2a_hex(swap32(prevBlock)).decode('ascii'),
188 b2a_hex(txn.data[:pos - len(self.extranonce1null) - 4]).decode('ascii'),
189 b2a_hex(txn.data[pos:]).decode('ascii'),
192 b2a_hex(bits[::-1]).decode('ascii'),
193 b2a_hex(struct.pack('>L', int(time()))).decode('ascii'),
196 }).encode('ascii') + b"\n"
202 def pre_schedule(self):
206 def _wakeNodes(self):
207 self.WakeRequest = None
210 self.logger.info('Nobody to wake up')
213 self.logger.debug("%d clients to wake up..." % (OC,))
217 for ic in list(C.values()):
222 # Ignore socket errors; let the main event loop take care of them later
225 self.logger.debug('Error sending new job:\n' + traceback.format_exc())
227 self.logger.info('New job sent to %d clients in %.3f seconds' % (OC, time() - now))