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