5fc51241d1f309bdb4f1529d13d71152d52b0cb9
[stratum-mining.git] / lib / template_registry.py
1 import weakref
2 import binascii
3 import util
4 import StringIO
5
6 import stratum.logger
7 log = stratum.logger.get_logger('template_registry')
8
9 from mining.interfaces import Interfaces
10 from extranonce_counter import ExtranonceCounter
11
12 class JobIdGenerator(object):
13     '''Generate pseudo-unique job_id. It does not need to be absolutely unique,
14     because pool sends "clean_jobs" flag to clients and they should drop all previous jobs.'''
15     counter = 0
16     
17     @classmethod
18     def get_new_id(cls):
19         cls.counter += 1
20         if cls.counter % 0xffff == 0:
21             cls.counter = 1
22         return "%x" % cls.counter
23                 
24 class TemplateRegistry(object):
25     '''Implements the main logic of the pool. Keep track
26     on valid block templates, provide internal interface for stratum
27     service and implements block validation and submits.'''
28     
29     def __init__(self, block_template_class, coinbaser, bitcoin_rpc, instance_id,
30                  on_block_callback):
31         self.prevhashes = {}
32         self.jobs = weakref.WeakValueDictionary()
33         
34         self.extranonce_counter = ExtranonceCounter(instance_id)
35         self.extranonce2_size = block_template_class.coinbase_transaction_class.extranonce_size \
36                 - self.extranonce_counter.get_size()
37                  
38         self.coinbaser = coinbaser
39         self.block_template_class = block_template_class
40         self.bitcoin_rpc = bitcoin_rpc
41         self.on_block_callback = on_block_callback
42         
43         self.last_block = None
44         self.update_in_progress = False
45         self.last_update = None
46         
47         # Create first block template on startup
48         self.update_block()
49         
50     def get_new_extranonce1(self):
51         '''Generates unique extranonce1 (e.g. for newly
52         subscribed connection.'''
53         return self.extranonce_counter.get_new_bin()
54     
55     def get_last_broadcast_args(self):
56         '''Returns arguments for mining.notify
57         from last known template.'''
58         return self.last_block.broadcast_args
59         
60     def add_template(self, block):
61         '''Adds new template to the registry.
62         It also clean up templates which should
63         not be used anymore.'''
64         
65         prevhash = block.prevhash_hex
66
67         if prevhash in self.prevhashes.keys():
68             new_block = False
69         else:
70             new_block = True
71             self.prevhashes[prevhash] = []
72                
73         # Blocks sorted by prevhash, so it's easy to drop
74         # them on blockchain update
75         self.prevhashes[prevhash].append(block)
76         
77         # Weak reference for fast lookup using job_id
78         self.jobs[block.job_id] = block
79         
80         # Use this template for every new request
81         self.last_block = block
82         
83         # Drop templates of obsolete blocks
84         for ph in self.prevhashes.keys():
85             if ph != prevhash:
86                 del self.prevhashes[ph]
87                 
88         log.info("New template for %s" % prevhash)
89         self.on_block_callback(new_block)
90         #from twisted.internet import reactor
91         #reactor.callLater(10, self.on_block_callback, new_block) 
92               
93     def update_block(self):
94         '''Registry calls the getblocktemplate() RPC
95         and build new block template.'''
96         
97         if self.update_in_progress:
98             # Block has been already detected
99             return
100         
101         self.update_in_progress = True
102         self.last_update = Interfaces.timestamper.time()
103         
104         d = self.bitcoin_rpc.getblocktemplate()
105         d.addCallback(self._update_block)
106         d.addErrback(self._update_block_failed)
107         
108     def _update_block_failed(self, failure):
109         log.error(str(failure))
110         self.update_in_progress = False
111         
112     def _update_block(self, data):
113         start = Interfaces.timestamper.time()
114                 
115         template = self.block_template_class(Interfaces.timestamper, self.coinbaser, JobIdGenerator.get_new_id())
116         template.fill_from_rpc(data)
117         self.add_template(template)
118
119         log.info("Update finished, %.03f sec, %d txes" % \
120                     (Interfaces.timestamper.time() - start, len(template.vtx)))
121         
122         self.update_in_progress = False        
123         return data
124     
125     def diff_to_target(self, difficulty):
126         '''Converts difficulty to target'''
127         diff1 = 0x00000000ffff0000000000000000000000000000000000000000000000000000 
128         return diff1 / difficulty
129     
130     def get_job(self, job_id):
131         '''For given job_id returns BlockTemplate instance or None'''
132         try:
133             j = self.jobs[job_id]
134         except:
135             log.info("Job id '%s' not found" % job_id)
136             return False
137         
138         # Now we have to check if job is still valid.
139         # Unfortunately weak references are not bulletproof and
140         # old reference can be found until next run of garbage collector.
141         if j.prevhash_hex not in self.prevhashes:
142             log.info("Prevhash of job '%s' is unknown" % job_id)
143             return False
144         
145         if j not in self.prevhashes[j.prevhash_hex]:
146             log.info("Job %s is unknown" % job_id)
147             return False
148         
149         return True
150         
151     def submit_share(self, job_id, worker_name, extranonce1_bin, extranonce2, ntime, nonce,
152                      difficulty, submitblock_callback):
153         '''Check parameters and finalize block template. If it leads
154            to valid block candidate, asynchronously submits the block
155            back to the bitcoin network.
156         
157             - extranonce1_bin is binary. No checks performed, it should be from session data
158             - job_id, extranonce2, ntime, nonce - in hex form sent by the client
159             - difficulty - decimal number from session, again no checks performed
160             - submitblock_callback - reference to method which receive result of submitblock()
161         '''
162         
163         # Check if extranonce2 looks correctly. extranonce2 is in hex form...
164         if len(extranonce2) != self.extranonce2_size * 2:
165             return (False, "Incorrect size of extranonce2. Expected %d chars" % (self.extranonce2_size*2), None, None)
166         
167         # Check for job
168         if not self.get_job(job_id):
169             return (False, "Job '%s' not found" % job_id, None, None)
170             
171         try:
172             job = self.jobs[job_id]
173         except KeyError:
174             return (False, "Job '%s' not found" % job_id, None, None)
175                 
176         # Check if ntime looks correct
177         if len(ntime) != 8:
178             return (False, "Incorrect size of ntime. Expected 8 chars", None, None)
179
180         if not job.check_ntime(int(ntime, 16)):
181             return (False, "Ntime out of range", None, None)
182         
183         # Check nonce        
184         if len(nonce) != 8:
185             return (False, "Incorrect size of nonce. Expected 8 chars", None, None)
186         
187         # Check for duplicated submit
188         if not job.register_submit(extranonce1_bin, extranonce2, ntime, nonce):
189             log.info("Duplicate from %s, (%s %s %s %s)" % \
190                     (worker_name, binascii.hexlify(extranonce1_bin), extranonce2, ntime, nonce))
191             return (False, "Duplicate share", None, None)
192         
193         # Now let's do the hard work!
194         # ---------------------------
195         
196         # 0. Some sugar
197         extranonce2_bin = binascii.unhexlify(extranonce2)
198         ntime_bin = binascii.unhexlify(ntime)
199         nonce_bin = binascii.unhexlify(nonce)
200                 
201         # 1. Build coinbase
202         coinbase_bin = job.serialize_coinbase(extranonce1_bin, extranonce2_bin)
203         coinbase_hash = util.doublesha(coinbase_bin)
204         
205         # 2. Calculate merkle root
206         merkle_root_bin = job.merkletree.withFirst(coinbase_hash)
207         merkle_root_int = util.uint256_from_str(merkle_root_bin)
208                 
209         # 3. Serialize header with given merkle, ntime and nonce
210         header_bin = job.serialize_header(merkle_root_int, ntime_bin, nonce_bin)
211     
212         # 4. Reverse header and compare it with target of the user
213         hash_bin = util.doublesha(''.join([ header_bin[i*4:i*4+4][::-1] for i in range(0, 20) ]))
214         hash_int = util.uint256_from_str(hash_bin)
215         block_hash_hex = "%064x" % hash_int
216         header_hex = binascii.hexlify(header_bin)
217                  
218         target_user = self.diff_to_target(difficulty)        
219         if hash_int > target_user:
220             return (False, "Share is above target", None, None)
221
222         # Mostly for debugging purposes
223         target_info = self.diff_to_target(100000)
224         if hash_int <= target_info:
225             log.info("Yay, share with diff above 100000")
226
227         # 5. Compare hash with target of the network        
228         if hash_int <= job.target:
229             # Yay! It is block candidate! 
230             log.info("We found a block candidate! %s" % block_hash_hex)
231            
232             # 6. Finalize and serialize block object 
233             job.finalize(merkle_root_int, extranonce1_bin, extranonce2_bin, int(ntime, 16), int(nonce, 16))
234             
235             if not job.is_valid():
236                 # Should not happen
237                 log.error("Final job validation failed!")
238                 return (False, 'Job validation failed', header_hex, block_hash_hex)
239                             
240             # 7. Submit block to the network
241             submit_time = Interfaces.timestamper.time()
242             serialized = binascii.hexlify(job.serialize())
243             d = self.bitcoin_rpc.submitblock(serialized)
244             
245             # Submit is lazy, we don't need to wait for the result
246             # Callback will just register success or failure to share manager
247             d.addCallback(self._on_submitblock, submitblock_callback,
248                           worker_name, header_hex, block_hash_hex, submit_time)
249             d.addErrback(self._on_submitblock_failure, submitblock_callback,
250                          worker_name, header_hex, block_hash_hex, submit_time)
251             
252             return (True, '', header_hex, block_hash_hex)
253         
254         return (True, '', header_hex, block_hash_hex)
255     
256     def _on_submitblock(self, is_accepted, callback, worker_name, block_header, block_hash, timestamp):
257         '''Helper method, bridges call from deferred to method reference given in submit()'''
258         # Forward submitblock result to share manager
259         callback(worker_name, block_header, block_hash, timestamp, is_accepted)
260         return is_accepted
261     
262     def _on_submitblock_failure(self, failure, callback, worker_name, block_header, block_hash, timestamp):
263         '''Helper method, bridges call from deferred to method reference given in submit()'''
264         # Forward submitblock failure to share manager
265         callback(worker_name, block_header, block_hash, timestamp, False)
266         log.exception(failure)