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