def add_complain(self, vote: LeaderVote): util.logger.spam(f"add_complain vote({vote})") if not self.epoch: util.logger.debug(f"Epoch is not initialized.") return if self.epoch.height == vote.block_height: if self.epoch.round == vote.round_: self.epoch.add_complain(vote) elif self.epoch.round > vote.round_: if vote.new_leader != ExternalAddress.empty(): self.__send_fail_leader_vote(vote) else: return else: # TODO: do round sync return elected_leader = self.epoch.complain_result() if elected_leader: if elected_leader == ExternalAddress.empty().hex_xx( ) and vote.round_ == self.epoch.round: util.logger.warning( f"Fail to elect the next leader on {self.epoch.round} round." ) elected_leader = self.blockchain.get_next_rep_in_reps( ExternalAddress.fromhex(self.epoch.leader_id), self.epoch.reps).hex_hx() if self.epoch.round == vote.round_: self.__channel_service.reset_leader(elected_leader, complained=True) elif self.epoch.height < vote.block_height: self.__channel_service.state_machine.block_sync()
def test_leader_votes_completed_with_out_of_round(self): ratio = 0.51 old_leader = self.reps[0] next_leader = self.reps[1] by_higher_rounder = ExternalAddress.empty() leader_votes = LeaderVotes(self.reps, ratio, 0, 0, old_leader) for i, (rep, signer) in enumerate(zip(self.reps[:26], self.signers[:26])): leader_vote = LeaderVote.new(signer, 0, 0, 0, old_leader, next_leader) leader_votes.add_vote(leader_vote) leader_votes.get_summary() print(f"leader_votes.is_completed(): {leader_votes.is_completed()}") print(f"leader_votes.get_result(): {leader_votes.get_result()}") self.assertEqual(leader_votes.is_completed(), False) self.assertEqual(leader_votes.get_result(), None) for i, (rep, signer) in enumerate(zip(self.reps[26:55], self.signers[26:55])): leader_vote = LeaderVote.new(signer, 0, 0, 0, old_leader, by_higher_rounder) leader_votes.add_vote(leader_vote) leader_votes.get_summary() print(f"leader_votes.is_completed(): {leader_votes.is_completed()}") print(f"leader_votes.get_result(): {leader_votes.get_result()}") self.assertEqual(leader_votes.is_completed(), True) self.assertEqual(leader_votes.get_result(), next_leader)
def __send_fail_leader_vote(self, leader_vote: LeaderVote): version = self.blockchain.block_versioner.get_version( leader_vote.block_height) fail_vote = Vote.get_leader_vote_class(version).new( signer=ChannelProperty().peer_auth, block_height=leader_vote.block_height, round_=leader_vote.round, old_leader=leader_vote.old_leader, new_leader=ExternalAddress.empty(), timestamp=util.get_time_stamp()) fail_vote_dumped = json.dumps(fail_vote.serialize()) request = loopchain_pb2.ComplainLeaderRequest( complain_vote=fail_vote_dumped, channel=self.channel_name) reps_hash = self.blockchain.last_block.header.revealed_next_reps_hash or ChannelProperty( ).crep_root_hash rep_id = leader_vote.rep.hex_hx() target = self.blockchain.find_preps_targets_by_roothash( reps_hash)[rep_id] util.logger.debug(f"fail leader complain " f"complained_leader_id({leader_vote.old_leader}), " f"new_leader_id({ExternalAddress.empty()})," f"round({leader_vote.round})," f"target({target})") self.__channel_service.broadcast_scheduler.schedule_send_failed_leader_complain( "ComplainLeader", request, target=target)
def is_unrecorded(self) -> bool: """Return is unrecorded block :return: bool """ return (self.next_leader == ExternalAddress.empty() and self.reps_hash == self.next_reps_hash == Hash32.empty())
def test_prep_changed_by_term_end_if_next_leader_is_empty(self, header_factory): header = header_factory(next_leader=ExternalAddress.empty(), reps_hash=Hash32(os.urandom(Hash32.size)), next_reps_hash=Hash32(os.urandom(Hash32.size))) assert header.prep_changed assert header.prep_changed_reason is NextRepsChangeReason.TermEnd
def verify_leader_votes(self, block: 'Block', prev_block: 'Block', reps: Sequence[ExternalAddress]): body: BlockBody = block.body if body.leader_votes: any_vote = next(vote for vote in body.leader_votes if vote) votes_class = v0_5.LeaderVotes if any_vote.version else v0_1a.LeaderVotes leader_votes = votes_class( reps, conf.VOTING_RATIO, block.header.height, any_vote.round, any_vote.old_leader, body.leader_votes) if leader_votes.get_result() == ExternalAddress.empty(): if leader_votes.block_height != block.header.height: exception = RuntimeError(f"Block({block.header.height}, {block.header.hash.hex()}, " f"Height({block.header.height}), " f"Expected({leader_votes.round}).") self._handle_exception(exception) elif leader_votes.get_result() != block.header.peer_id: exception = RuntimeError(f"Block({block.header.height}, {block.header.hash.hex()}, " f"Leader({block.header.peer_id.hex_xx()}), " f"Expected({leader_votes.get_result()}).") self._handle_exception(exception) try: leader_votes.verify() except Exception as e: # FIXME : leader_votes.verify does not verify all votes when raising an exception. self._handle_exception(e) else: prev_block_header: BlockHeader = prev_block.header if prev_block_header.next_leader != block.header.peer_id and not prev_block_header.prep_changed: exception = RuntimeError(f"Block({block.header.height}, {block.header.hash.hex()}, " f"Leader({block.header.peer_id.hex_xx()}), " f"Expected({prev_block_header.next_leader.hex_xx()}).\n " f"LeaderVotes({body.leader_votes}") self._handle_exception(exception)
def add_complain(self, vote: LeaderVote): util.logger.debug(f"vote({vote})") if not self.preps_contain(vote.rep): util.logger.debug(f"ignore vote from unknown prep: {vote.rep.hex_hx()}") return if not self.epoch: util.logger.debug(f"Epoch is not initialized.") return if self.epoch.height == vote.block_height: if self.epoch.round == vote.round: self.epoch.add_complain(vote) elected_leader = self.epoch.complain_result() if elected_leader: self.__channel_service.reset_leader(elected_leader, complained=True) elif self.epoch.round > vote.round: if vote.new_leader != ExternalAddress.empty(): self.__send_fail_leader_vote(vote) else: return else: # TODO: do round sync return elif self.epoch.height < vote.block_height: self.__channel_service.state_machine.block_sync()
def __get_next_leader_by_block(self, block: Block) -> str: if block.header.next_leader is None: if block.header.peer_id: return block.header.peer_id.hex_hx() else: return ExternalAddress.empty().hex_hx() else: return block.header.next_leader.hex_hx()
def deserialize(cls, votes_data: List[Dict], voting_ratio: float): if votes_data: votes = [LeaderVote.deserialize(vote_data) for vote_data in votes_data] reps = [vote.rep for vote in votes] votes_instance = cls(reps, voting_ratio, votes[0].block_height, votes[0].round_, votes[0].old_leader) for vote in votes: index = reps.index(vote.rep) votes_instance.votes[index] = vote return votes_instance else: return cls([], voting_ratio, -1, -1, ExternalAddress.empty())
def prep_changed_reason(self) -> Optional[NextRepsChangeReason]: """Return prep changed reason :return: NextRepsChangeReason : NoChange, TermEnd, Penalty """ if not self.prep_changed and not self.is_unrecorded: return NextRepsChangeReason.NoChange if self.next_leader == ExternalAddress.empty(): return NextRepsChangeReason.TermEnd return NextRepsChangeReason.Penalty
def _build_next_leader(self): if self.next_reps_change_reason is NextRepsChangeReason.TermEnd: return ExternalAddress.empty() elif self.next_reps_change_reason is NextRepsChangeReason.Penalty: if not self.is_max_made_block_count and self.peer_id in self.next_reps: next_index = self.reps.index(self.peer_id) else: curr_index = self.reps.index(self.peer_id) next_index = curr_index + 1 next_index = next_index if next_index < len(self.next_reps) else 0 return self.next_reps[next_index] else: return self.next_leader
def is_failed(self, value: ExternalAddress, count: int) -> bool: return value == ExternalAddress.empty() and count >= len( self.reps) - self.quorum + 1
def get_out_of_round(self): counter = Counter(vote.result() for vote in self.votes if vote) out_of_round = counter[ExternalAddress.empty()] return out_of_round
def get_majority(self): counter = Counter(vote.result() for vote in self.votes if (vote and vote.result() != ExternalAddress.empty())) majorities = counter.most_common() return majorities
def empty(cls, rep: ExternalAddress, block_height: int, round_: int, old_leader: ExternalAddress): return cls(rep, 0, Signature.empty(), block_height, round_, old_leader, ExternalAddress.empty())