From b5b23f07b3a7761b277b6b8f737bedcc9091a1ca Mon Sep 17 00:00:00 2001 From: Hubert Bugaj Date: Wed, 10 Sep 2025 17:18:16 +0200 Subject: [PATCH 1/4] feat: add `forest-cli state actor-cids` --- scripts/tests/calibnet_other_check.sh | 8 ++ src/cli/subcommands/state_cmd.rs | 22 ++++++ src/rpc/methods/state.rs | 108 +++++++++++++++++++++++++- src/rpc/mod.rs | 1 + src/shim/actors/builtin/system/mod.rs | 19 +++++ src/shim/machine/manifest.rs | 2 +- 6 files changed, 155 insertions(+), 5 deletions(-) diff --git a/scripts/tests/calibnet_other_check.sh b/scripts/tests/calibnet_other_check.sh index 62372fcf667f..872f1be03860 100755 --- a/scripts/tests/calibnet_other_check.sh +++ b/scripts/tests/calibnet_other_check.sh @@ -59,6 +59,14 @@ $FOREST_CLI_PATH healthcheck healthy --wait echo "Test subcommand: healthcheck ready" $FOREST_CLI_PATH healthcheck ready --wait +echo "Test subcommand: state actor-cids" +bundle_cid=$($FOREST_CLI_PATH state actor-cids --format json | jq -r '.Bundle["/"]') +manifest_path="./build/manifest.json" +if ! grep -q "$bundle_cid" "$manifest_path"; then + echo "Bundle CID $bundle_cid not found in $manifest_path" + exit 1 +fi + echo "Regression testing mempool select" gem install http --user-install $FOREST_CLI_PATH chain head --format json -n 1000 | scripts/mpool_select_killer.rb diff --git a/src/cli/subcommands/state_cmd.rs b/src/cli/subcommands/state_cmd.rs index 3318469ea628..273cffc4e489 100644 --- a/src/cli/subcommands/state_cmd.rs +++ b/src/cli/subcommands/state_cmd.rs @@ -12,6 +12,12 @@ use std::num::NonZeroUsize; use std::path::PathBuf; use std::time::Duration; +#[derive(Debug, Clone, clap::ValueEnum)] +pub enum Format { + Json, + Text, +} + #[derive(Debug, Subcommand)] pub enum StateCommands { Fetch { @@ -37,6 +43,12 @@ pub enum StateCommands { /// Actor address to read the state of actor_address: StrictAddress, }, + /// Returns the built-in actor bundle CIDs for the current network + ActorCids { + /// Format output + #[arg(long, default_value = "text")] + format: Format, + }, } impl StateCommands { @@ -83,6 +95,16 @@ impl StateCommands { .await?; println!("{}", ret.state.into_lotus_json_string_pretty()?); } + Self::ActorCids { format } => { + let info = client.call(StateActorInfo::request(())?).await?; + + match format { + Format::Json => { + println!("{}", serde_json::to_string_pretty(&info)?); + } + Format::Text => println!("{info}"), + } + } } Ok(()) } diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 7be0d5596be1..98125c3f7138 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -11,16 +11,16 @@ use crate::cid_collections::CidHashSet; use crate::eth::EthChainId; use crate::interpreter::{MessageCallbackCtx, VMTrace}; use crate::libp2p::NetworkMessage; -use crate::lotus_json::lotus_json_with_self; -use crate::networks::ChainConfig; +use crate::lotus_json::{LotusJson, lotus_json_with_self}; +use crate::networks::{ACTOR_BUNDLES_METADATA, ChainConfig}; use crate::rpc::registry::actors_reg::load_and_serialize_actor_state; -use crate::shim::actors::init; use crate::shim::actors::market::DealState; use crate::shim::actors::market::ext::MarketStateExt as _; use crate::shim::actors::miner::ext::DeadlineExt; use crate::shim::actors::state_load::*; use crate::shim::actors::verifreg::ext::VerifiedRegistryStateExt as _; use crate::shim::actors::verifreg::{Allocation, AllocationID, Claim}; +use crate::shim::actors::{init, system}; use crate::shim::actors::{ market, miner, miner::{MinerInfo, MinerPower}, @@ -32,6 +32,7 @@ use crate::shim::actors::{ power::ext::PowerStateExt as _, }; use crate::shim::address::Payload; +use crate::shim::machine::BuiltinActorManifest; use crate::shim::message::{Message, MethodNum}; use crate::shim::piece::PaddedPieceSize; use crate::shim::sector::{SectorNumber, SectorSize}; @@ -49,7 +50,7 @@ use crate::{ rpc::{ApiPaths, Ctx, Permission, RpcMethod, ServerError, types::*}, }; use ahash::{HashMap, HashMapExt, HashSet}; -use anyhow::Context as _; +use anyhow::Context; use anyhow::Result; use cid::Cid; use enumflags2::{BitFlags, make_bitflags}; @@ -3164,3 +3165,102 @@ fn get_pledge_ramp_params( Ok((0, 0)) } } + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)] +#[serde(rename_all = "PascalCase")] +pub struct StateActorCodeCidsOutput { + pub network_version: NetworkVersion, + pub actor_version: String, + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson")] + pub manifest: Cid, + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson")] + pub bundle: Cid, + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson>")] + pub actor_cids: HashMap, +} +lotus_json_with_self!(StateActorCodeCidsOutput); + +impl std::fmt::Display for StateActorCodeCidsOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Network Version: {}", self.network_version)?; + writeln!(f, "Actor Version: {}", self.actor_version)?; + writeln!(f, "Manifest CID: {}", self.manifest)?; + writeln!(f, "Bundle CID: {}", self.bundle)?; + writeln!(f, "Actor CIDs:")?; + let longest_name = self + .actor_cids + .keys() + .map(|name| name.len()) + .max() + .unwrap_or(0); + for (name, cid) in &self.actor_cids { + writeln!(f, " {:width$} : {}", name, cid, width = longest_name)?; + } + Ok(()) + } +} + +pub enum StateActorInfo {} + +impl RpcMethod<0> for StateActorInfo { + const NAME: &'static str = "Forest.StateActorInfo"; + const PARAM_NAMES: [&'static str; 0] = []; + const API_PATHS: BitFlags = ApiPaths::all(); + const PERMISSION: Permission = Permission::Read; + const DESCRIPTION: Option<&'static str> = + Some("Returns the builtin actor information for the current network."); + + type Params = (); + type Ok = StateActorCodeCidsOutput; + + async fn handle( + ctx: Ctx, + (): Self::Params, + ) -> Result { + let ts = ctx.chain_store().load_required_tipset_or_heaviest(None)?; + let state_tree = StateTree::new_from_tipset(ctx.store_owned(), &ts)?; + let system_state: system::State = state_tree.get_actor_state()?; + + let actors = system_state.builtin_actors_cid(); + + let bundle = ACTOR_BUNDLES_METADATA + .values() + .find(|metadata| { + metadata.network == ctx.chain_config().network + && metadata.manifest.actor_list_cid == *actors + }) + .with_context(|| { + format!( + "No actor bundle found for network {} with bundle CID {}", + ctx.chain_config().network, + actors + ) + })?; + + let current_manifest = BuiltinActorManifest::load_v1_actor_list(ctx.store(), actors)?; + + // Sanity check: the command would normally be used only for diagnostics, so we want to be + // sure the data is consistent. + if current_manifest != bundle.manifest { + return Err(anyhow::anyhow!("Actor bundle manifest does not match the manifest in the state tree. This indicates that the node is misconfigured or is running an unsupported network.") + .into()); + } + + let network_version = ctx.chain_config().network_version(ts.epoch() - 1); + let result = StateActorCodeCidsOutput { + network_version, + actor_version: bundle.version.to_owned(), + manifest: current_manifest.actor_list_cid, + bundle: bundle.bundle_cid, + actor_cids: current_manifest + .builtin_actors() + .map(|(a, c)| (a.name().to_string(), c)) + .collect(), + }; + + Ok(result) + } +} diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index b175b0427cd7..55461f7fb3dd 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -231,6 +231,7 @@ macro_rules! for_each_rpc_method { $callback!($crate::rpc::state::StateMinerSectors); $callback!($crate::rpc::state::StateNetworkName); $callback!($crate::rpc::state::StateNetworkVersion); + $callback!($crate::rpc::state::StateActorInfo); $callback!($crate::rpc::state::StateReadState); $callback!($crate::rpc::state::StateDecodeParams); $callback!($crate::rpc::state::StateReplay); diff --git a/src/shim/actors/builtin/system/mod.rs b/src/shim/actors/builtin/system/mod.rs index 5bf52de94a6c..583740227f32 100644 --- a/src/shim/actors/builtin/system/mod.rs +++ b/src/shim/actors/builtin/system/mod.rs @@ -1,6 +1,7 @@ // Copyright 2019-2025 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use cid::Cid; use fvm_shared2::address::Address; use serde::Serialize; @@ -25,3 +26,21 @@ pub enum State { V16(fil_actor_system_state::v16::State), V17(fil_actor_system_state::v17::State), } + +impl State { + /// Returns the builtin actors Cid. + pub fn builtin_actors_cid(&self) -> &Cid { + match self { + State::V8(s) => &s.builtin_actors, + State::V9(s) => &s.builtin_actors, + State::V10(s) => &s.builtin_actors, + State::V11(s) => &s.builtin_actors, + State::V12(s) => &s.builtin_actors, + State::V13(s) => &s.builtin_actors, + State::V14(s) => &s.builtin_actors, + State::V15(s) => &s.builtin_actors, + State::V16(s) => &s.builtin_actors, + State::V17(s) => &s.builtin_actors, + } + } +} diff --git a/src/shim/machine/manifest.rs b/src/shim/machine/manifest.rs index 1be8c9649557..2c726b25af3f 100644 --- a/src/shim/machine/manifest.rs +++ b/src/shim/machine/manifest.rs @@ -24,7 +24,7 @@ pub use fil_actors_shared::v11::runtime::builtins::Type as BuiltinActor; pub struct BuiltinActorManifest { builtin2cid: BTreeMap, /// The CID that this manifest was built from - actor_list_cid: Cid, + pub actor_list_cid: Cid, } // We need to implement Serialize and Deserialize manually because `BuiltinActor` is not `Serialize` or `Deserialize`, From c4ae130e713c7113aab47e23c324cde147388db4 Mon Sep 17 00:00:00 2001 From: Hubert Bugaj Date: Wed, 10 Sep 2025 17:41:21 +0200 Subject: [PATCH 2/4] changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c56e56116f62..194ce955b1d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ ### Added +- [#6061](https://github.com/ChainSafe/forest/pull/6061) Added `forest-cli state actor-cids` command for listing all actor CIDs in the state tree for the current tipset. + ### Changed ### Removed From 26f41446262d75b1a790b1627487d135f2993f0c Mon Sep 17 00:00:00 2001 From: Hubert Bugaj Date: Wed, 17 Sep 2025 13:45:55 +0200 Subject: [PATCH 3/4] ignore state actor info RPC [skip CI] --- src/tool/subcommands/api_cmd/test_snapshots_ignored.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tool/subcommands/api_cmd/test_snapshots_ignored.txt b/src/tool/subcommands/api_cmd/test_snapshots_ignored.txt index 5b26058d7984..9886a4937b10 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots_ignored.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots_ignored.txt @@ -85,3 +85,4 @@ Forest.StateCompute Forest.StateFetchRoot Forest.SyncSnapshotProgress Forest.SyncStatus +Forest.StateActorInfo From d6878931bc0af5c7a25d4ba2152ea43bebc55e49 Mon Sep 17 00:00:00 2001 From: Hubert Bugaj Date: Wed, 17 Sep 2025 14:55:03 +0200 Subject: [PATCH 4/4] add revision + use the metadata from stattree method --- src/rpc/methods/state.rs | 26 +++++++------------ .../api_cmd/test_snapshots_ignored.txt | 2 +- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 98125c3f7138..9bca3a5da154 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -12,7 +12,7 @@ use crate::eth::EthChainId; use crate::interpreter::{MessageCallbackCtx, VMTrace}; use crate::libp2p::NetworkMessage; use crate::lotus_json::{LotusJson, lotus_json_with_self}; -use crate::networks::{ACTOR_BUNDLES_METADATA, ChainConfig}; +use crate::networks::ChainConfig; use crate::rpc::registry::actors_reg::load_and_serialize_actor_state; use crate::shim::actors::market::DealState; use crate::shim::actors::market::ext::MarketStateExt as _; @@ -3170,6 +3170,7 @@ fn get_pledge_ramp_params( #[serde(rename_all = "PascalCase")] pub struct StateActorCodeCidsOutput { pub network_version: NetworkVersion, + pub network_version_revision: i64, pub actor_version: String, #[serde(with = "crate::lotus_json")] #[schemars(with = "LotusJson")] @@ -3186,6 +3187,11 @@ lotus_json_with_self!(StateActorCodeCidsOutput); impl std::fmt::Display for StateActorCodeCidsOutput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "Network Version: {}", self.network_version)?; + writeln!( + f, + "Network Version Revision: {}", + self.network_version_revision + )?; writeln!(f, "Actor Version: {}", self.actor_version)?; writeln!(f, "Manifest CID: {}", self.manifest)?; writeln!(f, "Bundle CID: {}", self.bundle)?; @@ -3222,24 +3228,10 @@ impl RpcMethod<0> for StateActorInfo { ) -> Result { let ts = ctx.chain_store().load_required_tipset_or_heaviest(None)?; let state_tree = StateTree::new_from_tipset(ctx.store_owned(), &ts)?; + let bundle = state_tree.get_actor_bundle_metadata()?; let system_state: system::State = state_tree.get_actor_state()?; - let actors = system_state.builtin_actors_cid(); - let bundle = ACTOR_BUNDLES_METADATA - .values() - .find(|metadata| { - metadata.network == ctx.chain_config().network - && metadata.manifest.actor_list_cid == *actors - }) - .with_context(|| { - format!( - "No actor bundle found for network {} with bundle CID {}", - ctx.chain_config().network, - actors - ) - })?; - let current_manifest = BuiltinActorManifest::load_v1_actor_list(ctx.store(), actors)?; // Sanity check: the command would normally be used only for diagnostics, so we want to be @@ -3250,8 +3242,10 @@ impl RpcMethod<0> for StateActorInfo { } let network_version = ctx.chain_config().network_version(ts.epoch() - 1); + let network_version_revision = ctx.chain_config().network_version_revision(ts.epoch() - 1); let result = StateActorCodeCidsOutput { network_version, + network_version_revision, actor_version: bundle.version.to_owned(), manifest: current_manifest.actor_list_cid, bundle: bundle.bundle_cid, diff --git a/src/tool/subcommands/api_cmd/test_snapshots_ignored.txt b/src/tool/subcommands/api_cmd/test_snapshots_ignored.txt index 9886a4937b10..e174ae3c0d9a 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots_ignored.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots_ignored.txt @@ -81,8 +81,8 @@ Forest.ChainExport Forest.ChainGetMinBaseFee Forest.NetInfo Forest.SnapshotGC +Forest.StateActorInfo Forest.StateCompute Forest.StateFetchRoot Forest.SyncSnapshotProgress Forest.SyncStatus -Forest.StateActorInfo