1use 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 Fetch {
38 #[arg(short, long, default_value = ".")]
39 directory: PathBuf,
40 #[arg(long, default_value_t = NetworkChain::Mainnet)]
42 chain: NetworkChain,
43 #[arg(short, long, value_enum, default_value_t = snapshot::TrustedVendor::default())]
45 vendor: snapshot::TrustedVendor,
46 },
47
48 ValidateDiffs {
50 #[arg(long, default_value_t = 2000)]
52 check_links: u32,
53 #[arg(long)]
56 check_network: Option<crate::networks::NetworkChain>,
57 #[arg(long, default_value_t = 60)]
59 check_stateroots: u32,
60 #[arg(required = true)]
62 snapshot_files: Vec<PathBuf>,
63 },
64
65 Validate {
67 #[arg(long, default_value_t = 2000)]
69 check_links: u32,
70 #[arg(long)]
73 check_network: Option<crate::networks::NetworkChain>,
74 #[arg(long, default_value_t = 60)]
76 check_stateroots: u32,
77 #[arg(required = true)]
79 snapshot_files: Vec<PathBuf>,
80 #[arg(long)]
82 fail_fast: bool,
83 },
84
85 Compress {
87 source: PathBuf,
89 #[arg(short, long, default_value = ".")]
94 output_path: PathBuf,
95 #[arg(long, default_value_t = 3)]
96 compression_level: u16,
97 #[arg(long, default_value_t = DEFAULT_FOREST_CAR_FRAME_SIZE)]
99 frame_size: usize,
100 #[arg(long, default_value_t = false)]
102 force: bool,
103 },
104 #[command(about = "Compute the state hash at a given epoch")]
122 ComputeState {
123 snapshot: PathBuf,
125 #[arg(long)]
127 epoch: ChainEpoch,
128 #[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 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 .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
275async 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 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
328async 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
357fn 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
387async 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 let last_epoch = ts.epoch() - epochs as i64 + 1;
416
417 load_actor_bundles(&db, &network).await?;
419
420 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 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 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 let store = Arc::new(AnyCar::try_from(snapshot.as_path())?);
468
469 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 }, )?;
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}