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