def create_default_shard_state(env, shard_id=0, diff_calc=None): genesis_manager = GenesisManager(env.quark_chain_config) shard_size = next(iter(env.quark_chain_config.shards.values())).SHARD_SIZE full_shard_id = shard_size | shard_id shard_state = ShardState(env=env, full_shard_id=full_shard_id, diff_calc=diff_calc) shard_state.init_genesis_state(genesis_manager.create_root_block()) return shard_state
def create_default_state(env, diff_calc=None): r_state = RootState(env=env, diff_calc=diff_calc) s_state_list = dict() for full_shard_id in env.quark_chain_config.get_full_shard_ids(): shard_state = ShardState(env=env, full_shard_id=full_shard_id, db=quarkchain.db.InMemoryDb()) mblock, coinbase_amount_map = shard_state.init_genesis_state( r_state.get_tip_block()) block_hash = mblock.header.get_hash() r_state.add_validated_minor_block_hash(block_hash, coinbase_amount_map.balance_map) s_state_list[full_shard_id] = shard_state # add a root block so that later minor blocks will be broadcasted to neighbor shards minor_header_list = [] for state in s_state_list.values(): minor_header_list.append(state.header_tip) root_block = r_state.create_block_to_mine(minor_header_list) assert r_state.add_block(root_block) for state in s_state_list.values(): assert state.add_root_block(root_block) return r_state, s_state_list
def test_add_minor_block_with_wrong_root_block_hash(self): """ Test for the following case +--+ |r1| /+--+ / | +--+ / +--+ +--+ |r0|<----|m1|<---|m3| +--+ \ +--+ +--+ ^ \ | | \+--+ | | |r2|<----+ | +--+ | | | +--+ +------|m2| +--+ where m3 is invalid because m3 depends on r2, whose minor chain is not the same chain as m3 """ env = get_test_env(shard_size=1) r_state, s_states = create_default_state(env) s_state0 = s_states[1 | 0] root_block0 = r_state.get_tip_block() m1 = s_state0.get_tip().create_block_to_append(nonce=0) m2 = s_state0.get_tip().create_block_to_append(nonce=1) add_minor_block_to_cluster(s_states, m1) add_minor_block_to_cluster(s_states, m2) r_state.add_validated_minor_block_hash(m1.header.get_hash()) r_state.add_validated_minor_block_hash(m2.header.get_hash()) root_block1 = (root_block0.create_block_to_append( nonce=0).add_minor_block_header(m1.header).finalize()) root_block2 = (root_block0.create_block_to_append( nonce=1).add_minor_block_header(m2.header).finalize()) self.assertTrue(r_state.add_block(root_block1)) self.assertFalse(r_state.add_block(root_block2)) self.assertTrue(s_state0.add_root_block(root_block1)) self.assertFalse(s_state0.add_root_block(root_block2)) m3 = m1.create_block_to_append() m3.header.hash_prev_root_block = root_block2.header.get_hash() with self.assertRaises(ValueError): add_minor_block_to_cluster(s_states, m3) m4 = m1.create_block_to_append() m4.header.hash_prev_root_block = root_block1.header.get_hash() add_minor_block_to_cluster(s_states, m4) # Test recovery s_state0_recovered = ShardState(env, full_shard_id=1 | 0, db=s_state0.raw_db) s_state0_recovered.init_from_root_block(root_block1) with self.assertRaises(ValueError): add_minor_block_to_cluster(s_states, m3)
def test_shard_state_recovery_from_root_block(self): id1 = Identity.create_random_identity() acc1 = Address.create_from_identity(id1, full_shard_id=0) env = get_test_env(genesis_account=acc1, genesis_minor_quarkash=10000000) state = create_default_shard_state(env=env, shard_id=0) blockHeaders = [] blockMetas = [] for i in range(12): b = state.get_tip().create_block_to_append(address=acc1) state.finalize_and_add_block(b) blockHeaders.append(b.header) blockMetas.append(b.meta) # add a fork b1 = state.db.get_minor_block_by_height(3) b1.header.create_time += 1 state.finalize_and_add_block(b1) self.assertEqual( state.db.get_minor_block_by_hash(b1.header.get_hash()), b1) root_block = state.root_tip.create_block_to_append() root_block.minor_block_header_list = blockHeaders[:5] root_block.finalize() state.add_root_block(root_block) recoveredState = ShardState(env=env, shard_id=0) recoveredState.init_from_root_block(root_block) # forks are pruned self.assertIsNone( recoveredState.db.get_minor_block_by_hash(b1.header.get_hash())) self.assertEqual( recoveredState.db.get_minor_block_by_hash(b1.header.get_hash(), consistency_check=False), b1, ) self.assertEqual(recoveredState.root_tip, root_block.header) self.assertEqual(recoveredState.header_tip, blockHeaders[4]) self.assertEqual(recoveredState.confirmed_header_tip, blockHeaders[4]) self.assertEqual(recoveredState.meta_tip, blockMetas[4]) self.assertEqual(recoveredState.evm_state.trie.root_hash, blockMetas[4].hash_evm_state_root)
def create_default_shard_state(env, shard_id=0, diff_calc=None, posw_override=False, no_coinbase=False): genesis_manager = GenesisManager(env.quark_chain_config) shard_size = next(iter(env.quark_chain_config.shards.values())).SHARD_SIZE full_shard_id = shard_size | shard_id if posw_override: posw_config = env.quark_chain_config.shards[full_shard_id].POSW_CONFIG posw_config.ENABLED = True if no_coinbase: env.quark_chain_config.shards[full_shard_id].COINBASE_AMOUNT = 0 shard_state = ShardState(env=env, full_shard_id=full_shard_id, diff_calc=diff_calc) shard_state.init_genesis_state(genesis_manager.create_root_block()) return shard_state
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 create_default_state(env, diff_calc=None): r_state = RootState(env=env, diff_calc=diff_calc) s_state_list = dict() for full_shard_id in env.quark_chain_config.get_full_shard_ids(): shard_state = ShardState(env=env, full_shard_id=full_shard_id, db=quarkchain.db.InMemoryDb()) shard_state.init_genesis_state(r_state.get_tip_block()) s_state_list[full_shard_id] = shard_state for state in s_state_list.values(): block_hash = state.header_tip.get_hash() for dst_state in s_state_list.values(): if state == dst_state: continue dst_state.add_cross_shard_tx_list_by_minor_block_hash( block_hash, CrossShardTransactionList(tx_list=[])) r_state.add_validated_minor_block_hash(block_hash) return (r_state, s_state_list)
def create_default_state(env, diff_calc=None): r_state = RootState(env=env, diff_calc=diff_calc) s_state_list = [] for shard_id in range(env.quark_chain_config.SHARD_SIZE): shard_state = ShardState(env=env, shard_id=shard_id, db=quarkchain.db.InMemoryDb()) shard_state.init_genesis_state(r_state.get_tip_block()) s_state_list.append(shard_state) for state in s_state_list: block_hash = state.header_tip.get_hash() for dst_state in s_state_list: if state == dst_state: continue dst_state.add_cross_shard_tx_list_by_minor_block_hash( block_hash, CrossShardTransactionList(tx_list=[])) r_state.add_validated_minor_block_hash(block_hash) return (r_state, s_state_list)
def create_default_state(env, diff_calc=None): r_state = RootState(env=env, diff_calc=diff_calc) s_state_list = dict() for full_shard_id in env.quark_chain_config.get_full_shard_ids(): shard_state = ShardState(env=env, full_shard_id=full_shard_id, db=quarkchain.db.InMemoryDb()) shard_state.init_genesis_state(r_state.get_tip_block()) s_state_list[full_shard_id] = shard_state # add a root block so that later minor blocks will be broadcasted to neighbor shards root_block = r_state.tip.create_block_to_append() for state in s_state_list.values(): root_block.add_minor_block_header(state.header_tip) block_hash = state.header_tip.get_hash() r_state.add_validated_minor_block_hash(block_hash) root_block.finalize() assert r_state.add_block(root_block) for state in s_state_list.values(): assert state.add_root_block(root_block) return (r_state, s_state_list)
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)
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)
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)
def create_default_shard_state(env, shard_id=0, diff_calc=None): genesis_manager = GenesisManager(env.quark_chain_config) shard_state = ShardState(env=env, shard_id=shard_id, diff_calc=diff_calc) shard_state.init_genesis_state(genesis_manager.create_root_block()) return shard_state