diff --git a/CHANGELOG.md b/CHANGELOG.md index 344ddeb7fd4f..d30ce777f306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ ### Breaking +- [#5946](https://github.com/ChainSafe/forest/pull/5946) Updated parameters and response of `Forest.StateCompute` RPC method to support new `forest-cli state compute` options. + ### Added - [#5835](https://github.com/ChainSafe/forest/issues/5835) Add `--format` flag to the `forest-cli snapshot export` subcommand. This allows exporting a Filecoin snapshot in v2 format(FRC-0108). @@ -37,6 +39,10 @@ - [#5867](https://github.com/ChainSafe/forest/pull/5867) Added `--unordered` to `forest-cli snapshot export` for exporting `CAR` blocks in non-deterministic order for better performance with more parallelization. +- [#5946](https://github.com/ChainSafe/forest/pull/5946) Added `--n-epochs` to `forest-cli state compute` for computating state trees in batch. + +- [#5946](https://github.com/ChainSafe/forest/pull/5946) Added `--verbose` to `forest-cli state compute` for printing epochs and tipset keys along with state roots. + - [#5886](https://github.com/ChainSafe/forest/issues/5886) Add `forest-tool archive merge-f3` subcommand for merging a v1 Filecoin snapshot and an F3 snapshot into a v2 Filecoin snapshot. - [#4976](https://github.com/ChainSafe/forest/issues/4976) Add support for the `Filecoin.EthSubscribe` and `Filecoin.EthUnsubscribe` API methods to enable subscriptions to Ethereum event types: `heads` and `logs`. diff --git a/src/chain_sync/chain_follower.rs b/src/chain_sync/chain_follower.rs index b59b5619caed..6e4b05a0fb89 100644 --- a/src/chain_sync/chain_follower.rs +++ b/src/chain_sync/chain_follower.rs @@ -446,7 +446,7 @@ pub async fn get_full_tipset( .chain_exchange_full_tipset(peer_id, tipset_keys) .await .map_err(|e| anyhow::anyhow!(e))?; - tipset.persist(&chain_store.db)?; + tipset.persist(chain_store.blockstore())?; Ok(tipset) } @@ -469,7 +469,7 @@ async fn get_full_tipset_batch( for tipset in tipsets.iter() { for block in tipset.blocks() { - block.persist(&chain_store.db)?; + block.persist(chain_store.blockstore())?; } } diff --git a/src/cli/subcommands/state_cmd.rs b/src/cli/subcommands/state_cmd.rs index f223e379d4e1..b922d589eac0 100644 --- a/src/cli/subcommands/state_cmd.rs +++ b/src/cli/subcommands/state_cmd.rs @@ -2,12 +2,13 @@ // SPDX-License-Identifier: Apache-2.0, MIT use crate::lotus_json::HasLotusJson; -use crate::rpc::state::ForestStateCompute; +use crate::rpc::state::{ForestComputeStateOutput, ForestStateCompute}; use crate::rpc::{self, prelude::*}; use crate::shim::address::{CurrentNetwork, Error, Network, StrictAddress}; use crate::shim::clock::ChainEpoch; use cid::Cid; use clap::Subcommand; +use std::num::NonZeroUsize; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; @@ -20,10 +21,17 @@ pub enum StateCommands { #[arg(short, long)] save_to_file: Option, }, + /// Compute state trees for epochs Compute { /// Which epoch to compute the state transition for #[arg(long)] epoch: ChainEpoch, + /// Number of tipset epochs to compute state for. Default is 1 + #[arg(short, long)] + n_epochs: Option, + /// Print epoch and tipset key along with state root + #[arg(short, long)] + verbose: bool, }, /// Read the state of an actor ReadState { @@ -43,11 +51,28 @@ impl StateCommands { .await?; println!("{ret}"); } - StateCommands::Compute { epoch } => { - let ret = client - .call(ForestStateCompute::request((epoch,))?.with_timeout(Duration::MAX)) + StateCommands::Compute { + epoch, + n_epochs, + verbose, + } => { + let results = client + .call( + ForestStateCompute::request((epoch, n_epochs))?.with_timeout(Duration::MAX), + ) .await?; - println!("{ret}"); + for ForestComputeStateOutput { + state_root, + epoch, + tipset_key, + } in results + { + if verbose { + println!("{state_root} (epoch: {epoch}, tipset key: {tipset_key})"); + } else { + println!("{state_root}"); + } + } } Self::ReadState { actor_address } => { let tipset = ChainHead::call(&client, ()).await?; diff --git a/src/lotus_json/mod.rs b/src/lotus_json/mod.rs index 6d2d35c56ec2..12adf9de4b63 100644 --- a/src/lotus_json/mod.rs +++ b/src/lotus_json/mod.rs @@ -526,6 +526,7 @@ lotus_json_with_self!( DeadlineInfo, PaddedPieceSize, Uuid, + std::num::NonZeroUsize, ); // TODO(forest): https://github.com/ChainSafe/forest/issues/4032 diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 9ba1580459e5..071d94db9d93 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT mod types; +use futures::stream::FuturesOrdered; pub use types::*; use crate::blocks::{Tipset, TipsetKey}; @@ -68,6 +69,7 @@ use nunny::vec as nonempty; use parking_lot::Mutex; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::num::NonZeroUsize; use std::ops::Mul; use std::path::PathBuf; use std::{sync::Arc, time::Duration}; @@ -1419,41 +1421,68 @@ impl RpcMethod<2> for StateFetchRoot { pub enum ForestStateCompute {} -impl RpcMethod<1> for ForestStateCompute { +impl RpcMethod<2> for ForestStateCompute { const NAME: &'static str = "Forest.StateCompute"; - const PARAM_NAMES: [&'static str; 1] = ["epoch"]; + const N_REQUIRED_PARAMS: usize = 1; + const PARAM_NAMES: [&'static str; 2] = ["epoch", "n_epochs"]; const API_PATHS: BitFlags = ApiPaths::all(); const PERMISSION: Permission = Permission::Read; - type Params = (ChainEpoch,); - type Ok = Cid; + type Params = (ChainEpoch, Option); + type Ok = Vec; async fn handle( ctx: Ctx, - (epoch,): Self::Params, + (from_epoch, n_epochs): Self::Params, ) -> Result { - let ts = ctx.chain_index().tipset_by_height( - epoch, + let n_epochs = n_epochs.map(|n| n.get()).unwrap_or(1) as ChainEpoch; + let to_epoch = from_epoch + n_epochs - 1; + let to_ts = ctx.chain_index().tipset_by_height( + to_epoch, ctx.chain_store().heaviest_tipset(), ResolveNullTipset::TakeOlder, )?; - // Attempt to load full tipset from the store - if crate::chain_sync::load_full_tipset(ctx.chain_store(), ts.key()).is_err() { - // Load full tipset from the network - let fts = ctx - .sync_network_context - .chain_exchange_messages(None, &ts) - .await - .map_err(|e| anyhow::anyhow!(e))?; - fts.persist(ctx.store())?; - } + let from_ts = ctx.chain_index().tipset_by_height( + from_epoch, + to_ts.clone(), + ResolveNullTipset::TakeOlder, + )?; - let StateOutput { state_root, .. } = ctx - .state_manager - .compute_tipset_state(ts, crate::state_manager::NO_CALLBACK, VMTrace::NotTraced) - .await?; + let mut futures = FuturesOrdered::new(); + for ts in to_ts + .chain_arc(ctx.store()) + .take_while(|ts| ts.epoch() >= from_ts.epoch()) + { + let chain_store = ctx.chain_store().clone(); + let network_context = ctx.sync_network_context.clone(); + futures.push_front(async move { + if crate::chain_sync::load_full_tipset(&chain_store, ts.key()).is_err() { + // Backfill full tipset from the network + let fts = network_context + .chain_exchange_messages(None, &ts) + .await + .map_err(|e| anyhow::anyhow!(e))?; + fts.persist(chain_store.blockstore())?; + } + anyhow::Ok(ts) + }); + } - Ok(state_root) + let mut results = Vec::with_capacity(n_epochs as _); + while let Some(Ok(ts)) = futures.next().await { + let epoch = ts.epoch(); + let tipset_key = ts.key().clone(); + let StateOutput { state_root, .. } = ctx + .state_manager + .compute_tipset_state(ts, crate::state_manager::NO_CALLBACK, VMTrace::NotTraced) + .await?; + results.push(ForestComputeStateOutput { + state_root, + epoch, + tipset_key, + }); + } + Ok(results) } } diff --git a/src/rpc/methods/state/types.rs b/src/rpc/methods/state/types.rs index c017f1c274f6..36cd381a1273 100644 --- a/src/rpc/methods/state/types.rs +++ b/src/rpc/methods/state/types.rs @@ -1,6 +1,7 @@ // Copyright 2019-2025 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use crate::blocks::TipsetKey; use crate::lotus_json::{LotusJson, lotus_json_with_self}; use crate::message::Message as _; use crate::shim::executor::ApplyRet; @@ -32,6 +33,20 @@ pub struct ComputeStateOutput { lotus_json_with_self!(ComputeStateOutput); +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct ForestComputeStateOutput { + #[schemars(with = "LotusJson")] + #[serde(with = "crate::lotus_json")] + pub state_root: Cid, + pub epoch: ChainEpoch, + #[schemars(with = "LotusJson")] + #[serde(with = "crate::lotus_json")] + pub tipset_key: TipsetKey, +} + +lotus_json_with_self!(ForestComputeStateOutput); + #[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] #[serde(rename_all = "PascalCase")] pub struct ApiInvocResult {