├── .gitignore ├── LICENSE ├── README.md ├── conf ├── __init__.py └── config_sample.py ├── launcher_demo.tac ├── lib ├── __init__.py ├── bitcoin_rpc.py ├── block_template.py ├── block_updater.py ├── coinbaser.py ├── coinbasetx.py ├── exceptions.py ├── extranonce_counter.py ├── halfnode.py ├── merkletree.py ├── template_registry.py └── util.py ├── mining ├── __init__.py ├── interfaces.py ├── service.py └── subscription.py └── scripts └── blocknotify.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Stratum mining - Bitcoin pool using Stratum protocol 2 | Copyright (C) 2012 Marek Palatinus 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as 6 | published by the Free Software Foundation, either version 3 of the 7 | License, or any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with this program. If not, see . 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | stratum-mining 2 | ============== 3 | 4 | Demo implementation of bitcoin mining pool using Stratum mining protocol. 5 | 6 | For Stratum mining protocol specification, please visit http://mining.bitcoin.cz/stratum-mining. 7 | 8 | Contact 9 | ------- 10 | 11 | This pool implementation is provided by http://mining.bitcoin.cz. You can contact 12 | me by email info(at)bitcoin.cz or on IRC #stratum on freenode. 13 | -------------------------------------------------------------------------------- /conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slush0/stratum-mining/b2a24d7424784cada95010232cdb79cfed481da6/conf/__init__.py -------------------------------------------------------------------------------- /conf/config_sample.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This is example configuration for Stratum server. 3 | Please rename it to settings.py and fill correct values. 4 | ''' 5 | 6 | # ******************** GENERAL SETTINGS *************** 7 | 8 | # Enable some verbose debug (logging requests and responses). 9 | DEBUG = False 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 = 'INFO' 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 = 10 24 | 25 | ENABLE_EXAMPLE_SERVICE = True 26 | 27 | # ******************** TRANSPORTS ********************* 28 | 29 | # Hostname or external IP to expose 30 | HOSTNAME = 'localhost' 31 | 32 | # Port used for Socket transport. Use 'None' for disabling the transport. 33 | LISTEN_SOCKET_TRANSPORT = 3333 34 | 35 | # Port used for HTTP Poll transport. Use 'None' for disabling the transport 36 | LISTEN_HTTP_TRANSPORT = None 37 | 38 | # Port used for HTTPS Poll transport 39 | LISTEN_HTTPS_TRANSPORT = None 40 | 41 | # Port used for WebSocket transport, 'None' for disabling WS 42 | LISTEN_WS_TRANSPORT = None 43 | 44 | # Port used for secure WebSocket, 'None' for disabling WSS 45 | LISTEN_WSS_TRANSPORT = None 46 | 47 | # Hostname and credentials for one trusted Bitcoin node ("Satoshi's client"). 48 | # Stratum uses both P2P port (which is 8333 already) and RPC port 49 | BITCOIN_TRUSTED_HOST = 'localhost' 50 | BITCOIN_TRUSTED_PORT = 8332 51 | BITCOIN_TRUSTED_USER = 'user' 52 | BITCOIN_TRUSTED_PASSWORD = 'somepassword' 53 | 54 | # Use "echo -n '' | sha256sum | cut -f1 -d' ' " 55 | # for calculating SHA256 of your preferred password 56 | ADMIN_PASSWORD_SHA256 = None 57 | #ADMIN_PASSWORD_SHA256 = '9e6c0c1db1e0dfb3fa5159deb4ecd9715b3c8cd6b06bd4a3ad77e9a8c5694219' # SHA256 of the password 58 | 59 | IRC_NICK = None 60 | 61 | ''' 62 | DATABASE_DRIVER = 'MySQLdb' 63 | DATABASE_HOST = 'localhost' 64 | DATABASE_DBNAME = 'pooldb' 65 | DATABASE_USER = 'pooldb' 66 | DATABASE_PASSWORD = '**empty**' 67 | ''' 68 | 69 | # Pool related settings 70 | INSTANCE_ID = 31 71 | CENTRAL_WALLET = 'set_valid_addresss_in_config!' 72 | PREVHASH_REFRESH_INTERVAL = 5 # in sec 73 | MERKLE_REFRESH_INTERVAL = 60 # How often check memorypool 74 | COINBASE_EXTRAS = '/stratum/' 75 | -------------------------------------------------------------------------------- /launcher_demo.tac: -------------------------------------------------------------------------------- 1 | # Run me with "twistd -ny launcher_demo.tac -l -" 2 | 3 | # Add conf directory to python path. 4 | # Configuration file is standard python module. 5 | import os, sys 6 | sys.path = [os.path.join(os.getcwd(), 'conf'),] + sys.path 7 | 8 | from twisted.internet import defer 9 | 10 | # Run listening when mining service is ready 11 | on_startup = defer.Deferred() 12 | 13 | # Bootstrap Stratum framework 14 | import stratum 15 | from stratum import settings 16 | application = stratum.setup(on_startup) 17 | 18 | # Load mining service into stratum framework 19 | import mining 20 | 21 | from mining.interfaces import Interfaces 22 | from mining.interfaces import WorkerManagerInterface, TimestamperInterface, \ 23 | ShareManagerInterface, ShareLimiterInterface 24 | 25 | Interfaces.set_share_manager(ShareManagerInterface()) 26 | Interfaces.set_share_limiter(ShareLimiterInterface()) 27 | Interfaces.set_worker_manager(WorkerManagerInterface()) 28 | Interfaces.set_timestamper(TimestamperInterface()) 29 | 30 | mining.setup(on_startup) 31 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slush0/stratum-mining/b2a24d7424784cada95010232cdb79cfed481da6/lib/__init__.py -------------------------------------------------------------------------------- /lib/bitcoin_rpc.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Implements simple interface to bitcoind's RPC. 3 | ''' 4 | 5 | import simplejson as json 6 | import base64 7 | from twisted.internet import defer 8 | from twisted.web import client 9 | 10 | import stratum.logger 11 | log = stratum.logger.get_logger('bitcoin_rpc') 12 | 13 | gbt_known_rules = ["segwit"] 14 | 15 | class BitcoinRPC(object): 16 | 17 | def __init__(self, host, port, username, password): 18 | self.bitcoin_url = 'http://%s:%d' % (host, port) 19 | self.credentials = base64.b64encode("%s:%s" % (username, password)) 20 | self.headers = { 21 | 'Content-Type': 'text/json', 22 | 'Authorization': 'Basic %s' % self.credentials, 23 | } 24 | 25 | def _call_raw(self, data): 26 | return client.getPage( 27 | url=self.bitcoin_url, 28 | method='POST', 29 | headers=self.headers, 30 | postdata=data, 31 | ) 32 | 33 | def _call(self, method, params): 34 | return self._call_raw(json.dumps({ 35 | 'jsonrpc': '2.0', 36 | 'method': method, 37 | 'params': params, 38 | 'id': '1', 39 | })) 40 | 41 | @defer.inlineCallbacks 42 | def submitblock(self, block_hex): 43 | resp = (yield self._call('submitblock', [block_hex,])) 44 | if json.loads(resp)['result'] == None: 45 | defer.returnValue(True) 46 | else: 47 | defer.returnValue(False) 48 | 49 | @defer.inlineCallbacks 50 | def getinfo(self): 51 | resp = (yield self._call('getinfo', [])) 52 | defer.returnValue(json.loads(resp)['result']) 53 | 54 | @defer.inlineCallbacks 55 | def getblocktemplate(self): 56 | resp = (yield self._call('getblocktemplate', [{"rules": gbt_known_rules}])) 57 | defer.returnValue(json.loads(resp)['result']) 58 | 59 | @defer.inlineCallbacks 60 | def prevhash(self): 61 | resp = (yield self._call('getbestblockhash', [])) 62 | try: 63 | defer.returnValue(json.loads(resp)['result']) 64 | except Exception as e: 65 | log.exception("Cannot decode prevhash %s" % str(e)) 66 | raise 67 | 68 | @defer.inlineCallbacks 69 | def validateaddress(self, address): 70 | resp = (yield self._call('validateaddress', [address,])) 71 | defer.returnValue(json.loads(resp)['result']) 72 | -------------------------------------------------------------------------------- /lib/block_template.py: -------------------------------------------------------------------------------- 1 | import StringIO 2 | import binascii 3 | import struct 4 | 5 | import util 6 | import merkletree 7 | import halfnode 8 | from coinbasetx import CoinbaseTransaction 9 | from Crypto.Hash import SHA256 10 | 11 | # Remove dependency to settings, coinbase extras should be 12 | # provided from coinbaser 13 | from stratum import settings 14 | 15 | witness_nonce = b'\0' * 0x20 16 | witness_magic = b'\xaa\x21\xa9\xed' 17 | 18 | class TxBlob(object): 19 | def __init__(self): 20 | self.data = '' 21 | def serialize(self): 22 | return self.data 23 | def deserialize(self, data): 24 | self.data = data 25 | 26 | class BlockTemplate(halfnode.CBlock): 27 | '''Template is used for generating new jobs for clients. 28 | Let's iterate extranonce1, extranonce2, ntime and nonce 29 | to find out valid bitcoin block!''' 30 | 31 | coinbase_transaction_class = CoinbaseTransaction 32 | 33 | def __init__(self, timestamper, coinbaser, job_id): 34 | super(BlockTemplate, self).__init__() 35 | 36 | self.job_id = job_id 37 | self.timestamper = timestamper 38 | self.coinbaser = coinbaser 39 | 40 | self.prevhash_bin = '' # reversed binary form of prevhash 41 | self.prevhash_hex = '' 42 | self.timedelta = 0 43 | self.curtime = 0 44 | self.target = 0 45 | self.witness = 0 46 | #self.coinbase_hex = None 47 | self.merkletree = None 48 | 49 | self.broadcast_args = [] 50 | 51 | # List of 4-tuples (extranonce1, extranonce2, ntime, nonce) 52 | # registers already submitted and checked shares 53 | # There may be registered also invalid shares inside! 54 | self.submits = [] 55 | 56 | def fill_from_rpc(self, data): 57 | '''Convert getblocktemplate result into BlockTemplate instance''' 58 | 59 | commitment = None 60 | txids = [] 61 | hashes = [None] + [ util.ser_uint256(int(t['hash'], 16)) for t in data['transactions'] ] 62 | try: 63 | txids = [None] + [ util.ser_uint256(int(t['txid'], 16)) for t in data['transactions'] ] 64 | mt = merkletree.MerkleTree(txids) 65 | except KeyError: 66 | mt = merkletree.MerkleTree(hashes) 67 | 68 | wmt = merkletree.MerkleTree(hashes).withFirst(binascii.unhexlify('0000000000000000000000000000000000000000000000000000000000000000')) 69 | self.witness = SHA256.new(SHA256.new(wmt + witness_nonce).digest()).digest() 70 | commitment = b'\x6a' + struct.pack(">b", len(self.witness) + len(witness_magic)) + witness_magic + self.witness 71 | try: 72 | default_witness = data['default_witness_commitment'] 73 | commitment_check = binascii.unhexlify(default_witness) 74 | if(commitment != commitment_check): 75 | print("calculated witness does not match supplied one! This block probably will not be accepted!") 76 | commitment = commitment_check 77 | except KeyError: 78 | pass 79 | self.witness = commitment[6:] 80 | 81 | coinbase = self.coinbase_transaction_class(self.timestamper, self.coinbaser, data['coinbasevalue'], 82 | data['coinbaseaux']['flags'], data['height'], commitment, settings.COINBASE_EXTRAS) 83 | 84 | self.height = data['height'] 85 | self.nVersion = data['version'] 86 | self.hashPrevBlock = int(data['previousblockhash'], 16) 87 | self.nBits = int(data['bits'], 16) 88 | self.hashMerkleRoot = 0 89 | self.nTime = 0 90 | self.nNonce = 0 91 | self.vtx = [ coinbase, ] 92 | 93 | for tx in data['transactions']: 94 | t = TxBlob() 95 | t.deserialize(binascii.unhexlify(tx['data'])) 96 | self.vtx.append(t) 97 | 98 | self.curtime = data['curtime'] 99 | self.timedelta = self.curtime - int(self.timestamper.time()) 100 | self.merkletree = mt 101 | self.target = util.uint256_from_compact(self.nBits) 102 | 103 | # Reversed prevhash 104 | self.prevhash_bin = binascii.unhexlify(util.reverse_hash(data['previousblockhash'])) 105 | self.prevhash_hex = "%064x" % self.hashPrevBlock 106 | 107 | self.broadcast_args = self.build_broadcast_args() 108 | 109 | def register_submit(self, extranonce1, extranonce2, ntime, nonce): 110 | '''Client submitted some solution. Let's register it to 111 | prevent double submissions.''' 112 | 113 | t = (extranonce1, extranonce2, ntime, nonce) 114 | if t not in self.submits: 115 | self.submits.append(t) 116 | return True 117 | return False 118 | 119 | def build_broadcast_args(self): 120 | '''Build parameters of mining.notify call. All clients 121 | may receive the same params, because they include 122 | their unique extranonce1 into the coinbase, so every 123 | coinbase_hash (and then merkle_root) will be unique as well.''' 124 | job_id = self.job_id 125 | prevhash = binascii.hexlify(self.prevhash_bin) 126 | (coinb1, coinb2) = [ binascii.hexlify(x) for x in self.vtx[0]._serialized ] 127 | merkle_branch = [ binascii.hexlify(x) for x in self.merkletree._steps ] 128 | version = binascii.hexlify(struct.pack(">i", self.nVersion)) 129 | nbits = binascii.hexlify(struct.pack(">I", self.nBits)) 130 | ntime = binascii.hexlify(struct.pack(">I", self.curtime)) 131 | clean_jobs = True 132 | 133 | return (job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, clean_jobs) 134 | 135 | def serialize_coinbase(self, extranonce1, extranonce2): 136 | '''Serialize coinbase with given extranonce1 and extranonce2 137 | in binary form''' 138 | (part1, part2) = self.vtx[0]._serialized 139 | return part1 + extranonce1 + extranonce2 + part2 140 | 141 | def check_ntime(self, ntime): 142 | '''Check for ntime restrictions.''' 143 | if ntime < self.curtime: 144 | return False 145 | 146 | if ntime > (self.timestamper.time() + 1000): 147 | # Be strict on ntime into the near future 148 | # may be unnecessary 149 | return False 150 | 151 | return True 152 | 153 | def serialize_header(self, merkle_root_int, ntime_bin, nonce_bin): 154 | '''Serialize header for calculating block hash''' 155 | r = struct.pack(">i", self.nVersion) 156 | r += self.prevhash_bin 157 | r += util.ser_uint256_be(merkle_root_int) 158 | r += ntime_bin 159 | r += struct.pack(">I", self.nBits) 160 | r += nonce_bin 161 | return r 162 | 163 | def finalize(self, merkle_root_int, extranonce1_bin, extranonce2_bin, ntime, nonce): 164 | '''Take all parameters required to compile block candidate. 165 | self.is_valid() should return True then...''' 166 | 167 | self.hashMerkleRoot = merkle_root_int 168 | self.nTime = ntime 169 | self.nNonce = nonce 170 | self.vtx[0].set_extranonce(extranonce1_bin + extranonce2_bin) 171 | self.sha256 = None # We changed block parameters, let's reset sha256 cache 172 | 173 | def serialize(self): 174 | r = [] 175 | r.append(struct.pack(" self.target: 188 | return False 189 | hashes = [] 190 | hashes.append(b'\0' * 0x20) 191 | for tx in self.vtx[1:]: 192 | hashes.append(SHA256.new(SHA256.new(tx.serialize()).digest()).digest()) 193 | while len(hashes) > 1: 194 | newhashes = [] 195 | for i in xrange(0, len(hashes), 2): 196 | i2 = min(i+1, len(hashes)-1) 197 | newhashes.append(SHA256.new(SHA256.new(hashes[i] + hashes[i2]).digest()).digest()) 198 | hashes = newhashes 199 | calcwitness = SHA256.new(SHA256.new(hashes[0] + witness_nonce).digest()).digest() 200 | if calcwitness != self.witness: 201 | return False 202 | return True 203 | -------------------------------------------------------------------------------- /lib/block_updater.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import reactor, defer 2 | from stratum import settings 3 | 4 | import util 5 | from mining.interfaces import Interfaces 6 | 7 | import stratum.logger 8 | log = stratum.logger.get_logger('block_updater') 9 | 10 | class BlockUpdater(object): 11 | ''' 12 | Polls upstream's getinfo() and detecting new block on the network. 13 | This will call registry.update_block when new prevhash appear. 14 | 15 | This is just failback alternative when something 16 | with ./bitcoind -blocknotify will go wrong. 17 | ''' 18 | 19 | def __init__(self, registry, bitcoin_rpc): 20 | self.bitcoin_rpc = bitcoin_rpc 21 | self.registry = registry 22 | self.clock = None 23 | self.schedule() 24 | 25 | def schedule(self): 26 | when = self._get_next_time() 27 | #log.debug("Next prevhash update in %.03f sec" % when) 28 | #log.debug("Merkle update in next %.03f sec" % \ 29 | # ((self.registry.last_update + settings.MERKLE_REFRESH_INTERVAL)-Interfaces.timestamper.time())) 30 | self.clock = reactor.callLater(when, self.run) 31 | 32 | def _get_next_time(self): 33 | when = settings.PREVHASH_REFRESH_INTERVAL - (Interfaces.timestamper.time() - self.registry.last_update) % \ 34 | settings.PREVHASH_REFRESH_INTERVAL 35 | return when 36 | 37 | @defer.inlineCallbacks 38 | def run(self): 39 | update = False 40 | 41 | try: 42 | if self.registry.last_block: 43 | current_prevhash = "%064x" % self.registry.last_block.hashPrevBlock 44 | else: 45 | current_prevhash = None 46 | 47 | prevhash = yield self.bitcoin_rpc.prevhash() 48 | if prevhash and prevhash != current_prevhash: 49 | log.info("New block! Prevhash: %s" % prevhash) 50 | update = True 51 | 52 | elif Interfaces.timestamper.time() - self.registry.last_update >= settings.MERKLE_REFRESH_INTERVAL: 53 | log.info("Merkle update! Prevhash: %s" % prevhash) 54 | update = True 55 | 56 | if update: 57 | self.registry.update_block() 58 | 59 | except Exception: 60 | log.exception("UpdateWatchdog.run failed") 61 | finally: 62 | self.schedule() 63 | 64 | 65 | -------------------------------------------------------------------------------- /lib/coinbaser.py: -------------------------------------------------------------------------------- 1 | import util 2 | from twisted.internet import defer 3 | 4 | import stratum.logger 5 | log = stratum.logger.get_logger('coinbaser') 6 | 7 | # TODO: Add on_* hooks in the app 8 | 9 | class SimpleCoinbaser(object): 10 | '''This very simple coinbaser uses constant bitcoin address 11 | for all generated blocks.''' 12 | 13 | def __init__(self, bitcoin_rpc, address): 14 | # Fire callback when coinbaser is ready 15 | self.on_load = defer.Deferred() 16 | 17 | self.address = address 18 | self.is_valid = False # We need to check if pool can use this address 19 | 20 | self.bitcoin_rpc = bitcoin_rpc 21 | self._validate() 22 | 23 | def _validate(self): 24 | d = self.bitcoin_rpc.validateaddress(self.address) 25 | d.addCallback(self._address_check) 26 | d.addErrback(self._failure) 27 | 28 | def _address_check(self, result): 29 | if result['isvalid'] and result['ismine']: 30 | self.is_valid = True 31 | log.info("Coinbase address '%s' is valid" % self.address) 32 | 33 | if not self.on_load.called: 34 | self.on_load.callback(True) 35 | 36 | else: 37 | self.is_valid = False 38 | log.error("Coinbase address '%s' is NOT valid!" % self.address) 39 | 40 | def _failure(self, failure): 41 | log.error("Cannot validate Bitcoin address '%s'" % self.address) 42 | raise 43 | 44 | #def on_new_block(self): 45 | # pass 46 | 47 | #def on_new_template(self): 48 | # pass 49 | 50 | def get_script_pubkey(self): 51 | if not self.is_valid: 52 | # Try again, maybe bitcoind was down? 53 | self._validate() 54 | raise Exception("Coinbase address is not validated!") 55 | return util.script_to_address(self.address) 56 | 57 | def get_coinbase_data(self): 58 | return '' 59 | -------------------------------------------------------------------------------- /lib/coinbasetx.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import halfnode 3 | import struct 4 | import util 5 | 6 | class CoinbaseTransaction(halfnode.CTransaction): 7 | '''Construct special transaction used for coinbase tx. 8 | It also implements quick serialization using pre-cached 9 | scriptSig template.''' 10 | 11 | extranonce_type = '>Q' 12 | extranonce_placeholder = struct.pack(extranonce_type, int('f000000ff111111f', 16)) 13 | extranonce_size = struct.calcsize(extranonce_type) 14 | 15 | def __init__(self, timestamper, coinbaser, value, flags, height, commitment, data): 16 | super(CoinbaseTransaction, self).__init__() 17 | 18 | #self.extranonce = 0 19 | 20 | if len(self.extranonce_placeholder) != self.extranonce_size: 21 | raise Exception("Extranonce placeholder don't match expected length!") 22 | 23 | tx_in = halfnode.CTxIn() 24 | tx_in.prevout.hash = 0L 25 | tx_in.prevout.n = 2**32-1 26 | tx_in._scriptSig_template = ( 27 | util.ser_number(height) + binascii.unhexlify(flags) + util.ser_number(int(timestamper.time())) + \ 28 | chr(self.extranonce_size), 29 | util.ser_string(coinbaser.get_coinbase_data() + data) 30 | ) 31 | 32 | tx_in.scriptSig = tx_in._scriptSig_template[0] + self.extranonce_placeholder + tx_in._scriptSig_template[1] 33 | 34 | tx_out = halfnode.CTxOut() 35 | tx_out.nValue = value 36 | tx_out.scriptPubKey = coinbaser.get_script_pubkey() 37 | 38 | self.vin.append(tx_in) 39 | self.vout.append(tx_out) 40 | 41 | if(commitment): 42 | txout_commitment = halfnode.CTxOut() 43 | txout_commitment.nValue = 0 44 | txout_commitment.scriptPubKey = commitment 45 | self.vout.append(txout_commitment) 46 | # Two parts of serialized coinbase, just put part1 + extranonce + part2 to have final serialized tx 47 | self._serialized = super(CoinbaseTransaction, self).serialize().split(self.extranonce_placeholder) 48 | 49 | def set_extranonce(self, extranonce): 50 | if len(extranonce) != self.extranonce_size: 51 | raise Exception("Incorrect extranonce size") 52 | 53 | (part1, part2) = self.vin[0]._scriptSig_template 54 | self.vin[0].scriptSig = part1 + extranonce + part2 55 | -------------------------------------------------------------------------------- /lib/exceptions.py: -------------------------------------------------------------------------------- 1 | from stratum.custom_exceptions import ServiceException 2 | 3 | class SubmitException(ServiceException): 4 | pass -------------------------------------------------------------------------------- /lib/extranonce_counter.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | class ExtranonceCounter(object): 4 | '''Implementation of a counter producing 5 | unique extranonce across all pool instances. 6 | This is just dumb "quick&dirty" solution, 7 | but it can be changed at any time without breaking anything.''' 8 | 9 | def __init__(self, instance_id): 10 | if instance_id < 0 or instance_id > 31: 11 | raise Exception("Current ExtranonceCounter implementation needs an instance_id in <0, 31>.") 12 | 13 | # Last 5 most-significant bits represents instance_id 14 | # The rest is just an iterator of jobs. 15 | self.counter = instance_id << 27 16 | self.size = struct.calcsize('>L') 17 | 18 | def get_size(self): 19 | '''Return expected size of generated extranonce in bytes''' 20 | return self.size 21 | 22 | def get_new_bin(self): 23 | self.counter += 1 24 | return struct.pack('>L', self.counter) 25 | 26 | -------------------------------------------------------------------------------- /lib/halfnode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Public Domain 3 | # Original author: ArtForz 4 | # Twisted integration: slush 5 | 6 | import struct 7 | import socket 8 | import binascii 9 | import time 10 | import sys 11 | import random 12 | import cStringIO 13 | from Crypto.Hash import SHA256 14 | 15 | from twisted.internet.protocol import Protocol 16 | from util import * 17 | 18 | MY_VERSION = 31402 19 | MY_SUBVERSION = ".4" 20 | 21 | class CAddress(object): 22 | def __init__(self): 23 | self.nTime = 0 24 | self.nServices = 1 25 | self.pchReserved = "\x00" * 10 + "\xff" * 2 26 | self.ip = "0.0.0.0" 27 | self.port = 0 28 | def deserialize(self, f): 29 | #self.nTime = struct.unpack("H", f.read(2))[0] 34 | def serialize(self): 35 | r = "" 36 | #r += struct.pack("H", self.port) 41 | return r 42 | def __repr__(self): 43 | return "CAddress(nServices=%i ip=%s port=%i)" % (self.nServices, self.ip, self.port) 44 | 45 | class CInv(object): 46 | typemap = { 47 | 0: "Error", 48 | 1: "TX", 49 | 2: "Block"} 50 | def __init__(self): 51 | self.type = 0 52 | self.hash = 0L 53 | def deserialize(self, f): 54 | self.type = struct.unpack(" 21000000L * 100000000L: 158 | return False 159 | return True 160 | def __repr__(self): 161 | return "CTransaction(nVersion=%i vin=%s vout=%s nLockTime=%i)" % (self.nVersion, repr(self.vin), repr(self.vout), self.nLockTime) 162 | 163 | class CBlock(object): 164 | def __init__(self): 165 | self.nVersion = 1 166 | self.hashPrevBlock = 0 167 | self.hashMerkleRoot = 0 168 | self.nTime = 0 169 | self.nBits = 0 170 | self.nNonce = 0 171 | self.vtx = [] 172 | self.sha256 = None 173 | def deserialize(self, f): 174 | self.nVersion = struct.unpack(" target: 207 | return False 208 | hashes = [] 209 | for tx in self.vtx: 210 | tx.sha256 = None 211 | if not tx.is_valid(): 212 | return False 213 | tx.calc_sha256() 214 | hashes.append(ser_uint256(tx.sha256)) 215 | 216 | while len(hashes) > 1: 217 | newhashes = [] 218 | for i in xrange(0, len(hashes), 2): 219 | i2 = min(i+1, len(hashes)-1) 220 | newhashes.append(SHA256.new(SHA256.new(hashes[i] + hashes[i2]).digest()).digest()) 221 | hashes = newhashes 222 | 223 | if uint256_from_str(hashes[0]) != self.hashMerkleRoot: 224 | return False 225 | return True 226 | def __repr__(self): 227 | return "CBlock(nVersion=%i hashPrevBlock=%064x hashMerkleRoot=%064x nTime=%s nBits=%08x nNonce=%08x vtx=%s)" % (self.nVersion, self.hashPrevBlock, self.hashMerkleRoot, time.ctime(self.nTime), self.nBits, self.nNonce, repr(self.vtx)) 228 | 229 | class msg_version(object): 230 | command = "version" 231 | def __init__(self): 232 | self.nVersion = MY_VERSION 233 | self.nServices = 0 234 | self.nTime = time.time() 235 | self.addrTo = CAddress() 236 | self.addrFrom = CAddress() 237 | self.nNonce = random.getrandbits(64) 238 | self.strSubVer = MY_SUBVERSION 239 | self.nStartingHeight = 0 240 | 241 | def deserialize(self, f): 242 | self.nVersion = struct.unpack(" 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as 6 | # published by the Free Software Foundation, either version 3 of the 7 | # License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from hashlib import sha256 18 | from util import doublesha 19 | 20 | class MerkleTree: 21 | def __init__(self, data, detailed=False): 22 | self.data = data 23 | self.recalculate(detailed) 24 | self._hash_steps = None 25 | 26 | def recalculate(self, detailed=False): 27 | L = self.data 28 | steps = [] 29 | if detailed: 30 | detail = [] 31 | PreL = [] 32 | StartL = 0 33 | else: 34 | detail = None 35 | PreL = [None] 36 | StartL = 2 37 | Ll = len(L) 38 | if detailed or Ll > 1: 39 | while True: 40 | if detailed: 41 | detail += L 42 | if Ll == 1: 43 | break 44 | steps.append(L[1]) 45 | if Ll % 2: 46 | L += [L[-1]] 47 | L = PreL + [doublesha(L[i] + L[i + 1]) for i in range(StartL, Ll, 2)] 48 | Ll = len(L) 49 | self._steps = steps 50 | self.detail = detail 51 | 52 | def hash_steps(self): 53 | if self._hash_steps == None: 54 | self._hash_steps = doublesha(''.join(self._steps)) 55 | return self._hash_steps 56 | 57 | def withFirst(self, f): 58 | steps = self._steps 59 | for s in steps: 60 | f = doublesha(f + s) 61 | return f 62 | 63 | def merkleRoot(self): 64 | return self.withFirst(self.data[0]) 65 | 66 | # MerkleTree tests 67 | def _test(): 68 | import binascii 69 | import time 70 | 71 | mt = MerkleTree([None] + [binascii.unhexlify(a) for a in [ 72 | '999d2c8bb6bda0bf784d9ebeb631d711dbbbfe1bc006ea13d6ad0d6a2649a971', 73 | '3f92594d5a3d7b4df29d7dd7c46a0dac39a96e751ba0fc9bab5435ea5e22a19d', 74 | 'a5633f03855f541d8e60a6340fc491d49709dc821f3acb571956a856637adcb6', 75 | '28d97c850eaf917a4c76c02474b05b70a197eaefb468d21c22ed110afe8ec9e0', 76 | ]]) 77 | assert( 78 | b'82293f182d5db07d08acf334a5a907012bbb9990851557ac0ec028116081bd5a' == 79 | binascii.b2a_hex(mt.withFirst(binascii.unhexlify('d43b669fb42cfa84695b844c0402d410213faa4f3e66cb7248f688ff19d5e5f7'))) 80 | ) 81 | 82 | print '82293f182d5db07d08acf334a5a907012bbb9990851557ac0ec028116081bd5a' 83 | txes = [binascii.unhexlify(a) for a in [ 84 | 'd43b669fb42cfa84695b844c0402d410213faa4f3e66cb7248f688ff19d5e5f7', 85 | '999d2c8bb6bda0bf784d9ebeb631d711dbbbfe1bc006ea13d6ad0d6a2649a971', 86 | '3f92594d5a3d7b4df29d7dd7c46a0dac39a96e751ba0fc9bab5435ea5e22a19d', 87 | 'a5633f03855f541d8e60a6340fc491d49709dc821f3acb571956a856637adcb6', 88 | '28d97c850eaf917a4c76c02474b05b70a197eaefb468d21c22ed110afe8ec9e0', 89 | ]] 90 | 91 | s = time.time() 92 | mt = MerkleTree(txes) 93 | for x in range(100): 94 | y = int('d43b669fb42cfa84695b844c0402d410213faa4f3e66cb7248f688ff19d5e5f7', 16) 95 | #y += x 96 | coinbasehash = binascii.unhexlify("%x" % y) 97 | x = binascii.b2a_hex(mt.withFirst(coinbasehash)) 98 | 99 | print x 100 | print time.time() - s 101 | 102 | if __name__ == '__main__': 103 | _test() 104 | -------------------------------------------------------------------------------- /lib/template_registry.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | import binascii 3 | import util 4 | import StringIO 5 | 6 | from twisted.internet import defer 7 | from lib.exceptions import SubmitException 8 | 9 | import stratum.logger 10 | log = stratum.logger.get_logger('template_registry') 11 | 12 | from mining.interfaces import Interfaces 13 | from extranonce_counter import ExtranonceCounter 14 | 15 | class JobIdGenerator(object): 16 | '''Generate pseudo-unique job_id. It does not need to be absolutely unique, 17 | because pool sends "clean_jobs" flag to clients and they should drop all previous jobs.''' 18 | counter = 0 19 | 20 | @classmethod 21 | def get_new_id(cls): 22 | cls.counter += 1 23 | if cls.counter % 0xffff == 0: 24 | cls.counter = 1 25 | return "%x" % cls.counter 26 | 27 | class TemplateRegistry(object): 28 | '''Implements the main logic of the pool. Keep track 29 | on valid block templates, provide internal interface for stratum 30 | service and implements block validation and submits.''' 31 | 32 | def __init__(self, block_template_class, coinbaser, bitcoin_rpc, instance_id, 33 | on_template_callback, on_block_callback): 34 | self.prevhashes = {} 35 | self.jobs = weakref.WeakValueDictionary() 36 | 37 | self.extranonce_counter = ExtranonceCounter(instance_id) 38 | self.extranonce2_size = block_template_class.coinbase_transaction_class.extranonce_size \ 39 | - self.extranonce_counter.get_size() 40 | 41 | self.coinbaser = coinbaser 42 | self.block_template_class = block_template_class 43 | self.bitcoin_rpc = bitcoin_rpc 44 | self.on_block_callback = on_block_callback 45 | self.on_template_callback = on_template_callback 46 | 47 | self.last_block = None 48 | self.update_in_progress = False 49 | self.last_update = None 50 | 51 | # Create first block template on startup 52 | self.update_block() 53 | 54 | def get_new_extranonce1(self): 55 | '''Generates unique extranonce1 (e.g. for newly 56 | subscribed connection.''' 57 | return self.extranonce_counter.get_new_bin() 58 | 59 | def get_last_broadcast_args(self): 60 | '''Returns arguments for mining.notify 61 | from last known template.''' 62 | return self.last_block.broadcast_args 63 | 64 | def add_template(self, block): 65 | '''Adds new template to the registry. 66 | It also clean up templates which should 67 | not be used anymore.''' 68 | 69 | prevhash = block.prevhash_hex 70 | 71 | if prevhash in self.prevhashes.keys(): 72 | new_block = False 73 | else: 74 | new_block = True 75 | self.prevhashes[prevhash] = [] 76 | 77 | # Blocks sorted by prevhash, so it's easy to drop 78 | # them on blockchain update 79 | self.prevhashes[prevhash].append(block) 80 | 81 | # Weak reference for fast lookup using job_id 82 | self.jobs[block.job_id] = block 83 | 84 | # Use this template for every new request 85 | self.last_block = block 86 | 87 | # Drop templates of obsolete blocks 88 | for ph in self.prevhashes.keys(): 89 | if ph != prevhash: 90 | del self.prevhashes[ph] 91 | 92 | log.info("New template for %s" % prevhash) 93 | 94 | if new_block: 95 | # Tell the system about new block 96 | # It is mostly important for share manager 97 | self.on_block_callback(prevhash) 98 | 99 | # Everything is ready, let's broadcast jobs! 100 | self.on_template_callback(new_block) 101 | 102 | 103 | #from twisted.internet import reactor 104 | #reactor.callLater(10, self.on_block_callback, new_block) 105 | 106 | def update_block(self): 107 | '''Registry calls the getblocktemplate() RPC 108 | and build new block template.''' 109 | 110 | if self.update_in_progress: 111 | # Block has been already detected 112 | return 113 | 114 | self.update_in_progress = True 115 | self.last_update = Interfaces.timestamper.time() 116 | 117 | d = self.bitcoin_rpc.getblocktemplate() 118 | d.addCallback(self._update_block) 119 | d.addErrback(self._update_block_failed) 120 | 121 | def _update_block_failed(self, failure): 122 | log.error(str(failure)) 123 | self.update_in_progress = False 124 | 125 | def _update_block(self, data): 126 | start = Interfaces.timestamper.time() 127 | 128 | template = self.block_template_class(Interfaces.timestamper, self.coinbaser, JobIdGenerator.get_new_id()) 129 | template.fill_from_rpc(data) 130 | self.add_template(template) 131 | 132 | log.info("Update finished, %.03f sec, %d txes" % \ 133 | (Interfaces.timestamper.time() - start, len(template.vtx))) 134 | 135 | self.update_in_progress = False 136 | return data 137 | 138 | def diff_to_target(self, difficulty): 139 | '''Converts difficulty to target''' 140 | diff1 = 0x00000000ffff0000000000000000000000000000000000000000000000000000 141 | return diff1 / difficulty 142 | 143 | def get_job(self, job_id): 144 | '''For given job_id returns BlockTemplate instance or None''' 145 | try: 146 | j = self.jobs[job_id] 147 | except: 148 | log.info("Job id '%s' not found" % job_id) 149 | return None 150 | 151 | # Now we have to check if job is still valid. 152 | # Unfortunately weak references are not bulletproof and 153 | # old reference can be found until next run of garbage collector. 154 | if j.prevhash_hex not in self.prevhashes: 155 | log.info("Prevhash of job '%s' is unknown" % job_id) 156 | return None 157 | 158 | if j not in self.prevhashes[j.prevhash_hex]: 159 | log.info("Job %s is unknown" % job_id) 160 | return None 161 | 162 | return j 163 | 164 | def submit_share(self, job_id, worker_name, extranonce1_bin, extranonce2, ntime, nonce, 165 | difficulty): 166 | '''Check parameters and finalize block template. If it leads 167 | to valid block candidate, asynchronously submits the block 168 | back to the bitcoin network. 169 | 170 | - extranonce1_bin is binary. No checks performed, it should be from session data 171 | - job_id, extranonce2, ntime, nonce - in hex form sent by the client 172 | - difficulty - decimal number from session, again no checks performed 173 | - submitblock_callback - reference to method which receive result of submitblock() 174 | ''' 175 | 176 | # Check if extranonce2 looks correctly. extranonce2 is in hex form... 177 | if len(extranonce2) != self.extranonce2_size * 2: 178 | raise SubmitException("Incorrect size of extranonce2. Expected %d chars" % (self.extranonce2_size*2)) 179 | 180 | # Check for job 181 | job = self.get_job(job_id) 182 | if job == None: 183 | raise SubmitException("Job '%s' not found" % job_id) 184 | 185 | # Check if ntime looks correct 186 | if len(ntime) != 8: 187 | raise SubmitException("Incorrect size of ntime. Expected 8 chars") 188 | 189 | if not job.check_ntime(int(ntime, 16)): 190 | raise SubmitException("Ntime out of range") 191 | 192 | # Check nonce 193 | if len(nonce) != 8: 194 | raise SubmitException("Incorrect size of nonce. Expected 8 chars") 195 | 196 | # Convert from hex to binary 197 | extranonce2_bin = binascii.unhexlify(extranonce2) 198 | ntime_bin = binascii.unhexlify(ntime) 199 | nonce_bin = binascii.unhexlify(nonce) 200 | 201 | # Check for duplicated submit 202 | if not job.register_submit(extranonce1_bin, extranonce2_bin, ntime_bin, nonce_bin): 203 | log.info("Duplicate from %s, (%s %s %s %s)" % \ 204 | (worker_name, binascii.hexlify(extranonce1_bin), extranonce2, ntime, nonce)) 205 | raise SubmitException("Duplicate share") 206 | 207 | # Now let's do the hard work! 208 | # --------------------------- 209 | 210 | # 1. Build coinbase 211 | coinbase_bin = job.serialize_coinbase(extranonce1_bin, extranonce2_bin) 212 | coinbase_hash = util.doublesha(coinbase_bin) 213 | 214 | # 2. Calculate merkle root 215 | merkle_root_bin = job.merkletree.withFirst(coinbase_hash) 216 | merkle_root_int = util.uint256_from_str(merkle_root_bin) 217 | 218 | # 3. Serialize header with given merkle, ntime and nonce 219 | header_bin = job.serialize_header(merkle_root_int, ntime_bin, nonce_bin) 220 | 221 | # 4. Reverse header and compare it with target of the user 222 | hash_bin = util.doublesha(''.join([ header_bin[i*4:i*4+4][::-1] for i in range(0, 20) ])) 223 | hash_int = util.uint256_from_str(hash_bin) 224 | block_hash_hex = "%064x" % hash_int 225 | header_hex = binascii.hexlify(header_bin) 226 | 227 | target_user = self.diff_to_target(difficulty) 228 | if hash_int > target_user: 229 | raise SubmitException("Share is above target") 230 | 231 | # Mostly for debugging purposes 232 | target_info = self.diff_to_target(100000) 233 | if hash_int <= target_info: 234 | log.info("Yay, share with diff above 100000") 235 | 236 | # 5. Compare hash with target of the network 237 | if hash_int <= job.target: 238 | # Yay! It is block candidate! 239 | log.info("We found a block candidate! %s" % block_hash_hex) 240 | 241 | # 6. Finalize and serialize block object 242 | job.finalize(merkle_root_int, extranonce1_bin, extranonce2_bin, int(ntime, 16), int(nonce, 16)) 243 | 244 | if not job.is_valid(): 245 | # Should not happen 246 | log.error("Final job validation failed!") 247 | 248 | # 7. Submit block to the network 249 | serialized = binascii.hexlify(job.serialize()) 250 | on_submit = self.bitcoin_rpc.submitblock(serialized) 251 | 252 | return (header_hex, block_hash_hex, on_submit) 253 | 254 | return (header_hex, block_hash_hex, None) 255 | -------------------------------------------------------------------------------- /lib/util.py: -------------------------------------------------------------------------------- 1 | '''Various helper methods. It probably needs some cleanup.''' 2 | 3 | import struct 4 | import StringIO 5 | import binascii 6 | from hashlib import sha256 7 | 8 | def deser_string(f): 9 | nit = struct.unpack(">= 32 39 | return rs 40 | 41 | def uint256_from_str(s): 42 | r = 0L 43 | t = struct.unpack("IIIIIIII", s[:32]) 51 | for i in xrange(8): 52 | r += t[i] << (i * 32) 53 | return r 54 | 55 | def uint256_from_compact(c): 56 | nbytes = (c >> 24) & 0xFF 57 | v = (c & 0xFFFFFFL) << (8 * (nbytes - 3)) 58 | return v 59 | 60 | def deser_vector(f, c): 61 | nit = struct.unpack("= 256: 129 | div, mod = divmod(long_value, 256) 130 | result = chr(mod) + result 131 | long_value = div 132 | result = chr(long_value) + result 133 | 134 | nPad = 0 135 | for c in v: 136 | if c == __b58chars[0]: nPad += 1 137 | else: break 138 | 139 | result = chr(0)*nPad + result 140 | if length is not None and len(result) != length: 141 | return None 142 | 143 | return result 144 | 145 | def reverse_hash(h): 146 | # This only revert byte order, nothing more 147 | if len(h) != 64: 148 | raise Exception('hash must have 64 hexa chars') 149 | 150 | return ''.join([ h[56-i:64-i] for i in range(0, 64, 8) ]) 151 | 152 | def doublesha(b): 153 | return sha256(sha256(b).digest()).digest() 154 | 155 | def bits_to_target(bits): 156 | return struct.unpack('I", u & 0xFFFFFFFFL) 181 | u >>= 32 182 | return rs 183 | 184 | def deser_uint256_be(f): 185 | r = 0L 186 | for i in xrange(8): 187 | t = struct.unpack(">I", f.read(4))[0] 188 | r += t << (i * 32) 189 | return r 190 | 191 | def ser_number(n): 192 | # For encoding nHeight into coinbase 193 | s = bytearray(b'\1') 194 | while n > 127: 195 | s[0] += 1 196 | s.append(n % 256) 197 | n //= 256 198 | s.append(n) 199 | return bytes(s) 200 | 201 | def script_to_address(addr): 202 | d = address_to_pubkeyhash(addr) 203 | if not d: 204 | raise ValueError('invalid address') 205 | (ver, pubkeyhash) = d 206 | return b'\x76\xa9\x14' + pubkeyhash + b'\x88\xac' 207 | -------------------------------------------------------------------------------- /mining/__init__.py: -------------------------------------------------------------------------------- 1 | from service import MiningService 2 | from subscription import MiningSubscription 3 | from twisted.internet import defer 4 | import time 5 | 6 | @defer.inlineCallbacks 7 | def setup(on_startup): 8 | '''Setup mining service internal environment. 9 | You should not need to change this. If you 10 | want to use another Worker manager or Share manager, 11 | you should set proper reference to Interfaces class 12 | *before* you call setup() in the launcher script.''' 13 | 14 | from stratum import settings 15 | from interfaces import Interfaces 16 | 17 | # Let's wait until share manager and worker manager boot up 18 | (yield Interfaces.share_manager.on_load) 19 | (yield Interfaces.worker_manager.on_load) 20 | 21 | from lib.block_updater import BlockUpdater 22 | from lib.template_registry import TemplateRegistry 23 | from lib.bitcoin_rpc import BitcoinRPC 24 | from lib.block_template import BlockTemplate 25 | from lib.coinbaser import SimpleCoinbaser 26 | 27 | bitcoin_rpc = BitcoinRPC(settings.BITCOIN_TRUSTED_HOST, 28 | settings.BITCOIN_TRUSTED_PORT, 29 | settings.BITCOIN_TRUSTED_USER, 30 | settings.BITCOIN_TRUSTED_PASSWORD) 31 | 32 | import stratum.logger 33 | log = stratum.logger.get_logger('mining') 34 | 35 | log.info('Waiting for bitcoin RPC...') 36 | 37 | while True: 38 | try: 39 | result = (yield bitcoin_rpc.getblocktemplate()) 40 | if isinstance(result, dict): 41 | log.info('Response from bitcoin RPC OK') 42 | break 43 | except: 44 | time.sleep(1) 45 | 46 | coinbaser = SimpleCoinbaser(bitcoin_rpc, settings.CENTRAL_WALLET) 47 | (yield coinbaser.on_load) 48 | 49 | registry = TemplateRegistry(BlockTemplate, 50 | coinbaser, 51 | bitcoin_rpc, 52 | settings.INSTANCE_ID, 53 | MiningSubscription.on_template, 54 | Interfaces.share_manager.on_network_block) 55 | 56 | # Template registry is the main interface between Stratum service 57 | # and pool core logic 58 | Interfaces.set_template_registry(registry) 59 | 60 | # Set up polling mechanism for detecting new block on the network 61 | # This is just failsafe solution when -blocknotify 62 | # mechanism is not working properly 63 | BlockUpdater(registry, bitcoin_rpc) 64 | 65 | log.info("MINING SERVICE IS READY") 66 | on_startup.callback(True) 67 | -------------------------------------------------------------------------------- /mining/interfaces.py: -------------------------------------------------------------------------------- 1 | '''This module contains classes used by pool core to interact with the rest of the pool. 2 | Default implementation do almost nothing, you probably want to override these classes 3 | and customize references to interface instances in your launcher. 4 | (see launcher_demo.tac for an example). 5 | ''' 6 | 7 | import time 8 | from twisted.internet import reactor, defer 9 | 10 | import stratum.logger 11 | log = stratum.logger.get_logger('interfaces') 12 | 13 | class WorkerManagerInterface(object): 14 | def __init__(self): 15 | # Fire deferred when manager is ready 16 | self.on_load = defer.Deferred() 17 | self.on_load.callback(True) 18 | 19 | def authorize(self, worker_name, worker_password): 20 | return True 21 | 22 | class ShareLimiterInterface(object): 23 | '''Implement difficulty adjustments here''' 24 | 25 | def submit(self, connection_ref, current_difficulty, timestamp): 26 | '''connection - weak reference to Protocol instance 27 | current_difficulty - difficulty of the connection 28 | timestamp - submission time of current share 29 | 30 | - raise SubmitException for stop processing this request 31 | - call mining.set_difficulty on connection to adjust the difficulty''' 32 | pass 33 | 34 | class ShareManagerInterface(object): 35 | def __init__(self): 36 | # Fire deferred when manager is ready 37 | self.on_load = defer.Deferred() 38 | self.on_load.callback(True) 39 | 40 | def on_network_block(self, prevhash): 41 | '''Prints when there's new block coming from the network (possibly new round)''' 42 | pass 43 | 44 | def on_submit_share(self, worker_name, block_header, block_hash, shares, timestamp, is_valid): 45 | log.info("%s %s %s" % (block_hash, 'valid' if is_valid else 'INVALID', worker_name)) 46 | 47 | def on_submit_block(self, is_accepted, worker_name, block_header, block_hash, timestamp): 48 | log.info("Block %s %s" % (block_hash, 'ACCEPTED' if is_accepted else 'REJECTED')) 49 | 50 | class TimestamperInterface(object): 51 | '''This is the only source for current time in the application. 52 | Override this for generating unix timestamp in different way.''' 53 | def time(self): 54 | return time.time() 55 | 56 | class PredictableTimestamperInterface(TimestamperInterface): 57 | '''Predictable timestamper may be useful for unit testing.''' 58 | start_time = 1345678900 # Some day in year 2012 59 | delta = 0 60 | 61 | def time(self): 62 | self.delta += 1 63 | return self.start_time + self.delta 64 | 65 | class Interfaces(object): 66 | worker_manager = None 67 | share_manager = None 68 | share_limiter = None 69 | timestamper = None 70 | template_registry = None 71 | 72 | @classmethod 73 | def set_worker_manager(cls, manager): 74 | cls.worker_manager = manager 75 | 76 | @classmethod 77 | def set_share_manager(cls, manager): 78 | cls.share_manager = manager 79 | 80 | @classmethod 81 | def set_share_limiter(cls, limiter): 82 | cls.share_limiter = limiter 83 | 84 | @classmethod 85 | def set_timestamper(cls, manager): 86 | cls.timestamper = manager 87 | 88 | @classmethod 89 | def set_template_registry(cls, registry): 90 | cls.template_registry = registry 91 | -------------------------------------------------------------------------------- /mining/service.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from twisted.internet import defer 3 | 4 | from stratum.services import GenericService, admin 5 | from stratum.pubsub import Pubsub 6 | from interfaces import Interfaces 7 | from subscription import MiningSubscription 8 | from lib.exceptions import SubmitException 9 | 10 | import stratum.logger 11 | log = stratum.logger.get_logger('mining') 12 | 13 | class MiningService(GenericService): 14 | '''This service provides public API for Stratum mining proxy 15 | or any Stratum-compatible miner software. 16 | 17 | Warning - any callable argument of this class will be propagated 18 | over Stratum protocol for public audience!''' 19 | 20 | service_type = 'mining' 21 | service_vendor = 'stratum' 22 | is_default = True 23 | 24 | @admin 25 | def update_block(self): 26 | '''Connect this RPC call to 'bitcoind -blocknotify' for 27 | instant notification about new block on the network. 28 | See blocknotify.sh in /scripts/ for more info.''' 29 | 30 | log.info("New block notification received") 31 | Interfaces.template_registry.update_block() 32 | return True 33 | 34 | def authorize(self, worker_name, worker_password): 35 | '''Let authorize worker on this connection.''' 36 | 37 | session = self.connection_ref().get_session() 38 | session.setdefault('authorized', {}) 39 | 40 | if Interfaces.worker_manager.authorize(worker_name, worker_password): 41 | session['authorized'][worker_name] = worker_password 42 | return True 43 | 44 | else: 45 | if worker_name in session['authorized']: 46 | del session['authorized'][worker_name] 47 | return False 48 | 49 | def subscribe(self, *args): 50 | '''Subscribe for receiving mining jobs. This will 51 | return subscription details, extranonce1_hex and extranonce2_size''' 52 | 53 | extranonce1 = Interfaces.template_registry.get_new_extranonce1() 54 | extranonce2_size = Interfaces.template_registry.extranonce2_size 55 | extranonce1_hex = binascii.hexlify(extranonce1) 56 | 57 | session = self.connection_ref().get_session() 58 | session['extranonce1'] = extranonce1 59 | session['difficulty'] = 1 # Following protocol specs, default diff is 1 60 | 61 | return Pubsub.subscribe(self.connection_ref(), MiningSubscription()) + (extranonce1_hex, extranonce2_size) 62 | 63 | ''' 64 | def submit(self, worker_name, job_id, extranonce2, ntime, nonce): 65 | import time 66 | start = time.time() 67 | 68 | for x in range(100): 69 | try: 70 | ret = self.submit2(worker_name, job_id, extranonce2, ntime, nonce) 71 | except: 72 | pass 73 | 74 | log.info("LEN %.03f" % (time.time() - start)) 75 | return ret 76 | ''' 77 | 78 | def submit(self, worker_name, job_id, extranonce2, ntime, nonce): 79 | '''Try to solve block candidate using given parameters.''' 80 | 81 | session = self.connection_ref().get_session() 82 | session.setdefault('authorized', {}) 83 | 84 | # Check if worker is authorized to submit shares 85 | if not Interfaces.worker_manager.authorize(worker_name, 86 | session['authorized'].get(worker_name)): 87 | raise SubmitException("Worker is not authorized") 88 | 89 | # Check if extranonce1 is in connection session 90 | extranonce1_bin = session.get('extranonce1', None) 91 | if not extranonce1_bin: 92 | raise SubmitException("Connection is not subscribed for mining") 93 | 94 | difficulty = session['difficulty'] 95 | submit_time = Interfaces.timestamper.time() 96 | 97 | Interfaces.share_limiter.submit(self.connection_ref, difficulty, submit_time) 98 | 99 | # This checks if submitted share meet all requirements 100 | # and it is valid proof of work. 101 | try: 102 | (block_header, block_hash, on_submit) = Interfaces.template_registry.submit_share(job_id, 103 | worker_name, extranonce1_bin, extranonce2, ntime, nonce, difficulty) 104 | except SubmitException: 105 | # block_header and block_hash are None when submitted data are corrupted 106 | Interfaces.share_manager.on_submit_share(worker_name, None, None, difficulty, 107 | submit_time, False) 108 | raise 109 | 110 | 111 | Interfaces.share_manager.on_submit_share(worker_name, block_header, block_hash, difficulty, 112 | submit_time, True) 113 | 114 | if on_submit != None: 115 | # Pool performs submitblock() to bitcoind. Let's hook 116 | # to result and report it to share manager 117 | on_submit.addCallback(Interfaces.share_manager.on_submit_block, 118 | worker_name, block_header, block_hash, submit_time) 119 | 120 | return True 121 | 122 | # Service documentation for remote discovery 123 | update_block.help_text = "Notify Stratum server about new block on the network." 124 | update_block.params = [('password', 'string', 'Administrator password'),] 125 | 126 | authorize.help_text = "Authorize worker for submitting shares on this connection." 127 | authorize.params = [('worker_name', 'string', 'Name of the worker, usually in the form of user_login.worker_id.'), 128 | ('worker_password', 'string', 'Worker password'),] 129 | 130 | subscribe.help_text = "Subscribes current connection for receiving new mining jobs." 131 | subscribe.params = [] 132 | 133 | submit.help_text = "Submit solved share back to the server. Excessive sending of invalid shares "\ 134 | "or shares above indicated target (see Stratum mining docs for set_target()) may lead "\ 135 | "to temporary or permanent ban of user,worker or IP address." 136 | submit.params = [('worker_name', 'string', 'Name of the worker, usually in the form of user_login.worker_id.'), 137 | ('job_id', 'string', 'ID of job (received by mining.notify) which the current solution is based on.'), 138 | ('extranonce2', 'string', 'hex-encoded big-endian extranonce2, length depends on extranonce2_size from mining.notify.'), 139 | ('ntime', 'string', 'UNIX timestamp (32bit integer, big-endian, hex-encoded), must be >= ntime provided by mining,notify and <= current time'), 140 | ('nonce', 'string', '32bit integer, hex-encoded, big-endian'),] 141 | 142 | -------------------------------------------------------------------------------- /mining/subscription.py: -------------------------------------------------------------------------------- 1 | from stratum.pubsub import Pubsub, Subscription 2 | from mining.interfaces import Interfaces 3 | 4 | import stratum.logger 5 | log = stratum.logger.get_logger('subscription') 6 | 7 | class MiningSubscription(Subscription): 8 | '''This subscription object implements 9 | logic for broadcasting new jobs to the clients.''' 10 | 11 | event = 'mining.notify' 12 | 13 | @classmethod 14 | def on_template(cls, is_new_block): 15 | '''This is called when TemplateRegistry registers 16 | new block which we have to broadcast clients.''' 17 | 18 | start = Interfaces.timestamper.time() 19 | 20 | clean_jobs = is_new_block 21 | (job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, _) = \ 22 | Interfaces.template_registry.get_last_broadcast_args() 23 | 24 | # Push new job to subscribed clients 25 | cls.emit(job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, clean_jobs) 26 | 27 | cnt = Pubsub.get_subscription_count(cls.event) 28 | log.info("BROADCASTED to %d connections in %.03f sec" % (cnt, (Interfaces.timestamper.time() - start))) 29 | 30 | def _finish_after_subscribe(self, result): 31 | '''Send new job to newly subscribed client''' 32 | try: 33 | (job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, _) = \ 34 | Interfaces.template_registry.get_last_broadcast_args() 35 | except Exception: 36 | log.error("Template not ready yet") 37 | return result 38 | 39 | # Force set higher difficulty 40 | # TODO 41 | #self.connection_ref().rpc('mining.set_difficulty', [2,], is_notification=True) 42 | #self.connection_ref().rpc('client.get_version', []) 43 | 44 | # Force client to remove previous jobs if any (eg. from previous connection) 45 | clean_jobs = True 46 | self.emit_single(job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, True) 47 | 48 | return result 49 | 50 | def after_subscribe(self, *args): 51 | '''This will send new job to the client *after* he receive subscription details. 52 | on_finish callback solve the issue that job is broadcasted *during* 53 | the subscription request and client receive messages in wrong order.''' 54 | self.connection_ref().on_finish.addCallback(self._finish_after_subscribe) 55 | -------------------------------------------------------------------------------- /scripts/blocknotify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Send notification to Stratum mining instance on localhost that there's new bitcoin block 3 | # You can use this script directly as an variable for -blocknotify argument: 4 | # ./bitcoind -blocknotify="blocknotify.sh --password admin_password" 5 | # This is also very basic example how to use Stratum protocol in native Python 6 | 7 | import socket 8 | import json 9 | import sys 10 | import argparse 11 | import time 12 | 13 | start = time.time() 14 | 15 | parser = argparse.ArgumentParser(description='Send notification to Stratum instance about new bitcoin block.') 16 | parser.add_argument('--password', dest='password', type=str, help='use admin password from Stratum server config') 17 | parser.add_argument('--host', dest='host', type=str, default='localhost', help='hostname of Stratum mining instance') 18 | parser.add_argument('--port', dest='port', type=int, default=3333, help='port of Stratum mining instance') 19 | 20 | args = parser.parse_args() 21 | 22 | if args.password == None: 23 | parser.print_help() 24 | sys.exit() 25 | 26 | message = {'id': 1, 'method': 'mining.update_block', 'params': [args.password]} 27 | 28 | try: 29 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 30 | s.connect((args.host, args.port)) 31 | s.sendall(json.dumps(message)+"\n") 32 | data = s.recv(16000) 33 | s.close() 34 | except IOError: 35 | print "blocknotify: Cannot connect to the pool" 36 | sys.exit() 37 | 38 | for line in data.split("\n"): 39 | if not line.strip(): 40 | # Skip last line which doesn't contain any message 41 | continue 42 | 43 | message = json.loads(line) 44 | if message['id'] == 1: 45 | if message['result'] == True: 46 | print "blocknotify: done in %.03f sec" % (time.time() - start) 47 | else: 48 | print "blocknotify: Error during request:", message['error'][1] 49 | else: 50 | print "blocknotify: Unexpected message from the server:", message 51 | --------------------------------------------------------------------------------