async def submit_work(self, header_hash: bytes, nonce: int, mixhash: bytes) -> bool: if not self.remote: raise ValueError("Should only be used for remote miner") if header_hash not in self.work_map: return False # this copy is necessary since there might be multiple submissions concurrently block = copy.deepcopy(self.work_map[header_hash]) header = block.header header.nonce, header.mixhash = nonce, mixhash # sign as a guardian if self.guardian_private_key and isinstance(block, RootBlock): header.sign_with_private_key(self.guardian_private_key) try: await self.add_block_async_func(block) # a previous submission of the same work could have removed the key if header_hash in self.work_map: del self.work_map[header_hash] self.current_work = None return True except Exception: Logger.error_exception() return False
async def _run(self) -> None: """ overrides BasePeer._run() forwards decrypted messages to QuarkChain Peer """ self.run_child_service(self.boot_manager) self.secure_peer.add_sync_task() if self.secure_peer.state == ConnectionState.CONNECTING: self.secure_peer.state = ConnectionState.ACTIVE self.secure_peer.active_future.set_result(None) try: while self.is_operational: metadata, raw_data = await self.secure_peer.read_metadata_and_raw_data( ) self.run_task( self.secure_peer.secure_handle_metadata_and_raw_data( metadata, raw_data)) except (PeerConnectionLost, TimeoutError) as err: self.logger.debug("%s stopped responding (%r), disconnecting", self.remote, err) except DecryptionError as err: self.logger.warning( "Unable to decrypt message from %s, disconnecting: %r", self.remote, err) except Exception as e: self.logger.error("Unknown exception from %s, message: %r", self.remote, e) Logger.error_exception() self.secure_peer.abort_in_flight_rpcs() self.secure_peer.close()
async def handle_sync_minor_block_list_request(self, req): """ Raises on error""" async def __download_blocks(block_hash_list): op, resp, rpc_id = await peer_shard_conn.write_rpc_request( CommandOp.GET_MINOR_BLOCK_LIST_REQUEST, GetMinorBlockListRequest(block_hash_list), ) return resp.minor_block_list shard = self.shards.get(req.branch, None) if not shard: return SyncMinorBlockListResponse(error_code=errno.EBADMSG) peer_shard_conn = shard.peers.get(req.cluster_peer_id, None) if not peer_shard_conn: return SyncMinorBlockListResponse(error_code=errno.EBADMSG) BLOCK_BATCH_SIZE = 100 block_hash_list = req.minor_block_hash_list # empty if not block_hash_list: return SyncMinorBlockListResponse(error_code=0) try: while len(block_hash_list) > 0: blocks_to_download = block_hash_list[:BLOCK_BATCH_SIZE] try: block_chain = await asyncio.wait_for( __download_blocks(blocks_to_download), TIMEOUT) except asyncio.TimeoutError as e: Logger.info( "[{}] sync request from master failed due to timeout". format(req.branch.get_full_shard_id())) raise e Logger.info( "[{}] sync request from master, downloaded {} blocks ({} - {})" .format( req.branch.get_full_shard_id(), len(block_chain), block_chain[0].header.height, block_chain[-1].header.height, )) check(len(block_chain) == len(blocks_to_download)) add_block_success = await self.slave_server.add_block_list_for_sync( block_chain) if not add_block_success: raise RuntimeError( "Failed to add minor blocks for syncing root block") block_hash_list = block_hash_list[BLOCK_BATCH_SIZE:] branch = block_chain[0].header.branch shard = self.slave_server.shards.get(branch, None) check(shard is not None) return SyncMinorBlockListResponse( error_code=0, shard_stats=shard.state.get_shard_stats()) except Exception: Logger.error_exception() return SyncMinorBlockListResponse(error_code=1)
async def add_block(self, block): """ Returns true if block is successfully added. False on any error. called by 1. local miner (will not run if syncing) 2. SyncTask """ old_tip = self.state.header_tip try: xshard_list = self.state.add_block(block) except Exception as e: Logger.error_exception() return False # only remove from pool if the block successfully added to state, # this may cache failed blocks but prevents them being broadcasted more than needed # TODO add ttl to blocks in new_block_pool self.state.new_block_pool.pop(block.header.get_hash(), None) # block has been added to local state, broadcast tip so that peers can sync if needed try: if old_tip != self.state.header_tip: self.broadcast_new_tip() except Exception: Logger.warning_every_sec("broadcast tip failure", 1) # block already existed in local shard state # but might not have been propagated to other shards and master # let's make sure all the shards and master got it before return if xshard_list is None: future = self.add_block_futures.get(block.header.get_hash(), None) if future: Logger.info( "[{}] {} is being added ... waiting for it to finish". format(block.header.branch.get_shard_id(), block.header.height)) await future return True self.add_block_futures[ block.header.get_hash()] = self.loop.create_future() # Start mining new one before propagating inside cluster # The propagation should be done by the time the new block is mined self.miner.mine_new_block_async() prev_root_height = self.state.db.get_root_block_by_hash( block.header.hash_prev_root_block).header.height await self.slave.broadcast_xshard_tx_list(block, xshard_list, prev_root_height) await self.slave.send_minor_block_header_to_master( block.header, len(block.tx_list), len(xshard_list), self.state.get_shard_stats(), ) self.add_block_futures[block.header.get_hash()].set_result(None) del self.add_block_futures[block.header.get_hash()] return True
async def add_block_list_for_sync(self, block_list): """ Add blocks in batch to reduce RPCs. Will NOT broadcast to peers. Returns true if blocks are successfully added. False on any error. Additionally, returns list of coinbase_amount_map for each block This function only adds blocks to local and propagate xshard list to other shards. It does NOT notify master because the master should already have the minor header list, and will add them once this function returns successfully. """ coinbase_amount_list = [] if not block_list: return True, coinbase_amount_list existing_add_block_futures = [] block_hash_to_x_shard_list = dict() for block in block_list: check( block.header.branch.get_full_shard_id() == self.full_shard_id) block_hash = block.header.get_hash() try: xshard_list, coinbase_amount_map = self.state.add_block( block, skip_if_too_old=False) # coinbase_amount_map may be None if the block exists # adding the block header one since the block is already validated. coinbase_amount_list.append(block.header.coinbase_amount_map) except Exception as e: Logger.error_exception() return False, coinbase_amount_list # block already existed in local shard state # but might not have been propagated to other shards and master # let's make sure all the shards and master got it before return if xshard_list is None: future = self.add_block_futures.get(block_hash, None) if future: existing_add_block_futures.append(future) else: prev_root_height = self.state.db.get_root_block_header_by_hash( block.header.hash_prev_root_block).height block_hash_to_x_shard_list[block_hash] = (xshard_list, prev_root_height) self.add_block_futures[block_hash] = self.loop.create_future() await self.slave.batch_broadcast_xshard_tx_list( block_hash_to_x_shard_list, block_list[0].header.branch) for block_hash in block_hash_to_x_shard_list.keys(): self.add_block_futures[block_hash].set_result(None) del self.add_block_futures[block_hash] await asyncio.gather(*existing_add_block_futures) return True, coinbase_amount_list
async def handle_sync_minor_block_list_request(self, req): async def __download_blocks(block_hash_list): op, resp, rpc_id = await peer_shard_conn.write_rpc_request( CommandOp.GET_MINOR_BLOCK_LIST_REQUEST, GetMinorBlockListRequest(block_hash_list), ) return resp.minor_block_list shard = self.shards.get(req.branch, None) if not shard: return SyncMinorBlockListResponse(error_code=errno.EBADMSG) peer_shard_conn = shard.peers.get(req.cluster_peer_id, None) if not peer_shard_conn: return SyncMinorBlockListResponse(error_code=errno.EBADMSG) BLOCK_BATCH_SIZE = 100 try: block_hash_list = req.minor_block_hash_list while len(block_hash_list) > 0: blocks_to_download = block_hash_list[:BLOCK_BATCH_SIZE] block_chain = await __download_blocks(blocks_to_download) Logger.info( "[{}] sync request from master, downloaded {} blocks ({} - {})".format( req.branch.get_shard_id(), len(block_chain), block_chain[0].header.height, block_chain[-1].header.height, ) ) check(len(block_chain) == len(blocks_to_download)) add_block_success = await self.slave_server.add_block_list_for_sync( block_chain ) if not add_block_success: raise RuntimeError( "Failed to add minor blocks for syncing root block" ) block_hash_list = block_hash_list[BLOCK_BATCH_SIZE:] except Exception: Logger.error_exception() return SyncMinorBlockListResponse(error_code=1) return SyncMinorBlockListResponse(error_code=0)
async def submit_work( self, header_hash: bytes, nonce: int, mixhash: bytes, signature: Optional[bytes] = None, ) -> bool: if not self.remote: raise ValueError("Should only be used for remote miner") if header_hash not in self.work_map: return False # this copy is necessary since there might be multiple submissions concurrently block = copy.deepcopy(self.work_map[header_hash]) header = block.header # reject if tip updated tip_hash = self.get_header_tip_func().get_hash() if header.hash_prev_block != tip_hash: del self.work_map[header_hash] return False header.nonce, header.mixhash = nonce, mixhash # sign using the root_signer_private_key if self.root_signer_private_key and isinstance(block, RootBlock): header.sign_with_private_key(self.root_signer_private_key) # remote sign as a guardian if isinstance(block, RootBlock) and signature is not None: header.signature = signature try: await self.add_block_async_func(block) # a previous submission of the same work could have removed the key if header_hash in self.work_map: del self.work_map[header_hash] return True except Exception: Logger.error_exception() return False
async def handle_mined_block(): while True: res = await self.output_q.coro_get() # type: MiningResult if not res: return # empty result means ending # start mining before processing and propagating mined block self._mine_new_block_async() block = self.work_map[res.header_hash] block.header.nonce = res.nonce block.header.mixhash = res.mixhash del self.work_map[res.header_hash] self._track(block) try: # FIXME: Root block should include latest minor block headers while it's being mined # This is a hack to get the latest minor block included since testnet does not check difficulty if self.consensus_type == ConsensusType.POW_SIMULATE: block = await self.create_block_async_func() block.header.nonce = random.randint(0, 2**32 - 1) self._track(block) self._log_status(block) await self.add_block_async_func(block) except Exception: Logger.error_exception()
async def submit_work(self, header_hash: bytes, nonce: int, mixhash: bytes) -> bool: if not self.remote: raise ValueError("Should only be used for remote miner") if header_hash not in self.work_map: return False # this copy is necessary since there might be multiple submissions concurrently block = copy.deepcopy(self.work_map[header_hash]) header = block.header header.nonce, header.mixhash = nonce, mixhash # lower the difficulty for root block signed by guardian if self.guardian_private_key and isinstance(block, RootBlock): diff = Guardian.adjust_difficulty(header.difficulty, header.height) try: validate_seal(header, self.consensus_type, adjusted_diff=diff) except ValueError: return False # sign as a guardian header.sign_with_private_key(self.guardian_private_key) else: # minor block, or doesn't have guardian private key try: validate_seal(header, self.consensus_type) except ValueError: return False try: await self.add_block_async_func(block) # a previous submission of the same work could have removed the key if header_hash in self.work_map: del self.work_map[header_hash] self.current_work = None return True except Exception: Logger.error_exception() return False
async def add_block_list_for_sync(self, block_list): """ Add blocks in batch to reduce RPCs. Will NOT broadcast to peers. Returns true if blocks are successfully added. False on any error. Additionally, returns list of coinbase_amount_map for each block This function only adds blocks to local and propagate xshard list to other shards. It does NOT notify master because the master should already have the minor header list, and will add them once this function returns successfully. """ coinbase_amount_list = [] if not block_list: return True, coinbase_amount_list existing_add_block_futures = [] block_hash_to_x_shard_list = dict() uncommitted_block_header_list = [] uncommitted_coinbase_amount_map_list = [] for block in block_list: check( block.header.branch.get_full_shard_id() == self.full_shard_id) block_hash = block.header.get_hash() # adding the block header one assuming the block will be validated. coinbase_amount_list.append(block.header.coinbase_amount_map) commit_status, future = self.__get_block_commit_status_by_hash( block_hash) if commit_status == BLOCK_COMMITTED: # Skip processing the block if it is already committed Logger.warning( "minor block to sync {} is already committed".format( block_hash.hex())) continue elif commit_status == BLOCK_COMMITTING: # Check if the block is being propagating to other slaves and the master # Let's make sure all the shards and master got it before committing it Logger.info( "[{}] {} is being added ... waiting for it to finish". format(block.header.branch.to_str(), block.header.height)) existing_add_block_futures.append(future) continue check(commit_status == BLOCK_UNCOMMITTED) # Validate and add the block try: xshard_list, coinbase_amount_map = self.state.add_block( block, skip_if_too_old=False, force=True) except Exception as e: Logger.error_exception() return False, None prev_root_height = self.state.db.get_root_block_header_by_hash( block.header.hash_prev_root_block).height block_hash_to_x_shard_list[block_hash] = (xshard_list, prev_root_height) self.add_block_futures[block_hash] = self.loop.create_future() uncommitted_block_header_list.append(block.header) uncommitted_coinbase_amount_map_list.append( block.header.coinbase_amount_map) await self.slave.batch_broadcast_xshard_tx_list( block_hash_to_x_shard_list, block_list[0].header.branch) check( len(uncommitted_coinbase_amount_map_list) == len( uncommitted_block_header_list)) await self.slave.send_minor_block_header_list_to_master( uncommitted_block_header_list, uncommitted_coinbase_amount_map_list) # Commit all blocks and notify all rest add block operations for block_header in uncommitted_block_header_list: block_hash = block_header.get_hash() self.state.commit_by_hash(block_hash) Logger.debug("committed mblock {}".format(block_hash.hex())) self.add_block_futures[block_hash].set_result(None) del self.add_block_futures[block_hash] # Wait for the other add block operations await asyncio.gather(*existing_add_block_futures) return True, coinbase_amount_list
async def add_block(self, block): """ Returns true if block is successfully added. False on any error. called by 1. local miner (will not run if syncing) 2. SyncTask """ block_hash = block.header.get_hash() commit_status, future = self.__get_block_commit_status_by_hash( block_hash) if commit_status == BLOCK_COMMITTED: return True elif commit_status == BLOCK_COMMITTING: Logger.info( "[{}] {} is being added ... waiting for it to finish".format( block.header.branch.to_str(), block.header.height)) await future return True check(commit_status == BLOCK_UNCOMMITTED) # Validate and add the block old_tip = self.state.header_tip try: xshard_list, coinbase_amount_map = self.state.add_block(block, force=True) except Exception as e: Logger.error_exception() return False # only remove from pool if the block successfully added to state, # this may cache failed blocks but prevents them being broadcasted more than needed # TODO add ttl to blocks in new_block_header_pool self.state.new_block_header_pool.pop(block_hash, None) # block has been added to local state, broadcast tip so that peers can sync if needed try: if old_tip != self.state.header_tip: self.broadcast_new_tip() except Exception: Logger.warning_every_sec("broadcast tip failure", 1) # Add the block in future and wait self.add_block_futures[block_hash] = self.loop.create_future() prev_root_height = self.state.db.get_root_block_header_by_hash( block.header.hash_prev_root_block).height await self.slave.broadcast_xshard_tx_list(block, xshard_list, prev_root_height) await self.slave.send_minor_block_header_to_master( block.header, len(block.tx_list), len(xshard_list), coinbase_amount_map, self.state.get_shard_stats(), ) # Commit the block self.state.commit_by_hash(block_hash) Logger.debug("committed mblock {}".format(block_hash.hex())) # Notify the rest self.add_block_futures[block_hash].set_result(None) del self.add_block_futures[block_hash] return True
async def handle_sync_minor_block_list_request(self, req): """ Raises on error""" async def __download_blocks(block_hash_list): op, resp, rpc_id = await peer_shard_conn.write_rpc_request( CommandOp.GET_MINOR_BLOCK_LIST_REQUEST, GetMinorBlockListRequest(block_hash_list), ) return resp.minor_block_list shard = self.shards.get(req.branch, None) if not shard: return SyncMinorBlockListResponse(error_code=errno.EBADMSG) peer_shard_conn = shard.peers.get(req.cluster_peer_id, None) if not peer_shard_conn: return SyncMinorBlockListResponse(error_code=errno.EBADMSG) BLOCK_BATCH_SIZE = 100 block_hash_list = req.minor_block_hash_list block_coinbase_map = {} # empty if not block_hash_list: return SyncMinorBlockListResponse(error_code=0) try: while len(block_hash_list) > 0: blocks_to_download = block_hash_list[:BLOCK_BATCH_SIZE] try: block_chain = await asyncio.wait_for( __download_blocks(blocks_to_download), TIMEOUT) except asyncio.TimeoutError as e: Logger.info( "[{}] sync request from master failed due to timeout". format(req.branch.to_str())) raise e Logger.info( "[{}] sync request from master, downloaded {} blocks ({} - {})" .format( req.branch.to_str(), len(block_chain), block_chain[0].header.height, block_chain[-1].header.height, )) # Step 1: Check if the len is correct if len(block_chain) != len(blocks_to_download): raise RuntimeError( "Failed to add minor blocks for syncing root block: " + "length of downloaded block list is incorrect") # Step 2: Check if the blocks are valid add_block_success, coinbase_amount_list = await self.slave_server.add_block_list_for_sync( block_chain) if not add_block_success: raise RuntimeError( "Failed to add minor blocks for syncing root block") check(len(blocks_to_download) == len(coinbase_amount_list)) for hash, coinbase in zip(blocks_to_download, coinbase_amount_list): if coinbase: block_coinbase_map[hash] = coinbase block_hash_list = block_hash_list[BLOCK_BATCH_SIZE:] branch = block_chain[0].header.branch shard = self.slave_server.shards.get(branch, None) check(shard is not None) return SyncMinorBlockListResponse( error_code=0, shard_stats=shard.state.get_shard_stats(), block_coinbase_map=block_coinbase_map, ) except Exception: Logger.error_exception() return SyncMinorBlockListResponse(error_code=1)