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..67b1304fe 100644 --- a/fendermint/vm/topdown/src/finality/null.rs +++ b/fendermint/vm/topdown/src/finality/null.rs @@ -172,15 +172,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 +185,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 +207,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() diff --git a/fendermint/vm/topdown/src/sync/mod.rs b/fendermint/vm/topdown/src/sync/mod.rs index 71c98b658..f9b3f9b32 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; @@ -178,7 +177,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 db73f7fd9..14180e1d7 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::{ @@ -23,9 +22,6 @@ pub(crate) struct LotusParentSyncer { provider: Arc>>, vote_tally: VoteTally, query: Arc, - - /// The pointers that indicate which height to poll parent next - sync_pointers: SyncPointers, } impl LotusParentSyncer @@ -33,72 +29,47 @@ 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), }) } - /// 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 } else { return Ok(()); }; + let (latest_height_fetched, first_non_null_parent_hash) = self.get_from_cache().await; + tracing::debug!(chain_head, latest_height_fetched, "syncing heights"); - tracing::debug!( - chain_head, - pointers = self.sync_pointers.to_string(), - "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 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(()); } @@ -107,8 +78,17 @@ where return Ok(()); } - self.poll_next().await?; - + match self + .poll_next(latest_height_fetched + 1, first_non_null_parent_hash) + .await + { + Ok(_) => {} + Err(Error::ParentChainReorgDetected) => { + tracing::warn!("potential reorg detected, clear cache and retry"); + self.reset().await?; + } + Err(e) => return Err(anyhow!(e)), + } Ok(()) } } @@ -123,11 +103,54 @@ where atomically(|| self.provider.cached_blocks()).await > max_cache_blocks } - /// Poll the next block height. Returns finalized and executed block data. - async fn poll_next(&mut self) -> Result<(), Error> { - let height = self.sync_pointers.head() + 1; - let parent_block_hash = self.non_null_parent_hash().await; + /// Get the data needed to pull the next block + async fn get_from_cache(&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 last committed finality, 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<(), Error> { tracing::debug!( height, parent_block_hash = hex::encode(&parent_block_hash), @@ -139,9 +162,19 @@ 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(); + 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?; return Ok(()); } @@ -159,48 +192,16 @@ 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?; - - atomically_or_err::<_, Error, _>(|| { - // 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()))?; - - 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"); - - Ok(()) - }) - .await?; - } else { - tracing::debug!(height, "non-null round at height, waiting for confirmation"); - }; - - self.sync_pointers - .set_tail(height, block_hash_res.block_hash); - self.sync_pointers.advance_head(); + let data = self.fetch_data(height, block_hash_res.block_hash).await?; + atomically_or_err::<_, Error, _>(|| { + 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(()) } @@ -245,47 +246,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 @@ -301,7 +261,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(()) @@ -321,6 +281,252 @@ fn map_voting_err(e: StmError) -> StmError { } } +// #[cfg(test)] +// mod tests { +// use crate::proxy::ParentQueryProxy; +// use crate::sync::syncer::LotusParentSyncer; +// use crate::sync::ParentFinalityStateQuery; +// use crate::voting::VoteTally; +// use crate::{ +// BlockHash, BlockHeight, CachedFinalityProvider, Config, IPCParentFinality, +// SequentialKeyCache, Toggle, NULL_ROUND_ERR_MSG, +// }; +// use anyhow::anyhow; +// use async_stm::atomically; +// use async_trait::async_trait; +// use fendermint_vm_genesis::{Power, Validator}; +// use ipc_api::cross::IpcEnvelope; +// use ipc_api::staking::StakingChangeRequest; +// use ipc_provider::manager::{GetBlockHashResult, TopDownQueryPayload}; +// use std::sync::Arc; +// +// struct TestParentFinalityStateQuery { +// latest_finality: IPCParentFinality, +// } +// +// impl ParentFinalityStateQuery for TestParentFinalityStateQuery { +// fn get_latest_committed_finality(&self) -> anyhow::Result> { +// Ok(Some(self.latest_finality.clone())) +// } +// fn get_power_table(&self) -> anyhow::Result>>> { +// Ok(Some(vec![])) +// } +// } +// +// struct TestParentProxy { +// blocks: SequentialKeyCache>, +// } +// +// #[async_trait] +// impl ParentQueryProxy for TestParentProxy { +// async fn get_chain_head_height(&self) -> anyhow::Result { +// Ok(self.blocks.upper_bound().unwrap()) +// } +// +// async fn get_genesis_epoch(&self) -> anyhow::Result { +// Ok(self.blocks.lower_bound().unwrap() - 1) +// } +// +// async fn get_block_hash(&self, height: BlockHeight) -> anyhow::Result { +// let r = self.blocks.get_value(height).unwrap(); +// if r.is_none() { +// return Err(anyhow!(NULL_ROUND_ERR_MSG)); +// } +// +// for h in (self.blocks.lower_bound().unwrap()..height).rev() { +// let v = self.blocks.get_value(h).unwrap(); +// if v.is_none() { +// continue; +// } +// return Ok(GetBlockHashResult { +// parent_block_hash: v.clone().unwrap(), +// block_hash: r.clone().unwrap(), +// }); +// } +// panic!("invalid testing data") +// } +// +// async fn get_top_down_msgs( +// &self, +// height: BlockHeight, +// ) -> anyhow::Result>> { +// Ok(TopDownQueryPayload { +// value: vec![], +// block_hash: self.blocks.get_value(height).cloned().unwrap().unwrap(), +// }) +// } +// +// async fn get_validator_changes( +// &self, +// height: BlockHeight, +// ) -> anyhow::Result>> { +// Ok(TopDownQueryPayload { +// value: vec![], +// block_hash: self.blocks.get_value(height).cloned().unwrap().unwrap(), +// }) +// } +// } +// +// async fn new_syncer( +// blocks: SequentialKeyCache>, +// ) -> LotusParentSyncer { +// let config = Config { +// chain_head_delay: 2, +// polling_interval: Default::default(), +// exponential_back_off: Default::default(), +// exponential_retry_limit: 0, +// max_proposal_range: Some(1), +// max_cache_blocks: None, +// proposal_delay: None, +// }; +// let genesis_epoch = blocks.lower_bound().unwrap(); +// let proxy = Arc::new(TestParentProxy { blocks }); +// let committed_finality = IPCParentFinality { +// height: genesis_epoch, +// 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![], +// ( +// committed_finality.height, +// committed_finality.block_hash.clone(), +// ), +// ); +// +// LotusParentSyncer::new( +// config, +// proxy, +// Arc::new(Toggle::enabled(provider)), +// vote_tally, +// Arc::new(TestParentFinalityStateQuery { +// latest_finality: committed_finality, +// }), +// ) +// .unwrap() +// } +// +// /// Creates a mock of a new parent blockchain view. The key is the height and the value is the +// /// block hash. If block hash is None, it means the current height is a null block. +// macro_rules! new_parent_blocks { +// ($($key:expr => $val:expr),* ,) => ( +// hash_map!($($key => $val),*) +// ); +// ($($key:expr => $val:expr),*) => ({ +// let mut map = SequentialKeyCache::sequential(); +// $( map.append($key, $val).unwrap(); )* +// map +// }); +// } +// +// #[tokio::test] +// async fn happy_path() { +// let parent_blocks = new_parent_blocks!( +// 100 => Some(vec![0; 32]), // genesis block +// 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 +// ); +// +// let mut syncer = new_syncer(parent_blocks).await; +// +// assert_eq!(syncer.sync_pointers.head(), 100); +// assert_eq!(syncer.sync_pointers.tail(), None); +// +// // sync block 101, which is a non-null block +// let r = syncer.sync().await; +// assert!(r.is_ok()); +// 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 +// let r = syncer.sync().await; +// assert!(r.is_ok()); +// 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) +// ); +// } +// +// #[tokio::test] +// async fn with_non_null_block() { +// 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).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 { +// let r = syncer.sync().await; +// assert!(r.is_ok()); +// 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 { +// let r = syncer.sync().await; +// assert!(r.is_ok()); +// assert_eq!(syncer.sync_pointers.head(), h); +// assert_eq!(syncer.sync_pointers.tail(), Some((104, vec![4; 32]))); +// } +// +// // 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) +// ); +// } +// } + #[cfg(test)] mod tests { use crate::proxy::ParentQueryProxy; @@ -335,9 +541,9 @@ mod tests { use async_stm::atomically; use async_trait::async_trait; use fendermint_vm_genesis::{Power, Validator}; - use ipc_api::cross::IpcEnvelope; - use ipc_api::staking::StakingChangeRequest; use ipc_provider::manager::{GetBlockHashResult, TopDownQueryPayload}; + use ipc_sdk::cross::CrossMsg; + use ipc_sdk::staking::StakingChangeRequest; use std::sync::Arc; struct TestParentFinalityStateQuery { @@ -389,7 +595,7 @@ mod tests { async fn get_top_down_msgs( &self, height: BlockHeight, - ) -> anyhow::Result>> { + ) -> anyhow::Result>> { Ok(TopDownQueryPayload { value: vec![], block_hash: self.blocks.get_value(height).cloned().unwrap().unwrap(), @@ -426,13 +632,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![], ( @@ -441,6 +640,12 @@ mod tests { ), ); + let provider = CachedFinalityProvider::new( + config.clone(), + genesis_epoch, + Some(committed_finality.clone()), + proxy.clone(), + ); LotusParentSyncer::new( config, proxy, @@ -450,7 +655,6 @@ mod tests { latest_finality: committed_finality, }), ) - .await .unwrap() } @@ -474,36 +678,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).await; - assert_eq!(syncer.sync_pointers.head(), 100); - assert_eq!(syncer.sync_pointers.tail(), None); - - // sync block 101, which is a non-null block - let r = syncer.sync().await; - assert!(r.is_ok()); - 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 - let r = syncer.sync().await; - assert!(r.is_ok()); - 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] @@ -525,45 +711,13 @@ mod tests { let mut syncer = new_syncer(parent_blocks).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 { - let r = syncer.sync().await; - assert!(r.is_ok()); - 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 { - let r = syncer.sync().await; - assert!(r.is_ok()); - assert_eq!(syncer.sync_pointers.head(), h); - assert_eq!(syncer.sync_pointers.tail(), Some((104, vec![4; 32]))); + for h in 101..=109 { + syncer.sync().await.unwrap(); + 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) - ); } } 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) + } }