def calculate_hash(self, base_block_hash: bytes) -> bytes: """ Hash of the Bitcoin produced header, this is used for the block hash. """ from hathor.merged_mining.bitcoin import build_merkle_root_from_path, sha256d_hash coinbase_tx_hash = sha256d_hash(self.coinbase_head + base_block_hash + self.coinbase_tail) merkle_root = bytes(reversed(build_merkle_root_from_path([coinbase_tx_hash] + self.merkle_path))) return sha256d_hash(self.header_head + merkle_root + self.header_tail)
def build_aux_pow(self, work: SingleMinerWork) -> BitcoinAuxPow: """ Build the Auxiliary Proof-of-Work from job and work data. """ from hathor.merged_mining.bitcoin import sha256d_hash bitcoin_header, coinbase_tx = self._make_bitcoin_block_and_coinbase(work) header = bytes(bitcoin_header) header_head, header_tail = header[:36], header[-12:] block_base_hash = sha256d_hash(self.hathor_data) coinbase = bytes(coinbase_tx) coinbase_head, coinbase_tail = coinbase.split(block_base_hash) return BitcoinAuxPow(header_head, coinbase_head, coinbase_tail, self.merkle_path, header_tail)
def handle_submit(self, params: List[Any], msgid: Optional[str]) -> None: """ Handles submit request by validating and propagating the result - params: rpc_user, job_id, xnonce2, time, nonce Example: - ['', '6a16cffa-47c0-41d9-b92f-44e05d3c25dd', '0000000000000000', 'c359f65c', '47c8f488'] """ from hathor.merged_mining.bitcoin import sha256d_hash self.log.debug('handle submit', msgid=msgid, params=params) work = SingleMinerWork.from_stratum_params(self.xnonce1, params) job = self.jobs.get(work.job_id) if not job: self.log.error('job not found', job_id=work.job_id) return bitcoin_block_header = job.build_bitcoin_block_header(work) block_base = job.hathor_data block_base_hash = sha256d_hash(block_base) self.log.debug('work received', bitcoin_header=bytes(bitcoin_block_header).hex(), block_base=block_base.hex(), block_base_hash=block_base_hash.hex(), hash=bitcoin_block_header.hash.hex()) aux_pow = job.build_aux_pow(work) aux_pow.verify(block_base_hash) # TODO: treat exception (respond with proper error) self.log.debug('forward work to hathor', aux_pow=aux_pow) self.submit_to_hathor(job, aux_pow) # XXX: work is always sent to bitcoin node, which rejects accordingly self.log.debug('forward work to bitcoin', work=work) self.submit_to_bitcoin(job, work) # TODO: don't always return success? self.send_result(True, msgid)
def new_single_miner_job(self, payback_address_bitcoin: str, xnonce_size: int) -> SingleMinerJob: """ Generate a partial job for a single miner, based on this job. """ from hathor.merged_mining.bitcoin import sha256d_hash # base txs for merkle tree, before coinbase transactions = self.bitcoin_coord.transactions # build coinbase transaction with hathor block hash hathor_block_hash = sha256d_hash(self.hathor_coord.data) coinbase_tx = self.bitcoin_coord.make_coinbase_transaction( hathor_block_hash, self.payback_address_bitcoin, xnonce_size, ) coinbase_bytes = bytes(coinbase_tx) coinbase_head, coinbase_tail = coinbase_bytes.split(hathor_block_hash + b'\0' * xnonce_size, 1) coinbase_head += hathor_block_hash assert len(coinbase_bytes) == len(coinbase_head) + xnonce_size + len(coinbase_tail) # just a sanity check # TODO: check if total transaction size increase exceed size and sigop limits, there's probably an RPC for this transactions.insert(0, coinbase_tx) return SingleMinerJob( job_id=str(uuid4()), prev_hash=self.bitcoin_coord.previous_block_hash, coinbase_head=coinbase_head, coinbase_tail=coinbase_tail, merkle_path=build_merkle_path_for_coinbase([tx.hash for tx in transactions]), version=self.bitcoin_coord.version, bits=self.bitcoin_coord.bits, hathor_data=self.hathor_coord.data, hathor_job_id=self.hathor_coord.job_id, timestamp=self.bitcoin_coord.get_timestamp(), transactions=transactions, clean=self.clean, )
def handle_submit(self, params: Dict, msgid: Optional[str]) -> None: """ Handles submit request by validating and propagating the result :param params: a dict containing a valid uui4 hex as `job_id` and a valid transaction nonce as `nonce` :type params: Dict :param msgid: JSON-RPC 2.0 message id :type msgid: Optional[UUID] """ from hathor.merged_mining.bitcoin import sha256d_hash self.log.debug('handle submit', msgid=msgid, params=params) if 'job_id' not in params or 'nonce' not in params: return self.send_error(INVALID_PARAMS, msgid, { 'params': params, 'required': ['job_id', 'nonce'] }) if not valid_uuid(params['job_id']): return self.send_error(INVALID_PARAMS, msgid, { 'job_id': params['job_id'], 'message': 'job_id is invalid uuid4' }) job_id = UUID(params['job_id']) job = self.jobs.get(job_id) if job is None: return self.send_error( JOB_NOT_FOUND, msgid, { 'current_job': self.current_job and self.current_job.id.hex, 'job_id': job_id.hex }) # It may take a while for pubsub to get a new job. # To avoid propagating the same tx multiple times, we check if it has already been submitted. if job is not self.current_job or job.submitted is not None: return self.send_error( STALE_JOB, msgid, { 'current_job': self.current_job and self.current_job.id.hex, 'job_id': job_id.hex }) tx = job.tx.clone() block_base = tx.get_header_without_nonce() block_base_hash = sha256d_hash(block_base) # Stratum sends the nonce as a big-endian hexadecimal string. if params.get('aux_pow'): assert isinstance( tx, MergeMinedBlock ), 'expected MergeMinedBlock got ' + type(tx).__name__ tx.aux_pow = BitcoinAuxPow.from_bytes( bytes.fromhex(params['aux_pow'])) tx.nonce = 0 else: tx.nonce = int(params['nonce'], 16) tx.update_hash() assert tx.hash is not None self.log.debug('share received', block=tx, block_base=block_base.hex(), block_base_hash=block_base_hash.hex()) try: tx.verify_pow(job.weight) except PowError: self.log.error('bad share, discard', job_weight=job.weight, tx=tx) return self.send_error( INVALID_SOLUTION, msgid, { 'hash': tx.hash.hex(), 'target': int(tx.get_target()).to_bytes(32, 'big').hex() }) job.submitted = self.factory.get_current_timestamp() self.completed_jobs += 1 # answer the miner soon, so it can start a new job self.send_result('ok', msgid) self.manager.reactor.callLater(0, self.job_request) try: tx.verify_pow() except PowError: # Transaction pow was not enough, but the share was succesfully submited self.log.info('high hash, keep mining', tx=tx) return else: self.log.info('low hash, new block candidate', tx=tx) if isinstance(tx, Block): try: # We only propagate blocks here in stratum # For tx we need to propagate in the resource, # so we can get the possible errors self.manager.submit_block(tx, fails_silently=False) self.blocks_found += 1 except (InvalidNewTransaction, TxValidationError) as e: # Block propagation failed, but the share was succesfully submited self.log.warn('block propagation failed', block=tx, error=e) else: self.log.info('new block found', block=tx) elif isinstance(tx, Transaction): self.log.info('transaction mined', tx=tx) funds_hash = tx.get_funds_hash() if funds_hash in self.factory.mining_tx_pool: self.factory.mined_txs[funds_hash] = tx del self.factory.mining_tx_pool[funds_hash] if funds_hash in self.factory.tx_queue: self.factory.tx_queue.remove(funds_hash) if funds_hash in self.factory.deferreds_tx: # Return to resolve the resource to send back the response d = self.factory.deferreds_tx.pop(funds_hash) d.callback(tx) else: assert False, 'tx should either be a Block or Transaction'
def get_base_hash(self) -> bytes: from hathor.merged_mining.bitcoin import sha256d_hash return sha256d_hash(self.get_header_without_nonce())