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 human_repr::HumanCount as _;
18use nonzero_ext::nonzero;
19use std::{num::NonZeroUsize, path::PathBuf, sync::Arc, 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 = Arc::new(ChainStore::new(
79            db.clone(),
80            db.clone(),
81            db.clone(),
82            chain_config,
83            genesis_header,
84        )?);
85        let (ts, ts_next) = {
86            // We don't want to track all entries that are visited by `tipset_by_height`
87            db.pause_tracking();
88            let ts = chain_store.chain_index().tipset_by_height(
89                epoch,
90                chain_store.heaviest_tipset(),
91                ResolveNullTipset::TakeOlder,
92            )?;
93            let ts_next = chain_store.chain_index().tipset_by_height(
94                epoch + 1,
95                chain_store.heaviest_tipset(),
96                ResolveNullTipset::TakeNewer,
97            )?;
98            db.resume_tracking();
99            SettingsStoreExt::write_obj(
100                &db.tracker,
101                crate::db::setting_keys::HEAD_KEY,
102                ts_next.key(),
103            )?;
104            // Only track the desired tipsets
105            (
106                Tipset::load_required(&db, ts.key())?,
107                Tipset::load_required(&db, ts_next.key())?,
108            )
109        };
110        let epoch = ts.epoch();
111        let state_manager = Arc::new(StateManager::new(chain_store)?);
112
113        let StateOutput {
114            state_root,
115            receipt_root,
116            ..
117        } = state_manager
118            .compute_tipset_state(ts, crate::state_manager::NO_CALLBACK, VMTrace::NotTraced)
119            .await?;
120        let mut db_snapshot = vec![];
121        db.export_forest_car(&mut db_snapshot).await?;
122        println!(
123            "epoch: {epoch}, state_root: {state_root}, receipt_root: {receipt_root}, db_snapshot_size: {}",
124            db_snapshot.len().human_count_bytes()
125        );
126        let expected_state_root = *ts_next.parent_state();
127        let expected_receipt_root = *ts_next.parent_message_receipts();
128        anyhow::ensure!(
129            state_root == expected_state_root,
130            "state root mismatch, state_root: {state_root}, expected_state_root: {expected_state_root}"
131        );
132        anyhow::ensure!(
133            receipt_root == expected_receipt_root,
134            "receipt root mismatch, receipt_root: {receipt_root}, expected_receipt_root: {expected_receipt_root}"
135        );
136        if let Some(export_db_to) = export_db_to {
137            std::fs::write(export_db_to, db_snapshot)?;
138        }
139        Ok(())
140    }
141}
142
143/// Replay state computation with a db snapshot
144/// To be used in conjunction with `forest-dev state compute`.
145#[derive(Debug, clap::Args)]
146pub struct ReplayComputeCommand {
147    /// Path to the database snapshot `CAR` file generated by `forest-dev state compute`
148    snapshot: PathBuf,
149    /// Filecoin network chain
150    #[arg(long, required = true)]
151    chain: NetworkChain,
152    /// Number of times to repeat the state computation
153    #[arg(short, long, default_value_t = nonzero!(1usize))]
154    n: NonZeroUsize,
155}
156
157impl ReplayComputeCommand {
158    pub async fn run(self) -> anyhow::Result<()> {
159        let Self { snapshot, chain, n } = self;
160        let (sm, ts, ts_next) =
161            crate::state_manager::utils::state_compute::prepare_state_compute(&chain, &snapshot)
162                .await?;
163        for _ in 0..n.get() {
164            crate::state_manager::utils::state_compute::state_compute(&sm, ts.clone(), &ts_next)
165                .await?;
166        }
167        Ok(())
168    }
169}
170
171/// Validate tipset at a certain epoch
172#[derive(Debug, clap::Args)]
173pub struct ValidateCommand {
174    /// Tipset epoch to validate
175    #[arg(long, required = true)]
176    epoch: ChainEpoch,
177    /// Filecoin network chain
178    #[arg(long, required = true)]
179    chain: NetworkChain,
180    /// Optional path to the database folder
181    #[arg(long)]
182    db: Option<PathBuf>,
183    /// Optional path to the database snapshot `CAR` file to write to for reproducing the computation
184    #[arg(long)]
185    export_db_to: Option<PathBuf>,
186}
187
188impl ValidateCommand {
189    pub async fn run(self) -> anyhow::Result<()> {
190        let Self {
191            epoch,
192            chain,
193            db,
194            export_db_to,
195        } = self;
196        disable_tipset_cache();
197        let db_root_path = if let Some(db) = db {
198            db
199        } else {
200            let (_, config) = read_config(None, Some(chain.clone()))?;
201            db_root(&chain_path(&config))?
202        };
203        let db = generate_test_snapshot::load_db(&db_root_path, Some(&chain)).await?;
204        let chain_config = Arc::new(ChainConfig::from_chain(&chain));
205        let genesis_header =
206            read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db)
207                .await?;
208        let chain_store = Arc::new(ChainStore::new(
209            db.clone(),
210            db.clone(),
211            db.clone(),
212            chain_config,
213            genesis_header,
214        )?);
215        let ts = {
216            // We don't want to track all entries that are visited by `tipset_by_height`
217            db.pause_tracking();
218            let ts = chain_store.chain_index().tipset_by_height(
219                epoch,
220                chain_store.heaviest_tipset(),
221                ResolveNullTipset::TakeOlder,
222            )?;
223            db.resume_tracking();
224            SettingsStoreExt::write_obj(&db.tracker, crate::db::setting_keys::HEAD_KEY, ts.key())?;
225            // Only track the desired tipset
226            Tipset::load_required(&db, ts.key())?
227        };
228        let epoch = ts.epoch();
229        let fts = load_full_tipset(&chain_store, ts.key())?;
230        let state_manager = Arc::new(StateManager::new(chain_store)?);
231        validate_tipset(&state_manager, fts, None).await?;
232        let mut db_snapshot = vec![];
233        db.export_forest_car(&mut db_snapshot).await?;
234        println!(
235            "epoch: {epoch}, db_snapshot_size: {}",
236            db_snapshot.len().human_count_bytes()
237        );
238        if let Some(export_db_to) = export_db_to {
239            std::fs::write(export_db_to, db_snapshot)?;
240        }
241        Ok(())
242    }
243}
244
245/// Replay tipset validation with a db snapshot
246/// To be used in conjunction with `forest-dev state validate`.
247#[derive(Debug, clap::Args)]
248pub struct ReplayValidateCommand {
249    /// Path to the database snapshot `CAR` file generated by `forest-dev state validate`
250    snapshot: PathBuf,
251    /// Filecoin network chain
252    #[arg(long, required = true)]
253    chain: NetworkChain,
254    /// Number of times to repeat the state computation
255    #[arg(short, long, default_value_t = nonzero!(1usize))]
256    n: NonZeroUsize,
257}
258
259impl ReplayValidateCommand {
260    pub async fn run(self) -> anyhow::Result<()> {
261        let Self { snapshot, chain, n } = self;
262        let (sm, fts) =
263            crate::state_manager::utils::state_compute::prepare_state_validate(&chain, &snapshot)
264                .await?;
265        let epoch = fts.epoch();
266        for _ in 0..n.get() {
267            let fts = fts.clone();
268            let start = Instant::now();
269            validate_tipset(&sm, fts, None).await?;
270            println!(
271                "epoch: {epoch}, took {}.",
272                humantime::format_duration(start.elapsed())
273            );
274        }
275        Ok(())
276    }
277}
278
279fn disable_tipset_cache() {
280    unsafe {
281        std::env::set_var("FOREST_TIPSET_CACHE_DISABLED", "1");
282    }
283}