forest/tool/subcommands/
snapshot_cmd.rs

1// Copyright 2019-2025 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use super::*;
5use crate::blocks::Tipset;
6use crate::chain::index::{ChainIndex, ResolveNullTipset};
7use crate::cli_shared::snapshot;
8use crate::daemon::bundle::load_actor_bundles;
9use crate::db::car::forest::DEFAULT_FOREST_CAR_FRAME_SIZE;
10use crate::db::car::{AnyCar, ManyCar};
11use crate::db::{MemoryDB, PersistentStore};
12use crate::interpreter::{MessageCallbackCtx, VMTrace};
13use crate::ipld::stream_chain;
14use crate::networks::{ChainConfig, NetworkChain, butterflynet, calibnet, mainnet};
15use crate::shim::address::CurrentNetwork;
16use crate::shim::clock::ChainEpoch;
17use crate::shim::fvm_shared_latest::address::Network;
18use crate::shim::machine::GLOBAL_MULTI_ENGINE;
19use crate::state_manager::{StateOutput, apply_block_messages};
20use crate::utils::db::car_stream::CarStream;
21use crate::utils::proofs_api::ensure_proof_params_downloaded;
22use anyhow::{Context as _, bail};
23use cid::Cid;
24use clap::Subcommand;
25use dialoguer::{Confirm, theme::ColorfulTheme};
26use futures::TryStreamExt;
27use fvm_ipld_blockstore::Blockstore;
28use indicatif::{ProgressBar, ProgressStyle};
29use std::path::PathBuf;
30use std::sync::Arc;
31use tokio::fs::File;
32use tokio::io::AsyncWriteExt;
33
34#[derive(Debug, Subcommand)]
35pub enum SnapshotCommands {
36    /// Fetches the most recent snapshot from a trusted, pre-defined location.
37    Fetch {
38        #[arg(short, long, default_value = ".")]
39        directory: PathBuf,
40        /// Network chain the snapshot will belong to
41        #[arg(long, default_value_t = NetworkChain::Mainnet)]
42        chain: NetworkChain,
43        /// Vendor to fetch the snapshot from
44        #[arg(short, long, value_enum, default_value_t = snapshot::TrustedVendor::default())]
45        vendor: snapshot::TrustedVendor,
46    },
47
48    /// Validate the provided snapshots as a whole.
49    ValidateDiffs {
50        /// Number of recent epochs to scan for broken links
51        #[arg(long, default_value_t = 2000)]
52        check_links: u32,
53        /// Assert the snapshot belongs to this network. If left blank, the
54        /// network will be inferred before executing messages.
55        #[arg(long)]
56        check_network: Option<crate::networks::NetworkChain>,
57        /// Number of recent epochs to scan for bad messages/transactions
58        #[arg(long, default_value_t = 60)]
59        check_stateroots: u32,
60        /// Path to a snapshot CAR, which may be zstd compressed
61        #[arg(required = true)]
62        snapshot_files: Vec<PathBuf>,
63    },
64
65    /// Validate the snapshots individually.
66    Validate {
67        /// Number of recent epochs to scan for broken links
68        #[arg(long, default_value_t = 2000)]
69        check_links: u32,
70        /// Assert the snapshot belongs to this network. If left blank, the
71        /// network will be inferred before executing messages.
72        #[arg(long)]
73        check_network: Option<crate::networks::NetworkChain>,
74        /// Number of recent epochs to scan for bad messages/transactions
75        #[arg(long, default_value_t = 60)]
76        check_stateroots: u32,
77        /// Path to a snapshot CAR, which may be zstd compressed
78        #[arg(required = true)]
79        snapshot_files: Vec<PathBuf>,
80        /// Fail at the first invalid snapshot
81        #[arg(long)]
82        fail_fast: bool,
83    },
84
85    /// Make this snapshot suitable for use as a compressed car-backed blockstore.
86    Compress {
87        /// Input CAR file, in `.car`, `.car.zst`, or `.forest.car.zst` format.
88        source: PathBuf,
89        /// Output file, will be in `.forest.car.zst` format.
90        ///
91        /// Will reuse the source name (with new extension) if pointed to a
92        /// directory.
93        #[arg(short, long, default_value = ".")]
94        output_path: PathBuf,
95        #[arg(long, default_value_t = 3)]
96        compression_level: u16,
97        /// End zstd frames after they exceed this length
98        #[arg(long, default_value_t = DEFAULT_FOREST_CAR_FRAME_SIZE)]
99        frame_size: usize,
100        /// Overwrite output file without prompting.
101        #[arg(long, default_value_t = false)]
102        force: bool,
103    },
104    /// Filecoin keeps track of "the state of the world", including:
105    /// wallets and their balances;
106    /// storage providers and their deals;
107    /// etc...
108    ///
109    /// It does this by (essentially) hashing the state of the world.
110    ///
111    /// The world can change when new blocks are mined and transmitted.
112    /// A block may contain a message to e.g transfer FIL between two parties.
113    /// Blocks are ordered by "epoch", which can be thought of as a timestamp.
114    ///
115    /// Snapshots contain (among other things) these messages.
116    ///
117    /// The command calculates the state of the world at EPOCH-1, applies all
118    /// the messages at EPOCH, and prints the resulting hash of the state of the world.
119    ///
120    /// If --json is supplied, details about each message execution will printed.
121    #[command(about = "Compute the state hash at a given epoch")]
122    ComputeState {
123        /// Path to a snapshot CAR, which may be zstd compressed
124        snapshot: PathBuf,
125        /// Which epoch to compute the state transition for
126        #[arg(long)]
127        epoch: ChainEpoch,
128        /// Generate JSON output
129        #[arg(long)]
130        json: bool,
131    },
132}
133
134impl SnapshotCommands {
135    pub async fn run(self) -> anyhow::Result<()> {
136        match self {
137            Self::Fetch {
138                directory,
139                chain,
140                vendor,
141            } => match snapshot::fetch(&directory, &chain, vendor).await {
142                Ok(out) => {
143                    println!("{}", out.display());
144                    Ok(())
145                }
146                Err(e) => cli_error_and_die(format!("Failed fetching the snapshot: {e}"), 1),
147            },
148            Self::ValidateDiffs {
149                check_links,
150                check_network,
151                check_stateroots,
152                snapshot_files,
153            } => {
154                let store = ManyCar::try_from(snapshot_files)?;
155                validate_with_blockstore(
156                    store.heaviest_tipset()?,
157                    Arc::new(store),
158                    check_links,
159                    check_network,
160                    check_stateroots,
161                )
162                .await
163            }
164            Self::Validate {
165                check_links,
166                check_network,
167                check_stateroots,
168                snapshot_files,
169                fail_fast,
170            } => {
171                let mut has_fail = false;
172                for file in snapshot_files {
173                    println!("Validating {}", file.display());
174                    let result = async {
175                        let store = ManyCar::new(MemoryDB::default())
176                            .with_read_only(AnyCar::try_from(file.as_path())?)?;
177                        validate_with_blockstore(
178                            store.heaviest_tipset()?,
179                            Arc::new(store),
180                            check_links,
181                            check_network.clone(),
182                            check_stateroots,
183                        )
184                        .await?;
185                        Ok::<(), anyhow::Error>(())
186                    }
187                    .await;
188                    if let Err(e) = result {
189                        has_fail = true;
190                        eprintln!("Error: {e:?}");
191                        if fail_fast {
192                            break;
193                        }
194                    }
195                }
196                if has_fail {
197                    bail!("validate failed");
198                };
199                Ok(())
200            }
201            Self::Compress {
202                source,
203                output_path,
204                compression_level,
205                frame_size,
206                force,
207            } => {
208                // If input is 'snapshot.car.zst' and output is '.', set the
209                // destination to './snapshot.forest.car.zst'.
210                let destination = match output_path.is_dir() {
211                    true => {
212                        let mut destination = output_path;
213                        destination.push(source.clone());
214                        while let Some(ext) = destination.extension() {
215                            if !(ext == "zst" || ext == "car" || ext == "forest") {
216                                break;
217                            }
218                            destination.set_extension("");
219                        }
220                        destination.with_extension("forest.car.zst")
221                    }
222                    false => output_path.clone(),
223                };
224
225                if !force && destination.exists() {
226                    let have_permission = Confirm::with_theme(&ColorfulTheme::default())
227                        .with_prompt(format!(
228                            "{} will be overwritten. Continue?",
229                            destination.to_string_lossy()
230                        ))
231                        .default(false)
232                        .interact()
233                        // e.g not a tty (or some other error), so haven't got permission.
234                        .unwrap_or(false);
235                    if !have_permission {
236                        return Ok(());
237                    }
238                }
239
240                println!("Generating forest.car.zst file: {:?}", &destination);
241
242                let file = File::open(&source).await?;
243                let pb = ProgressBar::new(file.metadata().await?.len()).with_style(
244                    ProgressStyle::with_template("{bar} {percent}%, eta: {eta}")
245                        .expect("infallible"),
246                );
247                let file = tokio::io::BufReader::new(pb.wrap_async_read(file));
248
249                let mut block_stream = CarStream::new(file).await?;
250                let roots = std::mem::replace(
251                    &mut block_stream.header_v1.roots,
252                    nunny::vec![Default::default()],
253                );
254
255                let mut dest = tokio::io::BufWriter::new(File::create(&destination).await?);
256
257                let frames = crate::db::car::forest::Encoder::compress_stream(
258                    frame_size,
259                    compression_level,
260                    block_stream.map_err(anyhow::Error::from),
261                );
262                crate::db::car::forest::Encoder::write(&mut dest, roots, frames).await?;
263                dest.flush().await?;
264                Ok(())
265            }
266            SnapshotCommands::ComputeState {
267                snapshot,
268                epoch,
269                json,
270            } => print_computed_state(snapshot, epoch, json),
271        }
272    }
273}
274
275// Check the validity of a snapshot by looking at IPLD links, the genesis block,
276// and message output. More checks may be added in the future.
277//
278// If the snapshot is valid, the output should look like this:
279//     Checking IPLD integrity:       ✅ verified!
280//     Identifying genesis block:     ✅ found!
281//     Verifying network identity:    ✅ verified!
282//     Running tipset transactions:   ✅ verified!
283//   Snapshot is valid
284//
285// If we receive a mainnet snapshot but expect a calibnet snapshot, the output
286// should look like this:
287//     Checking IPLD integrity:       ✅ verified!
288//     Identifying genesis block:     ✅ found!
289//     Verifying network identity:    ❌ wrong!
290//   Error: Expected mainnet but found calibnet
291async fn validate_with_blockstore<BlockstoreT>(
292    root: Tipset,
293    store: Arc<BlockstoreT>,
294    check_links: u32,
295    check_network: Option<NetworkChain>,
296    check_stateroots: u32,
297) -> anyhow::Result<()>
298where
299    BlockstoreT: PersistentStore + Send + Sync + 'static,
300{
301    if check_links != 0 {
302        validate_ipld_links(root.clone(), &store, check_links).await?;
303    }
304
305    if let Some(expected_network) = &check_network {
306        let actual_network = query_network(&root, &store)?;
307        // Somewhat silly use of a spinner but this makes the checks line up nicely.
308        let pb = validation_spinner("Verifying network identity:");
309        if expected_network != &actual_network {
310            pb.finish_with_message("❌ wrong!");
311            bail!("Expected {} but found {}", expected_network, actual_network);
312        } else {
313            pb.finish_with_message("✅ verified!");
314        }
315    }
316
317    if check_stateroots != 0 {
318        let network = check_network
319            .map(anyhow::Ok)
320            .unwrap_or_else(|| query_network(&root, &store))?;
321        validate_stateroots(root, &store, network, check_stateroots).await?;
322    }
323
324    println!("Snapshot is valid");
325    Ok(())
326}
327
328// The Filecoin block chain is a DAG of Ipld nodes. The complete graph isn't
329// required to sync to the network and snapshot files usually disgard data after
330// 2000 epochs. Validity can be verified by ensuring there are no bad IPLD or
331// broken links in the N most recent epochs.
332async fn validate_ipld_links<DB>(ts: Tipset, db: &DB, epochs: u32) -> anyhow::Result<()>
333where
334    DB: Blockstore + Send + Sync,
335{
336    let epoch_limit = ts.epoch() - epochs as i64;
337
338    let pb = validation_spinner("Checking IPLD integrity:").with_finish(
339        indicatif::ProgressFinish::AbandonWithMessage("❌ Invalid IPLD data!".into()),
340    );
341
342    let tipsets = ts.chain(db).inspect(|tipset| {
343        let height = tipset.epoch();
344        if height - epoch_limit >= 0 {
345            pb.set_message(format!("{} remaining epochs (state)", height - epoch_limit));
346        } else {
347            pb.set_message(format!("{height} remaining epochs (spine)"));
348        }
349    });
350    let mut stream = stream_chain(&db, tipsets, epoch_limit);
351    while stream.try_next().await?.is_some() {}
352
353    pb.finish_with_message("✅ verified!");
354    Ok(())
355}
356
357// The genesis block determines the network identity (e.g., mainnet or
358// calibnet). Scanning through the entire blockchain can be time-consuming, so
359// Forest keeps a list of known tipsets for each network. Finding a known tipset
360// short-circuits the search for the genesis block. If no genesis block can be
361// found or if the genesis block is unrecognizable, an error is returned.
362fn query_network(ts: &Tipset, db: &impl Blockstore) -> anyhow::Result<NetworkChain> {
363    let pb = validation_spinner("Identifying genesis block:").with_finish(
364        indicatif::ProgressFinish::AbandonWithMessage("✅ found!".into()),
365    );
366
367    fn match_genesis_block(block_cid: Cid) -> anyhow::Result<NetworkChain> {
368        if block_cid == *calibnet::GENESIS_CID {
369            Ok(NetworkChain::Calibnet)
370        } else if block_cid == *mainnet::GENESIS_CID {
371            Ok(NetworkChain::Mainnet)
372        } else if block_cid == *butterflynet::GENESIS_CID {
373            Ok(NetworkChain::Butterflynet)
374        } else {
375            bail!("Unrecognizable genesis block");
376        }
377    }
378
379    if let Ok(genesis_block) = ts.genesis(db) {
380        return match_genesis_block(*genesis_block.cid());
381    }
382
383    pb.finish_with_message("❌ No valid genesis block!");
384    bail!("Snapshot does not contain a genesis block")
385}
386
387// Each tipset in the blockchain contains a set of messages. A message is a
388// transaction that manipulates a persistent state-tree. The hashes of these
389// state-trees are stored in the tipsets and can be used to verify if the
390// messages were correctly executed.
391// Note: Messages may access state-trees 900 epochs in the past. So, if a
392// snapshot has state-trees for 2000 epochs, one can only validate the messages
393// for the last 1100 epochs.
394async fn validate_stateroots<DB>(
395    ts: Tipset,
396    db: &Arc<DB>,
397    network: NetworkChain,
398    epochs: u32,
399) -> anyhow::Result<()>
400where
401    DB: PersistentStore + Send + Sync + 'static,
402{
403    let chain_config = Arc::new(ChainConfig::from_chain(&network));
404    let genesis = ts.genesis(db)?;
405
406    let pb = validation_spinner("Running tipset transactions:").with_finish(
407        indicatif::ProgressFinish::AbandonWithMessage(
408            "❌ Transaction result differs from Lotus!".into(),
409        ),
410    );
411
412    // Fix off-by-1 bug: prevent validating more epochs than available in the snapshot.
413    // Without +1, specifying --check-stateroots=900 would validate 901 epochs,
414    // causing out-of-bounds errors when the snapshot contains only 900 recent state roots.
415    let last_epoch = ts.epoch() - epochs as i64 + 1;
416
417    // Bundles are required when doing state migrations.
418    load_actor_bundles(&db, &network).await?;
419
420    // Set proof parameter data dir and make sure the proofs are available
421    crate::utils::proofs_api::maybe_set_proofs_parameter_cache_dir_env(
422        &Config::default().client.data_dir,
423    );
424
425    ensure_proof_params_downloaded().await?;
426
427    let chain_index = Arc::new(ChainIndex::new(Arc::new(db.clone())));
428
429    // Prepare tipsets for validation
430    let tipsets = chain_index
431        .chain(Arc::new(ts))
432        .take_while(|tipset| tipset.epoch() >= last_epoch)
433        .inspect(|tipset| {
434            pb.set_message(format!("epoch queue: {}", tipset.epoch() - last_epoch));
435        });
436
437    let beacon = Arc::new(chain_config.get_beacon_schedule(genesis.timestamp));
438
439    // ProgressBar::wrap_iter believes the progress has been abandoned once the
440    // iterator is consumed.
441    crate::state_manager::validate_tipsets(
442        genesis.timestamp,
443        chain_index.clone(),
444        chain_config,
445        beacon,
446        &GLOBAL_MULTI_ENGINE,
447        tipsets,
448    )?;
449
450    pb.finish_with_message("✅ verified!");
451    Ok(())
452}
453
454fn validation_spinner(prefix: &'static str) -> indicatif::ProgressBar {
455    let pb = indicatif::ProgressBar::new_spinner()
456        .with_style(
457            indicatif::ProgressStyle::with_template("{spinner} {prefix:<30} {msg}")
458                .expect("indicatif template must be valid"),
459        )
460        .with_prefix(prefix);
461    pb.enable_steady_tick(std::time::Duration::from_secs_f32(0.1));
462    pb
463}
464
465fn print_computed_state(snapshot: PathBuf, epoch: ChainEpoch, json: bool) -> anyhow::Result<()> {
466    // Initialize Blockstore
467    let store = Arc::new(AnyCar::try_from(snapshot.as_path())?);
468
469    // Prepare call to apply_block_messages
470    let ts = store.heaviest_tipset()?;
471
472    let genesis = ts.genesis(&store)?;
473    let network = NetworkChain::from_genesis_or_devnet_placeholder(genesis.cid());
474
475    let timestamp = genesis.timestamp;
476    let chain_index = ChainIndex::new(Arc::clone(&store));
477    let chain_config = ChainConfig::from_chain(&network);
478    if chain_config.is_testnet() {
479        CurrentNetwork::set_global(Network::Testnet);
480    }
481    let beacon = Arc::new(chain_config.get_beacon_schedule(timestamp));
482    let tipset = chain_index
483        .tipset_by_height(epoch, Arc::new(ts), ResolveNullTipset::TakeOlder)
484        .with_context(|| format!("couldn't get a tipset at height {epoch}"))?;
485
486    let mut message_calls = vec![];
487
488    let StateOutput { state_root, .. } = apply_block_messages(
489        timestamp,
490        Arc::new(chain_index),
491        Arc::new(chain_config),
492        beacon,
493        &GLOBAL_MULTI_ENGINE,
494        tipset,
495        if json {
496            Some(|ctx: MessageCallbackCtx<'_>| {
497                message_calls.push((
498                    ctx.message.clone(),
499                    ctx.apply_ret.clone(),
500                    ctx.at,
501                    ctx.duration,
502                ));
503                Ok(())
504            })
505        } else {
506            None
507        },
508        match json {
509            true => VMTrace::Traced,
510            false => VMTrace::NotTraced,
511        }, // enable traces if json flag is used
512    )?;
513
514    if json {
515        println!("{:#}", structured::json(state_root, message_calls)?);
516    } else {
517        println!("computed state cid: {state_root}");
518    }
519
520    Ok(())
521}
522
523mod structured {
524    use cid::Cid;
525    use serde_json::json;
526
527    use crate::lotus_json::HasLotusJson as _;
528    use crate::state_manager::utils::structured;
529    use crate::{
530        interpreter::CalledAt,
531        message::{ChainMessage, Message as _},
532        shim::executor::ApplyRet,
533    };
534    use std::time::Duration;
535
536    pub fn json(
537        state_root: Cid,
538        contexts: Vec<(ChainMessage, ApplyRet, CalledAt, Duration)>,
539    ) -> anyhow::Result<serde_json::Value> {
540        Ok(json!({
541        "Root": state_root.into_lotus_json(),
542        "Trace": contexts
543            .into_iter()
544            .map(|(message, apply_ret, called_at, duration)| call_json(message, apply_ret, called_at, duration))
545            .collect::<Result<Vec<_>, _>>()?
546        }))
547    }
548
549    fn call_json(
550        chain_message: ChainMessage,
551        apply_ret: ApplyRet,
552        called_at: CalledAt,
553        duration: Duration,
554    ) -> anyhow::Result<serde_json::Value> {
555        let is_explicit = matches!(called_at.apply_kind(), fvm3::executor::ApplyKind::Explicit);
556
557        let chain_message_cid = chain_message.cid();
558        let unsigned_message_cid = chain_message.message().cid();
559
560        Ok(json!({
561            "MsgCid": chain_message_cid.into_lotus_json(),
562            "Msg": chain_message.message().clone().into_lotus_json(),
563            "MsgRct": apply_ret.msg_receipt().into_lotus_json(),
564            "Error": apply_ret.failure_info().unwrap_or_default(),
565            "GasCost": {
566                "Message": is_explicit.then_some(unsigned_message_cid.into_lotus_json()),
567                "GasUsed": if is_explicit { apply_ret.msg_receipt().gas_used() } else { Default::default() },
568                "BaseFeeBurn": apply_ret.base_fee_burn().into_lotus_json(),
569                "OverEstimationBurn": apply_ret.over_estimation_burn().into_lotus_json(),
570                "MinerPenalty": apply_ret.penalty().into_lotus_json(),
571                "MinerTip": apply_ret.miner_tip().into_lotus_json(),
572                "Refund": apply_ret.refund().into_lotus_json(),
573                "TotalCost": (chain_message.message().required_funds() - &apply_ret.refund()).into_lotus_json(),
574            },
575            "ExecutionTrace": structured::parse_events(apply_ret.exec_trace())?.into_lotus_json(),
576            "Duration": duration.as_nanos().clamp(0, u64::MAX as u128) as u64,
577        }))
578    }
579}