Typo fix
[stratum-mining.git] / 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,block_height):
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, block_height)
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,data['height'])
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 = 0x0000ffff00000000000000000000000000000000000000000000000000000000 
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, session, 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         # normalize the case to prevent duplication of valid shares by the client
197         ntime = ntime.lower()
198         nonce = nonce.lower()
199         extranonce2 = extranonce2.lower()
200
201         # Check for duplicated submit
202         if not job.register_submit(extranonce1_bin, extranonce2, ntime, nonce):
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         # 0. Some sugar
211         extranonce2_bin = binascii.unhexlify(extranonce2)
212         ntime_bin = binascii.unhexlify(ntime)
213         nonce_bin = binascii.unhexlify(nonce)
214                 
215         # 1. Build coinbase
216         coinbase_bin = job.serialize_coinbase(extranonce1_bin, extranonce2_bin)
217         coinbase_hash = util.doublesha(coinbase_bin)
218         
219         # 2. Calculate merkle root
220         merkle_root_bin = job.merkletree.withFirst(coinbase_hash)
221         merkle_root_int = util.uint256_from_str(merkle_root_bin)
222                 
223         # 3. Serialize header with given merkle, ntime and nonce
224         header_bin = job.serialize_header(merkle_root_int, ntime_bin, nonce_bin)
225     
226         # 4. Reverse header and compare it with target of the user
227         hash_bin = util.scrypt(''.join([ header_bin[i*4:i*4+4][::-1] for i in range(0, 20) ]))
228         hash_int = util.uint256_from_str(hash_bin)
229         block_hash_hex = "%064x" % hash_int
230         header_hex = binascii.hexlify(header_bin)
231                  
232         target_user = self.diff_to_target(difficulty)        
233         if hash_int > target_user and \
234                 ( 'prev_jobid' not in session or session['prev_jobid'] < job_id \
235                 or 'prev_diff' not in session or hash_int > self.diff_to_target(session['prev_diff']) ):
236             raise SubmitException("Share is above target")
237
238         # Mostly for debugging purposes
239         target_info = self.diff_to_target(100000)
240         if hash_int <= target_info:
241             log.info("Yay, share with diff above 100000")
242
243         # Algebra tells us the diff_to_target is the same as hash_to_diff
244         share_diff = int(self.diff_to_target(hash_int))
245
246         # 5. Compare hash with target of the network        
247         if hash_int <= job.target:
248             # Yay! It is block candidate! 
249             log.info("We found a block candidate! %s" % block_hash_hex)
250            
251             # 6. Finalize and serialize block object 
252             job.finalize(merkle_root_int, extranonce1_bin, extranonce2_bin, int(ntime, 16), int(nonce, 16))
253             
254             if not job.is_valid():
255                 # Should not happen
256                 log.error("Final job validation failed!")
257                             
258             # 7. Submit block to the network
259             serialized = binascii.hexlify(job.serialize())
260             on_submit = self.bitcoin_rpc.submitblock(serialized)
261             
262             return (header_hex, block_hash_hex, share_diff, on_submit)
263         
264         return (header_hex, block_hash_hex, share_diff, None)