diff --git a/CHANGELOG.md b/CHANGELOG.md index 64d043f3f82c..91748a9af05b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ ### Changed +- [#6471](https://github.com/ChainSafe/forest/pull/6471) Moved `forest-tool state` subcommand to `forest-dev`. + ### Removed ### Fixed diff --git a/docs/docs/users/reference/cli.sh b/docs/docs/users/reference/cli.sh index a9874808eeae..74d9d2f08683 100755 --- a/docs/docs/users/reference/cli.sh +++ b/docs/docs/users/reference/cli.sh @@ -105,10 +105,6 @@ generate_markdown_section "forest-tool" "benchmark graph-traversal" generate_markdown_section "forest-tool" "benchmark forest-encoding" generate_markdown_section "forest-tool" "benchmark export" -generate_markdown_section "forest-tool" "state" -generate_markdown_section "forest-tool" "state compute" -generate_markdown_section "forest-tool" "state replay-compute" - generate_markdown_section "forest-tool" "state-migration" generate_markdown_section "forest-tool" "state-migration actor-bundle" @@ -158,3 +154,13 @@ generate_markdown_section "forest-tool" "shed openrpc" generate_markdown_section "forest-tool" "index" generate_markdown_section "forest-tool" "index backfill" + +generate_markdown_section "forest-dev" "" + +generate_markdown_section "forest-dev" "fetch-rpc-tests" + +generate_markdown_section "forest-dev" "state" +generate_markdown_section "forest-dev" "state compute" +generate_markdown_section "forest-dev" "state replay-compute" +generate_markdown_section "forest-dev" "state validate" +generate_markdown_section "forest-dev" "state replay-validate" diff --git a/src/chain_sync/chain_follower.rs b/src/chain_sync/chain_follower.rs index 5e15146daefa..7bbdbd8147cb 100644 --- a/src/chain_sync/chain_follower.rs +++ b/src/chain_sync/chain_follower.rs @@ -828,7 +828,6 @@ impl SyncTask { bad_block_cache: Option>, ) -> Option { tracing::trace!("SyncTask::execute {self}"); - let cs = state_manager.chain_store(); match self { SyncTask::ValidateTipset { tipset, @@ -840,29 +839,19 @@ impl SyncTask { SyncTask::ValidateTipset { tipset, is_proposed_head, - } => { - let genesis = cs.genesis_tipset(); - match validate_tipset( - &state_manager, - cs, - tipset.clone(), - &genesis, - bad_block_cache, - ) - .await - { - Ok(()) => Some(SyncEvent::ValidatedTipset { - tipset, - is_proposed_head, - }), - Err(e) => { - warn!("Error validating tipset: {}", e); - Some(SyncEvent::BadTipset(tipset)) - } + } => match validate_tipset(&state_manager, tipset.clone(), bad_block_cache).await { + Ok(()) => Some(SyncEvent::ValidatedTipset { + tipset, + is_proposed_head, + }), + Err(e) => { + warn!("Error validating tipset: {e}"); + Some(SyncEvent::BadTipset(tipset)) } - } + }, SyncTask::FetchTipset(key, epoch) => { - match get_full_tipset_batch(&network, cs, None, &key).await { + match get_full_tipset_batch(&network, state_manager.chain_store(), None, &key).await + { Ok(parents) => Some(SyncEvent::NewFullTipsets(parents)), Err(e) => { tracing::warn!(%key, %epoch, "failed to fetch tipset: {e}"); diff --git a/src/chain_sync/mod.rs b/src/chain_sync/mod.rs index 998a5835c965..dcc13a2df287 100644 --- a/src/chain_sync/mod.rs +++ b/src/chain_sync/mod.rs @@ -8,7 +8,7 @@ pub mod consensus; pub mod metrics; pub mod network_context; mod sync_status; -mod tipset_syncer; +pub(crate) mod tipset_syncer; mod validation; pub use self::{ diff --git a/src/chain_sync/tipset_syncer.rs b/src/chain_sync/tipset_syncer.rs index fe2d321f05e6..76a2d7160506 100644 --- a/src/chain_sync/tipset_syncer.rs +++ b/src/chain_sync/tipset_syncer.rs @@ -99,12 +99,13 @@ impl TipsetSyncerError { /// validation. pub async fn validate_tipset( state_manager: &Arc>, - chainstore: &ChainStore, full_tipset: FullTipset, - genesis: &Tipset, bad_block_cache: Option>, ) -> Result<(), TipsetSyncerError> { - if full_tipset.key().eq(genesis.key()) { + if full_tipset + .key() + .eq(state_manager.chain_store().genesis_tipset().key()) + { trace!("Skipping genesis tipset validation"); return Ok(()); } @@ -123,7 +124,9 @@ pub async fn validate_tipset( while let Some(result) = validations.join_next().await { match result? { Ok(block) => { - chainstore.add_to_tipset_tracker(block.header()); + state_manager + .chain_store() + .add_to_tipset_tracker(block.header()); } Err((cid, why)) => { warn!("Validating block [CID = {cid}] in EPOCH = {epoch} failed: {why}"); diff --git a/src/dev/subcommands/mod.rs b/src/dev/subcommands/mod.rs index 846fc34d70bb..bb9dfa34854f 100644 --- a/src/dev/subcommands/mod.rs +++ b/src/dev/subcommands/mod.rs @@ -1,6 +1,8 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +mod state_cmd; + use crate::cli_shared::cli::HELP_MESSAGE; use crate::rpc::Client; use crate::utils::net::{DownloadFileOption, download_file_with_cache}; @@ -30,12 +32,15 @@ pub struct Cli { pub enum Subcommand { /// Fetch RPC test snapshots to the local cache FetchRpcTests, + #[command(subcommand)] + State(state_cmd::StateCommand), } impl Subcommand { pub async fn run(self, _client: Client) -> anyhow::Result<()> { match self { Self::FetchRpcTests => fetch_rpc_tests().await, + Self::State(cmd) => cmd.run().await, } } } diff --git a/src/dev/subcommands/state_cmd.rs b/src/dev/subcommands/state_cmd.rs new file mode 100644 index 000000000000..f68c375a9c35 --- /dev/null +++ b/src/dev/subcommands/state_cmd.rs @@ -0,0 +1,302 @@ +// Copyright 2019-2026 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::{ + blocks::Tipset, + chain::{ChainStore, index::ResolveNullTipset}, + chain_sync::{load_full_tipset, tipset_syncer::validate_tipset}, + cli_shared::{chain_path, read_config}, + db::{ + MemoryDB, SettingsStoreExt, + car::{AnyCar, ManyCar}, + db_engine::db_root, + }, + genesis::read_genesis_header, + interpreter::VMTrace, + networks::{ChainConfig, NetworkChain}, + shim::clock::ChainEpoch, + state_manager::{StateManager, StateOutput}, + tool::subcommands::api_cmd::generate_test_snapshot, +}; +use nonzero_ext::nonzero; +use std::{num::NonZeroUsize, path::PathBuf, sync::Arc, time::Instant}; + +/// Interact with Filecoin chain state +#[derive(Debug, clap::Subcommand)] +pub enum StateCommand { + Compute(ComputeCommand), + ReplayCompute(ReplayComputeCommand), + Validate(ValidateCommand), + ReplayValidate(ReplayValidateCommand), +} + +impl StateCommand { + pub async fn run(self) -> anyhow::Result<()> { + match self { + Self::Compute(cmd) => cmd.run().await, + Self::ReplayCompute(cmd) => cmd.run().await, + Self::Validate(cmd) => cmd.run().await, + Self::ReplayValidate(cmd) => cmd.run().await, + } + } +} + +/// Compute state tree for an epoch +#[derive(Debug, clap::Args)] +pub struct ComputeCommand { + /// Which epoch to compute the state transition for + #[arg(long, required = true)] + epoch: ChainEpoch, + /// Filecoin network chain + #[arg(long, required = true)] + chain: NetworkChain, + /// Optional path to the database folder + #[arg(long)] + db: Option, + /// Optional path to the database snapshot `CAR` file to write to for reproducing the computation + #[arg(long)] + export_db_to: Option, +} + +impl ComputeCommand { + pub async fn run(self) -> anyhow::Result<()> { + let Self { + epoch, + chain, + db, + export_db_to, + } = self; + disable_tipset_cache(); + let db_root_path = if let Some(db) = db { + db + } else { + let (_, config) = read_config(None, Some(chain.clone()))?; + db_root(&chain_path(&config))? + }; + let db = generate_test_snapshot::load_db(&db_root_path)?; + let chain_config = Arc::new(ChainConfig::from_chain(&chain)); + let genesis_header = + read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db) + .await?; + let chain_store = Arc::new(ChainStore::new( + db.clone(), + db.clone(), + db.clone(), + chain_config, + genesis_header, + )?); + let ts = { + // We don't want to track all entries that are visited by `tipset_by_height` + db.pause_tracking(); + let ts = chain_store.chain_index().tipset_by_height( + epoch, + chain_store.heaviest_tipset(), + ResolveNullTipset::TakeOlder, + )?; + db.resume_tracking(); + SettingsStoreExt::write_obj(&db.tracker, crate::db::setting_keys::HEAD_KEY, ts.key())?; + // Only track the desired tipset + Tipset::load_required(&db, ts.key())? + }; + let epoch = ts.epoch(); + let state_manager = Arc::new(StateManager::new(chain_store)?); + + let StateOutput { + state_root, + receipt_root, + .. + } = state_manager + .compute_tipset_state(ts, crate::state_manager::NO_CALLBACK, VMTrace::NotTraced) + .await?; + let mut db_snapshot = vec![]; + db.export_forest_car(&mut db_snapshot).await?; + println!( + "epoch: {epoch}, state_root: {state_root}, receipt_root: {receipt_root}, db_snapshot_size: {}", + human_bytes::human_bytes(db_snapshot.len() as f64) + ); + if let Some(export_db_to) = export_db_to { + std::fs::write(export_db_to, db_snapshot)?; + } + Ok(()) + } +} + +/// Replay state computation with a db snapshot +/// To be used in conjunction with `forest-dev state compute`. +#[derive(Debug, clap::Args)] +pub struct ReplayComputeCommand { + /// Path to the database snapshot `CAR` file generated by `forest-dev state compute` + snapshot: PathBuf, + /// Filecoin network chain + #[arg(long, required = true)] + chain: NetworkChain, + /// Number of times to repeat the state computation + #[arg(short, long, default_value_t = nonzero!(1usize))] + n: NonZeroUsize, +} + +impl ReplayComputeCommand { + pub async fn run(self) -> anyhow::Result<()> { + let Self { snapshot, chain, n } = self; + let snap_car = AnyCar::try_from(&snapshot)?; + let ts = snap_car.heaviest_tipset()?; + let epoch = ts.epoch(); + let db = Arc::new(ManyCar::new(MemoryDB::default()).with_read_only(snap_car)?); + let chain_config = Arc::new(ChainConfig::from_chain(&chain)); + let genesis_header = + read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db) + .await?; + let chain_store = Arc::new(ChainStore::new( + db.clone(), + db.clone(), + db.clone(), + chain_config, + genesis_header, + )?); + let state_manager = Arc::new(StateManager::new(chain_store)?); + for _ in 0..n.get() { + let start = Instant::now(); + let StateOutput { + state_root, + receipt_root, + .. + } = state_manager + .compute_tipset_state( + ts.clone(), + crate::state_manager::NO_CALLBACK, + VMTrace::NotTraced, + ) + .await?; + println!( + "epoch: {epoch}, state_root: {state_root}, receipt_root: {receipt_root}, took {}.", + humantime::format_duration(start.elapsed()) + ); + } + Ok(()) + } +} + +/// Validate tipset at a certain epoch +#[derive(Debug, clap::Args)] +pub struct ValidateCommand { + /// Tipset epoch to validate + #[arg(long, required = true)] + epoch: ChainEpoch, + /// Filecoin network chain + #[arg(long, required = true)] + chain: NetworkChain, + /// Optional path to the database folder + #[arg(long)] + db: Option, + /// Optional path to the database snapshot `CAR` file to write to for reproducing the computation + #[arg(long)] + export_db_to: Option, +} + +impl ValidateCommand { + pub async fn run(self) -> anyhow::Result<()> { + let Self { + epoch, + chain, + db, + export_db_to, + } = self; + disable_tipset_cache(); + let db_root_path = if let Some(db) = db { + db + } else { + let (_, config) = read_config(None, Some(chain.clone()))?; + db_root(&chain_path(&config))? + }; + let db = generate_test_snapshot::load_db(&db_root_path)?; + let chain_config = Arc::new(ChainConfig::from_chain(&chain)); + let genesis_header = + read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db) + .await?; + let chain_store = Arc::new(ChainStore::new( + db.clone(), + db.clone(), + db.clone(), + chain_config, + genesis_header, + )?); + let ts = { + // We don't want to track all entries that are visited by `tipset_by_height` + db.pause_tracking(); + let ts = chain_store.chain_index().tipset_by_height( + epoch, + chain_store.heaviest_tipset(), + ResolveNullTipset::TakeOlder, + )?; + db.resume_tracking(); + SettingsStoreExt::write_obj(&db.tracker, crate::db::setting_keys::HEAD_KEY, ts.key())?; + // Only track the desired tipset + Tipset::load_required(&db, ts.key())? + }; + let epoch = ts.epoch(); + let fts = load_full_tipset(&chain_store, ts.key())?; + let state_manager = Arc::new(StateManager::new(chain_store)?); + validate_tipset(&state_manager, fts, None).await?; + let mut db_snapshot = vec![]; + db.export_forest_car(&mut db_snapshot).await?; + println!( + "epoch: {epoch}, db_snapshot_size: {}", + human_bytes::human_bytes(db_snapshot.len() as f64) + ); + if let Some(export_db_to) = export_db_to { + std::fs::write(export_db_to, db_snapshot)?; + } + Ok(()) + } +} + +/// Replay tipset validation with a db snapshot +/// To be used in conjunction with `forest-dev state validate`. +#[derive(Debug, clap::Args)] +pub struct ReplayValidateCommand { + /// Path to the database snapshot `CAR` file generated by `forest-dev state validate` + snapshot: PathBuf, + /// Filecoin network chain + #[arg(long, required = true)] + chain: NetworkChain, + /// Number of times to repeat the state computation + #[arg(short, long, default_value_t = nonzero!(1usize))] + n: NonZeroUsize, +} + +impl ReplayValidateCommand { + pub async fn run(self) -> anyhow::Result<()> { + let Self { snapshot, chain, n } = self; + let snap_car = AnyCar::try_from(&snapshot)?; + let ts = snap_car.heaviest_tipset()?; + let epoch = ts.epoch(); + let db = Arc::new(ManyCar::new(MemoryDB::default()).with_read_only(snap_car)?); + let chain_config = Arc::new(ChainConfig::from_chain(&chain)); + let genesis_header = + read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db) + .await?; + let chain_store = Arc::new(ChainStore::new( + db.clone(), + db.clone(), + db.clone(), + chain_config, + genesis_header, + )?); + let state_manager = Arc::new(StateManager::new(chain_store)?); + let fts = load_full_tipset(state_manager.chain_store(), ts.key())?; + for _ in 0..n.get() { + let fts = fts.clone(); + let start = Instant::now(); + validate_tipset(&state_manager, fts, None).await?; + println!( + "epoch: {epoch}, took {}.", + humantime::format_duration(start.elapsed()) + ); + } + Ok(()) + } +} + +fn disable_tipset_cache() { + unsafe { std::env::set_var("FOREST_TIPSET_CACHE_DISABLED", "1") }; +} diff --git a/src/state_manager/utils.rs b/src/state_manager/utils.rs index a986a0bb16a6..0576bfdae92c 100644 --- a/src/state_manager/utils.rs +++ b/src/state_manager/utils.rs @@ -212,9 +212,17 @@ pub mod state_compute { pub async fn get_state_compute_snapshot( chain: &NetworkChain, epoch: i64, + ) -> anyhow::Result { + get_state_snapshot(chain, "state_compute", epoch).await + } + + pub async fn get_state_snapshot( + chain: &NetworkChain, + bucket: &str, + epoch: i64, ) -> anyhow::Result { let url = Url::parse(&format!( - "https://forest-snapshots.fra1.cdn.digitaloceanspaces.com/state_compute/{chain}_{epoch}.forest.car.zst" + "https://forest-snapshots.fra1.cdn.digitaloceanspaces.com/{bucket}/{chain}_{epoch}.forest.car.zst" ))?; Ok(crate::utils::retry( crate::utils::RetryArgs { @@ -270,6 +278,26 @@ pub mod state_compute { #[cfg(test)] mod tests { use super::*; + use crate::{ + blocks::FullTipset, + chain_sync::{load_full_tipset, tipset_syncer::validate_tipset}, + }; + + pub async fn get_state_validate_snapshot( + chain: &NetworkChain, + epoch: i64, + ) -> anyhow::Result { + get_state_snapshot(chain, "state_validate", epoch).await + } + + pub async fn prepare_state_validate( + chain: &NetworkChain, + snapshot: &Path, + ) -> anyhow::Result<(Arc>, FullTipset)> { + let (sm, ts) = prepare_state_compute(chain, snapshot, false).await?; + let fts = load_full_tipset(sm.chain_store(), ts.key())?; + Ok((sm, fts)) + } #[tokio::test(flavor = "multi_thread")] async fn state_compute_calibnet_3111900() { @@ -305,14 +333,22 @@ pub mod state_compute { } #[tokio::test(flavor = "multi_thread")] - async fn state_compute_mainnet_5427431() { + async fn state_compute_mainnet_5688000() { let chain = NetworkChain::Mainnet; - let snapshot = get_state_compute_snapshot(&chain, 5427431).await.unwrap(); + let snapshot = get_state_compute_snapshot(&chain, 5688000).await.unwrap(); let (sm, ts) = prepare_state_compute(&chain, &snapshot, false) .await .unwrap(); state_compute(sm, ts).await; } + + #[tokio::test(flavor = "multi_thread")] + async fn state_validate_mainnet_5688000() { + let chain = NetworkChain::Mainnet; + let snapshot = get_state_validate_snapshot(&chain, 5688000).await.unwrap(); + let (sm, fts) = prepare_state_validate(&chain, &snapshot).await.unwrap(); + validate_tipset(&sm, fts, None).await.unwrap(); + } } } diff --git a/src/tool/main.rs b/src/tool/main.rs index 8a2119a4ed70..d27e6c134bf2 100644 --- a/src/tool/main.rs +++ b/src/tool/main.rs @@ -32,7 +32,6 @@ where Subcommand::Api(cmd) => cmd.run().await, Subcommand::Net(cmd) => cmd.run().await, Subcommand::Shed(cmd) => cmd.run(client).await, - Subcommand::State(cmd) => cmd.run().await, Subcommand::Completion(cmd) => cmd.run(&mut std::io::stdout()), } } diff --git a/src/tool/subcommands/api_cmd.rs b/src/tool/subcommands/api_cmd.rs index f51ce4aa5f66..065d2ebc0273 100644 --- a/src/tool/subcommands/api_cmd.rs +++ b/src/tool/subcommands/api_cmd.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT mod api_compare_tests; -pub(super) mod generate_test_snapshot; +pub(crate) mod generate_test_snapshot; mod report; mod state_decode_params_tests; mod stateful_tests; diff --git a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs index 8425627f8384..46eb1baa09af 100644 --- a/src/tool/subcommands/api_cmd/generate_test_snapshot.rs +++ b/src/tool/subcommands/api_cmd/generate_test_snapshot.rs @@ -26,6 +26,7 @@ use fvm_shared4::address::Network; use openrpc_types::ParamStructure; use parking_lot::RwLock; use rpc::{RPCState, RpcMethod as _, eth::filter::EthEventHandler}; +use std::sync::atomic::{AtomicBool, Ordering}; use tokio::{sync::mpsc, task::JoinSet}; pub async fn run_test_with_dump( @@ -154,6 +155,21 @@ async fn ctx( pub struct ReadOpsTrackingStore { inner: T, pub tracker: Arc, + tracking: AtomicBool, +} + +impl ReadOpsTrackingStore { + pub fn resume_tracking(&self) { + self.tracking.store(true, Ordering::Relaxed); + } + + pub fn pause_tracking(&self) { + self.tracking.store(false, Ordering::Relaxed); + } + + fn tracking(&self) -> bool { + self.tracking.load(Ordering::Relaxed) + } } impl ReadOpsTrackingStore @@ -185,6 +201,7 @@ where Self { inner, tracker: Arc::new(Default::default()), + tracking: AtomicBool::new(true), } } @@ -209,7 +226,9 @@ impl HeaviestTipsetKeyProvider for ReadOpsTracking impl Blockstore for ReadOpsTrackingStore { fn get(&self, k: &Cid) -> anyhow::Result>> { let result = self.inner.get(k)?; - if let Some(v) = &result { + if self.tracking() + && let Some(v) = &result + { self.tracker.put_keyed(k, v.as_slice())?; } Ok(result) @@ -223,7 +242,9 @@ impl Blockstore for ReadOpsTrackingStore { impl SettingsStore for ReadOpsTrackingStore { fn read_bin(&self, key: &str) -> anyhow::Result>> { let result = self.inner.read_bin(key)?; - if let Some(v) = &result { + if self.tracking() + && let Some(v) = &result + { SettingsStore::write_bin(&self.tracker, key, v.as_slice())?; } Ok(result) @@ -235,7 +256,9 @@ impl SettingsStore for ReadOpsTrackingStore { fn exists(&self, key: &str) -> anyhow::Result { let result = self.inner.read_bin(key)?; - if let Some(v) = &result { + if self.tracking() + && let Some(v) = &result + { SettingsStore::write_bin(&self.tracker, key, v.as_slice())?; } Ok(result.is_some()) @@ -249,7 +272,9 @@ impl SettingsStore for ReadOpsTrackingStore { impl BitswapStoreRead for ReadOpsTrackingStore { fn contains(&self, cid: &Cid) -> anyhow::Result { let result = self.inner.get(cid)?; - if let Some(v) = &result { + if self.tracking() + && let Some(v) = &result + { Blockstore::put_keyed(&self.tracker, cid, v.as_slice())?; } Ok(result.is_some()) @@ -257,7 +282,9 @@ impl BitswapStoreRead for ReadOpsTrackingStore { fn get(&self, cid: &Cid) -> anyhow::Result>> { let result = self.inner.get(cid)?; - if let Some(v) = &result { + if self.tracking() + && let Some(v) = &result + { Blockstore::put_keyed(&self.tracker, cid, v.as_slice())?; } Ok(result) @@ -275,10 +302,12 @@ impl BitswapStoreReadWrite for ReadOpsTrackingStore impl EthMappingsStore for ReadOpsTrackingStore { fn read_bin(&self, key: &EthHash) -> anyhow::Result>> { let result = self.inner.read_bin(key)?; - if let Some(v) = &result { + if self.tracking() + && let Some(v) = &result + { EthMappingsStore::write_bin(&self.tracker, key, v.as_slice())?; } - self.inner.read_bin(key) + Ok(result) } fn write_bin(&self, key: &EthHash, value: &[u8]) -> anyhow::Result<()> { diff --git a/src/tool/subcommands/mod.rs b/src/tool/subcommands/mod.rs index 9a6227805472..95000fc3a5e9 100644 --- a/src/tool/subcommands/mod.rs +++ b/src/tool/subcommands/mod.rs @@ -1,7 +1,7 @@ // Copyright 2019-2026 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT -mod api_cmd; +pub(crate) mod api_cmd; pub(crate) mod archive_cmd; mod backup_cmd; mod benchmark_cmd; @@ -12,7 +12,6 @@ mod index_cmd; mod net_cmd; mod shed_cmd; mod snapshot_cmd; -mod state_compute_cmd; mod state_migration_cmd; use crate::cli_shared::cli::*; @@ -82,8 +81,5 @@ pub enum Subcommand { #[command(subcommand)] Shed(shed_cmd::ShedCommands), - #[command(subcommand)] - State(state_compute_cmd::StateCommand), - Completion(CompletionCommand), } diff --git a/src/tool/subcommands/state_compute_cmd.rs b/src/tool/subcommands/state_compute_cmd.rs deleted file mode 100644 index 3123e25e3243..000000000000 --- a/src/tool/subcommands/state_compute_cmd.rs +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2019-2026 ChainSafe Systems -// SPDX-License-Identifier: Apache-2.0, MIT - -use crate::{ - chain::{ChainStore, index::ResolveNullTipset}, - cli_shared::{chain_path, read_config}, - db::{ - MemoryDB, SettingsStoreExt, - car::{AnyCar, ManyCar}, - db_engine::db_root, - }, - genesis::read_genesis_header, - interpreter::VMTrace, - networks::{ChainConfig, NetworkChain}, - shim::clock::ChainEpoch, - state_manager::{StateManager, StateOutput}, -}; -use nonzero_ext::nonzero; -use std::{num::NonZeroUsize, path::PathBuf, sync::Arc, time::Instant}; - -/// Interact with Filecoin chain state -#[derive(Debug, clap::Subcommand)] -pub enum StateCommand { - Compute(ComputeCommand), - ReplayCompute(ReplayComputeCommand), -} - -impl StateCommand { - pub async fn run(self) -> anyhow::Result<()> { - match self { - Self::Compute(cmd) => cmd.run().await, - Self::ReplayCompute(cmd) => cmd.run().await, - } - } -} - -/// Compute state tree for an epoch -#[derive(Debug, clap::Args)] -pub struct ComputeCommand { - /// Which epoch to compute the state transition for - #[arg(long, required = true)] - epoch: ChainEpoch, - /// Filecoin network chain - #[arg(long, required = true)] - chain: NetworkChain, - /// Optional path to the database folder - #[arg(long)] - db: Option, - /// Optional path to the database snapshot `CAR` file to write to for reproducing the computation - #[arg(long)] - export_db_to: Option, -} - -impl ComputeCommand { - pub async fn run(self) -> anyhow::Result<()> { - let Self { - epoch, - chain, - db, - export_db_to, - } = self; - let db_root_path = if let Some(db) = db { - db - } else { - let (_, config) = read_config(None, Some(chain.clone()))?; - db_root(&chain_path(&config))? - }; - let db = super::api_cmd::generate_test_snapshot::load_db(&db_root_path)?; - let chain_config = Arc::new(ChainConfig::from_chain(&chain)); - let genesis_header = - read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db) - .await?; - let chain_store = Arc::new(ChainStore::new( - db.clone(), - db.clone(), - db.clone(), - chain_config, - genesis_header, - )?); - let ts = chain_store.chain_index().tipset_by_height( - epoch, - chain_store.heaviest_tipset(), - ResolveNullTipset::TakeOlder, - )?; - let epoch = ts.epoch(); - SettingsStoreExt::write_obj(&db.tracker, crate::db::setting_keys::HEAD_KEY, ts.key())?; - let state_manager = Arc::new(StateManager::new(chain_store.clone())?); - - let StateOutput { - state_root, - receipt_root, - .. - } = state_manager - .compute_tipset_state(ts, crate::state_manager::NO_CALLBACK, VMTrace::NotTraced) - .await?; - let mut db_snapshot = vec![]; - db.export_forest_car(&mut db_snapshot).await?; - println!( - "epoch: {epoch}, state_root: {state_root}, receipt_root: {receipt_root}, db_snapshot_size: {}", - human_bytes::human_bytes(db_snapshot.len() as f64) - ); - if let Some(export_db_to) = export_db_to { - std::fs::write(export_db_to, db_snapshot)?; - } - Ok(()) - } -} - -/// Replay state computation with a db snapshot -/// To be used in conjunction with `forest-tool state compute`. -#[derive(Debug, clap::Args)] -pub struct ReplayComputeCommand { - /// Path to the database snapshot `CAR` file generated by `forest-tool state compute` - snapshot: PathBuf, - /// Filecoin network chain - #[arg(long, required = true)] - chain: NetworkChain, - /// Number of times to repeat the state computation - #[arg(short, long, default_value_t = nonzero!(1usize))] - n: NonZeroUsize, -} - -impl ReplayComputeCommand { - pub async fn run(self) -> anyhow::Result<()> { - let Self { snapshot, chain, n } = self; - let snap_car = AnyCar::try_from(&snapshot)?; - let ts = snap_car.heaviest_tipset()?; - let epoch = ts.epoch(); - let db = Arc::new(ManyCar::new(MemoryDB::default()).with_read_only(snap_car)?); - let chain_config = Arc::new(ChainConfig::from_chain(&chain)); - let genesis_header = - read_genesis_header(None, chain_config.genesis_bytes(&db).await?.as_deref(), &db) - .await?; - let chain_store = Arc::new(ChainStore::new( - db.clone(), - db.clone(), - db.clone(), - chain_config, - genesis_header, - )?); - let state_manager = Arc::new(StateManager::new(chain_store.clone())?); - for _ in 0..n.get() { - let start = Instant::now(); - let StateOutput { - state_root, - receipt_root, - .. - } = state_manager - .compute_tipset_state( - ts.clone(), - crate::state_manager::NO_CALLBACK, - VMTrace::NotTraced, - ) - .await?; - println!( - "epoch: {epoch}, state_root: {state_root}, receipt_root: {receipt_root}, took {}.", - humantime::format_duration(start.elapsed()) - ); - } - Ok(()) - } -}