class ConsensusSiever(ConsensusBase): def __init__(self, block_manager): super().__init__(block_manager) self.__block_generation_timer = None self.__lock = None self._loop: asyncio.BaseEventLoop = None self._vote_queue: asyncio.Queue = None def start_timer(self, timer_service: TimerService): self._loop = timer_service.get_event_loop() self.__lock = asyncio.Lock(loop=self._loop) self.__block_generation_timer = SlotTimer( TimerService.TIMER_KEY_BLOCK_GENERATE, conf.INTERVAL_BLOCKGENERATION, timer_service, self.consensus, self.__lock, self._loop) self.__block_generation_timer.start( is_run_at_start=conf.ALLOW_MAKE_EMPTY_BLOCK is False) def __put_vote(self, vote): async def _put(): if self._vote_queue is not None: await self._vote_queue.put(vote) # sentinel asyncio.run_coroutine_threadsafe(_put(), self._loop) def stop(self): self.__block_generation_timer.stop() self.__stop_broadcast_send_unconfirmed_block_timer() if self._loop: self.__put_vote(None) @property def is_running(self): return self.__block_generation_timer.is_running def vote(self, vote_block_hash, vote_code, peer_id, group_id): if self._loop: self.__put_vote((vote_block_hash, vote_code, peer_id, group_id)) return util.logger.debug("Cannot vote before starting consensus.") # raise RuntimeError("Cannot vote before starting consensus.") def __build_candidate_block(self, block_builder, next_leader, vote_result): last_block = self._blockchain.last_block block_builder.height = last_block.header.height + 1 block_builder.prev_hash = last_block.header.hash block_builder.next_leader = next_leader block_builder.signer = ObjectManager().channel_service.peer_auth block_builder.confirm_prev_block = vote_result or ( self._made_block_count > 0) # TODO: This should be changed when IISS is applied. block_builder.reps = ObjectManager().channel_service.get_rep_ids() return block_builder.build() async def __add_block(self, block: Block): vote = self._block_manager.candidate_blocks.get_vote(block.header.hash) vote_result = await self._wait_for_voting(block) if not vote_result: raise NotEnoughVotes self._block_manager.get_blockchain().add_block(block, vote) self._block_manager.candidate_blocks.remove_block(block.header.hash) self._blockchain.last_unconfirmed_block = None self._made_block_count += 1 async def __add_block_and_new_epoch(self, block_builder, last_unconfirmed_block: Block): """Add Block and start new epoch :param block_builder: :param last_unconfirmed_block: :return: next leader """ await self.__add_block(last_unconfirmed_block) self.__remove_duplicate_tx_when_turn_to_leader(block_builder, last_unconfirmed_block) self._block_manager.epoch = Epoch.new_epoch(ChannelProperty().peer_id) return last_unconfirmed_block.header.next_leader def __remove_duplicate_tx_when_turn_to_leader(self, block_builder, last_unconfirmed_block): if self.made_block_count == 1: for tx_hash_in_unconfirmed_block in last_unconfirmed_block.body.transactions: block_builder.transactions.pop(tx_hash_in_unconfirmed_block, None) async def consensus(self): util.logger.debug( f"-------------------consensus " f"candidate_blocks({len(self._block_manager.candidate_blocks.blocks)})" ) async with self.__lock: if self._block_manager.epoch.leader_id != ChannelProperty( ).peer_id: util.logger.warning( f"This peer is not leader. epoch leader={self._block_manager.epoch.leader_id}" ) return self._vote_queue = asyncio.Queue(loop=self._loop) complained_result = self._block_manager.epoch.complained_result block_builder = self._block_manager.epoch.makeup_block( complained_result) vote_result = None last_unconfirmed_block = self._blockchain.last_unconfirmed_block next_leader = ExternalAddress.fromhex(ChannelProperty().peer_id) need_next_call = False try: if complained_result: util.logger.spam("consensus block_builder.complained") """ confirm_info = self._blockchain.find_confirm_info_by_hash(self._blockchain.last_block.header.hash) if not confirm_info and self._blockchain.last_block.header.height > 0: util.logger.spam("Can't make a block as a leader, this peer will be complained too.") return """ self._made_block_count += 1 elif self.made_block_count >= (conf.MAX_MADE_BLOCK_COUNT - 1): if last_unconfirmed_block: await self.__add_block(last_unconfirmed_block) peer_manager = ObjectManager( ).channel_service.peer_manager next_leader = ExternalAddress.fromhex( peer_manager.get_next_leader_peer( current_leader_peer_id=ChannelProperty( ).peer_id).peer_id) else: util.logger.info( f"This leader already made {self.made_block_count} blocks. " f"MAX_MADE_BLOCK_COUNT is {conf.MAX_MADE_BLOCK_COUNT} " f"There is no more right. Consensus loop will return." ) return elif len(block_builder.transactions ) > 0 or conf.ALLOW_MAKE_EMPTY_BLOCK: if last_unconfirmed_block: next_leader = await self.__add_block_and_new_epoch( block_builder, last_unconfirmed_block) elif len(block_builder.transactions) == 0 and ( last_unconfirmed_block and len(last_unconfirmed_block.body.transactions) > 0): next_leader = await self.__add_block_and_new_epoch( block_builder, last_unconfirmed_block) else: need_next_call = True except NotEnoughVotes: need_next_call = True finally: if need_next_call: return self.__block_generation_timer.call() candidate_block = self.__build_candidate_block( block_builder, next_leader, vote_result) candidate_block, invoke_results = ObjectManager( ).channel_service.score_invoke(candidate_block) self._block_manager.set_invoke_results( candidate_block.header.hash.hex(), invoke_results) util.logger.spam(f"candidate block : {candidate_block.header}") self._block_manager.vote_unconfirmed_block( candidate_block.header.hash, True) self._block_manager.candidate_blocks.add_block(candidate_block) self._blockchain.last_unconfirmed_block = candidate_block broadcast_func = partial( self._block_manager.broadcast_send_unconfirmed_block, candidate_block) self.__start_broadcast_send_unconfirmed_block_timer(broadcast_func) if await self._wait_for_voting(candidate_block) is None: return if next_leader.hex_hx() != ChannelProperty().peer_id: util.logger.spam(f"-------------------turn_to_peer " f"next_leader({next_leader.hex_hx()}) " f"peer_id({ChannelProperty().peer_id})") ObjectManager().channel_service.reset_leader( next_leader.hex_hx()) ObjectManager().channel_service.turn_on_leader_complain_timer() else: self._block_manager.epoch = Epoch.new_epoch( next_leader.hex_hx()) if not conf.ALLOW_MAKE_EMPTY_BLOCK: self.__block_generation_timer.call_instantly() else: self.__block_generation_timer.call() async def _wait_for_voting(self, candidate_block: 'Block'): """Waiting validator's vote for the candidate_block. :param candidate_block: :return: vote_result or None """ # util.logger.notice(f"_wait_for_voting block({candidate_block.header.hash})") while True: vote = self._block_manager.candidate_blocks.get_vote( candidate_block.header.hash) vote_result = vote.get_result(candidate_block.header.hash.hex(), conf.VOTING_RATIO) if vote_result: self.__stop_broadcast_send_unconfirmed_block_timer() return vote_result await asyncio.sleep(conf.WAIT_SECONDS_FOR_VOTE) timeout_timestamp = candidate_block.header.timestamp + conf.BLOCK_VOTE_TIMEOUT * 1_000_000 timeout = -util.diff_in_seconds(timeout_timestamp) try: if timeout < 0: raise asyncio.TimeoutError if await asyncio.wait_for(self._vote_queue.get(), timeout=timeout) is None: # sentinel return None except asyncio.TimeoutError: util.logger.warning( "Timed Out Block not confirmed duration: " + str(util.diff_in_seconds(candidate_block.header.timestamp)) ) return None @staticmethod def __start_broadcast_send_unconfirmed_block_timer(broadcast_func): timer_key = TimerService.TIMER_KEY_BROADCAST_SEND_UNCONFIRMED_BLOCK timer_service = ObjectManager().channel_service.timer_service timer_service.add_timer( timer_key, Timer(target=timer_key, duration=conf.INTERVAL_BROADCAST_SEND_UNCONFIRMED_BLOCK, is_repeat=True, is_run_at_start=True, callback=broadcast_func)) @staticmethod def __stop_broadcast_send_unconfirmed_block_timer(): timer_key = TimerService.TIMER_KEY_BROADCAST_SEND_UNCONFIRMED_BLOCK timer_service = ObjectManager().channel_service.timer_service if timer_key in timer_service.timer_list: timer_service.stop_timer(timer_key)
class ConsensusSiever(ConsensusBase): def __init__(self, block_manager: 'BlockManager'): super().__init__(block_manager) self.__block_generation_timer = None self.__lock = None self._loop: asyncio.BaseEventLoop = None self._vote_queue: asyncio.Queue = None util.logger.debug(f"Stop previous broadcast!") self.stop_broadcast_send_unconfirmed_block_timer() def start_timer(self, timer_service: TimerService): self._loop = timer_service.get_event_loop() self.__lock = asyncio.Lock(loop=self._loop) self.__block_generation_timer = SlotTimer( TimerService.TIMER_KEY_BLOCK_GENERATE, conf.INTERVAL_BLOCKGENERATION, timer_service, self.consensus, self.__lock, self._loop, call_instantly=not conf.ALLOW_MAKE_EMPTY_BLOCK) self.__block_generation_timer.start( is_run_at_start=conf.ALLOW_MAKE_EMPTY_BLOCK is False) def __put_vote(self, vote): async def _put(): if self._vote_queue is not None: await self._vote_queue.put(vote) # sentinel asyncio.run_coroutine_threadsafe(_put(), self._loop) def stop(self): self.__block_generation_timer.stop() if self._loop: self.__put_vote(None) @property def is_running(self): return self.__block_generation_timer.is_running def vote(self, vote): if self._loop: self.__put_vote(vote) return util.logger.debug("Cannot vote before starting consensus.") # raise RuntimeError("Cannot vote before starting consensus.") def __build_candidate_block(self, block_builder: 'BlockBuilder'): last_block = self._blockchain.last_block block_builder.height = last_block.header.height + 1 block_builder.prev_hash = last_block.header.hash block_builder.signer = ChannelProperty().peer_auth block_builder.confirm_prev_block = (block_builder.version == '0.1a') if block_builder.version == '0.1a' or (not block_builder.next_leader and not block_builder.reps): block_builder.next_leader = ExternalAddress.fromhex_address( self._block_manager.epoch.leader_id) block_builder.reps = self._block_manager.epoch.reps try: if block_builder.next_reps is None: # to build temporary block (version >= 0.4) block_builder.next_reps = [] except AttributeError as e: util.logger.info(f"block_version = {block_builder.version} : {e}") return block_builder.build() async def __add_block(self, block: Block): vote = await self._wait_for_voting(block) if not vote: raise NotEnoughVotes elif not vote.get_result(): raise InvalidBlock self._blockchain.add_block(block, confirm_info=vote.votes) self._block_manager.candidate_blocks.remove_block(block.header.hash) self._blockchain.last_unconfirmed_block = None def _makeup_new_block(self, block_version, complain_votes, block_hash): self._blockchain.last_unconfirmed_block = None dumped_votes = self._blockchain.find_confirm_info_by_hash(block_hash) if block_version == '0.1a': votes = dumped_votes else: votes = BlockVotes.deserialize_votes( json.loads(dumped_votes.decode('utf-8'))) return self._block_manager.epoch.makeup_block(complain_votes, votes) def __get_complaint_votes(self): if self._block_manager.epoch.complained_result: return self._block_manager.epoch.complain_votes[ self._block_manager.epoch.round - 1] return None async def consensus(self): util.logger.debug(f"-------------------consensus-------------------") async with self.__lock: if self._block_manager.epoch.leader_id != ChannelProperty( ).peer_id: util.logger.warning( f"This peer is not leader. epoch leader={self._block_manager.epoch.leader_id}" ) self._vote_queue = asyncio.Queue(loop=self._loop) complain_votes = self.__get_complaint_votes() complained_result = self._block_manager.epoch.complained_result if complained_result: self._blockchain.last_unconfirmed_block = None else: self._block_manager.epoch.remove_duplicate_tx_when_turn_to_leader( ) last_block_vote_list = await self.__get_votes( self._blockchain.latest_block.header.hash) if last_block_vote_list is None: return last_unconfirmed_block: Optional[ Block] = self._blockchain.last_unconfirmed_block last_block_header = self._blockchain.last_block.header if last_block_header.prep_changed: new_term = last_unconfirmed_block is None else: new_term = False if last_unconfirmed_block and not last_block_vote_list and not new_term: return # unrecorded_block means the last block of term to add prep changed block. if last_unconfirmed_block and last_unconfirmed_block.header.prep_changed: first_leader_of_term = self._blockchain.find_preps_ids_by_roothash( last_unconfirmed_block.header.revealed_next_reps_hash)[0] is_unrecorded_block = ChannelProperty( ).peer_address != first_leader_of_term else: is_unrecorded_block = False skip_add_tx = is_unrecorded_block or complained_result block_builder = self._block_manager.epoch.makeup_block( complain_votes, last_block_vote_list, new_term, skip_add_tx) need_next_call = False try: if complained_result or new_term: util.logger.spam( "consensus block_builder.complained or new term") """ confirm_info = self._blockchain.find_confirm_info_by_hash(self._blockchain.last_block.header.hash) if not confirm_info and self._blockchain.last_block.header.height > 0: util.logger.spam("Can't make a block as a leader, this peer will be complained too.") return """ block_builder = self._makeup_new_block( block_builder.version, complain_votes, self._blockchain.last_block.header.hash) elif self._blockchain.my_made_block_count == ( conf.MAX_MADE_BLOCK_COUNT - 2): # (conf.MAX_MADE_BLOCK_COUNT - 2) means if made_block_count is 8, # but after __add_block, it becomes 9 # so next unconfirmed block height is 10 (last). if last_unconfirmed_block: await self.__add_block(last_unconfirmed_block) else: util.logger.info( f"This leader already made " f"{self._blockchain.my_made_block_count} blocks. " f"MAX_MADE_BLOCK_COUNT is {conf.MAX_MADE_BLOCK_COUNT} " f"There is no more right. Consensus loop will return." ) return elif len(block_builder.transactions) == 0 and not conf.ALLOW_MAKE_EMPTY_BLOCK and \ (last_unconfirmed_block and len(last_unconfirmed_block.body.transactions) == 0): need_next_call = True elif last_unconfirmed_block: await self.__add_block(last_unconfirmed_block) except (NotEnoughVotes, InvalidBlock): need_next_call = True except ThereIsNoCandidateBlock: util.logger.warning(f"There is no candidate block.") return finally: if need_next_call: return self.__block_generation_timer.call() util.logger.spam( f"self._block_manager.epoch.leader_id: {self._block_manager.epoch.leader_id}" ) candidate_block = self.__build_candidate_block(block_builder) candidate_block, invoke_results = self._blockchain.score_invoke( candidate_block, self._blockchain.latest_block, is_block_editable=True, is_unrecorded_block=is_unrecorded_block) util.logger.spam(f"candidate block : {candidate_block.header}") self._block_manager.candidate_blocks.add_block( candidate_block, self._blockchain.find_preps_addresses_by_header( candidate_block.header)) self.__broadcast_block(candidate_block) if is_unrecorded_block: self._blockchain.last_unconfirmed_block = None else: self._block_manager.vote_unconfirmed_block( candidate_block, self._block_manager.epoch.round, True) self._blockchain.last_unconfirmed_block = candidate_block try: await self._wait_for_voting(candidate_block) except NotEnoughVotes: return if not candidate_block.header.prep_changed: if (self._blockchain.made_block_count_reached_max( self._blockchain.last_block) or self._block_manager.epoch.leader_id != ChannelProperty().peer_id): ObjectManager().channel_service.reset_leader( self._block_manager.epoch.leader_id) self.__block_generation_timer.call() async def _wait_for_voting(self, block: 'Block'): """Waiting validator's vote for the candidate_block. :param block: :return: vote_result or None """ while True: vote = self._block_manager.candidate_blocks.get_votes( block.header.hash, self._block_manager.epoch.round) if not vote: raise ThereIsNoCandidateBlock util.logger.info(f"Votes : {vote.get_summary()}") if vote.is_completed(): self._block_manager.epoch.complained_result = None self.stop_broadcast_send_unconfirmed_block_timer() return vote await asyncio.sleep(conf.WAIT_SECONDS_FOR_VOTE) try: timeout = self.__check_timeout(block) if not await asyncio.wait_for(self._vote_queue.get(), timeout=timeout): # sentinel raise NotEnoughVotes except (TimeoutError, asyncio.TimeoutError): util.logger.warning( "Timed Out Block not confirmed duration: " + str(util.diff_in_seconds(block.header.timestamp))) raise NotEnoughVotes def __check_timeout(self, block): timeout_timestamp = block.header.timestamp + conf.BLOCK_VOTE_TIMEOUT * 1_000_000 timeout = -util.diff_in_seconds(timeout_timestamp) if timeout < 0: raise TimeoutError return timeout async def __get_votes(self, block_hash: Hash32): try: prev_votes = self._block_manager.candidate_blocks.get_votes( block_hash, self._block_manager.epoch.round) except KeyError as e: util.logger.spam(f"There is no block in candidates list: {e}") prev_votes = None if prev_votes: try: last_unconfirmed_block = self._blockchain.last_unconfirmed_block if last_unconfirmed_block is None: warning_msg = f"There is prev_votes({prev_votes}). But I have no last_unconfirmed_block." if self._blockchain.find_block_by_hash(block_hash): warning_msg += "\nBut already added block so no longer have to wait for the vote." # TODO An analysis of the cause of this situation is necessary. util.logger.notice(warning_msg) self._block_manager.candidate_blocks.remove_block( block_hash) else: util.logger.warning(warning_msg) return None self.__check_timeout(last_unconfirmed_block) if not prev_votes.is_completed(): self.__broadcast_block(last_unconfirmed_block) if await self._wait_for_voting(last_unconfirmed_block ) is None: return None prev_votes_list = prev_votes.votes except TimeoutError: util.logger.warning(f"Timeout block of hash : {block_hash}") if self._block_manager.epoch.complained_result: self._blockchain.last_unconfirmed_block = None self.stop_broadcast_send_unconfirmed_block_timer() ObjectManager().channel_service.state_machine.switch_role() return None except NotEnoughVotes: if last_unconfirmed_block: util.logger.warning( f"The last unconfirmed block has not enough votes. {block_hash}" ) return None else: util.exit_and_msg( f"The block that has not enough votes added to the blockchain." ) else: prev_votes_dumped = self._blockchain.find_confirm_info_by_hash( block_hash) try: prev_votes_serialized = json.loads(prev_votes_dumped) except json.JSONDecodeError as e: # handle exception for old votes util.logger.spam(f"{e}") prev_votes_list = [] except TypeError as e: # handle exception for not existing (NoneType) votes util.logger.spam(f"{e}") prev_votes_list = [] else: prev_votes_list = BlockVotes.deserialize_votes( prev_votes_serialized) return prev_votes_list @staticmethod def __start_broadcast_send_unconfirmed_block_timer(broadcast_func): timer_key = TimerService.TIMER_KEY_BROADCAST_SEND_UNCONFIRMED_BLOCK timer_service = ObjectManager().channel_service.timer_service timer_service.add_timer( timer_key, Timer(target=timer_key, duration=conf.INTERVAL_BROADCAST_SEND_UNCONFIRMED_BLOCK, is_repeat=True, repeat_timeout=conf.TIMEOUT_FOR_LEADER_COMPLAIN, is_run_at_start=True, callback=broadcast_func)) @staticmethod def stop_broadcast_send_unconfirmed_block_timer(): timer_key = TimerService.TIMER_KEY_BROADCAST_SEND_UNCONFIRMED_BLOCK timer_service = ObjectManager().channel_service.timer_service if timer_key in timer_service.timer_list: timer_service.stop_timer(timer_key) def __broadcast_block(self, block: 'Block'): broadcast_func = partial( self._block_manager.broadcast_send_unconfirmed_block, block, self._block_manager.epoch.round) self.__start_broadcast_send_unconfirmed_block_timer(broadcast_func)