Skip to main content

forest/dev/subcommands/
mod.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4mod archive_missing_cmd;
5mod export_state_tree_cmd;
6mod export_tipset_lookup_cmd;
7mod state_cmd;
8mod tests_cmd;
9mod update_checkpoints_cmd;
10
11use crate::cli_shared::cli::HELP_MESSAGE;
12use crate::networks::generate_actor_bundle;
13use crate::rpc::Client;
14use crate::state_manager::utils::state_compute::{
15    get_state_snapshot_file, list_state_snapshot_files,
16};
17use crate::utils::net::{DownloadFileOption, download_file_with_cache};
18use crate::utils::proofs_api::ensure_proof_params_downloaded;
19use crate::utils::version::FOREST_VERSION_STRING;
20use anyhow::Context as _;
21use clap::Parser;
22use directories::ProjectDirs;
23use std::borrow::Cow;
24use std::path::PathBuf;
25use std::time::Duration;
26use tokio::task::JoinSet;
27use url::Url;
28
29/// Command-line options for the `forest-dev` binary
30#[derive(Parser)]
31#[command(name = env!("CARGO_PKG_NAME"), bin_name = "forest-dev", author = env!("CARGO_PKG_AUTHORS"), version = FOREST_VERSION_STRING.as_str(), about = env!("CARGO_PKG_DESCRIPTION")
32)]
33#[command(help_template(HELP_MESSAGE))]
34pub struct Cli {
35    #[command(subcommand)]
36    pub cmd: Subcommand,
37}
38
39/// forest-dev sub-commands
40#[derive(clap::Subcommand)]
41pub enum Subcommand {
42    /// Fetch test snapshots to the local cache
43    FetchTestSnapshots {
44        // Save actor bundle to
45        #[arg(long)]
46        actor_bundle: Option<PathBuf>,
47    },
48    #[command(subcommand)]
49    State(state_cmd::StateCommand),
50    /// Update known blocks (checkpoints), normally in `build/known_blocks.yaml`, by querying RPC
51    /// endpoints
52    UpdateCheckpoints(update_checkpoints_cmd::UpdateCheckpointsCommand),
53    /// Find missing archival snapshots on the Forest Archive for a given epoch range
54    ArchiveMissing(archive_missing_cmd::ArchiveMissingCommand),
55    ExportTipsetLookup(export_tipset_lookup_cmd::ExportTipsetLookupCommand),
56    ExportStateTree(export_state_tree_cmd::ExportStateTreeCommand),
57    #[command(subcommand)]
58    Tests(tests_cmd::TestsCommand),
59}
60
61impl Subcommand {
62    pub async fn run(self, _client: Client) -> anyhow::Result<()> {
63        match self {
64            Self::FetchTestSnapshots { actor_bundle } => fetch_test_snapshots(actor_bundle).await,
65            Self::State(cmd) => cmd.run().await,
66            Self::UpdateCheckpoints(cmd) => cmd.run().await,
67            Self::ArchiveMissing(cmd) => cmd.run().await,
68            Self::ExportTipsetLookup(cmd) => cmd.run().await,
69            Self::ExportStateTree(cmd) => cmd.run().await,
70            Self::Tests(cmd) => cmd.run().await,
71        }
72    }
73}
74
75async fn fetch_test_snapshots(actor_bundle: Option<PathBuf>) -> anyhow::Result<()> {
76    // Prepare proof parameter files
77    crate::utils::proofs_api::maybe_set_proofs_parameter_cache_dir_env(
78        &crate::cli_shared::default_data_dir(),
79    );
80    ensure_proof_params_downloaded().await?;
81
82    // Prepare actor bundles
83    if let Some(actor_bundle) = actor_bundle {
84        generate_actor_bundle(&actor_bundle).await?;
85        println!("Wrote the actors bundle to {}", actor_bundle.display());
86    }
87
88    // Prepare state computation and validation snapshots
89    fetch_state_tests().await?;
90
91    // Prepare RPC test snapshots
92    fetch_rpc_tests().await?;
93
94    Ok(())
95}
96
97pub async fn fetch_state_tests() -> anyhow::Result<()> {
98    let files = list_state_snapshot_files().await?;
99    let mut joinset = JoinSet::new();
100    for file in files {
101        joinset.spawn(async move { get_state_snapshot_file(&file).await });
102    }
103    for result in joinset.join_all().await {
104        if let Err(e) = result {
105            tracing::warn!("{e:#}");
106        }
107    }
108    Ok(())
109}
110
111async fn fetch_rpc_tests() -> anyhow::Result<()> {
112    let tests = include_str!("../../tool/subcommands/api_cmd/test_snapshots.txt")
113        .lines()
114        .map(|i| {
115            // Remove comment
116            i.split("#").next().unwrap().trim().to_string()
117        })
118        .filter(|l| !l.is_empty() && !l.starts_with('#'));
119    let mut joinset = JoinSet::new();
120    for test in tests {
121        joinset.spawn(fetch_rpc_test_snapshot(test.into()));
122    }
123    for result in joinset.join_all().await {
124        if let Err(e) = result {
125            tracing::warn!("{e:#}");
126        }
127    }
128    Ok(())
129}
130
131pub async fn fetch_rpc_test_snapshot<'a>(name: Cow<'a, str>) -> anyhow::Result<PathBuf> {
132    let url: Url =
133        format!("https://forest-snapshots.fra1.cdn.digitaloceanspaces.com/rpc_test/{name}")
134            .parse()
135            .with_context(|| format!("Failed to parse URL for test: {name}"))?;
136    let project_dir =
137        ProjectDirs::from("com", "ChainSafe", "Forest").context("failed to get project dir")?;
138    let cache_dir = project_dir.cache_dir().join("test").join("rpc-snapshots");
139    let path = crate::utils::retry(
140        crate::utils::RetryArgs {
141            timeout: Some(Duration::from_secs(30)),
142            max_retries: Some(5),
143            delay: Some(Duration::from_secs(1)),
144        },
145        || download_file_with_cache(&url, &cache_dir, DownloadFileOption::NonResumable),
146    )
147    .await
148    .with_context(|| format!("failed to fetch rpc test snapshot {name}"))?
149    .path;
150    Ok(path)
151}