Skip to main content

forest/dev/subcommands/
state_cmd.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use crate::{
5    chain::{ChainStore, index::ResolveNullTipset},
6    chain_sync::{load_full_tipset, tipset_syncer::validate_tipset},
7    cli_shared::{chain_path, read_config},
8    db::{SettingsStoreExt, db_engine::db_root},
9    genesis::read_genesis_header,
10    interpreter::VMTrace,
11    networks::{ChainConfig, NetworkChain},
12    prelude::*,
13    shim::clock::ChainEpoch,
14    state_manager::{ExecutedTipset, StateManager},
15    tool::subcommands::api_cmd::generate_test_snapshot,
16};
17use human_repr::HumanCount as _;
18use nonzero_ext::nonzero;
19use std::{num::NonZeroUsize, path::PathBuf, time::Instant};
20
21/// Interact with Filecoin chain state
22#[derive(Debug, clap::Subcommand)]
23pub enum StateCommand {
24    Compute(ComputeCommand),
25    ReplayCompute(ReplayComputeCommand),
26    Validate(ValidateCommand),
27    ReplayValidate(ReplayValidateCommand),
28}
29
30impl StateCommand {
31    pub async fn run(self) -> anyhow::Result<()> {
32        match self {
33            Self::Compute(cmd) => cmd.run().await,
34            Self::ReplayCompute(cmd) => cmd.run().await,
35            Self::Validate(cmd) => cmd.run().await,
36            Self::ReplayValidate(cmd) => cmd.run().await,
37        }
38    }
39}
40
41/// Compute state tree for an epoch
42#[derive(Debug, clap::Args)]
43pub struct ComputeCommand {
44    /// Which epoch to compute the state transition for
45    #[arg(long, required = true)]
46    epoch: ChainEpoch,
47    /// Filecoin network chain
48    #[arg(long, required = true)]
49    chain: NetworkChain,
50    /// Optional path to the database folder
51    #[arg(long)]
52    db: Option<PathBuf>,
53    /// Optional path to the database snapshot `CAR` file to write to for reproducing the computation
54    #[arg(long)]
55    export_db_to: Option<PathBuf>,
56}
57
58impl ComputeCommand {
59    pub async fn run(self) -> anyhow::Result<()> {
60        let Self {
61            epoch,
62            chain,
63            db,
64            export_db_to,
65        } = self;
66        disable_tipset_cache();
67        let db_root_path = if let Some(db) = db {
68            db
69        } else {
70            let (_, config) = read_config(None, Some(chain.clone()))?;
71            db_root(&chain_path(&config))?
72        };
73        let db = generate_test_snapshot::load_db(&db_root_path, Some(&chain)).await?;
74        let chain_config = Arc::new(ChainConfig::from_chain(&chain));
75        let genesis_header =
76            read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db)
77                .await?;
78        let chain_store = ChainStore::new(db.clone(), chain_config, genesis_header)?;
79        let chain_index = chain_store.chain_index();
80        let (ts, ts_next) = {
81            // We don't want to track all entries that are visited by `tipset_by_height`
82            db.pause_tracking();
83            let ts = chain_index
84                .load_required_tipset_by_height(
85                    epoch,
86                    chain_store.heaviest_tipset(),
87                    ResolveNullTipset::TakeOlder,
88                )
89                .await?;
90            let ts_next = chain_store.load_child_tipset(&ts).await?.with_context(|| {
91                format!(
92                    "no child tipset for epoch {} (may be chain head)",
93                    ts.epoch()
94                )
95            })?;
96            db.resume_tracking();
97            SettingsStoreExt::write_obj(
98                &db.tracker,
99                crate::db::setting_keys::HEAD_KEY,
100                ts_next.key(),
101            )?;
102            // Only track the desired tipsets
103            (
104                chain_index.load_required_tipset(ts.key())?,
105                chain_index.load_required_tipset(ts_next.key())?,
106            )
107        };
108        let epoch = ts.epoch();
109        let state_manager = StateManager::new(chain_store)?;
110
111        let ExecutedTipset {
112            state_root,
113            receipt_root,
114            ..
115        } = state_manager
116            .compute_tipset_state(ts, crate::state_manager::NO_CALLBACK, VMTrace::NotTraced)
117            .await?;
118        let mut db_snapshot = vec![];
119        db.export_forest_car(&mut db_snapshot).await?;
120        println!(
121            "epoch: {epoch}, state_root: {state_root}, receipt_root: {receipt_root}, db_snapshot_size: {}",
122            db_snapshot.len().human_count_bytes()
123        );
124        if let Some(export_db_to) = export_db_to {
125            std::fs::write(export_db_to, db_snapshot)?;
126        }
127        let expected_state_root = *ts_next.parent_state();
128        let expected_receipt_root = *ts_next.parent_message_receipts();
129        anyhow::ensure!(
130            state_root == expected_state_root,
131            "state root mismatch, state_root: {state_root}, expected_state_root: {expected_state_root}"
132        );
133        anyhow::ensure!(
134            receipt_root == expected_receipt_root,
135            "receipt root mismatch, receipt_root: {receipt_root}, expected_receipt_root: {expected_receipt_root}"
136        );
137        Ok(())
138    }
139}
140
141/// Replay state computation with a db snapshot
142/// To be used in conjunction with `forest-dev state compute`.
143#[derive(Debug, clap::Args)]
144pub struct ReplayComputeCommand {
145    /// Path to the database snapshot `CAR` file generated by `forest-dev state compute`
146    snapshot: PathBuf,
147    /// Filecoin network chain
148    #[arg(long, required = true)]
149    chain: NetworkChain,
150    /// Number of times to repeat the state computation
151    #[arg(short, long, default_value_t = nonzero!(1usize))]
152    n: NonZeroUsize,
153}
154
155impl ReplayComputeCommand {
156    pub async fn run(self) -> anyhow::Result<()> {
157        let Self { snapshot, chain, n } = self;
158        let (sm, ts, ts_next) =
159            crate::state_manager::utils::state_compute::prepare_state_compute(&chain, &snapshot)
160                .await?;
161        for _ in 0..n.get() {
162            crate::state_manager::utils::state_compute::state_compute(
163                &sm,
164                ts.shallow_clone(),
165                &ts_next,
166            )
167            .await?;
168        }
169        Ok(())
170    }
171}
172
173/// Validate tipset at a certain epoch
174#[derive(Debug, clap::Args)]
175pub struct ValidateCommand {
176    /// Tipset epoch to validate
177    #[arg(long, required = true)]
178    epoch: ChainEpoch,
179    /// Filecoin network chain
180    #[arg(long, required = true)]
181    chain: NetworkChain,
182    /// Optional path to the database folder
183    #[arg(long)]
184    db: Option<PathBuf>,
185    /// Optional path to the database snapshot `CAR` file to write to for reproducing the computation
186    #[arg(long)]
187    export_db_to: Option<PathBuf>,
188}
189
190impl ValidateCommand {
191    pub async fn run(self) -> anyhow::Result<()> {
192        let Self {
193            epoch,
194            chain,
195            db,
196            export_db_to,
197        } = self;
198        disable_tipset_cache();
199        let db_root_path = if let Some(db) = db {
200            db
201        } else {
202            let (_, config) = read_config(None, Some(chain.clone()))?;
203            db_root(&chain_path(&config))?
204        };
205        let db = generate_test_snapshot::load_db(&db_root_path, Some(&chain)).await?;
206        let chain_config = Arc::new(ChainConfig::from_chain(&chain));
207        let genesis_header =
208            read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db)
209                .await?;
210        let chain_store = ChainStore::new(db.clone(), chain_config, genesis_header)?;
211        let chain_index = chain_store.chain_index();
212        let ts = {
213            // We don't want to track all entries that are visited by `tipset_by_height`
214            db.pause_tracking();
215            let ts = chain_index
216                .load_required_tipset_by_height(
217                    epoch,
218                    chain_store.heaviest_tipset(),
219                    ResolveNullTipset::TakeOlder,
220                )
221                .await?;
222            db.resume_tracking();
223            SettingsStoreExt::write_obj(&db.tracker, crate::db::setting_keys::HEAD_KEY, ts.key())?;
224            // Only track the desired tipset
225            chain_index.load_required_tipset(ts.key())?
226        };
227        let epoch = ts.epoch();
228        let fts = load_full_tipset(&chain_store, ts.key())?;
229        let state_manager = StateManager::new(chain_store)?;
230        validate_tipset(&state_manager, fts, None).await?;
231        let mut db_snapshot = vec![];
232        db.export_forest_car(&mut db_snapshot).await?;
233        println!(
234            "epoch: {epoch}, db_snapshot_size: {}",
235            db_snapshot.len().human_count_bytes()
236        );
237        if let Some(export_db_to) = export_db_to {
238            std::fs::write(export_db_to, db_snapshot)?;
239        }
240        Ok(())
241    }
242}
243
244/// Replay tipset validation with a db snapshot
245/// To be used in conjunction with `forest-dev state validate`.
246#[derive(Debug, clap::Args)]
247pub struct ReplayValidateCommand {
248    /// Path to the database snapshot `CAR` file generated by `forest-dev state validate`
249    snapshot: PathBuf,
250    /// Filecoin network chain
251    #[arg(long, required = true)]
252    chain: NetworkChain,
253    /// Number of times to repeat the state computation
254    #[arg(short, long, default_value_t = nonzero!(1usize))]
255    n: NonZeroUsize,
256}
257
258impl ReplayValidateCommand {
259    pub async fn run(self) -> anyhow::Result<()> {
260        let Self { snapshot, chain, n } = self;
261        let (sm, fts) =
262            crate::state_manager::utils::state_compute::prepare_state_validate(&chain, &snapshot)
263                .await?;
264        let epoch = fts.epoch();
265        for _ in 0..n.get() {
266            let fts = fts.clone();
267            let start = Instant::now();
268            validate_tipset(&sm, fts, None).await?;
269            println!(
270                "epoch: {epoch}, took {}.",
271                humantime::format_duration(start.elapsed())
272            );
273        }
274        Ok(())
275    }
276}
277
278fn disable_tipset_cache() {
279    unsafe {
280        std::env::set_var("FOREST_TIPSET_CACHE_DISABLED", "1");
281        std::env::set_var("FOREST_TIPSET_LOOKUP_TABLE_DISABLED", "1");
282    }
283}