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")
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)
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
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)
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)
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
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)
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")
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")
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
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
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
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
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()
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
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() ) )
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()) )
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))
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
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)
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()
def close_with_error(self, error): Logger.info("Closing connection with slave {}".format(self.id)) return super().close_with_error(error)
def close_with_error(self, error): Logger.info("Closing connection with master: {}".format(error)) return super().close_with_error(error)
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)
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
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 close_with_error(self, error): Logger.error("Closing shard connection with error {}".format(error)) return super().close_with_error(error)
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 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))