diff --git a/CHANGELOG.md b/CHANGELOG.md index caf651981894..0e06dcaf5a2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ ### Added +- [#3167](https://github.com/ChainSafe/forest/pull/3167): Added a new option + `--validate-tipsets` for `forest-cli snapshot validate`. - [#3166](https://github.com/ChainSafe/forest/issues/3166): Add `forest-cli archive info` command for inspecting archives. - [#3159](https://github.com/ChainSafe/forest/issues/3159): Add diff --git a/scripts/tests/forest_cli_check.sh b/scripts/tests/forest_cli_check.sh index 688f665503e6..4909160f76c4 100755 --- a/scripts/tests/forest_cli_check.sh +++ b/scripts/tests/forest_cli_check.sh @@ -49,7 +49,7 @@ pushd "$(mktemp --directory)" validate_me=$(find . -type f | head -1) : : validating under calibnet chain should succeed - "$FOREST_CLI_PATH" --chain calibnet snapshot validate "$validate_me" + "$FOREST_CLI_PATH" --chain calibnet snapshot validate "$validate_me" --validate-tipsets=-1000 : : validating under mainnet chain should fail if "$FOREST_CLI_PATH" --chain mainnet snapshot validate "$validate_me"; then diff --git a/src/blocks/tipset.rs b/src/blocks/tipset.rs index 99541c3a211f..71dbd47f7d73 100644 --- a/src/blocks/tipset.rs +++ b/src/blocks/tipset.rs @@ -282,6 +282,15 @@ impl Tipset { } broken } + /// Returns an iterator of all tipsets + pub fn chain(self, store: impl Blockstore) -> impl Iterator { + itertools::unfold(Some(self), move |tipset| { + tipset.take().map(|child| { + *tipset = Tipset::load(&store, child.parents()).ok().flatten(); + child + }) + }) + } } /// `FullTipset` is an expanded version of a tipset that contains all the blocks diff --git a/src/cli/subcommands/snapshot_cmd.rs b/src/cli/subcommands/snapshot_cmd.rs index f6e5faae32b8..ab23e28fd261 100644 --- a/src/cli/subcommands/snapshot_cmd.rs +++ b/src/cli/subcommands/snapshot_cmd.rs @@ -9,14 +9,16 @@ use crate::car_backed_blockstore::{ use crate::chain::ChainStore; use crate::cli::subcommands::{cli_error_and_die, handle_rpc_err}; use crate::cli_shared::snapshot::{self, TrustedVendor}; +use crate::fil_cns::composition as cns; use crate::genesis::read_genesis_header; use crate::ipld::{recurse_links_hash, CidHashSet}; use crate::networks::NetworkChain; use crate::rpc_api::{chain_api::ChainExportParams, progress_api::GetProgressType}; use crate::rpc_client::{chain_ops::*, progress_ops::get_progress}; use crate::shim::clock::ChainEpoch; -use crate::utils::io::ProgressBar; -use anyhow::bail; +use crate::state_manager::StateManager; +use crate::utils::{io::ProgressBar, proofs_api::paramfetch::ensure_params_downloaded}; +use anyhow::{bail, Context}; use chrono::Utc; use clap::Subcommand; use fvm_ipld_blockstore::Blockstore; @@ -56,6 +58,10 @@ pub enum SnapshotCommands { /// Number of block headers to validate from the tip #[arg(long, default_value = "2000")] recent_stateroots: i64, + /// Validate already computed tipsets at given EPOCH, + /// use a negative value -N to validate the last N EPOCH(s) starting at HEAD. + #[arg(long)] + validate_tipsets: Option, /// Path to a snapshot CAR, which may be zstd compressed snapshot: PathBuf, }, @@ -157,6 +163,7 @@ impl SnapshotCommands { } Self::Validate { recent_stateroots, + validate_tipsets, snapshot, } => { // this is all blocking... @@ -168,6 +175,7 @@ impl SnapshotCommands { store.roots(), Arc::new(store), recent_stateroots, + *validate_tipsets, ) .await } @@ -187,6 +195,7 @@ impl SnapshotCommands { store.roots(), Arc::new(store), recent_stateroots, + *validate_tipsets, ) .await } @@ -240,9 +249,10 @@ async fn validate_with_blockstore( roots: Vec, store: Arc, recent_stateroots: &i64, -) -> Result<(), anyhow::Error> + validate_tipsets: Option, +) -> anyhow::Result<()> where - BlockstoreT: Blockstore + Send + Sync, + BlockstoreT: Blockstore + Send + Sync + 'static, { let genesis = read_genesis_header( config.client.genesis_file.as_ref(), @@ -250,19 +260,19 @@ where &store, ) .await?; - + let chain_data_root = TempDir::new()?; let chain_store = Arc::new(ChainStore::new( - store, + Arc::clone(&store), config.chain.clone(), &genesis, - TempDir::new()?.path(), + chain_data_root.path(), )?); - let ts = chain_store.tipset_from_keys(&TipsetKeys::new(roots))?; + let ts = Tipset::load(&store, &TipsetKeys::new(roots))?.context("missing root tipset")?; validate_links_and_genesis_traversal( &chain_store, - ts, + &ts, chain_store.blockstore(), *recent_stateroots, &Tipset::from(genesis), @@ -270,12 +280,40 @@ where ) .await?; + if let Some(validate_from) = validate_tipsets { + let last_epoch = match validate_from.is_negative() { + true => ts.epoch() + validate_from, + false => validate_from, + }; + // Set proof parameter data dir + if cns::FETCH_PARAMS { + crate::utils::proofs_api::paramfetch::set_proofs_parameter_cache_dir_env( + &config.client.data_dir, + ); + } + // Initialize StateManager + let state_manager = Arc::new(StateManager::new( + chain_store, + Arc::clone(&config.chain), + Arc::new(crate::interpreter::RewardActorMessageCalc), + )?); + ensure_params_downloaded().await?; + // Prepare tipset stream to validate + let tipsets = ts + .chain(&store) + .map(|ts| Arc::clone(&Arc::new(ts))) + .take_while(|tipset| tipset.epoch() >= last_epoch); + + state_manager.validate_tipsets(tipsets)? + } + + println!("Snapshot is valid"); Ok(()) } async fn validate_links_and_genesis_traversal( chain_store: &ChainStore, - ts: Arc, + ts: &Tipset, db: &DB, recent_stateroots: ChainEpoch, genesis_tipset: &Tipset, @@ -340,7 +378,5 @@ where drop(pb); - println!("Snapshot is valid"); - Ok(()) } diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index a46d96846200..24cb982cf932 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -432,8 +432,10 @@ pub(super) async fn start( assert!(current_height.is_positive()); match validate_from.is_negative() { // allow --height=-1000 to scroll back from the current head - true => state_manager.validate((current_height + validate_from)..=current_height)?, - false => state_manager.validate(validate_from..=current_height)?, + true => { + state_manager.validate_range((current_height + validate_from)..=current_height)? + } + false => state_manager.validate_range(validate_from..=current_height)?, } } diff --git a/src/state_manager/mod.rs b/src/state_manager/mod.rs index 0eeeedb8ffe8..c5a8f84969da 100644 --- a/src/state_manager/mod.rs +++ b/src/state_manager/mod.rs @@ -1250,9 +1250,7 @@ where /// This function is blocking, but we do observe threads waiting and synchronizing. /// This is suspected to be due something in the VM or its `WASM` runtime. #[tracing::instrument(skip(self))] - pub fn validate(self: &Arc, epochs: RangeInclusive) -> anyhow::Result<()> { - use rayon::iter::ParallelIterator as _; - + pub fn validate_range(self: &Arc, epochs: RangeInclusive) -> anyhow::Result<()> { let heaviest = self.cs.heaviest_tipset(); let heaviest_epoch = heaviest.epoch(); let end = self @@ -1269,10 +1267,18 @@ where // if this has parents, unfold them in the next iteration *tipset = self.cs.tipset_from_keys(child.parents()).ok(); Some(child) - }); + }) + .take_while(|tipset| tipset.epoch() >= *epochs.start()); + self.validate_tipsets(tipsets) + } + + pub fn validate_tipsets(self: &Arc, tipsets: T) -> anyhow::Result<()> + where + T: Iterator> + Send, + { + use rayon::iter::ParallelIterator as _; tipsets - .take_while(|tipset| tipset.epoch() >= *epochs.start()) .tuple_windows() .par_bridge() .try_for_each(|(child, parent)| {