6 from twisted.internet import defer
7 from lib.exceptions import SubmitException
10 log = stratum.logger.get_logger('template_registry')
12 from mining.interfaces import Interfaces
13 from extranonce_counter import ExtranonceCounter
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.'''
23 if cls.counter % 0xffff == 0:
25 return "%x" % cls.counter
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.'''
32 def __init__(self, block_template_class, coinbaser, bitcoin_rpc, instance_id,
33 on_template_callback, on_block_callback):
35 self.jobs = weakref.WeakValueDictionary()
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()
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
47 self.last_block = None
48 self.update_in_progress = False
49 self.last_update = None
51 # Create first block template on startup
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()
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
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.'''
69 prevhash = block.prevhash_hex
71 if prevhash in self.prevhashes.keys():
75 self.prevhashes[prevhash] = []
77 # Blocks sorted by prevhash, so it's easy to drop
78 # them on blockchain update
79 self.prevhashes[prevhash].append(block)
81 # Weak reference for fast lookup using job_id
82 self.jobs[block.job_id] = block
84 # Use this template for every new request
85 self.last_block = block
87 # Drop templates of obsolete blocks
88 for ph in self.prevhashes.keys():
90 del self.prevhashes[ph]
92 log.info("New template for %s" % prevhash)
95 # Tell the system about new block
96 # It is mostly important for share manager
97 self.on_block_callback(prevhash, block_height)
99 # Everything is ready, let's broadcast jobs!
100 self.on_template_callback(new_block)
103 #from twisted.internet import reactor
104 #reactor.callLater(10, self.on_block_callback, new_block)
106 def update_block(self):
107 '''Registry calls the getblocktemplate() RPC
108 and build new block template.'''
110 if self.update_in_progress:
111 # Block has been already detected
114 self.update_in_progress = True
115 self.last_update = Interfaces.timestamper.time()
117 d = self.bitcoin_rpc.getblocktemplate()
118 d.addCallback(self._update_block)
119 d.addErrback(self._update_block_failed)
121 def _update_block_failed(self, failure):
122 log.error(str(failure))
123 self.update_in_progress = False
125 def _update_block(self, data):
126 start = Interfaces.timestamper.time()
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'])
132 log.info("Update finished, %.03f sec, %d txes" % \
133 (Interfaces.timestamper.time() - start, len(template.vtx)))
135 self.update_in_progress = False
138 def diff_to_target(self, difficulty):
139 '''Converts difficulty to target'''
140 diff1 = 0x0000ffff00000000000000000000000000000000000000000000000000000000
141 return diff1 / difficulty
143 def get_job(self, job_id):
144 '''For given job_id returns BlockTemplate instance or None'''
146 j = self.jobs[job_id]
148 log.info("Job id '%s' not found" % job_id)
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)
158 if j not in self.prevhashes[j.prevhash_hex]:
159 log.info("Job %s is unknown" % job_id)
164 def submit_share(self, job_id, worker_name, session, extranonce1_bin, extranonce2, ntime, nonce,
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.
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()
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))
181 job = self.get_job(job_id)
183 raise SubmitException("Job '%s' not found" % job_id)
185 # Check if ntime looks correct
187 raise SubmitException("Incorrect size of ntime. Expected 8 chars")
189 if not job.check_ntime(int(ntime, 16)):
190 raise SubmitException("Ntime out of range")
194 raise SubmitException("Incorrect size of nonce. Expected 8 chars")
196 # Check for duplicated submit
197 if not job.register_submit(extranonce1_bin, extranonce2, ntime, nonce):
198 log.info("Duplicate from %s, (%s %s %s %s)" % \
199 (worker_name, binascii.hexlify(extranonce1_bin), extranonce2, ntime, nonce))
200 raise SubmitException("Duplicate share")
202 # Now let's do the hard work!
203 # ---------------------------
206 extranonce2_bin = binascii.unhexlify(extranonce2)
207 ntime_bin = binascii.unhexlify(ntime)
208 nonce_bin = binascii.unhexlify(nonce)
211 coinbase_bin = job.serialize_coinbase(extranonce1_bin, extranonce2_bin)
212 coinbase_hash = util.doublesha(coinbase_bin)
214 # 2. Calculate merkle root
215 merkle_root_bin = job.merkletree.withFirst(coinbase_hash)
216 merkle_root_int = util.uint256_from_str(merkle_root_bin)
218 # 3. Serialize header with given merkle, ntime and nonce
219 header_bin = job.serialize_header(merkle_root_int, ntime_bin, nonce_bin)
221 # 4. Reverse header and compare it with target of the user
222 hash_bin = util.scrypt(''.join([ header_bin[i*4:i*4+4][::-1] for i in range(0, 20) ]))
223 hash_int = util.uint256_from_str(hash_bin)
224 block_hash_hex = "%064x" % hash_int
225 header_hex = binascii.hexlify(header_bin)
227 target_user = self.diff_to_target(difficulty)
228 if hash_int > target_user and \
229 ( 'prev_jobid' not in session or session['prev_jobid'] < job_id \
230 or 'prev_diff' not in session or hash_int > self.diff_to_target(session['prev_diff']) ):
231 raise SubmitException("Share is above target")
233 # Mostly for debugging purposes
234 target_info = self.diff_to_target(100000)
235 if hash_int <= target_info:
236 log.info("Yay, share with diff above 100000")
238 # Algebra tells us the diff_to_target is the same as hash_to_diff
239 share_diff = int(self.diff_to_target(hash_int))
241 # 5. Compare hash with target of the network
242 if hash_int <= job.target:
243 # Yay! It is block candidate!
244 log.info("We found a block candidate! %s" % block_hash_hex)
246 # 6. Finalize and serialize block object
247 job.finalize(merkle_root_int, extranonce1_bin, extranonce2_bin, int(ntime, 16), int(nonce, 16))
249 if not job.is_valid():
251 log.error("Final job validation failed!")
253 # 7. Submit block to the network
254 serialized = binascii.hexlify(job.serialize())
255 on_submit = self.bitcoin_rpc.submitblock(serialized)
257 return (header_hex, block_hash_hex, share_diff, on_submit)
259 return (header_hex, block_hash_hex, share_diff, None)