Initial commit
authorslush <info@bitcoin.cz>
Sun, 9 Sep 2012 15:28:06 +0000 (17:28 +0200)
committerslush <info@bitcoin.cz>
Sun, 9 Sep 2012 15:28:06 +0000 (17:28 +0200)
18 files changed:
conf/__init__.py [new file with mode: 0644]
conf/config_sample.py [new file with mode: 0644]
launcher_demo.tac [new file with mode: 0644]
lib/__init__.py [new file with mode: 0644]
lib/bitcoin_rpc.py [new file with mode: 0644]
lib/block_template.py [new file with mode: 0644]
lib/block_updater.py [new file with mode: 0644]
lib/coinbaser.py [new file with mode: 0644]
lib/coinbasetx.py [new file with mode: 0644]
lib/extranonce_counter.py [new file with mode: 0644]
lib/halfnode.py [new file with mode: 0644]
lib/merkletree.py [new file with mode: 0644]
lib/template_registry.py [new file with mode: 0644]
lib/util.py [new file with mode: 0644]
mining/__init__.py [new file with mode: 0644]
mining/interfaces.py [new file with mode: 0644]
mining/service.py [new file with mode: 0644]
mining/subscription.py [new file with mode: 0644]

diff --git a/conf/__init__.py b/conf/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/conf/config_sample.py b/conf/config_sample.py
new file mode 100644 (file)
index 0000000..02c7d30
--- /dev/null
@@ -0,0 +1,74 @@
+'''
+This is example configuration for Stratum server.
+Please rename it to settings.py and fill correct values.
+'''
+
+# ******************** GENERAL SETTINGS ***************
+
+# Enable some verbose debug (logging requests and responses).
+DEBUG = False
+
+# Destination for application logs, files rotated once per day.
+LOGDIR = 'log/'
+
+# Main application log file.
+LOGFILE = None#'stratum.log'
+
+# Possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL
+LOGLEVEL = 'INFO'
+
+# How many threads use for synchronous methods (services).
+# 30 is enough for small installation, for real usage
+# it should be slightly more, say 100-300.
+THREAD_POOL_SIZE = 10
+
+ENABLE_EXAMPLE_SERVICE = True
+
+# ******************** TRANSPORTS *********************
+
+# Hostname or external IP to expose
+HOSTNAME = 'localhost'
+
+# Port used for Socket transport. Use 'None' for disabling the transport.
+LISTEN_SOCKET_TRANSPORT = 3333
+
+# Port used for HTTP Poll transport. Use 'None' for disabling the transport
+LISTEN_HTTP_TRANSPORT = None
+
+# Port used for HTTPS Poll transport
+LISTEN_HTTPS_TRANSPORT = None
+
+# Port used for WebSocket transport, 'None' for disabling WS
+LISTEN_WS_TRANSPORT = None
+
+# Port used for secure WebSocket, 'None' for disabling WSS
+LISTEN_WSS_TRANSPORT = None
+
+# Hostname and credentials for one trusted Bitcoin node ("Satoshi's client").
+# Stratum uses both P2P port (which is 8333 already) and RPC port
+BITCOIN_TRUSTED_HOST = 'localhost'
+BITCOIN_TRUSTED_PORT = 8332
+BITCOIN_TRUSTED_USER = 'user'
+BITCOIN_TRUSTED_PASSWORD = 'somepassword'
+
+# Use "echo -n '<yourpassword>' | sha256sum | cut -f1 -d' ' "
+# for calculating SHA256 of your preferred password
+ADMIN_PASSWORD_SHA256 = None
+#ADMIN_PASSWORD_SHA256 = '9e6c0c1db1e0dfb3fa5159deb4ecd9715b3c8cd6b06bd4a3ad77e9a8c5694219' # SHA256 of the password
+
+IRC_NICK = None
+
+'''
+DATABASE_DRIVER = 'MySQLdb'
+DATABASE_HOST = 'localhost'
+DATABASE_DBNAME = 'pooldb'
+DATABASE_USER = 'pooldb'
+DATABASE_PASSWORD = '**empty**'
+'''
+
+# Pool related settings
+INSTANCE_ID = 31
+CENTRAL_WALLET = 'set_valid_addresss_in_config!'
+PREVHASH_REFRESH_INTERVAL = 5 # in sec
+MERKLE_REFRESH_INTERVAL = 60 # How often check memorypool
+COINBASE_EXTRAS = '/stratum/'
diff --git a/launcher_demo.tac b/launcher_demo.tac
new file mode 100644 (file)
index 0000000..5e66817
--- /dev/null
@@ -0,0 +1,23 @@
+# Run me with "twistd -ny launcher_demo.tac"
+
+# Add conf directory to python path.
+# Configuration file is standard python module.
+import os, sys
+sys.path = [os.path.join(os.getcwd(), 'conf'),] + sys.path
+
+# Bootstrap Stratum framework
+import stratum
+from stratum import settings
+application = stratum.setup()
+
+# Load mining service into stratum framework
+import mining
+
+from mining.interfaces import Interfaces
+from mining.interfaces import WorkerManagerInterface, ShareManagerInterface, TimestamperInterface
+
+Interfaces.set_share_manager(ShareManagerInterface())
+Interfaces.set_worker_manager(WorkerManagerInterface())
+Interfaces.set_timestamper(TimestamperInterface())
+
+mining.setup()
diff --git a/lib/__init__.py b/lib/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/bitcoin_rpc.py b/lib/bitcoin_rpc.py
new file mode 100644 (file)
index 0000000..3517b6d
--- /dev/null
@@ -0,0 +1,64 @@
+'''
+    Implements simple interface to bitcoind's RPC.
+'''
+
+import simplejson as json
+import base64
+from twisted.internet import defer
+from twisted.web import client
+
+import stratum.logger
+log = stratum.logger.get_logger('bitcoin_rpc')
+
+class BitcoinRPC(object):
+    
+    def __init__(self, host, port, username, password):
+        self.bitcoin_url = 'http://%s:%d' % (host, port)
+        self.credentials = base64.b64encode("%s:%s" % (username, password))
+        self.headers = {
+            'Content-Type': 'text/json',
+            'Authorization': 'Basic %s' % self.credentials,
+        }
+        
+    def _call_raw(self, data):
+        return client.getPage(
+            url=self.bitcoin_url,
+            method='POST',
+            headers=self.headers,
+            postdata=data,
+        )
+           
+    def _call(self, method, params):
+        return self._call_raw(json.dumps({
+                'jsonrpc': '2.0',
+                'method': method,
+                'params': params,
+                'id': '1',
+            }))
+
+    @defer.inlineCallbacks
+    def submitblock(self, block_hex):
+        resp = (yield self._call('submitblock', [block_hex,]))
+        if json.loads(resp)['result'] == None:
+            defer.returnValue(True)
+        else:
+            defer.returnValue(False)
+
+    @defer.inlineCallbacks
+    def getblocktemplate(self):
+        resp = (yield self._call('getblocktemplate', []))
+        defer.returnValue(json.loads(resp)['result'])
+                                                  
+    @defer.inlineCallbacks
+    def prevhash(self):
+        resp = (yield self._call('getwork', []))
+        try:
+            defer.returnValue(json.loads(resp)['result']['data'][8:72])
+        except Exception as e:
+            log.exception("Cannot decode prevhash %s" % str(e))
+            raise
+        
+    @defer.inlineCallbacks
+    def validateaddress(self, address):
+        resp = (yield self._call('validateaddress', [address,]))
+        defer.returnValue(json.loads(resp)['result'])
\ No newline at end of file
diff --git a/lib/block_template.py b/lib/block_template.py
new file mode 100644 (file)
index 0000000..4573c0d
--- /dev/null
@@ -0,0 +1,140 @@
+import StringIO
+import binascii
+import struct
+
+import util
+import merkletree
+import halfnode
+from coinbasetx import CoinbaseTransaction
+
+# Remove dependency to settings, coinbase extras should be
+# provided from coinbaser
+from stratum import settings
+
+class BlockTemplate(halfnode.CBlock):
+    '''Template is used for generating new jobs for clients.
+    Let's iterate extranonce1, extranonce2, ntime and nonce
+    to find out valid bitcoin block!'''
+    
+    coinbase_transaction_class = CoinbaseTransaction
+    
+    def __init__(self, timestamper, coinbaser, job_id):
+        super(BlockTemplate, self).__init__()
+        
+        self.job_id = job_id 
+        self.timestamper = timestamper
+        self.coinbaser = coinbaser
+        
+        self.prevhash_bin = '' # reversed binary form of prevhash
+        self.prevhash_hex = ''
+        self.timedelta = 0
+        self.curtime = 0
+        self.target = 0
+        #self.coinbase_hex = None 
+        self.merkletree = None
+                
+        self.broadcast_args = []
+        
+        # List of 4-tuples (extranonce1, extranonce2, ntime, nonce)
+        # registers already submitted and checked shares
+        # There may be registered also invalid shares inside!
+        self.submits = [] 
+                
+    def fill_from_rpc(self, data):
+        '''Convert getblocktemplate result into BlockTemplate instance'''
+        
+        #txhashes = [None] + [ binascii.unhexlify(t['hash']) for t in data['transactions'] ]
+        txhashes = [None] + [ util.ser_uint256(int(t['hash'], 16)) for t in data['transactions'] ]
+        mt = merkletree.MerkleTree(txhashes)
+
+        coinbase = self.coinbase_transaction_class(self.timestamper, self.coinbaser, data['coinbasevalue'],
+                        data['coinbaseaux']['flags'], data['height'], settings.COINBASE_EXTRAS)
+        
+        self.height = data['height']
+        self.nVersion = data['version']
+        self.hashPrevBlock = int(data['previousblockhash'], 16)
+        self.nBits = int(data['bits'], 16)
+        self.hashMerkleRoot = 0
+        self.nTime = 0
+        self.nNonce = 0
+        self.vtx = [ coinbase, ]
+        
+        for tx in data['transactions']:
+            t = halfnode.CTransaction()
+            t.deserialize(StringIO.StringIO(binascii.unhexlify(tx['data'])))
+            self.vtx.append(t)
+            
+        self.curtime = data['curtime']
+        self.timedelta = self.curtime - int(self.timestamper.time()) 
+        self.merkletree = mt
+        self.target = util.uint256_from_compact(self.nBits)
+        
+        # Reversed prevhash
+        self.prevhash_bin = binascii.unhexlify(util.reverse_hash(data['previousblockhash']))
+        self.prevhash_hex = "%064x" % self.hashPrevBlock
+        
+        self.broadcast_args = self.build_broadcast_args()
+                
+    def register_submit(self, extranonce1, extranonce2, ntime, nonce):
+        '''Client submitted some solution. Let's register it to
+        prevent double submissions.'''
+        
+        t = (extranonce1, extranonce2, ntime, nonce)
+        if t not in self.submits:
+            self.submits.append(t)
+            return True
+        return False
+            
+    def build_broadcast_args(self):
+        '''Build parameters of mining.notify call. All clients
+        may receive the same params, because they include
+        their unique extranonce1 into the coinbase, so every
+        coinbase_hash (and then merkle_root) will be unique as well.'''
+        job_id = self.job_id
+        prevhash = binascii.hexlify(self.prevhash_bin)
+        (coinb1, coinb2) = [ binascii.hexlify(x) for x in self.vtx[0]._serialized ]
+        merkle_branch = [ binascii.hexlify(x) for x in self.merkletree._steps ]
+        version = binascii.hexlify(struct.pack(">i", self.nVersion))
+        nbits = binascii.hexlify(struct.pack(">I", self.nBits))
+        ntime = binascii.hexlify(struct.pack(">I", self.curtime))
+        clean_jobs = True
+        
+        return (job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, clean_jobs)
+
+    def serialize_coinbase(self, extranonce1, extranonce2):
+        '''Serialize coinbase with given extranonce1 and extranonce2
+        in binary form'''
+        (part1, part2) = self.vtx[0]._serialized
+        return part1 + extranonce1 + extranonce2 + part2
+    
+    def check_ntime(self, ntime):
+        '''Check for ntime restrictions.'''
+        if ntime < self.curtime:
+            return False
+        
+        if ntime > (self.timestamper.time() + 60):
+            # Be strict on ntime into the near future
+            # may be unnecessary
+            return False
+        
+        return True
+
+    def serialize_header(self, merkle_root_int, ntime_bin, nonce_bin):
+        '''Serialize header for calculating block hash'''
+        r  = struct.pack(">i", self.nVersion)
+        r += self.prevhash_bin
+        r += util.ser_uint256_be(merkle_root_int)
+        r += ntime_bin
+        r += struct.pack(">I", self.nBits)
+        r += nonce_bin    
+        return r       
+
+    def finalize(self, merkle_root_int, extranonce1_bin, extranonce2_bin, ntime, nonce):
+        '''Take all parameters required to compile block candidate.
+        self.is_valid() should return True then...'''
+        
+        self.hashMerkleRoot = merkle_root_int
+        self.nTime = ntime
+        self.nNonce = nonce
+        self.vtx[0].set_extranonce(extranonce1_bin + extranonce2_bin)        
+        self.sha256 = None # We changed block parameters, let's reset sha256 cache
\ No newline at end of file
diff --git a/lib/block_updater.py b/lib/block_updater.py
new file mode 100644 (file)
index 0000000..3ed44b8
--- /dev/null
@@ -0,0 +1,64 @@
+from twisted.internet import reactor, defer
+from stratum import settings
+
+import util
+from mining.interfaces import Interfaces
+
+import stratum.logger
+log = stratum.logger.get_logger('block_updater')
+
+class BlockUpdater(object):
+    '''
+        Polls upstream's getinfo() and detecting new block on the network.
+        This will call registry.update_block when new prevhash appear.
+        
+        This is just failback alternative when something
+        with ./bitcoind -blocknotify will go wrong. 
+    '''
+    
+    def __init__(self, registry, bitcoin_rpc):
+        self.bitcoin_rpc = bitcoin_rpc
+        self.registry = registry
+        self.clock = None
+        self.schedule()
+                        
+    def schedule(self):
+        when = self._get_next_time()
+        log.debug("Next prevhash update in %.03f sec" % when)
+        log.debug("Merkle update in next %.03f sec" % \
+                  ((self.registry.last_update + settings.MERKLE_REFRESH_INTERVAL)-Interfaces.timestamper.time()))
+        self.clock = reactor.callLater(when, self.run)
+        
+    def _get_next_time(self):
+        when = settings.PREVHASH_REFRESH_INTERVAL - (Interfaces.timestamper.time() - self.registry.last_update) % \
+               settings.PREVHASH_REFRESH_INTERVAL
+        return when  
+                     
+    @defer.inlineCallbacks
+    def run(self):
+        update = False
+       
+        try:             
+            if self.registry.last_block:
+                current_prevhash = "%064x" % self.registry.last_block.hashPrevBlock
+            else:
+                current_prevhash = None
+                
+            prevhash = util.reverse_hash((yield self.bitcoin_rpc.prevhash()))
+            if prevhash and prevhash != current_prevhash:
+                log.info("New block! Prevhash: %s" % prevhash)
+                update = True
+            
+            elif Interfaces.timestamper.time() - self.registry.last_update >= settings.MERKLE_REFRESH_INTERVAL:
+                log.info("Merkle update! Prevhash: %s" % prevhash)
+                update = True
+                
+            if update:
+                self.registry.update_block()
+
+        except Exception:
+            log.exception("UpdateWatchdog.run failed")
+        finally:
+            self.schedule()
+
+    
\ No newline at end of file
diff --git a/lib/coinbaser.py b/lib/coinbaser.py
new file mode 100644 (file)
index 0000000..04b7d80
--- /dev/null
@@ -0,0 +1,51 @@
+import util
+
+import stratum.logger
+log = stratum.logger.get_logger('coinbaser')
+
+# TODO: Add on_* hooks in the app
+    
+class SimpleCoinbaser(object):
+    '''This very simple coinbaser uses constant bitcoin address
+    for all generated blocks.'''
+    
+    def __init__(self, bitcoin_rpc, address):
+        self.address = address
+        self.is_valid = False # We need to check if pool can use this address
+        
+        self.bitcoin_rpc = bitcoin_rpc
+        
+        self._validate()
+
+    def _validate(self):
+        d = self.bitcoin_rpc.validateaddress(self.address)
+        d.addCallback(self._address_check)
+        d.addErrback(self._failure)
+        
+    def _address_check(self, result):
+        if result['isvalid'] and result['ismine']:
+            self.is_valid = True
+            log.info("Coinbase address '%s' is valid" % self.address)
+        else:
+            self.is_valid = False
+            log.error("Coinbase address '%s' is NOT valid!" % self.address)
+        
+    def _failure(self, failure):
+        log.error("Cannot validate Bitcoin address '%s'" % self.address)
+        raise
+    
+    #def on_new_block(self):
+    #    pass
+    
+    #def on_new_template(self):
+    #    pass
+    
+    def get_script_pubkey(self):
+        if not self.is_valid:
+            # Try again, maybe bitcoind was down?
+            self._validate()
+            raise Exception("Coinbase address is not validated!")
+        return util.script_to_address(self.address)    
+                   
+    def get_coinbase_data(self):
+        return ''
\ No newline at end of file
diff --git a/lib/coinbasetx.py b/lib/coinbasetx.py
new file mode 100644 (file)
index 0000000..11c7e36
--- /dev/null
@@ -0,0 +1,49 @@
+import binascii
+import halfnode
+import struct
+import util
+
+class CoinbaseTransaction(halfnode.CTransaction):
+    '''Construct special transaction used for coinbase tx.
+    It also implements quick serialization using pre-cached
+    scriptSig template.'''
+    
+    extranonce_type = '>Q'
+    extranonce_placeholder = struct.pack(extranonce_type, int('f000000ff111111f', 16))
+    extranonce_size = struct.calcsize(extranonce_type)
+
+    def __init__(self, timestamper, coinbaser, value, flags, height, data):
+        super(CoinbaseTransaction, self).__init__()
+        
+        #self.extranonce = 0
+        
+        if len(self.extranonce_placeholder) != self.extranonce_size:
+            raise Exception("Extranonce placeholder don't match expected length!")
+
+        tx_in = halfnode.CTxIn()
+        tx_in.prevout.hash = 0L
+        tx_in.prevout.n = 2**32-1
+        tx_in._scriptSig_template = (
+            util.ser_number(height) + binascii.unhexlify(flags) + util.ser_number(int(timestamper.time())) + \
+            chr(self.extranonce_size),
+            util.ser_string(coinbaser.get_coinbase_data() + data)
+        )
+                
+        tx_in.scriptSig = tx_in._scriptSig_template[0] + self.extranonce_placeholder + tx_in._scriptSig_template[1]
+    
+        tx_out = halfnode.CTxOut()
+        tx_out.nValue = value
+        tx_out.scriptPubKey = coinbaser.get_script_pubkey()
+        
+        self.vin.append(tx_in)
+        self.vout.append(tx_out)
+        
+        # Two parts of serialized coinbase, just put part1 + extranonce + part2 to have final serialized tx
+        self._serialized = super(CoinbaseTransaction, self).serialize().split(self.extranonce_placeholder)
+
+    def set_extranonce(self, extranonce):
+        if len(extranonce) != self.extranonce_size:
+            raise Exception("Incorrect extranonce size")
+        
+        (part1, part2) = self.vin[0]._scriptSig_template
+        self.vin[0].scriptSig = part1 + extranonce + part2
\ No newline at end of file
diff --git a/lib/extranonce_counter.py b/lib/extranonce_counter.py
new file mode 100644 (file)
index 0000000..19254f4
--- /dev/null
@@ -0,0 +1,25 @@
+import struct
+
+class ExtranonceCounter(object):
+    '''Implementation of a counter producing
+       unique extranonce across all pool instances.
+       This is just dumb "quick&dirty" solution,
+       but it can be changed at any time without breaking anything.'''       
+
+    def __init__(self, instance_id):
+        if instance_id < 0 or instance_id > 31:
+            raise Exception("Current ExtranonceCounter implementation needs an instance_id in <0, 31>.")
+        
+        # Last 5 most-significant bits represents instance_id
+        # The rest is just an iterator of jobs.
+        self.counter = instance_id << 27
+        self.size = struct.calcsize('>L')
+        
+    def get_size(self):
+        '''Return expected size of generated extranonce in bytes'''
+        return self.size
+    
+    def get_new_bin(self):
+        self.counter += 1
+        return struct.pack('>L', self.counter)
+        
\ No newline at end of file
diff --git a/lib/halfnode.py b/lib/halfnode.py
new file mode 100644 (file)
index 0000000..233a5c1
--- /dev/null
@@ -0,0 +1,523 @@
+#!/usr/bin/python
+# Public Domain
+# Original author: ArtForz
+# Twisted integration: slush
+
+import struct
+import socket
+import binascii
+import time
+import sys
+import random
+import cStringIO
+from Crypto.Hash import SHA256
+
+from twisted.internet.protocol import Protocol
+from util import *
+
+MY_VERSION = 31402
+MY_SUBVERSION = ".4"
+
+class CAddress(object):
+    def __init__(self):
+        self.nTime = 0
+        self.nServices = 1
+        self.pchReserved = "\x00" * 10 + "\xff" * 2
+        self.ip = "0.0.0.0"
+        self.port = 0
+    def deserialize(self, f):
+        #self.nTime = struct.unpack("<I", f.read(4))[0]
+        self.nServices = struct.unpack("<Q", f.read(8))[0]
+        self.pchReserved = f.read(12)
+        self.ip = socket.inet_ntoa(f.read(4))
+        self.port = struct.unpack(">H", f.read(2))[0]
+    def serialize(self):
+        r = ""
+        #r += struct.pack("<I", self.nTime)
+        r += struct.pack("<Q", self.nServices)
+        r += self.pchReserved
+        r += socket.inet_aton(self.ip)
+        r += struct.pack(">H", self.port)
+        return r
+    def __repr__(self):
+        return "CAddress(nServices=%i ip=%s port=%i)" % (self.nServices, self.ip, self.port)
+
+class CInv(object):
+    typemap = {
+        0: "Error",
+        1: "TX",
+        2: "Block"}
+    def __init__(self):
+        self.type = 0
+        self.hash = 0L
+    def deserialize(self, f):
+        self.type = struct.unpack("<i", f.read(4))[0]
+        self.hash = deser_uint256(f)
+    def serialize(self):
+        r = ""
+        r += struct.pack("<i", self.type)
+        r += ser_uint256(self.hash)
+        return r
+    def __repr__(self):
+        return "CInv(type=%s hash=%064x)" % (self.typemap[self.type], self.hash)
+
+class CBlockLocator(object):
+    def __init__(self):
+        self.nVersion = MY_VERSION
+        self.vHave = []
+    def deserialize(self, f):
+        self.nVersion = struct.unpack("<i", f.read(4))[0]
+        self.vHave = deser_uint256_vector(f)
+    def serialize(self):
+        r = ""
+        r += struct.pack("<i", self.nVersion)
+        r += ser_uint256_vector(self.vHave)
+        return r
+    def __repr__(self):
+        return "CBlockLocator(nVersion=%i vHave=%s)" % (self.nVersion, repr(self.vHave))
+
+class COutPoint(object):
+    def __init__(self):
+        self.hash = 0
+        self.n = 0
+    def deserialize(self, f):
+        self.hash = deser_uint256(f)
+        self.n = struct.unpack("<I", f.read(4))[0]
+    def serialize(self):
+        r = ""
+        r += ser_uint256(self.hash)
+        r += struct.pack("<I", self.n)
+        return r
+    def __repr__(self):
+        return "COutPoint(hash=%064x n=%i)" % (self.hash, self.n)
+
+class CTxIn(object):
+    def __init__(self):
+        self.prevout = COutPoint()
+        self.scriptSig = ""
+        self.nSequence = 0
+    def deserialize(self, f):
+        self.prevout = COutPoint()
+        self.prevout.deserialize(f)
+        self.scriptSig = deser_string(f)
+        self.nSequence = struct.unpack("<I", f.read(4))[0]
+    def serialize(self):
+        r = ""
+        r += self.prevout.serialize()
+        r += ser_string(self.scriptSig)
+        r += struct.pack("<I", self.nSequence)
+        return r
+    def __repr__(self):
+        return "CTxIn(prevout=%s scriptSig=%s nSequence=%i)" % (repr(self.prevout), binascii.hexlify(self.scriptSig), self.nSequence)
+
+class CTxOut(object):
+    def __init__(self):
+        self.nValue = 0
+        self.scriptPubKey = ""
+    def deserialize(self, f):
+        self.nValue = struct.unpack("<q", f.read(8))[0]
+        self.scriptPubKey = deser_string(f)
+    def serialize(self):
+        r = ""
+        r += struct.pack("<q", self.nValue)
+        r += ser_string(self.scriptPubKey)
+        return r
+    def __repr__(self):
+        return "CTxOut(nValue=%i.%08i scriptPubKey=%s)" % (self.nValue // 100000000, self.nValue % 100000000, binascii.hexlify(self.scriptPubKey))
+
+class CTransaction(object):
+    def __init__(self):
+        self.nVersion = 1
+        self.vin = []
+        self.vout = []
+        self.nLockTime = 0
+        self.sha256 = None
+    def deserialize(self, f):
+        self.nVersion = struct.unpack("<i", f.read(4))[0]
+        self.vin = deser_vector(f, CTxIn)
+        self.vout = deser_vector(f, CTxOut)
+        self.nLockTime = struct.unpack("<I", f.read(4))[0]
+        self.sha256 = None
+    def serialize(self):
+        r = ""
+        r += struct.pack("<i", self.nVersion)
+        r += ser_vector(self.vin)
+        r += ser_vector(self.vout)
+        r += struct.pack("<I", self.nLockTime)
+        return r
+    
+    def calc_sha256(self):
+        if self.sha256 is None:
+            self.sha256 = uint256_from_str(SHA256.new(SHA256.new(self.serialize()).digest()).digest())
+        return self.sha256
+    
+    def is_valid(self):
+        self.calc_sha256()
+        for tout in self.vout:
+            if tout.nValue < 0 or tout.nValue > 21000000L * 100000000L:
+                return False
+        return True
+    def __repr__(self):
+        return "CTransaction(nVersion=%i vin=%s vout=%s nLockTime=%i)" % (self.nVersion, repr(self.vin), repr(self.vout), self.nLockTime)
+
+class CBlock(object):
+    def __init__(self):
+        self.nVersion = 1
+        self.hashPrevBlock = 0
+        self.hashMerkleRoot = 0
+        self.nTime = 0
+        self.nBits = 0
+        self.nNonce = 0
+        self.vtx = []
+        self.sha256 = None
+    def deserialize(self, f):
+        self.nVersion = struct.unpack("<i", f.read(4))[0]
+        self.hashPrevBlock = deser_uint256(f)
+        self.hashMerkleRoot = deser_uint256(f)
+        self.nTime = struct.unpack("<I", f.read(4))[0]
+        self.nBits = struct.unpack("<I", f.read(4))[0]
+        self.nNonce = struct.unpack("<I", f.read(4))[0]
+        self.vtx = deser_vector(f, CTransaction)
+    def serialize(self):
+        r = []
+        r.append(struct.pack("<i", self.nVersion))
+        r.append(ser_uint256(self.hashPrevBlock))
+        r.append(ser_uint256(self.hashMerkleRoot))
+        r.append(struct.pack("<I", self.nTime))
+        r.append(struct.pack("<I", self.nBits))
+        r.append(struct.pack("<I", self.nNonce))
+        r.append(ser_vector(self.vtx))
+        return ''.join(r)
+    def calc_sha256(self):
+        if self.sha256 is None:
+            r = []
+            r.append(struct.pack("<i", self.nVersion))
+            r.append(ser_uint256(self.hashPrevBlock))
+            r.append(ser_uint256(self.hashMerkleRoot))
+            r.append(struct.pack("<I", self.nTime))
+            r.append(struct.pack("<I", self.nBits))
+            r.append(struct.pack("<I", self.nNonce))
+            self.sha256 = uint256_from_str(SHA256.new(SHA256.new(''.join(r)).digest()).digest())
+        return self.sha256
+
+    def is_valid(self):
+        self.calc_sha256()
+        target = uint256_from_compact(self.nBits)
+        if self.sha256 > target:
+            return False
+        hashes = []
+        for tx in self.vtx:
+            tx.sha256 = None
+            if not tx.is_valid():
+                return False
+            tx.calc_sha256()
+            hashes.append(ser_uint256(tx.sha256))
+        
+        while len(hashes) > 1:
+            newhashes = []
+            for i in xrange(0, len(hashes), 2):
+                i2 = min(i+1, len(hashes)-1)
+                newhashes.append(SHA256.new(SHA256.new(hashes[i] + hashes[i2]).digest()).digest())
+            hashes = newhashes
+        
+        if uint256_from_str(hashes[0]) != self.hashMerkleRoot:
+            return False
+        return True
+    def __repr__(self):
+        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))
+
+class msg_version(object):
+    command = "version"
+    def __init__(self):
+        self.nVersion = MY_VERSION
+        self.nServices = 0
+        self.nTime = time.time()
+        self.addrTo = CAddress()
+        self.addrFrom = CAddress()
+        self.nNonce = random.getrandbits(64)
+        self.strSubVer = MY_SUBVERSION
+        self.nStartingHeight = 0
+        
+    def deserialize(self, f):
+        self.nVersion = struct.unpack("<i", f.read(4))[0]
+        if self.nVersion == 10300:
+            self.nVersion = 300
+        self.nServices = struct.unpack("<Q", f.read(8))[0]
+        self.nTime = struct.unpack("<q", f.read(8))[0]
+        self.addrTo = CAddress()
+        self.addrTo.deserialize(f)
+        self.addrFrom = CAddress()
+        self.addrFrom.deserialize(f)
+        self.nNonce = struct.unpack("<Q", f.read(8))[0]
+        self.strSubVer = deser_string(f)
+        self.nStartingHeight = struct.unpack("<i", f.read(4))[0]
+    def serialize(self):
+        r = []
+        r.append(struct.pack("<i", self.nVersion))
+        r.append(struct.pack("<Q", self.nServices))
+        r.append(struct.pack("<q", self.nTime))
+        r.append(self.addrTo.serialize())
+        r.append(self.addrFrom.serialize())
+        r.append(struct.pack("<Q", self.nNonce))
+        r.append(ser_string(self.strSubVer))
+        r.append(struct.pack("<i", self.nStartingHeight))
+        return ''.join(r)
+    def __repr__(self):
+        return "msg_version(nVersion=%i nServices=%i nTime=%s addrTo=%s addrFrom=%s nNonce=0x%016X strSubVer=%s nStartingHeight=%i)" % (self.nVersion, self.nServices, time.ctime(self.nTime), repr(self.addrTo), repr(self.addrFrom), self.nNonce, self.strSubVer, self.nStartingHeight)
+
+class msg_verack(object):
+    command = "verack"
+    def __init__(self):
+        pass
+    def deserialize(self, f):
+        pass
+    def serialize(self):
+        return ""
+    def __repr__(self):
+        return "msg_verack()"
+
+class msg_addr(object):
+    command = "addr"
+    def __init__(self):
+        self.addrs = []
+    def deserialize(self, f):
+        self.addrs = deser_vector(f, CAddress)
+    def serialize(self):
+        return ser_vector(self.addrs)
+    def __repr__(self):
+        return "msg_addr(addrs=%s)" % (repr(self.addrs))
+
+class msg_inv(object):
+    command = "inv"
+    def __init__(self):
+        self.inv = []
+    def deserialize(self, f):
+        self.inv = deser_vector(f, CInv)
+    def serialize(self):
+        return ser_vector(self.inv)
+    def __repr__(self):
+        return "msg_inv(inv=%s)" % (repr(self.inv))
+
+class msg_getdata(object):
+    command = "getdata"
+    def __init__(self):
+        self.inv = []
+    def deserialize(self, f):
+        self.inv = deser_vector(f, CInv)
+    def serialize(self):
+        return ser_vector(self.inv)
+    def __repr__(self):
+        return "msg_getdata(inv=%s)" % (repr(self.inv))
+
+class msg_getblocks(object):
+    command = "getblocks"
+    def __init__(self):
+        self.locator = CBlockLocator()
+        self.hashstop = 0L
+    def deserialize(self, f):
+        self.locator = CBlockLocator()
+        self.locator.deserialize(f)
+        self.hashstop = deser_uint256(f)
+    def serialize(self):
+        r = []
+        r.append(self.locator.serialize())
+        r.append(ser_uint256(self.hashstop))
+        return ''.join(r)
+    def __repr__(self):
+        return "msg_getblocks(locator=%s hashstop=%064x)" % (repr(self.locator), self.hashstop)
+
+class msg_tx(object):
+    command = "tx"
+    def __init__(self):
+        self.tx = CTransaction()
+    def deserialize(self, f):
+        self.tx.deserialize(f)
+    def serialize(self):
+        return self.tx.serialize()
+    def __repr__(self):
+        return "msg_tx(tx=%s)" % (repr(self.tx))
+
+class msg_block(object):
+    command = "block"
+    def __init__(self):
+        self.block = CBlock()
+    def deserialize(self, f):
+        self.block.deserialize(f)
+    def serialize(self):
+        return self.block.serialize()
+    def __repr__(self):
+        return "msg_block(block=%s)" % (repr(self.block))
+
+class msg_getaddr(object):
+    command = "getaddr"
+    def __init__(self):
+        pass
+    def deserialize(self, f):
+        pass
+    def serialize(self):
+        return ""
+    def __repr__(self):
+        return "msg_getaddr()"
+
+class msg_ping(object):
+    command = "ping"
+    def __init__(self):
+        pass
+    def deserialize(self, f):
+        pass
+    def serialize(self):
+        return ""
+    def __repr__(self):
+        return "msg_ping()"
+
+class msg_alert(object):
+    command = "alert"
+    def __init__(self):
+        pass
+    def deserialize(self, f):
+        pass
+    def serialize(self):
+        return ""
+    def __repr__(self):
+        return "msg_alert()"
+    
+class BitcoinP2PProtocol(Protocol):
+    messagemap = {
+        "version": msg_version,
+        "verack": msg_verack,
+        "addr": msg_addr,
+        "inv": msg_inv,
+        "getdata": msg_getdata,
+        "getblocks": msg_getblocks,
+        "tx": msg_tx,
+        "block": msg_block,
+        "getaddr": msg_getaddr,
+        "ping": msg_ping,
+        "alert": msg_alert,
+    }
+   
+    def connectionMade(self):
+        peer = self.transport.getPeer()
+        self.dstaddr = peer.host
+        self.dstport = peer.port
+        self.recvbuf = ""
+        self.last_sent = 0
+        t = msg_version()
+        t.nStartingHeight = getattr(self, 'nStartingHeight', 0)
+        t.addrTo.ip = self.dstaddr
+        t.addrTo.port = self.dstport
+        t.addrTo.nTime = time.time()
+        t.addrFrom.ip = "0.0.0.0"
+        t.addrFrom.port = 0
+        t.addrFrom.nTime = time.time()
+        self.send_message(t)
+        
+    def dataReceived(self, data):
+        self.recvbuf += data
+        self.got_data()
+        
+    def got_data(self):
+        while True:
+            if len(self.recvbuf) < 4:
+                return
+            if self.recvbuf[:4] != "\xf9\xbe\xb4\xd9":
+                raise ValueError("got garbage %s" % repr(self.recvbuf))
+
+            if len(self.recvbuf) < 4 + 12 + 4 + 4:
+                return
+            command = self.recvbuf[4:4+12].split("\x00", 1)[0]
+            msglen = struct.unpack("<i", self.recvbuf[4+12:4+12+4])[0]
+            checksum = self.recvbuf[4+12+4:4+12+4+4]
+            if len(self.recvbuf) < 4 + 12 + 4 + 4 + msglen:
+                return
+            msg = self.recvbuf[4+12+4+4:4+12+4+4+msglen]
+            th = SHA256.new(msg).digest()
+            h = SHA256.new(th).digest()
+            if checksum != h[:4]:
+                raise ValueError("got bad checksum %s" % repr(self.recvbuf))
+            self.recvbuf = self.recvbuf[4+12+4+4+msglen:]
+
+            if command in self.messagemap:
+                f = cStringIO.StringIO(msg)
+                t = self.messagemap[command]()
+                t.deserialize(f)
+                self.got_message(t)
+            else:
+                print "UNKNOWN COMMAND", command, repr(msg)
+                
+    def prepare_message(self, message):       
+        command = message.command
+        data = message.serialize()
+        tmsg = "\xf9\xbe\xb4\xd9"
+        tmsg += command
+        tmsg += "\x00" * (12 - len(command))
+        tmsg += struct.pack("<I", len(data))
+        th = SHA256.new(data).digest()
+        h = SHA256.new(th).digest()
+        tmsg += h[:4]
+        tmsg += data
+        return tmsg
+    
+    def send_serialized_message(self, tmsg):
+        if not self.connected:
+            return
+        
+        self.transport.write(tmsg)
+        self.last_sent = time.time()       
+        
+    def send_message(self, message):
+        if not self.connected:
+            return
+        
+        #print message.command
+        
+        #print "send %s" % repr(message)
+        command = message.command
+        data = message.serialize()
+        tmsg = "\xf9\xbe\xb4\xd9"
+        tmsg += command
+        tmsg += "\x00" * (12 - len(command))
+        tmsg += struct.pack("<I", len(data))
+        th = SHA256.new(data).digest()
+        h = SHA256.new(th).digest()
+        tmsg += h[:4]
+        tmsg += data
+        
+        #print tmsg, len(tmsg)
+        self.transport.write(tmsg)
+        self.last_sent = time.time()
+        
+    def got_message(self, message):
+        if self.last_sent + 30 * 60 < time.time():
+            self.send_message(msg_ping())
+
+        mname = 'do_' + message.command
+        #print mname
+        if not hasattr(self, mname):
+            return
+
+        method = getattr(self, mname)
+        method(message)
+
+#        if message.command == "tx":
+#            message.tx.calc_sha256()
+#            sha256 = message.tx.sha256
+#            pubkey = binascii.hexlify(message.tx.vout[0].scriptPubKey)
+#            txlock.acquire()
+#            tx.append([str(sha256), str(time.time()), str(self.dstaddr), pubkey])
+#            txlock.release()
+
+    def do_version(self, message):
+        #print message
+        self.send_message(msg_verack())
+
+    def do_inv(self, message):
+        want = msg_getdata()
+        for i in message.inv:
+            if i.type == 1:
+                want.inv.append(i)
+            if i.type == 2:
+                want.inv.append(i)
+        if len(want.inv):
+            self.send_message(want)
diff --git a/lib/merkletree.py b/lib/merkletree.py
new file mode 100644 (file)
index 0000000..8233019
--- /dev/null
@@ -0,0 +1,103 @@
+# Eloipool - Python Bitcoin pool server
+# Copyright (C) 2011-2012  Luke Dashjr <luke-jr+eloipool@utopios.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from hashlib import sha256
+from util import doublesha
+
+class MerkleTree:
+    def __init__(self, data, detailed=False):
+        self.data = data
+        self.recalculate(detailed)
+        self._hash_steps = None
+    
+    def recalculate(self, detailed=False):
+        L = self.data
+        steps = []
+        if detailed:
+            detail = []
+            PreL = []
+            StartL = 0
+        else:
+            detail = None
+            PreL = [None]
+            StartL = 2
+        Ll = len(L)
+        if detailed or Ll > 1:
+            while True:
+                if detailed:
+                    detail += L
+                if Ll == 1:
+                    break
+                steps.append(L[1])
+                if Ll % 2:
+                    L += [L[-1]]
+                L = PreL + [doublesha(L[i] + L[i + 1]) for i in range(StartL, Ll, 2)]
+                Ll = len(L)
+        self._steps = steps
+        self.detail = detail
+    
+    def hash_steps(self):
+        if self._hash_steps == None:
+            self._hash_steps = doublesha(''.join(self._steps))
+        return self._hash_steps
+        
+    def withFirst(self, f):
+        steps = self._steps
+        for s in steps:
+            f = doublesha(f + s)
+        return f
+    
+    def merkleRoot(self):
+        return self.withFirst(self.data[0])
+
+# MerkleTree tests
+def _test():
+    import binascii
+    import time    
+        
+    mt = MerkleTree([None] + [binascii.unhexlify(a) for a in [
+        '999d2c8bb6bda0bf784d9ebeb631d711dbbbfe1bc006ea13d6ad0d6a2649a971',
+        '3f92594d5a3d7b4df29d7dd7c46a0dac39a96e751ba0fc9bab5435ea5e22a19d',
+        'a5633f03855f541d8e60a6340fc491d49709dc821f3acb571956a856637adcb6',
+        '28d97c850eaf917a4c76c02474b05b70a197eaefb468d21c22ed110afe8ec9e0',
+    ]])
+    assert(
+        b'82293f182d5db07d08acf334a5a907012bbb9990851557ac0ec028116081bd5a' ==
+        binascii.b2a_hex(mt.withFirst(binascii.unhexlify('d43b669fb42cfa84695b844c0402d410213faa4f3e66cb7248f688ff19d5e5f7')))
+    )
+    
+    print '82293f182d5db07d08acf334a5a907012bbb9990851557ac0ec028116081bd5a'
+    txes = [binascii.unhexlify(a) for a in [
+        'd43b669fb42cfa84695b844c0402d410213faa4f3e66cb7248f688ff19d5e5f7',
+        '999d2c8bb6bda0bf784d9ebeb631d711dbbbfe1bc006ea13d6ad0d6a2649a971',
+        '3f92594d5a3d7b4df29d7dd7c46a0dac39a96e751ba0fc9bab5435ea5e22a19d',
+        'a5633f03855f541d8e60a6340fc491d49709dc821f3acb571956a856637adcb6',
+        '28d97c850eaf917a4c76c02474b05b70a197eaefb468d21c22ed110afe8ec9e0',
+    ]]
+             
+    s = time.time()
+    mt = MerkleTree(txes)
+    for x in range(100):
+        y = int('d43b669fb42cfa84695b844c0402d410213faa4f3e66cb7248f688ff19d5e5f7', 16)
+        #y += x
+        coinbasehash = binascii.unhexlify("%x" % y)
+        x = binascii.b2a_hex(mt.withFirst(coinbasehash))
+
+    print x
+    print time.time() - s
+
+if __name__ == '__main__':
+    _test()
\ No newline at end of file
diff --git a/lib/template_registry.py b/lib/template_registry.py
new file mode 100644 (file)
index 0000000..4d3f061
--- /dev/null
@@ -0,0 +1,262 @@
+import weakref
+import binascii
+import util
+import StringIO
+
+import stratum.logger
+log = stratum.logger.get_logger('template_registry')
+
+from mining.interfaces import Interfaces
+from extranonce_counter import ExtranonceCounter
+
+class JobIdGenerator(object):
+    '''Generate pseudo-unique job_id. It does not need to be absolutely unique,
+    because pool sends "clean_jobs" flag to clients and they should drop all previous jobs.'''
+    counter = 0
+    
+    @classmethod
+    def get_new_id(cls):
+        cls.counter += 1
+        if cls.counter % 0xffff == 0:
+            cls.counter = 1
+        return "%x" % cls.counter
+                
+class TemplateRegistry(object):
+    '''Implements the main logic of the pool. Keep track
+    on valid block templates, provide internal interface for stratum
+    service and implements block validation and submits.'''
+    
+    def __init__(self, block_template_class, coinbaser, bitcoin_rpc, instance_id,
+                 on_block_callback):
+        self.prevhashes = {}
+        self.jobs = weakref.WeakValueDictionary()
+        
+        self.extranonce_counter = ExtranonceCounter(instance_id)
+        self.extranonce2_size = block_template_class.coinbase_transaction_class.extranonce_size \
+                - self.extranonce_counter.get_size()
+                 
+        self.coinbaser = coinbaser
+        self.block_template_class = block_template_class
+        self.bitcoin_rpc = bitcoin_rpc
+        self.on_block_callback = on_block_callback
+        
+        self.last_block = None
+        self.update_in_progress = False
+        self.last_update = None
+        
+        # Create first block template on startup
+        self.update_block()
+        
+    def get_new_extranonce1(self):
+        '''Generates unique extranonce1 (e.g. for newly
+        subscribed connection.'''
+        return self.extranonce_counter.get_new_bin()
+    
+    def get_last_broadcast_args(self):
+        '''Returns arguments for mining.notify
+        from last known template.'''
+        return self.last_block.broadcast_args
+        
+    def add_template(self, block):
+        '''Adds new template to the registry.
+        It also clean up templates which should
+        not be used anymore.'''
+        
+        prevhash = block.prevhash_hex
+
+        if prevhash in self.prevhashes.keys():
+            new_block = False
+        else:
+            new_block = True
+            self.prevhashes[prevhash] = []
+               
+        # Blocks sorted by prevhash, so it's easy to drop
+        # them on blockchain update
+        self.prevhashes[prevhash].append(block)
+        
+        # Weak reference for fast lookup using job_id
+        self.jobs[block.job_id] = block
+        
+        # Use this template for every new request
+        self.last_block = block
+        
+        # Drop templates of obsolete blocks
+        for ph in self.prevhashes.keys():
+            if ph != prevhash:
+                del self.prevhashes[ph]
+                
+        log.info("New template for %s" % prevhash)
+        self.on_block_callback(new_block)
+        #from twisted.internet import reactor
+        #reactor.callLater(10, self.on_block_callback, new_block) 
+              
+    def update_block(self):
+        '''Registry calls the getblocktemplate() RPC
+        and build new block template.'''
+        
+        if self.update_in_progress:
+            # Block has been already detected
+            return
+        
+        self.update_in_progress = True
+        self.last_update = Interfaces.timestamper.time()
+        
+        d = self.bitcoin_rpc.getblocktemplate()
+        d.addCallback(self._update_block)
+        d.addErrback(self._update_block_failed)
+        
+    def _update_block_failed(self, failure):
+        log.error(str(failure))
+        self.update_in_progress = False
+        
+    def _update_block(self, data):
+        start = Interfaces.timestamper.time()
+                
+        template = self.block_template_class(Interfaces.timestamper, self.coinbaser, JobIdGenerator.get_new_id())
+        template.fill_from_rpc(data)
+        self.add_template(template)
+
+        log.info("Update finished, %.03f sec, %d txes" % \
+                    (Interfaces.timestamper.time() - start, len(template.vtx)))
+        
+        self.update_in_progress = False        
+        return data
+    
+    def diff_to_target(self, difficulty):
+        '''Converts difficulty to target'''
+        diff1 = 0x00000000ffff0000000000000000000000000000000000000000000000000000 
+        return diff1 / difficulty
+    
+    def get_job(self, job_id):
+        '''For given job_id returns BlockTemplate instance or None'''
+        try:
+            j = self.jobs[job_id]
+        except:
+            log.info("Job id '%s' not found" % job_id)
+            return False
+        
+        # Now we have to check if job is still valid.
+        # Unfortunately weak references are not bulletproof and
+        # old reference can be found until next run of garbage collector.
+        if j.prevhash_hex not in self.prevhashes:
+            log.info("Prevhash of job '%s' is unknown" % job_id)
+            return False
+        
+        if j not in self.prevhashes[j.prevhash_hex]:
+            log.info("Job %s is unknown" % job_id)
+            return False
+        
+        return True
+        
+    def submit_share(self, job_id, worker_name, extranonce1_bin, extranonce2, ntime, nonce,
+                     difficulty, submitblock_callback):
+        '''Check parameters and finalize block template. If it leads
+           to valid block candidate, asynchronously submits the block
+           back to the bitcoin network.
+        
+            - extranonce1_bin is binary. No checks performed, it should be from session data
+            - job_id, extranonce2, ntime, nonce - in hex form sent by the client
+            - difficulty - decimal number from session, again no checks performed
+            - submitblock_callback - reference to method which receive result of submitblock()
+        '''
+        
+        # Check if extranonce2 looks correctly. extranonce2 is in hex form...
+        if len(extranonce2) != self.extranonce2_size * 2:
+            return (False, "Incorrect size of extranonce2. Expected %d chars" % (self.extranonce2_size*2), None, None)
+        
+        # Check for job
+        if not self.get_job(job_id):
+            return (False, "Job '%s' not found" % job_id, None, None)
+            
+        try:
+            job = self.jobs[job_id]
+        except KeyError:
+            return (False, "Job '%s' not found" % job_id, None, None)
+                
+        # Check if ntime looks correct
+        if len(ntime) != 8:
+            return (False, "Incorrect size of ntime. Expected 8 chars", None, None)
+
+        if not job.check_ntime(int(ntime, 16)):
+            return (False, "Ntime out of range", None, None)
+        
+        # Check nonce        
+        if len(nonce) != 8:
+            return (False, "Incorrect size of nonce. Expected 8 chars", None, None)
+        
+        # Check for duplicated submit
+        if not job.register_submit(extranonce1_bin, extranonce2, ntime, nonce):
+            log.info("Duplicate from %s, (%s %s %s %s)" % \
+                    (worker_name, binascii.hexlify(extranonce1_bin), extranonce2, ntime, nonce))
+            return (False, "Duplicate share", None, None)
+        
+        # Now let's do the hard work!
+        # ---------------------------
+        
+        # 0. Some sugar
+        extranonce2_bin = binascii.unhexlify(extranonce2)
+        ntime_bin = binascii.unhexlify(ntime)
+        nonce_bin = binascii.unhexlify(nonce)
+                
+        # 1. Build coinbase
+        coinbase_bin = job.serialize_coinbase(extranonce1_bin, extranonce2_bin)
+        coinbase_hash = util.doublesha(coinbase_bin)
+        
+        # 2. Calculate merkle root
+        merkle_root_bin = job.merkletree.withFirst(coinbase_hash)
+        merkle_root_int = util.uint256_from_str(merkle_root_bin)
+                
+        # 3. Serialize header with given merkle, ntime and nonce
+        header_bin = job.serialize_header(merkle_root_int, ntime_bin, nonce_bin)
+    
+        # 4. Reverse header and compare it with target of the user
+        hash_bin = util.doublesha(''.join([ header_bin[i*4:i*4+4][::-1] for i in range(0, 20) ]))
+        hash_int = util.uint256_from_str(hash_bin)
+        block_hash_hex = "%064x" % hash_int
+        header_hex = binascii.hexlify(header_bin)
+                 
+        target = self.diff_to_target(difficulty)
+        
+        if hash_int > target:
+            return (False, "Share is above target", None, None)
+
+        # 5. Compare hash with target of the network        
+        if hash_int <= job.target:
+            # Yay! It is block candidate! 
+            log.info("We found a block candidate! %s" % block_hash_hex)
+           
+            # 6. Finalize and serialize block object 
+            job.finalize(merkle_root_int, extranonce1_bin, extranonce2_bin, int(ntime, 16), int(nonce, 16))
+            
+            if not job.is_valid():
+                # Should not happen
+                log.error("Final job validation failed!")
+                return (False, 'Job validation failed', header_hex, block_hash_hex)
+                            
+            # 7. Submit block to the network
+            submit_time = Interfaces.timestamper.time()
+            serialized = binascii.hexlify(job.serialize())
+            d = self.bitcoin_rpc.submitblock(serialized)
+            
+            # Submit is lazy, we don't need to wait for the result
+            # Callback will just register success or failure to share manager
+            d.addCallback(self._on_submitblock, submitblock_callback,
+                          worker_name, header_hex, block_hash_hex, submit_time)
+            d.addErrback(self._on_submitblock_failure, submitblock_callback,
+                         worker_name, header_hex, block_hash_hex, submit_time)
+            
+            return (True, '', header_hex, block_hash_hex)
+        
+        return (True, '', header_hex, block_hash_hex)
+    
+    def _on_submitblock(self, is_accepted, callback, worker_name, block_header, block_hash, timestamp):
+        '''Helper method, bridges call from deferred to method reference given in submit()'''
+        # Forward submitblock result to share manager
+        callback(worker_name, block_header, block_hash, timestamp, is_accepted)
+        return is_accepted
+    
+    def _on_submitblock_failure(self, failure, callback, worker_name, block_header, block_hash, timestamp):
+        '''Helper method, bridges call from deferred to method reference given in submit()'''
+        # Forward submitblock failure to share manager
+        callback(worker_name, block_header, block_hash, timestamp, False)
+        log.exception(failure)
\ No newline at end of file
diff --git a/lib/util.py b/lib/util.py
new file mode 100644 (file)
index 0000000..3e88fd8
--- /dev/null
@@ -0,0 +1,201 @@
+'''Various helper methods. It probably needs some cleanup.'''
+
+import struct
+import StringIO
+import binascii
+from hashlib import sha256
+
+def deser_string(f):
+    nit = struct.unpack("<B", f.read(1))[0]
+    if nit == 253:
+        nit = struct.unpack("<H", f.read(2))[0]
+    elif nit == 254:
+        nit = struct.unpack("<I", f.read(4))[0]
+    elif nit == 255:
+        nit = struct.unpack("<Q", f.read(8))[0]
+    return f.read(nit)
+
+def ser_string(s):
+    if len(s) < 253:
+        return chr(len(s)) + s
+    elif len(s) < 0x10000:
+        return chr(253) + struct.pack("<H", len(s)) + s
+    elif len(s) < 0x100000000L:
+        return chr(254) + struct.pack("<I", len(s)) + s
+    return chr(255) + struct.pack("<Q", len(s)) + s
+
+def deser_uint256(f):
+    r = 0L
+    for i in xrange(8):
+        t = struct.unpack("<I", f.read(4))[0]
+        r += t << (i * 32)
+    return r
+
+def ser_uint256(u):
+    rs = ""
+    for i in xrange(8):
+        rs += struct.pack("<I", u & 0xFFFFFFFFL)
+        u >>= 32
+    return rs
+
+def uint256_from_str(s):
+    r = 0L
+    t = struct.unpack("<IIIIIIII", s[:32])
+    for i in xrange(8):
+        r += t[i] << (i * 32)
+    return r
+
+def uint256_from_str_be(s):
+    r = 0L
+    t = struct.unpack(">IIIIIIII", s[:32])
+    for i in xrange(8):
+        r += t[i] << (i * 32)
+    return r
+
+def uint256_from_compact(c):
+    nbytes = (c >> 24) & 0xFF
+    v = (c & 0xFFFFFFL) << (8 * (nbytes - 3))
+    return v
+
+def deser_vector(f, c):
+    nit = struct.unpack("<B", f.read(1))[0]
+    if nit == 253:
+        nit = struct.unpack("<H", f.read(2))[0]
+    elif nit == 254:
+        nit = struct.unpack("<I", f.read(4))[0]
+    elif nit == 255:
+        nit = struct.unpack("<Q", f.read(8))[0]
+    r = []
+    for i in xrange(nit):
+        t = c()
+        t.deserialize(f)
+        r.append(t)
+    return r
+
+def ser_vector(l):
+    r = ""
+    if len(l) < 253:
+        r = chr(len(l))
+    elif len(l) < 0x10000:
+        r = chr(253) + struct.pack("<H", len(l))
+    elif len(l) < 0x100000000L:
+        r = chr(254) + struct.pack("<I", len(l))
+    else:
+        r = chr(255) + struct.pack("<Q", len(l))
+    for i in l:
+        r += i.serialize()
+    return r
+
+def deser_uint256_vector(f):
+    nit = struct.unpack("<B", f.read(1))[0]
+    if nit == 253:
+        nit = struct.unpack("<H", f.read(2))[0]
+    elif nit == 254:
+        nit = struct.unpack("<I", f.read(4))[0]
+    elif nit == 255:
+        nit = struct.unpack("<Q", f.read(8))[0]
+    r = []
+    for i in xrange(nit):
+        t = deser_uint256(f)
+        r.append(t)
+    return r
+
+def ser_uint256_vector(l):
+    r = ""
+    if len(l) < 253:
+        r = chr(len(l))
+    elif len(l) < 0x10000:
+        r = chr(253) + struct.pack("<H", len(l))
+    elif len(l) < 0x100000000L:
+        r = chr(254) + struct.pack("<I", len(l))
+    else:
+        r = chr(255) + struct.pack("<Q", len(l))
+    for i in l:
+        r += ser_uint256(i)
+    return r
+
+__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
+__b58base = len(__b58chars)
+
+def b58decode(v, length):
+    """ decode v into a string of len bytes
+    """
+    long_value = 0L
+    for (i, c) in enumerate(v[::-1]):
+        long_value += __b58chars.find(c) * (__b58base**i)
+
+    result = ''
+    while long_value >= 256:
+        div, mod = divmod(long_value, 256)
+        result = chr(mod) + result
+        long_value = div
+    result = chr(long_value) + result
+
+    nPad = 0
+    for c in v:
+        if c == __b58chars[0]: nPad += 1
+        else: break
+
+    result = chr(0)*nPad + result
+    if length is not None and len(result) != length:
+        return None
+    
+    return result
+
+def reverse_hash(h):
+    # This only revert byte order, nothing more
+    if len(h) != 64:
+        raise Exception('hash must have 64 hexa chars')
+    
+    return ''.join([ h[56-i:64-i] for i in range(0, 64, 8) ])
+
+def doublesha(b):
+    return sha256(sha256(b).digest()).digest()
+
+def bits_to_target(bits):
+    return struct.unpack('<L', bits[:3] + b'\0')[0] * 2**(8*(int(bits[3], 16) - 3))
+
+def address_to_pubkeyhash(addr):
+    try:
+        addr = b58decode(addr, 25)
+    except:
+        return None
+    
+    if addr is None:
+        return None
+    
+    ver = addr[0]
+    cksumA = addr[-4:]
+    cksumB = doublesha(addr[:-4])[:4]
+    
+    if cksumA != cksumB:
+        return None
+    
+    return (ver, addr[1:-4])
+
+def ser_uint256_be(u):
+    '''ser_uint256 to big endian'''
+    rs = ""
+    for i in xrange(8):
+        rs += struct.pack(">I", u & 0xFFFFFFFFL)
+        u >>= 32
+    return rs    
+
+def deser_uint256_be(f):
+    r = 0L
+    for i in xrange(8):
+        t = struct.unpack(">I", f.read(4))[0]
+        r += t << (i * 32)
+    return r
+
+def ser_number(num):
+    # For encoding nHeight into coinbase
+    d = struct.pack("<I", num).rstrip("\x00")
+    return chr(len(d)) + d
+    
+def script_to_address(addr):
+    d = address_to_pubkeyhash(addr)
+    if not d:
+        raise ValueError('invalid address')
+    (ver, pubkeyhash) = d
+    return b'\x76\xa9\x14' + pubkeyhash + b'\x88\xac'
\ No newline at end of file
diff --git a/mining/__init__.py b/mining/__init__.py
new file mode 100644 (file)
index 0000000..4709e14
--- /dev/null
@@ -0,0 +1,38 @@
+from service import MiningService
+from subscription import MiningSubscription
+
+def setup():
+    '''Setup mining service internal environment.
+    You should not need to change this. If you
+    want to use another Worker manager or Share manager,
+    you should set proper reference to Interfaces class
+    *before* you call setup() in the launcher script.'''
+        
+    from stratum import settings
+    from interfaces import Interfaces
+    
+    from lib.block_updater import BlockUpdater
+    from lib.template_registry import TemplateRegistry
+    from lib.bitcoin_rpc import BitcoinRPC
+    from lib.block_template import BlockTemplate
+    from lib.coinbaser import SimpleCoinbaser
+    
+    bitcoin_rpc = BitcoinRPC(settings.BITCOIN_TRUSTED_HOST,
+                             settings.BITCOIN_TRUSTED_PORT,
+                             settings.BITCOIN_TRUSTED_USER,
+                             settings.BITCOIN_TRUSTED_PASSWORD)
+    
+    registry = TemplateRegistry(BlockTemplate,
+                                SimpleCoinbaser(bitcoin_rpc, settings.CENTRAL_WALLET),
+                                bitcoin_rpc,
+                                settings.INSTANCE_ID,
+                                MiningSubscription.on_block)
+
+    # Template registry is the main interface between Stratum service
+    # and pool core logic
+    Interfaces.set_template_registry(registry)
+    
+    # Set up polling mechanism for detecting new block on the network
+    # This is just failsafe solution when -blocknotify
+    # mechanism is not working properly    
+    BlockUpdater(registry, bitcoin_rpc)
\ No newline at end of file
diff --git a/mining/interfaces.py b/mining/interfaces.py
new file mode 100644 (file)
index 0000000..812f0d0
--- /dev/null
@@ -0,0 +1,58 @@
+'''This module contains classes used by pool core to interact with the rest of the pool.
+   Default implementation do almost nothing, you probably want to override these classes
+   and customize references to interface instances in your launcher.
+   (see launcher_demo.tac for an example).
+''' 
+
+import time
+
+import stratum.logger
+log = stratum.logger.get_logger('interfaces')
+
+class WorkerManagerInterface(object):
+    def authorize(self, worker_name, worker_password):
+        return True
+
+class ShareManagerInterface(object):
+    def on_submit_share(self, worker_name, block_header, block_hash, shares, timestamp, is_valid):
+        log.info("%s %s %s" % ('Valid' if is_valid else 'INVALID', worker_name, block_hash))
+    
+    def on_submit_block(self, worker_name, block_header, block_hash, timestamp, is_accepted):
+        log.info("Block %s %s" % (block_hash, 'ACCEPTED' if is_accepted else 'REJECTED'))
+    
+class TimestamperInterface(object):
+    '''This is the only source for current time in the application.
+    Override this for generating unix timestamp in different way.'''
+    def time(self):
+        return time.time()
+
+class PredictableTimestamperInterface(TimestamperInterface):
+    '''Predictable timestamper may be useful for unit testing.'''
+    start_time = 1345678900 # Some day in year 2012
+    delta = 0
+    
+    def time(self):
+        self.delta += 1
+        return self.start_time + self.delta
+        
+class Interfaces(object):
+    worker_manager = None
+    share_manager = None
+    timestamper = None
+    template_registry = None
+    
+    @classmethod
+    def set_worker_manager(cls, manager):
+        cls.worker_manager = manager    
+    
+    @classmethod        
+    def set_share_manager(cls, manager):
+        cls.share_manager = manager
+    
+    @classmethod
+    def set_timestamper(cls, manager):
+        cls.timestamper = manager
+        
+    @classmethod
+    def set_template_registry(cls, registry):
+        cls.template_registry = registry
\ No newline at end of file
diff --git a/mining/service.py b/mining/service.py
new file mode 100644 (file)
index 0000000..f3a6861
--- /dev/null
@@ -0,0 +1,118 @@
+import binascii
+from twisted.internet import defer
+
+from stratum.services import GenericService, admin
+from stratum.custom_exceptions import ServiceException
+from stratum.pubsub import Pubsub
+from interfaces import Interfaces
+from subscription import MiningSubscription
+
+import stratum.logger
+log = stratum.logger.get_logger('mining')
+
+class SubmitException(ServiceException):
+    pass
+                
+class MiningService(GenericService):
+    '''This service provides public API for Stratum mining proxy
+    or any Stratum-compatible miner software.
+    
+    Warning - any callable argument of this class will be propagated
+    over Stratum protocol for public audience!'''
+    
+    service_type = 'mining'
+    service_vendor = 'stratum'
+    is_default = True
+    
+    @admin
+    def update_block(self):
+        '''Connect this RPC call to 'bitcoind -blocknotify' for 
+        instant notification about new block on the network.
+        See blocknotify.sh in /scripts/ for more info.'''
+        Interfaces.template_registry.update_block()
+        return True 
+    
+    def authorize(self, worker_name, worker_password):
+        '''Let authorize worker on this connection.'''
+        return Interfaces.worker_manager.authorize(worker_name, worker_password)
+        
+    def subscribe(self):
+        '''Subscribe for receiving mining jobs. This will
+        return subscription details, extranonce1_hex and extranonce2_size'''
+        
+        extranonce1 = Interfaces.template_registry.get_new_extranonce1()
+        extranonce2_size = Interfaces.template_registry.extranonce2_size
+
+        session = self.connection_ref().get_session()
+        session['extranonce1'] = extranonce1
+        session['difficulty'] = 1 # Following protocol specs, default diff is 1
+
+        extranonce1_hex = binascii.hexlify(extranonce1)
+            
+        return Pubsub.subscribe(self.connection_ref(), MiningSubscription()) + (extranonce1_hex, extranonce2_size)
+    
+    '''    
+    def submit(self, worker_name, job_id, extranonce2, ntime, nonce):
+        import time
+        start = time.time()
+        
+        for x in range(100):
+            try:
+                ret = self.submit2(worker_name, job_id, extranonce2, ntime, nonce)
+            except:
+                pass
+            
+        log.info("LEN %.03f" % (time.time() - start))
+        return ret
+    '''
+    
+    def submit(self, worker_name, job_id, extranonce2, ntime, nonce):
+        '''Try to solve block candidate using given parameters.'''
+        
+        session = self.connection_ref().get_session()
+
+        # Check if extranonce1 is in connection session
+        extranonce1_bin = session.get('extranonce1', None)
+        if not extranonce1_bin:
+            raise SubmitException("Connection is not subscribed for mining")
+        
+        difficulty = session['difficulty']
+
+        # This checks if submitted share meet all requirements
+        # and it is valid proof of work.
+        (is_valid, reason, block_header, block_hash) = Interfaces.template_registry.submit_share(job_id,
+                                                worker_name, extranonce1_bin, extranonce2, ntime, nonce, difficulty,
+                                                Interfaces.share_manager.on_submit_block)
+        
+        if block_header != None:
+            # block header is missing when template registry was unable to build it
+            # from given parameters. Client side is probably broken, storing such
+            # submit don't have any sense.                   
+            Interfaces.share_manager.on_submit_share(worker_name, block_header, block_hash, difficulty,
+                                              Interfaces.timestamper.time(), is_valid)
+        
+        if not is_valid:
+            raise SubmitException(reason)
+
+        return True
+            
+    # Service documentation for remote discovery
+    update_block.help_text = "Notify Stratum server about new block on the network."
+    update_block.params = [('password', 'string', 'Administrator password'),]
+    
+    authorize.help_text = "Authorize worker for submitting shares on this connection."
+    authorize.params = [('worker_name', 'string', 'Name of the worker, usually in the form of user_login.worker_id.'),
+                        ('worker_password', 'string', 'Worker password'),]
+    
+    subscribe.help_text = "Subscribes current connection for receiving new mining jobs."
+    subscribe.params = []
+    
+    submit.help_text = "Submit solved share back to the server. Excessive sending of invalid shares "\
+                       "or shares above indicated target (see Stratum mining docs for set_target()) may lead "\
+                       "to temporary or permanent ban of user,worker or IP address."
+    submit.params = [('worker_name', 'string', 'Name of the worker, usually in the form of user_login.worker_id.'),
+                     ('job_id', 'string', 'ID of job (received by mining.notify) which the current solution is based on.'),
+                     ('extranonce2', 'string', 'hex-encoded big-endian extranonce2, length depends on extranonce2_size from mining.notify.'),
+                     ('ntime', 'string', 'UNIX timestamp (32bit integer, big-endian, hex-encoded), must be >= ntime provided by mining,notify and <= current time'),
+                     ('nonce', 'string', '32bit integer, hex-encoded, big-endian'),]
+        
\ No newline at end of file
diff --git a/mining/subscription.py b/mining/subscription.py
new file mode 100644 (file)
index 0000000..cbbeea5
--- /dev/null
@@ -0,0 +1,53 @@
+from stratum.pubsub import Pubsub, Subscription
+from mining.interfaces import Interfaces
+
+import stratum.logger
+log = stratum.logger.get_logger('subscription')
+
+class MiningSubscription(Subscription):
+    '''This subscription object implements
+    logic for broadcasting new jobs to the clients.'''
+    
+    event = 'mining.notify'
+    
+    @classmethod
+    def on_block(cls, is_new_block):
+        '''This is called when TemplateRegistry registers
+           new block which we have to broadcast clients.'''
+        
+        start = Interfaces.timestamper.time()
+        
+        clean_jobs = is_new_block
+        (job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, clean_jobs) = \
+                        Interfaces.template_registry.get_last_broadcast_args()
+        
+        # Push new job to subscribed clients
+        cls.emit(job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, clean_jobs)
+        
+        cnt = Pubsub.get_subscription_count(cls.event)
+        log.info("BROADCASTED to %d connections in %.03f sec" % (cnt, (Interfaces.timestamper.time() - start)))
+        
+    def _finish_after_subscribe(self, result):
+        '''Send new job to newly subscribed client'''
+        try:        
+            (job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, clean_jobs) = \
+                        Interfaces.template_registry.get_last_broadcast_args()
+        except Exception:
+            log.error("Template not ready yet")
+            return result
+        
+        # Force set higher difficulty
+        # TODO
+        #self.connection_ref().rpc('mining.set_difficulty', [2,], is_notification=True)
+        
+        # Force client to remove previous jobs if any (eg. from previous connection)
+        clean_jobs = True
+        self.emit_single(job_id, prevhash, coinb1, coinb2, merkle_branch, version, nbits, ntime, clean_jobs)
+        
+        return result
+                
+    def after_subscribe(self, *args):
+        '''This will send new job to the client *after* he receive subscription details.
+        on_finish callback solve the issue that job is broadcasted *during*
+        the subscription request and client receive messages in wrong order.'''
+        self.connection_ref().on_finish.addCallback(self._finish_after_subscribe)
\ No newline at end of file