예제 #1
0
class Shard:
    def __init__(self, env, shard_id, slave):
        self.env = env
        self.shard_id = shard_id
        self.slave = slave

        self.state = ShardState(env, shard_id, self.__init_shard_db())

        self.loop = asyncio.get_event_loop()
        self.synchronizer = Synchronizer()

        self.peers = dict()  # cluster_peer_id -> PeerShardConnection

        # block hash -> future (that will return when the block is fully propagated in the cluster)
        # the block that has been added locally but not have been fully propagated will have an entry here
        self.add_block_futures = dict()

        self.tx_generator = TransactionGenerator(self.env.quark_chain_config,
                                                 self)

        self.__init_miner()

    def __init_shard_db(self):
        """
        Create a PersistentDB or use the env.db if DB_PATH_ROOT is not specified in the ClusterConfig.
        """
        if self.env.cluster_config.use_mem_db():
            return InMemoryDb()

        db_path = "{path}/shard-{shard_id}.db".format(
            path=self.env.cluster_config.DB_PATH_ROOT, shard_id=self.shard_id)
        return PersistentDb(db_path, clean=self.env.cluster_config.CLEAN)

    def __init_miner(self):
        miner_address = self.env.quark_chain_config.testnet_master_address.address_in_branch(
            Branch.create(self.__get_shard_size(), self.shard_id))

        async def __create_block():
            # hold off mining if the shard is syncing
            while self.synchronizer.running or not self.state.initialized:
                await asyncio.sleep(0.1)

            return self.state.create_block_to_mine(address=miner_address)

        async def __add_block(block):
            # Do not add block if there is a sync in progress
            if self.synchronizer.running:
                return
            # Do not add stale block
            if self.state.header_tip.height >= block.header.height:
                return
            await self.handle_new_block(block)

        def __get_mining_param():
            return {
                "target_block_time":
                self.slave.artificial_tx_config.target_minor_block_time
            }

        shard_config = self.env.quark_chain_config.SHARD_LIST[
            self.shard_id]  # type: ShardConfig
        self.miner = Miner(
            shard_config.CONSENSUS_TYPE,
            __create_block,
            __add_block,
            __get_mining_param,
            remote=shard_config.CONSENSUS_CONFIG.REMOTE_MINE,
        )

    def __get_shard_size(self):
        return self.env.quark_chain_config.SHARD_SIZE

    @property
    def genesis_root_height(self):
        return self.env.quark_chain_config.get_genesis_root_height(
            self.shard_id)

    def add_peer(self, peer: PeerShardConnection):
        self.peers[peer.cluster_peer_id] = peer

    async def __init_genesis_state(self, root_block: RootBlock):
        block = self.state.init_genesis_state(root_block)
        xshard_list = []
        await self.slave.broadcast_xshard_tx_list(block, xshard_list,
                                                  root_block.header.height)
        await self.slave.send_minor_block_header_to_master(
            block.header,
            len(block.tx_list),
            len(xshard_list),
            self.state.get_shard_stats(),
        )

    async def init_from_root_block(self, root_block: RootBlock):
        """ Either recover state from local db or create genesis state based on config"""
        if root_block.header.height > self.genesis_root_height:
            return self.state.init_from_root_block(root_block)

        if root_block.header.height == self.genesis_root_height:
            await self.__init_genesis_state(root_block)

    async def add_root_block(self, root_block: RootBlock):
        check(root_block.header.height >= self.genesis_root_height)

        if root_block.header.height > self.genesis_root_height:
            return self.state.add_root_block(root_block)

        # this happens when there is a root chain fork
        if root_block.header.height == self.genesis_root_height:
            await self.__init_genesis_state(root_block)

    def broadcast_new_block(self, block):
        for cluster_peer_id, peer in self.peers.items():
            peer.send_new_block(block)

    def broadcast_new_tip(self):
        for cluster_peer_id, peer in self.peers.items():
            peer.broadcast_new_tip()

    def broadcast_tx_list(self, tx_list, source_peer=None):
        for cluster_peer_id, peer in self.peers.items():
            if source_peer == peer:
                continue
            peer.broadcast_tx_list(tx_list)

    async def handle_new_block(self, block):
        """
        0. if local shard is syncing, doesn't make sense to add, skip
        1. if block parent is not in local state/new block pool, discard
        2. if already in cache or in local state/new block pool, pass
        3. validate: check time, difficulty, POW
        4. add it to new minor block broadcast cache
        5. broadcast to all peers (minus peer that sent it, optional)
        6. add_block() to local state (then remove from cache)
             also, broadcast tip if tip is updated (so that peers can sync if they missed blocks, or are new)
        """
        if self.synchronizer.running:
            # TODO optinal: queue the block if it came from broadcast to so that once sync is over, catch up immediately
            return

        if block.header.get_hash() in self.state.new_block_pool:
            return
        if self.state.db.contain_minor_block_by_hash(block.header.get_hash()):
            return

        if not self.state.db.contain_minor_block_by_hash(
                block.header.hash_prev_minor_block):
            if block.header.hash_prev_minor_block not in self.state.new_block_pool:
                return

        # TODO check difficulty and POW here
        # one option is to use __validate_block but we may not need the full check
        if block.header.create_time > time_ms() // 1000 + 30:
            return

        self.state.new_block_pool[block.header.get_hash()] = block

        self.broadcast_new_block(block)
        await self.add_block(block)

    async def add_block(self, block):
        """ Returns true if block is successfully added. False on any error.
        called by 1. local miner (will not run if syncing) 2. SyncTask
        """
        old_tip = self.state.header_tip
        try:
            xshard_list = self.state.add_block(block)
        except Exception as e:
            Logger.error_exception()
            return False

        # only remove from pool if the block successfully added to state,
        #   this may cache failed blocks but prevents them being broadcasted more than needed
        # TODO add ttl to blocks in new_block_pool
        self.state.new_block_pool.pop(block.header.get_hash(), None)
        # block has been added to local state, broadcast tip so that peers can sync if needed
        try:
            if old_tip != self.state.header_tip:
                self.broadcast_new_tip()
        except Exception:
            Logger.warning_every_sec("broadcast tip failure", 1)

        # block already existed in local shard state
        # but might not have been propagated to other shards and master
        # let's make sure all the shards and master got it before return
        if xshard_list is None:
            future = self.add_block_futures.get(block.header.get_hash(), None)
            if future:
                Logger.info(
                    "[{}] {} is being added ... waiting for it to finish".
                    format(block.header.branch.get_shard_id(),
                           block.header.height))
                await future
            return True

        self.add_block_futures[
            block.header.get_hash()] = self.loop.create_future()

        prev_root_height = self.state.db.get_root_block_by_hash(
            block.header.hash_prev_root_block).header.height
        await self.slave.broadcast_xshard_tx_list(block, xshard_list,
                                                  prev_root_height)
        await self.slave.send_minor_block_header_to_master(
            block.header,
            len(block.tx_list),
            len(xshard_list),
            self.state.get_shard_stats(),
        )

        self.add_block_futures[block.header.get_hash()].set_result(None)
        del self.add_block_futures[block.header.get_hash()]
        return True

    async def add_block_list_for_sync(self, block_list):
        """ Add blocks in batch to reduce RPCs. Will NOT broadcast to peers.

        Returns true if blocks are successfully added. False on any error.
        This function only adds blocks to local and propagate xshard list to other shards.
        It does NOT notify master because the master should already have the minor header list,
        and will add them once this function returns successfully.
        """
        if not block_list:
            return True

        existing_add_block_futures = []
        block_hash_to_x_shard_list = dict()
        for block in block_list:
            check(block.header.branch.get_shard_id() == self.shard_id)

            block_hash = block.header.get_hash()
            try:
                xshard_list = self.state.add_block(block)
            except Exception as e:
                Logger.error_exception()
                return False

            # block already existed in local shard state
            # but might not have been propagated to other shards and master
            # let's make sure all the shards and master got it before return
            if xshard_list is None:
                future = self.add_block_futures.get(block_hash, None)
                if future:
                    existing_add_block_futures.append(future)
            else:
                prev_root_height = self.state.db.get_root_block_by_hash(
                    block.header.hash_prev_root_block).header.height
                block_hash_to_x_shard_list[block_hash] = (xshard_list,
                                                          prev_root_height)
                self.add_block_futures[block_hash] = self.loop.create_future()

        await self.slave.batch_broadcast_xshard_tx_list(
            block_hash_to_x_shard_list, block_list[0].header.branch)

        for block_hash in block_hash_to_x_shard_list.keys():
            self.add_block_futures[block_hash].set_result(None)
            del self.add_block_futures[block_hash]

        await asyncio.gather(*existing_add_block_futures)

        return True

    def add_tx_list(self, tx_list, source_peer=None):
        if not tx_list:
            return
        valid_tx_list = []
        for tx in tx_list:
            if self.add_tx(tx):
                valid_tx_list.append(tx)
        if not valid_tx_list:
            return
        self.broadcast_tx_list(valid_tx_list, source_peer)

    def add_tx(self, tx: Transaction):
        return self.state.add_tx(tx)
예제 #2
0
class Shard:
    def __init__(self, env, full_shard_id, slave):
        self.env = env
        self.full_shard_id = full_shard_id
        self.slave = slave

        self.state = ShardState(env, full_shard_id, self.__init_shard_db())

        self.loop = asyncio.get_event_loop()
        self.synchronizer = Synchronizer(
            self.state.subscription_manager.notify_sync,
            lambda: self.state.header_tip)

        self.peers = dict()  # cluster_peer_id -> PeerShardConnection

        # block hash -> future (that will return when the block is fully propagated in the cluster)
        # the block that has been added locally but not have been fully propagated will have an entry here
        self.add_block_futures = dict()

        self.tx_generator = TransactionGenerator(self.env.quark_chain_config,
                                                 self)

        self.__init_miner()

    def __init_shard_db(self):
        """
        Create a PersistentDB or use the env.db if DB_PATH_ROOT is not specified in the ClusterConfig.
        """
        if self.env.cluster_config.use_mem_db():
            return InMemoryDb()

        db_path = "{path}/shard-{shard_id}.db".format(
            path=self.env.cluster_config.DB_PATH_ROOT,
            shard_id=self.full_shard_id)
        return PersistentDb(db_path, clean=self.env.cluster_config.CLEAN)

    def __init_miner(self):
        async def __create_block(coinbase_addr: Address, retry=True):
            # hold off mining if the shard is syncing
            while self.synchronizer.running or not self.state.initialized:
                if not retry:
                    break
                await asyncio.sleep(0.1)

            if coinbase_addr.is_empty():  # devnet or wrong config
                coinbase_addr.full_shard_key = self.full_shard_id
            return self.state.create_block_to_mine(address=coinbase_addr)

        async def __add_block(block):
            # do not add block if there is a sync in progress
            if self.synchronizer.running:
                return
            # do not add stale block
            if self.state.header_tip.height >= block.header.height:
                return
            await self.handle_new_block(block)

        def __get_mining_param():
            return {
                "target_block_time":
                self.slave.artificial_tx_config.target_minor_block_time
            }

        shard_config = self.env.quark_chain_config.shards[
            self.full_shard_id]  # type: ShardConfig
        self.miner = Miner(
            shard_config.CONSENSUS_TYPE,
            __create_block,
            __add_block,
            __get_mining_param,
            lambda: self.state.header_tip,
            remote=shard_config.CONSENSUS_CONFIG.REMOTE_MINE,
        )

    @property
    def genesis_root_height(self):
        return self.env.quark_chain_config.get_genesis_root_height(
            self.full_shard_id)

    def add_peer(self, peer: PeerShardConnection):
        self.peers[peer.cluster_peer_id] = peer
        Logger.info("[{}] connected to peer {}".format(
            Branch(self.full_shard_id).to_str(), peer.cluster_peer_id))

    async def create_peer_shard_connections(self, cluster_peer_ids,
                                            master_conn):
        conns = []
        for cluster_peer_id in cluster_peer_ids:
            peer_shard_conn = PeerShardConnection(
                master_conn=master_conn,
                cluster_peer_id=cluster_peer_id,
                shard=self,
                name="{}_vconn_{}".format(master_conn.name, cluster_peer_id),
            )
            asyncio.ensure_future(peer_shard_conn.active_and_loop_forever())
            conns.append(peer_shard_conn)
        await asyncio.gather(*[conn.active_future for conn in conns])
        for conn in conns:
            self.add_peer(conn)

    async def __init_genesis_state(self, root_block: RootBlock):
        block, coinbase_amount_map = self.state.init_genesis_state(root_block)
        xshard_list = []
        await self.slave.broadcast_xshard_tx_list(block, xshard_list,
                                                  root_block.header.height)
        await self.slave.send_minor_block_header_to_master(
            block.header,
            len(block.tx_list),
            len(xshard_list),
            coinbase_amount_map,
            self.state.get_shard_stats(),
        )

    async def init_from_root_block(self, root_block: RootBlock):
        """ Either recover state from local db or create genesis state based on config"""
        if root_block.header.height > self.genesis_root_height:
            return self.state.init_from_root_block(root_block)

        if root_block.header.height == self.genesis_root_height:
            await self.__init_genesis_state(root_block)

    async def add_root_block(self, root_block: RootBlock):
        if root_block.header.height > self.genesis_root_height:
            return self.state.add_root_block(root_block)

        # this happens when there is a root chain fork
        if root_block.header.height == self.genesis_root_height:
            await self.__init_genesis_state(root_block)

    def broadcast_new_block(self, block):
        for cluster_peer_id, peer in self.peers.items():
            peer.send_new_block(block)

    def broadcast_new_tip(self):
        for cluster_peer_id, peer in self.peers.items():
            peer.broadcast_new_tip()

    def broadcast_tx_list(self, tx_list, source_peer=None):
        for cluster_peer_id, peer in self.peers.items():
            if source_peer == peer:
                continue
            peer.broadcast_tx_list(tx_list)

    async def handle_new_block(self, block):
        """
        This is a fast path for block propagation. The block is broadcasted to peers before being added to local state.
        0. if local shard is syncing, doesn't make sense to add, skip
        1. if block parent is not in local state/new block pool, discard (TODO: is this necessary?)
        2. if already in cache or in local state/new block pool, pass
        3. validate: check time, difficulty, POW
        4. add it to new minor block broadcast cache
        5. broadcast to all peers (minus peer that sent it, optional)
        6. add_block() to local state (then remove from cache)
           also, broadcast tip if tip is updated (so that peers can sync if they missed blocks, or are new)
        """
        if self.synchronizer.running:
            # TODO optional: queue the block if it came from broadcast to so that once sync is over,
            # catch up immediately
            return

        if block.header.get_hash() in self.state.new_block_header_pool:
            return
        if self.state.db.contain_minor_block_by_hash(block.header.get_hash()):
            return

        prev_hash, prev_header = block.header.hash_prev_minor_block, None
        if prev_hash in self.state.new_block_header_pool:
            prev_header = self.state.new_block_header_pool[prev_hash]
        else:
            prev_header = self.state.db.get_minor_block_header_by_hash(
                prev_hash)
        if prev_header is None:  # Missing prev
            return

        # Sanity check on timestamp and block height
        if (block.header.create_time >
                time_ms() // 1000 + ALLOWED_FUTURE_BLOCKS_TIME_BROADCAST):
            return
        # Ignore old blocks
        if (self.state.header_tip
                and self.state.header_tip.height - block.header.height >
                self.state.shard_config.max_stale_minor_block_height_diff):
            return

        # There is a race that the root block may not be processed at the moment.
        # Ignore it if its root block is not found.
        # Otherwise, validate_block() will fail and we will disconnect the peer.
        if (self.state.get_root_block_header_by_hash(
                block.header.hash_prev_root_block) is None):
            return

        try:
            self.state.validate_block(block)
        except Exception as e:
            Logger.warning("[{}] got bad block in handle_new_block: {}".format(
                block.header.branch.to_str(), str(e)))
            raise e

        self.state.new_block_header_pool[
            block.header.get_hash()] = block.header

        Logger.info("[{}/{}] got new block with height {}".format(
            block.header.branch.get_chain_id(),
            block.header.branch.get_shard_id(),
            block.header.height,
        ))

        self.broadcast_new_block(block)
        await self.add_block(block)

    def __get_block_commit_status_by_hash(self, block_hash):
        # If the block is committed, it means
        # - All neighbor shards/slaves receives x-shard tx list
        # - The block header is sent to master
        # then return immediately
        if self.state.is_committed_by_hash(block_hash):
            return BLOCK_COMMITTED, None

        # Check if the block is being propagating to other slaves and the master
        # Let's make sure all the shards and master got it before committing it
        future = self.add_block_futures.get(block_hash)
        if future is not None:
            return BLOCK_COMMITTING, future

        return BLOCK_UNCOMMITTED, None

    async def add_block(self, block):
        """ Returns true if block is successfully added. False on any error.
        called by 1. local miner (will not run if syncing) 2. SyncTask
        """

        block_hash = block.header.get_hash()
        commit_status, future = self.__get_block_commit_status_by_hash(
            block_hash)
        if commit_status == BLOCK_COMMITTED:
            return True
        elif commit_status == BLOCK_COMMITTING:
            Logger.info(
                "[{}] {} is being added ... waiting for it to finish".format(
                    block.header.branch.to_str(), block.header.height))
            await future
            return True

        check(commit_status == BLOCK_UNCOMMITTED)
        # Validate and add the block
        old_tip = self.state.header_tip
        try:
            xshard_list, coinbase_amount_map = self.state.add_block(block,
                                                                    force=True)
        except Exception as e:
            Logger.error_exception()
            return False

        # only remove from pool if the block successfully added to state,
        # this may cache failed blocks but prevents them being broadcasted more than needed
        # TODO add ttl to blocks in new_block_header_pool
        self.state.new_block_header_pool.pop(block_hash, None)
        # block has been added to local state, broadcast tip so that peers can sync if needed
        try:
            if old_tip != self.state.header_tip:
                self.broadcast_new_tip()
        except Exception:
            Logger.warning_every_sec("broadcast tip failure", 1)

        # Add the block in future and wait
        self.add_block_futures[block_hash] = self.loop.create_future()

        prev_root_height = self.state.db.get_root_block_header_by_hash(
            block.header.hash_prev_root_block).height
        await self.slave.broadcast_xshard_tx_list(block, xshard_list,
                                                  prev_root_height)
        await self.slave.send_minor_block_header_to_master(
            block.header,
            len(block.tx_list),
            len(xshard_list),
            coinbase_amount_map,
            self.state.get_shard_stats(),
        )

        # Commit the block
        self.state.commit_by_hash(block_hash)
        Logger.debug("committed mblock {}".format(block_hash.hex()))

        # Notify the rest
        self.add_block_futures[block_hash].set_result(None)
        del self.add_block_futures[block_hash]
        return True

    def check_minor_block_by_header(self, header):
        """ Raise exception of the block is invalid
        """
        block = self.state.get_block_by_hash(header.get_hash())
        if block is None:
            raise RuntimeError("block {} cannot be found".format(
                header.get_hash()))
        if header.height == 0:
            return
        self.state.add_block(block,
                             force=True,
                             write_db=False,
                             skip_if_too_old=False)

    async def add_block_list_for_sync(self, block_list):
        """ Add blocks in batch to reduce RPCs. Will NOT broadcast to peers.

        Returns true if blocks are successfully added. False on any error.
        Additionally, returns list of coinbase_amount_map for each block
        This function only adds blocks to local and propagate xshard list to other shards.
        It does NOT notify master because the master should already have the minor header list,
        and will add them once this function returns successfully.
        """
        coinbase_amount_list = []
        if not block_list:
            return True, coinbase_amount_list

        existing_add_block_futures = []
        block_hash_to_x_shard_list = dict()
        uncommitted_block_header_list = []
        uncommitted_coinbase_amount_map_list = []
        for block in block_list:
            check(
                block.header.branch.get_full_shard_id() == self.full_shard_id)

            block_hash = block.header.get_hash()
            # adding the block header one assuming the block will be validated.
            coinbase_amount_list.append(block.header.coinbase_amount_map)

            commit_status, future = self.__get_block_commit_status_by_hash(
                block_hash)
            if commit_status == BLOCK_COMMITTED:
                # Skip processing the block if it is already committed
                Logger.warning(
                    "minor block to sync {} is already committed".format(
                        block_hash.hex()))
                continue
            elif commit_status == BLOCK_COMMITTING:
                # Check if the block is being propagating to other slaves and the master
                # Let's make sure all the shards and master got it before committing it
                Logger.info(
                    "[{}] {} is being added ... waiting for it to finish".
                    format(block.header.branch.to_str(), block.header.height))
                existing_add_block_futures.append(future)
                continue

            check(commit_status == BLOCK_UNCOMMITTED)
            # Validate and add the block
            try:
                xshard_list, coinbase_amount_map = self.state.add_block(
                    block, skip_if_too_old=False, force=True)
            except Exception as e:
                Logger.error_exception()
                return False, None

            prev_root_height = self.state.db.get_root_block_header_by_hash(
                block.header.hash_prev_root_block).height
            block_hash_to_x_shard_list[block_hash] = (xshard_list,
                                                      prev_root_height)
            self.add_block_futures[block_hash] = self.loop.create_future()
            uncommitted_block_header_list.append(block.header)
            uncommitted_coinbase_amount_map_list.append(
                block.header.coinbase_amount_map)

        await self.slave.batch_broadcast_xshard_tx_list(
            block_hash_to_x_shard_list, block_list[0].header.branch)
        check(
            len(uncommitted_coinbase_amount_map_list) == len(
                uncommitted_block_header_list))
        await self.slave.send_minor_block_header_list_to_master(
            uncommitted_block_header_list,
            uncommitted_coinbase_amount_map_list)

        # Commit all blocks and notify all rest add block operations
        for block_header in uncommitted_block_header_list:
            block_hash = block_header.get_hash()
            self.state.commit_by_hash(block_hash)
            Logger.debug("committed mblock {}".format(block_hash.hex()))

            self.add_block_futures[block_hash].set_result(None)
            del self.add_block_futures[block_hash]

        # Wait for the other add block operations
        await asyncio.gather(*existing_add_block_futures)

        return True, coinbase_amount_list

    def add_tx_list(self, tx_list, source_peer=None):
        if not tx_list:
            return
        valid_tx_list = []
        for tx in tx_list:
            if self.add_tx(tx):
                valid_tx_list.append(tx)
        if not valid_tx_list:
            return
        self.broadcast_tx_list(valid_tx_list, source_peer)

    def add_tx(self, tx: TypedTransaction):
        return self.state.add_tx(tx)
예제 #3
0
class Shard:
    def __init__(self, env, full_shard_id, slave):
        self.env = env
        self.full_shard_id = full_shard_id
        self.slave = slave

        self.state = ShardState(env, full_shard_id, self.__init_shard_db())

        self.loop = asyncio.get_event_loop()
        self.synchronizer = Synchronizer()

        self.peers = dict()  # cluster_peer_id -> PeerShardConnection

        # block hash -> future (that will return when the block is fully propagated in the cluster)
        # the block that has been added locally but not have been fully propagated will have an entry here
        self.add_block_futures = dict()

        self.tx_generator = TransactionGenerator(self.env.quark_chain_config,
                                                 self)

        self.__init_miner()

    def __init_shard_db(self):
        """
        Create a PersistentDB or use the env.db if DB_PATH_ROOT is not specified in the ClusterConfig.
        """
        if self.env.cluster_config.use_mem_db():
            return InMemoryDb()

        db_path = "{path}/shard-{shard_id}.db".format(
            path=self.env.cluster_config.DB_PATH_ROOT,
            shard_id=self.full_shard_id)
        return PersistentDb(db_path, clean=self.env.cluster_config.CLEAN)

    def __init_miner(self):
        miner_address = Address.create_from(self.env.quark_chain_config.shards[
            self.full_shard_id].COINBASE_ADDRESS)

        async def __create_block(retry=True):
            # hold off mining if the shard is syncing
            while self.synchronizer.running or not self.state.initialized:
                if not retry:
                    break
                await asyncio.sleep(0.1)

            return self.state.create_block_to_mine(address=miner_address)

        async def __add_block(block):
            # Do not add block if there is a sync in progress
            if self.synchronizer.running:
                return
            # Do not add stale block
            if self.state.header_tip.height >= block.header.height:
                return
            await self.handle_new_block(block)

        def __get_mining_param():
            return {
                "target_block_time":
                self.slave.artificial_tx_config.target_minor_block_time
            }

        shard_config = self.env.quark_chain_config.shards[
            self.full_shard_id]  # type: ShardConfig
        self.miner = Miner(
            shard_config.CONSENSUS_TYPE,
            __create_block,
            __add_block,
            __get_mining_param,
            remote=shard_config.CONSENSUS_CONFIG.REMOTE_MINE,
        )

    @property
    def genesis_root_height(self):
        return self.env.quark_chain_config.get_genesis_root_height(
            self.full_shard_id)

    def add_peer(self, peer: PeerShardConnection):
        self.peers[peer.cluster_peer_id] = peer
        Logger.info("[{}] connected to peer {}".format(
            Branch(self.full_shard_id).to_str(), peer.cluster_peer_id))

    async def create_peer_shard_connections(self, cluster_peer_ids,
                                            master_conn):
        conns = []
        for cluster_peer_id in cluster_peer_ids:
            peer_shard_conn = PeerShardConnection(
                master_conn=master_conn,
                cluster_peer_id=cluster_peer_id,
                shard=self,
                name="{}_vconn_{}".format(master_conn.name, cluster_peer_id),
            )
            asyncio.ensure_future(peer_shard_conn.active_and_loop_forever())
            conns.append(peer_shard_conn)
        await asyncio.gather(*[conn.active_future for conn in conns])
        for conn in conns:
            self.add_peer(conn)

    async def __init_genesis_state(self, root_block: RootBlock):
        block, coinbase_amount_map = self.state.init_genesis_state(root_block)
        xshard_list = []
        await self.slave.broadcast_xshard_tx_list(block, xshard_list,
                                                  root_block.header.height)
        await self.slave.send_minor_block_header_to_master(
            block.header,
            len(block.tx_list),
            len(xshard_list),
            coinbase_amount_map,
            self.state.get_shard_stats(),
        )

    async def init_from_root_block(self, root_block: RootBlock):
        """ Either recover state from local db or create genesis state based on config"""
        if root_block.header.height > self.genesis_root_height:
            return self.state.init_from_root_block(root_block)

        if root_block.header.height == self.genesis_root_height:
            await self.__init_genesis_state(root_block)

    async def add_root_block(self, root_block: RootBlock):
        if root_block.header.height > self.genesis_root_height:
            return self.state.add_root_block(root_block)

        # this happens when there is a root chain fork
        if root_block.header.height == self.genesis_root_height:
            await self.__init_genesis_state(root_block)

    def broadcast_new_block(self, block):
        for cluster_peer_id, peer in self.peers.items():
            peer.send_new_block(block)

    def broadcast_new_tip(self):
        for cluster_peer_id, peer in self.peers.items():
            peer.broadcast_new_tip()

    def broadcast_tx_list(self, tx_list, source_peer=None):
        for cluster_peer_id, peer in self.peers.items():
            if source_peer == peer:
                continue
            peer.broadcast_tx_list(tx_list)

    async def handle_new_block(self, block):
        """
        This is a fast path for block propagation. The block is broadcasted to peers before being added to local state.
        0. if local shard is syncing, doesn't make sense to add, skip
        1. if block parent is not in local state/new block pool, discard (TODO: is this necessary?)
        2. if already in cache or in local state/new block pool, pass
        3. validate: check time, difficulty, POW
        4. add it to new minor block broadcast cache
        5. broadcast to all peers (minus peer that sent it, optional)
        6. add_block() to local state (then remove from cache)
           also, broadcast tip if tip is updated (so that peers can sync if they missed blocks, or are new)
        """
        if self.synchronizer.running:
            # TODO optional: queue the block if it came from broadcast to so that once sync is over,
            # catch up immediately
            return

        if block.header.get_hash() in self.state.new_block_pool:
            return
        if self.state.db.contain_minor_block_by_hash(block.header.get_hash()):
            return

        if not self.state.db.contain_minor_block_by_hash(
                block.header.hash_prev_minor_block):
            if block.header.hash_prev_minor_block not in self.state.new_block_pool:
                return

        # Doing full POSW check requires prev block has been added to the state, which could
        # slow down block propagation.
        # TODO: this is a copy of the code in SyncTask.__validate_block_headers. this it a helper
        try:
            header = block.header
            # Note that PoSW may lower diff, so checks here are necessary but not sufficient
            # More checks happen during block addition
            shard_config = self.env.quark_chain_config.shards[
                header.branch.get_full_shard_id()]
            consensus_type = shard_config.CONSENSUS_TYPE
            diff = header.difficulty
            if shard_config.POSW_CONFIG.ENABLED:
                diff //= shard_config.POSW_CONFIG.DIFF_DIVIDER
            validate_seal(header, consensus_type, adjusted_diff=diff)
        except Exception as e:
            Logger.warning(
                "[{}] got block with bad seal in handle_new_block: {}".format(
                    header.branch.to_str(), str(e)))
            raise e

        if block.header.create_time > time_ms() // 1000 + 30:
            return

        self.state.new_block_pool[block.header.get_hash()] = block

        Logger.info("[{}/{}] got new block with height {}".format(
            block.header.branch.get_chain_id(),
            block.header.branch.get_shard_id(),
            block.header.height,
        ))
        self.broadcast_new_block(block)
        await self.add_block(block)

    async def add_block(self, block):
        """ Returns true if block is successfully added. False on any error.
        called by 1. local miner (will not run if syncing) 2. SyncTask
        """
        old_tip = self.state.header_tip
        try:
            xshard_list, coinbase_amount_map = self.state.add_block(block)
        except Exception as e:
            Logger.error_exception()
            return False

        # only remove from pool if the block successfully added to state,
        #   this may cache failed blocks but prevents them being broadcasted more than needed
        # TODO add ttl to blocks in new_block_pool
        self.state.new_block_pool.pop(block.header.get_hash(), None)
        # block has been added to local state, broadcast tip so that peers can sync if needed
        try:
            if old_tip != self.state.header_tip:
                self.broadcast_new_tip()
        except Exception:
            Logger.warning_every_sec("broadcast tip failure", 1)

        # block already existed in local shard state
        # but might not have been propagated to other shards and master
        # let's make sure all the shards and master got it before return
        if xshard_list is None:
            future = self.add_block_futures.get(block.header.get_hash(), None)
            if future:
                Logger.info(
                    "[{}] {} is being added ... waiting for it to finish".
                    format(block.header.branch.to_str(), block.header.height))
                await future
            return True

        self.add_block_futures[
            block.header.get_hash()] = self.loop.create_future()

        prev_root_height = self.state.db.get_root_block_by_hash(
            block.header.hash_prev_root_block).header.height
        await self.slave.broadcast_xshard_tx_list(block, xshard_list,
                                                  prev_root_height)
        await self.slave.send_minor_block_header_to_master(
            block.header,
            len(block.tx_list),
            len(xshard_list),
            coinbase_amount_map,
            self.state.get_shard_stats(),
        )

        self.add_block_futures[block.header.get_hash()].set_result(None)
        del self.add_block_futures[block.header.get_hash()]
        return True

    async def add_block_list_for_sync(self, block_list):
        """ Add blocks in batch to reduce RPCs. Will NOT broadcast to peers.

        Returns true if blocks are successfully added. False on any error.
        Additionally, returns list of coinbase_amount_map for each block
            (list can contain None indicating that the block has been added and master should receive token map soon)
        This function only adds blocks to local and propagate xshard list to other shards.
        It does NOT notify master because the master should already have the minor header list,
        and will add them once this function returns successfully.
        """
        coinbase_amount_list = []
        if not block_list:
            return True, coinbase_amount_list

        existing_add_block_futures = []
        block_hash_to_x_shard_list = dict()
        for block in block_list:
            check(
                block.header.branch.get_full_shard_id() == self.full_shard_id)

            block_hash = block.header.get_hash()
            try:
                xshard_list, coinbase_amount_map = self.state.add_block(
                    block, skip_if_too_old=False)
                coinbase_amount_list.append(coinbase_amount_map)
            except Exception as e:
                Logger.error_exception()
                return False, coinbase_amount_list

            # block already existed in local shard state
            # but might not have been propagated to other shards and master
            # let's make sure all the shards and master got it before return
            if xshard_list is None:
                future = self.add_block_futures.get(block_hash, None)
                if future:
                    existing_add_block_futures.append(future)
            else:
                prev_root_height = self.state.db.get_root_block_by_hash(
                    block.header.hash_prev_root_block).header.height
                block_hash_to_x_shard_list[block_hash] = (xshard_list,
                                                          prev_root_height)
                self.add_block_futures[block_hash] = self.loop.create_future()

        await self.slave.batch_broadcast_xshard_tx_list(
            block_hash_to_x_shard_list, block_list[0].header.branch)

        for block_hash in block_hash_to_x_shard_list.keys():
            self.add_block_futures[block_hash].set_result(None)
            del self.add_block_futures[block_hash]

        await asyncio.gather(*existing_add_block_futures)

        return True, coinbase_amount_list

    def add_tx_list(self, tx_list, source_peer=None):
        if not tx_list:
            return
        valid_tx_list = []
        for tx in tx_list:
            if self.add_tx(tx):
                valid_tx_list.append(tx)
        if not valid_tx_list:
            return
        self.broadcast_tx_list(valid_tx_list, source_peer)

    def add_tx(self, tx: TypedTransaction):
        return self.state.add_tx(tx)