示例#1
0
 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()
示例#2
0
 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)
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)
示例#4
0
 def start_timer(self, timer_service):
     self.__block_generation_timer = SlotTimer(
         TimerService.TIMER_KEY_BLOCK_GENERATE,
         conf.INTERVAL_BLOCKGENERATION, timer_service, self.consensus,
         self.__lock)
示例#5
0
class ConsensusSiever(ConsensusBase):
    def __init__(self, block_manager):
        super().__init__(block_manager)
        self.__block_generation_timer = None
        self.__lock = threading.Lock()

    def start_timer(self, timer_service):
        self.__block_generation_timer = SlotTimer(
            TimerService.TIMER_KEY_BLOCK_GENERATE,
            conf.INTERVAL_BLOCKGENERATION, timer_service, self.consensus,
            self.__lock)

    def stop(self):
        self.__block_generation_timer.stop()
        self.__stop_broadcast_send_unconfirmed_block_timer()

    async def consensus(self):
        util.logger.debug(
            f"-------------------consensus "
            f"candidate_blocks({len(self._blockmanager.candidate_blocks.blocks)})"
        )
        with self.__lock:
            block_builder = self._makeup_block()
            vote_result = None

            if len(block_builder.transactions) > 0:
                # util.logger.debug(f"-------------------consensus logic-1")
                next_leader = ExternalAddress.fromhex(
                    ChannelProperty().peer_id)

                if self._blockchain.last_unconfirmed_block:
                    if (len(self._blockchain.last_unconfirmed_block.body.
                            transactions) > 0) or (
                                len(self._blockchain.last_unconfirmed_block.
                                    body.transactions) == 0 and
                                self._blockchain.last_unconfirmed_block.header.
                                peer_id.hex_hx() != ChannelProperty().peer_id):
                        # util.logger.debug(f"-------------------consensus logic-2")
                        vote = self._blockmanager.candidate_blocks.get_vote(
                            self._blockchain.last_unconfirmed_block.header.hash
                        )
                        vote_result = vote.get_result(
                            self._blockchain.last_unconfirmed_block.header.
                            hash.hex(), conf.VOTING_RATIO)
                        if not vote_result:
                            return self.__block_generation_timer.call()

                        self._blockmanager.add_block(
                            self._blockchain.last_unconfirmed_block, vote)
                        self._made_block_count += 1

                        next_leader = self._blockchain.last_unconfirmed_block.header.next_leader
            else:
                if self._blockchain.last_unconfirmed_block and len(
                        self._blockchain.last_unconfirmed_block.body.
                        transactions) > 0:
                    # util.logger.debug(f"-------------------consensus logic-3")
                    vote = self._blockmanager.candidate_blocks.get_vote(
                        self._blockchain.last_unconfirmed_block.header.hash)
                    vote_result = vote.get_result(
                        self._blockchain.last_unconfirmed_block.header.hash.
                        hex(), conf.VOTING_RATIO)
                    if not vote_result:
                        return self.__block_generation_timer.call()

                    self._blockmanager.add_block(
                        self._blockchain.last_unconfirmed_block, vote)
                    self._made_block_count += 1

                    peer_manager = ObjectManager().channel_service.peer_manager
                    next_leader = ExternalAddress.fromhex(
                        peer_manager.get_next_leader_peer().peer_id)
                else:
                    # util.logger.spam(f"tx count in block({len(block_builder.transactions)})")
                    return self.__block_generation_timer.call()

            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.peer_private_key = ObjectManager(
            ).channel_service.peer_auth.peer_private_key
            block_builder.confirm_prev_block = vote_result or (
                self._made_block_count > 0)

            candidate_block = block_builder.build()
            candidate_block, invoke_results = ObjectManager(
            ).channel_service.score_invoke(candidate_block)
            self._blockmanager.set_invoke_results(
                candidate_block.header.hash.hex(), invoke_results)

            block_verifier = BlockVerifier.new(candidate_block.header.version,
                                               self._blockchain.tx_versioner)
            block_verifier.verify(candidate_block, self._blockchain.last_block,
                                  self._blockchain)

            logging.debug(f"candidate block : {candidate_block.header}")

            self._blockmanager.vote_unconfirmed_block(
                candidate_block.header.hash, True)
            self._blockmanager.candidate_blocks.add_block(candidate_block)

            self._blockchain.last_unconfirmed_block = candidate_block
            broadcast_func = partial(
                self._blockmanager.broadcast_send_unconfirmed_block,
                candidate_block)
            self.__start_broadcast_send_unconfirmed_block_timer(broadcast_func)

            if len(block_builder.transactions) == 0 and not conf.ALLOW_MAKE_EMPTY_BLOCK and \
                    next_leader.hex() != ChannelProperty().peer_id:
                # util.logger.debug(f"-------------------turn_to_peer")
                ObjectManager().channel_service.state_machine.turn_to_peer()
            else:
                self.__block_generation_timer.call()

    def count_votes(self, block_hash: Hash32):
        # count votes
        vote = self._blockmanager.candidate_blocks.get_vote(block_hash)
        if not vote.get_result(block_hash.hex(), conf.VOTING_RATIO):
            return True  # vote not complete yet

        self.__stop_broadcast_send_unconfirmed_block_timer()

    # async def _wait_for_voting(self, candidate_block: 'Block'):
    #     while True:
    #         result = self._blockmanager.candidate_blocks.get_vote_result(candidate_block.header.hash)
    #         if result:
    #             return True
    #
    #         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
    #
    #             vote_result = await asyncio.wait_for(self._vote_queue.get(), timeout=timeout)
    #             if vote_result is None:  # sentinel
    #                 return False
    #
    #         except asyncio.TimeoutError:
    #             logging.warning("Timed Out Block not confirmed duration: " +
    #                             str(util.diff_in_seconds(candidate_block.header.timestamp)))
    #             return False

    @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)
示例#6
0
class ConsensusSiever(ConsensusBase):
    def __init__(self, block_manager):
        super().__init__(block_manager)
        self.__block_generation_timer = None
        self.__lock = threading.Lock()

    def start_timer(self, timer_service):
        self.__block_generation_timer = SlotTimer(
            TimerService.TIMER_KEY_BLOCK_GENERATE,
            conf.INTERVAL_BLOCKGENERATION, timer_service, self.consensus,
            self.__lock)

    def stop(self):
        self.__block_generation_timer.stop()
        self.__stop_broadcast_send_unconfirmed_block_timer()

    async def consensus(self):
        util.logger.debug(
            f"-------------------consensus "
            f"candidate_blocks({len(self._block_manager.candidate_blocks.blocks)})"
        )
        with self.__lock:
            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)

            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
                vote_result = True
                self._block_manager.epoch.set_epoch_leader(
                    ChannelProperty().peer_id)
                self._made_block_count += 1
            elif len(block_builder.transactions) > 0:
                util.logger.spam(
                    f"consensus len(block_builder.transactions) > 0")
                if last_unconfirmed_block:
                    if (len(last_unconfirmed_block.body.transactions) > 0
                            or last_unconfirmed_block.header.complained) or (
                                len(last_unconfirmed_block.body.transactions)
                                == 0 and
                                last_unconfirmed_block.header.peer_id.hex_hx()
                                != ChannelProperty().peer_id):
                        vote = self._block_manager.candidate_blocks.get_vote(
                            last_unconfirmed_block.header.hash)
                        vote_result = vote.get_result(
                            last_unconfirmed_block.header.hash.hex(),
                            conf.VOTING_RATIO)
                        if not vote_result:
                            return self.__block_generation_timer.call()

                        self.__add_block(last_unconfirmed_block, vote)

                        next_leader = last_unconfirmed_block.header.next_leader
            else:
                if (last_unconfirmed_block) and (
                        len(last_unconfirmed_block.body.transactions) > 0
                        or last_unconfirmed_block.header.complained):
                    vote = self._block_manager.candidate_blocks.get_vote(
                        last_unconfirmed_block.header.hash)
                    vote_result = vote.get_result(
                        last_unconfirmed_block.header.hash.hex(),
                        conf.VOTING_RATIO)
                    if not vote_result:
                        return self.__block_generation_timer.call()

                    self.__add_block(last_unconfirmed_block, vote)

                    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:
                    return self.__block_generation_timer.call()

            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.peer_private_key = ObjectManager(
            ).channel_service.peer_auth.private_key
            block_builder.confirm_prev_block = vote_result or (
                self._made_block_count > 0)

            candidate_block = block_builder.build()
            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}")
            block_verifier = BlockVerifier.new(candidate_block.header.version,
                                               self._blockchain.tx_versioner)
            block_verifier.verify(candidate_block, self._blockchain.last_block,
                                  self._blockchain)

            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)

            # TODO Temporary ignore below line for developing leader complain
            self.__start_broadcast_send_unconfirmed_block_timer(broadcast_func)

            if len(block_builder.transactions) == 0 and not conf.ALLOW_MAKE_EMPTY_BLOCK and \
                    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})")
                await ObjectManager().channel_service.reset_leader(
                    next_leader.hex_hx())
            else:
                self.__block_generation_timer.call()

    def count_votes(self, block_hash: Hash32):
        # count votes
        vote = self._block_manager.candidate_blocks.get_vote(block_hash)
        if not vote.get_result(block_hash.hex(), conf.VOTING_RATIO):
            return True  # vote not complete yet

        self.__stop_broadcast_send_unconfirmed_block_timer()

    # async def _wait_for_voting(self, candidate_block: 'Block'):
    #     while True:
    #         result = self._block_manager.candidate_blocks.get_vote_result(candidate_block.header.hash)
    #         if result:
    #             return True
    #
    #         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
    #
    #             vote_result = await asyncio.wait_for(self._vote_queue.get(), timeout=timeout)
    #             if vote_result is None:  # sentinel
    #                 return False
    #
    #         except asyncio.TimeoutError:
    #             logging.warning("Timed Out Block not confirmed duration: " +
    #                             str(util.diff_in_seconds(candidate_block.header.timestamp)))
    #             return False

    def __add_block(self, block: Block, vote: Vote):
        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

    @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)
示例#7
0
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)