7 log = stratum.logger.get_logger('template_registry')
9 from mining.interfaces import Interfaces
10 from extranonce_counter import ExtranonceCounter
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.'''
20 if cls.counter % 0xffff == 0:
22 return "%x" % cls.counter
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.'''
29 def __init__(self, block_template_class, coinbaser, bitcoin_rpc, instance_id,
32 self.jobs = weakref.WeakValueDictionary()
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()
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
43 self.last_block = None
44 self.update_in_progress = False
45 self.last_update = None
47 # Create first block template on startup
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()
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
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.'''
65 prevhash = block.prevhash_hex
67 if prevhash in self.prevhashes.keys():
71 self.prevhashes[prevhash] = []
73 # Blocks sorted by prevhash, so it's easy to drop
74 # them on blockchain update
75 self.prevhashes[prevhash].append(block)
77 # Weak reference for fast lookup using job_id
78 self.jobs[block.job_id] = block
80 # Use this template for every new request
81 self.last_block = block
83 # Drop templates of obsolete blocks
84 for ph in self.prevhashes.keys():
86 del self.prevhashes[ph]
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)
93 def update_block(self):
94 '''Registry calls the getblocktemplate() RPC
95 and build new block template.'''
97 if self.update_in_progress:
98 # Block has been already detected
101 self.update_in_progress = True
102 self.last_update = Interfaces.timestamper.time()
104 d = self.bitcoin_rpc.getblocktemplate()
105 d.addCallback(self._update_block)
106 d.addErrback(self._update_block_failed)
108 def _update_block_failed(self, failure):
109 log.error(str(failure))
110 self.update_in_progress = False
112 def _update_block(self, data):
113 start = Interfaces.timestamper.time()
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)
119 log.info("Update finished, %.03f sec, %d txes" % \
120 (Interfaces.timestamper.time() - start, len(template.vtx)))
122 self.update_in_progress = False
125 def diff_to_target(self, difficulty):
126 '''Converts difficulty to target'''
127 diff1 = 0x00000000ffff0000000000000000000000000000000000000000000000000000
128 return diff1 / difficulty
130 def get_job(self, job_id):
131 '''For given job_id returns BlockTemplate instance or None'''
133 j = self.jobs[job_id]
135 log.info("Job id '%s' not found" % job_id)
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)
145 if j not in self.prevhashes[j.prevhash_hex]:
146 log.info("Job %s is unknown" % job_id)
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.
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()
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)
168 if not self.get_job(job_id):
169 return (False, "Job '%s' not found" % job_id, None, None)
172 job = self.jobs[job_id]
174 return (False, "Job '%s' not found" % job_id, None, None)
176 # Check if ntime looks correct
178 return (False, "Incorrect size of ntime. Expected 8 chars", None, None)
180 if not job.check_ntime(int(ntime, 16)):
181 return (False, "Ntime out of range", None, None)
185 return (False, "Incorrect size of nonce. Expected 8 chars", None, None)
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)
193 # Now let's do the hard work!
194 # ---------------------------
197 extranonce2_bin = binascii.unhexlify(extranonce2)
198 ntime_bin = binascii.unhexlify(ntime)
199 nonce_bin = binascii.unhexlify(nonce)
202 coinbase_bin = job.serialize_coinbase(extranonce1_bin, extranonce2_bin)
203 coinbase_hash = util.doublesha(coinbase_bin)
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)
209 # 3. Serialize header with given merkle, ntime and nonce
210 header_bin = job.serialize_header(merkle_root_int, ntime_bin, nonce_bin)
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)
218 target_user = self.diff_to_target(difficulty)
219 if hash_int > target_user:
220 return (False, "Share is above target", None, None)
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")
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)
232 # 6. Finalize and serialize block object
233 job.finalize(merkle_root_int, extranonce1_bin, extranonce2_bin, int(ntime, 16), int(nonce, 16))
235 if not job.is_valid():
237 log.error("Final job validation failed!")
238 return (False, 'Job validation failed', header_hex, block_hash_hex)
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)
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)
252 return (True, '', header_hex, block_hash_hex)
254 return (True, '', header_hex, block_hash_hex)
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)
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)