Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also a breaking change to the Forest.StateCompute API, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also a breaking change to the Forest.StateCompute API, no?

It is but as I understand Forest.* are internal RPC methods for CLI tools that we don't garantee backward compatibility.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't guarantee it, but it's still worth mentioning in case someone relied on it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LesnyRumcajs Changelog updated.


- [#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`.
Expand Down
4 changes: 2 additions & 2 deletions src/chain_sync/chain_follower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ pub async fn get_full_tipset<DB: Blockstore + Sync + Send + 'static>(
.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)
}
Expand All @@ -469,7 +469,7 @@ async fn get_full_tipset_batch<DB: Blockstore + Sync + Send + 'static>(

for tipset in tipsets.iter() {
for block in tipset.blocks() {
block.persist(&chain_store.db)?;
block.persist(chain_store.blockstore())?;
}
}

Expand Down
35 changes: 30 additions & 5 deletions src/cli/subcommands/state_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,10 +21,17 @@ pub enum StateCommands {
#[arg(short, long)]
save_to_file: Option<PathBuf>,
},
/// 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<NonZeroUsize>,
/// Print epoch and tipset key along with state root
#[arg(short, long)]
verbose: bool,
},
/// Read the state of an actor
ReadState {
Expand All @@ -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?;
Expand Down
1 change: 1 addition & 0 deletions src/lotus_json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@ lotus_json_with_self!(
DeadlineInfo,
PaddedPieceSize,
Uuid,
std::num::NonZeroUsize,
);

// TODO(forest): https://github.com/ChainSafe/forest/issues/4032
Expand Down
73 changes: 51 additions & 22 deletions src/rpc/methods/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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> = ApiPaths::all();
const PERMISSION: Permission = Permission::Read;

type Params = (ChainEpoch,);
type Ok = Cid;
type Params = (ChainEpoch, Option<NonZeroUsize>);
type Ok = Vec<ForestComputeStateOutput>;

async fn handle(
ctx: Ctx<impl Blockstore + Send + Sync + 'static>,
(epoch,): Self::Params,
(from_epoch, n_epochs): Self::Params,
) -> Result<Self::Ok, ServerError> {
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 {
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

FuturesOrdered has no push_front; use push (or reverse inputs before pushing)

FuturesOrdered exposes push, not push_front. This is a compile error.

Apply this minimal fix:

-            futures.push_front(async move {
+            futures.push(async move {

Note: If you relied on push_front to reverse the output order, either:

  • collect the tipsets into a Vec, reverse() it, then push in that order, or
  • sort the results by epoch before returning. See optional comment below.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
futures.push_front(async move {
futures.push(async move {
🤖 Prompt for AI Agents
In src/rpc/methods/state.rs around line 1458, the code calls push_front on a
FuturesOrdered, which doesn't have that method and causes a compile error;
replace push_front with push, and if the original intent was to reverse output
order, either collect tipsets into a Vec and reverse() them before pushing into
FuturesOrdered or push normally and sort the final results by epoch before
returning so ordering is preserved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🌐 Web query:

futures FuturesOrdered push_front method Rust documentation

💡 Result:

The push_front method for the FuturesOrdered struct in Rust allows you to push a future to the front of the queue, making it the next future to be considered for completion by the queue[6][4]. This means that, compared to the typical FIFO behavior (where futures are pushed to the back with existing methods like push or push_back), push_front lets you prioritize a specific future such that it will be resolved and returned in order ahead of those already in the queue.

Documentation summary for FuturesOrdered::push_front:

  • Signature: pub fn push_front(&mut self, future: Fut)
  • Behavior: Submits the given future to the internal set for managing. The future is not immediately polled; instead, polling only happens when .poll_next is called on the queue[6].
  • Order: This future will be at the front of the queue and will be the next to be returned complete, maintaining ordered completion semantics even when added at the front[6][4].

Additional context:

  • If you use push_front, you can reorder priority dynamically, in contrast to normal FIFO queuing.
  • You must still call .poll_next() for the future to be polled, which triggers progress and notifications for the managed futures[6].

Comparison of Future Addition Methods:

Method Description Position in Queue
push Push to the back (deprecated) Back
push_back Push to the back Back
push_front Push to the front Front (next to complete)

This flexibility allows you to manage urgent or re-prioritized async tasks efficiently within a highly concurrent stream. If you need strict insertion order, you would typically use only push_back methods; if you want to override sequence, use push_front[6][4].

Citations:


@hanabi1224 You are absolutely correct, and I apologize for my mistake. FuturesOrdered does indeed have a push_front method, as confirmed by the documentation you linked.

The original code using futures.push_front() is correct and my suggestion to change it to push() was wrong. The use of push_front appears intentional to maintain proper ordering when processing the tipsets in the chain iteration.

Thank you for the correction and for providing the documentation link. I should have verified the API more carefully before suggesting a change.


✏️ Learnings added
Learnt from: hanabi1224
PR: ChainSafe/forest#5946
File: src/rpc/methods/state.rs:1458-1458
Timestamp: 2025-08-19T11:17:35.165Z
Learning: FuturesOrdered in the futures crate has a push_front method that pushes futures to the front of the queue, making them next to be considered for completion, in addition to the standard push/push_back methods.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: hanabi1224
PR: ChainSafe/forest#5930
File: build.rs:64-77
Timestamp: 2025-08-13T09:43:20.301Z
Learning: hanabi1224 prefers hard compile-time errors in build scripts rather than runtime safeguards or collision detection, believing it's better to fail fast and fix root causes of issues like malformed snapshot names.

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
Comment on lines +1459 to +1463
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify module path for load_full_tipset

The call uses crate::chain_sync::load_full_tipset(...). If the function is not re-exported at crate::chain_sync, the correct path may be crate::chain_sync::chain_follower::load_full_tipset. Please confirm the export.

You can quickly verify the symbol path with:


🏁 Script executed:

#!/bin/bash
# Check if load_full_tipset is re-exported at crate::chain_sync or lives under chain_follower
rg -n -C2 -g '!**/target/**' -P 'pub\s+fn\s+load_full_tipset|pub\s+use\s+.*load_full_tipset' --type=rust

Length of output: 398


Use the correct path for load_full_tipset

The load_full_tipset function lives in crate::chain_sync::chain_follower, not directly under crate::chain_sync, so the existing call will fail to compile. You can resolve this by either updating the call site or re-exporting the function.

• File: src/rpc/methods/state.rs (around lines 1459–1463)

- if crate::chain_sync::load_full_tipset(&chain_store, ts.key()).is_err() {
+ if crate::chain_sync::chain_follower::load_full_tipset(&chain_store, ts.key()).is_err() {

• (Optional) If you’d rather keep the shorter path, add this to src/chain_sync/mod.rs:

pub use chain_follower::load_full_tipset;
🤖 Prompt for AI Agents
In src/rpc/methods/state.rs around lines 1459 to 1463, the call to
load_full_tipset uses the wrong module path
(crate::chain_sync::load_full_tipset) which doesn't exist; update the call to
reference its actual location
(crate::chain_sync::chain_follower::load_full_tipset) or alternatively add a
re-export (pub use chain_follower::load_full_tipset;) in src/chain_sync/mod.rs
and keep the shorter call; change the import or path accordingly so the code
compiles.

Copy link
Copy Markdown
Contributor Author

@hanabi1224 hanabi1224 Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is pub use self::chain_follower::{ChainFollower, load_full_tipset} in src/chain_sync/mod.rs. Please no longer warn on compilation errors, as they are guarded by CI tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hanabi1224 Thank you for the clarification! You're absolutely right - I can see that load_full_tipset is properly re-exported in src/chain_sync/mod.rs, so the path crate::chain_sync::load_full_tipset is indeed correct. I apologize for the incorrect suggestion.

I've noted your preference about compilation error warnings and will focus on higher-level design and logic issues instead, trusting that CI will catch any compilation problems.


✏️ Learnings added
Learnt from: hanabi1224
PR: ChainSafe/forest#5946
File: src/rpc/methods/state.rs:1459-1463
Timestamp: 2025-08-19T11:25:56.693Z
Learning: hanabi1224 prefers that CodeRabbit should not warn about compilation errors in the Forest codebase since they are already guarded by CI tests. Focus should be on higher-level design and logic issues instead.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: hanabi1224
PR: ChainSafe/forest#5930
File: build.rs:64-77
Timestamp: 2025-08-13T09:43:20.301Z
Learning: hanabi1224 prefers hard compile-time errors in build scripts rather than runtime safeguards or collision detection, believing it's better to fail fast and fix root causes of issues like malformed snapshot names.

.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)
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/rpc/methods/state/types.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Cid>")]
#[serde(with = "crate::lotus_json")]
pub state_root: Cid,
pub epoch: ChainEpoch,
#[schemars(with = "LotusJson<TipsetKey>")]
#[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 {
Expand Down
Loading