Example #1
0
def update_genesis_alloc(cluser_config):
    """ Update ShardConfig.GENESIS.ALLOC """
    ALLOC_FILE = "alloc.json"
    LOADTEST_FILE = "loadtest.json"

    if not cluser_config.GENESIS_DIR:
        return
    alloc_file = os.path.join(cluser_config.GENESIS_DIR, ALLOC_FILE)
    loadtest_file = os.path.join(cluser_config.GENESIS_DIR, LOADTEST_FILE)

    qkc_config = cluser_config.QUARKCHAIN

    # each account in alloc_file is only funded on the shard it belongs to
    try:
        with open(alloc_file, "r") as f:
            items = json.load(f)
        for item in items:
            address = Address.create_from(item["address"])
            shard = address.get_shard_id(qkc_config.SHARD_SIZE)
            qkc_config.SHARD_LIST[shard].GENESIS.ALLOC[item["address"]] = 1000000 * (
                10 ** 18
            )

        Logger.info(
            "Imported {} accounts from genesis alloc at {}".format(
                len(items), alloc_file
            )
        )
    except Exception as e:
        Logger.warning("Unable to load genesis alloc from {}: {}".format(alloc_file, e))

    # each account in loadtest file is funded on all the shards
    try:
        with open(loadtest_file, "r") as f:
            items = json.load(f)
            qkc_config.loadtest_accounts = items

        for item in items:
            address = Address.create_from(item["address"])
            for i, shard in enumerate(qkc_config.SHARD_LIST):
                shard.GENESIS.ALLOC[
                    address.address_in_shard(i).serialize().hex()
                ] = 1000 * (10 ** 18)

        Logger.info(
            "Imported {} loadtest accounts from {}".format(len(items), loadtest_file)
        )
    except Exception:
        Logger.info("No loadtest accounts imported into genesis alloc")
Example #2
0
    async def __handle(self, request):
        request = await request.text()
        Logger.info(request)

        d = dict()
        try:
            d = json.loads(request)
        except Exception:
            pass
        method = d.get("method", "null")
        if method in self.counters:
            self.counters[method] += 1
        else:
            self.counters[method] = 1
        # Use armor to prevent the handler from being cancelled when
        # aiohttp server loses connection to client
        response = await armor(self.handlers.dispatch(request))
        if "error" in response:
            Logger.error(response)
        if response.is_notification:
            return web.Response()
        return web.json_response(response, status=response.http_status)
Example #3
0
    def __recover_from_db(self):
        """ Recover the best chain from local database.
        """
        Logger.info("Recovering root chain from local database...")

        if b"tipHash" not in self.db:
            return None

        r_hash = self.db.get(b"tipHash")
        r_block = RootBlock.deserialize(self.db.get(b"rblock_" + r_hash))
        self.tip_header = r_block.header

        while len(self.r_header_pool) < self.max_num_blocks_to_recover:
            self.r_header_pool[r_hash] = r_block.header
            for m_header in r_block.minor_block_header_list:
                self.m_hash_set.add(m_header.get_hash())

            if r_block.header.height <= 0:
                break

            r_hash = r_block.header.hash_prev_block
            r_block = RootBlock.deserialize(self.db.get(b"rblock_" + r_hash))
 async def connect(self, ip, port):
     Logger.info("connecting {} {}".format(ip, port))
     try:
         reader, writer = await asyncio.open_connection(ip,
                                                        port,
                                                        loop=self.loop)
     except Exception as e:
         Logger.info("failed to connect {} {}: {}".format(ip, port, e))
         return None
     peer = Peer(
         self.env,
         reader,
         writer,
         self,
         self.master_server,
         self.__get_next_cluster_peer_id(),
     )
     peer.send_hello()
     result = await peer.start(is_server=False)
     if result is not None:
         return None
     return peer
Example #5
0
    async def handle_sync_minor_block_list_request(self, req):
        async def __download_blocks(block_hash_list):
            op, resp, rpc_id = await peer_shard_conn.write_rpc_request(
                CommandOp.GET_MINOR_BLOCK_LIST_REQUEST,
                GetMinorBlockListRequest(block_hash_list),
            )
            return resp.minor_block_list

        shard = self.shards.get(req.branch, None)
        if not shard:
            return SyncMinorBlockListResponse(error_code=errno.EBADMSG)
        peer_shard_conn = shard.peers.get(req.cluster_peer_id, None)
        if not peer_shard_conn:
            return SyncMinorBlockListResponse(error_code=errno.EBADMSG)

        BLOCK_BATCH_SIZE = 100
        try:
            block_hash_list = req.minor_block_hash_list
            while len(block_hash_list) > 0:
                blocks_to_download = block_hash_list[:BLOCK_BATCH_SIZE]
                block_chain = await __download_blocks(blocks_to_download)
                Logger.info(
                    "[{}] sync request from master, downloaded {} blocks ({} - {})"
                    .format(
                        req.branch.get_shard_id(),
                        len(block_chain),
                        block_chain[0].header.height,
                        block_chain[-1].header.height,
                    ))
                check(len(block_chain) == len(blocks_to_download))

                await self.slave_server.add_block_list_for_sync(block_chain)
                block_hash_list = block_hash_list[BLOCK_BATCH_SIZE:]

        except Exception as e:
            Logger.error_exception()
            return SyncMinorBlockListResponse(error_code=1)

        return SyncMinorBlockListResponse(error_code=0)
Example #6
0
 def get_random_nodes(self, count: int) -> Iterator[Node]:
     if count > len(self):
         if time.monotonic() - self._initialized_at > 30:
             Logger.warning(
                 "Cannot get %d nodes as RoutingTable contains only %d nodes",
                 count,
                 len(self),
             )
         count = len(self)
     seen = []
     # This is a rather inneficient way of randomizing nodes from all buckets, but even if we
     # iterate over all nodes in the routing table, the time it takes would still be
     # insignificant compared to the time it takes for the network roundtrips when connecting
     # to nodes.
     while len(seen) < count:
         bucket = random.choice(self.buckets)
         if not bucket.nodes:
             continue
         node = random.choice(bucket.nodes)
         if node not in seen:
             yield node
             seen.append(node)
Example #7
0
    async def submit_work(self, header_hash: bytes, nonce: int, mixhash: bytes) -> bool:
        if not self.remote:
            raise ValueError("Should only be used for remote miner")

        if header_hash not in self.work_map:
            return False
        block = self.work_map[header_hash]
        header = copy.copy(block.header)
        header.nonce, header.mixhash = nonce, mixhash
        try:
            validate_seal(header, self.consensus_type)
        except ValueError:
            return False

        block.header = header  # actual update
        try:
            await self.add_block_async_func(block)
            del self.work_map[header_hash]
            self.current_work = None
            return True
        except Exception as ex:
            Logger.error(ex)
            return False
Example #8
0
 async def handle_mined_block():
     while True:
         res = await self.output_q.coro_get()  # type: MiningResult
         if not res:
             return  # empty result means ending
         # start mining before processing and propagating mined block
         self._mine_new_block_async()
         block = self.work_map[res.header_hash]
         block.header.nonce = res.nonce
         block.header.mixhash = res.mixhash
         del self.work_map[res.header_hash]
         self._track(block)
         try:
             # FIXME: Root block should include latest minor block headers while it's being mined
             # This is a hack to get the latest minor block included since testnet does not check difficulty
             if self.consensus_type == ConsensusType.POW_SIMULATE:
                 block = await self.create_block_async_func()
                 block.header.nonce = random.randint(0, 2**32 - 1)
                 self._track(block)
                 self._log_status(block)
             await self.add_block_async_func(block)
         except Exception as ex:
             Logger.error(ex)
Example #9
0
    def __init__(self, env, diff_calc=None):
        self.env = env
        if not diff_calc:
            cutoff = env.quark_chain_config.ROOT.DIFFICULTY_ADJUSTMENT_CUTOFF_TIME
            diff_factor = env.quark_chain_config.ROOT.DIFFICULTY_ADJUSTMENT_FACTOR
            min_diff = env.quark_chain_config.ROOT.GENESIS.DIFFICULTY
            check(cutoff > 0 and diff_factor > 0 and min_diff > 0)
            diff_calc = EthDifficultyCalculator(cutoff=cutoff,
                                                diff_factor=diff_factor,
                                                minimum_diff=min_diff)
        self.diff_calc = diff_calc
        self.raw_db = env.db
        self.db = RootDb(self.raw_db,
                         env.quark_chain_config.ROOT.max_root_blocks_in_memory)

        persisted_tip = self.db.get_tip_header()
        if persisted_tip:
            self.tip = persisted_tip
            Logger.info("Recovered root state with tip height {}".format(
                self.tip.height))
        else:
            self.__create_genesis_block()
            Logger.info("Created genesis root block")
Example #10
0
    def __init__(self, env, diff_calc=None):
        self.env = env
        self.diff_calc = (
            diff_calc
            if diff_calc
            else EthDifficultyCalculator(
                cutoff=45, diff_factor=2048, minimum_diff=1000000
            )
        )
        self.raw_db = env.db
        self.db = RootDb(
            self.raw_db, env.quark_chain_config.ROOT.max_root_blocks_in_memory
        )

        persisted_tip = self.db.get_tip_header()
        if persisted_tip:
            self.tip = persisted_tip
            Logger.info(
                "Recovered root state with tip height {}".format(self.tip.height)
            )
        else:
            self.__create_genesis_block()
            Logger.info("Created genesis root block")
Example #11
0
    def __init__(
        self,
        consensus_type: ConsensusType,
        create_block_async_func: Callable[..., Awaitable[Optional[Block]]],
        add_block_async_func: Callable[[Block], Awaitable[None]],
        get_mining_param_func: Callable[[], Dict[str, Any]],
        get_header_tip_func: Callable[[], Header],
        remote: bool = False,
        guardian_private_key: Optional[KeyAPI.PrivateKey] = None,
    ):
        """Mining will happen on a subprocess managed by this class

        create_block_async_func: takes no argument, returns a block (either RootBlock or MinorBlock)
        add_block_async_func: takes a block, add it to chain
        get_mining_param_func: takes no argument, returns the mining-specific params
        """
        self.consensus_type = consensus_type

        self.create_block_async_func = create_block_async_func
        self.add_block_async_func = add_block_async_func
        self.get_mining_param_func = get_mining_param_func
        self.get_header_tip_func = get_header_tip_func
        self.enabled = False
        self.process = None

        self.input_q = AioQueue()  # [(MiningWork, param dict)]
        self.output_q = AioQueue()  # [MiningResult]

        # header hash -> work
        self.work_map = {}  # type: Dict[bytes, Block]

        if not remote and consensus_type != ConsensusType.POW_SIMULATE:
            Logger.warning("Mining locally, could be slow and error-prone")
        # remote miner specific attributes
        self.remote = remote
        self.current_work = None  # type: Optional[Block]
        self.guardian_private_key = guardian_private_key
Example #12
0
    async def __gen(self, num_tx, x_shard_percent, sample_tx: Transaction):
        Logger.info(
            "[{}] start generating {} transactions with {}% cross-shard".
            format(self.shard_id, num_tx, x_shard_percent))
        if num_tx <= 0:
            return
        start_time = time.time()
        tx_list = []
        total = 0
        sample_evm_tx = sample_tx.code.get_evm_transaction()
        while True:
            for account in self.accounts:
                nonce = self.shard.state.get_transaction_count(
                    account.address.recipient)
                tx = self.create_transaction(account, nonce, x_shard_percent,
                                             sample_evm_tx)
                if not tx:
                    continue
                tx_list.append(tx)
                total += 1
                if len(tx_list) >= 600 or total >= num_tx:
                    self.shard.add_tx_list(tx_list)
                    tx_list = []
                    await asyncio.sleep(
                        random.uniform(8, 12)
                    )  # yield CPU so that other stuff won't be held for too long

                if total >= num_tx:
                    break
            if total >= num_tx:
                break

        end_time = time.time()
        Logger.info("[{}] generated {} transactions in {:.2f} seconds".format(
            self.shard_id, total, end_time - start_time))
        self.running = False
Example #13
0
    async def submit_work(self, header_hash: bytes, nonce: int,
                          mixhash: bytes) -> bool:
        if not self.remote:
            raise ValueError("Should only be used for remote miner")

        if header_hash not in self.work_map:
            return False
        # this copy is necessary since there might be multiple submissions concurrently
        block = copy.deepcopy(self.work_map[header_hash])
        header = block.header
        header.nonce, header.mixhash = nonce, mixhash

        # lower the difficulty for root block signed by guardian
        if self.guardian_private_key and isinstance(block, RootBlock):
            diff = Guardian.adjust_difficulty(header.difficulty, header.height)
            try:
                validate_seal(header, self.consensus_type, adjusted_diff=diff)
            except ValueError:
                return False
            # sign as a guardian
            header.sign_with_private_key(self.guardian_private_key)
        else:  # minor block, or doesn't have guardian private key
            try:
                validate_seal(header, self.consensus_type)
            except ValueError:
                return False
        try:
            await self.add_block_async_func(block)
            # a previous submission of the same work could have removed the key
            if header_hash in self.work_map:
                del self.work_map[header_hash]
                self.current_work = None
            return True
        except Exception:
            Logger.error_exception()
            return False
Example #14
0
 def __validate_block_headers(self,
                              block_header_list: List[MinorBlockHeader]):
     for i in range(len(block_header_list) - 1):
         header, prev = block_header_list[i:i + 2]  # type: MinorBlockHeader
         if header.height != prev.height + 1:
             return False
         if header.hash_prev_minor_block != prev.get_hash():
             return False
         try:
             # Note that PoSW may lower diff, so checks here are necessary but not sufficient
             # More checks happen during block addition
             shard_config = self.shard.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 sync: {}".format(
                     header.branch.to_str(), str(e)))
             return False
     return True
Example #15
0
    def get_connection_to_forward(self, metadata):
        """ Override ProxyConnection.get_connection_to_forward()
        """
        if metadata.cluster_peer_id == 0:
            # RPC from master
            return None

        shard = self.shards.get(metadata.branch, None)
        if not shard:
            self.close_with_error("incorrect forwarding branch")
            return

        peer_shard_conn = shard.peers.get(metadata.cluster_peer_id, None)
        if peer_shard_conn is None:
            # Master can close the peer connection at any time
            # TODO: any way to avoid this race?
            Logger.warning_every_sec(
                "cannot find peer shard conn for cluster id {}".format(
                    metadata.cluster_peer_id),
                1,
            )
            return NULL_CONNECTION

        return peer_shard_conn.get_forwarding_connection()
Example #16
0
    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()

        # Start mining new one before propagating inside cluster
        # The propagation should be done by the time the new block is mined
        self.miner.mine_new_block_async()
        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
Example #17
0
    async def start(self) -> str:
        """ Override Peer.start()
        exchange hello command, establish cluster connections in master;
        executed during subprotocol handshake
        """
        if self.cluster_peer_id == RESERVED_CLUSTER_PEER_ID:
            return self.close_with_error(
                "Remote is using reserved cluster peer id which is prohibited"
            )
        self.send_hello()

        op, cmd, rpc_id = await self.read_command()
        if op is None:
            assert self.state == ConnectionState.CLOSED
            Logger.info("Failed to read command, peer may have closed connection")
            return "Failed to read command"

        if op != CommandOp.HELLO:
            return self.close_with_error("Hello must be the first command")

        if cmd.version != self.env.quark_chain_config.P2P_PROTOCOL_VERSION:
            return self.close_with_error("incompatible protocol version")

        if cmd.network_id != self.env.quark_chain_config.NETWORK_ID:
            return self.close_with_error("incompatible network id")

        if (
            cmd.genesis_root_block_hash
            != self.master_server.root_state.get_genesis_block_hash()
        ):
            return self.close_with_error("genesis block mismatch")

        self.id = cmd.peer_id
        self.chain_mask_list = cmd.chain_mask_list
        # ip is from peer.remote, there may be 2 cases:
        #  1. dialed-out: ip is from discovery service;
        #  2. dialed-in: ip is from writer.get_extra_info("peername")
        self.ip = ipaddress.ip_address(self.quark_peer.remote.address.ip)
        # port is what peer claim to be using
        self.port = cmd.peer_port

        Logger.info(
            "Got HELLO from peer {} ({}:{})".format(self.quark_peer, self.ip, self.port)
        )

        self.best_root_block_header_observed = cmd.root_block_header

        await self.master_server.create_peer_cluster_connections(self.cluster_peer_id)
        Logger.info(
            "Established virtual shard connections with {} cluster_peer_id={} id={}".format(
                self.quark_peer, self.cluster_peer_id, self.id.hex()
            )
        )
Example #18
0
    async def start(self) -> str:
        """ Override Peer.start()
        exchange hello command, establish cluster connections in master
        """
        self.send_hello()

        op, cmd, rpc_id = await self.read_command()
        if op is None:
            assert self.state == ConnectionState.CLOSED
            Logger.info("Failed to read command, peer may have closed connection")
            return "Failed to read command"

        if op != CommandOp.HELLO:
            return self.close_with_error("Hello must be the first command")

        if cmd.version != self.env.quark_chain_config.P2P_PROTOCOL_VERSION:
            return self.close_with_error("incompatible protocol version")

        if cmd.network_id != self.env.quark_chain_config.NETWORK_ID:
            return self.close_with_error("incompatible network id")

        self.id = cmd.peer_id
        self.shard_mask_list = cmd.shard_mask_list
        # ip is from peer.remote, there may be 2 cases:
        #  1. dialed-out: ip is from discovery service;
        #  2. dialed-in: ip is from writer.get_extra_info("peername")
        self.ip = ipaddress.ip_address(self.quark_peer.remote.address.ip)
        # port is what peer claim to be using
        self.port = cmd.peer_port

        Logger.info(
            "Got HELLO from peer {} ({}:{})".format(self.quark_peer, self.ip, self.port)
        )

        if (
            cmd.root_block_header.shard_info.get_shard_size()
            != self.env.quark_chain_config.SHARD_SIZE
        ):
            return self.close_with_error(
                "Shard size from root block header does not match local"
            )

        self.best_root_block_header_observed = cmd.root_block_header

        await self.master_server.create_peer_cluster_connections(self.cluster_peer_id)
        Logger.info(
            "Established virtual shard connections with peer {}".format(self.id.hex())
        )
Example #19
0
 async def refresh_connections(self, peers):
     Logger.info("Refreshing connections to {} peers: {}".format(
         len(peers), peers))
     # 1. disconnect peers that are not in devp2p peer list
     to_be_disconnected = []
     for peer_id, peer in self.active_peer_pool.items():
         ip_port = "{}:{}".format(peer.ip, peer.port)
         if ip_port not in peers:
             to_be_disconnected.append(peer)
     if len(to_be_disconnected) > 0:
         Logger.info(
             "Disconnecting peers not in devp2p discovery: {}".format([
                 "{}:{}".format(peer.ip, peer.port)
                 for peer in to_be_disconnected
             ]))
     for peer in to_be_disconnected:
         peer.close_dead_peer()
     # 2. connect to peers that are in devp2p peer list
     # only initiate connections from smaller of ip_port,
     # to avoid peers trying to connect each other at the same time
     active = [
         "{}:{}".format(p.ip, p.port)
         for i, p in self.active_peer_pool.items()
     ]
     to_be_connected = set(peers) - set(active)
     if len(to_be_connected) > 0:
         Logger.info("Connecting to peers from devp2p discovery: {}".format(
             to_be_connected))
     self_ip_port = "{}:{}".format(self.ip, self.port)
     for ip_port in to_be_connected:
         if self_ip_port < ip_port:
             ip, port = ip_port.split(":")
             asyncio.ensure_future(self.connect(ip, port))
         else:
             Logger.info(
                 "skipping {} to prevent concurrent peer initialization".
                 format(ip_port))
Example #20
0
    async def start(self, is_server=False):
        """
        race condition may arise when two peers connecting each other at the same time
        to resolve: 1. acquire asyncio lock (what if the corotine holding the lock failed?)
        2. disconnect whenever duplicates are detected, right after await (what if both connections are disconnected?)
        3. only initiate connection from one side, eg. from smaller of ip_port; in SimpleNetwork, from new nodes only
        3 is the way to go
        """
        op, cmd, rpc_id = await self.read_command()
        if op is None:
            Logger.info(
                "Failed to read command, peer may have closed connection")
            return super().close_with_error("Failed to read command")

        if op != CommandOp.HELLO:
            return self.close_with_error("Hello must be the first command")

        if cmd.version != self.env.quark_chain_config.P2P_PROTOCOL_VERSION:
            return self.close_with_error("incompatible protocol version")

        if cmd.network_id != self.env.quark_chain_config.NETWORK_ID:
            return self.close_with_error("incompatible network id")

        if cmd.genesis_root_block_hash != self.root_state.get_genesis_block_hash(
        ):
            return self.close_with_error("genesis block mismatch")

        self.id = cmd.peer_id
        self.chain_mask_list = cmd.chain_mask_list
        self.ip = ipaddress.ip_address(cmd.peer_ip)
        self.port = cmd.peer_port

        Logger.info("Got HELLO from peer {} ({}:{})".format(
            self.id.hex(), self.ip, self.port))

        self.best_root_block_header_observed = cmd.root_block_header

        if self.id == self.network.self_id:
            # connect to itself, stop it
            return self.close_with_error("Cannot connect to itself")

        if self.id in self.network.active_peer_pool:
            return self.close_with_error("Peer {} already connected".format(
                self.id.hex()))

        # Send hello back
        if is_server:
            self.send_hello()

        await self.master_server.create_peer_cluster_connections(
            self.cluster_peer_id)
        Logger.info(
            "Established virtual shard connections with peer {}".format(
                self.id.hex()))

        asyncio.ensure_future(self.active_and_loop_forever())
        await self.wait_until_active()

        # Only make the peer connection avaialbe after exchanging HELLO and creating virtual shard connections
        self.network.active_peer_pool[self.id] = self
        self.network.cluster_peer_pool[self.cluster_peer_id] = self
        Logger.info("Peer {} added to active peer pool".format(self.id.hex()))

        self.master_server.handle_new_root_block_header(
            self.best_root_block_header_observed, self)
        return None
Example #21
0
 def close_with_error(self, error):
     Logger.info(
         "Closing peer %s with the following reason: %s" %
         (self.id.hex() if self.id is not None else "unknown", error))
     return super().close_with_error(error)
Example #22
0
 def stop_mining(self):
     self.mining = False
     for branch, shard in self.shards.items():
         Logger.info("[{}] stop mining".format(branch.get_shard_id()))
         shard.miner.disable()
Example #23
0
 def close_with_error(self, error):
     Logger.info("Closing connection with slave {}".format(self.id))
     return super().close_with_error(error)
Example #24
0
 def close_with_error(self, error):
     Logger.info("Closing connection with master: {}".format(error))
     return super().close_with_error(error)
Example #25
0
 async def handle_new_transaction_list(self, op, cmd, rpc_id):
     for tx in cmd.transaction_list:
         Logger.debug("Received tx {} from peer {}".format(
             tx.get_hash().hex(), self.id.hex()))
         await self.master_server.add_transaction(tx, self)
Example #26
0
    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
Example #27
0
    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
Example #28
0
 def close_with_error(self, error):
     Logger.error("Closing shard connection with error {}".format(error))
     return super().close_with_error(error)
Example #29
0
    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)
Example #30
0
 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))