- 
                Notifications
    You must be signed in to change notification settings 
- Fork 113
implement two-phase commitment of adding minor blocks (#487) #493
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -38,6 +38,12 @@ | |
| from quarkchain.p2p.utils import RESERVED_CLUSTER_PEER_ID | ||
|  | ||
|  | ||
| class BlockCommitStatus: | ||
| UNCOMMITTED = 0 # The other slaves and the master may not have the block info | ||
| COMMITING = 1 # The block info is propagating to other slaves and the master | ||
| COMMITTED = 2 # The other slaves and the master have received the block info | ||
|  | ||
|  | ||
| class PeerShardConnection(VirtualConnection): | ||
| """ A virtual connection between local shard and remote shard | ||
| """ | ||
|  | @@ -621,43 +627,62 @@ async def handle_new_block(self, block): | |
| 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 neighor 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 BlockCommitStatus.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, None) | ||
| if future is not None: | ||
| return BlockCommitStatus.COMMITTING, future | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oopsy, can you spot the bug here? @qizhou | ||
|  | ||
| return BlockCommitStatus.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 == BlockCommitStatus.COMMITTED: | ||
| return True | ||
| elif commit_status == BlockCommitStatus.COMMITING: | ||
| 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 == BlockCommitStatus.UNCOMMITTED) | ||
| # Validate and add the block | ||
| old_tip = self.state.header_tip | ||
| try: | ||
| xshard_list, coinbase_amount_map = self.state.add_block(block) | ||
| 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_pool | ||
| self.state.new_block_pool.pop(block.header.get_hash(), None) | ||
| self.state.new_block_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) | ||
|  | ||
| # 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() | ||
| # 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 | ||
|  | @@ -671,8 +696,13 @@ async def add_block(self, block): | |
| 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()] | ||
| # 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 | ||
|  | ||
| async def add_block_list_for_sync(self, block_list): | ||
|  | @@ -690,13 +720,39 @@ async def add_block_list_for_sync(self, block_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() | ||
| commit_status, future = self.__get_block_commit_status_by_hash(block_hash) | ||
| if commit_status == BlockCommitStatus.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 == BlockCommitStatus.COMMITING: | ||
| # 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 == BlockCommitStatus.UNCOMMITTED) | ||
| # Validate and add the block | ||
| try: | ||
| xshard_list, coinbase_amount_map = self.state.add_block( | ||
| block, skip_if_too_old=False | ||
| block, | ||
| skip_if_too_old=False, | ||
| force=True, | ||
| ) | ||
| # coinbase_amount_map may be None if the block exists | ||
| # adding the block header one since the block is already validated. | ||
|  | @@ -705,28 +761,33 @@ async def add_block_list_for_sync(self, block_list): | |
| 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_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() | ||
| 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( | ||
|         
                  ninjaahhh marked this conversation as resolved.
              Show resolved
            Hide resolved | ||
| 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())) | ||
|  | ||
| 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] | ||
|  | ||
| # Wait for the other add block operations | ||
| await asyncio.gather(*existing_add_block_futures) | ||
|  | ||
| return True, coinbase_amount_list | ||
|  | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -816,11 +816,16 @@ def __remove_transactions_from_block(self, block): | |
| self.tx_queue = self.tx_queue.diff(evm_tx_list) | ||
|  | ||
| def add_block( | ||
| self, block, skip_if_too_old=True, gas_limit=None, xshard_gas_limit=None | ||
| self, | ||
| block, | ||
| skip_if_too_old=True, | ||
| gas_limit=None, | ||
| xshard_gas_limit=None, | ||
| force=False, | ||
| ): | ||
| """ Add a block to local db. Perform validate and update tip accordingly | ||
| gas_limit and xshard_gas_limit are used for testing only. | ||
| Returns None if block is already added. | ||
| Returns None if block is already added (if force is False). | ||
| Returns a list of CrossShardTransactionDeposit from block. | ||
| Additionally, returns a map of reward token balances for this block | ||
| Raises on any error. | ||
|  | @@ -847,7 +852,7 @@ def add_block( | |
| ) | ||
|  | ||
| block_hash = block.header.get_hash() | ||
| if self.db.contain_minor_block_by_hash(block_hash): | ||
| if not force and self.db.contain_minor_block_by_hash(block_hash): | ||
| return None, None | ||
|  | ||
| evm_tx_included = [] | ||
|  | @@ -1749,3 +1754,9 @@ def _get_posw_coinbase_blockcnt( | |
| length = self.shard_config.POSW_CONFIG.WINDOW_SIZE | ||
| coinbase_addrs = self.__get_coinbase_addresses_until_block(header_hash, length) | ||
| return Counter(coinbase_addrs) | ||
|  | ||
| def is_committed_by_hash(self, h): | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: prefer letting the caller directly access  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As an abstraction, the client of state_shard should not access db directly - i.e., we should probably rename db to __db to enforce that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok. however currently  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is bad, and we should fix that.... | ||
| return self.db.is_minor_block_committed_by_hash(h) | ||
|  | ||
| def commit_by_hash(self, h): | ||
| self.db.commit_minor_block_by_hash(h) | ||
Uh oh!
There was an error while loading. Please reload this page.