stratum-mining/mining/service.py

188 lines
9.5 KiB
Python

import binascii
from twisted.internet import defer
import lib.settings as settings
from stratum.services import GenericService, admin
from stratum.pubsub import Pubsub
from interfaces import Interfaces
from subscription import MiningSubscription
from lib.exceptions import SubmitException
import lib.logger
log = lib.logger.get_logger('mining')
class MiningService(GenericService):
'''This service provides public API for Stratum mining proxy
or any Stratum-compatible miner software.
Warning - any callable argument of this class will be propagated
over Stratum protocol for public audience!'''
service_type = 'mining'
service_vendor = 'stratum'
is_default = True
@admin
def update_block(self):
'''Connect this RPC call to 'litecoind -blocknotify' for
instant notification about new block on the network.
See blocknotify.sh in /scripts/ for more info.'''
log.info("New block notification received")
Interfaces.template_registry.update_block()
return True
@admin
def add_litecoind(self, *args):
''' Function to add a litecoind instance live '''
if len(args) != 4:
raise SubmitException("Incorrect number of parameters sent")
#(host, port, user, password) = args
Interfaces.template_registry.bitcoin_rpc.add_connection(args[0], args[1], args[2], args[3])
log.info("New litecoind connection added %s:%s" % (args[0], args[1]))
return True
def authorize(self, worker_name, worker_password):
'''Let authorize worker on this connection.'''
session = self.connection_ref().get_session()
session.setdefault('authorized', {})
if Interfaces.worker_manager.authorize(worker_name, worker_password):
session['authorized'][worker_name] = worker_password
is_ext_diff = False
if settings.ALLOW_EXTERNAL_DIFFICULTY:
(is_ext_diff, session['difficulty']) = Interfaces.worker_manager.get_user_difficulty(worker_name)
self.connection_ref().rpc('mining.set_difficulty', [session['difficulty'], ], is_notification=True)
else:
session['difficulty'] = settings.POOL_TARGET
# worker_log = (valid, invalid, is_banned, diff, is_ext_diff, timestamp)
Interfaces.worker_manager.worker_log['authorized'][worker_name] = (0, 0, False, session['difficulty'], is_ext_diff, Interfaces.timestamper.time())
return True
else:
if worker_name in session['authorized']:
del session['authorized'][worker_name]
if worker_name in Interfaces.worker_manager.worker_log['authorized']:
del Interfaces.worker_manager.worker_log['authorized'][worker_name]
return False
def subscribe(self, *args):
'''Subscribe for receiving mining jobs. This will
return subscription details, extranonce1_hex and extranonce2_size'''
extranonce1 = Interfaces.template_registry.get_new_extranonce1()
extranonce2_size = Interfaces.template_registry.extranonce2_size
extranonce1_hex = binascii.hexlify(extranonce1)
session = self.connection_ref().get_session()
session['extranonce1'] = extranonce1
session['difficulty'] = settings.POOL_TARGET # Following protocol specs, default diff is 1
return Pubsub.subscribe(self.connection_ref(), MiningSubscription()) + (extranonce1_hex, extranonce2_size)
def submit(self, worker_name, work_id, extranonce2, ntime, nonce):
'''Try to solve block candidate using given parameters.'''
session = self.connection_ref().get_session()
session.setdefault('authorized', {})
# Check if worker is authorized to submit shares
if not Interfaces.worker_manager.authorize(worker_name, session['authorized'].get(worker_name)):
raise SubmitException("Worker is not authorized")
# Check if extranonce1 is in connection session
extranonce1_bin = session.get('extranonce1', None)
if not extranonce1_bin:
raise SubmitException("Connection is not subscribed for mining")
# Get current block job_id
difficulty = session['difficulty']
if worker_name in Interfaces.worker_manager.job_log and work_id in Interfaces.worker_manager.job_log[worker_name]:
(job_id, difficulty, job_ts) = Interfaces.worker_manager.job_log[worker_name][work_id]
else:
job_ts = Interfaces.timestamper.time()
Interfaces.worker_manager.job_log.setdefault(worker_name, {})[work_id] = (work_id, difficulty, job_ts)
job_id = work_id
#log.debug("worker_job_log: %s" % repr(Interfaces.worker_manager.job_log))
submit_time = Interfaces.timestamper.time()
ip = self.connection_ref()._get_ip()
(valid, invalid, is_banned, diff, is_ext_diff, last_ts) = Interfaces.worker_manager.worker_log['authorized'][worker_name]
percent = float(float(invalid) / (float(valid) if valid else 1) * 100)
if is_banned and submit_time - last_ts > settings.WORKER_BAN_TIME:
if percent > settings.INVALID_SHARES_PERCENT:
log.debug("Worker invalid percent: %0.2f %s STILL BANNED!" % (percent, worker_name))
else:
is_banned = False
log.debug("Clearing ban for worker: %s UNBANNED" % worker_name)
(valid, invalid, is_banned, last_ts) = (0, 0, is_banned, Interfaces.timestamper.time())
if submit_time - last_ts > settings.WORKER_CACHE_TIME and not is_banned:
if percent > settings.INVALID_SHARES_PERCENT and settings.ENABLE_WORKER_BANNING:
is_banned = True
log.debug("Worker invalid percent: %0.2f %s BANNED!" % (percent, worker_name))
else:
log.debug("Clearing worker stats for: %s" % worker_name)
(valid, invalid, is_banned, last_ts) = (0, 0, is_banned, Interfaces.timestamper.time())
log.debug("%s (%d, %d, %s, %s, %d) %0.2f%% work_id(%s) job_id(%s) diff(%f)" % (worker_name, valid, invalid, is_banned, is_ext_diff, last_ts, percent, work_id, job_id, difficulty))
if not is_ext_diff:
Interfaces.share_limiter.submit(self.connection_ref, job_id, difficulty, submit_time, worker_name)
# This checks if submitted share meet all requirements
# and it is valid proof of work.
try:
(block_header, block_hash, share_diff, on_submit) = Interfaces.template_registry.submit_share(job_id,
worker_name, session, extranonce1_bin, extranonce2, ntime, nonce, difficulty)
except SubmitException as e:
# block_header and block_hash are None when submitted data are corrupted
invalid += 1
Interfaces.worker_manager.worker_log['authorized'][worker_name] = (valid, invalid, is_banned, difficulty, is_ext_diff, last_ts)
if is_banned:
raise SubmitException("Worker is temporarily banned")
Interfaces.share_manager.on_submit_share(worker_name, False, False, difficulty,
submit_time, False, ip, e[0], 0)
raise
valid += 1
Interfaces.worker_manager.worker_log['authorized'][worker_name] = (valid, invalid, is_banned, difficulty, is_ext_diff, last_ts)
if is_banned:
raise SubmitException("Worker is temporarily banned")
Interfaces.share_manager.on_submit_share(worker_name, block_header,
block_hash, difficulty, submit_time, True, ip, '', share_diff)
if on_submit != None:
# Pool performs submitblock() to litecoind. Let's hook
# to result and report it to share manager
on_submit.addCallback(Interfaces.share_manager.on_submit_block,
worker_name, block_header, block_hash, submit_time, ip, share_diff)
return True
# Service documentation for remote discovery
update_block.help_text = "Notify Stratum server about new block on the network."
update_block.params = [('password', 'string', 'Administrator password'), ]
authorize.help_text = "Authorize worker for submitting shares on this connection."
authorize.params = [('worker_name', 'string', 'Name of the worker, usually in the form of user_login.worker_id.'),
('worker_password', 'string', 'Worker password'), ]
subscribe.help_text = "Subscribes current connection for receiving new mining jobs."
subscribe.params = []
submit.help_text = "Submit solved share back to the server. Excessive sending of invalid shares "\
"or shares above indicated target (see Stratum mining docs for set_target()) may lead "\
"to temporary or permanent ban of user,worker or IP address."
submit.params = [('worker_name', 'string', 'Name of the worker, usually in the form of user_login.worker_id.'),
('job_id', 'string', 'ID of job (received by mining.notify) which the current solution is based on.'),
('extranonce2', 'string', 'hex-encoded big-endian extranonce2, length depends on extranonce2_size from mining.notify.'),
('ntime', 'string', 'UNIX timestamp (32bit integer, big-endian, hex-encoded), must be >= ntime provided by mining,notify and <= current time'),
('nonce', 'string', '32bit integer, hex-encoded, big-endian'), ]