예제 #1
0
    def __init__(self, name: str, channel_manager, peer_id, channel_name,
                 level_db_identity):
        self.__channel_service: ChannelService = channel_manager
        self.__channel_name = channel_name
        self.__pre_validate_strategy = self.__pre_validate
        self.__peer_id = peer_id
        self.__level_db = None
        self.__level_db_path = ""
        self.__level_db, self.__level_db_path = util.init_level_db(
            level_db_identity=f"{level_db_identity}_{channel_name}",
            allow_rename_path=False)
        self.__txQueue = AgingCache(
            max_age_seconds=conf.MAX_TX_QUEUE_AGING_SECONDS,
            default_item_status=TransactionStatusInQueue.normal)
        self.__unconfirmedBlockQueue = queue.Queue()
        self.__blockchain = BlockChain(self.__level_db, channel_name)
        self.__peer_type = None
        self.__consensus = None
        self.__consensus_algorithm = None
        self.candidate_blocks = CandidateBlocks()
        self.__block_height_sync_lock = threading.Lock()
        self.__block_height_thread_pool = ThreadPoolExecutor(
            1, 'BlockHeightSyncThread')
        self.__block_height_future: Future = None
        self.__subscribe_target_peer_stub = None
        self.__block_generation_scheduler = BlockGenerationScheduler(
            self.__channel_name)
        self.__precommit_block: Block = None
        self.set_peer_type(loopchain_pb2.PEER)
        self.name = name
        self.__service_status = status_code.Service.online

        self.epoch: Epoch = None
예제 #2
0
    def __init__(self,
                 channel_service: 'ChannelService',
                 channel: str = None,
                 **kwargs):
        CommonThread.__init__(self)
        Publisher.__init__(self, [
            Consensus.EVENT_COMPLETE_CONSENSUS,
            Consensus.EVENT_LEADER_COMPLAIN_F_1,
            Consensus.EVENT_LEADER_COMPLAIN_2F_1, Consensus.EVENT_MAKE_BLOCK
        ])

        self.channel_name = channel
        self.__channel_service = channel_service
        self.__peer_manager = channel_service.peer_manager
        self.__last_epoch: Epoch = None
        self.__precommit_block: Block = None
        self.__epoch: Epoch = None
        self.__leader_id = None
        self.__tx_queue = AgingCache(
            max_age_seconds=conf.MAX_TX_QUEUE_AGING_SECONDS,
            default_item_status=TransactionStatusInQueue.normal)
        self.__sleep_time = None
        self.__run_logic = None
        self.__block_generation_scheduler = BlockGenerationScheduler(
            self.channel_name)

        self.__init_data()
예제 #3
0
    def __init__(self, channel_manager, peer_id, channel_name,
                 level_db_identity):
        super().__init__()

        self.__channel_service = channel_manager
        self.__channel_name = channel_name
        self.__pre_validate_strategy = None
        self.__set_send_tx_type(
            conf.CHANNEL_OPTION[channel_name]["send_tx_type"])
        self.__peer_id = peer_id
        self.__level_db = None
        self.__level_db_path = ""
        self.__level_db, self.__level_db_path = util.init_level_db(
            level_db_identity=f"{level_db_identity}_{channel_name}",
            allow_rename_path=False)
        self.__txQueue = AgingCache(
            max_age_seconds=conf.MAX_TX_QUEUE_AGING_SECONDS,
            default_item_status=TransactionStatusInQueue.normal)
        self.__unconfirmedBlockQueue = queue.Queue()
        self.__candidate_blocks = None
        self.__candidate_blocks = CandidateBlocks(peer_id, channel_name)
        self.__blockchain = BlockChain(self.__level_db, channel_name)
        self.__peer_type = None
        self.__block_type = BlockType.general
        self.__consensus = None
        self.__consensus_algorithm = None
        self.__run_logic = None
        self.__block_height_sync_lock = threading.Lock()
        self.__block_height_thread_pool = ThreadPoolExecutor(
            1, 'BlockHeightSyncThread')
        self.__block_height_future: Future = None
        self.__block_generation_scheduler = BlockGenerationScheduler(
            self.__channel_name)
        self.__prev_epoch: Epoch = None
        self.__precommit_block: Block = None
        self.__epoch: Epoch = None
        self._event_list = [("complete_consensus",
                             self.callback_complete_consensus)]
        self.set_peer_type(loopchain_pb2.PEER)
        self.name = "loopchain.peer.BlockManager"
        self.__service_status = status_code.Service.online
예제 #4
0
class BlockManager(CommonThread, Subscriber):
    """P2P Service 를 담당하는 BlockGeneratorService, PeerService 와 분리된
    Thread 로 BlockChain 을 관리한다.
    BlockGenerator 의 BlockManager 는 주기적으로 Block 을 생성하여 Peer 로 broadcast 한다.
    Peer 의 BlockManager 는 전달 받은 Block 을 검증 처리 한다.
    """

    MAINNET = "cf43b3fd45981431a0e64f79d07bfcf703e064b73b802c5f32834eec72142190"
    TESTNET = "885b8021826f7e741be7f53bb95b48221e9ab263f377e997b2e47a7b8f4a2a8b"

    def __init__(self, channel_manager, peer_id, channel_name,
                 level_db_identity):
        super().__init__()

        self.__channel_service = channel_manager
        self.__channel_name = channel_name
        self.__pre_validate_strategy = None
        self.__set_send_tx_type(
            conf.CHANNEL_OPTION[channel_name]["send_tx_type"])
        self.__peer_id = peer_id
        self.__level_db = None
        self.__level_db_path = ""
        self.__level_db, self.__level_db_path = util.init_level_db(
            level_db_identity=f"{level_db_identity}_{channel_name}",
            allow_rename_path=False)
        self.__txQueue = AgingCache(
            max_age_seconds=conf.MAX_TX_QUEUE_AGING_SECONDS,
            default_item_status=TransactionStatusInQueue.normal)
        self.__unconfirmedBlockQueue = queue.Queue()
        self.__candidate_blocks = None
        self.__candidate_blocks = CandidateBlocks(peer_id, channel_name)
        self.__blockchain = BlockChain(self.__level_db, channel_name)
        self.__peer_type = None
        self.__block_type = BlockType.general
        self.__consensus = None
        self.__consensus_algorithm = None
        self.__run_logic = None
        self.__block_height_sync_lock = threading.Lock()
        self.__block_height_thread_pool = ThreadPoolExecutor(
            1, 'BlockHeightSyncThread')
        self.__block_height_future: Future = None
        self.__block_generation_scheduler = BlockGenerationScheduler(
            self.__channel_name)
        self.__prev_epoch: Epoch = None
        self.__precommit_block: Block = None
        self.__epoch: Epoch = None
        self._event_list = [("complete_consensus",
                             self.callback_complete_consensus)]
        self.set_peer_type(loopchain_pb2.PEER)
        self.name = "loopchain.peer.BlockManager"
        self.__service_status = status_code.Service.online

    def __set_send_tx_type(self, send_tx_type):
        if send_tx_type == conf.SendTxType.icx:
            self.__pre_validate_strategy = self.__pre_validate
        else:
            self.__pre_validate_strategy = self.__pre_validate_pass

    @property
    def channel_name(self):
        return self.__channel_name

    @property
    def service_status(self):
        # Return string for compatibility.
        if self.__service_status >= 0:
            return "Service is online: " + str(self.peer_type)
        else:
            return "Service is offline: " + status_code.get_status_reason(
                self.__service_status)

    def __update_service_status(self, status):
        self.__service_status = status
        StubCollection().peer_stub.sync_task().update_status(
            self.__channel_name, {"status": self.service_status})

    @property
    def peer_type(self):
        return self.__peer_type

    @property
    def consensus(self):
        return self.__consensus

    @consensus.setter
    def consensus(self, consensus: Consensus):
        self.__consensus = consensus

    @property
    def consensus_algorithm(self):
        return self.__consensus_algorithm

    @consensus_algorithm.setter
    def consensus_algorithm(self, consensus_algorithm):
        self.__consensus_algorithm = consensus_algorithm

    @property
    def precommit_block(self):
        return self.__precommit_block

    @precommit_block.setter
    def precommit_block(self, block):
        self.__precommit_block = block

    @property
    def block_type(self):
        return self.__block_type

    @block_type.setter
    def block_type(self, block_type):
        self.__block_type = block_type

    @property
    def block_generation_scheduler(self):
        return self.__block_generation_scheduler

    def get_level_db(self):
        return self.__level_db

    def clear_all_blocks(self):
        logging.debug(f"clear level db({self.__level_db_path})")
        shutil.rmtree(self.__level_db_path)

    def set_peer_type(self, peer_type):
        self.__peer_type = peer_type

        if conf.CONSENSUS_ALGORITHM == conf.ConsensusAlgorithm.lft:
            if self.__peer_type != loopchain_pb2.BLOCK_GENERATOR and self.__peer_type != loopchain_pb2.PEER:
                self.__set_run_logic_by_peer_type()
        else:
            self.__set_run_logic_by_peer_type()

    def __create_block_generation_schedule(self):
        # util.logger.spam(f"block_manager.py:__create_block_generation_schedule:: CREATE BLOCK GENERATION SCHEDULE")
        Schedule = namedtuple("Schedule", "callback kwargs")
        schedule = Schedule(self.__consensus_algorithm.consensus, {})
        self.__block_generation_scheduler.add_schedule(schedule)

        time.sleep(conf.INTERVAL_BLOCKGENERATION)

    def __set_run_logic_by_peer_type(self):
        if ChannelProperty().node_type == conf.NodeType.CommunityNode:
            if self.__peer_type == loopchain_pb2.BLOCK_GENERATOR:
                if conf.ALLOW_MAKE_EMPTY_BLOCK:
                    self.__run_logic = self.__create_block_generation_schedule
                else:
                    self.__run_logic = self.__consensus_algorithm.consensus
            elif self.__peer_type == loopchain_pb2.PEER:
                self.__run_logic = self.__do_vote
        elif ChannelProperty().node_type == conf.NodeType.CitizenNode:
            self.__run_logic = self.__do_nothing

    def set_invoke_results(self, block_hash, invoke_results):
        self.__blockchain.set_invoke_results(block_hash, invoke_results)

    def set_last_commit_state(self, block_height, commit_state):
        self.__blockchain.set_last_commit_state(block_height, commit_state)

    def get_run_logic(self):
        try:
            return self.__run_logic.__name__
        except Exception as e:
            return "unknown"

    def get_total_tx(self):
        """
        블럭체인의 Transaction total 리턴합니다.

        :return: 블럭체인안의 transaction total count
        """
        return self.__blockchain.total_tx

    def get_blockchain(self):
        return self.__blockchain

    def get_candidate_blocks(self):
        return self.__candidate_blocks

    def pre_validate(self, tx: Transaction):
        return self.__pre_validate_strategy(tx)

    def __pre_validate(self, tx: Transaction):
        if tx.tx_hash in self.__txQueue:
            raise TransactionInvalidDuplicatedHash(tx.tx_hash)

        if not util.is_in_time_boundary(tx.get_timestamp(),
                                        conf.ALLOW_TIMESTAMP_BOUNDARY_SECOND):
            raise TransactionInvalidOutOfTimeBound(tx.tx_hash,
                                                   tx.get_timestamp(),
                                                   util.get_now_time_stamp())

    def __pre_validate_pass(self, tx: Transaction):
        pass

    def broadcast_send_unconfirmed_block(self, block_: Block):
        """생성된 unconfirmed block 을 피어들에게 broadcast 하여 검증을 요청한다.
        """
        logging.debug(
            f"BroadCast AnnounceUnconfirmedBlock...peers: "
            f"{ObjectManager().channel_service.peer_manager.get_peer_count()}")

        # util.logger.spam(f'block_manager:zip_test num of tx is {block_.confirmed_tx_len}')
        block_dump = util.block_dumps(block_)
        if conf.ALLOW_MAKE_EMPTY_BLOCK or block_.confirmed_tx_len > 0:
            self.__blockchain.increase_made_block_count()

        ObjectManager().channel_service.broadcast_scheduler.schedule_broadcast(
            "AnnounceUnconfirmedBlock",
            loopchain_pb2.BlockSend(block=block_dump,
                                    channel=self.__channel_name))

    def broadcast_audience_set(self):
        """Check Broadcast Audience and Return Status

        """
        ObjectManager().channel_service.broadcast_scheduler.schedule_job(
            BroadcastCommand.STATUS, "audience set")

    def add_tx_obj(self, tx):
        """전송 받은 tx 를 Block 생성을 위해서 큐에 입력한다. load 하지 않은 채 입력한다.

        :param tx: transaction object
        """
        self.__txQueue[tx.tx_hash] = tx

    def get_tx(self, tx_hash) -> Transaction:
        """Get transaction from block_db by tx_hash

        :param tx_hash: tx hash
        :return: tx object or None
        """
        return self.__blockchain.find_tx_by_key(tx_hash)

    def get_tx_info(self, tx_hash) -> dict:
        """Get transaction info from block_db by tx_hash

        :param tx_hash: tx hash
        :return: {'block_hash': "", 'block_height': "", "transaction": "", "result": {"code": ""}}
        """
        return self.__blockchain.find_tx_info(tx_hash)

    def get_invoke_result(self, tx_hash):
        """ get invoke result by tx

        :param tx_hash:
        :return:
        """
        return self.__blockchain.find_invoke_result_by_tx_hash(tx_hash)

    def get_tx_queue(self):
        if conf.CONSENSUS_ALGORITHM == conf.ConsensusAlgorithm.lft:
            return self.__consensus.get_tx_queue()

        return self.__txQueue

    def get_count_of_unconfirmed_tx(self):
        """BlockManager 의 상태를 확인하기 위하여 현재 입력된 unconfirmed_tx 의 카운트를 구한다.

        :return: 현재 입력된 unconfirmed tx 의 갯수
        """
        return len(self.__txQueue)

    def confirm_block(self, block: Block):
        try:
            confirmed_block = self.__blockchain.confirm_block(
                block.prev_block_hash)
            if ObjectManager(
            ).channel_service.broadcast_scheduler.audience_subscriber:
                self.__broadcast_block_to_audience_subscriber(confirmed_block)

        except BlockchainError as e:
            logging.warning(
                f"BlockchainError while confirm_block({e}), retry block_height_sync"
            )
            self.block_height_sync()

    def add_unconfirmed_block(self, unconfirmed_block):
        # siever 인 경우 블럭에 담긴 투표 결과를 이전 블럭에 반영한다.
        if conf.CONSENSUS_ALGORITHM == conf.ConsensusAlgorithm.siever:
            if unconfirmed_block.prev_block_confirm:
                # logging.debug(f"block confirm by siever: "
                #               f"hash({unconfirmed_block.prev_block_hash}) "
                #               f"block.channel({unconfirmed_block.channel_name})")
                self.confirm_block(unconfirmed_block)
            elif unconfirmed_block.block_type is BlockType.peer_list:
                logging.debug(
                    f"peer manager block confirm by siever: "
                    f"hash({unconfirmed_block.block_hash}) block.channel({unconfirmed_block.channel_name})"
                )
                self.confirm_block(unconfirmed_block)
            else:
                # 투표에 실패한 블럭을 받은 경우
                # 특별한 처리가 필요 없다. 새로 받은 블럭을 아래 로직에서 add_unconfirm_block 으로 수행하면 된다.
                pass
        elif conf.CONSENSUS_ALGORITHM == conf.ConsensusAlgorithm.lft:
            if unconfirmed_block.prev_block_confirm:

                # turn off previous vote's timer when a general peer received new block for vote
                ObjectManager().peer_service.timer_service.stop_timer(
                    unconfirmed_block.prev_block_hash)
                # logging.debug(f"block confirm by lft: "
                #               f"hash({unconfirmed_block.prev_block_hash}) "
                #               f"block.channel({unconfirmed_block.channel_name})")

                self.confirm_block(unconfirmed_block)
            elif unconfirmed_block.block_type is BlockType.peer_list:
                logging.debug(
                    f"peer manager block confirm by lft: "
                    f"hash({unconfirmed_block.block_hash}) block.channel({unconfirmed_block.channel_name})"
                )
                self.confirm_block(unconfirmed_block)
            else:
                # 투표에 실패한 블럭을 받은 경우
                # 특별한 처리가 필요 없다. 새로 받은 블럭을 아래 로직에서 add_unconfirm_block 으로 수행하면 된다.
                pass

        self.__unconfirmedBlockQueue.put(unconfirmed_block)

    def add_confirmed_block(self, confirmed_block: Block):
        is_commit_state_validation = False if not confirmed_block.commit_state else True
        result = self.__blockchain.add_block(confirmed_block,
                                             is_commit_state_validation)
        if not result:
            self.block_height_sync(target_peer_stub=ObjectManager().
                                   channel_service.radio_station_stub)

        if ObjectManager(
        ).channel_service.broadcast_scheduler.audience_subscriber:
            self.__broadcast_block_to_audience_subscriber(confirmed_block)

    def add_block(self,
                  block_: Block,
                  is_commit_state_validation=False) -> bool:
        """ add committed block

        :param block_: a block after confirmation
        :param is_commit_state_validation: if True: add only commit state validate pass
        :return: to add block is success or not
        """
        result = self.__blockchain.add_block(block_,
                                             is_commit_state_validation)

        last_block = self.__blockchain.last_block
        if ObjectManager(
        ).channel_service.broadcast_scheduler.audience_subscriber:
            self.__broadcast_block_to_audience_subscriber(last_block)

        peer_id = ChannelProperty().peer_id
        util.apm_event(
            peer_id, {
                'event_type': 'TotalTx',
                'peer_id': peer_id,
                'peer_name': conf.PEER_NAME,
                'channel_name': self.__channel_name,
                'data': {
                    'block_hash': block_.block_hash,
                    'total_tx': self.__blockchain.total_tx
                }
            })

        return result

    def rebuild_block(self):
        self.__blockchain.rebuild_transaction_count()

        nid = self.get_blockchain().find_nid()
        if nid is None:
            genesis_block = self.get_blockchain().find_block_by_height(0)
            self.__rebuild_nid(genesis_block)
        else:
            ChannelProperty().nid = nid

    def __rebuild_nid(self, block: Block):
        nid = NID.unknown.value
        if block.block_hash == BlockManager.MAINNET:
            nid = NID.mainnet.value
        elif block.block_hash == BlockManager.TESTNET:
            nid = NID.testnet.value
        elif block.confirmed_tx_len > 0:
            nid = block.confirmed_transaction_list[0].nid
            if nid is None:
                nid = NID.unknown.value

        self.get_blockchain().put_nid(nid)
        ChannelProperty().nid = nid

    def block_height_sync(self, target_peer_stub=None):
        with self.__block_height_sync_lock:
            need_to_sync = (self.__block_height_future is None
                            or self.__block_height_future.done())

            if need_to_sync:
                self.__block_height_future = self.__block_height_thread_pool.submit(
                    self.__block_height_sync, target_peer_stub)
            else:
                logging.warning(
                    'Tried block_height_sync. But failed. The thread is already running'
                )

            return need_to_sync, self.__block_height_future

    def __block_request(self, peer_stub, block_height):
        """request block by gRPC or REST

        :param peer_stub:
        :param block_height:
        :return block, max_block_height, response_code
        """
        if ObjectManager().channel_service.is_support_node_function(
                conf.NodeFunction.Vote):
            response = peer_stub.BlockSync(
                loopchain_pb2.BlockSyncRequest(block_height=block_height,
                                               channel=self.__channel_name),
                conf.GRPC_TIMEOUT)
            return util.block_loads(
                response.block
            ), response.max_block_height, response.response_code
        else:
            # request REST(json-rpc) way to radiostation (mother peer)
            return self.__block_request_by_citizen(
                block_height,
                ObjectManager().channel_service.radio_station_stub)

    def __block_request_by_citizen(self, block_height, rs_rest_stub):
        try:
            get_block_result = rs_rest_stub.call("GetBlockByHeight", {
                'channel': self.__channel_name,
                'height': str(block_height)
            })
            max_height_result = rs_rest_stub.call("Status")

            if max_height_result.status_code != 200:
                raise ConnectionError

            block_data_str = json.dumps(get_block_result['block'])
            block = Block(self.__channel_name)
            block.deserialize_block(block_data_str.encode('utf-8'))

            return block, json.loads(
                max_height_result.text
            )['block_height'], message_code.Response.success

        except ReceivedErrorResponse as e:
            rs_rest_stub.update_methods_version()
            return self.__block_request_by_citizen(block_height, rs_rest_stub)

    def __precommit_block_request(self, peer_stub, last_block_height):
        """request precommit block by gRPC

        :param peer_stub:
        :param block_height:
        :return block, max_block_height, response_code
        """
        response = peer_stub.GetPrecommitBlock(
            loopchain_pb2.PrecommitBlockRequest(
                last_block_height=last_block_height,
                channel=self.__channel_name), conf.GRPC_TIMEOUT)

        if response.block == b"":
            return None, response.response_code, response.response_message
        else:
            precommit_block = pickle.loads(response.block)
            # util.logger.spam(
            #     f"GetPrecommitBlock:response::{response.response_code}/{response.response_message}/"
            #     f"{precommit_block}/{precommit_block.confirmed_transaction_list}")
            return precommit_block, response.response_code, response.response_message

    def __start_block_height_sync_timer(self, target_peer_stub):
        timer_key = TimerService.TIMER_KEY_BLOCK_HEIGHT_SYNC
        timer_service: TimerService = self.__channel_service.timer_service

        if timer_key not in timer_service.timer_list.keys():
            util.logger.spam(
                f"add timer for block_request_call to radiostation...")
            timer_service.add_timer(
                timer_key,
                Timer(target=timer_key,
                      duration=conf.GET_LAST_BLOCK_TIMER,
                      is_repeat=True,
                      callback=self.__block_height_sync,
                      callback_kwargs={'target_peer_stub': target_peer_stub}))

    def __stop_block_height_sync_timer(self):
        timer_key = TimerService.TIMER_KEY_BLOCK_HEIGHT_SYNC
        timer_service: TimerService = self.__channel_service.timer_service
        if timer_key in timer_service.timer_list.keys():
            timer_service.stop_timer(timer_key)

    def __block_height_sync(self, target_peer_stub=None, target_height=None):
        """synchronize block height with other peers"""
        self.__update_service_status(status_code.Service.block_height_sync)
        is_sync_complete = False

        try:
            channel_service = ObjectManager().channel_service
            block_manager = channel_service.block_manager
            peer_manager = channel_service.peer_manager
            blockchain = block_manager.get_blockchain()

            if target_peer_stub is None:
                target_peer_stub = peer_manager.get_leader_stub_manager()

            # The adjustment of block height and the process for data synchronization of peer
            # === Love&Hate Algorithm === #
            logging.info("try block height sync...with love&hate")

            # Make Peer Stub List [peer_stub, ...] and get max_height of network
            # max_height: current max height
            # peer_stubs: peer stub list for block height synchronization
            max_height, peer_stubs = self.__get_peer_stub_list(
                target_peer_stub)
            if target_height is not None:
                max_height = target_height

            my_height = blockchain.block_height
            retry_number = 0
            util.logger.spam(
                f"block_manager:block_height_sync my_height({my_height})")

            if len(peer_stubs) == 0:
                util.logger.warning(
                    "peer_service:block_height_sync there is no other peer to height sync!"
                )
                return False
            else:
                self.__stop_block_height_sync_timer()

            logging.info(
                f"You need block height sync to: {max_height} yours: {my_height}"
            )

            while max_height > my_height:
                for peer_stub in peer_stubs:
                    response_code = message_code.Response.fail
                    try:
                        block, max_block_height, response_code = self.__block_request(
                            peer_stub, my_height + 1)
                    except Exception as e:
                        logging.warning("There is a bad peer, I hate you: " +
                                        str(e))

                    if response_code == message_code.Response.success:
                        logging.debug(f"try add block height: {block.height}")

                        try:
                            result = False
                            commit_state = getattr(block,
                                                   "_Block__commit_state",
                                                   None)
                            logging.debug(
                                f"block_manager.py >> block_height_sync :: "
                                f"height({block.height}) commit_state({commit_state})"
                            )
                            result = block_manager.add_block(
                                block_=block,
                                is_commit_state_validation=True
                                if commit_state else False)

                            if result:
                                if block.height == 0:
                                    self.__rebuild_nid(block)
                                elif self.get_blockchain().find_nid() is None:
                                    genesis_block = self.get_blockchain(
                                    ).find_block_by_height(0)
                                    self.__rebuild_nid(genesis_block)

                        except KeyError as e:
                            result = False
                            logging.error("fail block height sync: " + str(e))
                            break
                        except exception.BlockError:
                            result = False
                            logging.error(
                                "Block Error Clear all block and restart peer."
                            )
                            block_manager.clear_all_blocks()
                            util.exit_and_msg(
                                "Block Error Clear all block and restart peer."
                            )
                            break
                        finally:
                            if result:
                                my_height = block.height
                                retry_number = 0
                            else:
                                retry_number += 1
                                logging.warning(
                                    f"Block height({my_height}) synchronization is fail. "
                                    f"{retry_number}/{conf.BLOCK_SYNC_RETRY_NUMBER}"
                                )
                                if retry_number >= conf.BLOCK_SYNC_RETRY_NUMBER:
                                    util.exit_and_msg(
                                        f"This peer already tried to synchronize {my_height} block "
                                        f"for max retry number({conf.BLOCK_SYNC_RETRY_NUMBER}). "
                                        f"Peer will be down.")

                        if target_height is None:
                            if max_block_height > max_height:
                                util.logger.spam(
                                    f"set max_height :{max_height} -> {max_block_height}"
                                )
                                max_height = max_block_height
                    else:
                        peer_stubs.remove(peer_stub)
                        logging.warning(
                            f"Not responding peer({peer_stub}) is removed from the peer stubs target."
                        )

                        if len(peer_stubs) < 1:
                            raise ConnectionError

            if my_height >= max_height:
                is_sync_complete = True
        except Exception as e:
            logging.warning(f"block_manager.py >>> block_height_sync :: {e}")
            traceback.print_exc()
            self.__start_block_height_sync_timer(target_peer_stub)
            return False

        if not is_sync_complete:
            # block height sync 가 완료되지 않았으면 다시 시도한다.
            logging.warning(
                f"it's not completed block height synchronization in once ...\n"
                f"try block_height_sync again... my_height({my_height}) in channel({self.__channel_name})"
            )
            self.__block_height_sync(target_peer_stub)

        if conf.CONSENSUS_ALGORITHM == conf.ConsensusAlgorithm.lft \
                and channel_service.is_support_node_function(conf.NodeFunction.Vote):
            last_block = blockchain.last_block

            for peer_stub in peer_stubs:
                if peer_stub is not None:
                    precommit_block, response_code, response_message = \
                        self.__precommit_block_request(peer_stub, last_block.height)
                    util.logger.spam(
                        f"block_manager:block_height_sync::precommit_block("
                        f"{precommit_block if precommit_block else None})")
                    break

            if precommit_block:
                if last_block.height + 1 == precommit_block.height:
                    self.__channel_service.score_write_precommit_state(
                        precommit_block)
                    blockchain.put_precommit_block(precommit_block)
                    self.__precommit_block = precommit_block
                    self.consensus.leader_id = precommit_block.peer_id
                    self.consensus.precommit_block = None
                    util.logger.spam(
                        f"set precommit bock {self.__precommit_block.block_hash}/"
                        f"{self.__precommit_block.height} after block height synchronization."
                    )
                else:
                    util.logger.warning(
                        f"precommit block is weird, an expected block height is {last_block.height+1}, "
                        f"but it's {precommit_block.height}")

            else:
                util.logger.spam(
                    f"precommit bock is None after block height synchronization."
                )

            self.__consensus.change_epoch(
                prev_epoch=None, precommit_block=self.__precommit_block)

        logging.debug(f"block_manager:block_height_sync is complete.")

        # Subscribe to block_sync_target_stub and radiostation
        loop = self.__channel_service.timer_service.get_event_loop()
        if self.__channel_service.is_support_node_function(
                conf.NodeFunction.Vote):
            asyncio.run_coroutine_threadsafe(
                self.__channel_service.subscribe_to_radio_station(), loop)
        asyncio.run_coroutine_threadsafe(
            self.__channel_service.subscribe_to_target_stub(target_peer_stub),
            loop)

        self.__update_service_status(status_code.Service.online)
        return True

    def __broadcast_block_to_audience_subscriber(self, confirmed_block: Block):
        try:
            # repr can convert dict to string. And this string can convert dict again with ast.literal_eval
            commit_state = repr(confirmed_block.commit_state)
            util.logger.spam(
                f"block_manager:__broadcast_block_to_audience_subscriber "
                f"commit_state({commit_state})")
        except Exception as e:
            logging.warning(
                f"block_manager:__broadcast_block_to_audience_subscriber "
                f"FAIL json.dumps commit_state({confirmed_block.commit_state})"
            )
            commit_state = ""

        json_data = confirmed_block.get_json_data()
        ObjectManager().channel_service.broadcast_scheduler.schedule_broadcast(
            "AnnounceConfirmedBlock", {
                'block_hash': confirmed_block.block_hash,
                'channel': self.__channel_name,
                'block': json_data,
                'commit_state': commit_state
            })

    def __get_peer_stub_list(self, target_peer_stub=None):
        """It updates peer list for block manager refer to peer list on the loopchain network.
        This peer list is not same to the peer list of the loopchain network.

        :return max_height: a height of current blockchain
        :return peer_stubs: current peer list on the loopchain network
        """
        peer_target = ChannelProperty().peer_target
        peer_manager = ObjectManager().channel_service.peer_manager

        # Make Peer Stub List [peer_stub, ...] and get max_height of network
        max_height = -1  # current max height
        peer_stubs = []  # peer stub list for block height synchronization

        if ObjectManager().channel_service.is_support_node_function(
                conf.NodeFunction.Vote):
            target_dict = peer_manager.get_IP_of_peers_dict()
            target_list = [
                peer_target for peer_id, peer_target in target_dict.items()
                if peer_id != ChannelProperty().peer_id
            ]
        else:
            target_list = [f"{target_peer_stub.target}"]

        for target in target_list:
            if target != peer_target:
                logging.debug(f"try to target({target})")
                channel = GRPCHelper().create_client_channel(target)
                stub = loopchain_pb2_grpc.PeerServiceStub(channel)
                try:
                    if ObjectManager(
                    ).channel_service.is_support_node_function(
                            conf.NodeFunction.Vote):
                        response = stub.GetStatus(
                            loopchain_pb2.StatusRequest(
                                request="",
                                channel=self.__channel_name,
                            ), conf.GRPC_TIMEOUT_SHORT)
                    else:
                        response = target_peer_stub.call("Status")
                        util.logger.spam('{/api/v1/status/peer} response: ' +
                                         response.text)
                        response.block_height = int(
                            json.loads(response.text)["block_height"])
                        stub.target = target

                    if response.block_height > max_height:
                        # Add peer as higher than this
                        max_height = response.block_height
                        peer_stubs.append(stub)

                except Exception as e:
                    logging.warning(
                        f"This peer has already been removed from the block height target node. {e}"
                    )

        return max_height, peer_stubs

    def __close_level_db(self):
        del self.__level_db
        self.__level_db = None
        self.__blockchain.close_blockchain_db()

    def run(self, e: threading.Event):
        """Block Manager Thread Loop
        PEER 의 type 에 따라 Block Generator 또는 Peer 로 동작한다.
        Block Generator 인 경우 conf 에 따라 사용할 Consensus 알고리즘이 변경된다.
        """

        logging.info(
            f"channel({self.__channel_name}) Block Manager thread Start.")
        e.set()

        while self.is_run():
            self.__run_logic()

        # for reuse level db when restart channel.
        self.__close_level_db()

        logging.info(
            f"channel({self.__channel_name}) Block Manager thread Ended.")

    def stop(self):
        if conf.ALLOW_MAKE_EMPTY_BLOCK:
            self.__block_generation_scheduler.stop()
        CommonThread.stop(self)

    def __vote_unconfirmed_block(self, block_hash, is_validated):
        logging.debug(
            f"block_manager:__vote_unconfirmed_block ({self.channel_name}/{is_validated})"
        )

        if is_validated:
            vote_code, message = message_code.get_response(
                message_code.Response.success_validate_block)
        else:
            vote_code, message = message_code.get_response(
                message_code.Response.fail_validate_block)

        block_vote = loopchain_pb2.BlockVote(
            vote_code=vote_code,
            channel=self.channel_name,
            message=message,
            block_hash=block_hash,
            peer_id=self.__peer_id,
            group_id=ChannelProperty().group_id)

        self.__channel_service.broadcast_scheduler.schedule_broadcast(
            "VoteUnconfirmedBlock", block_vote)

    @staticmethod
    def __do_nothing():
        time.sleep(conf.SLEEP_SECONDS_IN_SERVICE_NONE)

    def __do_vote(self):
        """Announce 받은 unconfirmed block 에 투표를 한다.
        """
        if not self.__unconfirmedBlockQueue.empty():
            unconfirmed_block = self.__unconfirmedBlockQueue.get()
            logging.debug(
                f"we got unconfirmed block ....{unconfirmed_block.block_hash}")
        else:
            time.sleep(conf.SLEEP_SECONDS_IN_SERVICE_LOOP)
            # logging.debug("No unconfirmed block ....")
            return

        my_height = self.__blockchain.block_height
        while my_height < (unconfirmed_block.height - 1):
            _, future = self.block_height_sync()
            if future.result():
                my_height = self.__blockchain.block_height

        # a block is already added that same height unconfirmed_block height
        if my_height >= unconfirmed_block.height:
            return

        logging.info("PeerService received unconfirmed block: " +
                     unconfirmed_block.block_hash)

        if unconfirmed_block.confirmed_tx_len == 0 \
                and unconfirmed_block.block_type is not BlockType.peer_list \
                and not conf.ALLOW_MAKE_EMPTY_BLOCK:
            # siever 에서 사용하는 vote block 은 tx 가 없다. (검증 및 투표 불필요)
            # siever 에서 vote 블럭 발송 빈도를 보기 위해 warning 으로 로그 남김, 그 외의 경우 아래 로그는 주석처리 할 것
            # logging.warning("This is vote block by siever")
            pass
        else:
            # block 검증
            block_is_validated = False
            try:
                block_is_validated = Block.validate(unconfirmed_block)

                if conf.CHANNEL_OPTION[
                        self.__channel_name]['store_valid_transaction_only']:
                    block_is_validated, need_rebuild, invoke_results = unconfirmed_block.verify_through_score_invoke(
                    )
                    self.set_invoke_results(unconfirmed_block.block_hash,
                                            invoke_results)

            except Exception as e:
                logging.error(e)

            if block_is_validated:
                # broadcast 를 받으면 받은 블럭을 검증한 후 검증되면 자신의 blockchain 의 unconfirmed block 으로 등록해 둔다.
                confirmed, reason = self.__blockchain.add_unconfirm_block(
                    unconfirmed_block)
                if confirmed:
                    # block is confirmed
                    # validated 일 때 투표 할 것이냐? confirmed 일 때 투표할 것이냐? 현재는 validate 만 체크
                    pass
                elif reason == "block_height":
                    pass
                    # Announce 되는 블럭과 자신의 height 가 다르면 Block Height Sync 를 다시 시도한다.
                    # self.block_height_sync()

            self.__vote_unconfirmed_block(unconfirmed_block.block_hash,
                                          block_is_validated)

    def callback_complete_consensus(self, **kwargs):
        self.__prev_epoch = kwargs.get("prev_epoch", None)
        self.__epoch = kwargs.get("epoch", None)
        last_block = self.get_blockchain().last_block
        last_block_height = last_block.height

        if last_block_height > 0 and self.__precommit_block is None:
            logging.error(
                "It's weird what a precommit block is None. "
                "That's why a timer can't be added to timer service.")

        if self.__prev_epoch:
            if self.__prev_epoch.status == EpochStatus.success:
                util.logger.spam(
                    f"BlockManager:callback_complete_consensus::epoch status is success !! "
                    f"self.__precommit_block({self.__precommit_block})")

                if self.__precommit_block:
                    if not self.add_block(self.__precommit_block):
                        self.__precommit_block = self.__blockchain.get_precommit_block(
                        )

                self.__precommit_block = kwargs.get("precommit_block", None)
                if self.__channel_service.score_write_precommit_state(self.__precommit_block) and \
                        self.__blockchain.put_precommit_block(self.__precommit_block):
                    util.logger.spam(
                        f"start timer :: success precommit block info - {self.__precommit_block.height}"
                    )

            elif self.__prev_epoch.status == EpochStatus.leader_complain:
                self.__epoch.fixed_vote_list = self.__prev_epoch.ready_vote_list
                self.__precommit_block = self.__consensus.precommit_block
                self.__prev_epoch = self.__prev_epoch.prev_epoch
                util.logger.spam(
                    f"start timer :: fail precommit block info - {self.__precommit_block.height}"
                )

            self.__channel_service.consensus.start_timer(
                self.__channel_service.acceptor.callback_leader_complain)
        else:
            util.logger.spam(
                f"start timer :: after genesis or rebuild block / "
                f"precommit block info - {last_block_height}")
예제 #5
0
class BlockManager:
    """Manage the blockchain of a channel. It has objects for consensus and db object.
    """

    MAINNET = "cf43b3fd45981431a0e64f79d07bfcf703e064b73b802c5f32834eec72142190"
    TESTNET = "885b8021826f7e741be7f53bb95b48221e9ab263f377e997b2e47a7b8f4a2a8b"

    def __init__(self, name: str, channel_manager, peer_id, channel_name,
                 level_db_identity):
        self.__channel_service: ChannelService = channel_manager
        self.__channel_name = channel_name
        self.__pre_validate_strategy = self.__pre_validate
        self.__peer_id = peer_id
        self.__level_db = None
        self.__level_db_path = ""
        self.__level_db, self.__level_db_path = util.init_level_db(
            level_db_identity=f"{level_db_identity}_{channel_name}",
            allow_rename_path=False)
        self.__txQueue = AgingCache(
            max_age_seconds=conf.MAX_TX_QUEUE_AGING_SECONDS,
            default_item_status=TransactionStatusInQueue.normal)
        self.__unconfirmedBlockQueue = queue.Queue()
        self.__blockchain = BlockChain(self.__level_db, channel_name)
        self.__peer_type = None
        self.__consensus = None
        self.__consensus_algorithm = None
        self.candidate_blocks = CandidateBlocks()
        self.__block_height_sync_lock = threading.Lock()
        self.__block_height_thread_pool = ThreadPoolExecutor(
            1, 'BlockHeightSyncThread')
        self.__block_height_future: Future = None
        self.__subscribe_target_peer_stub = None
        self.__block_generation_scheduler = BlockGenerationScheduler(
            self.__channel_name)
        self.__precommit_block: Block = None
        self.set_peer_type(loopchain_pb2.PEER)
        self.name = name
        self.__service_status = status_code.Service.online

        self.epoch: Epoch = None

    @property
    def channel_name(self):
        return self.__channel_name

    @property
    def service_status(self):
        # Return string for compatibility.
        if self.__service_status >= 0:
            return "Service is online: " + \
                   str(1 if self.__channel_service.state_machine.state == "BlockGenerate" else 0)
        else:
            return "Service is offline: " + status_code.get_status_reason(
                self.__service_status)

    def init_epoch(self):
        """Call this after peer list update

        :return:
        """
        self.epoch = Epoch(self.__blockchain.last_block.header.height +
                           1 if self.__blockchain.last_block else 1)

    def update_service_status(self, status):
        self.__service_status = status
        StubCollection().peer_stub.sync_task().update_status(
            self.__channel_name, {"status": self.service_status})

    @property
    def peer_type(self):
        return self.__peer_type

    @property
    def made_block_count(self):
        if self.__consensus_algorithm:
            return self.__consensus_algorithm.made_block_count
        return 0

    @property
    def consensus(self):
        return self.__consensus

    @consensus.setter
    def consensus(self, consensus):
        self.__consensus = consensus

    @property
    def consensus_algorithm(self):
        return self.__consensus_algorithm

    @property
    def precommit_block(self):
        return self.__precommit_block

    @precommit_block.setter
    def precommit_block(self, block):
        self.__precommit_block = block

    @property
    def block_generation_scheduler(self):
        return self.__block_generation_scheduler

    @property
    def subscribe_target_peer_stub(self):
        return self.__subscribe_target_peer_stub

    def get_level_db(self):
        return self.__level_db

    def clear_all_blocks(self):
        logging.debug(f"clear level db({self.__level_db_path})")
        shutil.rmtree(self.__level_db_path)

    def set_peer_type(self, peer_type):
        self.__peer_type = peer_type

    async def __create_block_generation_schedule(self):
        # util.logger.spam(f"__create_block_generation_schedule:: CREATE BLOCK GENERATION SCHEDULE")
        if conf.CONSENSUS_ALGORITHM == conf.ConsensusAlgorithm.lft:
            Schedule = namedtuple("Schedule", "callback kwargs")
            schedule = Schedule(self.__consensus_algorithm.consensus, {})
            self.__block_generation_scheduler.add_schedule(schedule)
        else:
            await self.__consensus_algorithm.consensus()

    def set_invoke_results(self, block_hash, invoke_results):
        self.__blockchain.set_invoke_results(block_hash, invoke_results)

    def get_total_tx(self):
        """
        블럭체인의 Transaction total 리턴합니다.

        :return: 블럭체인안의 transaction total count
        """
        return self.__blockchain.total_tx

    def get_blockchain(self):
        return self.__blockchain

    def pre_validate(self, tx: Transaction):
        return self.__pre_validate_strategy(tx)

    def __pre_validate(self, tx: Transaction):
        if tx.hash.hex() in self.__txQueue:
            raise TransactionInvalidDuplicatedHash(tx.hash.hex())

        if not util.is_in_time_boundary(tx.timestamp,
                                        conf.ALLOW_TIMESTAMP_BOUNDARY_SECOND):
            raise TransactionInvalidOutOfTimeBound(tx.hash.hex(), tx.timestamp,
                                                   util.get_now_time_stamp())

    def __pre_validate_pass(self, tx: Transaction):
        pass

    def broadcast_send_unconfirmed_block(self, block_: Block):
        """생성된 unconfirmed block 을 피어들에게 broadcast 하여 검증을 요청한다.
        """
        if self.__channel_service.state_machine.state == "BlockGenerate":
            logging.debug(
                f"BroadCast AnnounceUnconfirmedBlock "
                f"height({block_.header.height}) block({block_.header.hash}) peers: "
                f"{ObjectManager().channel_service.peer_manager.get_peer_count()}"
            )

            # util.logger.spam(f'block_manager:zip_test num of tx is {block_.confirmed_tx_len}')
            block_dump = util.block_dumps(block_)

            ObjectManager(
            ).channel_service.broadcast_scheduler.schedule_broadcast(
                "AnnounceUnconfirmedBlock",
                loopchain_pb2.BlockSend(block=block_dump,
                                        channel=self.__channel_name))

    def add_tx_obj(self, tx):
        """전송 받은 tx 를 Block 생성을 위해서 큐에 입력한다. load 하지 않은 채 입력한다.

        :param tx: transaction object
        """
        self.__txQueue[tx.hash.hex()] = tx

    def get_tx(self, tx_hash) -> Transaction:
        """Get transaction from block_db by tx_hash

        :param tx_hash: tx hash
        :return: tx object or None
        """
        return self.__blockchain.find_tx_by_key(tx_hash)

    def get_tx_info(self, tx_hash) -> dict:
        """Get transaction info from block_db by tx_hash

        :param tx_hash: tx hash
        :return: {'block_hash': "", 'block_height': "", "transaction": "", "result": {"code": ""}}
        """
        return self.__blockchain.find_tx_info(tx_hash)

    def get_invoke_result(self, tx_hash):
        """ get invoke result by tx

        :param tx_hash:
        :return:
        """
        return self.__blockchain.find_invoke_result_by_tx_hash(tx_hash)

    def get_tx_queue(self):
        if conf.CONSENSUS_ALGORITHM == conf.ConsensusAlgorithm.lft:
            return self.__consensus.get_tx_queue()

        return self.__txQueue

    def get_count_of_unconfirmed_tx(self):
        """BlockManager 의 상태를 확인하기 위하여 현재 입력된 unconfirmed_tx 의 카운트를 구한다.

        :return: 현재 입력된 unconfirmed tx 의 갯수
        """
        return len(self.__txQueue)

    def confirm_prev_block(self, current_block: Block):
        try:
            self.__blockchain.confirm_prev_block(current_block)
        except BlockchainError as e:
            logging.warning(
                f"BlockchainError while confirm_block({e}), retry block_height_sync"
            )
            self.block_height_sync()

    def add_unconfirmed_block(self, unconfirmed_block):
        logging.info(
            f"unconfirmed_block {unconfirmed_block.header.height}, {unconfirmed_block.body.confirm_prev_block}"
        )
        # util.logger.debug(f"-------------------add_unconfirmed_block---before confirm_prev_block, "
        #                    f"tx count({len(unconfirmed_block.body.transactions)}), "
        #                    f"height({unconfirmed_block.header.height})")
        if unconfirmed_block.body.confirm_prev_block:
            self.confirm_prev_block(unconfirmed_block)

        self.epoch.set_epoch_leader(
            unconfirmed_block.header.next_leader.hex_hx())

        self.__unconfirmedBlockQueue.put(unconfirmed_block)

    def add_confirmed_block(self, confirmed_block: Block):
        result = self.__blockchain.add_block(confirmed_block)
        if not result:
            self.block_height_sync(target_peer_stub=ObjectManager().
                                   channel_service.radio_station_stub)

    # TODO The current block height sync message does not include voting.
    #  You need to change it and remove the default None parameter here.
    def add_block(self, block_: Block, vote_: Vote = None) -> bool:
        """

        :param block_: block to add
        :param vote_: additional info for this block, but It came from next block
        :return:
        """
        result = self.__blockchain.add_block(block_, vote_)

        last_block = self.__blockchain.last_block

        peer_id = ChannelProperty().peer_id
        util.apm_event(
            peer_id, {
                'event_type': 'TotalTx',
                'peer_id': peer_id,
                'peer_name': conf.PEER_NAME,
                'channel_name': self.__channel_name,
                'data': {
                    'block_hash': block_.header.hash.hex(),
                    'total_tx': self.__blockchain.total_tx
                }
            })

        return result

    def rebuild_block(self):
        self.__blockchain.rebuild_transaction_count()

        nid = self.get_blockchain().find_nid()
        if nid is None:
            genesis_block = self.get_blockchain().find_block_by_height(0)
            self.__rebuild_nid(genesis_block)
        else:
            ChannelProperty().nid = nid

    def __rebuild_nid(self, block: Block):
        nid = NID.unknown.value
        if block.header.hash.hex() == BlockManager.MAINNET:
            nid = NID.mainnet.value
        elif block.header.hash.hex() == BlockManager.TESTNET:
            nid = NID.testnet.value
        elif len(block.body.transactions) > 0:
            tx = next(iter(block.body.transactions.values()))
            nid = tx.nid
            if nid is None:
                nid = NID.unknown.value

        if isinstance(nid, int):
            nid = hex(16)

        self.get_blockchain().put_nid(nid)
        ChannelProperty().nid = nid

    def block_height_sync(self, target_peer_stub=None):
        with self.__block_height_sync_lock:
            need_to_sync = (self.__block_height_future is None
                            or self.__block_height_future.done())

            if need_to_sync:
                self.__block_height_future = self.__block_height_thread_pool.submit(
                    self.__block_height_sync, target_peer_stub)
            else:
                logging.warning(
                    'Tried block_height_sync. But failed. The thread is already running'
                )

            return need_to_sync, self.__block_height_future

    def __block_request(self, peer_stub, block_height):
        """request block by gRPC or REST

        :param peer_stub:
        :param block_height:
        :return block, max_block_height, response_code
        """
        if ObjectManager().channel_service.is_support_node_function(
                conf.NodeFunction.Vote):
            response = peer_stub.BlockSync(
                loopchain_pb2.BlockSyncRequest(block_height=block_height,
                                               channel=self.__channel_name),
                conf.GRPC_TIMEOUT)
            return util.block_loads(
                response.block
            ), response.max_block_height, response.response_code
        else:
            # request REST(json-rpc) way to radiostation (mother peer)
            return self.__block_request_by_citizen(
                block_height,
                ObjectManager().channel_service.radio_station_stub)

    def __block_request_by_citizen(self, block_height, rs_rest_stub):
        try:
            get_block_result = rs_rest_stub.call("GetBlockByHeight", {
                'channel': self.__channel_name,
                'height': str(block_height)
            })
            max_height_result = rs_rest_stub.call("Status")

            if max_height_result.status_code != 200:
                raise ConnectionError

            block_version = self.get_blockchain().block_versioner.get_version(
                block_height)
            block_serializer = BlockSerializer.new(
                block_version,
                self.get_blockchain().tx_versioner)
            block = block_serializer.deserialize(get_block_result['block'])

            return block, json.loads(
                max_height_result.text
            )['block_height'], message_code.Response.success

        except ReceivedErrorResponse as e:
            rs_rest_stub.update_methods_version()
            return self.__block_request_by_citizen(block_height, rs_rest_stub)

    def __precommit_block_request(self, peer_stub, last_block_height):
        """request precommit block by gRPC

        :param peer_stub:
        :param block_height:
        :return block, max_block_height, response_code
        """
        response = peer_stub.GetPrecommitBlock(
            loopchain_pb2.PrecommitBlockRequest(
                last_block_height=last_block_height,
                channel=self.__channel_name), conf.GRPC_TIMEOUT)

        if response.block == b"":
            return None, response.response_code, response.response_message
        else:
            precommit_block = pickle.loads(response.block)
            # util.logger.spam(
            #     f"GetPrecommitBlock:response::{response.response_code}/{response.response_message}/"
            #     f"{precommit_block}/{precommit_block.confirmed_transaction_list}")
            return precommit_block, response.response_code, response.response_message

    def __start_block_height_sync_timer(self, target_peer_stub):
        timer_key = TimerService.TIMER_KEY_BLOCK_HEIGHT_SYNC
        timer_service: TimerService = self.__channel_service.timer_service

        if timer_key not in timer_service.timer_list:
            util.logger.spam(
                f"add timer for block_request_call to radiostation...")
            timer_service.add_timer(
                timer_key,
                Timer(target=timer_key,
                      duration=conf.GET_LAST_BLOCK_TIMER,
                      is_repeat=True,
                      callback=self.block_height_sync,
                      callback_kwargs={'target_peer_stub': target_peer_stub}))

    def stop_block_height_sync_timer(self):
        timer_key = TimerService.TIMER_KEY_BLOCK_HEIGHT_SYNC
        timer_service: TimerService = self.__channel_service.timer_service
        if timer_key in timer_service.timer_list:
            timer_service.stop_timer(timer_key)

    def start_block_generate_timer(self):
        timer_key = TimerService.TIMER_KEY_BLOCK_GENERATE
        timer_service: TimerService = self.__channel_service.timer_service

        if timer_key not in timer_service.timer_list:
            if self.__consensus_algorithm:
                self.__consensus_algorithm.stop()

        self.__consensus_algorithm = ConsensusSiever(self)
        self.__consensus_algorithm.start_timer(timer_service)

    def stop_block_generate_timer(self):
        if self.__consensus_algorithm:
            self.__consensus_algorithm.stop()

    def __current_block_height(self):
        if self.__blockchain.last_unconfirmed_block and \
                self.__blockchain.last_unconfirmed_block.header.height == self.__blockchain.block_height + 1:
            return self.__blockchain.block_height + 1
        else:
            return self.__blockchain.block_height

    def __add_block_by_sync(self, block_):
        commit_state = block_.header.commit_state
        logging.debug(
            f"block_manager.py >> block_height_sync :: "
            f"height({block_.header.height}) commit_state({commit_state})")

        block_version = self.get_blockchain().block_versioner.get_version(
            block_.header.height)
        block_verifier = BlockVerifier.new(block_version,
                                           self.get_blockchain().tx_versioner)
        if block_.header.height == 0:
            block_verifier.invoke_func = self.__channel_service.genesis_invoke
        else:
            block_verifier.invoke_func = self.__channel_service.score_invoke
        invoke_results = block_verifier.verify_loosely(
            block_, self.__blockchain.last_block, self.__blockchain)
        self.__blockchain.set_invoke_results(block_.header.hash.hex(),
                                             invoke_results)
        return self.add_block(block_)

    def __block_height_sync(self, target_peer_stub=None, target_height=None):
        """synchronize block height with other peers"""
        channel_service = ObjectManager().channel_service
        peer_manager = channel_service.peer_manager

        if target_peer_stub is None:
            target_peer_stub = peer_manager.get_leader_stub_manager()
        self.__subscribe_target_peer_stub = target_peer_stub

        # The adjustment of block height and the process for data synchronization of peer
        # === Love&Hate Algorithm === #
        util.logger.debug("try block height sync...with love&hate")

        # Make Peer Stub List [peer_stub, ...] and get max_height of network
        # max_height: current max height
        # peer_stubs: peer stub list for block height synchronization
        max_height, peer_stubs = self.__get_peer_stub_list(target_peer_stub)
        if target_height is not None:
            max_height = target_height

        my_height = self.__current_block_height()
        retry_number = 0
        util.logger.spam(
            f"block_manager:block_height_sync my_height({my_height})")

        if len(peer_stubs) == 0:
            util.logger.warning(
                "peer_service:block_height_sync there is no other peer to height sync!"
            )
            return False

        logging.info(
            f"In block height sync max: {max_height} yours: {my_height}")

        self.get_blockchain().prevent_next_block_mismatch(
            self.__blockchain.block_height)

        try:
            while max_height > my_height:
                for peer_stub in peer_stubs:
                    response_code = message_code.Response.fail
                    try:
                        block, max_block_height, response_code = self.__block_request(
                            peer_stub, my_height + 1)
                    except Exception as e:
                        logging.warning("There is a bad peer, I hate you: " +
                                        str(e))
                        traceback.print_exc()

                    if response_code == message_code.Response.success:
                        logging.debug(
                            f"try add block height: {block.header.height}")

                        try:
                            result = False
                            if max_height > 0 and max_height == block.header.height:
                                self.candidate_blocks.add_block(block)
                                self.__blockchain.last_unconfirmed_block = block
                                result = True
                            else:
                                result = self.__add_block_by_sync(block)

                            if result:
                                if block.header.height == 0:
                                    self.__rebuild_nid(block)
                                elif self.__blockchain.find_nid() is None:
                                    genesis_block = self.get_blockchain(
                                    ).find_block_by_height(0)
                                    self.__rebuild_nid(genesis_block)

                        except KeyError as e:
                            result = False
                            logging.error("fail block height sync: " + str(e))
                            break
                        except exception.BlockError:
                            result = False
                            logging.error(
                                "Block Error Clear all block and restart peer."
                            )
                            self.clear_all_blocks()
                            util.exit_and_msg(
                                "Block Error Clear all block and restart peer."
                            )
                            break
                        finally:
                            if result:
                                my_height += 1
                                retry_number = 0
                            else:
                                retry_number += 1
                                logging.warning(
                                    f"Block height({my_height}) synchronization is fail. "
                                    f"{retry_number}/{conf.BLOCK_SYNC_RETRY_NUMBER}"
                                )
                                if retry_number >= conf.BLOCK_SYNC_RETRY_NUMBER:
                                    util.exit_and_msg(
                                        f"This peer already tried to synchronize {my_height} block "
                                        f"for max retry number({conf.BLOCK_SYNC_RETRY_NUMBER}). "
                                        f"Peer will be down.")

                        if target_height is None:
                            if max_block_height > max_height:
                                util.logger.spam(
                                    f"set max_height :{max_height} -> {max_block_height}"
                                )
                                max_height = max_block_height
                    else:
                        peer_stubs.remove(peer_stub)
                        logging.warning(
                            f"Not responding peer({peer_stub}) is removed from the peer stubs target."
                        )

                        if len(peer_stubs) < 1:
                            raise ConnectionError
        except Exception as e:
            logging.warning(f"block_manager.py >>> block_height_sync :: {e}")
            traceback.print_exc()
            self.__start_block_height_sync_timer(target_peer_stub)
            return False

        if my_height >= max_height:
            util.logger.debug(f"block_manager:block_height_sync is complete.")
            self.epoch.set_epoch_leader(
                self.__channel_service.peer_manager.get_leader_id(
                    conf.ALL_GROUP_ID))
            self.__channel_service.state_machine.subscribe_network()
        else:
            logging.warning(
                f"it's not completed block height synchronization in once ...\n"
                f"try block_height_sync again... my_height({my_height}) in channel({self.__channel_name})"
            )
            self.__channel_service.state_machine.block_sync()

        if conf.CONSENSUS_ALGORITHM == conf.ConsensusAlgorithm.lft \
                and channel_service.is_support_node_function(conf.NodeFunction.Vote):
            last_block = self.__blockchain.last_block
            precommit_block = None
            for peer_stub in peer_stubs:
                if peer_stub is not None:
                    precommit_block, response_code, response_message = \
                        self.__precommit_block_request(peer_stub, last_block.height)
                    util.logger.spam(
                        f"block_manager:block_height_sync::precommit_block("
                        f"{precommit_block if precommit_block else None})")
                    break

            if precommit_block:
                if last_block.height + 1 == precommit_block.height:
                    self.__blockchain.invoke_for_precommit(precommit_block)
                    self.__channel_service.score_write_precommit_state(
                        precommit_block)
                    self.__blockchain.put_precommit_block(precommit_block)
                    self.__precommit_block = precommit_block
                    self.consensus.leader_id = precommit_block.peer_id
                    self.consensus.precommit_block = None
                    util.logger.spam(
                        f"set precommit bock {self.__precommit_block.block_hash}/"
                        f"{self.__precommit_block.height} after block height synchronization."
                    )
                    self.__consensus.change_epoch(
                        prev_epoch=None,
                        precommit_block=self.__precommit_block)
                else:
                    util.logger.warning(
                        f"precommit block is weird, an expected block height is {last_block.height+1}, "
                        f"but it's {precommit_block.height}")

            else:
                util.logger.spam(
                    f"precommit bock is None after block height synchronization."
                )

        return True

    def __get_peer_stub_list(self, target_peer_stub=None):
        """It updates peer list for block manager refer to peer list on the loopchain network.
        This peer list is not same to the peer list of the loopchain network.

        :return max_height: a height of current blockchain
        :return peer_stubs: current peer list on the loopchain network
        """
        peer_target = ChannelProperty().peer_target
        peer_manager = ObjectManager().channel_service.peer_manager

        # Make Peer Stub List [peer_stub, ...] and get max_height of network
        max_height = -1  # current max height
        peer_stubs = []  # peer stub list for block height synchronization

        if ObjectManager().channel_service.is_support_node_function(
                conf.NodeFunction.Vote):
            target_dict = peer_manager.get_IP_of_peers_dict()
            target_list = [
                peer_target for peer_id, peer_target in target_dict.items()
                if peer_id != ChannelProperty().peer_id
            ]
        else:
            target_list = [f"{target_peer_stub.target}"]

        for target in target_list:
            if target != peer_target:
                logging.debug(f"try to target({target})")
                channel = GRPCHelper().create_client_channel(target)
                stub = loopchain_pb2_grpc.PeerServiceStub(channel)
                try:
                    if ObjectManager(
                    ).channel_service.is_support_node_function(
                            conf.NodeFunction.Vote):
                        response = stub.GetStatus(
                            loopchain_pb2.StatusRequest(
                                request="",
                                channel=self.__channel_name,
                            ), conf.GRPC_TIMEOUT_SHORT)
                    else:
                        response = target_peer_stub.call("Status")
                        util.logger.spam('{/api/v1/status/peer} response: ' +
                                         response.text)
                        response.block_height = int(
                            json.loads(response.text)["block_height"])
                        response.unconfirmed_block_height = int(
                            json.loads(response.text).get(
                                "unconfirmed_block_height", -1))
                        stub.target = target

                    response.block_height = max(
                        response.block_height,
                        response.unconfirmed_block_height)

                    if response.block_height > max_height:
                        # Add peer as higher than this
                        max_height = response.block_height
                        peer_stubs.append(stub)

                except Exception as e:
                    logging.warning(
                        f"This peer has already been removed from the block height target node. {e}"
                    )

        return max_height, peer_stubs

    def __close_level_db(self):
        del self.__level_db
        self.__level_db = None
        self.__blockchain.close_blockchain_db()

    def stop(self):
        # for reuse level db when restart channel.
        self.__close_level_db()

        if conf.ALLOW_MAKE_EMPTY_BLOCK:
            self.__block_generation_scheduler.stop()

        if self.consensus_algorithm:
            self.consensus_algorithm.stop()

    def leader_complain(self):
        complained_leader_id = self.epoch.leader_id
        new_leader = self.__channel_service.peer_manager.get_next_leader_peer(
            current_leader_peer_id=self.epoch.leader_id)
        new_leader_id = new_leader.peer_id if new_leader else None

        if not isinstance(new_leader_id, str):
            new_leader_id = ""

        if not isinstance(complained_leader_id, str):
            complained_leader_id = ""

        self.epoch.add_complain(complained_leader_id, new_leader_id,
                                self.epoch.height, self.__peer_id,
                                ChannelProperty().group_id)

        request = loopchain_pb2.ComplainLeaderRequest(
            complained_leader_id=complained_leader_id,
            channel=self.channel_name,
            new_leader_id=new_leader_id,
            block_height=self.epoch.height,
            message="I'm your father.",
            peer_id=self.__peer_id,
            group_id=ChannelProperty().group_id)

        util.logger.debug(f"complain group_id({ChannelProperty().group_id})")

        self.__channel_service.broadcast_scheduler.schedule_broadcast(
            "ComplainLeader", request)

    def vote_unconfirmed_block(self, block_hash, is_validated):
        logging.debug(
            f"block_manager:vote_unconfirmed_block ({self.channel_name}/{is_validated})"
        )

        if is_validated:
            vote_code, message = message_code.get_response(
                message_code.Response.success_validate_block)
        else:
            vote_code, message = message_code.get_response(
                message_code.Response.fail_validate_block)

        block_vote = loopchain_pb2.BlockVote(
            vote_code=vote_code,
            channel=self.channel_name,
            message=message,
            block_hash=block_hash,
            peer_id=self.__peer_id,
            group_id=ChannelProperty().group_id)

        self.candidate_blocks.add_vote(block_hash,
                                       ChannelProperty().group_id,
                                       ChannelProperty().peer_id, is_validated)
        self.__channel_service.broadcast_scheduler.schedule_broadcast(
            "VoteUnconfirmedBlock", block_vote)

    def vote_as_peer(self):
        """Vote to AnnounceUnconfirmedBlock
        """
        if self.__unconfirmedBlockQueue.empty():
            return

        unconfirmed_block: Block = self.__unconfirmedBlockQueue.get()
        logging.debug(
            f"we got unconfirmed block ....{unconfirmed_block.header.hash.hex()}"
        )

        my_height = self.__blockchain.block_height
        if my_height < (unconfirmed_block.header.height - 1):
            self.__channel_service.state_machine.block_sync()
            return

        # a block is already added that same height unconfirmed_block height
        if my_height >= unconfirmed_block.header.height:
            return

        logging.info("PeerService received unconfirmed block: " +
                     unconfirmed_block.header.hash.hex())

        block_version = self.__blockchain.block_versioner.get_version(
            unconfirmed_block.header.height)
        block_verifier = BlockVerifier.new(block_version,
                                           self.__blockchain.tx_versioner)
        block_verifier.invoke_func = self.__channel_service.score_invoke

        exception = None
        try:
            invoke_results = block_verifier.verify(
                unconfirmed_block, self.__blockchain.last_block,
                self.__blockchain,
                self.__blockchain.last_block.header.next_leader)
        except Exception as e:
            exception = e
            logging.error(e)
            traceback.print_exc()
        else:
            self.set_invoke_results(unconfirmed_block.header.hash.hex(),
                                    invoke_results)
            self.candidate_blocks.add_block(unconfirmed_block)
        finally:
            self.vote_unconfirmed_block(unconfirmed_block.header.hash,
                                        exception is None)
예제 #6
0
class BlockManager:
    """Manage the blockchain of a channel. It has objects for consensus and db object.
    """

    MAINNET = "cf43b3fd45981431a0e64f79d07bfcf703e064b73b802c5f32834eec72142190"
    TESTNET = "885b8021826f7e741be7f53bb95b48221e9ab263f377e997b2e47a7b8f4a2a8b"

    def __init__(self, name: str, channel_manager, peer_id, channel_name,
                 level_db_identity):
        self.__channel_service: ChannelService = channel_manager
        self.__channel_name = channel_name
        self.__pre_validate_strategy = self.__pre_validate
        self.__peer_id = peer_id
        self.__level_db = None
        self.__level_db_path = ""
        self.__level_db, self.__level_db_path = util.init_level_db(
            level_db_identity=f"{level_db_identity}_{channel_name}",
            allow_rename_path=False)
        self.__txQueue = AgingCache(
            max_age_seconds=conf.MAX_TX_QUEUE_AGING_SECONDS,
            default_item_status=TransactionStatusInQueue.normal)
        self.__blockchain = BlockChain(self.__level_db, channel_name)
        self.__peer_type = None
        self.__consensus = None
        self.__consensus_algorithm = None
        self.candidate_blocks = CandidateBlocks()
        self.__block_height_sync_lock = threading.Lock()
        self.__block_height_thread_pool = ThreadPoolExecutor(
            1, 'BlockHeightSyncThread')
        self.__block_height_future: Future = None
        self.__block_generation_scheduler = BlockGenerationScheduler(
            self.__channel_name)
        self.__precommit_block: Block = None
        self.set_peer_type(loopchain_pb2.PEER)
        self.name = name
        self.__service_status = status_code.Service.online

        self.epoch: Epoch = None

    @property
    def channel_name(self):
        return self.__channel_name

    @property
    def service_status(self):
        # Return string for compatibility.
        if self.__service_status >= 0:
            return "Service is online: " + \
                   str(1 if self.__channel_service.state_machine.state == "BlockGenerate" else 0)
        else:
            return "Service is offline: " + status_code.get_status_reason(
                self.__service_status)

    def init_epoch(self):
        """Call this after peer list update

        :return:
        """
        self.epoch = Epoch(self)

    def update_service_status(self, status):
        self.__service_status = status
        StubCollection().peer_stub.sync_task().update_status(
            self.__channel_name, {"status": self.service_status})

    @property
    def peer_type(self):
        return self.__peer_type

    @property
    def made_block_count(self):
        if self.__consensus_algorithm:
            return self.__consensus_algorithm.made_block_count
        return 0

    @property
    def consensus(self):
        return self.__consensus

    @consensus.setter
    def consensus(self, consensus):
        self.__consensus = consensus

    @property
    def consensus_algorithm(self):
        return self.__consensus_algorithm

    @property
    def precommit_block(self):
        return self.__precommit_block

    @precommit_block.setter
    def precommit_block(self, block):
        self.__precommit_block = block

    @property
    def block_generation_scheduler(self):
        return self.__block_generation_scheduler

    def get_level_db(self):
        return self.__level_db

    def clear_all_blocks(self):
        logging.debug(f"clear level db({self.__level_db_path})")
        shutil.rmtree(self.__level_db_path)

    def set_peer_type(self, peer_type):
        self.__peer_type = peer_type

    async def __create_block_generation_schedule(self):
        # util.logger.spam(f"__create_block_generation_schedule:: CREATE BLOCK GENERATION SCHEDULE")
        if conf.CONSENSUS_ALGORITHM == conf.ConsensusAlgorithm.lft:
            Schedule = namedtuple("Schedule", "callback kwargs")
            schedule = Schedule(self.__consensus_algorithm.consensus, {})
            self.__block_generation_scheduler.add_schedule(schedule)
        else:
            await self.__consensus_algorithm.consensus()

    def set_invoke_results(self, block_hash, invoke_results):
        self.__blockchain.set_invoke_results(block_hash, invoke_results)

    def get_total_tx(self):
        """
        블럭체인의 Transaction total 리턴합니다.

        :return: 블럭체인안의 transaction total count
        """
        return self.__blockchain.total_tx

    def get_blockchain(self):
        return self.__blockchain

    def pre_validate(self, tx: Transaction):
        return self.__pre_validate_strategy(tx)

    def __pre_validate(self, tx: Transaction):
        if tx.hash.hex() in self.__txQueue:
            raise TransactionInvalidDuplicatedHash(tx.hash.hex())

        if not util.is_in_time_boundary(tx.timestamp,
                                        conf.ALLOW_TIMESTAMP_BOUNDARY_SECOND):
            raise TransactionInvalidOutOfTimeBound(tx.hash.hex(), tx.timestamp,
                                                   util.get_now_time_stamp())

    def __pre_validate_pass(self, tx: Transaction):
        pass

    def broadcast_send_unconfirmed_block(self, block_: Block):
        """생성된 unconfirmed block 을 피어들에게 broadcast 하여 검증을 요청한다.
        """
        if self.__channel_service.state_machine.state == "BlockGenerate":
            logging.debug(
                f"BroadCast AnnounceUnconfirmedBlock "
                f"height({block_.header.height}) block({block_.header.hash}) peers: "
                f"{ObjectManager().channel_service.peer_manager.get_peer_count()}"
            )

            # util.logger.spam(f'block_manager:zip_test num of tx is {block_.confirmed_tx_len}')
            block_dumped = self.__blockchain.block_dumps(block_)

            ObjectManager(
            ).channel_service.broadcast_scheduler.schedule_broadcast(
                "AnnounceUnconfirmedBlock",
                loopchain_pb2.BlockSend(block=block_dumped,
                                        channel=self.__channel_name))

    def add_tx_obj(self, tx):
        """전송 받은 tx 를 Block 생성을 위해서 큐에 입력한다. load 하지 않은 채 입력한다.

        :param tx: transaction object
        """
        self.__txQueue[tx.hash.hex()] = tx

    def get_tx(self, tx_hash) -> Transaction:
        """Get transaction from block_db by tx_hash

        :param tx_hash: tx hash
        :return: tx object or None
        """
        return self.__blockchain.find_tx_by_key(tx_hash)

    def get_tx_info(self, tx_hash) -> dict:
        """Get transaction info from block_db by tx_hash

        :param tx_hash: tx hash
        :return: {'block_hash': "", 'block_height': "", "transaction": "", "result": {"code": ""}}
        """
        return self.__blockchain.find_tx_info(tx_hash)

    def get_invoke_result(self, tx_hash):
        """ get invoke result by tx

        :param tx_hash:
        :return:
        """
        return self.__blockchain.find_invoke_result_by_tx_hash(tx_hash)

    def get_tx_queue(self):
        if conf.CONSENSUS_ALGORITHM == conf.ConsensusAlgorithm.lft:
            return self.__consensus.get_tx_queue()

        return self.__txQueue

    def get_count_of_unconfirmed_tx(self):
        """BlockManager 의 상태를 확인하기 위하여 현재 입력된 unconfirmed_tx 의 카운트를 구한다.

        :return: 현재 입력된 unconfirmed tx 의 갯수
        """
        return len(self.__txQueue)

    def confirm_prev_block(self, current_block: Block):
        confirmed_block = self.__blockchain.confirm_prev_block(current_block)
        if confirmed_block is None:
            return

        # stop leader complain timer
        self.__channel_service.stop_leader_complain_timer()

        # start new epoch
        if not current_block.header.complained:
            self.epoch = Epoch.new_epoch()

        # reset leader
        self.__channel_service.reset_leader(
            current_block.header.next_leader.hex_hx())

    def __validate_duplication_unconfirmed_block(self,
                                                 unconfirmed_block: Block):
        last_unconfirmed_block: Block = self.__blockchain.last_unconfirmed_block
        try:
            candidate_block = self.candidate_blocks.blocks[
                unconfirmed_block.header.hash].block
        except KeyError:
            # When an unconfirmed block confirmed previous block, the block become last unconfirmed block,
            # But if the block is failed to verify, the block doesn't be added into candidate block.
            candidate_block: Block = last_unconfirmed_block

        if candidate_block is None or unconfirmed_block.header.hash != candidate_block.header.hash:
            return

        if self.__channel_service.state_machine.state == 'LeaderComplain' \
                and self.epoch.leader_id == unconfirmed_block.header.peer_id.hex_hx():
            raise InvalidUnconfirmedBlock(
                f"Unconfirmed block is made by complained leader. {unconfirmed_block})"
            )

        raise DuplicationUnconfirmedBlock(
            "Unconfirmed block has already been added.")

    def add_unconfirmed_block(self, unconfirmed_block):
        """

        :param unconfirmed_block:
        """
        logging.info(
            f"unconfirmed_block {unconfirmed_block.header.height}, {unconfirmed_block.body.confirm_prev_block}"
        )

        self.__validate_duplication_unconfirmed_block(unconfirmed_block)

        # TODO set below variable with right result.
        check_unconfirmed_block_has_valid_block_info_for_prev_block = True
        if not check_unconfirmed_block_has_valid_block_info_for_prev_block:
            raise InvalidUnconfirmedBlock(
                "Unconfirmed block has no valid block info for previous block")

        last_unconfirmed_block: Block = self.__blockchain.last_unconfirmed_block

        try:
            if unconfirmed_block.body.confirm_prev_block:
                self.confirm_prev_block(unconfirmed_block)
            elif last_unconfirmed_block is None:
                if self.__blockchain.last_block.header.hash != unconfirmed_block.header.prev_hash:
                    raise BlockchainError(
                        f"last block is not previous block. block={unconfirmed_block}"
                    )

                self.__blockchain.last_unconfirmed_block = unconfirmed_block
                self.__channel_service.stop_leader_complain_timer()
        except BlockchainError as e:
            logging.warning(
                f"BlockchainError while confirm_block({e}), retry block_height_sync"
            )
            self.__channel_service.state_machine.block_sync()
            raise InvalidUnconfirmedBlock(e)

    def add_confirmed_block(self, confirmed_block: Block, confirm_info=None):
        if self.__channel_service.state_machine.state != "Watch":
            util.logger.info(
                f"Can't add confirmed block if state is not Watch. {confirmed_block.header.hash.hex()}"
            )
            return

        my_height = self.__blockchain.last_block.header.height
        if confirmed_block.header.height == my_height + 1:
            result = self.__blockchain.add_block(confirmed_block,
                                                 confirm_info=confirm_info)
            if result:
                return

        self.block_height_sync()

    def rebuild_block(self):
        self.__blockchain.rebuild_transaction_count()

        nid = self.get_blockchain().find_nid()
        if nid is None:
            genesis_block = self.get_blockchain().find_block_by_height(0)
            self.__rebuild_nid(genesis_block)
        else:
            ChannelProperty().nid = nid

    def __rebuild_nid(self, block: Block):
        nid = NID.unknown.value
        if block.header.hash.hex() == BlockManager.MAINNET:
            nid = NID.mainnet.value
        elif block.header.hash.hex() == BlockManager.TESTNET:
            nid = NID.testnet.value
        elif len(block.body.transactions) > 0:
            tx = next(iter(block.body.transactions.values()))
            nid = tx.nid
            if nid is None:
                nid = NID.unknown.value

        if isinstance(nid, int):
            nid = hex(nid)

        self.get_blockchain().put_nid(nid)
        ChannelProperty().nid = nid

    def block_height_sync(self):
        with self.__block_height_sync_lock:
            need_to_sync = (self.__block_height_future is None
                            or self.__block_height_future.done())

            if need_to_sync:
                self.__channel_service.stop_leader_complain_timer()
                self.__block_height_future = self.__block_height_thread_pool.submit(
                    self.__block_height_sync)
            else:
                logging.warning(
                    'Tried block_height_sync. But failed. The thread is already running'
                )

            return need_to_sync, self.__block_height_future

    def __block_request(self, peer_stub, block_height):
        """request block by gRPC or REST

        :param peer_stub:
        :param block_height:
        :return block, max_block_height, confirm_info, response_code
        """
        if ObjectManager().channel_service.is_support_node_function(
                conf.NodeFunction.Vote):
            response = peer_stub.BlockSync(
                loopchain_pb2.BlockSyncRequest(block_height=block_height,
                                               channel=self.__channel_name),
                conf.GRPC_TIMEOUT)
            try:
                block = self.__blockchain.block_loads(response.block)
            except Exception as e:
                traceback.print_exc()
                raise exception.BlockError(
                    f"Received block is invalid: original exception={e}")
            return block, response.max_block_height, response.unconfirmed_block_height,\
                response.confirm_info, response.response_code
        else:
            # request REST(json-rpc) way to RS peer
            return self.__block_request_by_citizen(
                block_height,
                ObjectManager().channel_service.radio_station_stub)

    def __block_request_by_citizen(self, block_height, rs_rest_stub):
        get_block_result = rs_rest_stub.call("GetBlockByHeight", {
            'channel': self.__channel_name,
            'height': str(block_height)
        })
        max_height_result = rs_rest_stub.call("Status")

        if max_height_result.status_code != 200:
            raise ConnectionError

        block_version = self.get_blockchain().block_versioner.get_version(
            block_height)
        block_serializer = BlockSerializer.new(
            block_version,
            self.get_blockchain().tx_versioner)
        block = block_serializer.deserialize(get_block_result['block'])
        confirm_info = get_block_result.get('confirm_info', '')
        if isinstance(confirm_info, str):
            confirm_info = confirm_info.encode('utf-8')

        return block, json.loads(
            max_height_result.text
        )['block_height'], -1, confirm_info, message_code.Response.success

    def __precommit_block_request(self, peer_stub, last_block_height):
        """request precommit block by gRPC

        :param peer_stub:
        :param block_height:
        :return block, max_block_height, response_code
        """
        response = peer_stub.GetPrecommitBlock(
            loopchain_pb2.PrecommitBlockRequest(
                last_block_height=last_block_height,
                channel=self.__channel_name), conf.GRPC_TIMEOUT)

        if response.block == b"":
            return None, response.response_code, response.response_message
        else:
            try:
                precommit_block = self.__blockchain.block_loads(response.block)
            except Exception as e:
                traceback.print_exc()
                raise exception.BlockError(
                    f"Received block is invalid: original exception={e}")
            # util.logger.spam(
            #     f"GetPrecommitBlock:response::{response.response_code}/{response.response_message}/"
            #     f"{precommit_block}/{precommit_block.confirmed_transaction_list}")
            return precommit_block, response.response_code, response.response_message

    def __start_block_height_sync_timer(self):
        timer_key = TimerService.TIMER_KEY_BLOCK_HEIGHT_SYNC
        timer_service: TimerService = self.__channel_service.timer_service

        if timer_key not in timer_service.timer_list:
            util.logger.spam(
                f"add timer for block_request_call to radiostation...")
            timer_service.add_timer(
                timer_key,
                Timer(target=timer_key,
                      duration=conf.GET_LAST_BLOCK_TIMER,
                      is_repeat=True,
                      callback=self.block_height_sync))

    def stop_block_height_sync_timer(self):
        timer_key = TimerService.TIMER_KEY_BLOCK_HEIGHT_SYNC
        timer_service: TimerService = self.__channel_service.timer_service
        if timer_key in timer_service.timer_list:
            timer_service.stop_timer(timer_key)

    def start_block_generate_timer(self):
        timer_key = TimerService.TIMER_KEY_BLOCK_GENERATE
        timer_service: TimerService = self.__channel_service.timer_service

        if timer_key not in timer_service.timer_list:
            if self.__consensus_algorithm:
                self.__consensus_algorithm.stop()

        self.__consensus_algorithm = ConsensusSiever(self)
        self.__consensus_algorithm.start_timer(timer_service)

    def stop_block_generate_timer(self):
        if self.__consensus_algorithm:
            self.__consensus_algorithm.stop()

    def __current_block_height(self):
        if self.__blockchain.last_unconfirmed_block and \
                self.__blockchain.last_unconfirmed_block.header.height == self.__blockchain.block_height + 1:
            return self.__blockchain.block_height + 1
        else:
            return self.__blockchain.block_height

    def __current_last_block(self):
        return self.__blockchain.last_unconfirmed_block or self.__blockchain.last_block

    def __add_block_by_sync(self, block_, confirm_info=None):
        logging.debug(
            f"block_manager.py >> block_height_sync :: "
            f"height({block_.header.height}) confirm_info({confirm_info})")

        block_version = self.get_blockchain().block_versioner.get_version(
            block_.header.height)
        block_verifier = BlockVerifier.new(block_version,
                                           self.get_blockchain().tx_versioner,
                                           raise_exceptions=False)
        if block_.header.height == 0:
            block_verifier.invoke_func = self.__channel_service.genesis_invoke
        else:
            block_verifier.invoke_func = self.__channel_service.score_invoke

        reps = self.__channel_service.get_rep_ids()
        invoke_results = block_verifier.verify_loosely(
            block_, self.__blockchain.last_block, self.__blockchain, reps=reps)
        need_to_write_tx_info, need_to_score_invoke = True, True
        for exc in block_verifier.exceptions:
            if isinstance(exc, TransactionInvalidDuplicatedHash):
                need_to_write_tx_info = False
            if isinstance(exc, ScoreInvokeError) and not need_to_write_tx_info:
                need_to_score_invoke = False

        exc = next((exc for exc in block_verifier.exceptions
                    if not isinstance(exc, TransactionInvalidDuplicatedHash)),
                   None)
        if exc:
            if isinstance(exc, ScoreInvokeError) and not need_to_score_invoke:
                pass
            else:
                raise exc

        self.__blockchain.set_invoke_results(block_.header.hash.hex(),
                                             invoke_results)
        return self.__blockchain.add_block(block_, confirm_info,
                                           need_to_write_tx_info,
                                           need_to_score_invoke)

    def __confirm_prev_block_by_sync(self, block_):
        prev_block = self.__blockchain.last_unconfirmed_block
        confirm_info = block_.body.confirm_prev_block

        logging.debug(
            f"block_manager.py >> block_height_sync :: height({prev_block.header.height})"
        )

        block_version = self.get_blockchain().block_versioner.get_version(
            prev_block.header.height)
        block_verifier = BlockVerifier.new(block_version,
                                           self.get_blockchain().tx_versioner)
        if prev_block.header.height == 0:
            block_verifier.invoke_func = self.__channel_service.genesis_invoke
        else:
            block_verifier.invoke_func = self.__channel_service.score_invoke

        reps = self.__channel_service.get_rep_ids()
        invoke_results = block_verifier.verify_loosely(
            prev_block,
            self.__blockchain.last_block,
            self.__blockchain,
            reps=reps)
        self.__blockchain.set_invoke_results(prev_block.header.hash.hex(),
                                             invoke_results)
        return self.__blockchain.add_block(prev_block, confirm_info)

    def __block_request_to_peers_in_sync(self, peer_stubs, my_height,
                                         unconfirmed_block_height, max_height):
        """Extracted func from __block_height_sync.
        It has block request loop with peer_stubs for block height sync.

        :param peer_stubs:
        :param my_height:
        :param unconfirmed_block_height:
        :param max_height:
        :return: my_height, max_height
        """
        peer_stubs_len = len(peer_stubs)
        peer_index = 0
        retry_number = 0

        while max_height > my_height:
            if self.__channel_service.state_machine.state != 'BlockSync':
                break

            peer_stub = peer_stubs[peer_index]
            try:
                block, max_block_height, current_unconfirmed_block_height, confirm_info, response_code = \
                    self.__block_request(peer_stub, my_height + 1)
            except Exception as e:
                logging.warning("There is a bad peer, I hate you: " + str(e))
                traceback.print_exc()
                response_code = message_code.Response.fail

            if response_code == message_code.Response.success:
                logging.debug(f"try add block height: {block.header.height}")

                max_block_height = max(max_block_height,
                                       current_unconfirmed_block_height)
                if max_block_height > max_height:
                    util.logger.spam(
                        f"set max_height :{max_height} -> {max_block_height}")
                    max_height = max_block_height
                    if current_unconfirmed_block_height == max_block_height:
                        unconfirmed_block_height = current_unconfirmed_block_height

                try:
                    result = True
                    if max_height == unconfirmed_block_height == block.header.height \
                            and max_height > 0 and not confirm_info:
                        self.candidate_blocks.add_block(block)
                        self.__blockchain.last_unconfirmed_block = block
                        result = True
                    else:
                        result = self.__add_block_by_sync(block, confirm_info)

                    if result:
                        if block.header.height == 0:
                            self.__rebuild_nid(block)
                        elif self.__blockchain.find_nid() is None:
                            genesis_block = self.get_blockchain(
                            ).find_block_by_height(0)
                            self.__rebuild_nid(genesis_block)

                except KeyError as e:
                    result = False
                    logging.error("fail block height sync: " + str(e))
                    break
                except exception.BlockError:
                    result = False
                    logging.error(
                        "Block Error Clear all block and restart peer.")
                    self.clear_all_blocks()
                    util.exit_and_msg(
                        "Block Error Clear all block and restart peer.")
                    break
                finally:
                    peer_index = (peer_index + 1) % peer_stubs_len
                    if result:
                        my_height += 1
                        retry_number = 0
                    else:
                        retry_number += 1
                        logging.warning(
                            f"Block height({my_height}) synchronization is fail. "
                            f"{retry_number}/{conf.BLOCK_SYNC_RETRY_NUMBER}")
                        if retry_number >= conf.BLOCK_SYNC_RETRY_NUMBER:
                            util.exit_and_msg(
                                f"This peer already tried to synchronize {my_height} block "
                                f"for max retry number({conf.BLOCK_SYNC_RETRY_NUMBER}). "
                                f"Peer will be down.")
            else:
                logging.warning(
                    f"Not responding peer({peer_stub}) is removed from the peer stubs target."
                )
                if peer_stubs_len == 1:
                    raise ConnectionError
                del peer_stubs[peer_index]
                peer_stubs_len -= 1
                peer_index %= peer_stubs_len  # If peer_index is last index, go to first

        return my_height, max_height

    def __block_height_sync(self):
        # Make Peer Stub List [peer_stub, ...] and get max_height of network
        max_height, unconfirmed_block_height, peer_stubs = self.__get_peer_stub_list(
        )

        if self.__blockchain.last_unconfirmed_block is not None:
            self.candidate_blocks.remove_block(
                self.__blockchain.last_unconfirmed_block.header.hash)
        self.__blockchain.last_unconfirmed_block = None

        my_height = self.__current_block_height()
        logging.debug(
            f"in __block_height_sync max_height({max_height}), my_height({my_height})"
        )

        # prevent_next_block_mismatch until last_block_height in block DB. (excludes last_unconfirmed_block_height)
        self.get_blockchain().prevent_next_block_mismatch(
            self.__blockchain.block_height + 1)

        try:
            if peer_stubs:
                my_height, max_height = self.__block_request_to_peers_in_sync(
                    peer_stubs, my_height, unconfirmed_block_height,
                    max_height)
        except Exception as e:
            logging.warning(f"block_manager.py >>> block_height_sync :: {e}")
            traceback.print_exc()
            self.__start_block_height_sync_timer()
            return False

        curr_state = self.__channel_service.state_machine.state
        if curr_state != 'BlockSync':
            util.logger.info(f"Current state{curr_state} is not BlockSync")
            return True

        if my_height >= max_height:
            util.logger.debug(f"block_manager:block_height_sync is complete.")
            next_leader = self.__current_last_block().header.next_leader
            leader_peer = self.__channel_service.peer_manager.get_peer(
                next_leader.hex_hx()) if next_leader else None

            if leader_peer:
                self.__channel_service.peer_manager.set_leader_peer(
                    leader_peer, None)
                self.epoch = Epoch.new_epoch(leader_peer.peer_id)
            elif self.epoch.height < my_height:
                self.epoch = Epoch.new_epoch()

            self.__channel_service.state_machine.complete_sync()
        else:
            logging.warning(
                f"it's not completed block height synchronization in once ...\n"
                f"try block_height_sync again... my_height({my_height}) in channel({self.__channel_name})"
            )
            self.__channel_service.state_machine.block_sync()

        return True

    def __get_peer_stub_list(self):
        """It updates peer list for block manager refer to peer list on the loopchain network.
        This peer list is not same to the peer list of the loopchain network.

        :return max_height: a height of current blockchain
        :return peer_stubs: current peer list on the loopchain network
        """
        max_height = -1  # current max height
        unconfirmed_block_height = -1
        peer_stubs = []  # peer stub list for block height synchronization

        if not ObjectManager().channel_service.is_support_node_function(
                conf.NodeFunction.Vote):
            rest_stub = ObjectManager().channel_service.radio_station_stub
            peer_stubs.append(rest_stub)
            response = rest_stub.call("Status")
            height_from_status = int(json.loads(response.text)["block_height"])
            last_height = rest_stub.call("GetLastBlock").get('height')
            logging.debug(
                f"last_height: {last_height}, height_from_status: {height_from_status}"
            )
            max_height = max(height_from_status, last_height)
            unconfirmed_block_height = int(
                json.loads(response.text).get("unconfirmed_block_height", -1))
            return max_height, unconfirmed_block_height, peer_stubs

        # Make Peer Stub List [peer_stub, ...] and get max_height of network
        peer_target = ChannelProperty().peer_target
        peer_manager = ObjectManager().channel_service.peer_manager
        target_dict = peer_manager.get_IP_of_peers_dict()
        target_list = [
            peer_target for peer_id, peer_target in target_dict.items()
            if peer_id != ChannelProperty().peer_id
        ]

        for target in target_list:
            if target != peer_target:
                logging.debug(f"try to target({target})")
                channel = GRPCHelper().create_client_channel(target)
                stub = loopchain_pb2_grpc.PeerServiceStub(channel)
                try:
                    response = stub.GetStatus(
                        loopchain_pb2.StatusRequest(
                            request="",
                            channel=self.__channel_name,
                        ), conf.GRPC_TIMEOUT_SHORT)

                    response.block_height = max(
                        response.block_height,
                        response.unconfirmed_block_height)

                    if response.block_height > max_height:
                        # Add peer as higher than this
                        max_height = response.block_height
                        unconfirmed_block_height = response.unconfirmed_block_height
                        peer_stubs.append(stub)

                except Exception as e:
                    logging.warning(
                        f"This peer has already been removed from the block height target node. {e}"
                    )

        return max_height, unconfirmed_block_height, peer_stubs

    def __close_level_db(self):
        del self.__level_db
        self.__level_db = None
        self.__blockchain.close_blockchain_db()

    def stop(self):
        # for reuse level db when restart channel.
        self.__close_level_db()

        if conf.ALLOW_MAKE_EMPTY_BLOCK:
            self.__block_generation_scheduler.stop()

        if self.consensus_algorithm:
            self.consensus_algorithm.stop()

    def add_complain(self, complained_leader_id, new_leader_id, block_height,
                     peer_id, group_id):
        if new_leader_id == self.epoch.leader_id:
            util.logger.info(
                f"Complained new leader is current leader({new_leader_id})")
            return

        if self.epoch.height == block_height:
            self.epoch.add_complain(complained_leader_id, new_leader_id,
                                    block_height, peer_id, group_id)

            elected_leader = self.epoch.complain_result()
            if elected_leader:
                self.__channel_service.reset_leader(elected_leader,
                                                    complained=True)
                self.__channel_service.reset_leader_complain_timer()
        elif self.epoch.height < block_height:
            self.__channel_service.state_machine.block_sync()

    def leader_complain(self):
        # util.logger.notice(f"do leader complain.")
        new_leader_id = self.epoch.pop_complained_candidate_leader()
        complained_leader_id = self.epoch.leader_id

        if not new_leader_id:
            new_leader = self.__channel_service.peer_manager.get_next_leader_peer(
                current_leader_peer_id=complained_leader_id)
            new_leader_id = new_leader.peer_id if new_leader else None

            if not isinstance(new_leader_id, str):
                new_leader_id = ""

        if not isinstance(complained_leader_id, str):
            complained_leader_id = ""

        self.add_complain(complained_leader_id, new_leader_id,
                          self.epoch.height, self.__peer_id,
                          ChannelProperty().group_id)

        request = loopchain_pb2.ComplainLeaderRequest(
            complained_leader_id=complained_leader_id,
            channel=self.channel_name,
            new_leader_id=new_leader_id,
            block_height=self.epoch.height,
            message="I'm your father.",
            peer_id=self.__peer_id,
            group_id=ChannelProperty().group_id)

        util.logger.debug(f"leader complain "
                          f"complained_leader_id({complained_leader_id}), "
                          f"new_leader_id({new_leader_id})")

        self.__channel_service.broadcast_scheduler.schedule_broadcast(
            "ComplainLeader", request)

    def vote_unconfirmed_block(self, block_hash, is_validated):
        logging.debug(
            f"block_manager:vote_unconfirmed_block ({self.channel_name}/{is_validated})"
        )

        if is_validated:
            vote_code, message = message_code.get_response(
                message_code.Response.success_validate_block)
        else:
            vote_code, message = message_code.get_response(
                message_code.Response.fail_validate_block)

        block_vote = loopchain_pb2.BlockVote(
            vote_code=vote_code,
            channel=self.channel_name,
            message=message,
            block_hash=block_hash,
            peer_id=self.__peer_id,
            group_id=ChannelProperty().group_id)

        self.candidate_blocks.add_vote(block_hash,
                                       ChannelProperty().group_id,
                                       ChannelProperty().peer_id, is_validated)
        self.__channel_service.broadcast_scheduler.schedule_broadcast(
            "VoteUnconfirmedBlock", block_vote)

    def __validate_unconfirmed_block_height(self, unconfirmed_block: Block):
        my_height = self.__blockchain.block_height
        if my_height < (unconfirmed_block.header.height - 2):
            self.__channel_service.state_machine.block_sync()
            raise BlockchainError(
                f"trigger block sync in _vote my_height({my_height}), "
                f"unconfirmed_block.header.height({unconfirmed_block.header.height})"
            )

        # a block is already added that same height unconfirmed_block height
        if my_height >= unconfirmed_block.header.height:
            raise BlockchainError(
                f"block is already added my_height({my_height}), "
                f"unconfirmed_block.header.height({unconfirmed_block.header.height})"
            )

    async def _vote(self, unconfirmed_block: Block):
        exc = None
        try:
            block_version = self.__blockchain.block_versioner.get_version(
                unconfirmed_block.header.height)
            block_verifier = BlockVerifier.new(block_version,
                                               self.__blockchain.tx_versioner)
            block_verifier.invoke_func = self.__channel_service.score_invoke
            reps = self.__channel_service.get_rep_ids()
            logging.debug(
                f"unconfirmed_block.header({unconfirmed_block.header})")
            invoke_results = block_verifier.verify(
                unconfirmed_block,
                self.__blockchain.last_block,
                self.__blockchain,
                self.__blockchain.last_block.header.next_leader,
                reps=reps)
        except Exception as e:
            exc = e
            logging.error(e)
            traceback.print_exc()
        else:
            self.set_invoke_results(unconfirmed_block.header.hash.hex(),
                                    invoke_results)
            self.candidate_blocks.add_block(unconfirmed_block)
        finally:
            self.vote_unconfirmed_block(unconfirmed_block.header.hash,
                                        exc is None)

    async def vote_as_peer(self, unconfirmed_block: Block):
        """Vote to AnnounceUnconfirmedBlock
        """
        util.logger.debug(
            f"in vote_as_peer "
            f"height({unconfirmed_block.header.height}) "
            f"unconfirmed_block({unconfirmed_block.header.hash.hex()})")

        try:
            self.__validate_unconfirmed_block_height(unconfirmed_block)
        except BlockchainError as e:
            util.logger.debug(e)
            return

        try:
            self.add_unconfirmed_block(unconfirmed_block)
        except InvalidUnconfirmedBlock as e:
            util.logger.warning(e)
        except DuplicationUnconfirmedBlock as e:
            util.logger.debug(e)
            await self._vote(unconfirmed_block)
        else:
            await self._vote(unconfirmed_block)

        self.__channel_service.start_leader_complain_timer_if_tx_exists()
예제 #7
0
class Consensus(CommonThread, Publisher):
    EVENT_COMPLETE_CONSENSUS = "complete_consensus"
    EVENT_MAKE_BLOCK = "make_block"
    EVENT_LEADER_COMPLAIN_F_1 = "leader_complain_f_1"
    EVENT_LEADER_COMPLAIN_2F_1 = "leader_complain_2f_1"

    def __init__(self,
                 channel_service: 'ChannelService',
                 channel: str = None,
                 **kwargs):
        CommonThread.__init__(self)
        Publisher.__init__(self, [
            Consensus.EVENT_COMPLETE_CONSENSUS,
            Consensus.EVENT_LEADER_COMPLAIN_F_1,
            Consensus.EVENT_LEADER_COMPLAIN_2F_1, Consensus.EVENT_MAKE_BLOCK
        ])

        self.channel_name = channel
        self.__channel_service = channel_service
        self.__peer_manager = channel_service.peer_manager
        self.__last_epoch: Epoch = None
        self.__precommit_block: Block = None
        self.__epoch: Epoch = None
        self.__leader_id = None
        self.__tx_queue = AgingCache(
            max_age_seconds=conf.MAX_TX_QUEUE_AGING_SECONDS,
            default_item_status=TransactionStatusInQueue.normal)
        self.__sleep_time = None
        self.__run_logic = None
        self.__block_generation_scheduler = BlockGenerationScheduler(
            self.channel_name)

        self.__init_data()

    @property
    def epoch(self):
        return self.__epoch

    @property
    def leader_id(self) -> str:
        return self.__leader_id

    @leader_id.setter
    def leader_id(self, peer_id: str):
        self.__leader_id = peer_id

    @property
    def precommit_block(self):
        return self.__precommit_block

    @precommit_block.setter
    def precommit_block(self, precommit_block):
        self.__precommit_block = precommit_block

    @property
    def block_generation_scheduler(self):
        return self.__block_generation_scheduler

    def __init_data(self):
        self.__init_sleep_time()
        self.__set_run_logic()

    def __set_run_logic(self):
        if conf.ALLOW_MAKE_EMPTY_BLOCK:
            self.__run_logic = self.__create_block_generation_schedule
        else:
            self.__run_logic = self.__notify

    def __init_sleep_time(self):
        if conf.ALLOW_MAKE_EMPTY_BLOCK:
            self.__sleep_time = conf.INTERVAL_BLOCKGENERATION
        else:
            # self.__sleep_time = conf.SLEEP_SECONDS_IN_SERVICE_LOOP
            self.__sleep_time = 5

    def __create_block_generation_schedule(self):
        util.logger.spam(
            f"block_manager.py:__create_block_generation_schedule:: CREATE BLOCK GENERATION SCHEDULE"
        )
        Schedule = namedtuple("Schedule", "callback kwargs")
        schedule = Schedule(self._notify, {
            "event_name": Consensus.EVENT_MAKE_BLOCK,
            "tx_queue": self.__tx_queue
        })
        self.__block_generation_scheduler.add_schedule(schedule)

        time.sleep(conf.INTERVAL_BLOCKGENERATION)

    def __create_epoch(self):
        quorum, complain_quorum = self.__peer_manager.get_quorum()
        self.__epoch = Epoch(prev_epoch=self.__last_epoch,
                             precommit_block=self.__precommit_block,
                             leader_id=self.__leader_id,
                             quorum=quorum,
                             complain_quorum=complain_quorum)

        util.logger.spam(
            f"hrkim>>>consensus :: create_epoch : epoch height : {self.__epoch.block_height}"
        )
        if self.__precommit_block is not None:
            util.logger.spam(
                f"hrkim>>>consensus :: create_epoch : precommit height : {self.__precommit_block.height}"
            )

        self._notify(event_name=Consensus.EVENT_COMPLETE_CONSENSUS,
                     precommit_block=self.__precommit_block,
                     prev_epoch=self.__last_epoch,
                     epoch=self.__epoch,
                     tx_queue=self.__tx_queue)

    def get_tx_queue(self) -> AgingCache:
        return self.__tx_queue

    def add_tx_obj(self, tx):
        self.__tx_queue[tx.tx_hash] = tx

    def start(self):
        if conf.ALLOW_MAKE_EMPTY_BLOCK:
            self.__block_generation_scheduler.start()
        CommonThread.start(self)

    def stop(self):
        if conf.ALLOW_MAKE_EMPTY_BLOCK:
            self.__block_generation_scheduler.stop()
        CommonThread.stop(self)

    def run(self, e: threading.Event):
        """Consensus Thread Loop
        Collect transactions every configuration time and request to make a block to proposer.
        :param e:
        :return:
        """

        logging.info(f"channel({self.channel_name}) Consensus thread Start.")
        e.set()

        while self.is_run():
            self.__run_logic()

        logging.info(f"channel({self.channel_name}) Consensus thread Ended.")

    def change_epoch(self,
                     prev_epoch: Epoch = None,
                     precommit_block: Block = None):
        logging.debug(f"Consensus:change_epoch:: create new epoch.")
        util.logger.spam(
            f"prev_epoch: {prev_epoch} / self.__precommit_block: {self.__precommit_block}"
            f" / precommit_block: {precommit_block}")

        if precommit_block is not None:
            util.logger.spam(
                f"precommit_block:{precommit_block.height}/{precommit_block.block_hash}"
            )

        if prev_epoch is not None:
            if prev_epoch == self.__epoch and prev_epoch.status == EpochStatus.success:
                self.__precommit_block = precommit_block

            self.__last_epoch = self.__epoch
            self.__epoch = None
        elif self.__precommit_block is None and precommit_block is not None:
            self.__precommit_block = precommit_block

        if self.__precommit_block is not None:
            util.logger.spam(
                f"hrkim>>>consensus :: change_epoch : before create epoch : precommit height :"
                f"{self.__precommit_block.height}/{self.__precommit_block.block_hash}"
            )

        self.__create_epoch()

    def set_quorum(self, quorum: int, complain_quorum: int):
        self.__epoch.set_quorum(quorum, complain_quorum)

    def notify_leader_complain_f_1(self):
        pass

    def notify_leader_complain_2f_1(self):
        pass

    def block_sync(self):
        pass

    def start_timer(self, callback):
        timer_key = f"{ChannelProperty().peer_id}:{self.__precommit_block.height}"
        logging.debug(
            f"start_timer ({timer_key}/{self.__precommit_block.block_hash})")
        timer = Timer(target=timer_key,
                      duration=conf.TIMEOUT_FOR_PEER_VOTE,
                      callback=callback,
                      callback_kwargs={"epoch": self.__epoch})
        ObjectManager().channel_service.timer_service.add_timer(
            timer_key, timer)