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