From d05cffa527f40733d0cc4b655a5a55086510ef1c Mon Sep 17 00:00:00 2001 From: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com> Date: Fri, 1 Mar 2024 09:34:19 +0800 Subject: [PATCH] Topdown lost commits (#757) Co-authored-by: Akosh Farkash --- fendermint/vm/topdown/src/finality/fetch.rs | 4 + fendermint/vm/topdown/src/finality/null.rs | 121 +++--- fendermint/vm/topdown/src/sync/mod.rs | 2 - fendermint/vm/topdown/src/sync/pointers.rs | 50 --- fendermint/vm/topdown/src/sync/syncer.rs | 401 +++++++------------- fendermint/vm/topdown/src/toggle.rs | 4 + 6 files changed, 222 insertions(+), 360 deletions(-) delete mode 100644 fendermint/vm/topdown/src/sync/pointers.rs diff --git a/fendermint/vm/topdown/src/finality/fetch.rs b/fendermint/vm/topdown/src/finality/fetch.rs index 6c7ffee65..fb4203045 100644 --- a/fendermint/vm/topdown/src/finality/fetch.rs +++ b/fendermint/vm/topdown/src/finality/fetch.rs @@ -233,6 +233,10 @@ impl CachedFinalityProvider { pub fn cached_blocks(&self) -> Stm { self.inner.cached_blocks() } + + pub fn first_non_null_block(&self, height: BlockHeight) -> Stm> { + self.inner.first_non_null_block(height) + } } #[cfg(test)] diff --git a/fendermint/vm/topdown/src/finality/null.rs b/fendermint/vm/topdown/src/finality/null.rs index 7609774c6..c0eec6fe7 100644 --- a/fendermint/vm/topdown/src/finality/null.rs +++ b/fendermint/vm/topdown/src/finality/null.rs @@ -73,15 +73,8 @@ impl FinalityWithNull { maybe_payload: Option, ) -> StmResult<(), Error> { if let Some((block_hash, validator_changes, top_down_msgs)) = maybe_payload { - emit!( - EventType::NewParentView, - is_null = false, - height, - block_hash = hex::encode(&block_hash) - ); self.parent_block_filled(height, block_hash, validator_changes, top_down_msgs) } else { - emit!(EventType::NewParentView, is_null = true, height); self.parent_null_round(height) } } @@ -172,15 +165,12 @@ impl FinalityWithNull { }; Ok(Some(h)) } -} -/// All the private functions -impl FinalityWithNull { - /// Get the first non-null block in the range [start, end]. - fn first_non_null_block_before(&self, height: BlockHeight) -> Stm> { + /// Get the first non-null block in the range of earliest cache block till the height specified, inclusive. + pub(crate) fn first_non_null_block(&self, height: BlockHeight) -> Stm> { let cache = self.cached_data.read()?; Ok(cache.lower_bound().and_then(|lower_bound| { - for h in (lower_bound..height).rev() { + for h in (lower_bound..=height).rev() { if let Some(Some(_)) = cache.get_value(h) { return Some(h); } @@ -188,7 +178,10 @@ impl FinalityWithNull { None })) } +} +/// All the private functions +impl FinalityWithNull { fn propose_next_height(&self) -> Stm> { let latest_height = if let Some(h) = self.latest_height_in_cache()? { h @@ -207,18 +200,17 @@ impl FinalityWithNull { let candidate_height = min(max_proposal_height, latest_height); tracing::debug!(max_proposal_height, candidate_height, "propose heights"); - let first_non_null_height = - if let Some(h) = self.first_non_null_block_before(candidate_height)? { - h - } else { - tracing::debug!(height = candidate_height, "no non-null block found before"); - return Ok(None); - }; + let first_non_null_height = if let Some(h) = self.first_non_null_block(candidate_height)? { + h + } else { + tracing::debug!(height = candidate_height, "no non-null block found before"); + return Ok(None); + }; tracing::debug!(first_non_null_height, candidate_height); // an extra layer of delay let maybe_proposal_height = - self.first_non_null_block_before(first_non_null_height - self.config.proposal_delay())?; + self.first_non_null_block(first_non_null_height - self.config.proposal_delay())?; tracing::debug!( delayed_height = maybe_proposal_height, delay = self.config.proposal_delay() @@ -407,19 +399,18 @@ mod tests { let parent_blocks = vec![ (100, Some((vec![0; 32], vec![], vec![]))), // last committed block (101, Some((vec![1; 32], vec![], vec![]))), // cache start - (102, Some((vec![2; 32], vec![], vec![]))), // final proposal height - (103, Some((vec![3; 32], vec![], vec![]))), // final delayed height - (104, Some((vec![4; 32], vec![], vec![]))), - (105, Some((vec![5; 32], vec![], vec![]))), // first non null block - (106, Some((vec![6; 32], vec![], vec![]))), // max proposal height (last committed + 6) - (107, Some((vec![7; 32], vec![], vec![]))), - (108, Some((vec![8; 32], vec![], vec![]))), // cache latest height + (102, Some((vec![2; 32], vec![], vec![]))), + (103, Some((vec![3; 32], vec![], vec![]))), + (104, Some((vec![4; 32], vec![], vec![]))), // final delayed height + proposal height + (105, Some((vec![5; 32], vec![], vec![]))), + (106, Some((vec![6; 32], vec![], vec![]))), // max proposal height (last committed + 6), first non null block + (107, Some((vec![7; 32], vec![], vec![]))), // cache latest height ]; let provider = new_provider(parent_blocks).await; let f = IPCParentFinality { - height: 102, - block_hash: vec![2; 32], + height: 104, + block_hash: vec![4; 32], }; assert_eq!( atomically(|| provider.next_proposal()).await, @@ -439,7 +430,7 @@ mod tests { ); // this ensures sequential insertion is still valid - atomically_or_err(|| provider.new_parent_view(109, None)) + atomically_or_err(|| provider.new_parent_view(108, None)) .await .unwrap(); } @@ -449,11 +440,11 @@ mod tests { // max_proposal_range is 6. proposal_delay is 2 let parent_blocks = vec![ (100, Some((vec![0; 32], vec![], vec![]))), // last committed block - (101, Some((vec![1; 32], vec![], vec![]))), // cache start and final height - (102, Some((vec![2; 32], vec![], vec![]))), // delayed height - (103, Some((vec![3; 32], vec![], vec![]))), - (104, Some((vec![4; 32], vec![], vec![]))), // first non null block - (105, Some((vec![4; 32], vec![], vec![]))), // cache latest height + (101, Some((vec![1; 32], vec![], vec![]))), + (102, Some((vec![2; 32], vec![], vec![]))), + (103, Some((vec![3; 32], vec![], vec![]))), // delayed height + final height + (104, Some((vec![4; 32], vec![], vec![]))), + (105, Some((vec![4; 32], vec![], vec![]))), // cache latest height, first non null block // max proposal height is 106 ]; let provider = new_provider(parent_blocks).await; @@ -461,8 +452,8 @@ mod tests { assert_eq!( atomically(|| provider.next_proposal()).await, Some(IPCParentFinality { - height: 101, - block_hash: vec![1; 32] + height: 103, + block_hash: vec![3; 32] }) ); } @@ -497,14 +488,14 @@ mod tests { (104, None), // we wont have a proposal because after delay, there is no more non-null proposal (105, None), (106, None), - (107, Some((vec![7; 32], vec![], vec![]))), // first non null block - (108, None), - (109, None), - (110, Some((vec![10; 32], vec![], vec![]))), // cache latest height + (107, None), + (108, None), // delayed block + (109, Some((vec![8; 32], vec![], vec![]))), + (110, Some((vec![10; 32], vec![], vec![]))), // cache latest height, first non null block // max proposal height is 112 ]; let mut provider = new_provider(parent_blocks).await; - provider.config.max_proposal_range = Some(8); + provider.config.max_proposal_range = Some(10); assert_eq!(atomically(|| provider.next_proposal()).await, None); } @@ -514,24 +505,52 @@ mod tests { // max_proposal_range is 10. proposal_delay is 2 let parent_blocks = vec![ (102, Some((vec![2; 32], vec![], vec![]))), // last committed block - (103, Some((vec![3; 32], vec![], vec![]))), // first non null delayed block, final + (103, Some((vec![3; 32], vec![], vec![]))), (104, None), - (105, None), // delayed block + (105, None), (106, None), - (107, Some((vec![7; 32], vec![], vec![]))), // first non null block - (108, None), + (107, Some((vec![7; 32], vec![], vec![]))), // first non null after delay + (108, None), // delayed block (109, None), - (110, Some((vec![10; 32], vec![], vec![]))), // cache latest height + (110, Some((vec![10; 32], vec![], vec![]))), // cache latest height, first non null block // max proposal height is 112 ]; let mut provider = new_provider(parent_blocks).await; - provider.config.max_proposal_range = Some(8); + provider.config.max_proposal_range = Some(10); assert_eq!( atomically(|| provider.next_proposal()).await, Some(IPCParentFinality { - height: 103, - block_hash: vec![3; 32] + height: 107, + block_hash: vec![7; 32] + }) + ); + } + + #[tokio::test] + async fn test_with_partially_null_blocks_iii() { + let parent_blocks = vec![ + (102, Some((vec![2; 32], vec![], vec![]))), // last committed block + (103, Some((vec![3; 32], vec![], vec![]))), + (104, None), + (105, None), + (106, None), + (107, Some((vec![7; 32], vec![], vec![]))), // first non null delayed block, final + (108, None), // delayed block + (109, None), + (110, Some((vec![10; 32], vec![], vec![]))), // first non null block + (111, None), + (112, None), + // max proposal height is 122 + ]; + let mut provider = new_provider(parent_blocks).await; + provider.config.max_proposal_range = Some(20); + + assert_eq!( + atomically(|| provider.next_proposal()).await, + Some(IPCParentFinality { + height: 107, + block_hash: vec![7; 32] }) ); } diff --git a/fendermint/vm/topdown/src/sync/mod.rs b/fendermint/vm/topdown/src/sync/mod.rs index b07b819a1..2ac1128d2 100644 --- a/fendermint/vm/topdown/src/sync/mod.rs +++ b/fendermint/vm/topdown/src/sync/mod.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0, MIT //! A constant running process that fetch or listener to parent state -mod pointers; mod syncer; mod tendermint; @@ -179,7 +178,6 @@ fn start_syncing( tokio::spawn(async move { let lotus_syncer = LotusParentSyncer::new(config, parent_proxy, view_provider, vote_tally, query) - .await .expect(""); let mut tendermint_syncer = TendermintAwareSyncer::new(lotus_syncer, tendermint_client); diff --git a/fendermint/vm/topdown/src/sync/pointers.rs b/fendermint/vm/topdown/src/sync/pointers.rs deleted file mode 100644 index cace87d39..000000000 --- a/fendermint/vm/topdown/src/sync/pointers.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2022-2024 Protocol Labs -// SPDX-License-Identifier: Apache-2.0, MIT - -use crate::{BlockHash, BlockHeight}; -use ethers::utils::hex; -use std::fmt::{Display, Formatter}; - -#[derive(Clone, Debug)] -pub(crate) struct SyncPointers { - tail: Option<(BlockHeight, BlockHash)>, - head: BlockHeight, -} - -impl SyncPointers { - pub fn new(head: BlockHeight) -> Self { - Self { tail: None, head } - } - - pub fn head(&self) -> BlockHeight { - self.head - } - - pub fn tail(&self) -> Option<(BlockHeight, BlockHash)> { - self.tail.clone() - } - - pub fn advance_head(&mut self) { - self.head += 1; - } - - pub fn set_tail(&mut self, height: BlockHeight, hash: BlockHash) { - self.tail = Some((height, hash)); - } -} - -impl Display for SyncPointers { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if let Some((height, hash)) = &self.tail { - write!( - f, - "{{tail: {{height: {}, hash: {}}}, head: {}}}", - height, - hex::encode(hash), - self.head - ) - } else { - write!(f, "{{tail: None, head: {}}}", self.head) - } - } -} diff --git a/fendermint/vm/topdown/src/sync/syncer.rs b/fendermint/vm/topdown/src/sync/syncer.rs index e66449f78..2b0ea8b3e 100644 --- a/fendermint/vm/topdown/src/sync/syncer.rs +++ b/fendermint/vm/topdown/src/sync/syncer.rs @@ -4,7 +4,6 @@ use crate::finality::ParentViewPayload; use crate::proxy::ParentQueryProxy; -use crate::sync::pointers::SyncPointers; use crate::sync::{query_starting_finality, ParentFinalityStateQuery}; use crate::voting::{self, VoteTally}; use crate::{ @@ -13,6 +12,7 @@ use crate::{ use anyhow::anyhow; use async_stm::{atomically, atomically_or_err, StmError}; use ethers::utils::hex; +use fendermint_vm_event::{emit, EventType}; use std::sync::Arc; /// Parent syncer that constantly poll parent. This struct handles lotus null blocks and deferred @@ -24,9 +24,6 @@ pub(crate) struct LotusParentSyncer { vote_tally: VoteTally, query: Arc, - /// The pointers that indicate which height to poll parent next - sync_pointers: SyncPointers, - /// For testing purposes, we can sync one block at a time. /// Not part of `Config` as it's a very niche setting; /// if enabled it would slow down catching up with parent @@ -41,51 +38,24 @@ where T: ParentFinalityStateQuery + Send + Sync + 'static, P: ParentQueryProxy + Send + Sync + 'static, { - pub async fn new( + pub fn new( config: Config, parent_proxy: Arc

, provider: Arc>>, vote_tally: VoteTally, query: Arc, ) -> anyhow::Result { - let last_committed_finality = atomically(|| provider.last_committed_finality()) - .await - .ok_or_else(|| anyhow!("parent finality not ready"))?; - Ok(Self { config, parent_proxy, provider, vote_tally, query, - sync_pointers: SyncPointers::new(last_committed_finality.height), sync_many: true, }) } - /// Sync as many blocks as we can. - /// - /// There are 2 pointers, each refers to a block height, when syncing with parent. As Lotus has - /// delayed execution and null round, we need to ensure the topdown messages and validator - /// changes polled are indeed finalized and executed. The following three pointers are introduced: - /// - tail: The next block height in cache to be confirmed executed, could be None - /// - head: The latest block height fetched in cache, finalized but may not be executed. - /// - /// Say we have block chain as follows: - /// NonNullBlock(1) -> NonNullBlock(2) -> NullBlock(3) -> NonNullBlock(4) -> NullBlock(5) -> NonNullBlock(6) - /// and block height 1 is the previously finalized and executed block height. - /// - /// At the beginning, head == 1 and tail == None. With a new block height fetched, - /// `head = 2`. Since height at 2 is not a null block, `tail = Some(2)`, because we cannot be sure - /// block 2 has executed yet. When a new block is fetched, `head = 3`. Since head is a null block, we - /// cannot confirm block height 2. When `head = 4`, it's not a null block, we can confirm block 2 is - /// executed (also with some checks to ensure no reorg has occurred). We fetch block 2's data and set - /// `tail = Some(4)`. - /// The data fetch at block height 2 is pushed to cache and height 2 is ready to be proposed. - /// - /// At height 6, it's block height 4 will be confirmed and its data pushed to cache. At the same - /// time, since block 3 is a null block, empty data will also be pushed to cache. Block 4 is ready - /// to be proposed. + /// Insert the height into cache when we see a new non null block pub async fn sync(&mut self) -> anyhow::Result<()> { let chain_head = if let Some(h) = self.finalized_chain_head().await? { h @@ -93,23 +63,25 @@ where return Ok(()); }; - tracing::debug!( - chain_head, - pointers = self.sync_pointers.to_string(), - "syncing heights" - ); + let (mut latest_height_fetched, mut first_non_null_parent_hash) = + self.latest_cached_data().await; + tracing::debug!(chain_head, latest_height_fetched, "syncing heights"); - if self.detected_reorg_by_height(chain_head) { + if latest_height_fetched > chain_head { tracing::warn!( - pointers = self.sync_pointers.to_string(), chain_head, - "reorg detected from height" + latest_height_fetched, + "chain head went backwards, potential reorg detected from height" ); - return self.reset_cache().await; + return self.reset().await; } - if !self.has_new_blocks(chain_head) { - tracing::debug!("the parent has yet to produce a new block"); + if latest_height_fetched == chain_head { + tracing::debug!( + chain_head, + latest_height_fetched, + "the parent has yet to produce a new block" + ); return Ok(()); } @@ -119,9 +91,22 @@ where break; } - let synced_height = self.poll_next().await?; + first_non_null_parent_hash = match self + .poll_next(latest_height_fetched + 1, first_non_null_parent_hash) + .await + { + Ok(h) => h, + Err(Error::ParentChainReorgDetected) => { + tracing::warn!("potential reorg detected, clear cache and retry"); + self.reset().await?; + break; + } + Err(e) => return Err(anyhow!(e)), + }; + + latest_height_fetched += 1; - if synced_height == chain_head { + if latest_height_fetched == chain_head { tracing::debug!("reached the tip of the chain"); break; } else if !self.sync_many { @@ -143,16 +128,54 @@ where atomically(|| self.provider.cached_blocks()).await > max_cache_blocks } - /// Poll the next block height. Adds the finalized and executed block data to the caches. - /// - /// Returns the height which was polled. - /// - /// This method is only expected to be called after the caller has checked that the chain - /// is supposed to have data at the next height. - async fn poll_next(&mut self) -> Result { - let height = self.sync_pointers.head() + 1; - let parent_block_hash = self.non_null_parent_hash().await; + /// Get the latest data stored in the cache to pull the next block + async fn latest_cached_data(&self) -> (BlockHeight, BlockHash) { + // we are getting the latest height fetched in cache along with the first non null block + // that is stored in cache. + // we are doing two fetches in one `atomically` as if we get the data in two `atomically`, + // the cache might be updated in between the two calls. `atomically` should guarantee atomicity. + atomically(|| { + let latest_height = if let Some(h) = self.provider.latest_height()? { + h + } else { + unreachable!("guaranteed to have latest height, report bug please") + }; + + // first try to get the first non null block before latest_height + 1, i.e. from cache + let prev_non_null_height = + if let Some(height) = self.provider.first_non_null_block(latest_height)? { + tracing::debug!(height, "first non null block in cache"); + height + } else if let Some(p) = self.provider.last_committed_finality()? { + tracing::debug!( + height = p.height, + "first non null block not in cache, use latest finality" + ); + p.height + } else { + unreachable!("guaranteed to have last committed finality, report bug please") + }; + + let hash = if let Some(h) = self.provider.block_hash(prev_non_null_height)? { + h + } else { + unreachable!( + "guaranteed to have hash as the height {} is found", + prev_non_null_height + ) + }; + Ok((latest_height, hash)) + }) + .await + } + + /// Poll the next block height. Returns finalized and executed block data. + async fn poll_next( + &mut self, + height: BlockHeight, + parent_block_hash: BlockHash, + ) -> Result { tracing::debug!( height, parent_block_hash = hex::encode(&parent_block_hash), @@ -164,11 +187,25 @@ where Err(e) => { let err = e.to_string(); if is_null_round_str(&err) { - tracing::debug!(height, "detected null round at height"); - - self.sync_pointers.advance_head(); - - return Ok(height); + tracing::debug!( + height, + "detected null round at height, inserted None to cache" + ); + + atomically_or_err::<_, Error, _>(|| { + self.provider.new_parent_view(height, None)?; + self.vote_tally + .add_block(height, None) + .map_err(map_voting_err)?; + Ok(()) + }) + .await?; + + emit!(EventType::NewParentView, is_null = true, height); + + // Null block received, no block hash for the current height being polled. + // Return the previous parent hash as the non-null block hash. + return Ok(parent_block_hash); } return Err(Error::CannotQueryParent( format!("get_block_hash: {e}"), @@ -187,60 +224,37 @@ where return Err(Error::ParentChainReorgDetected); } - if let Some((to_confirm_height, to_confirm_hash)) = self.sync_pointers.tail() { - tracing::debug!( - height, - confirm = to_confirm_height, - "non-null round at height, confirmed previous height" - ); - - let data = self.fetch_data(to_confirm_height, to_confirm_hash).await?; - - tracing::debug!( - height, - staking_requests = data.1.len(), - cross_messages = data.2.len(), - "fetched data" - ); - - atomically_or_err::<_, Error, _>(|| { - // This is here so we see if there is abnormal amount of retries for some reason. - tracing::debug!(height, "adding data to the cache"); - - // we only push the null block in cache when we confirmed a block so that in cache - // the latest height is always a confirmed non null block. - let latest_height = self - .provider - .latest_height()? - .expect("provider contains data at this point"); - - for h in (latest_height + 1)..to_confirm_height { - self.provider.new_parent_view(h, None)?; - self.vote_tally.add_block(h, None).map_err(map_voting_err)?; - tracing::debug!(height = h, "found null block pushed to cache"); - } - - self.provider - .new_parent_view(to_confirm_height, Some(data.clone()))?; + let data = self.fetch_data(height, block_hash_res.block_hash).await?; - self.vote_tally - .add_block(to_confirm_height, Some(data.0.clone())) - .map_err(map_voting_err)?; - - tracing::debug!(height = to_confirm_height, "non-null block pushed to cache"); + tracing::debug!( + height, + staking_requests = data.1.len(), + cross_messages = data.2.len(), + "fetched data" + ); - Ok(()) - }) - .await?; - } else { - tracing::debug!(height, "non-null round at height, waiting for confirmation"); - }; + atomically_or_err::<_, Error, _>(|| { + // This is here so we see if there is abnormal amount of retries for some reason. + tracing::debug!(height, "adding data to the cache"); - self.sync_pointers - .set_tail(height, block_hash_res.block_hash); - self.sync_pointers.advance_head(); + self.provider.new_parent_view(height, Some(data.clone()))?; + self.vote_tally + .add_block(height, Some(data.0.clone())) + .map_err(map_voting_err)?; + tracing::debug!(height, "non-null block pushed to cache"); + Ok(()) + }) + .await?; - Ok(height) + emit!( + EventType::NewParentView, + is_null = false, + height, + block_hash = hex::encode(&data.0), + num_topdown_messages = data.2.len(), + num_validator_changes = data.1.len(), + ); + Ok(data.0) } async fn fetch_data( @@ -283,47 +297,6 @@ where Ok((block_hash, changes_res.value, topdown_msgs_res.value)) } - /// We only want the non-null parent block's hash - async fn non_null_parent_hash(&self) -> BlockHash { - if let Some((height, hash)) = self.sync_pointers.tail() { - tracing::debug!( - pending_height = height, - "previous non null parent is the pending confirmation block" - ); - return hash; - }; - - atomically(|| { - Ok(if let Some(h) = self.provider.latest_height_in_cache()? { - tracing::debug!( - previous_confirmed_height = h, - "found previous non null block in cache" - ); - // safe to unwrap as we have height recorded - self.provider.block_hash(h)?.unwrap() - } else if let Some(p) = self.provider.last_committed_finality()? { - tracing::debug!( - previous_confirmed_height = p.height, - "no cache, found previous non null block as last committed finality" - ); - p.block_hash - } else { - unreachable!("guaranteed to non null block hash, report bug please") - }) - }) - .await - } - - fn has_new_blocks(&self, height: BlockHeight) -> bool { - self.sync_pointers.head() < height - } - - fn detected_reorg_by_height(&self, height: BlockHeight) -> bool { - // If the below is true, we are going backwards in terms of block height, the latest block - // height is lower than our previously fetched head. It could be a chain reorg. - self.sync_pointers.head() > height - } - async fn finalized_chain_head(&self) -> anyhow::Result> { let parent_chain_head_height = self.parent_proxy.get_chain_head_height().await?; // sanity check @@ -339,7 +312,7 @@ where } /// Reset the cache in the face of a reorg - async fn reset_cache(&self) -> anyhow::Result<()> { + async fn reset(&self) -> anyhow::Result<()> { let finality = query_starting_finality(&self.query, &self.parent_proxy).await?; atomically(|| self.provider.reset(finality.clone())).await; Ok(()) @@ -468,13 +441,6 @@ mod tests { block_hash: vec![0; 32], }; - let provider = CachedFinalityProvider::new( - config.clone(), - genesis_epoch, - Some(committed_finality.clone()), - proxy.clone(), - ); - let vote_tally = VoteTally::new( vec![], ( @@ -483,6 +449,12 @@ mod tests { ), ); + let provider = CachedFinalityProvider::new( + config.clone(), + genesis_epoch, + Some(committed_finality.clone()), + proxy.clone(), + ); let mut syncer = LotusParentSyncer::new( config, proxy, @@ -492,7 +464,6 @@ mod tests { latest_finality: committed_finality, }), ) - .await .unwrap(); // Some tests expect to sync one block at a time. @@ -521,34 +492,18 @@ mod tests { 101 => Some(vec![1; 32]), 102 => Some(vec![2; 32]), 103 => Some(vec![3; 32]), - 104 => Some(vec![4; 32]), - 105 => Some(vec![5; 32]) // chain head + 104 => Some(vec![4; 32]), // after chain head delay, we fetch only to here + 105 => Some(vec![5; 32]), + 106 => Some(vec![6; 32]) // chain head ); let mut syncer = new_syncer(parent_blocks, false).await; - assert_eq!(syncer.sync_pointers.head(), 100); - assert_eq!(syncer.sync_pointers.tail(), None); - - // sync block 101, which is a non-null block - syncer.sync().await.unwrap(); - assert_eq!(syncer.sync_pointers.head(), 101); - assert_eq!(syncer.sync_pointers.tail(), Some((101, vec![1; 32]))); - // latest height is None as we are yet to confirm block 101, so latest height should equal - // to the last committed finality initialized, which is the genesis block 100 - assert_eq!( - atomically(|| syncer.provider.latest_height()).await, - Some(100) - ); - - // sync block 101, which is a non-null block - syncer.sync().await.unwrap(); - assert_eq!(syncer.sync_pointers.head(), 102); - assert_eq!(syncer.sync_pointers.tail(), Some((102, vec![2; 32]))); - assert_eq!( - atomically(|| syncer.provider.latest_height()).await, - Some(101) - ); + for h in 101..=104 { + syncer.sync().await.unwrap(); + let p = atomically(|| syncer.provider.latest_height()).await; + assert_eq!(p, Some(h)); + } } #[tokio::test] @@ -570,80 +525,12 @@ mod tests { let mut syncer = new_syncer(parent_blocks, false).await; - assert_eq!(syncer.sync_pointers.head(), 100); - assert_eq!(syncer.sync_pointers.tail(), None); - - // sync block 101 to 103, which are null blocks - for h in 101..=103 { - syncer.sync().await.unwrap(); - assert_eq!(syncer.sync_pointers.head(), h); - assert_eq!(syncer.sync_pointers.tail(), None); - } - - // sync block 104, which is a non-null block - syncer.sync().await.unwrap(); - assert_eq!(syncer.sync_pointers.head(), 104); - assert_eq!(syncer.sync_pointers.tail(), Some((104, vec![4; 32]))); - // latest height is None as we are yet to confirm block 104, so latest height should equal - // to the last committed finality initialized, which is the genesis block 100 - assert_eq!( - atomically(|| syncer.provider.latest_height()).await, - Some(100) - ); - - // sync block 105 to 107, which are null blocks - for h in 105..=107 { + for h in 101..=109 { syncer.sync().await.unwrap(); - assert_eq!(syncer.sync_pointers.head(), h); - assert_eq!(syncer.sync_pointers.tail(), Some((104, vec![4; 32]))); + assert_eq!( + atomically(|| syncer.provider.latest_height()).await, + Some(h) + ); } - - // sync block 108, which is a non-null block - syncer.sync().await.unwrap(); - assert_eq!(syncer.sync_pointers.head(), 108); - assert_eq!(syncer.sync_pointers.tail(), Some((108, vec![5; 32]))); - // latest height is None as we are yet to confirm block 108, so latest height should equal - // to the previous confirmed block, which is 104 - assert_eq!( - atomically(|| syncer.provider.latest_height()).await, - Some(104) - ); - } - - #[tokio::test] - async fn with_non_null_block_many() { - let parent_blocks = new_parent_blocks!( - 100 => Some(vec![0; 32]), // genesis block - 101 => None, - 102 => None, - 103 => None, - 104 => Some(vec![4; 32]), - 105 => None, - 106 => None, - 107 => None, - 108 => Some(vec![5; 32]), - 109 => None, - 110 => None, - 111 => None - ); - - let mut syncer = new_syncer(parent_blocks, true).await; - - assert_eq!(syncer.sync_pointers.head(), 100); - assert_eq!(syncer.sync_pointers.tail(), None); - - // Sync all the way to the head of the chain. - syncer.sync().await.unwrap(); - - // The end height should be the finalized height: `top - delay``. - // NOTE: The syncer doesn't care that the height isn't buried in `delay` number of non-null blocks, null blocks finalize as well. - assert_eq!(syncer.sync_pointers.head(), 111 - FINALITY_DELAY); - assert_eq!(syncer.sync_pointers.tail(), Some((108, vec![5; 32]))); - - // latest height is None as we are yet to confirm block 108 by a non-null block. - assert_eq!( - atomically(|| syncer.provider.latest_height()).await, - Some(104) - ); } } diff --git a/fendermint/vm/topdown/src/toggle.rs b/fendermint/vm/topdown/src/toggle.rs index 195c13623..c7dd10065 100644 --- a/fendermint/vm/topdown/src/toggle.rs +++ b/fendermint/vm/topdown/src/toggle.rs @@ -123,4 +123,8 @@ impl

Toggle> { pub fn cached_blocks(&self) -> Stm { self.perform_or_else(|p| p.cached_blocks(), BlockHeight::MAX) } + + pub fn first_non_null_block(&self, height: BlockHeight) -> Stm> { + self.perform_or_else(|p| p.first_non_null_block(height), None) + } }