├── debian ├── compat ├── dirs ├── source │ └── format ├── stratum-mining-proxy.default ├── stratum-mining-proxy.postrm ├── stratum-mining-proxy.logrotate ├── stratum-mining-proxy.preinst ├── stratum-mining-proxy.prerm ├── control ├── stratum-mining-proxy.postinst ├── rules ├── changelog └── stratum-mining-proxy.init ├── midstatec ├── __init__.py ├── Makefile ├── midstatec.py └── midstatemodule.c ├── mining_libs ├── __init__.py ├── version.py ├── worker_registry.py ├── multicast_responder.py ├── utils.py ├── midstate.py ├── client_service.py ├── jobs.py ├── stratum_listener.py └── getwork_listener.py ├── stratum ├── version.py ├── __init__.py ├── storage.py ├── event_handler.py ├── connection_registry.py ├── stats.py ├── logger.py ├── websocket_transport.py ├── custom_exceptions.py ├── semaphore.py ├── settings.py ├── helpers.py ├── example_service.py ├── jsonical.py ├── socksclient.py ├── irc.py ├── signature.py ├── server.py ├── socket_transport.py ├── config_default.py ├── pubsub.py ├── http_transport.py ├── services.py └── protocol.py ├── .gitignore ├── setup.py ├── README.md ├── example_multicast.py ├── mining_proxy.py └── distribute_setup.py /debian/compat: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /midstatec/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mining_libs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/dirs: -------------------------------------------------------------------------------- 1 | usr/sbin 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 1.0 2 | -------------------------------------------------------------------------------- /stratum/version.py: -------------------------------------------------------------------------------- 1 | VERSION='0.2.15' 2 | -------------------------------------------------------------------------------- /stratum/__init__.py: -------------------------------------------------------------------------------- 1 | from server import setup 2 | -------------------------------------------------------------------------------- /mining_libs/version.py: -------------------------------------------------------------------------------- 1 | # last stable: 1.5.5 2 | VERSION='1.5.7' 3 | -------------------------------------------------------------------------------- /debian/stratum-mining-proxy.default: -------------------------------------------------------------------------------- 1 | #POOL_HOST=stratum.bitcoin.cz 2 | #POOL_PORT=3333 3 | #SCRYPT_TARGET=1 4 | #CUSTOM_USER=... 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | dist 4 | distribute* 5 | *egg-info* 6 | debian/files 7 | debian/*/ 8 | debian/*.debhelper* 9 | debian/*.substvars 10 | log 11 | *.bat -------------------------------------------------------------------------------- /debian/stratum-mining-proxy.postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Automatically added by dh_installinit 3 | if [ "$1" = "purge" ] ; then 4 | update-rc.d stratum-mining-proxy remove >/dev/null 5 | fi 6 | # End automatically added section 7 | -------------------------------------------------------------------------------- /debian/stratum-mining-proxy.logrotate: -------------------------------------------------------------------------------- 1 | /var/log/stratum-mining-proxy.log 2 | { 3 | rotate 7 4 | daily 5 | missingok 6 | notifempty 7 | delaycompress 8 | compress 9 | postrotate 10 | invoke-rc.d stratum-mining-proxy restart > /dev/null 11 | endscript 12 | } 13 | -------------------------------------------------------------------------------- /midstatec/Makefile: -------------------------------------------------------------------------------- 1 | CC = gcc 2 | CFLAGS = -march=native -Wall -funroll-all-loops -O3 -fstrict-aliasing -Wall -std=c99 -I/usr/include/python2.7 3 | LDFLAGS = -Wl,-O1 -Wl,--as-needed -lpython2.7 4 | 5 | all: test midstate.so 6 | 7 | test: midstatemodule.c 8 | $(CC) $(CFLAGS) midstatemodule.c -o test $(LDFLAGS) 9 | 10 | midstate.so: midstatemodule.c 11 | $(CC) $(CFLAGS) -fPIC -shared midstatemodule.c -o midstate.so $(LDFLAGS) 12 | 13 | .PHONY: clean 14 | 15 | clean: 16 | rm -f midstate.so test 17 | -------------------------------------------------------------------------------- /debian/stratum-mining-proxy.preinst: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | # This was added by stdeb to workaround Debian #479852. In a nutshell, 6 | # pycentral does not remove normally remove its symlinks on an 7 | # upgrade. Since we're using python-support, however, those symlinks 8 | # will be broken. This tells python-central to clean up any symlinks. 9 | if [ -e /var/lib/dpkg/info/stratum-mining-proxy.list ] && which pycentral >/dev/null 2>&1 10 | then 11 | pycentral pkgremove stratum-mining-proxy 12 | fi 13 | 14 | #DEBHELPER# 15 | -------------------------------------------------------------------------------- /debian/stratum-mining-proxy.prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Automatically added by dh_installinit 3 | if [ -x "/etc/init.d/stratum-mining-proxy" ]; then 4 | if [ -x "`which invoke-rc.d 2>/dev/null`" ]; then 5 | invoke-rc.d stratum-mining-proxy stop || exit $? 6 | else 7 | /etc/init.d/stratum-mining-proxy stop || exit $? 8 | fi 9 | fi 10 | # End automatically added section 11 | # Automatically added by dh_pysupport 12 | if which update-python-modules >/dev/null 2>&1; then 13 | update-python-modules -c stratum-mining-proxy.public 14 | fi 15 | # End automatically added section 16 | -------------------------------------------------------------------------------- /stratum/storage.py: -------------------------------------------------------------------------------- 1 | #class StorageFactory(object): 2 | 3 | class Storage(object): 4 | #def __new__(self, session_id): 5 | # pass 6 | 7 | def __init__(self): 8 | self.__services = {} 9 | self.session = None 10 | 11 | def get(self, service_type, vendor, default_object): 12 | self.__services.setdefault(service_type, {}) 13 | self.__services[service_type].setdefault(vendor, default_object) 14 | return self.__services[service_type][vendor] 15 | 16 | def __repr__(self): 17 | return str(self.__services) -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: stratum-mining-proxy 2 | Maintainer: Corey Ralph 3 | Section: python 4 | Priority: optional 5 | Build-Depends: python-setuptools (>= 0.6b3), debhelper (>= 7), python-support (>= 0.8.4), libssl-dev, python-dev 6 | Standards-Version: 3.8.4 7 | XS-Python-Version: current 8 | 9 | Package: stratum-mining-proxy 10 | Architecture: i386 amd64 11 | Depends: ${misc:Depends}, ${python:Depends}, python-stratum, python-twisted-web, python-ecdsa, python-argparse 12 | XB-Python-Version: ${python:Versions} 13 | Provides: ${python:Provides} 14 | Description: Getwork-compatible proxy for Stratum mining pools 15 | -------------------------------------------------------------------------------- /debian/stratum-mining-proxy.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Automatically added by dh_pysupport 3 | if which update-python-modules >/dev/null 2>&1; then 4 | update-python-modules stratum-mining-proxy.public 5 | fi 6 | # End automatically added section 7 | # Automatically added by dh_installinit 8 | if [ -x "/etc/init.d/stratum-mining-proxy" ]; then 9 | update-rc.d stratum-mining-proxy defaults >/dev/null 10 | if [ -x "`which invoke-rc.d 2>/dev/null`" ]; then 11 | invoke-rc.d stratum-mining-proxy start || exit $? 12 | else 13 | /etc/init.d/stratum-mining-proxy start || exit $? 14 | fi 15 | fi 16 | # End automatically added section 17 | -------------------------------------------------------------------------------- /stratum/event_handler.py: -------------------------------------------------------------------------------- 1 | import custom_exceptions 2 | from twisted.internet import defer 3 | from services import wrap_result_object 4 | 5 | class GenericEventHandler(object): 6 | def _handle_event(self, msg_method, msg_params, connection_ref): 7 | return defer.maybeDeferred(wrap_result_object, self.handle_event(msg_method, msg_params, connection_ref)) 8 | 9 | def handle_event(self, msg_method, msg_params, connection_ref): 10 | '''In most cases you'll only need to overload this method.''' 11 | print "Other side called method", msg_method, "with params", msg_params 12 | raise custom_exceptions.MethodNotFoundException("Method '%s' not implemented" % msg_method) -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # This file was automatically generated by stdeb 0.6.0 at 4 | # Tue, 28 Jan 2014 15:45:25 +1100 5 | 6 | # Unset the environment variables set by dpkg-buildpackage. (This is 7 | # necessary because distutils is brittle with compiler/linker flags 8 | # set. Specifically, packages using f2py will break without this.) 9 | unexport CPPFLAGS 10 | unexport CFLAGS 11 | unexport CXXFLAGS 12 | unexport FFLAGS 13 | unexport LDFLAGS 14 | 15 | #exports specified using stdeb Setup-Env-Vars: 16 | #export DH_OPTIONS=--buildsystem=python_distutils 17 | 18 | %: 19 | dh $@ 20 | 21 | override_dh_install: 22 | dh_install 23 | # Rename binary to match package name and move to sbin 24 | mv debian/stratum-mining-proxy/usr/bin/mining_proxy.py debian/stratum-mining-proxy/usr/sbin/stratum-mining-proxy 25 | 26 | override_dh_clean: 27 | dh_clean 28 | # distribute module is downloaded during build 29 | rm -f distribute-*.egg distribute-*.tar.gz 30 | rm -rf stratum_mining_proxy.egg-info 31 | -------------------------------------------------------------------------------- /midstatec/midstatec.py: -------------------------------------------------------------------------------- 1 | # Original source: https://gitorious.org/midstate/midstate 2 | 3 | import struct 4 | import binascii 5 | from midstate import SHA256 6 | 7 | test_data = binascii.unhexlify("0000000293d5a732e749dbb3ea84318bd0219240a2e2945046015880000003f5000000008d8e2673e5a071a2c83c86e28033b1a0a4aac90dde7a0670827cd0c3ef8caf7d5076c7b91a057e0800000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000080020000") 8 | test_target_midstate = binascii.unhexlify("4c8226f95a31c9619f5197809270e4fa0a2d34c10215cf4456325e1237cb009d") 9 | 10 | 11 | def midstate(data): 12 | reversed = struct.pack('>IIIIIIIIIIIIIIII', *struct.unpack('>IIIIIIIIIIIIIIII', data[:64])[::-1])[::-1] 13 | return struct.pack('=0.6c11', 'twisted>=12.2.0', 'stratum>=0.2.15', 'argparse'], 28 | 'scripts': ['mining_proxy.py'], 29 | } 30 | 31 | if py2exe != None: 32 | args.update({ 33 | # py2exe options 34 | 'options': {'py2exe': 35 | {'optimize': 2, 36 | 'bundle_files': 1, 37 | 'compressed': True, 38 | 'dll_excludes': ['mswsock.dll', 'powrprof.dll'], 39 | }, 40 | }, 41 | 'console': ['mining_proxy.py'], 42 | 'zipfile': None, 43 | }) 44 | 45 | setup(**args) 46 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | stratum-mining-proxy (1.5.7-1) unstable; urgency=low 2 | 3 | * Make midstatec module installable in debian. 4 | 5 | -- Jacek Krysztofik Fri, 9 May 2014 23:59:59 +0200 6 | 7 | stratum-mining-proxy (1.5.6-1) unstable; urgency=low 8 | 9 | * Add CUSTOM_USER and SCRYPT_TARGET options in default file 10 | 11 | -- Marek Palatinus Sun, 9 Feb 2014 18:36:00 +0000 12 | 13 | stratum-mining-proxy (1.5.2-3) unstable; urgency=low 14 | 15 | * Add CUSTOM_USER and SCRYPT_TARGET options in default file 16 | 17 | -- Corey Ralph Wed, 29 Jan 2014 15:16:18 +1100 18 | 19 | stratum-mining-proxy (1.5.2-2) unstable; urgency=low 20 | 21 | * Add dependency on python-argparse 22 | 23 | -- Corey Ralph Wed, 29 Jan 2014 12:19:12 +1100 24 | 25 | stratum-mining-proxy (1.5.2-1.3) unstable; urgency=low 26 | 27 | * Clean egg-info build artifact 28 | 29 | -- Corey Ralph Wed, 29 Jan 2014 11:36:47 +1100 30 | 31 | stratum-mining-proxy (1.5.2-1.2) unstable; urgency=low 32 | 33 | * Fix comments in init script and change defaults 34 | 35 | -- Corey Ralph Wed, 29 Jan 2014 11:16:24 +1100 36 | 37 | stratum-mining-proxy (1.5.2-1.1) unstable; urgency=low 38 | 39 | * Add logrotate config, init and defaults 40 | 41 | -- Corey Ralph Wed, 29 Jan 2014 10:44:27 +1100 42 | 43 | stratum-mining-proxy (1.5.2-1) unstable; urgency=low 44 | 45 | * source package automatically created by stdeb 0.6.0 46 | 47 | -- Corey Ralph Tue, 28 Jan 2014 15:45:25 +1100 48 | -------------------------------------------------------------------------------- /stratum/semaphore.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import defer 2 | 3 | class Semaphore: 4 | """A semaphore for event driven systems.""" 5 | 6 | def __init__(self, tokens): 7 | self.waiting = [] 8 | self.tokens = tokens 9 | self.limit = tokens 10 | 11 | def is_locked(self): 12 | return (bool)(not self.tokens) 13 | 14 | def acquire(self): 15 | """Attempt to acquire the token. 16 | 17 | @return Deferred which returns on token acquisition. 18 | """ 19 | assert self.tokens >= 0 20 | d = defer.Deferred() 21 | if not self.tokens: 22 | self.waiting.append(d) 23 | else: 24 | self.tokens = self.tokens - 1 25 | d.callback(self) 26 | return d 27 | 28 | def release(self): 29 | """Release the token. 30 | 31 | Should be called by whoever did the acquire() when the shared 32 | resource is free. 33 | """ 34 | assert self.tokens < self.limit 35 | self.tokens = self.tokens + 1 36 | if self.waiting: 37 | # someone is waiting to acquire token 38 | self.tokens = self.tokens - 1 39 | d = self.waiting.pop(0) 40 | d.callback(self) 41 | 42 | def _releaseAndReturn(self, r): 43 | self.release() 44 | return r 45 | 46 | def run(self, f, *args, **kwargs): 47 | """Acquire token, run function, release token. 48 | 49 | @return Deferred of function result. 50 | """ 51 | d = self.acquire() 52 | d.addCallback(lambda r: defer.maybeDeferred(f, *args, 53 | **kwargs).addBoth(self._releaseAndReturn)) 54 | return d 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | eth-stratum-mining-proxy 2 | ==================== 3 | 4 | This is fork of proxy created by Slush. 5 | 6 | Application providing bridge between Ethereum HTTP/getwork protocol and Stratum mining protocol 7 | as described here: http://mining.bitcoin.cz/stratum-mining. 8 | 9 | Installation on Windows 10 | ----------------------- 11 | 12 | 1. Download official Windows binaries (EXE) from https://github.com/Coinotron/eth-stratum-mining-proxy/releases/tag/v1.0 13 | 2. Open downloaded file. It will open console window. Using default settings, proxy connects to Coinotron 14 | 3. Another way to start proxy: mining_proxy.exe -o coinotron.com -p 3344 15 | 4. You can also download python sources from github and run: python mining_proxy.py -o coinotron.com -p 3344 16 | 4. If you want to connect to another pool or change other proxy settings ( for example define custom worker and password ), type "mining_proxy.exe --help" in console window. 17 | 18 | Installation on Linux 19 | --------------------------------------- 20 | 21 | 1. install twisted apt-get: 22 | install python-twisted 23 | 2. download https://github.com/coinotron/eth-stratum-mining-proxy/ 24 | 3. run proxy: 25 | python ./mining_proxy.py -o coinotron.com -p 3344 26 | 4. If you want to connect to another pool or change other proxy settings ( for example define custom worker and password ), type "mining_proxy.py --help". 27 | 28 | 29 | Mining GPU 30 | --------------------------------------- 31 | 32 | 1. Start mining proxy 33 | 2. Start ethminer with following parameters: 34 | ethminer.exe --farm-recheck 200 -G -F http://127.0.0.1:8332/workername:workerpassword 35 | 36 | Contact 37 | ------- 38 | 39 | You can contact the author by email coinotron(at)gmail.com. 40 | 41 | 42 | -------------------------------------------------------------------------------- /stratum/settings.py: -------------------------------------------------------------------------------- 1 | def setup(): 2 | ''' 3 | This will import modules config_default and config and move their variables 4 | into current module (variables in config have higher priority than config_default). 5 | Thanks to this, you can import settings anywhere in the application and you'll get 6 | actual application settings. 7 | 8 | This config is related to server side. You don't need config.py if you 9 | want to use client part only. 10 | ''' 11 | 12 | def read_values(cfg): 13 | for varname in cfg.__dict__.keys(): 14 | if varname.startswith('__'): 15 | continue 16 | 17 | value = getattr(cfg, varname) 18 | yield (varname, value) 19 | 20 | import config_default 21 | 22 | try: 23 | import config 24 | except ImportError: 25 | # Custom config not presented, but we can still use defaults 26 | config = None 27 | 28 | import sys 29 | module = sys.modules[__name__] 30 | 31 | for name,value in read_values(config_default): 32 | module.__dict__[name] = value 33 | 34 | changes = {} 35 | if config: 36 | for name,value in read_values(config): 37 | if value != module.__dict__.get(name, None): 38 | changes[name] = value 39 | module.__dict__[name] = value 40 | 41 | if module.__dict__['DEBUG'] and changes: 42 | print "----------------" 43 | print "Custom settings:" 44 | for k, v in changes.items(): 45 | if 'passw' in k.lower(): 46 | print k, ": ********" 47 | else: 48 | print k, ":", v 49 | print "----------------" 50 | 51 | setup() 52 | -------------------------------------------------------------------------------- /mining_libs/worker_registry.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import stratum.logger 4 | log = stratum.logger.get_logger('proxy') 5 | 6 | class WorkerRegistry(object): 7 | def __init__(self, f): 8 | self.f = f # Factory of Stratum client 9 | self.clear_authorizations() 10 | 11 | def clear_authorizations(self): 12 | self.authorized = [] 13 | self.unauthorized = [] 14 | self.last_failure = 0 15 | 16 | def _on_authorized(self, result, worker_name): 17 | if result == True: 18 | self.authorized.append(worker_name) 19 | else: 20 | self.unauthorized.append(worker_name) 21 | return result 22 | 23 | def _on_failure(self, failure, worker_name): 24 | log.exception("Cannot authorize worker '%s'" % worker_name) 25 | self.last_failure = time.time() 26 | 27 | def authorize(self, worker_name, password): 28 | if worker_name in self.authorized: 29 | return True 30 | 31 | if worker_name in self.unauthorized and time.time() - self.last_failure < 60: 32 | # Prevent flooding of mining.authorize() requests 33 | log.warning("Authentication of worker '%s' with password '%s' failed, next attempt in few seconds..." % \ 34 | (worker_name, password)) 35 | return False 36 | 37 | d = self.f.rpc('mining.authorize', [worker_name, password]) 38 | d.addCallback(self._on_authorized, worker_name) 39 | d.addErrback(self._on_failure, worker_name) 40 | return d 41 | 42 | def is_authorized(self, worker_name): 43 | return (worker_name in self.authorized) 44 | 45 | def is_unauthorized(self, worker_name): 46 | return (worker_name in self.unauthorized) 47 | -------------------------------------------------------------------------------- /mining_libs/multicast_responder.py: -------------------------------------------------------------------------------- 1 | import json 2 | from twisted.internet.protocol import DatagramProtocol 3 | 4 | import stratum.logger 5 | log = stratum.logger.get_logger('proxy') 6 | 7 | class MulticastResponder(DatagramProtocol): 8 | def __init__(self, pool_host, stratum_port, getwork_port): 9 | # Upstream Stratum host/port 10 | # Used for identifying the pool which we're connected to. 11 | # Some load balancing strategies can change the host/port 12 | # during the mining session (by mining.reconnect()), but this points 13 | # to initial host/port provided by user on cmdline or by X-Stratum 14 | self.pool_host = pool_host 15 | 16 | self.stratum_port = stratum_port 17 | self.getwork_port = getwork_port 18 | 19 | def startProtocol(self): 20 | # 239.0.0.0/8 are for private use within an organization 21 | self.transport.joinGroup("239.3.3.3") 22 | self.transport.setTTL(5) 23 | 24 | def writeResponse(self, address, msg_id, result, error=None): 25 | self.transport.write(json.dumps({"id": msg_id, "result": result, "error": error}), address) 26 | 27 | def datagramReceived(self, datagram, address): 28 | log.info("Received local discovery request from %s:%d" % address) 29 | 30 | try: 31 | data = json.loads(datagram) 32 | except: 33 | # Skip response if datagram is not parsable 34 | log.error("Unparsable datagram") 35 | return 36 | 37 | msg_id = data.get('id') 38 | msg_method = data.get('method') 39 | #msg_params = data.get('params') 40 | 41 | if msg_method == 'mining.get_upstream': 42 | self.writeResponse(address, msg_id, (self.pool_host, self.stratum_port, self.getwork_port)) -------------------------------------------------------------------------------- /stratum/helpers.py: -------------------------------------------------------------------------------- 1 | from zope.interface import implements 2 | from twisted.internet import defer 3 | from twisted.internet import reactor 4 | from twisted.internet.protocol import Protocol 5 | from twisted.web.iweb import IBodyProducer 6 | from twisted.web.client import Agent 7 | from twisted.web.http_headers import Headers 8 | 9 | import settings 10 | 11 | class ResponseCruncher(Protocol): 12 | '''Helper for get_page()''' 13 | def __init__(self, finished): 14 | self.finished = finished 15 | self.response = "" 16 | 17 | def dataReceived(self, data): 18 | self.response += data 19 | 20 | def connectionLost(self, reason): 21 | self.finished.callback(self.response) 22 | 23 | class StringProducer(object): 24 | '''Helper for get_page()''' 25 | implements(IBodyProducer) 26 | 27 | def __init__(self, body): 28 | self.body = body 29 | self.length = len(body) 30 | 31 | def startProducing(self, consumer): 32 | consumer.write(self.body) 33 | return defer.succeed(None) 34 | 35 | def pauseProducing(self): 36 | pass 37 | 38 | def stopProducing(self): 39 | pass 40 | 41 | @defer.inlineCallbacks 42 | def get_page(url, method='GET', payload=None, headers=None): 43 | '''Downloads the page from given URL, using asynchronous networking''' 44 | agent = Agent(reactor) 45 | 46 | producer = None 47 | if payload: 48 | producer = StringProducer(payload) 49 | 50 | _headers = {'User-Agent': [settings.USER_AGENT,]} 51 | if headers: 52 | for key, value in headers.items(): 53 | _headers[key] = [value,] 54 | 55 | response = (yield agent.request( 56 | method, 57 | str(url), 58 | Headers(_headers), 59 | producer)) 60 | 61 | #for h in response.headers.getAllRawHeaders(): 62 | # print h 63 | 64 | try: 65 | finished = defer.Deferred() 66 | (yield response).deliverBody(ResponseCruncher(finished)) 67 | except: 68 | raise Exception("Downloading page '%s' failed" % url) 69 | 70 | defer.returnValue((yield finished)) 71 | 72 | @defer.inlineCallbacks 73 | def ask_old_server(method, *args): 74 | '''Perform request in old protocol to electrum servers. 75 | This is deprecated, used only for proxying some calls.''' 76 | import urllib 77 | import ast 78 | 79 | # Hack for methods without arguments 80 | if not len(args): 81 | args = ['',] 82 | 83 | res = (yield get_page('http://electrum.bitcoin.cz/electrum.php', method='POST', 84 | headers={"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain"}, 85 | payload=urllib.urlencode({'q': repr([method,] + list(args))}))) 86 | 87 | try: 88 | data = ast.literal_eval(res) 89 | except SyntaxError: 90 | print "Data received from server:", res 91 | raise Exception("Corrupted data from old electrum server") 92 | defer.returnValue(data) 93 | -------------------------------------------------------------------------------- /example_multicast.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | This is just an example script for miner developers. 4 | If you're end user, you don't need to use this script. 5 | 6 | Detector of Stratum mining proxies on local network 7 | Copyright (C) 2012 Marek Palatinus 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program. If not, see . 21 | ''' 22 | 23 | from twisted.internet.protocol import DatagramProtocol 24 | from twisted.internet import reactor, defer 25 | 26 | import json 27 | 28 | class MulticastClient(DatagramProtocol): 29 | 30 | def startProtocol(self): 31 | self.transport.joinGroup("239.3.3.3") 32 | self.transport.write(json.dumps({"id": 0, "method": "mining.get_upstream", "params": []}), ('239.3.3.3', 3333)) 33 | 34 | def datagramReceived(self, datagram, address): 35 | '''Some data from peers received. 36 | 37 | Example of valid datagram: 38 | {"id": 0, "result": [["api-stratum.bitcoin.cz", 3333], 3333, 8332], "error": null} 39 | 40 | First argument - (host, port) of upstream pool 41 | Second argument - Stratum port where proxy is listening 42 | Third parameter - Getwork port where proxy is listening 43 | ''' 44 | #print "Datagram %s received from %s" % (datagram, address) 45 | 46 | try: 47 | data = json.loads(datagram) 48 | except: 49 | print "Unparsable datagram received" 50 | 51 | 52 | if data.get('id') != 0 or data.get('result') == None: 53 | return 54 | 55 | 56 | (proxy_host, proxy_port) = address 57 | (pool_host, pool_port) = data['result'][0] 58 | stratum_port = data['result'][1] 59 | getwork_port = data['result'][2] 60 | 61 | print "Found stratum proxy on %(proxy_host)s:%(stratum_port)d (stratum), "\ 62 | "%(proxy_host)s:%(getwork_port)d (getwork), "\ 63 | "mining for %(pool_host)s:%(pool_port)d" % \ 64 | {'proxy_host': proxy_host, 65 | 'pool_host': pool_host, 66 | 'pool_port': pool_port, 67 | 'stratum_port': stratum_port, 68 | 'getwork_port': getwork_port} 69 | 70 | def stop(): 71 | print "Local discovery of Stratum proxies is finished." 72 | reactor.stop() 73 | 74 | print "Listening for Stratum proxies on local network..." 75 | reactor.listenMulticast(3333, MulticastClient(), listenMultiple=True) 76 | reactor.callLater(5, stop) 77 | reactor.run() 78 | -------------------------------------------------------------------------------- /mining_libs/utils.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import struct 3 | 4 | from twisted.internet import defer, reactor 5 | from twisted.web import client 6 | 7 | import stratum.logger 8 | log = stratum.logger.get_logger('proxy') 9 | 10 | def show_message(msg): 11 | '''Repeatedly displays the message received from 12 | the server.''' 13 | log.warning("MESSAGE FROM THE SERVER OPERATOR: %s" % msg) 14 | log.warning("Restart proxy to discard the message") 15 | reactor.callLater(10, show_message, msg) 16 | 17 | def format_hash(h): 18 | # For printing hashes to console 19 | return "%s" % h[:8] 20 | 21 | def uint256_from_str(s): 22 | r = 0L 23 | t = struct.unpack(">= 32 33 | return rs 34 | 35 | def reverse_hash(h): 36 | return struct.pack('>IIIIIIII', *struct.unpack('>IIIIIIII', h)[::-1])[::-1] 37 | 38 | def doublesha(b): 39 | return hashlib.sha256(hashlib.sha256(b).digest()).digest() 40 | 41 | @defer.inlineCallbacks 42 | def detect_stratum(host, port): 43 | '''Perform getwork request to given 44 | host/port. If server respond, it will 45 | try to parse X-Stratum header. 46 | Not the most elegant code, but it works, 47 | because Stratum server should close the connection 48 | when client uses unknown payload.''' 49 | 50 | def get_raw_page(url, *args, **kwargs): 51 | # In Twisted 13.1.0 _parse() function replaced by _URI class. 52 | # In Twisted 15.0.0 _URI class renamed to URI. 53 | if hasattr(client, "_parse"): 54 | scheme, host, port, path = client._parse(url) 55 | else: 56 | try: 57 | from twisted.web.client import _URI as URI 58 | except ImportError: 59 | from twisted.web.client import URI 60 | 61 | uri = URI.fromBytes(url) 62 | scheme = uri.scheme 63 | host = uri.host 64 | port = uri.port 65 | 66 | factory = client.HTTPClientFactory(url, *args, **kwargs) 67 | reactor.connectTCP(host, port, factory) 68 | return factory 69 | 70 | def _on_callback(_, d):d.callback(True) 71 | def _on_errback(_, d): d.callback(True) 72 | f = get_raw_page('http://%s:%d' % (host, port)) 73 | 74 | d = defer.Deferred() 75 | f.deferred.addCallback(_on_callback, d) 76 | f.deferred.addErrback(_on_errback, d) 77 | (yield d) 78 | 79 | if not f.response_headers: 80 | # Most likely we're already connecting to Stratum 81 | defer.returnValue((host, port)) 82 | 83 | header = f.response_headers.get('x-stratum', None)[0] 84 | if not header: 85 | # Looks like pool doesn't support stratum 86 | defer.returnValue(None) 87 | 88 | if 'stratum+tcp://' not in header: 89 | # Invalid header or unsupported transport 90 | defer.returnValue(None) 91 | 92 | header = header.replace('stratum+tcp://', '').strip() 93 | host = header.split(':') 94 | 95 | if len(host) == 1: 96 | # Port is not specified 97 | defer.returnValue((host[0], 3333)) 98 | elif len(host) == 2: 99 | defer.returnValue((host[0], int(host[1]))) 100 | 101 | defer.returnValue(None) -------------------------------------------------------------------------------- /stratum/example_service.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import defer 2 | from twisted.internet import reactor 3 | from twisted.names import client 4 | import random 5 | import time 6 | 7 | from services import GenericService, signature, synchronous 8 | import pubsub 9 | 10 | import logger 11 | log = logger.get_logger('example') 12 | 13 | class ExampleService(GenericService): 14 | service_type = 'example' 15 | service_vendor = 'Stratum' 16 | is_default = True 17 | 18 | def hello_world(self): 19 | return "Hello world!" 20 | hello_world.help_text = "Returns string 'Hello world!'" 21 | hello_world.params = [] 22 | 23 | @signature 24 | def ping(self, payload): 25 | return payload 26 | ping.help_text = "Returns signed message with the payload given by the client." 27 | ping.params = [('payload', 'mixed', 'This payload will be sent back to the client.'),] 28 | 29 | @synchronous 30 | def synchronous(self, how_long): 31 | '''This can use blocking calls, because it runs in separate thread''' 32 | for _ in range(int(how_long)): 33 | time.sleep(1) 34 | return 'Request finished in %d seconds' % how_long 35 | synchronous.help_text = "Run time consuming algorithm in server's threadpool and return the result when it finish." 36 | synchronous.params = [('how_long', 'int', 'For how many seconds the algorithm should run.'),] 37 | 38 | def throw_exception(self): 39 | raise Exception("Some error") 40 | throw_exception.help_text = "Throw an exception and send error result to the client." 41 | throw_exception.params = [] 42 | 43 | @signature 44 | def throw_signed_exception(self): 45 | raise Exception("Some error") 46 | throw_signed_exception.help_text = "Throw an exception and send signed error result to the client." 47 | throw_signed_exception.params = [] 48 | 49 | class TimeSubscription(pubsub.Subscription): 50 | event = 'example.pubsub.time_event' 51 | 52 | def process(self, t): 53 | # Process must return list of parameters for notification 54 | # or None if notification should not be send 55 | if t % self.params.get('period', 1) == 0: 56 | return (t,) 57 | 58 | def after_subscribe(self, _): 59 | # Some objects want to fire up notification or other 60 | # action directly after client subscribes. 61 | # after_subscribe is the right place for such logic 62 | pass 63 | 64 | class PubsubExampleService(GenericService): 65 | service_type = 'example.pubsub' 66 | service_vendor = 'Stratum' 67 | is_default = True 68 | 69 | def _setup(self): 70 | self._emit_time_event() 71 | 72 | @pubsub.subscribe 73 | def subscribe(self, period): 74 | return TimeSubscription(period=period) 75 | subscribe.help_text = "Subscribe client for receiving current server's unix timestamp." 76 | subscribe.params = [('period', 'int', 'Broadcast to the client only if timestamp%period==0. Use 1 for receiving an event in every second.'),] 77 | 78 | @pubsub.unsubscribe 79 | def unsubscribe(self, subscription_key):#period): 80 | return subscription_key 81 | unsubscribe.help_text = "Stop broadcasting unix timestampt to the client." 82 | unsubscribe.params = [('subscription_key', 'string', 'Key obtained by calling of subscribe method.'),] 83 | 84 | def _emit_time_event(self): 85 | # This will emit a publish event, 86 | # so all subscribed clients will receive 87 | # the notification 88 | 89 | t = time.time() 90 | TimeSubscription.emit(int(t)) 91 | reactor.callLater(1, self._emit_time_event) 92 | 93 | # Let's print some nice stats 94 | cnt = pubsub.Pubsub.get_subscription_count('example.pubsub.time_event') 95 | if cnt: 96 | log.info("Example event emitted in %.03f sec to %d subscribers" % (time.time() - t, cnt)) -------------------------------------------------------------------------------- /stratum/jsonical.py: -------------------------------------------------------------------------------- 1 | # Copyright 2009 New England Biolabs 2 | # 3 | # This file is part of the nebgbhist package released under the MIT license. 4 | # 5 | r"""Canonical JSON serialization. 6 | 7 | Basic approaches for implementing canonical JSON serialization. 8 | 9 | Encoding basic Python object hierarchies:: 10 | 11 | >>> import jsonical 12 | >>> jsonical.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) 13 | '["foo",{"bar":["baz",null,1.0,2]}]' 14 | >>> print jsonical.dumps("\"foo\bar") 15 | "\"foo\bar" 16 | >>> print jsonical.dumps(u'\u1234') 17 | "\u1234" 18 | >>> print jsonical.dumps('\\') 19 | "\\" 20 | >>> print jsonical.dumps({"c": 0, "b": 0, "a": 0}) 21 | {"a":0,"b":0,"c":0} 22 | >>> from StringIO import StringIO 23 | >>> io = StringIO() 24 | >>> json.dump(['streaming API'], io) 25 | >>> io.getvalue() 26 | '["streaming API"]' 27 | 28 | Decoding JSON:: 29 | 30 | >>> import jsonical 31 | >>> jsonical.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') 32 | [u'foo', {u'bar': [u'baz', None, Decimal('1.0'), 2]}] 33 | >>> jsonical.loads('"\\"foo\\bar"') 34 | u'"foo\x08ar' 35 | >>> from StringIO import StringIO 36 | >>> io = StringIO('["streaming API"]') 37 | >>> jsonical.load(io) 38 | [u'streaming API'] 39 | 40 | Using jsonical from the shell to canonicalize: 41 | 42 | $ echo '{"json":"obj","bar":2.333333}' | python -mjsonical 43 | {"bar":2.333333,"json":"obj"} 44 | $ echo '{1.2:3.4}' | python -mjson.tool 45 | Expecting property name: line 1 column 2 (char 2) 46 | 47 | """ 48 | import datetime 49 | import decimal 50 | import sys 51 | import types 52 | import unicodedata 53 | 54 | try: 55 | import json 56 | except ImportError: 57 | import simplejson as json 58 | 59 | class Encoder(json.JSONEncoder): 60 | def __init__(self, *args, **kwargs): 61 | kwargs.pop("sort_keys", None) 62 | super(Encoder, self).__init__(sort_keys=True, *args, **kwargs) 63 | 64 | def default(self, obj): 65 | """This is slightly different than json.JSONEncoder.default(obj) 66 | in that it should returned the serialized representation of the 67 | passed object, not a serializable representation. 68 | """ 69 | if isinstance(obj, (datetime.date, datetime.time, datetime.datetime)): 70 | return '"%s"' % obj.isoformat() 71 | elif isinstance(obj, unicode): 72 | return '"%s"' % unicodedata.normalize('NFD', obj).encode('utf-8') 73 | elif isinstance(obj, decimal.Decimal): 74 | return str(obj) 75 | return super(Encoder, self).default(obj) 76 | 77 | def _iterencode_default(self, o, markers=None): 78 | yield self.default(o) 79 | 80 | def dump(obj, fp, indent=None): 81 | return json.dump(obj, fp, separators=(',', ':'), indent=indent, cls=Encoder) 82 | 83 | def dumps(obj, indent=None): 84 | return json.dumps(obj, separators=(',', ':'), indent=indent, cls=Encoder) 85 | 86 | class Decoder(json.JSONDecoder): 87 | def raw_decode(self, s, **kw): 88 | obj, end = super(Decoder, self).raw_decode(s, **kw) 89 | if isinstance(obj, types.StringTypes): 90 | obj = unicodedata.normalize('NFD', unicode(obj)) 91 | return obj, end 92 | 93 | def load(fp): 94 | return json.load(fp, cls=Decoder, parse_float=decimal.Decimal) 95 | 96 | def loads(s): 97 | return json.loads(s, cls=Decoder, parse_float=decimal.Decimal) 98 | 99 | def tool(): 100 | infile = sys.stdin 101 | outfile = sys.stdout 102 | if len(sys.argv) > 1: 103 | infile = open(sys.argv[1], 'rb') 104 | if len(sys.argv) > 2: 105 | outfile = open(sys.argv[2], 'wb') 106 | if len(sys.argv) > 3: 107 | raise SystemExit("{0} [infile [outfile]]".format(sys.argv[0])) 108 | try: 109 | obj = load(infile) 110 | except ValueError, e: 111 | raise SystemExit(e) 112 | dump(obj, outfile) 113 | outfile.write('\n') 114 | 115 | if __name__ == '__main__': 116 | tool() -------------------------------------------------------------------------------- /stratum/socksclient.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2012, Linus Nordberg 2 | # Taken from https://github.com/ln5/twisted-socks/ 3 | 4 | import socket 5 | import struct 6 | from zope.interface import implements 7 | from twisted.internet import defer 8 | from twisted.internet.interfaces import IStreamClientEndpoint 9 | from twisted.internet.protocol import Protocol, ClientFactory 10 | from twisted.internet.endpoints import _WrappingFactory 11 | 12 | class SOCKSError(Exception): 13 | def __init__(self, val): 14 | self.val = val 15 | def __str__(self): 16 | return repr(self.val) 17 | 18 | class SOCKSv4ClientProtocol(Protocol): 19 | buf = '' 20 | 21 | def SOCKSConnect(self, host, port): 22 | # only socksv4a for now 23 | ver = 4 24 | cmd = 1 # stream connection 25 | user = '\x00' 26 | dnsname = '' 27 | try: 28 | addr = socket.inet_aton(host) 29 | except socket.error: 30 | addr = '\x00\x00\x00\x01' 31 | dnsname = '%s\x00' % host 32 | msg = struct.pack('!BBH', ver, cmd, port) + addr + user + dnsname 33 | self.transport.write(msg) 34 | 35 | def verifySocksReply(self, data): 36 | """ 37 | Return True on success, False on need-more-data. 38 | Raise SOCKSError on request rejected or failed. 39 | """ 40 | if len(data) < 8: 41 | return False 42 | if ord(data[0]) != 0: 43 | self.transport.loseConnection() 44 | raise SOCKSError((1, "bad data")) 45 | status = ord(data[1]) 46 | if status != 0x5a: 47 | self.transport.loseConnection() 48 | raise SOCKSError((status, "request not granted: %d" % status)) 49 | return True 50 | 51 | def isSuccess(self, data): 52 | self.buf += data 53 | return self.verifySocksReply(self.buf) 54 | 55 | def connectionMade(self): 56 | self.SOCKSConnect(self.postHandshakeEndpoint._host, 57 | self.postHandshakeEndpoint._port) 58 | 59 | def dataReceived(self, data): 60 | if self.isSuccess(data): 61 | # Build protocol from provided factory and transfer control to it. 62 | self.transport.protocol = self.postHandshakeFactory.buildProtocol( 63 | self.transport.getHost()) 64 | self.transport.protocol.transport = self.transport 65 | self.transport.protocol.connected = 1 66 | self.transport.protocol.connectionMade() 67 | self.handshakeDone.callback(self.transport.getPeer()) 68 | 69 | class SOCKSv4ClientFactory(ClientFactory): 70 | protocol = SOCKSv4ClientProtocol 71 | 72 | def buildProtocol(self, addr): 73 | r=ClientFactory.buildProtocol(self, addr) 74 | r.postHandshakeEndpoint = self.postHandshakeEndpoint 75 | r.postHandshakeFactory = self.postHandshakeFactory 76 | r.handshakeDone = self.handshakeDone 77 | return r 78 | 79 | class SOCKSWrapper(object): 80 | implements(IStreamClientEndpoint) 81 | factory = SOCKSv4ClientFactory 82 | 83 | def __init__(self, reactor, host, port, endpoint): 84 | self._host = host 85 | self._port = port 86 | self._reactor = reactor 87 | self._endpoint = endpoint 88 | 89 | def connect(self, protocolFactory): 90 | """ 91 | Return a deferred firing when the SOCKS connection is established. 92 | """ 93 | 94 | try: 95 | # Connect with an intermediate SOCKS factory/protocol, 96 | # which then hands control to the provided protocolFactory 97 | # once a SOCKS connection has been established. 98 | f = self.factory() 99 | f.postHandshakeEndpoint = self._endpoint 100 | f.postHandshakeFactory = protocolFactory 101 | f.handshakeDone = defer.Deferred() 102 | wf = _WrappingFactory(f) 103 | self._reactor.connectTCP(self._host, self._port, wf) 104 | return f.handshakeDone 105 | except: 106 | return defer.fail() -------------------------------------------------------------------------------- /mining_libs/midstate.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011 by jedi95 and 2 | # CFSworks 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | import struct 23 | 24 | # Some SHA-256 constants... 25 | K = [ 26 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 27 | 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 28 | 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 29 | 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 30 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 31 | 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 32 | 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 33 | 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 34 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 35 | 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 36 | 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, 37 | ] 38 | 39 | A0 = 0x6a09e667 40 | B0 = 0xbb67ae85 41 | C0 = 0x3c6ef372 42 | D0 = 0xa54ff53a 43 | E0 = 0x510e527f 44 | F0 = 0x9b05688c 45 | G0 = 0x1f83d9ab 46 | H0 = 0x5be0cd19 47 | 48 | def rotateright(i,p): 49 | """i>>>p""" 50 | p &= 0x1F # p mod 32 51 | return i>>p | ((i<<(32-p)) & 0xFFFFFFFF) 52 | 53 | def addu32(*i): 54 | return sum(list(i))&0xFFFFFFFF 55 | 56 | def calculateMidstate(data, state=None, rounds=None): 57 | """Given a 512-bit (64-byte) block of (little-endian byteswapped) data, 58 | calculate a Bitcoin-style midstate. (That is, if SHA-256 were little-endian 59 | and only hashed the first block of input.) 60 | """ 61 | if len(data) != 64: 62 | raise ValueError('data must be 64 bytes long') 63 | 64 | w = list(struct.unpack('> 3) 94 | s1 = rotateright(w[14],17) ^ rotateright(w[14],19) ^ (w[14] >> 10) 95 | w.append(addu32(w[0], s0, w[9], s1)) 96 | w.pop(0) 97 | 98 | if rounds is None: 99 | a = addu32(a, A0) 100 | b = addu32(b, B0) 101 | c = addu32(c, C0) 102 | d = addu32(d, D0) 103 | e = addu32(e, E0) 104 | f = addu32(f, F0) 105 | g = addu32(g, G0) 106 | h = addu32(h, H0) 107 | 108 | return struct.pack(' 13 | 14 | # Do NOT "set -e" 15 | 16 | # PATH should only include /usr/* if it runs after the mountnfs.sh script 17 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 18 | DESC="Stratum mining proxy" 19 | NAME=stratum-mining-proxy 20 | DAEMON=/usr/sbin/$NAME 21 | PIDFILE=/var/run/$NAME.pid 22 | DAEMON_ARGS="--log-file=/var/log/$NAME.log --pid-file=$PIDFILE" 23 | # did have --quiet 24 | SCRIPTNAME=/etc/init.d/$NAME 25 | 26 | # Exit if the package is not installed 27 | [ -x "$DAEMON" ] || exit 0 28 | 29 | # Read configuration variable file if it is present 30 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 31 | 32 | # Add settings from default file to $DAEMON_ARGS 33 | [ -n "$POOL_HOST" ] && DAEMON_ARGS="$DAEMON_ARGS -o $POOL_HOST" 34 | [ -n "$POOL_PORT" ] && DAEMON_ARGS="$DAEMON_ARGS -p $POOL_PORT" 35 | [ -n "$SCRYPT_TARGET" ] && DAEMON_ARGS="$DAEMON_ARGS -st" 36 | [ -n "$CUSTOM_USER" ] && DAEMON_ARGS="$DAEMON_ARGS -cu $CUSTOM_USER" 37 | 38 | # Load the VERBOSE setting and other rcS variables 39 | . /lib/init/vars.sh 40 | 41 | # Define LSB log_* functions. 42 | # Depend on lsb-base (>= 3.2-14) to ensure that this file is present 43 | # and status_of_proc is working. 44 | . /lib/lsb/init-functions 45 | 46 | # 47 | # Function that starts the daemon/service 48 | # 49 | do_start() 50 | { 51 | # Return 52 | # 0 if daemon has been started 53 | # 1 if daemon was already running 54 | # 2 if daemon could not be started 55 | start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ 56 | || return 1 57 | start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --background -- \ 58 | $DAEMON_ARGS \ 59 | || return 2 60 | # Add code here, if necessary, that waits for the process to be ready 61 | # to handle requests from services started subsequently which depend 62 | # on this one. As a last resort, sleep for some time. 63 | } 64 | 65 | # 66 | # Function that stops the daemon/service 67 | # 68 | do_stop() 69 | { 70 | # Return 71 | # 0 if daemon has been stopped 72 | # 1 if daemon was already stopped 73 | # 2 if daemon could not be stopped 74 | # other if a failure occurred 75 | start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE 76 | RETVAL="$?" 77 | [ "$RETVAL" = 2 ] && return 2 78 | # Wait for children to finish too if this is a daemon that forks 79 | # and if the daemon is only ever run from this initscript. 80 | # If the above conditions are not satisfied then add some other code 81 | # that waits for the process to drop all resources that could be 82 | # needed by services started subsequently. A last resort is to 83 | # sleep for some time. 84 | start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON 85 | [ "$?" = 2 ] && return 2 86 | # Many daemons don't delete their pidfiles when they exit. 87 | rm -f $PIDFILE 88 | return "$RETVAL" 89 | } 90 | 91 | # 92 | # Function that sends a SIGHUP to the daemon/service 93 | # 94 | do_reload() { 95 | # 96 | # If the daemon can reload its configuration without 97 | # restarting (for example, when it is sent a SIGHUP), 98 | # then implement that here. 99 | # 100 | start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME 101 | return 0 102 | } 103 | 104 | case "$1" in 105 | start) 106 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" 107 | do_start 108 | case "$?" in 109 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 110 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 111 | esac 112 | ;; 113 | stop) 114 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" 115 | do_stop 116 | case "$?" in 117 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 118 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 119 | esac 120 | ;; 121 | status) 122 | status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? 123 | ;; 124 | #reload|force-reload) 125 | # 126 | # If do_reload() is not implemented then leave this commented out 127 | # and leave 'force-reload' as an alias for 'restart'. 128 | # 129 | #log_daemon_msg "Reloading $DESC" "$NAME" 130 | #do_reload 131 | #log_end_msg $? 132 | #;; 133 | restart|force-reload) 134 | # 135 | # If the "reload" option is implemented then remove the 136 | # 'force-reload' alias 137 | # 138 | log_daemon_msg "Restarting $DESC" "$NAME" 139 | do_stop 140 | case "$?" in 141 | 0|1) 142 | do_start 143 | case "$?" in 144 | 0) log_end_msg 0 ;; 145 | 1) log_end_msg 1 ;; # Old process is still running 146 | *) log_end_msg 1 ;; # Failed to start 147 | esac 148 | ;; 149 | *) 150 | # Failed to stop 151 | log_end_msg 1 152 | ;; 153 | esac 154 | ;; 155 | *) 156 | #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 157 | echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 158 | exit 3 159 | ;; 160 | esac 161 | 162 | : 163 | -------------------------------------------------------------------------------- /stratum/signature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | try: 3 | import ecdsa 4 | from ecdsa import curves 5 | except ImportError: 6 | print "ecdsa package not installed. Signing of messages not available." 7 | ecdsa = None 8 | 9 | import base64 10 | import hashlib 11 | import time 12 | 13 | import jsonical 14 | import json 15 | import custom_exceptions 16 | 17 | if ecdsa: 18 | # secp256k1, http://www.oid-info.com/get/1.3.132.0.10 19 | _p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2FL 20 | _r = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141L 21 | _b = 0x0000000000000000000000000000000000000000000000000000000000000007L 22 | _a = 0x0000000000000000000000000000000000000000000000000000000000000000L 23 | _Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798L 24 | _Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8L 25 | curve_secp256k1 = ecdsa.ellipticcurve.CurveFp(_p, _a, _b) 26 | generator_secp256k1 = ecdsa.ellipticcurve.Point(curve_secp256k1, _Gx, _Gy, _r) 27 | oid_secp256k1 = (1,3,132,0,10) 28 | SECP256k1 = ecdsa.curves.Curve("SECP256k1", curve_secp256k1, generator_secp256k1, oid_secp256k1 ) 29 | 30 | # Register SECP256k1 to ecdsa library 31 | curves.curves.append(SECP256k1) 32 | 33 | def generate_keypair(): 34 | if not ecdsa: 35 | raise custom_exceptions.SigningNotAvailableException("ecdsa not installed") 36 | 37 | private_key = ecdsa.SigningKey.generate(curve=SECP256k1) 38 | public_key = private_key.get_verifying_key() 39 | return (private_key, public_key) 40 | 41 | def load_privkey_pem(filename): 42 | return ecdsa.SigningKey.from_pem(open(filename, 'r').read().strip()) 43 | 44 | def sign(privkey, data): 45 | if not ecdsa: 46 | raise custom_exceptions.SigningNotAvailableException("ecdsa not installed") 47 | 48 | hash = hashlib.sha256(data).digest() 49 | signature = privkey.sign_digest(hash, sigencode=ecdsa.util.sigencode_der) 50 | return base64.b64encode(signature) 51 | 52 | def verify(pubkey, signature, data): 53 | if not ecdsa: 54 | raise custom_exceptions.SigningNotAvailableException("ecdsa not installed") 55 | 56 | hash = hashlib.sha256(data).digest() 57 | sign = base64.b64decode(signature) 58 | try: 59 | return pubkey.verify_digest(sign, hash, sigdecode=ecdsa.util.sigdecode_der) 60 | except: 61 | return False 62 | 63 | def jsonrpc_dumps_sign(privkey, privkey_id, is_request, message_id, method='', params=[], result=None, error=None): 64 | '''Create the signature for given json-rpc data and returns signed json-rpc text stream''' 65 | 66 | # Build data object to sign 67 | sign_time = int(time.time()) 68 | data = {'method': method, 'params': params, 'result': result, 'error': error, 'sign_time': sign_time} 69 | 70 | # Serialize data to sign and perform signing 71 | txt = jsonical.dumps(data) 72 | signature = sign(privkey, txt) 73 | 74 | # Reconstruct final data object and put signature 75 | if is_request: 76 | data = {'id': message_id, 'method': method, 'params': params, 77 | 'sign': signature, 'sign_algo': 'ecdsa;SECP256k1', 'sign_id': privkey_id, 'sign_time': sign_time} 78 | else: 79 | data = {'id': message_id, 'result': result, 'error': error, 80 | 'sign': signature, 'sign_algo': 'ecdsa;SECP256k1', 'sign_id': privkey_id, 'sign_time': sign_time} 81 | 82 | # Return original data extended with signature 83 | return jsonical.dumps(data) 84 | 85 | def jsonrpc_loads_verify(pubkeys, txt): 86 | ''' 87 | Pubkeys is mapping (dict) of sign_id -> ecdsa public key. 88 | This method deserialize provided json-encoded data, load signature ID, perform the lookup for public key 89 | and check stored signature of the message. If signature is OK, returns message data. 90 | ''' 91 | data = json.loads(txt) 92 | signature_algo = data['sign_algo'] 93 | signature_id = data['sign_id'] 94 | signature_time = data['sign_time'] 95 | 96 | if signature_algo != 'ecdsa;SECP256k1': 97 | raise custom_exceptions.UnknownSignatureAlgorithmException("%s is not supported" % signature_algo) 98 | 99 | try: 100 | pubkey = pubkeys[signature_id] 101 | except KeyError: 102 | raise custom_exceptions.UnknownSignatureIdException("Public key for '%s' not found" % signature_id) 103 | 104 | signature = data['sign'] 105 | message_id = data['id'] 106 | method = data.get('method', '') 107 | params = data.get('params', []) 108 | result = data.get('result', None) 109 | error = data.get('error', None) 110 | 111 | # Build data object to verify 112 | data = {'method': method, 'params': params, 'result': result, 'error': error, 'sign_time': signature_time} 113 | txt = jsonical.dumps(data) 114 | 115 | if not verify(pubkey, signature, txt): 116 | raise custom_exceptions.SignatureVerificationFailedException("Signature doesn't match to given data") 117 | 118 | if method: 119 | # It's a request 120 | return {'id': message_id, 'method': method, 'params': params} 121 | 122 | else: 123 | # It's aresponse 124 | return {'id': message_id, 'result': result, 'error': error} 125 | 126 | if __name__ == '__main__': 127 | (private, public) = generate_keypair() 128 | print private.to_pem() 129 | -------------------------------------------------------------------------------- /midstatec/midstatemodule.c: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Johannes Kimmel 2 | // Distributed under the MIT/X11 software license, see 3 | // http://www.opensource.org/licenses/mit-license.php 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | typedef union sha256_state_t sha256_state_t; 13 | union sha256_state_t { 14 | uint32_t h[8]; 15 | unsigned char byte[32]; 16 | }; 17 | 18 | static uint32_t h[] = { 19 | 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, 20 | }; 21 | 22 | static uint32_t k[] = { 23 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 24 | 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 25 | 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 26 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 27 | 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 28 | 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 29 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 30 | 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, 31 | }; 32 | 33 | static inline uint32_t ror32(const uint32_t v, const uint32_t n) { 34 | return (v >> n) | (v << (32 - n)); 35 | }; 36 | 37 | static inline void update_state(sha256_state_t *state, const uint32_t data[16]) { 38 | uint32_t w[64]; 39 | sha256_state_t t = *state; 40 | 41 | for (size_t i = 0 ; i < 16; i++) { 42 | w[i] = htonl(data[i]); 43 | } 44 | 45 | for (size_t i = 16; i < 64; i++) { 46 | uint32_t s0 = ror32(w[i - 15], 7) ^ ror32(w[i - 15], 18) ^ (w[i - 15] >> 3); 47 | uint32_t s1 = ror32(w[i - 2], 17) ^ ror32(w[i - 2], 19) ^ (w[i - 2] >> 10); 48 | w[i] = w[i - 16] + s0 + w[i - 7] + s1; 49 | } 50 | 51 | for (size_t i = 0; i < 64; i++) { 52 | uint32_t s0 = ror32(t.h[0], 2) ^ ror32(t.h[0], 13) ^ ror32(t.h[0], 22); 53 | uint32_t maj = (t.h[0] & t.h[1]) ^ (t.h[0] & t.h[2]) ^ (t.h[1] & t.h[2]); 54 | uint32_t t2 = s0 + maj; 55 | uint32_t s1 = ror32(t.h[4], 6) ^ ror32(t.h[4], 11) ^ ror32(t.h[4], 25); 56 | uint32_t ch = (t.h[4] & t.h[5]) ^ (~t.h[4] & t.h[6]); 57 | uint32_t t1 = t.h[7] + s1 + ch + k[i] + w[i]; 58 | 59 | t.h[7] = t.h[6]; 60 | t.h[6] = t.h[5]; 61 | t.h[5] = t.h[4]; 62 | t.h[4] = t.h[3] + t1; 63 | t.h[3] = t.h[2]; 64 | t.h[2] = t.h[1]; 65 | t.h[1] = t.h[0]; 66 | t.h[0] = t1 + t2; 67 | } 68 | 69 | for (size_t i = 0; i < 8; i++) { 70 | state->h[i] += t.h[i]; 71 | } 72 | } 73 | 74 | static inline void init_state(sha256_state_t *state) { 75 | for (size_t i = 0; i < 8; i++) { 76 | state->h[i] = h[i]; 77 | } 78 | } 79 | 80 | static sha256_state_t midstate(const unsigned char data[64]) { 81 | sha256_state_t state; 82 | 83 | init_state(&state); 84 | update_state(&state, (const uint32_t const *) data); 85 | 86 | return state; 87 | } 88 | 89 | void print_hex(char unsigned *data, size_t s) { 90 | for (size_t i = 0; i < s; i++) { 91 | printf("%02hhx", data[i]); 92 | } 93 | printf("\n"); 94 | } 95 | 96 | PyObject *midstate_helper(PyObject *self, PyObject *arg) { 97 | Py_ssize_t s; 98 | PyObject *ret = NULL; 99 | PyObject *t_int = NULL; 100 | char *t; 101 | unsigned char data[64]; 102 | sha256_state_t mstate; 103 | 104 | if (PyBytes_Check(arg) != true) { 105 | PyErr_SetString(PyExc_ValueError, "Need bytes object as argument."); 106 | goto error; 107 | } 108 | if (PyBytes_AsStringAndSize(arg, &t, &s) == -1) { 109 | // Got exception 110 | goto error; 111 | } 112 | if (s < 64) { 113 | PyErr_SetString(PyExc_ValueError, "Argument length must be at least 64 bytes."); 114 | goto error; 115 | } 116 | 117 | memcpy(data, t, 64); 118 | mstate = midstate(data); 119 | 120 | ret = PyTuple_New(8); 121 | for (size_t i = 0; i < 8; i++) { 122 | t_int = PyLong_FromUnsignedLong(mstate.h[i]); 123 | if (PyTuple_SetItem(ret, i, t_int) != 0) { 124 | t_int = NULL; // ret is owner of the int now 125 | goto error; 126 | } 127 | } 128 | 129 | return ret; 130 | 131 | error: 132 | Py_XDECREF(t_int); 133 | Py_XDECREF(ret); 134 | 135 | return NULL; 136 | } 137 | 138 | static struct PyMethodDef midstate_functions[] = { 139 | {"SHA256", midstate_helper, METH_O, NULL}, 140 | {NULL, NULL, 0, NULL}, 141 | }; 142 | 143 | #if PY_MAJOR_VERSION >= 3 144 | static struct PyModuleDef moduledef = { 145 | PyModuleDef_HEAD_INIT, 146 | "midstate", 147 | NULL, 148 | -1, 149 | midstate_functions, 150 | NULL, 151 | NULL, 152 | NULL, 153 | NULL, 154 | }; 155 | #endif 156 | 157 | PyMODINIT_FUNC 158 | #if PY_MAJOR_VERSION >= 3 159 | PyInit_midstate(void) 160 | { 161 | return PyModule_Create(&midstatemodule); 162 | } 163 | #else 164 | initmidstate(void) { 165 | Py_InitModule3("midstate", midstate_functions, NULL); 166 | } 167 | #endif 168 | 169 | int main(int argc, char *argv[]) { 170 | const unsigned char data[] = "\1\0\0\0\xe4\xe8\x9d\xf8H\x1b\xc5v\xb9\x9f" "fWb\xcb\x82" "f\xf8U\xc6h" "@\x16\xb8\xb4\xd1iv\xf2\0\0\0\0\xe1\xd1O\x08\x98\xe6\x1d\x02O\x0e\1r\xfc" "cFi\xf5\xfc\xd5mN\1\xca\x10\xe9" "7{\x05hc\xd1U\xc8" "f O\xf8\xff\x07\x1d\0\0\0"; 171 | 172 | sha256_state_t state; 173 | 174 | //for (size_t i = 0; i < 1000000; i++) 175 | state = midstate(data); 176 | 177 | printf("b8101f7c4a8e294ecbccb941dde17fd461dc39ff102bc37bb7ac7d5b95290166 <-- want\n"); 178 | print_hex(state.byte, 32); 179 | return 0; 180 | } 181 | -------------------------------------------------------------------------------- /stratum/server.py: -------------------------------------------------------------------------------- 1 | def setup(setup_event=None): 2 | try: 3 | from twisted.internet import epollreactor 4 | epollreactor.install() 5 | except ImportError: 6 | print "Failed to install epoll reactor, default reactor will be used instead." 7 | 8 | try: 9 | import settings 10 | except ImportError: 11 | print "***** Is configs.py missing? Maybe you want to copy and customize config_default.py?" 12 | 13 | from twisted.application import service 14 | application = service.Application("stratum-server") 15 | 16 | # Setting up logging 17 | from twisted.python.log import ILogObserver, FileLogObserver 18 | from twisted.python.logfile import DailyLogFile 19 | 20 | #logfile = DailyLogFile(settings.LOGFILE, settings.LOGDIR) 21 | #application.setComponent(ILogObserver, FileLogObserver(logfile).emit) 22 | 23 | if settings.ENABLE_EXAMPLE_SERVICE: 24 | import stratum.example_service 25 | 26 | if setup_event == None: 27 | setup_finalize(None, application) 28 | else: 29 | setup_event.addCallback(setup_finalize, application) 30 | 31 | return application 32 | 33 | def setup_finalize(event, application): 34 | 35 | from twisted.application import service, internet 36 | from twisted.internet import reactor, ssl 37 | from twisted.web.server import Site 38 | from twisted.python import log 39 | #from twisted.enterprise import adbapi 40 | import OpenSSL.SSL 41 | 42 | from services import ServiceEventHandler 43 | 44 | import socket_transport 45 | import http_transport 46 | import websocket_transport 47 | import irc 48 | 49 | from stratum import settings 50 | 51 | try: 52 | import signature 53 | signing_key = signature.load_privkey_pem(settings.SIGNING_KEY) 54 | except: 55 | print "Loading of signing key '%s' failed, protocol messages cannot be signed." % settings.SIGNING_KEY 56 | signing_key = None 57 | 58 | # Attach HTTPS Poll Transport service to application 59 | try: 60 | sslContext = ssl.DefaultOpenSSLContextFactory(settings.SSL_PRIVKEY, settings.SSL_CACERT) 61 | except OpenSSL.SSL.Error: 62 | sslContext = None 63 | print "Cannot initiate SSL context, are SSL_PRIVKEY or SSL_CACERT missing?" 64 | print "This will skip all SSL-based transports." 65 | 66 | # Set up thread pool size for service threads 67 | reactor.suggestThreadPoolSize(settings.THREAD_POOL_SIZE) 68 | 69 | if settings.LISTEN_SOCKET_TRANSPORT: 70 | # Attach Socket Transport service to application 71 | socket = internet.TCPServer(settings.LISTEN_SOCKET_TRANSPORT, 72 | socket_transport.SocketTransportFactory(debug=settings.DEBUG, 73 | signing_key=signing_key, 74 | signing_id=settings.SIGNING_ID, 75 | event_handler=ServiceEventHandler, 76 | tcp_proxy_protocol_enable=settings.TCP_PROXY_PROTOCOL)) 77 | socket.setServiceParent(application) 78 | 79 | # Build the HTTP interface 80 | httpsite = Site(http_transport.Root(debug=settings.DEBUG, signing_key=signing_key, signing_id=settings.SIGNING_ID, 81 | event_handler=ServiceEventHandler)) 82 | httpsite.sessionFactory = http_transport.HttpSession 83 | 84 | if settings.LISTEN_HTTP_TRANSPORT: 85 | # Attach HTTP Poll Transport service to application 86 | http = internet.TCPServer(settings.LISTEN_HTTP_TRANSPORT, httpsite) 87 | http.setServiceParent(application) 88 | 89 | if settings.LISTEN_HTTPS_TRANSPORT and sslContext: 90 | https = internet.SSLServer(settings.LISTEN_HTTPS_TRANSPORT, httpsite, contextFactory = sslContext) 91 | https.setServiceParent(application) 92 | 93 | if settings.LISTEN_WS_TRANSPORT: 94 | from autobahn.websocket import listenWS 95 | log.msg("Starting WS transport on %d" % settings.LISTEN_WS_TRANSPORT) 96 | ws = websocket_transport.WebsocketTransportFactory(settings.LISTEN_WS_TRANSPORT, 97 | debug=settings.DEBUG, 98 | signing_key=signing_key, 99 | signing_id=settings.SIGNING_ID, 100 | event_handler=ServiceEventHandler) 101 | listenWS(ws) 102 | 103 | if settings.LISTEN_WSS_TRANSPORT and sslContext: 104 | from autobahn.websocket import listenWS 105 | log.msg("Starting WSS transport on %d" % settings.LISTEN_WSS_TRANSPORT) 106 | wss = websocket_transport.WebsocketTransportFactory(settings.LISTEN_WSS_TRANSPORT, is_secure=True, 107 | debug=settings.DEBUG, 108 | signing_key=signing_key, 109 | signing_id=settings.SIGNING_ID, 110 | event_handler=ServiceEventHandler) 111 | listenWS(wss, contextFactory=sslContext) 112 | 113 | if settings.IRC_NICK: 114 | reactor.connectTCP(settings.IRC_SERVER, settings.IRC_PORT, irc.IrcLurkerFactory(settings.IRC_ROOM, settings.IRC_NICK, settings.IRC_HOSTNAME)) 115 | 116 | return event 117 | 118 | if __name__ == '__main__': 119 | print "This is not executable script. Try 'twistd -ny launcher.tac instead!" 120 | -------------------------------------------------------------------------------- /stratum/socket_transport.py: -------------------------------------------------------------------------------- 1 | from twisted.internet.protocol import ServerFactory 2 | from twisted.internet.protocol import ReconnectingClientFactory 3 | from twisted.internet import reactor, defer, endpoints 4 | 5 | import socksclient 6 | import custom_exceptions 7 | from protocol import Protocol, ClientProtocol 8 | from event_handler import GenericEventHandler 9 | 10 | import logger 11 | log = logger.get_logger('socket_transport') 12 | 13 | def sockswrapper(proxy, dest): 14 | endpoint = endpoints.TCP4ClientEndpoint(reactor, dest[0], dest[1]) 15 | return socksclient.SOCKSWrapper(reactor, proxy[0], proxy[1], endpoint) 16 | 17 | class SocketTransportFactory(ServerFactory): 18 | def __init__(self, debug=False, signing_key=None, signing_id=None, event_handler=GenericEventHandler, 19 | tcp_proxy_protocol_enable=False): 20 | self.debug = debug 21 | self.signing_key = signing_key 22 | self.signing_id = signing_id 23 | self.event_handler = event_handler 24 | self.protocol = Protocol 25 | 26 | # Read settings.TCP_PROXY_PROTOCOL documentation 27 | self.tcp_proxy_protocol_enable = tcp_proxy_protocol_enable 28 | 29 | class SocketTransportClientFactory(ReconnectingClientFactory): 30 | def __init__(self, host, port, allow_trusted=True, allow_untrusted=False, 31 | debug=False, signing_key=None, signing_id=None, 32 | is_reconnecting=True, proxy=None, 33 | event_handler=GenericEventHandler): 34 | self.debug = debug 35 | self.is_reconnecting = is_reconnecting 36 | self.signing_key = signing_key 37 | self.signing_id = signing_id 38 | self.client = None # Reference to open connection 39 | self.on_disconnect = defer.Deferred() 40 | self.on_connect = defer.Deferred() 41 | self.peers_trusted = {} 42 | self.peers_untrusted = {} 43 | self.main_host = (host, port) 44 | self.new_host = None 45 | self.proxy = proxy 46 | 47 | self.event_handler = event_handler 48 | self.protocol = ClientProtocol 49 | self.after_connect = [] 50 | 51 | self.connect() 52 | 53 | def connect(self): 54 | if self.proxy: 55 | self.timeout_handler = reactor.callLater(60, self.connection_timeout) 56 | sw = sockswrapper(self.proxy, self.main_host) 57 | sw.connect(self) 58 | else: 59 | self.timeout_handler = reactor.callLater(30, self.connection_timeout) 60 | reactor.connectTCP(self.main_host[0], self.main_host[1], self) 61 | 62 | ''' 63 | This shouldn't be a part of transport layer 64 | def add_peers(self, peers): 65 | # FIXME: Use this list when current connection fails 66 | for peer in peers: 67 | hash = "%s%s%s" % (peer['hostname'], peer['ipv4'], peer['ipv6']) 68 | 69 | which = self.peers_trusted if peer['trusted'] else self.peers_untrusted 70 | which[hash] = peer 71 | 72 | #print self.peers_trusted 73 | #print self.peers_untrusted 74 | ''' 75 | 76 | def connection_timeout(self): 77 | self.timeout_handler = None 78 | 79 | if self.client: 80 | return 81 | 82 | e = custom_exceptions.TransportException("SocketTransportClientFactory connection timed out") 83 | if not self.on_connect.called: 84 | d = self.on_connect 85 | self.on_connect = defer.Deferred() 86 | d.errback(e) 87 | 88 | else: 89 | raise e 90 | 91 | def rpc(self, method, params, *args, **kwargs): 92 | if not self.client: 93 | raise custom_exceptions.TransportException("Not connected") 94 | 95 | return self.client.rpc(method, params, *args, **kwargs) 96 | 97 | def subscribe(self, method, params, *args, **kwargs): 98 | ''' 99 | This is like standard RPC call, except that parameters are stored 100 | into after_connect list, so the same command will perform again 101 | on restored connection. 102 | ''' 103 | if not self.client: 104 | raise custom_exceptions.TransportException("Not connected") 105 | 106 | self.after_connect.append((method, params)) 107 | return self.client.rpc(method, params, *args, **kwargs) 108 | 109 | def reconnect(self, host=None, port=None, wait=None): 110 | '''Close current connection and start new one. 111 | If host or port specified, it will be used for new connection.''' 112 | 113 | new = list(self.main_host) 114 | if host: 115 | new[0] = host 116 | if port: 117 | new[1] = port 118 | self.new_host = tuple(new) 119 | 120 | if self.client and self.client.connected: 121 | if wait != None: 122 | self.delay = wait 123 | self.client.transport.connector.disconnect() 124 | 125 | def retry(self, connector=None): 126 | if not self.is_reconnecting: 127 | return 128 | 129 | if connector is None: 130 | if self.connector is None: 131 | raise ValueError("no connector to retry") 132 | else: 133 | connector = self.connector 134 | 135 | if self.new_host: 136 | # Switch to new host if any 137 | connector.host = self.new_host[0] 138 | connector.port = self.new_host[1] 139 | self.main_host = self.new_host 140 | self.new_host = None 141 | 142 | return ReconnectingClientFactory.retry(self, connector) 143 | 144 | def buildProtocol(self, addr): 145 | self.resetDelay() 146 | #if not self.is_reconnecting: raise 147 | return ReconnectingClientFactory.buildProtocol(self, addr) 148 | 149 | def clientConnectionLost(self, connector, reason): 150 | if self.is_reconnecting: 151 | log.debug(reason) 152 | ReconnectingClientFactory.clientConnectionLost(self, connector, reason) 153 | 154 | def clientConnectionFailed(self, connector, reason): 155 | if self.is_reconnecting: 156 | log.debug(reason) 157 | ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) 158 | 159 | -------------------------------------------------------------------------------- /stratum/config_default.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This is example configuration for Stratum server. 3 | Please rename it to config.py and fill correct values. 4 | ''' 5 | 6 | # ******************** GENERAL SETTINGS *************** 7 | 8 | # Enable some verbose debug (logging requests and responses). 9 | DEBUG = True 10 | 11 | # Destination for application logs, files rotated once per day. 12 | LOGDIR = 'log/' 13 | 14 | # Main application log file. 15 | LOGFILE = None #'stratum.log' 16 | 17 | # Possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL 18 | LOGLEVEL = 'DEBUG' 19 | 20 | # How many threads use for synchronous methods (services). 21 | # 30 is enough for small installation, for real usage 22 | # it should be slightly more, say 100-300. 23 | THREAD_POOL_SIZE = 30 24 | 25 | # RPC call throws TimeoutServiceException once total time since request has been 26 | # placed (time to delivery to client + time for processing on the client) 27 | # crosses _TOTAL (in second). 28 | # _TOTAL reflects the fact that not all transports deliver RPC requests to the clients 29 | # instantly, so request can wait some time in the buffer on server side. 30 | # NOT IMPLEMENTED YET 31 | #RPC_TIMEOUT_TOTAL = 600 32 | 33 | # RPC call throws TimeoutServiceException once client is processing request longer 34 | # than _PROCESS (in second) 35 | # NOT IMPLEMENTED YET 36 | #RPC_TIMEOUT_PROCESS = 30 37 | 38 | # Do you want to expose "example" service in server? 39 | # Useful for learning the server,you probably want to disable 40 | # this on production 41 | ENABLE_EXAMPLE_SERVICE = True 42 | 43 | # ******************** TRANSPORTS ********************* 44 | 45 | # Hostname or external IP to expose 46 | HOSTNAME = 'stratum.example.com' 47 | 48 | # Port used for Socket transport. Use 'None' for disabling the transport. 49 | LISTEN_SOCKET_TRANSPORT = 3333 50 | 51 | # Port used for HTTP Poll transport. Use 'None' for disabling the transport 52 | LISTEN_HTTP_TRANSPORT = 8000 53 | 54 | # Port used for HTTPS Poll transport 55 | LISTEN_HTTPS_TRANSPORT = 8001 56 | 57 | # Port used for WebSocket transport, 'None' for disabling WS 58 | LISTEN_WS_TRANSPORT = 8002 59 | 60 | # Port used for secure WebSocket, 'None' for disabling WSS 61 | LISTEN_WSS_TRANSPORT = 8003 62 | 63 | # ******************** SSL SETTINGS ****************** 64 | 65 | # Private key and certification file for SSL protected transports 66 | # You can find howto for generating self-signed certificate in README file 67 | SSL_PRIVKEY = 'server.key' 68 | SSL_CACERT = 'server.crt' 69 | 70 | # ******************** TCP SETTINGS ****************** 71 | 72 | # Enables support for socket encapsulation, which is compatible 73 | # with haproxy 1.5+. By enabling this, first line of received 74 | # data will represent some metadata about proxied stream: 75 | # PROXY \n 76 | # 77 | # Full specification: http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt 78 | TCP_PROXY_PROTOCOL = False 79 | 80 | # ******************** HTTP SETTINGS ***************** 81 | 82 | # Keepalive for HTTP transport sessions (at this time for both poll and push) 83 | # High value leads to higher memory usage (all sessions are stored in memory ATM). 84 | # Low value leads to more frequent session reinitializing (like downloading address history). 85 | HTTP_SESSION_TIMEOUT = 3600 # in seconds 86 | 87 | # Maximum number of messages (notifications, responses) waiting to delivery to HTTP Poll clients. 88 | # Buffer length is PER CONNECTION. High value will consume a lot of RAM, 89 | # short history will cause that in some edge cases clients won't receive older events. 90 | HTTP_BUFFER_LIMIT = 10000 91 | 92 | # User agent used in HTTP requests (for both HTTP transports and for proxy calls from services) 93 | USER_AGENT = 'Stratum/0.1' 94 | 95 | # Provide human-friendly user interface on HTTP transports for browsing exposed services. 96 | BROWSER_ENABLE = True 97 | 98 | # ******************** BITCOIND SETTINGS ************ 99 | 100 | # Hostname and credentials for one trusted Bitcoin node ("Satoshi's client"). 101 | # Stratum uses both P2P port (which is 8333 everytime) and RPC port 102 | BITCOIN_TRUSTED_HOST = '127.0.0.1' 103 | BITCOIN_TRUSTED_PORT = 8332 # RPC port 104 | BITCOIN_TRUSTED_USER = 'stratum' 105 | BITCOIN_TRUSTED_PASSWORD = '***somepassword***' 106 | 107 | # ******************** OTHER CORE SETTINGS ********************* 108 | # Use "echo -n '' | sha256sum | cut -f1 -d' ' " 109 | # for calculating SHA256 of your preferred password 110 | ADMIN_PASSWORD_SHA256 = None # Admin functionality is disabled 111 | #ADMIN_PASSWORD_SHA256 = '9e6c0c1db1e0dfb3fa5159deb4ecd9715b3c8cd6b06bd4a3ad77e9a8c5694219' # SHA256 of the password 112 | 113 | # IP from which admin calls are allowed. 114 | # Set None to allow admin calls from all IPs 115 | ADMIN_RESTRICT_INTERFACE = '127.0.0.1' 116 | 117 | # Use "./signature.py > signing_key.pem" to generate unique signing key for your server 118 | SIGNING_KEY = None # Message signing is disabled 119 | #SIGNING_KEY = 'signing_key.pem' 120 | 121 | # Origin of signed messages. Provide some unique string, 122 | # ideally URL where users can find some information about your identity 123 | SIGNING_ID = None 124 | #SIGNING_ID = 'stratum.somedomain.com' # Use custom string 125 | #SIGNING_ID = HOSTNAME # Use hostname as the signing ID 126 | 127 | # *********************** IRC / PEER CONFIGURATION ************* 128 | 129 | IRC_NICK = None # Skip IRC registration 130 | #IRC_NICK = "stratum" # Use nickname of your choice 131 | 132 | # Which hostname / external IP expose in IRC room 133 | # This should be official HOSTNAME for normal operation. 134 | IRC_HOSTNAME = HOSTNAME 135 | 136 | # Don't change this unless you're creating private Stratum cloud. 137 | IRC_SERVER = 'irc.freenode.net' 138 | IRC_ROOM = '#stratum-nodes' 139 | IRC_PORT = 6667 140 | 141 | # Hardcoded list of Stratum nodes for clients to switch when this node is not available. 142 | PEERS = [ 143 | { 144 | 'hostname': 'stratum.bitcoin.cz', 145 | 'trusted': True, # This node is trustworthy 146 | 'weight': -1, # Higher number means higher priority for selection. 147 | # -1 will work mostly as a backup when other servers won't work. 148 | # (IRC peers have weight=0 automatically). 149 | }, 150 | ] 151 | 152 | 153 | ''' 154 | DATABASE_DRIVER = 'MySQLdb' 155 | DATABASE_HOST = 'palatinus.cz' 156 | DATABASE_DBNAME = 'marekp_bitcointe' 157 | DATABASE_USER = 'marekp_bitcointe' 158 | DATABASE_PASSWORD = '**empty**' 159 | ''' 160 | -------------------------------------------------------------------------------- /stratum/pubsub.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | from connection_registry import ConnectionRegistry 3 | import custom_exceptions 4 | import hashlib 5 | 6 | def subscribe(func): 7 | '''Decorator detect Subscription object in result and subscribe connection''' 8 | def inner(self, *args, **kwargs): 9 | subs = func(self, *args, **kwargs) 10 | return Pubsub.subscribe(self.connection_ref(), subs) 11 | return inner 12 | 13 | def unsubscribe(func): 14 | '''Decorator detect Subscription object in result and unsubscribe connection''' 15 | def inner(self, *args, **kwargs): 16 | subs = func(self, *args, **kwargs) 17 | if isinstance(subs, Subscription): 18 | return Pubsub.unsubscribe(self.connection_ref(), subscription=subs) 19 | else: 20 | return Pubsub.unsubscribe(self.connection_ref(), key=subs) 21 | return inner 22 | 23 | class Subscription(object): 24 | def __init__(self, event=None, **params): 25 | if hasattr(self, 'event'): 26 | if event: 27 | raise Exception("Event name already defined in Subscription object") 28 | else: 29 | if not event: 30 | raise Exception("Please define event name in constructor") 31 | else: 32 | self.event = event 33 | 34 | self.params = params # Internal parameters for subscription object 35 | self.connection_ref = None 36 | 37 | def process(self, *args, **kwargs): 38 | return args 39 | 40 | def get_key(self): 41 | '''This is an identifier for current subscription. It is sent to the client, 42 | so result should not contain any sensitive information.''' 43 | return hashlib.md5(str((self.event, self.params))).hexdigest() 44 | 45 | def get_session(self): 46 | '''Connection session may be useful in filter or process functions''' 47 | return self.connection_ref().get_session() 48 | 49 | @classmethod 50 | def emit(cls, *args, **kwargs): 51 | '''Shortcut for emiting this event to all subscribers.''' 52 | if not hasattr(cls, 'event'): 53 | raise Exception("Subscription.emit() can be used only for subclasses with filled 'event' class variable.") 54 | return Pubsub.emit(cls.event, *args, **kwargs) 55 | 56 | def emit_single(self, *args, **kwargs): 57 | '''Perform emit of this event just for current subscription.''' 58 | conn = self.connection_ref() 59 | if conn == None: 60 | # Connection is closed 61 | return 62 | 63 | payload = self.process(*args, **kwargs) 64 | if payload != None: 65 | if isinstance(payload, (tuple, list)): 66 | conn.writeJsonRequest(self.event, payload, is_notification=True) 67 | self.after_emit(*args, **kwargs) 68 | else: 69 | raise Exception("Return object from process() method must be list or None") 70 | 71 | def after_emit(self, *args, **kwargs): 72 | pass 73 | 74 | # Once function is defined, it will be called every time 75 | #def after_subscribe(self, _): 76 | # pass 77 | 78 | def __eq__(self, other): 79 | return (isinstance(other, Subscription) and other.get_key() == self.get_key()) 80 | 81 | def __ne__(self, other): 82 | return not self.__eq__(other) 83 | 84 | class Pubsub(object): 85 | __subscriptions = {} 86 | 87 | @classmethod 88 | def subscribe(cls, connection, subscription): 89 | if connection == None: 90 | raise custom_exceptions.PubsubException("Subscriber not connected") 91 | 92 | key = subscription.get_key() 93 | session = ConnectionRegistry.get_session(connection) 94 | if session == None: 95 | raise custom_exceptions.PubsubException("No session found") 96 | 97 | subscription.connection_ref = weakref.ref(connection) 98 | session.setdefault('subscriptions', {}) 99 | 100 | if key in session['subscriptions']: 101 | raise custom_exceptions.AlreadySubscribedException("This connection is already subscribed for such event.") 102 | 103 | session['subscriptions'][key] = subscription 104 | 105 | cls.__subscriptions.setdefault(subscription.event, weakref.WeakKeyDictionary()) 106 | cls.__subscriptions[subscription.event][subscription] = None 107 | 108 | if hasattr(subscription, 'after_subscribe'): 109 | if connection.on_finish != None: 110 | # If subscription is processed during the request, wait to 111 | # finish and then process the callback 112 | connection.on_finish.addCallback(subscription.after_subscribe) 113 | else: 114 | # If subscription is NOT processed during the request (any real use case?), 115 | # process callback instantly (better now than never). 116 | subscription.after_subscribe(True) 117 | 118 | # List of 2-tuples is prepared for future multi-subscriptions 119 | return ((subscription.event, key),) 120 | 121 | @classmethod 122 | def unsubscribe(cls, connection, subscription=None, key=None): 123 | if connection == None: 124 | raise custom_exceptions.PubsubException("Subscriber not connected") 125 | 126 | session = ConnectionRegistry.get_session(connection) 127 | if session == None: 128 | raise custom_exceptions.PubsubException("No session found") 129 | 130 | if subscription: 131 | key = subscription.get_key() 132 | 133 | try: 134 | # Subscription don't need to be removed from cls.__subscriptions, 135 | # because it uses weak reference there. 136 | del session['subscriptions'][key] 137 | except KeyError: 138 | print "Warning: Cannot remove subscription from connection session" 139 | return False 140 | 141 | return True 142 | 143 | @classmethod 144 | def get_subscription_count(cls, event): 145 | return len(cls.__subscriptions.get(event, {})) 146 | 147 | @classmethod 148 | def get_subscription(cls, connection, event, key=None): 149 | '''Return subscription object for given connection and event''' 150 | session = ConnectionRegistry.get_session(connection) 151 | if session == None: 152 | raise custom_exceptions.PubsubException("No session found") 153 | 154 | if key == None: 155 | sub = [ sub for sub in session.get('subscriptions', {}).values() if sub.event == event ] 156 | try: 157 | return sub[0] 158 | except IndexError: 159 | raise custom_exceptions.PubsubException("Not subscribed for event %s" % event) 160 | 161 | else: 162 | raise Exception("Searching subscriptions by key is not implemented yet") 163 | 164 | @classmethod 165 | def iterate_subscribers(cls, event): 166 | for subscription in cls.__subscriptions.get(event, weakref.WeakKeyDictionary()).iterkeyrefs(): 167 | subscription = subscription() 168 | if subscription == None: 169 | # Subscriber is no more connected 170 | continue 171 | 172 | yield subscription 173 | 174 | @classmethod 175 | def emit(cls, event, *args, **kwargs): 176 | for subscription in cls.iterate_subscribers(event): 177 | subscription.emit_single(*args, **kwargs) -------------------------------------------------------------------------------- /mining_libs/jobs.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import time 3 | import struct 4 | import subprocess 5 | import weakref 6 | import datetime 7 | 8 | from twisted.internet import defer 9 | 10 | import utils 11 | 12 | import stratum.logger 13 | log = stratum.logger.get_logger('proxy') 14 | 15 | # This fix py2exe issue with packaging the midstate module 16 | from midstate import calculateMidstate as __unusedimport 17 | 18 | try: 19 | from midstatec import test as midstateTest, midstate as calculateMidstate 20 | if not midstateTest(): 21 | log.warning("midstate library didn't passed self test!") 22 | raise ImportError("midstatec not usable") 23 | log.info("Using C extension for midstate speedup. Good!") 24 | except ImportError: 25 | log.info("C extension for midstate not available. Using default implementation instead.") 26 | try: 27 | from midstate import calculateMidstate 28 | except ImportError: 29 | calculateMidstate = None 30 | log.exception("No midstate generator available. Some old miners won't work properly.") 31 | 32 | class Job(object): 33 | def __init__(self): 34 | self.job_id = None 35 | self.hexHeaderHash = '' 36 | self.hexSeedHash = '' 37 | self.hexShareTarget = '' 38 | self.dtCreated = datetime.datetime.now() 39 | self.extranonce2 = 0 40 | self.merkle_to_extranonce2 = {} # Relation between merkle_hash and extranonce2 41 | 42 | @classmethod 43 | def build_from_broadcast(cls, job_id, hexHeaderHash, hexSeedHash, hexShareTarget): 44 | '''Build job object from Stratum server broadcast''' 45 | job = Job() 46 | job.job_id = job_id 47 | job.hexHeaderHash = hexHeaderHash 48 | job.hexSeedHash = hexSeedHash 49 | job.hexShareTarget = hexShareTarget 50 | return job 51 | 52 | def increase_extranonce2(self): 53 | self.extranonce2 += 1 54 | return self.extranonce2 55 | 56 | 57 | class JobRegistry(object): 58 | def __init__(self, f, cmd, no_midstate, real_target, use_old_target=False, scrypt_target=False): 59 | self.f = f 60 | self.cmd = cmd # execute this command on new block 61 | self.scrypt_target = scrypt_target # calculate target for scrypt algorithm instead of sha256 62 | self.no_midstate = no_midstate # Indicates if calculate midstate for getwork 63 | self.real_target = real_target # Indicates if real stratum target will be propagated to miners 64 | self.use_old_target = use_old_target # Use 00000000fffffff...f instead of correct 00000000ffffffff...0 target for really old miners 65 | self.jobs = [] 66 | self.last_job = None 67 | self.extranonce1 = None 68 | self.extranonce1_bin = None 69 | self.extranonce2_size = None 70 | 71 | self.target = 0 72 | self.target_hex = '' 73 | self.difficulty = 1 74 | self.set_difficulty(1) 75 | self.target1_hex = self.target_hex 76 | 77 | # Relation between merkle and job 78 | self.merkle_to_job= weakref.WeakValueDictionary() 79 | 80 | # Hook for LP broadcasts 81 | self.on_block = defer.Deferred() 82 | 83 | self.headerHash2Job= weakref.WeakValueDictionary() 84 | 85 | def execute_cmd(self, prevhash): 86 | if self.cmd: 87 | return subprocess.Popen(self.cmd.replace('%s', prevhash), shell=True) 88 | 89 | def set_extranonce(self, extranonce1, extranonce2_size): 90 | self.extranonce2_size = extranonce2_size 91 | self.extranonce1_bin = binascii.unhexlify(extranonce1) 92 | 93 | def set_difficulty(self, new_difficulty): 94 | if self.scrypt_target: 95 | dif1 = 0x0000ffff00000000000000000000000000000000000000000000000000000000 96 | else: 97 | dif1 = 0x00000000ffff0000000000000000000000000000000000000000000000000000 98 | self.target = int(dif1 / new_difficulty) 99 | self.target_hex = binascii.hexlify(utils.uint256_to_str(self.target)) 100 | self.difficulty = new_difficulty 101 | 102 | def build_full_extranonce(self, extranonce2): 103 | '''Join extranonce1 and extranonce2 together while padding 104 | extranonce2 length to extranonce2_size (provided by server).''' 105 | return self.extranonce1_bin + self.extranonce2_padding(extranonce2) 106 | 107 | def extranonce2_padding(self, extranonce2): 108 | '''Return extranonce2 with padding bytes''' 109 | 110 | if not self.extranonce2_size: 111 | raise Exception("Extranonce2_size isn't set yet") 112 | 113 | extranonce2_bin = struct.pack('>I', extranonce2) 114 | missing_len = self.extranonce2_size - len(extranonce2_bin) 115 | 116 | if missing_len < 0: 117 | # extranonce2 is too long, we should print warning on console, 118 | # but try to shorten extranonce2 119 | log.info("Extranonce size mismatch. Please report this error to pool operator!") 120 | return extranonce2_bin[abs(missing_len):] 121 | 122 | # This is probably more common situation, but it is perfectly 123 | # safe to add whitespaces 124 | return '\x00' * missing_len + extranonce2_bin 125 | 126 | def add_template(self, template, clean_jobs): 127 | if clean_jobs: 128 | # Pool asked us to stop submitting shares from previous jobs 129 | #log.info("Start deleting old jobs") 130 | newJobs = [] 131 | for job in self.jobs: 132 | dtDiff = datetime.datetime.now() - job.dtCreated 133 | if dtDiff.total_seconds() < 300: 134 | newJobs.append(job) 135 | self.jobs = newJobs 136 | for job in self.jobs: 137 | dtDiff = datetime.datetime.now() - job.dtCreated 138 | #log.info("Job %s %s " % (job.job_id, dtDiff.total_seconds())) 139 | 140 | self.jobs.append(template) 141 | self.last_job = template 142 | self.headerHash2Job[template.hexHeaderHash] = template 143 | 144 | if clean_jobs: 145 | # Force miners to reload jobs 146 | on_block = self.on_block 147 | self.on_block = defer.Deferred() 148 | on_block.callback(True) 149 | 150 | # blocknotify-compatible call 151 | self.execute_cmd(template.hexHeaderHash) 152 | 153 | def register_merkle(self, job, merkle_hash, extranonce2): 154 | # merkle_to_job is weak-ref, so it is cleaned up automatically 155 | # when job is dropped 156 | self.merkle_to_job[merkle_hash] = job 157 | job.merkle_to_extranonce2[merkle_hash] = extranonce2 158 | 159 | def get_job_from_header(self, header): 160 | '''Lookup for job and extranonce2 used for given blockheader (in hex)''' 161 | merkle_hash = header[72:136].lower() 162 | job = self.merkle_to_job[merkle_hash] 163 | extranonce2 = job.merkle_to_extranonce2[merkle_hash] 164 | return (job, extranonce2) 165 | 166 | def getwork(self): 167 | '''Miner requests for new getwork''' 168 | 169 | job = self.last_job # Pick the latest job from pool 170 | 171 | return [job.hexHeaderHash, job.hexSeedHash, job.hexShareTarget] 172 | 173 | def submit(self, hexNonce, hexHeaderHash, hexMixDigest, worker_name): 174 | #log.info("Submitting %s %s %s %s" % (hexHeaderHash, hexNonce, hexMixDigest, worker_name)) 175 | 176 | try: 177 | job = self.headerHash2Job[hexHeaderHash] 178 | except KeyError: 179 | log.info("Job not found") 180 | return False 181 | 182 | return self.f.rpc('mining.submit', [worker_name, job.job_id, hexNonce, hexHeaderHash, hexMixDigest]) 183 | -------------------------------------------------------------------------------- /mining_libs/stratum_listener.py: -------------------------------------------------------------------------------- 1 | import time 2 | import binascii 3 | import struct 4 | 5 | from twisted.internet import defer 6 | 7 | from stratum.services import GenericService 8 | from stratum.pubsub import Pubsub, Subscription 9 | from stratum.custom_exceptions import ServiceException, RemoteServiceException 10 | 11 | from jobs import JobRegistry 12 | 13 | import stratum.logger 14 | log = stratum.logger.get_logger('proxy') 15 | 16 | def var_int(i): 17 | if i <= 0xff: 18 | return struct.pack('>B', i) 19 | elif i <= 0xffff: 20 | return struct.pack('>H', i) 21 | raise Exception("number is too big") 22 | 23 | class UpstreamServiceException(ServiceException): 24 | code = -2 25 | 26 | class SubmitException(ServiceException): 27 | code = -2 28 | 29 | class DifficultySubscription(Subscription): 30 | event = 'mining.set_difficulty' 31 | difficulty = 1 32 | 33 | @classmethod 34 | def on_new_difficulty(cls, new_difficulty): 35 | cls.difficulty = new_difficulty 36 | cls.emit(new_difficulty) 37 | 38 | def after_subscribe(self, *args): 39 | self.emit_single(self.difficulty) 40 | 41 | class MiningSubscription(Subscription): 42 | '''This subscription object implements 43 | logic for broadcasting new jobs to the clients.''' 44 | 45 | event = 'mining.notify' 46 | 47 | last_broadcast = None 48 | 49 | @classmethod 50 | def disconnect_all(cls): 51 | for subs in Pubsub.iterate_subscribers(cls.event): 52 | if subs.connection_ref().transport != None: 53 | subs.connection_ref().transport.loseConnection() 54 | 55 | @classmethod 56 | def on_template(cls, job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, clean_jobs): 57 | '''Push new job to subscribed clients''' 58 | cls.last_broadcast = (job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, clean_jobs) 59 | cls.emit(job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, clean_jobs) 60 | 61 | def _finish_after_subscribe(self, result): 62 | '''Send new job to newly subscribed client''' 63 | try: 64 | (job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, _) = self.last_broadcast 65 | except Exception: 66 | log.error("Template not ready yet") 67 | return result 68 | 69 | self.emit_single(job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, True) 70 | return result 71 | 72 | def after_subscribe(self, *args): 73 | '''This will send new job to the client *after* he receive subscription details. 74 | on_finish callback solve the issue that job is broadcasted *during* 75 | the subscription request and client receive messages in wrong order.''' 76 | self.connection_ref().on_finish.addCallback(self._finish_after_subscribe) 77 | 78 | class StratumProxyService(GenericService): 79 | service_type = 'mining' 80 | service_vendor = 'mining_proxy' 81 | is_default = True 82 | 83 | _f = None # Factory of upstream Stratum connection 84 | custom_user = None 85 | custom_password = None 86 | extranonce1 = None 87 | extranonce2_size = None 88 | tail_iterator = 0 89 | registered_tails= [] 90 | 91 | @classmethod 92 | def _set_upstream_factory(cls, f): 93 | cls._f = f 94 | 95 | @classmethod 96 | def _set_custom_user(cls, custom_user, custom_password): 97 | cls.custom_user = custom_user 98 | cls.custom_password = custom_password 99 | 100 | @classmethod 101 | def _set_extranonce(cls, extranonce1, extranonce2_size): 102 | cls.extranonce1 = extranonce1 103 | cls.extranonce2_size = extranonce2_size 104 | 105 | @classmethod 106 | def _get_unused_tail(cls): 107 | '''Currently adds up to two bytes to extranonce1, 108 | limiting proxy for up to 65535 connected clients.''' 109 | 110 | for _ in range(0, 0xffff): # 0-65535 111 | cls.tail_iterator += 1 112 | cls.tail_iterator %= 0xffff 113 | 114 | # Zero extranonce is reserved for getwork connections 115 | if cls.tail_iterator == 0: 116 | cls.tail_iterator += 1 117 | 118 | # var_int throws an exception when input is >= 0xffff 119 | tail = var_int(cls.tail_iterator) 120 | tail_len = len(tail) 121 | 122 | if tail not in cls.registered_tails: 123 | cls.registered_tails.append(tail) 124 | return (binascii.hexlify(tail), cls.extranonce2_size - tail_len) 125 | 126 | raise Exception("Extranonce slots are full, please disconnect some miners!") 127 | 128 | def _drop_tail(self, result, tail): 129 | tail = binascii.unhexlify(tail) 130 | if tail in self.registered_tails: 131 | self.registered_tails.remove(tail) 132 | else: 133 | log.error("Given extranonce is not registered1") 134 | return result 135 | 136 | @defer.inlineCallbacks 137 | def authorize(self, worker_name, worker_password, *args): 138 | if self._f.client == None or not self._f.client.connected: 139 | yield self._f.on_connect 140 | 141 | if self.custom_user != None: 142 | # Already subscribed by main() 143 | defer.returnValue(True) 144 | 145 | result = (yield self._f.rpc('mining.authorize', [worker_name, worker_password])) 146 | defer.returnValue(result) 147 | 148 | @defer.inlineCallbacks 149 | def subscribe(self, *args): 150 | if self._f.client == None or not self._f.client.connected: 151 | yield self._f.on_connect 152 | 153 | if self._f.client == None or not self._f.client.connected: 154 | raise UpstreamServiceException("Upstream not connected") 155 | 156 | if self.extranonce1 == None: 157 | # This should never happen, because _f.on_connect is fired *after* 158 | # connection receive mining.subscribe response 159 | raise UpstreamServiceException("Not subscribed on upstream yet") 160 | 161 | (tail, extranonce2_size) = self._get_unused_tail() 162 | 163 | session = self.connection_ref().get_session() 164 | session['tail'] = tail 165 | 166 | # Remove extranonce from registry when client disconnect 167 | self.connection_ref().on_disconnect.addCallback(self._drop_tail, tail) 168 | 169 | subs1 = Pubsub.subscribe(self.connection_ref(), DifficultySubscription())[0] 170 | subs2 = Pubsub.subscribe(self.connection_ref(), MiningSubscription())[0] 171 | defer.returnValue(((subs1, subs2),) + (self.extranonce1+tail, extranonce2_size)) 172 | 173 | @defer.inlineCallbacks 174 | def submit(self, worker_name, job_id, extranonce2, ntime, nonce, *args): 175 | if self._f.client == None or not self._f.client.connected: 176 | raise SubmitException("Upstream not connected") 177 | 178 | session = self.connection_ref().get_session() 179 | tail = session.get('tail') 180 | if tail == None: 181 | raise SubmitException("Connection is not subscribed") 182 | 183 | if self.custom_user: 184 | worker_name = self.custom_user 185 | 186 | start = time.time() 187 | 188 | try: 189 | result = (yield self._f.rpc('mining.submit', [worker_name, job_id, tail+extranonce2, ntime, nonce])) 190 | except RemoteServiceException as exc: 191 | response_time = (time.time() - start) * 1000 192 | log.info("[%dms] Share from '%s' REJECTED: %s" % (response_time, worker_name, str(exc))) 193 | raise SubmitException(*exc.args) 194 | 195 | response_time = (time.time() - start) * 1000 196 | log.info("[%dms] Share from '%s' accepted, diff %d" % (response_time, worker_name, DifficultySubscription.difficulty)) 197 | defer.returnValue(result) 198 | 199 | def get_transactions(self, *args): 200 | log.warn("mining.get_transactions isn't supported by proxy") 201 | return [] 202 | -------------------------------------------------------------------------------- /mining_libs/getwork_listener.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from twisted.internet import defer 5 | from twisted.web.resource import Resource 6 | from twisted.web.server import NOT_DONE_YET 7 | 8 | import stratum.logger 9 | log = stratum.logger.get_logger('proxy') 10 | 11 | class Root(Resource): 12 | isLeaf = True 13 | 14 | def __init__(self, job_registry, workers, stratum_host, stratum_port, 15 | custom_stratum=None, custom_lp=None, custom_user=None, custom_password=''): 16 | Resource.__init__(self) 17 | self.job_registry = job_registry 18 | self.workers = workers 19 | self.stratum_host = stratum_host 20 | self.stratum_port = stratum_port 21 | self.custom_stratum = custom_stratum 22 | self.custom_lp = custom_lp 23 | self.custom_user = custom_user 24 | self.custom_password = custom_password 25 | 26 | def json_response(self, msg_id, result): 27 | resp = json.dumps({'id': msg_id, 'jsonrpc': '2.0', 'result': result}) 28 | #print "RESPONSE", resp 29 | return resp 30 | 31 | def json_error(self, msg_id, code, message): 32 | resp = json.dumps({'id': msg_id, 'jsonrpc': '2.0', 'result': False, 'error': message}) 33 | #print "ERROR", resp 34 | return resp 35 | 36 | def _on_submit(self, result, request, msg_id, blockheader, worker_name, start_time): 37 | response_time = (time.time() - start_time) * 1000 38 | if result == True: 39 | log.warning("[%dms] Share from '%s' accepted, diff %d" % (response_time, worker_name, self.job_registry.difficulty)) 40 | else: 41 | log.warning("[%dms] Share from '%s' REJECTED" % (response_time, worker_name)) 42 | 43 | try: 44 | request.write(self.json_response(msg_id, result)) 45 | request.finish() 46 | except RuntimeError: 47 | # RuntimeError is thrown by Request class when 48 | # client is disconnected already 49 | pass 50 | 51 | def _on_submit_failure(self, failure, request, msg_id, blockheader, worker_name, start_time): 52 | response_time = (time.time() - start_time) * 1000 53 | 54 | # Submit for some reason failed 55 | try: 56 | request.write(self.json_response(msg_id, False)) 57 | request.finish() 58 | except RuntimeError: 59 | # RuntimeError is thrown by Request class when 60 | # client is disconnected already 61 | pass 62 | 63 | log.warning("[%dms] Share from '%s' REJECTED: %s" % \ 64 | (response_time, worker_name, failure.getErrorMessage())) 65 | 66 | def _on_authorized(self, is_authorized, request, worker_name): 67 | data = json.loads(request.content.read()) 68 | 69 | if not is_authorized: 70 | request.write(self.json_error(data.get('id', 0), -1, "Bad worker credentials")) 71 | request.finish() 72 | return 73 | 74 | if not self.job_registry.last_job: 75 | log.warning('Getworkmaker is waiting for a job...') 76 | request.write(self.json_error(data.get('id', 0), -1, "Getworkmake is waiting for a job...")) 77 | request.finish() 78 | return 79 | 80 | if data['method'] == 'getwork': 81 | if 'params' not in data or not len(data['params']): 82 | 83 | # getwork request 84 | log.info("Worker '%s' asks for new work" % worker_name) 85 | extensions = request.getHeader('x-mining-extensions') 86 | no_midstate = extensions and 'midstate' in extensions 87 | request.write(self.json_response(data.get('id', 0), self.job_registry.getwork(no_midstate=no_midstate))) 88 | request.finish() 89 | return 90 | 91 | else: 92 | 93 | # submit 94 | d = defer.maybeDeferred(self.job_registry.submit, data['params'][0], worker_name) 95 | 96 | start_time = time.time() 97 | d.addCallback(self._on_submit, request, data.get('id', 0), data['params'][0][:160], worker_name, start_time) 98 | d.addErrback(self._on_submit_failure, request, data.get('id', 0), data['params'][0][:160], worker_name, start_time) 99 | return 100 | elif data['method'] == 'eth_getWork': 101 | #log.info("Worker '%s' eth_getWork" % worker_name) 102 | request.write(self.json_response(data.get('id', 0), self.job_registry.getwork())) 103 | request.finish() 104 | return 105 | elif data['method'] == 'eth_submitWork': 106 | d = defer.maybeDeferred(self.job_registry.submit, data['params'][0], data['params'][1], data['params'][2], worker_name) 107 | start_time = time.time() 108 | d.addCallback(self._on_submit, request, data.get('id', 0), data['params'], worker_name, start_time) 109 | d.addErrback(self._on_submit_failure, request, data.get('id', 0), data['params'], worker_name, start_time) 110 | return 111 | elif data['method'] == 'eth_submitHashrate': 112 | #log.info("Worker '%s' eth_submitHashrate" % worker_name) 113 | request.write(self.json_response(data.get('id', 0), True)) 114 | request.finish() 115 | return 116 | 117 | 118 | request.write(self.json_error(1, -1, "Unsupported method '%s'" % data['method'])) 119 | request.finish() 120 | 121 | def _on_failure(self, failure, request): 122 | request.write(self.json_error(0, -1, "Unexpected error during authorization")) 123 | request.finish() 124 | raise failure 125 | 126 | def _prepare_headers(self, request): 127 | request.setHeader('content-type', 'application/json') 128 | 129 | if self.custom_stratum: 130 | request.setHeader('x-stratum', self.custom_stratum) 131 | elif self.stratum_port: 132 | request.setHeader('x-stratum', 'stratum+tcp://%s:%d' % (request.getRequestHostname(), self.stratum_port)) 133 | 134 | if self.custom_lp: 135 | request.setHeader('x-long-polling', self.custom_lp) 136 | else: 137 | request.setHeader('x-long-polling', '/lp') 138 | 139 | request.setHeader('x-roll-ntime', 1) 140 | 141 | def _on_lp_broadcast(self, _, request): 142 | try: 143 | worker_name = request.getUser() 144 | except: 145 | worker_name = '' 146 | 147 | log.info("LP broadcast for worker '%s'" % worker_name) 148 | extensions = request.getHeader('x-mining-extensions') 149 | no_midstate = extensions and 'midstate' in extensions 150 | payload = self.json_response(0, self.job_registry.getwork(no_midstate=no_midstate)) 151 | 152 | try: 153 | request.write(payload) 154 | request.finish() 155 | except RuntimeError: 156 | # RuntimeError is thrown by Request class when 157 | # client is disconnected already 158 | pass 159 | 160 | def render_POST(self, request): 161 | self._prepare_headers(request) 162 | 163 | worker_name = '' 164 | uriSplit = request.uri.split(":") 165 | if len(uriSplit) >= 2: 166 | worker_name = uriSplit[0].replace("/", "") 167 | password = uriSplit[1] 168 | 169 | if self.custom_user: 170 | worker_name = self.custom_user 171 | password = self.custom_password 172 | 173 | if worker_name == '': 174 | log.warning("Authorization required") 175 | request.setResponseCode(401) 176 | request.setHeader('WWW-Authenticate', 'Basic realm="stratum-mining-proxy"') 177 | return "Authorization required" 178 | 179 | self._prepare_headers(request) 180 | 181 | if request.path.startswith('/lp'): 182 | log.info("Worker '%s' subscribed for LP" % worker_name) 183 | self.job_registry.on_block.addCallback(self._on_lp_broadcast, request) 184 | return NOT_DONE_YET 185 | 186 | d = defer.maybeDeferred(self.workers.authorize, worker_name, password) 187 | d.addCallback(self._on_authorized, request, worker_name) 188 | d.addErrback(self._on_failure, request) 189 | return NOT_DONE_YET 190 | 191 | def render_GET(self, request): 192 | self._prepare_headers(request) 193 | 194 | try: 195 | worker_name = request.getUser() 196 | except: 197 | worker_name = '' 198 | 199 | if self.custom_user: 200 | worker_name = self.custom_user 201 | password = self.custom_password 202 | 203 | log.info("Worker '%s' subscribed for LP at %s" % (worker_name, request.path)) 204 | self.job_registry.on_block.addCallback(self._on_lp_broadcast, request) 205 | return NOT_DONE_YET 206 | -------------------------------------------------------------------------------- /stratum/http_transport.py: -------------------------------------------------------------------------------- 1 | from twisted.web.resource import Resource 2 | from twisted.web.server import Request, Session, NOT_DONE_YET 3 | from twisted.internet import defer 4 | from twisted.python.failure import Failure 5 | import hashlib 6 | import json 7 | import string 8 | 9 | import helpers 10 | import semaphore 11 | #from storage import Storage 12 | from protocol import Protocol, RequestCounter 13 | from event_handler import GenericEventHandler 14 | import settings 15 | 16 | import logger 17 | log = logger.get_logger('http_transport') 18 | 19 | class Transport(object): 20 | def __init__(self, session_id, lock): 21 | self.buffer = [] 22 | self.session_id = session_id 23 | self.lock = lock 24 | self.push_url = None # None or full URL for HTTP Push 25 | self.peer = None 26 | 27 | # For compatibility with generic transport, not used in HTTP transport 28 | self.disconnecting = False 29 | 30 | def getPeer(self): 31 | return self.peer 32 | 33 | def write(self, data): 34 | if len(self.buffer) >= settings.HTTP_BUFFER_LIMIT: 35 | # Drop first (oldest) item in buffer 36 | # if buffer crossed allowed limit. 37 | # This isn't totally exact, because one record in buffer 38 | # can teoretically contains more than one message (divided by \n), 39 | # but current server implementation don't store responses in this way, 40 | # so counting exact number of messages will lead to unnecessary overhead. 41 | self.buffer.pop(0) 42 | 43 | self.buffer.append(data) 44 | 45 | if not self.lock.is_locked() and self.push_url: 46 | # Push the buffer to callback URL 47 | # TODO: Buffer responses and perform callbgitacks in batches 48 | self.push_buffer() 49 | 50 | def push_buffer(self): 51 | '''Push the content of the buffer into callback URL''' 52 | if not self.push_url: 53 | return 54 | 55 | # FIXME: Don't expect any response 56 | helpers.get_page(self.push_url, method='POST', 57 | headers={"content-type": "application/stratum", 58 | "x-session-id": self.session_id}, 59 | payload=self.fetch_buffer()) 60 | 61 | def fetch_buffer(self): 62 | ret = ''.join(self.buffer) 63 | self.buffer = [] 64 | return ret 65 | 66 | def set_push_url(self, url): 67 | self.push_url = url 68 | 69 | def monkeypatch_method(cls): 70 | '''Perform monkey patch for given class.''' 71 | def decorator(func): 72 | setattr(cls, func.__name__, func) 73 | return func 74 | return decorator 75 | 76 | @monkeypatch_method(Request) 77 | def getSession(self, sessionInterface=None, cookie_prefix='TWISTEDSESSION'): 78 | '''Monkey patch for Request object, providing backward-compatible 79 | getSession method which can handle custom cookie as a session ID 80 | (which is necessary for following Stratum protocol specs). 81 | Unfortunately twisted developers rejected named-cookie feature, 82 | which is pressing me into this ugly solution... 83 | 84 | TODO: Especially this would deserve some unit test to be sure it doesn't break 85 | in future twisted versions. 86 | ''' 87 | # Session management 88 | if not self.session: 89 | cookiename = string.join([cookie_prefix] + self.sitepath, "_") 90 | sessionCookie = self.getCookie(cookiename) 91 | if sessionCookie: 92 | try: 93 | self.session = self.site.getSession(sessionCookie) 94 | except KeyError: 95 | pass 96 | # if it still hasn't been set, fix it up. 97 | if not self.session: 98 | self.session = self.site.makeSession() 99 | self.addCookie(cookiename, self.session.uid, path='/') 100 | self.session.touch() 101 | if sessionInterface: 102 | return self.session.getComponent(sessionInterface) 103 | return self.session 104 | 105 | class HttpSession(Session): 106 | sessionTimeout = settings.HTTP_SESSION_TIMEOUT 107 | 108 | def __init__(self, *args, **kwargs): 109 | Session.__init__(self, *args, **kwargs) 110 | #self.storage = Storage() 111 | 112 | # Reference to connection object (Protocol instance) 113 | self.protocol = None 114 | 115 | # Synchronizing object for avoiding race condition on session 116 | self.lock = semaphore.Semaphore(1) 117 | 118 | # Output buffering 119 | self.transport = Transport(self.uid, self.lock) 120 | 121 | # Setup cleanup method on session expiration 122 | self.notifyOnExpire(lambda: HttpSession.on_expire(self)) 123 | 124 | @classmethod 125 | def on_expire(cls, sess_obj): 126 | # FIXME: Close protocol connection 127 | print "EXPIRING SESSION", sess_obj 128 | 129 | if sess_obj.protocol: 130 | sess_obj.protocol.connectionLost(Failure(Exception("HTTP session closed"))) 131 | 132 | sess_obj.protocol = None 133 | 134 | class Root(Resource): 135 | isLeaf = True 136 | 137 | def __init__(self, debug=False, signing_key=None, signing_id=None, 138 | event_handler=GenericEventHandler): 139 | Resource.__init__(self) 140 | self.signing_key = signing_key 141 | self.signing_id = signing_id 142 | self.debug = debug # This class acts as a 'factory', debug is used by Protocol 143 | self.event_handler = event_handler 144 | 145 | def render_GET(self, request): 146 | if not settings.BROWSER_ENABLE: 147 | return "Welcome to %s server. Use HTTP POST to talk with the server." % settings.USER_AGENT 148 | 149 | # TODO: Web browser 150 | return "Web browser not implemented yet" 151 | 152 | def render_OPTIONS(self, request): 153 | session = request.getSession(cookie_prefix='STRATUM_SESSION') 154 | 155 | request.setHeader('server', settings.USER_AGENT) 156 | request.setHeader('x-session-timeout', session.sessionTimeout) 157 | request.setHeader('access-control-allow-origin', '*') # Allow access from any other domain 158 | request.setHeader('access-control-allow-methods', 'POST, OPTIONS') 159 | request.setHeader('access-control-allow-headers', 'Content-Type') 160 | return '' 161 | 162 | def render_POST(self, request): 163 | session = request.getSession(cookie_prefix='STRATUM_SESSION') 164 | 165 | l = session.lock.acquire() 166 | l.addCallback(self._perform_request, request, session) 167 | return NOT_DONE_YET 168 | 169 | def _perform_request(self, _, request, session): 170 | request.setHeader('content-type', 'application/stratum') 171 | request.setHeader('server', settings.USER_AGENT) 172 | request.setHeader('x-session-timeout', session.sessionTimeout) 173 | request.setHeader('access-control-allow-origin', '*') # Allow access from any other domain 174 | 175 | # Update client's IP address 176 | session.transport.peer = request.getHost() 177 | 178 | # Although it isn't intuitive at all, request.getHeader reads request headers, 179 | # but request.setHeader (few lines above) writes response headers... 180 | if 'application/stratum' not in request.getHeader('content-type'): 181 | session.transport.write("%s\n" % json.dumps({'id': None, 'result': None, 'error': (-1, "Content-type must be 'application/stratum'. See http://stratum.bitcoin.cz for more info.", "")})) 182 | self._finish(None, request, session.transport, session.lock) 183 | return 184 | 185 | if not session.protocol: 186 | # Build a "protocol connection" 187 | proto = Protocol() 188 | proto.transport = session.transport 189 | proto.factory = self 190 | proto.connectionMade() 191 | session.protocol = proto 192 | else: 193 | proto = session.protocol 194 | 195 | # Update callback URL if presented 196 | callback_url = request.getHeader('x-callback-url') 197 | if callback_url != None: 198 | if callback_url == '': 199 | # Blank value of callback URL switches HTTP Push back to HTTP Poll 200 | session.transport.push_url = None 201 | else: 202 | session.transport.push_url = callback_url 203 | 204 | data = request.content.read() 205 | if data: 206 | counter = RequestCounter() 207 | counter.on_finish.addCallback(self._finish, request, session.transport, session.lock) 208 | proto.dataReceived(data, request_counter=counter) 209 | else: 210 | # Ping message (empty request) of HTTP Polling 211 | self._finish(None, request, session.transport, session.lock) 212 | 213 | 214 | @classmethod 215 | def _finish(cls, _, request, transport, lock): 216 | # First parameter is callback result; not used here 217 | data = transport.fetch_buffer() 218 | request.setHeader('content-length', len(data)) 219 | request.setHeader('content-md5', hashlib.md5(data).hexdigest()) 220 | request.setHeader('x-content-sha256', hashlib.sha256(data).hexdigest()) 221 | request.write(data) 222 | request.finish() 223 | lock.release() 224 | -------------------------------------------------------------------------------- /stratum/services.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import defer, threads 2 | from twisted.python import log 3 | import hashlib 4 | import weakref 5 | import re 6 | 7 | import custom_exceptions 8 | 9 | VENDOR_RE = re.compile(r'\[(.*)\]') 10 | 11 | class ServiceEventHandler(object): # reimplements event_handler.GenericEventHandler 12 | def _handle_event(self, msg_method, msg_params, connection_ref): 13 | return ServiceFactory.call(msg_method, msg_params, connection_ref=connection_ref) 14 | 15 | class ResultObject(object): 16 | def __init__(self, result=None, sign=False, sign_algo=None, sign_id=None): 17 | self.result = result 18 | self.sign = sign 19 | self.sign_algo = sign_algo 20 | self.sign_id = sign_id 21 | 22 | def wrap_result_object(obj): 23 | def _wrap(o): 24 | if isinstance(o, ResultObject): 25 | return o 26 | return ResultObject(result=o) 27 | 28 | if isinstance(obj, defer.Deferred): 29 | # We don't have result yet, just wait for it and wrap it later 30 | obj.addCallback(_wrap) 31 | return obj 32 | 33 | return _wrap(obj) 34 | 35 | class ServiceFactory(object): 36 | registry = {} # Mapping service_type -> vendor -> cls 37 | 38 | @classmethod 39 | def _split_method(cls, method): 40 | '''Parses "some.service[vendor].method" string 41 | and returns 3-tuple with (service_type, vendor, rpc_method)''' 42 | 43 | # Splits the service type and method name 44 | (service_type, method_name) = method.rsplit('.', 1) 45 | vendor = None 46 | 47 | if '[' in service_type: 48 | # Use regular expression only when brackets found 49 | try: 50 | vendor = VENDOR_RE.search(service_type).group(1) 51 | service_type = service_type.replace('[%s]' % vendor, '') 52 | except: 53 | raise 54 | #raise custom_exceptions.ServiceNotFoundException("Invalid syntax in service name '%s'" % type_name[0]) 55 | 56 | return (service_type, vendor, method_name) 57 | 58 | @classmethod 59 | def call(cls, method, params, connection_ref=None): 60 | try: 61 | (service_type, vendor, func_name) = cls._split_method(method) 62 | except ValueError: 63 | raise custom_exceptions.MethodNotFoundException("Method name parsing failed. You *must* use format ., e.g. 'example.ping'") 64 | 65 | try: 66 | if func_name.startswith('_'): 67 | raise 68 | 69 | _inst = cls.lookup(service_type, vendor=vendor)() 70 | _inst.connection_ref = weakref.ref(connection_ref) 71 | func = _inst.__getattribute__(func_name) 72 | if not callable(func): 73 | raise 74 | except: 75 | raise custom_exceptions.MethodNotFoundException("Method '%s' not found for service '%s'" % (func_name, service_type)) 76 | 77 | def _run(func, *params): 78 | return wrap_result_object(func(*params)) 79 | 80 | # Returns Defer which will lead to ResultObject sometimes 81 | return defer.maybeDeferred(_run, func, *params) 82 | 83 | @classmethod 84 | def lookup(cls, service_type, vendor=None): 85 | # Lookup for service type provided by specific vendor 86 | if vendor: 87 | try: 88 | return cls.registry[service_type][vendor] 89 | except KeyError: 90 | raise custom_exceptions.ServiceNotFoundException("Class for given service type and vendor isn't registered") 91 | 92 | # Lookup for any vendor, prefer default one 93 | try: 94 | vendors = cls.registry[service_type] 95 | except KeyError: 96 | raise custom_exceptions.ServiceNotFoundException("Class for given service type isn't registered") 97 | 98 | last_found = None 99 | for _, _cls in vendors.items(): 100 | last_found = _cls 101 | if last_found.is_default: 102 | return last_found 103 | 104 | if not last_found: 105 | raise custom_exceptions.ServiceNotFoundException("Class for given service type isn't registered") 106 | 107 | return last_found 108 | 109 | @classmethod 110 | def register_service(cls, _cls, meta): 111 | # Register service class to ServiceFactory 112 | service_type = meta.get('service_type') 113 | service_vendor = meta.get('service_vendor') 114 | is_default = meta.get('is_default') 115 | 116 | if str(_cls.__name__) in ('GenericService',): 117 | # str() is ugly hack, but it is avoiding circular references 118 | return 119 | 120 | if not service_type: 121 | raise custom_exceptions.MissingServiceTypeException("Service class '%s' is missing 'service_type' property." % _cls) 122 | 123 | if not service_vendor: 124 | raise custom_exceptions.MissingServiceVendorException("Service class '%s' is missing 'service_vendor' property." % _cls) 125 | 126 | if is_default == None: 127 | raise custom_exceptions.MissingServiceIsDefaultException("Service class '%s' is missing 'is_default' property." % _cls) 128 | 129 | if is_default: 130 | # Check if there's not any other default service 131 | 132 | try: 133 | current = cls.lookup(service_type) 134 | if current.is_default: 135 | raise custom_exceptions.DefaultServiceAlreadyExistException("Default service already exists for type '%s'" % service_type) 136 | except custom_exceptions.ServiceNotFoundException: 137 | pass 138 | 139 | setup_func = meta.get('_setup', None) 140 | if setup_func != None: 141 | _cls()._setup() 142 | 143 | ServiceFactory.registry.setdefault(service_type, {}) 144 | ServiceFactory.registry[service_type][service_vendor] = _cls 145 | 146 | log.msg("Registered %s for service '%s', vendor '%s' (default: %s)" % (_cls, service_type, service_vendor, is_default)) 147 | 148 | def signature(func): 149 | '''Decorate RPC method result with server's signature. 150 | This decorator can be chained with Deferred or inlineCallbacks, thanks to _sign_generator() hack.''' 151 | 152 | def _sign_generator(iterator): 153 | '''Iterate thru generator object, detects BaseException 154 | and inject signature into exception's value (=result of inner method). 155 | This is black magic because of decorating inlineCallbacks methods. 156 | See returnValue documentation for understanding this: 157 | http://twistedmatrix.com/documents/11.0.0/api/twisted.internet.defer.html#returnValue''' 158 | 159 | for i in iterator: 160 | try: 161 | iterator.send((yield i)) 162 | except BaseException as exc: 163 | exc.value = wrap_result_object(exc.value) 164 | exc.value.sign = True 165 | raise 166 | 167 | def _sign_deferred(res): 168 | obj = wrap_result_object(res) 169 | obj.sign = True 170 | return obj 171 | 172 | def _sign_failure(fail): 173 | fail.value = wrap_result_object(fail.value) 174 | fail.value.sign = True 175 | return fail 176 | 177 | def inner(*args, **kwargs): 178 | ret = defer.maybeDeferred(func, *args, **kwargs) 179 | #if isinstance(ret, defer.Deferred): 180 | ret.addCallback(_sign_deferred) 181 | ret.addErrback(_sign_failure) 182 | return ret 183 | # return ret 184 | #elif isinstance(ret, types.GeneratorType): 185 | # return _sign_generator(ret) 186 | #else: 187 | # ret = wrap_result_object(ret) 188 | # ret.sign = True 189 | # return ret 190 | return inner 191 | 192 | def synchronous(func): 193 | '''Run given method synchronously in separate thread and return the result.''' 194 | def inner(*args, **kwargs): 195 | return threads.deferToThread(func, *args, **kwargs) 196 | return inner 197 | 198 | def admin(func): 199 | '''Requires an extra first parameter with superadministrator password''' 200 | import settings 201 | def inner(*args, **kwargs): 202 | if not len(args): 203 | raise custom_exceptions.UnauthorizedException("Missing password") 204 | 205 | if settings.ADMIN_RESTRICT_INTERFACE != None: 206 | ip = args[0].connection_ref()._get_ip() 207 | if settings.ADMIN_RESTRICT_INTERFACE != ip: 208 | raise custom_exceptions.UnauthorizedException("RPC call not allowed from your IP") 209 | 210 | if not settings.ADMIN_PASSWORD_SHA256: 211 | raise custom_exceptions.UnauthorizedException("Admin password not set, RPC call disabled") 212 | 213 | (password, args) = (args[1], [args[0],] + list(args[2:])) 214 | 215 | if hashlib.sha256(password).hexdigest() != settings.ADMIN_PASSWORD_SHA256: 216 | raise custom_exceptions.UnauthorizedException("Wrong password") 217 | 218 | return func(*args, **kwargs) 219 | return inner 220 | 221 | class ServiceMetaclass(type): 222 | def __init__(cls, name, bases, _dict): 223 | super(ServiceMetaclass, cls).__init__(name, bases, _dict) 224 | ServiceFactory.register_service(cls, _dict) 225 | 226 | class GenericService(object): 227 | __metaclass__ = ServiceMetaclass 228 | service_type = None 229 | service_vendor = None 230 | is_default = None 231 | 232 | # Keep weak reference to connection which asked for current 233 | # RPC call. Useful for pubsub mechanism, but use it with care. 234 | # It does not need to point to actual and valid data, so 235 | # you have to check if connection still exists every time. 236 | connection_ref = None 237 | 238 | class ServiceDiscovery(GenericService): 239 | service_type = 'discovery' 240 | service_vendor = 'Stratum' 241 | is_default = True 242 | 243 | def list_services(self): 244 | return ServiceFactory.registry.keys() 245 | 246 | def list_vendors(self, service_type): 247 | return ServiceFactory.registry[service_type].keys() 248 | 249 | def list_methods(self, service_name): 250 | # Accepts also vendors in square brackets: firstbits[firstbits.com] 251 | 252 | # Parse service type and vendor. We don't care about the method name, 253 | # but _split_method needs full path to some RPC method. 254 | (service_type, vendor, _) = ServiceFactory._split_method("%s.foo" % service_name) 255 | service = ServiceFactory.lookup(service_type, vendor) 256 | out = [] 257 | 258 | for name, obj in service.__dict__.items(): 259 | 260 | if name.startswith('_'): 261 | continue 262 | 263 | if not callable(obj): 264 | continue 265 | 266 | out.append(name) 267 | 268 | return out 269 | 270 | def list_params(self, method): 271 | (service_type, vendor, meth) = ServiceFactory._split_method(method) 272 | service = ServiceFactory.lookup(service_type, vendor) 273 | 274 | # Load params and helper text from method attributes 275 | func = service.__dict__[meth] 276 | params = getattr(func, 'params', None) 277 | help_text = getattr(func, 'help_text', None) 278 | 279 | return (help_text, params) 280 | list_params.help_text = "Accepts name of method and returns its description and available parameters. Example: 'firstbits.resolve'" 281 | list_params.params = [('method', 'string', 'Method to lookup for description and parameters.'),] -------------------------------------------------------------------------------- /mining_proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Stratum mining proxy 4 | Copyright (C) 2012 Marek Palatinus 5 | Copyright (C) 2015 Coinotron 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | ''' 20 | 21 | import argparse 22 | import time 23 | import os 24 | import socket 25 | 26 | def parse_args(): 27 | parser = argparse.ArgumentParser(description='This proxy allows you to run getwork-based Ethereum miners against Stratum mining pool.') 28 | parser.add_argument('-o', '--host', dest='host', type=str, default='coinotron.com', help='Hostname of Stratum mining pool') 29 | parser.add_argument('-p', '--port', dest='port', type=int, default=3344, help='Port of Stratum mining pool') 30 | parser.add_argument('-sh', '--stratum-host', dest='stratum_host', type=str, default='0.0.0.0', help='On which network interface listen for stratum miners. Use "localhost" for listening on internal IP only.') 31 | parser.add_argument('-sp', '--stratum-port', dest='stratum_port', type=int, default=3333, help='Port on which port listen for stratum miners.') 32 | parser.add_argument('-oh', '--getwork-host', dest='getwork_host', type=str, default='0.0.0.0', help='On which network interface listen for getwork miners. Use "localhost" for listening on internal IP only.') 33 | parser.add_argument('-gp', '--getwork-port', dest='getwork_port', type=int, default=8332, help='Port on which port listen for getwork miners. Use another port if you have bitcoind RPC running on this machine already.') 34 | parser.add_argument('-nm', '--no-midstate', dest='no_midstate', action='store_true', help="Don't compute midstate for getwork. This has outstanding performance boost, but some old miners like Diablo don't work without midstate.") 35 | parser.add_argument('-rt', '--real-target', dest='real_target', action='store_true', help="Propagate >diff1 target to getwork miners. Some miners work incorrectly with higher difficulty.") 36 | parser.add_argument('-cl', '--custom-lp', dest='custom_lp', type=str, help='Override URL provided in X-Long-Polling header') 37 | parser.add_argument('-cs', '--custom-stratum', dest='custom_stratum', type=str, help='Override URL provided in X-Stratum header') 38 | parser.add_argument('-cu', '--custom-user', dest='custom_user', type=str, help='Use this username for submitting shares') 39 | parser.add_argument('-cp', '--custom-password', dest='custom_password', type=str, help='Use this password for submitting shares') 40 | parser.add_argument('--old-target', dest='old_target', action='store_true', help='Provides backward compatible targets for some deprecated getwork miners.') 41 | parser.add_argument('--blocknotify', dest='blocknotify_cmd', type=str, default='', help='Execute command when the best block changes (%%s in BLOCKNOTIFY_CMD is replaced by block hash)') 42 | parser.add_argument('--socks', dest='proxy', type=str, default='', help='Use socks5 proxy for upstream Stratum connection, specify as host:port') 43 | parser.add_argument('--tor', dest='tor', action='store_true', help='Configure proxy to mine over Tor (requires Tor running on local machine)') 44 | parser.add_argument('-t', '--test', dest='test', action='store_true', help='Run performance test on startup') 45 | parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='Enable low-level debugging messages') 46 | parser.add_argument('-q', '--quiet', dest='quiet', action='store_true', help='Make output more quiet') 47 | parser.add_argument('-i', '--pid-file', dest='pid_file', type=str, help='Store process pid to the file') 48 | parser.add_argument('-l', '--log-file', dest='log_file', type=str, help='Log to specified file') 49 | parser.add_argument('-st', '--scrypt-target', dest='scrypt_target', action='store_true', help='Calculate targets for scrypt algorithm') 50 | return parser.parse_args() 51 | 52 | from stratum import settings 53 | settings.LOGLEVEL='INFO' 54 | 55 | if __name__ == '__main__': 56 | # We need to parse args & setup Stratum environment 57 | # before any other imports 58 | args = parse_args() 59 | if args.quiet: 60 | settings.DEBUG = False 61 | settings.LOGLEVEL = 'WARNING' 62 | elif args.verbose: 63 | settings.DEBUG = True 64 | settings.LOGLEVEL = 'DEBUG' 65 | if args.log_file: 66 | settings.LOGFILE = args.log_file 67 | 68 | from twisted.internet import reactor, defer 69 | from stratum.socket_transport import SocketTransportFactory, SocketTransportClientFactory 70 | from stratum.services import ServiceEventHandler 71 | from twisted.web.server import Site 72 | 73 | from mining_libs import stratum_listener 74 | from mining_libs import getwork_listener 75 | from mining_libs import client_service 76 | from mining_libs import jobs 77 | from mining_libs import worker_registry 78 | from mining_libs import multicast_responder 79 | from mining_libs import version 80 | from mining_libs import utils 81 | 82 | import stratum.logger 83 | log = stratum.logger.get_logger('proxy') 84 | 85 | def on_shutdown(f): 86 | '''Clean environment properly''' 87 | log.info("Shutting down proxy...") 88 | f.is_reconnecting = False # Don't let stratum factory to reconnect again 89 | 90 | @defer.inlineCallbacks 91 | def on_connect(f, workers, job_registry): 92 | '''Callback when proxy get connected to the pool''' 93 | log.info("Connected to Stratum pool at %s:%d" % f.main_host) 94 | #reactor.callLater(30, f.client.transport.loseConnection) 95 | 96 | # Hook to on_connect again 97 | f.on_connect.addCallback(on_connect, workers, job_registry) 98 | 99 | # Every worker have to re-autorize 100 | workers.clear_authorizations() 101 | 102 | # Subscribe for receiving jobs 103 | log.info("Subscribing for mining jobs") 104 | (_, extranonce1, extranonce2_size) = (yield f.rpc('mining.subscribe', []))[:3] 105 | job_registry.set_extranonce(extranonce1, extranonce2_size) 106 | stratum_listener.StratumProxyService._set_extranonce(extranonce1, extranonce2_size) 107 | 108 | if args.custom_user: 109 | log.warning("Authorizing custom user %s, password %s" % (args.custom_user, args.custom_password)) 110 | workers.authorize(args.custom_user, args.custom_password) 111 | 112 | defer.returnValue(f) 113 | 114 | def on_disconnect(f, workers, job_registry): 115 | '''Callback when proxy get disconnected from the pool''' 116 | log.info("Disconnected from Stratum pool at %s:%d" % f.main_host) 117 | f.on_disconnect.addCallback(on_disconnect, workers, job_registry) 118 | 119 | stratum_listener.MiningSubscription.disconnect_all() 120 | 121 | # Reject miners because we don't give a *job :-) 122 | workers.clear_authorizations() 123 | 124 | return f 125 | 126 | def test_launcher(result, job_registry): 127 | def run_test(): 128 | log.info("Running performance self-test...") 129 | for m in (True, False): 130 | log.info("Generating with midstate: %s" % m) 131 | log.info("Example getwork:") 132 | log.info(job_registry.getwork(no_midstate=not m)) 133 | 134 | start = time.time() 135 | n = 10000 136 | 137 | for x in range(n): 138 | job_registry.getwork(no_midstate=not m) 139 | 140 | log.info("%d getworks generated in %.03f sec, %d gw/s" % \ 141 | (n, time.time() - start, n / (time.time()-start))) 142 | 143 | log.info("Test done") 144 | reactor.callLater(1, run_test) 145 | return result 146 | 147 | def print_deprecation_warning(): 148 | '''Once new version is detected, this method prints deprecation warning every 30 seconds.''' 149 | 150 | log.warning("New proxy version available! Please update!") 151 | reactor.callLater(30, print_deprecation_warning) 152 | 153 | def test_update(): 154 | '''Perform lookup for newer proxy version, on startup and then once a day. 155 | When new version is found, it starts printing warning message and turned off next checks.''' 156 | 157 | GIT_URL='https://raw.github.com/slush0/stratum-mining-proxy/master/mining_libs/version.py' 158 | 159 | import urllib2 160 | log.warning("Checking for updates...") 161 | try: 162 | if version.VERSION not in urllib2.urlopen(GIT_URL).read(): 163 | print_deprecation_warning() 164 | return # New version already detected, stop periodic checks 165 | except: 166 | log.warning("Check failed.") 167 | 168 | reactor.callLater(3600*24, test_update) 169 | 170 | @defer.inlineCallbacks 171 | def main(args): 172 | if args.pid_file: 173 | fp = file(args.pid_file, 'w') 174 | fp.write(str(os.getpid())) 175 | fp.close() 176 | 177 | if args.port != 3333: 178 | '''User most likely provided host/port 179 | for getwork interface. Let's try to detect 180 | Stratum host/port of given getwork pool.''' 181 | 182 | try: 183 | new_host = (yield utils.detect_stratum(args.host, args.port)) 184 | except: 185 | log.exception("Stratum host/port autodetection failed") 186 | new_host = None 187 | 188 | if new_host != None: 189 | args.host = new_host[0] 190 | args.port = new_host[1] 191 | 192 | log.warning("Stratum proxy version: %s" % version.VERSION) 193 | # Setup periodic checks for a new version 194 | #test_update() 195 | 196 | if args.tor: 197 | log.warning("Configuring Tor connection") 198 | args.proxy = '127.0.0.1:9050' 199 | args.host = 'pool57wkuu5yuhzb.onion' 200 | args.port = 3333 201 | 202 | if args.proxy: 203 | proxy = args.proxy.split(':') 204 | if len(proxy) < 2: 205 | proxy = (proxy, 9050) 206 | else: 207 | proxy = (proxy[0], int(proxy[1])) 208 | log.warning("Using proxy %s:%d" % proxy) 209 | else: 210 | proxy = None 211 | 212 | log.warning("Trying to connect to Stratum pool at %s:%d" % (args.host, args.port)) 213 | 214 | # Connect to Stratum pool 215 | f = SocketTransportClientFactory(args.host, args.port, 216 | debug=args.verbose, proxy=proxy, 217 | event_handler=client_service.ClientMiningService) 218 | 219 | 220 | job_registry = jobs.JobRegistry(f, cmd=args.blocknotify_cmd, scrypt_target=args.scrypt_target, 221 | no_midstate=args.no_midstate, real_target=args.real_target, use_old_target=args.old_target) 222 | client_service.ClientMiningService.job_registry = job_registry 223 | client_service.ClientMiningService.reset_timeout() 224 | 225 | workers = worker_registry.WorkerRegistry(f) 226 | f.on_connect.addCallback(on_connect, workers, job_registry) 227 | f.on_disconnect.addCallback(on_disconnect, workers, job_registry) 228 | 229 | if args.test: 230 | f.on_connect.addCallback(test_launcher, job_registry) 231 | 232 | # Cleanup properly on shutdown 233 | reactor.addSystemEventTrigger('before', 'shutdown', on_shutdown, f) 234 | 235 | # Block until proxy connect to the pool 236 | yield f.on_connect 237 | 238 | # Setup getwork listener 239 | if args.getwork_port > 0: 240 | conn = reactor.listenTCP(args.getwork_port, Site(getwork_listener.Root(job_registry, workers, 241 | stratum_host=args.stratum_host, stratum_port=args.stratum_port, 242 | custom_lp=args.custom_lp, custom_stratum=args.custom_stratum, 243 | custom_user=args.custom_user, custom_password=args.custom_password)), 244 | interface=args.getwork_host) 245 | 246 | try: 247 | conn.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # Enable keepalive packets 248 | conn.socket.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 60) # Seconds before sending keepalive probes 249 | conn.socket.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 1) # Interval in seconds between keepalive probes 250 | conn.socket.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 5) # Failed keepalive probles before declaring other end dead 251 | except: 252 | pass # Some socket features are not available on all platforms (you can guess which one) 253 | 254 | # Setup stratum listener 255 | #if args.stratum_port > 0: 256 | # stratum_listener.StratumProxyService._set_upstream_factory(f) 257 | # stratum_listener.StratumProxyService._set_custom_user(args.custom_user, args.custom_password) 258 | # reactor.listenTCP(args.stratum_port, SocketTransportFactory(debug=False, event_handler=ServiceEventHandler), interface=args.stratum_host) 259 | 260 | # Setup multicast responder 261 | #reactor.listenMulticast(3333, multicast_responder.MulticastResponder((args.host, args.port), args.stratum_port, args.getwork_port), listenMultiple=True) 262 | 263 | log.warning("-----------------------------------------------------------------------") 264 | if args.getwork_host == '0.0.0.0': 265 | log.warning("PROXY IS LISTENING ON ALL IPs ON PORT %d (getwork)" % args.getwork_port) 266 | else: 267 | log.warning("LISTENING FOR MINERS ON http://%s:%d (getwork) " % \ 268 | (args.getwork_host, args.getwork_port)) 269 | log.warning("-----------------------------------------------------------------------") 270 | 271 | if __name__ == '__main__': 272 | main(args) 273 | reactor.run() 274 | -------------------------------------------------------------------------------- /stratum/protocol.py: -------------------------------------------------------------------------------- 1 | import json 2 | #import jsonical 3 | import time 4 | import socket 5 | 6 | from twisted.protocols.basic import LineOnlyReceiver 7 | from twisted.internet import defer, reactor, error 8 | from twisted.python.failure import Failure 9 | 10 | #import services 11 | import stats 12 | import signature 13 | import custom_exceptions 14 | import connection_registry 15 | import settings 16 | 17 | import logger 18 | log = logger.get_logger('protocol') 19 | 20 | class RequestCounter(object): 21 | def __init__(self): 22 | self.on_finish = defer.Deferred() 23 | self.counter = 0 24 | 25 | def set_count(self, cnt): 26 | self.counter = cnt 27 | 28 | def decrease(self): 29 | self.counter -= 1 30 | if self.counter <= 0: 31 | self.finish() 32 | 33 | def finish(self): 34 | if not self.on_finish.called: 35 | self.on_finish.callback(True) 36 | 37 | class Protocol(LineOnlyReceiver): 38 | delimiter = '\n' 39 | 40 | def _get_id(self): 41 | self.request_id += 1 42 | return self.request_id 43 | 44 | def _get_ip(self): 45 | return self.proxied_ip or self.transport.getPeer().host 46 | 47 | def get_ident(self): 48 | # Get global unique ID of connection 49 | return "%s:%s" % (self.proxied_ip or self.transport.getPeer().host, "%x" % id(self)) 50 | 51 | def get_session(self): 52 | return self.session 53 | 54 | def connectionMade(self): 55 | try: 56 | self.transport.setTcpNoDelay(True) 57 | self.transport.setTcpKeepAlive(True) 58 | self.transport.socket.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 120) # Seconds before sending keepalive probes 59 | self.transport.socket.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 1) # Interval in seconds between keepalive probes 60 | self.transport.socket.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 5) # Failed keepalive probles before declaring other end dead 61 | except: 62 | # Supported only by the socket transport, 63 | # but there's really no better place in code to trigger this. 64 | pass 65 | 66 | # Read settings.TCP_PROXY_PROTOCOL documentation 67 | self.expect_tcp_proxy_protocol_header = self.factory.__dict__.get('tcp_proxy_protocol_enable', False) 68 | self.proxied_ip = None # IP obtained from TCP proxy protocol 69 | 70 | self.request_id = 0 71 | self.lookup_table = {} 72 | self.event_handler = self.factory.event_handler() 73 | self.on_disconnect = defer.Deferred() 74 | self.on_finish = None # Will point to defer which is called 75 | # once all client requests are processed 76 | 77 | # Initiate connection session 78 | self.session = {} 79 | 80 | stats.PeerStats.client_connected(self._get_ip()) 81 | log.debug("Connected %s" % self.transport.getPeer().host) 82 | connection_registry.ConnectionRegistry.add_connection(self) 83 | 84 | def transport_write(self, data): 85 | '''Overwrite this if transport needs some extra care about data written 86 | to the socket, like adding message format in websocket.''' 87 | try: 88 | self.transport.write(data) 89 | except AttributeError: 90 | # Transport is disconnected 91 | pass 92 | 93 | def connectionLost(self, reason): 94 | if self.on_disconnect != None and not self.on_disconnect.called: 95 | self.on_disconnect.callback(self) 96 | self.on_disconnect = None 97 | 98 | stats.PeerStats.client_disconnected(self._get_ip()) 99 | connection_registry.ConnectionRegistry.remove_connection(self) 100 | self.transport = None # Fixes memory leak (cyclic reference) 101 | 102 | def writeJsonRequest(self, method, params, is_notification=False): 103 | request_id = None if is_notification else self._get_id() 104 | serialized = json.dumps({'id': request_id, 'method': method, 'params': params}) 105 | 106 | if self.factory.debug: 107 | log.debug("< %s" % serialized) 108 | 109 | self.transport_write("%s\n" % serialized) 110 | return request_id 111 | 112 | def writeJsonResponse(self, data, message_id, use_signature=False, sign_method='', sign_params=[]): 113 | if use_signature: 114 | serialized = signature.jsonrpc_dumps_sign(self.factory.signing_key, self.factory.signing_id, False,\ 115 | message_id, sign_method, sign_params, data, None) 116 | else: 117 | serialized = json.dumps({'id': message_id, 'result': data, 'error': None}) 118 | 119 | if self.factory.debug: 120 | log.debug("< %s" % serialized) 121 | 122 | self.transport_write("%s\n" % serialized) 123 | 124 | def writeJsonError(self, code, message, traceback, message_id, use_signature=False, sign_method='', sign_params=[]): 125 | if use_signature: 126 | serialized = signature.jsonrpc_dumps_sign(self.factory.signing_key, self.factory.signing_id, False,\ 127 | message_id, sign_method, sign_params, None, (code, message, traceback)) 128 | else: 129 | serialized = json.dumps({'id': message_id, 'result': None, 'error': (code, message, traceback)}) 130 | 131 | self.transport_write("%s\n" % serialized) 132 | 133 | def writeGeneralError(self, message, code=-1): 134 | log.error(message) 135 | return self.writeJsonError(code, message, None, None) 136 | 137 | def process_response(self, data, message_id, sign_method, sign_params, request_counter): 138 | self.writeJsonResponse(data.result, message_id, data.sign, sign_method, sign_params) 139 | request_counter.decrease() 140 | 141 | 142 | def process_failure(self, failure, message_id, sign_method, sign_params, request_counter): 143 | if not isinstance(failure.value, custom_exceptions.ServiceException): 144 | # All handled exceptions should inherit from ServiceException class. 145 | # Throwing other exception class means that it is unhandled error 146 | # and we should log it. 147 | log.exception(failure) 148 | 149 | sign = False 150 | code = getattr(failure.value, 'code', -1) 151 | 152 | #if isinstance(failure.value, services.ResultObject): 153 | # # Strip ResultObject 154 | # sign = failure.value.sign 155 | # failure.value = failure.value.result 156 | 157 | if message_id != None: 158 | # Other party doesn't care of error state for notifications 159 | if settings.DEBUG: 160 | tb = failure.getBriefTraceback() 161 | else: 162 | tb = None 163 | self.writeJsonError(code, failure.getErrorMessage(), tb, message_id, sign, sign_method, sign_params) 164 | 165 | request_counter.decrease() 166 | 167 | def dataReceived(self, data, request_counter=None): 168 | '''Original code from Twisted, hacked for request_counter proxying. 169 | request_counter is hack for HTTP transport, didn't found cleaner solution how 170 | to indicate end of request processing in asynchronous manner. 171 | 172 | TODO: This would deserve some unit test to be sure that future twisted versions 173 | will work nicely with this.''' 174 | 175 | if request_counter == None: 176 | request_counter = RequestCounter() 177 | 178 | lines = (self._buffer+data).split(self.delimiter) 179 | self._buffer = lines.pop(-1) 180 | request_counter.set_count(len(lines)) 181 | self.on_finish = request_counter.on_finish 182 | 183 | for line in lines: 184 | if self.transport.disconnecting: 185 | request_counter.finish() 186 | return 187 | if len(line) > self.MAX_LENGTH: 188 | request_counter.finish() 189 | return self.lineLengthExceeded(line) 190 | else: 191 | try: 192 | self.lineReceived(line, request_counter) 193 | except Exception as exc: 194 | request_counter.finish() 195 | #log.exception("Processing of message failed") 196 | log.warning("Failed message: %s from %s" % (str(exc), self._get_ip())) 197 | return error.ConnectionLost('Processing of message failed') 198 | 199 | if len(self._buffer) > self.MAX_LENGTH: 200 | request_counter.finish() 201 | return self.lineLengthExceeded(self._buffer) 202 | 203 | def lineReceived(self, line, request_counter): 204 | if self.expect_tcp_proxy_protocol_header: 205 | # This flag may be set only for TCP transport AND when TCP_PROXY_PROTOCOL 206 | # is enabled in server config. Then we expect the first line of the stream 207 | # may contain proxy metadata. 208 | 209 | # We don't expect this header during this session anymore 210 | self.expect_tcp_proxy_protocol_header = False 211 | 212 | if line.startswith('PROXY'): 213 | self.proxied_ip = line.split()[2] 214 | 215 | # Let's process next line 216 | request_counter.decrease() 217 | return 218 | 219 | try: 220 | message = json.loads(line) 221 | except: 222 | #self.writeGeneralError("Cannot decode message '%s'" % line) 223 | request_counter.finish() 224 | raise custom_exceptions.ProtocolException("Cannot decode message '%s'" % line.strip()) 225 | 226 | if self.factory.debug: 227 | log.debug("> %s" % message) 228 | 229 | msg_id = message.get('id', 0) 230 | msg_method = message.get('method') 231 | msg_params = message.get('params') 232 | msg_result = message.get('result') 233 | msg_error = message.get('error') 234 | 235 | if msg_method: 236 | # It's a RPC call or notification 237 | try: 238 | result = self.event_handler._handle_event(msg_method, msg_params, connection_ref=self) 239 | if result == None and msg_id != None: 240 | # event handler must return Deferred object or raise an exception for RPC request 241 | raise custom_exceptions.MethodNotFoundException("Event handler cannot process method '%s'" % msg_method) 242 | except: 243 | failure = Failure() 244 | self.process_failure(failure, msg_id, msg_method, msg_params, request_counter) 245 | 246 | else: 247 | if msg_id == None: 248 | # It's notification, don't expect the response 249 | request_counter.decrease() 250 | else: 251 | # It's a RPC call 252 | result.addCallback(self.process_response, msg_id, msg_method, msg_params, request_counter) 253 | result.addErrback(self.process_failure, msg_id, msg_method, msg_params, request_counter) 254 | 255 | elif msg_id: 256 | # It's a RPC response 257 | # Perform lookup to the table of waiting requests. 258 | request_counter.decrease() 259 | 260 | try: 261 | meta = self.lookup_table[msg_id] 262 | del self.lookup_table[msg_id] 263 | except KeyError: 264 | # When deferred object for given message ID isn't found, it's an error 265 | raise custom_exceptions.ProtocolException("Lookup for deferred object for message ID '%s' failed." % msg_id) 266 | 267 | # If there's an error, handle it as errback 268 | # If both result and error are null, handle it as a success with blank result 269 | if msg_error != None: 270 | meta['defer'].errback(custom_exceptions.RemoteServiceException(msg_error[0], msg_error[1], msg_error[2])) 271 | else: 272 | meta['defer'].callback(msg_result) 273 | 274 | else: 275 | request_counter.decrease() 276 | raise custom_exceptions.ProtocolException("Cannot handle message '%s'" % line) 277 | 278 | def rpc(self, method, params, is_notification=False): 279 | ''' 280 | This method performs remote RPC call. 281 | 282 | If method should expect an response, it store 283 | request ID to lookup table and wait for corresponding 284 | response message. 285 | ''' 286 | 287 | request_id = self.writeJsonRequest(method, params, is_notification) 288 | 289 | if is_notification: 290 | return 291 | 292 | d = defer.Deferred() 293 | self.lookup_table[request_id] = {'defer': d, 'method': method, 'params': params} 294 | return d 295 | 296 | class ClientProtocol(Protocol): 297 | def connectionMade(self): 298 | Protocol.connectionMade(self) 299 | self.factory.client = self 300 | 301 | if self.factory.timeout_handler: 302 | self.factory.timeout_handler.cancel() 303 | self.factory.timeout_handler = None 304 | 305 | if isinstance(getattr(self.factory, 'after_connect', None), list): 306 | log.debug("Resuming connection: %s" % self.factory.after_connect) 307 | for cmd in self.factory.after_connect: 308 | self.rpc(cmd[0], cmd[1]) 309 | 310 | if not self.factory.on_connect.called: 311 | d = self.factory.on_connect 312 | self.factory.on_connect = defer.Deferred() 313 | d.callback(self.factory) 314 | 315 | 316 | #d = self.rpc('node.get_peers', []) 317 | #d.addCallback(self.factory.add_peers) 318 | 319 | def connectionLost(self, reason): 320 | self.factory.client = None 321 | 322 | if self.factory.timeout_handler: 323 | self.factory.timeout_handler.cancel() 324 | self.factory.timeout_handler = None 325 | 326 | if not self.factory.on_disconnect.called: 327 | d = self.factory.on_disconnect 328 | self.factory.on_disconnect = defer.Deferred() 329 | d.callback(self.factory) 330 | 331 | Protocol.connectionLost(self, reason) 332 | -------------------------------------------------------------------------------- /distribute_setup.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """Bootstrap distribute installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from distribute_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import os 17 | import sys 18 | import time 19 | import fnmatch 20 | import tempfile 21 | import tarfile 22 | from distutils import log 23 | 24 | try: 25 | from site import USER_SITE 26 | except ImportError: 27 | USER_SITE = None 28 | 29 | try: 30 | import subprocess 31 | 32 | def _python_cmd(*args): 33 | args = (sys.executable,) + args 34 | return subprocess.call(args) == 0 35 | 36 | except ImportError: 37 | # will be used for python 2.3 38 | def _python_cmd(*args): 39 | args = (sys.executable,) + args 40 | # quoting arguments if windows 41 | if sys.platform == 'win32': 42 | def quote(arg): 43 | if ' ' in arg: 44 | return '"%s"' % arg 45 | return arg 46 | args = [quote(arg) for arg in args] 47 | return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 48 | 49 | DEFAULT_VERSION = "0.6.28" 50 | DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" 51 | SETUPTOOLS_FAKED_VERSION = "0.6c11" 52 | 53 | SETUPTOOLS_PKG_INFO = """\ 54 | Metadata-Version: 1.0 55 | Name: setuptools 56 | Version: %s 57 | Summary: xxxx 58 | Home-page: xxx 59 | Author: xxx 60 | Author-email: xxx 61 | License: xxx 62 | Description: xxx 63 | """ % SETUPTOOLS_FAKED_VERSION 64 | 65 | 66 | def _install(tarball, install_args=()): 67 | # extracting the tarball 68 | tmpdir = tempfile.mkdtemp() 69 | log.warn('Extracting in %s', tmpdir) 70 | old_wd = os.getcwd() 71 | try: 72 | os.chdir(tmpdir) 73 | tar = tarfile.open(tarball) 74 | _extractall(tar) 75 | tar.close() 76 | 77 | # going in the directory 78 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 79 | os.chdir(subdir) 80 | log.warn('Now working in %s', subdir) 81 | 82 | # installing 83 | log.warn('Installing Distribute') 84 | if not _python_cmd('setup.py', 'install', *install_args): 85 | log.warn('Something went wrong during the installation.') 86 | log.warn('See the error message above.') 87 | finally: 88 | os.chdir(old_wd) 89 | 90 | 91 | def _build_egg(egg, tarball, to_dir): 92 | # extracting the tarball 93 | tmpdir = tempfile.mkdtemp() 94 | log.warn('Extracting in %s', tmpdir) 95 | old_wd = os.getcwd() 96 | try: 97 | os.chdir(tmpdir) 98 | tar = tarfile.open(tarball) 99 | _extractall(tar) 100 | tar.close() 101 | 102 | # going in the directory 103 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 104 | os.chdir(subdir) 105 | log.warn('Now working in %s', subdir) 106 | 107 | # building an egg 108 | log.warn('Building a Distribute egg in %s', to_dir) 109 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) 110 | 111 | finally: 112 | os.chdir(old_wd) 113 | # returning the result 114 | log.warn(egg) 115 | if not os.path.exists(egg): 116 | raise IOError('Could not build the egg.') 117 | 118 | 119 | def _do_download(version, download_base, to_dir, download_delay): 120 | egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' 121 | % (version, sys.version_info[0], sys.version_info[1])) 122 | if not os.path.exists(egg): 123 | tarball = download_setuptools(version, download_base, 124 | to_dir, download_delay) 125 | _build_egg(egg, tarball, to_dir) 126 | sys.path.insert(0, egg) 127 | import setuptools 128 | setuptools.bootstrap_install_from = egg 129 | 130 | 131 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 132 | to_dir=os.curdir, download_delay=15, no_fake=True): 133 | # making sure we use the absolute path 134 | to_dir = os.path.abspath(to_dir) 135 | was_imported = 'pkg_resources' in sys.modules or \ 136 | 'setuptools' in sys.modules 137 | try: 138 | try: 139 | import pkg_resources 140 | if not hasattr(pkg_resources, '_distribute'): 141 | if not no_fake: 142 | _fake_setuptools() 143 | raise ImportError 144 | except ImportError: 145 | return _do_download(version, download_base, to_dir, download_delay) 146 | try: 147 | pkg_resources.require("distribute>=" + version) 148 | return 149 | except pkg_resources.VersionConflict: 150 | e = sys.exc_info()[1] 151 | if was_imported: 152 | sys.stderr.write( 153 | "The required version of distribute (>=%s) is not available,\n" 154 | "and can't be installed while this script is running. Please\n" 155 | "install a more recent version first, using\n" 156 | "'easy_install -U distribute'." 157 | "\n\n(Currently using %r)\n" % (version, e.args[0])) 158 | sys.exit(2) 159 | else: 160 | del pkg_resources, sys.modules['pkg_resources'] # reload ok 161 | return _do_download(version, download_base, to_dir, 162 | download_delay) 163 | except pkg_resources.DistributionNotFound: 164 | return _do_download(version, download_base, to_dir, 165 | download_delay) 166 | finally: 167 | if not no_fake: 168 | _create_fake_setuptools_pkg_info(to_dir) 169 | 170 | 171 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 172 | to_dir=os.curdir, delay=15): 173 | """Download distribute from a specified location and return its filename 174 | 175 | `version` should be a valid distribute version number that is available 176 | as an egg for download under the `download_base` URL (which should end 177 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 178 | `delay` is the number of seconds to pause before an actual download 179 | attempt. 180 | """ 181 | # making sure we use the absolute path 182 | to_dir = os.path.abspath(to_dir) 183 | try: 184 | from urllib.request import urlopen 185 | except ImportError: 186 | from urllib2 import urlopen 187 | tgz_name = "distribute-%s.tar.gz" % version 188 | url = download_base + tgz_name 189 | saveto = os.path.join(to_dir, tgz_name) 190 | src = dst = None 191 | if not os.path.exists(saveto): # Avoid repeated downloads 192 | try: 193 | log.warn("Downloading %s", url) 194 | src = urlopen(url) 195 | # Read/write all in one block, so we don't create a corrupt file 196 | # if the download is interrupted. 197 | data = src.read() 198 | dst = open(saveto, "wb") 199 | dst.write(data) 200 | finally: 201 | if src: 202 | src.close() 203 | if dst: 204 | dst.close() 205 | return os.path.realpath(saveto) 206 | 207 | 208 | def _no_sandbox(function): 209 | def __no_sandbox(*args, **kw): 210 | try: 211 | from setuptools.sandbox import DirectorySandbox 212 | if not hasattr(DirectorySandbox, '_old'): 213 | def violation(*args): 214 | pass 215 | DirectorySandbox._old = DirectorySandbox._violation 216 | DirectorySandbox._violation = violation 217 | patched = True 218 | else: 219 | patched = False 220 | except ImportError: 221 | patched = False 222 | 223 | try: 224 | return function(*args, **kw) 225 | finally: 226 | if patched: 227 | DirectorySandbox._violation = DirectorySandbox._old 228 | del DirectorySandbox._old 229 | 230 | return __no_sandbox 231 | 232 | 233 | def _patch_file(path, content): 234 | """Will backup the file then patch it""" 235 | existing_content = open(path).read() 236 | if existing_content == content: 237 | # already patched 238 | log.warn('Already patched.') 239 | return False 240 | log.warn('Patching...') 241 | _rename_path(path) 242 | f = open(path, 'w') 243 | try: 244 | f.write(content) 245 | finally: 246 | f.close() 247 | return True 248 | 249 | _patch_file = _no_sandbox(_patch_file) 250 | 251 | 252 | def _same_content(path, content): 253 | return open(path).read() == content 254 | 255 | 256 | def _rename_path(path): 257 | new_name = path + '.OLD.%s' % time.time() 258 | log.warn('Renaming %s into %s', path, new_name) 259 | os.rename(path, new_name) 260 | return new_name 261 | 262 | 263 | def _remove_flat_installation(placeholder): 264 | if not os.path.isdir(placeholder): 265 | log.warn('Unkown installation at %s', placeholder) 266 | return False 267 | found = False 268 | for file in os.listdir(placeholder): 269 | if fnmatch.fnmatch(file, 'setuptools*.egg-info'): 270 | found = True 271 | break 272 | if not found: 273 | log.warn('Could not locate setuptools*.egg-info') 274 | return 275 | 276 | log.warn('Removing elements out of the way...') 277 | pkg_info = os.path.join(placeholder, file) 278 | if os.path.isdir(pkg_info): 279 | patched = _patch_egg_dir(pkg_info) 280 | else: 281 | patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) 282 | 283 | if not patched: 284 | log.warn('%s already patched.', pkg_info) 285 | return False 286 | # now let's move the files out of the way 287 | for element in ('setuptools', 'pkg_resources.py', 'site.py'): 288 | element = os.path.join(placeholder, element) 289 | if os.path.exists(element): 290 | _rename_path(element) 291 | else: 292 | log.warn('Could not find the %s element of the ' 293 | 'Setuptools distribution', element) 294 | return True 295 | 296 | _remove_flat_installation = _no_sandbox(_remove_flat_installation) 297 | 298 | 299 | def _after_install(dist): 300 | log.warn('After install bootstrap.') 301 | placeholder = dist.get_command_obj('install').install_purelib 302 | _create_fake_setuptools_pkg_info(placeholder) 303 | 304 | 305 | def _create_fake_setuptools_pkg_info(placeholder): 306 | if not placeholder or not os.path.exists(placeholder): 307 | log.warn('Could not find the install location') 308 | return 309 | pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) 310 | setuptools_file = 'setuptools-%s-py%s.egg-info' % \ 311 | (SETUPTOOLS_FAKED_VERSION, pyver) 312 | pkg_info = os.path.join(placeholder, setuptools_file) 313 | if os.path.exists(pkg_info): 314 | log.warn('%s already exists', pkg_info) 315 | return 316 | 317 | if not os.access(pkg_info, os.W_OK): 318 | log.warn("Don't have permissions to write %s, skipping", pkg_info) 319 | 320 | log.warn('Creating %s', pkg_info) 321 | f = open(pkg_info, 'w') 322 | try: 323 | f.write(SETUPTOOLS_PKG_INFO) 324 | finally: 325 | f.close() 326 | 327 | pth_file = os.path.join(placeholder, 'setuptools.pth') 328 | log.warn('Creating %s', pth_file) 329 | f = open(pth_file, 'w') 330 | try: 331 | f.write(os.path.join(os.curdir, setuptools_file)) 332 | finally: 333 | f.close() 334 | 335 | _create_fake_setuptools_pkg_info = _no_sandbox( 336 | _create_fake_setuptools_pkg_info 337 | ) 338 | 339 | 340 | def _patch_egg_dir(path): 341 | # let's check if it's already patched 342 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 343 | if os.path.exists(pkg_info): 344 | if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): 345 | log.warn('%s already patched.', pkg_info) 346 | return False 347 | _rename_path(path) 348 | os.mkdir(path) 349 | os.mkdir(os.path.join(path, 'EGG-INFO')) 350 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 351 | f = open(pkg_info, 'w') 352 | try: 353 | f.write(SETUPTOOLS_PKG_INFO) 354 | finally: 355 | f.close() 356 | return True 357 | 358 | _patch_egg_dir = _no_sandbox(_patch_egg_dir) 359 | 360 | 361 | def _before_install(): 362 | log.warn('Before install bootstrap.') 363 | _fake_setuptools() 364 | 365 | 366 | def _under_prefix(location): 367 | if 'install' not in sys.argv: 368 | return True 369 | args = sys.argv[sys.argv.index('install') + 1:] 370 | for index, arg in enumerate(args): 371 | for option in ('--root', '--prefix'): 372 | if arg.startswith('%s=' % option): 373 | top_dir = arg.split('root=')[-1] 374 | return location.startswith(top_dir) 375 | elif arg == option: 376 | if len(args) > index: 377 | top_dir = args[index + 1] 378 | return location.startswith(top_dir) 379 | if arg == '--user' and USER_SITE is not None: 380 | return location.startswith(USER_SITE) 381 | return True 382 | 383 | 384 | def _fake_setuptools(): 385 | log.warn('Scanning installed packages') 386 | try: 387 | import pkg_resources 388 | except ImportError: 389 | # we're cool 390 | log.warn('Setuptools or Distribute does not seem to be installed.') 391 | return 392 | ws = pkg_resources.working_set 393 | try: 394 | setuptools_dist = ws.find( 395 | pkg_resources.Requirement.parse('setuptools', replacement=False) 396 | ) 397 | except TypeError: 398 | # old distribute API 399 | setuptools_dist = ws.find( 400 | pkg_resources.Requirement.parse('setuptools') 401 | ) 402 | 403 | if setuptools_dist is None: 404 | log.warn('No setuptools distribution found') 405 | return 406 | # detecting if it was already faked 407 | setuptools_location = setuptools_dist.location 408 | log.warn('Setuptools installation detected at %s', setuptools_location) 409 | 410 | # if --root or --preix was provided, and if 411 | # setuptools is not located in them, we don't patch it 412 | if not _under_prefix(setuptools_location): 413 | log.warn('Not patching, --root or --prefix is installing Distribute' 414 | ' in another location') 415 | return 416 | 417 | # let's see if its an egg 418 | if not setuptools_location.endswith('.egg'): 419 | log.warn('Non-egg installation') 420 | res = _remove_flat_installation(setuptools_location) 421 | if not res: 422 | return 423 | else: 424 | log.warn('Egg installation') 425 | pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') 426 | if (os.path.exists(pkg_info) and 427 | _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): 428 | log.warn('Already patched.') 429 | return 430 | log.warn('Patching...') 431 | # let's create a fake egg replacing setuptools one 432 | res = _patch_egg_dir(setuptools_location) 433 | if not res: 434 | return 435 | log.warn('Patched done.') 436 | _relaunch() 437 | 438 | 439 | def _relaunch(): 440 | log.warn('Relaunching...') 441 | # we have to relaunch the process 442 | # pip marker to avoid a relaunch bug 443 | _cmd = ['-c', 'install', '--single-version-externally-managed'] 444 | if sys.argv[:3] == _cmd: 445 | sys.argv[0] = 'setup.py' 446 | args = [sys.executable] + sys.argv 447 | sys.exit(subprocess.call(args)) 448 | 449 | 450 | def _extractall(self, path=".", members=None): 451 | """Extract all members from the archive to the current working 452 | directory and set owner, modification time and permissions on 453 | directories afterwards. `path' specifies a different directory 454 | to extract to. `members' is optional and must be a subset of the 455 | list returned by getmembers(). 456 | """ 457 | import copy 458 | import operator 459 | from tarfile import ExtractError 460 | directories = [] 461 | 462 | if members is None: 463 | members = self 464 | 465 | for tarinfo in members: 466 | if tarinfo.isdir(): 467 | # Extract directories with a safe mode. 468 | directories.append(tarinfo) 469 | tarinfo = copy.copy(tarinfo) 470 | tarinfo.mode = 448 # decimal for oct 0700 471 | self.extract(tarinfo, path) 472 | 473 | # Reverse sort directories. 474 | if sys.version_info < (2, 4): 475 | def sorter(dir1, dir2): 476 | return cmp(dir1.name, dir2.name) 477 | directories.sort(sorter) 478 | directories.reverse() 479 | else: 480 | directories.sort(key=operator.attrgetter('name'), reverse=True) 481 | 482 | # Set correct owner, mtime and filemode on directories. 483 | for tarinfo in directories: 484 | dirpath = os.path.join(path, tarinfo.name) 485 | try: 486 | self.chown(tarinfo, dirpath) 487 | self.utime(tarinfo, dirpath) 488 | self.chmod(tarinfo, dirpath) 489 | except ExtractError: 490 | e = sys.exc_info()[1] 491 | if self.errorlevel > 1: 492 | raise 493 | else: 494 | self._dbg(1, "tarfile: %s" % e) 495 | 496 | 497 | def _build_install_args(argv): 498 | install_args = [] 499 | user_install = '--user' in argv 500 | if user_install and sys.version_info < (2, 6): 501 | log.warn("--user requires Python 2.6 or later") 502 | raise SystemExit(1) 503 | if user_install: 504 | install_args.append('--user') 505 | return install_args 506 | 507 | 508 | def main(argv, version=DEFAULT_VERSION): 509 | """Install or upgrade setuptools and EasyInstall""" 510 | tarball = download_setuptools() 511 | _install(tarball, _build_install_args(argv)) 512 | 513 | 514 | if __name__ == '__main__': 515 | main(sys.argv[1:]) 516 | --------------------------------------------------------------------------------