diff --git a/CHANGELOG.md b/CHANGELOG.md index 1012eab00eb0..67790329ead6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,10 @@ - [#6231](https://github.com/ChainSafe/forest/pull/6231) Implemented `Filecoin.ChainGetTipSet` for API v2. +- [#6312](https://github.com/ChainSafe/forest/pull/6312) Implemented `Filecoin.StateGetActor` for API v2. + +- [#6312](https://github.com/ChainSafe/forest/pull/6312) Implemented `Filecoin.StateGetID` for API v2. + ### Changed ### Removed diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index 44c4f229bfc3..82771feaf1ac 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -19,7 +19,7 @@ use crate::lotus_json::{assert_all_snapshots, assert_unchanged_via_json}; use crate::message::{ChainMessage, SignedMessage}; use crate::rpc::eth::{EthLog, eth_logs_with_filter, types::ApiHeaders, types::EthFilterSpec}; use crate::rpc::f3::F3ExportLatestSnapshot; -use crate::rpc::types::{ApiExportResult, ApiExportStatus, ApiTipsetKey, Event}; +use crate::rpc::types::*; use crate::rpc::{ApiPaths, Ctx, EthEventHandler, Permission, RpcMethod, ServerError}; use crate::shim::clock::ChainEpoch; use crate::shim::error::ExitCode; @@ -1101,22 +1101,11 @@ impl ChainGetTipSetV2 { Ok(None) } } -} - -impl RpcMethod<1> for ChainGetTipSetV2 { - const NAME: &'static str = "Filecoin.ChainGetTipSet"; - const PARAM_NAMES: [&'static str; 1] = ["tipsetSelector"]; - const API_PATHS: BitFlags = make_bitflags!(ApiPaths::{ V2 }); - const PERMISSION: Permission = Permission::Read; - const DESCRIPTION: Option<&'static str> = Some("Returns the tipset with the specified CID."); - type Params = (TipsetSelector,); - type Ok = Option; - - async fn handle( - ctx: Ctx, - (selector,): Self::Params, - ) -> Result { + pub async fn get_tipset( + ctx: &Ctx, + selector: &TipsetSelector, + ) -> anyhow::Result> { selector.validate()?; // Get tipset by key. if let ApiTipsetKey(Some(tsk)) = &selector.key { @@ -1125,7 +1114,7 @@ impl RpcMethod<1> for ChainGetTipSetV2 { } // Get tipset by height. if let Some(height) = &selector.height { - let anchor = Self::get_tipset_by_anchor(&ctx, &height.anchor).await?; + let anchor = Self::get_tipset_by_anchor(ctx, &height.anchor).await?; let ts = ctx.chain_index().tipset_by_height( height.at, anchor.unwrap_or_else(|| ctx.chain_store().heaviest_tipset()), @@ -1135,10 +1124,37 @@ impl RpcMethod<1> for ChainGetTipSetV2 { } // Get tipset by tag, either latest or finalized. if let Some(tag) = &selector.tag { - let ts = Self::get_tipset_by_tag(&ctx, *tag).await?; + let ts = Self::get_tipset_by_tag(ctx, *tag).await?; return Ok(ts); } - Err(anyhow::anyhow!("no tipset found for selector").into()) + anyhow::bail!("no tipset found for selector") + } + + pub async fn get_required_tipset( + ctx: &Ctx, + selector: &TipsetSelector, + ) -> anyhow::Result { + Self::get_tipset(ctx, selector) + .await? + .context("failed to select a tipset") + } +} + +impl RpcMethod<1> for ChainGetTipSetV2 { + const NAME: &'static str = "Filecoin.ChainGetTipSet"; + const PARAM_NAMES: [&'static str; 1] = ["tipsetSelector"]; + const API_PATHS: BitFlags = make_bitflags!(ApiPaths::{ V2 }); + const PERMISSION: Permission = Permission::Read; + const DESCRIPTION: Option<&'static str> = Some("Returns the tipset with the specified CID."); + + type Params = (TipsetSelector,); + type Ok = Option; + + async fn handle( + ctx: Ctx, + (selector,): Self::Params, + ) -> Result { + Ok(Self::get_tipset(&ctx, &selector).await?) } } diff --git a/src/rpc/methods/chain/types.rs b/src/rpc/methods/chain/types.rs index 7b119ab6cb4c..d1309da561cb 100644 --- a/src/rpc/methods/chain/types.rs +++ b/src/rpc/methods/chain/types.rs @@ -3,9 +3,6 @@ use super::*; -#[cfg(test)] -mod tests; - #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Eq, PartialEq, Default)] #[serde(rename_all = "PascalCase")] pub struct ObjStat { @@ -13,124 +10,3 @@ pub struct ObjStat { pub links: usize, } lotus_json_with_self!(ObjStat); - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -pub struct TipsetSelector { - #[serde( - with = "crate::lotus_json", - skip_serializing_if = "ApiTipsetKey::is_none", - default - )] - #[schemars(with = "LotusJson")] - pub key: ApiTipsetKey, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub height: Option, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub tag: Option, -} -lotus_json_with_self!(TipsetSelector); - -impl TipsetSelector { - /// Validate ensures that the [`TipsetSelector`] is valid. It checks that only one of - /// the selection criteria is specified. - pub fn validate(&self) -> anyhow::Result<()> { - match (&self.key.0, &self.tag, &self.height) { - (Some(_), None, None) | (None, Some(_), None) => {} - (None, None, Some(height)) => { - height.validate()?; - } - _ => { - let criteria = [ - self.key.0.is_some(), - self.tag.is_some(), - self.height.is_some(), - ] - .into_iter() - .filter(|&b| b) - .count(); - anyhow::bail!( - "exactly one tipset selection criteria must be specified, found: {criteria}" - ) - } - } - Ok(()) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -pub struct TipsetHeight { - pub at: ChainEpoch, - pub previous: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub anchor: Option, -} -lotus_json_with_self!(TipsetHeight); - -impl TipsetHeight { - /// Ensures that the [`TipsetHeight`] is valid. It checks that the height is - /// not negative and the anchor is valid. - pub fn validate(&self) -> anyhow::Result<()> { - anyhow::ensure!( - self.at >= 0, - "invalid tipset height: epoch cannot be less than zero" - ); - TipsetAnchor::validate(&self.anchor)?; - Ok(()) - } - - pub fn resolve_null_tipset_policy(&self) -> ResolveNullTipset { - if self.previous { - ResolveNullTipset::TakeOlder - } else { - ResolveNullTipset::TakeNewer - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -pub struct TipsetAnchor { - #[serde(with = "crate::lotus_json")] - #[schemars(with = "LotusJson")] - pub key: ApiTipsetKey, - #[serde(skip_serializing_if = "Option::is_none")] - pub tag: Option, -} -lotus_json_with_self!(TipsetAnchor); - -impl TipsetAnchor { - /// Validate ensures that the [`TipsetAnchor`] is valid. It checks that at most one - /// of [`TipsetKey`] or [`TipsetTag`] is specified. Otherwise, it returns an error. - /// - /// Note that a [`None`] anchor is valid, and is considered to be - /// equivalent to the default anchor, which is the tipset tagged as [`TipsetTag::Finalized`]. - pub fn validate(anchor: &Option) -> anyhow::Result<()> { - if let Some(anchor) = anchor { - anyhow::ensure!( - anchor.key.0.is_none() || anchor.tag.is_none(), - "invalid tipset anchor: at most one of key or tag must be specified" - ); - } - // None anchor is valid, and considered to be an equivalent to whatever the API decides the default to be. - Ok(()) - } -} - -#[derive( - Debug, - Clone, - Copy, - strum::Display, - strum::EnumString, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, -)] -#[strum(serialize_all = "lowercase")] -#[serde(rename_all = "lowercase")] -pub enum TipsetTag { - Latest, - Finalized, - Safe, -} diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 31371f2b9ccd..3689c9681b78 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -5,6 +5,7 @@ mod types; use futures::stream::FuturesOrdered; pub use types::*; +use super::chain::ChainGetTipSetV2; use crate::blocks::{Tipset, TipsetKey}; use crate::chain::index::ResolveNullTipset; use crate::cid_collections::CidHashSet; @@ -301,6 +302,50 @@ impl RpcMethod<2> for StateGetActor { } } +pub enum StateGetActorV2 {} + +impl RpcMethod<2> for StateGetActorV2 { + const NAME: &'static str = "Filecoin.StateGetActor"; + const PARAM_NAMES: [&'static str; 2] = ["address", "tipsetSelector"]; + const API_PATHS: BitFlags = make_bitflags!(ApiPaths::{ V2 }); + const PERMISSION: Permission = Permission::Read; + const DESCRIPTION: Option<&'static str> = + Some("Returns the nonce and balance for the specified actor."); + + type Params = (Address, TipsetSelector); + type Ok = Option; + + async fn handle( + ctx: Ctx, + (address, selector): Self::Params, + ) -> Result { + let ts = ChainGetTipSetV2::get_required_tipset(&ctx, &selector).await?; + Ok(ctx.state_manager.get_actor(&address, *ts.parent_state())?) + } +} + +pub enum StateGetID {} + +impl RpcMethod<2> for StateGetID { + const NAME: &'static str = "Filecoin.StateGetID"; + const PARAM_NAMES: [&'static str; 2] = ["address", "tipsetSelector"]; + const API_PATHS: BitFlags = make_bitflags!(ApiPaths::{ V2 }); + const PERMISSION: Permission = Permission::Read; + const DESCRIPTION: Option<&'static str> = + Some("Retrieves the ID address for the specified address at the selected tipset."); + + type Params = (Address, TipsetSelector); + type Ok = Address; + + async fn handle( + ctx: Ctx, + (address, selector): Self::Params, + ) -> Result { + let ts = ChainGetTipSetV2::get_required_tipset(&ctx, &selector).await?; + Ok(ctx.state_manager.lookup_required_id(&address, &ts)?) + } +} + pub enum StateLookupRobustAddress {} macro_rules! get_robust_address { diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 20944cea8177..c02678ae30bf 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -197,6 +197,8 @@ macro_rules! for_each_rpc_method { $callback!($crate::rpc::state::StateDealProviderCollateralBounds); $callback!($crate::rpc::state::StateFetchRoot); $callback!($crate::rpc::state::StateGetActor); + $callback!($crate::rpc::state::StateGetActorV2); + $callback!($crate::rpc::state::StateGetID); $callback!($crate::rpc::state::StateGetAllAllocations); $callback!($crate::rpc::state::StateGetAllClaims); $callback!($crate::rpc::state::StateGetAllocation); diff --git a/src/rpc/types/mod.rs b/src/rpc/types/mod.rs index 85fbbd6852ec..c0b019e71d91 100644 --- a/src/rpc/types/mod.rs +++ b/src/rpc/types/mod.rs @@ -5,6 +5,9 @@ //! //! If a type here is used by only one API, it should be relocated. +mod tipset_selector; +pub use tipset_selector::*; + mod address_impl; mod deal_impl; mod sector_impl; diff --git a/src/rpc/types/tipset_selector.rs b/src/rpc/types/tipset_selector.rs new file mode 100644 index 000000000000..6533cb8309c7 --- /dev/null +++ b/src/rpc/types/tipset_selector.rs @@ -0,0 +1,129 @@ +// Copyright 2019-2025 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use super::*; +use crate::chain::index::ResolveNullTipset; + +#[cfg(test)] +mod tests; + +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub struct TipsetSelector { + #[serde( + with = "crate::lotus_json", + skip_serializing_if = "ApiTipsetKey::is_none", + default + )] + #[schemars(with = "LotusJson")] + pub key: ApiTipsetKey, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub height: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub tag: Option, +} +lotus_json_with_self!(TipsetSelector); + +impl TipsetSelector { + /// Validate ensures that the [`TipsetSelector`] is valid. It checks that only one of + /// the selection criteria is specified. + pub fn validate(&self) -> anyhow::Result<()> { + match (&self.key.0, &self.tag, &self.height) { + (Some(_), None, None) | (None, Some(_), None) => {} + (None, None, Some(height)) => { + height.validate()?; + } + _ => { + let criteria = [ + self.key.0.is_some(), + self.tag.is_some(), + self.height.is_some(), + ] + .into_iter() + .filter(|&b| b) + .count(); + anyhow::bail!( + "exactly one tipset selection criteria must be specified, found: {criteria}" + ) + } + } + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub struct TipsetHeight { + pub at: ChainEpoch, + pub previous: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub anchor: Option, +} +lotus_json_with_self!(TipsetHeight); + +impl TipsetHeight { + /// Ensures that the [`TipsetHeight`] is valid. It checks that the height is + /// not negative and the anchor is valid. + pub fn validate(&self) -> anyhow::Result<()> { + anyhow::ensure!( + self.at >= 0, + "invalid tipset height: epoch cannot be less than zero" + ); + TipsetAnchor::validate(&self.anchor)?; + Ok(()) + } + + pub fn resolve_null_tipset_policy(&self) -> ResolveNullTipset { + if self.previous { + ResolveNullTipset::TakeOlder + } else { + ResolveNullTipset::TakeNewer + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub struct TipsetAnchor { + #[serde(with = "crate::lotus_json")] + #[schemars(with = "LotusJson")] + pub key: ApiTipsetKey, + #[serde(skip_serializing_if = "Option::is_none")] + pub tag: Option, +} +lotus_json_with_self!(TipsetAnchor); + +impl TipsetAnchor { + /// Validate ensures that the [`TipsetAnchor`] is valid. It checks that at most one + /// of [`TipsetKey`] or [`TipsetTag`] is specified. Otherwise, it returns an error. + /// + /// Note that a [`None`] anchor is valid, and is considered to be + /// equivalent to the default anchor, which is the tipset tagged as [`TipsetTag::Finalized`]. + pub fn validate(anchor: &Option) -> anyhow::Result<()> { + if let Some(anchor) = anchor { + anyhow::ensure!( + anchor.key.0.is_none() || anchor.tag.is_none(), + "invalid tipset anchor: at most one of key or tag must be specified" + ); + } + // None anchor is valid, and considered to be an equivalent to whatever the API decides the default to be. + Ok(()) + } +} + +#[derive( + Debug, + Clone, + Copy, + strum::Display, + strum::EnumString, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, +)] +#[strum(serialize_all = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum TipsetTag { + Latest, + Finalized, + Safe, +} diff --git a/src/rpc/methods/chain/types/tests.rs b/src/rpc/types/tipset_selector/tests.rs similarity index 100% rename from src/rpc/methods/chain/types/tests.rs rename to src/rpc/types/tipset_selector/tests.rs diff --git a/src/tool/subcommands/api_cmd/api_compare_tests.rs b/src/tool/subcommands/api_cmd/api_compare_tests.rs index 67f6c2b01769..640cf755b479 100644 --- a/src/tool/subcommands/api_cmd/api_compare_tests.rs +++ b/src/tool/subcommands/api_cmd/api_compare_tests.rs @@ -11,7 +11,6 @@ use crate::message::{Message as _, SignedMessage}; use crate::rpc::FilterList; use crate::rpc::auth::AuthNewParams; use crate::rpc::beacon::BeaconGetEntry; -use crate::rpc::chain::types::*; use crate::rpc::eth::{ BlockNumberOrHash, EthInt64, ExtBlockNumberOrHash, ExtPredefined, Predefined, new_eth_tx_from_signed_message, types::*, @@ -20,7 +19,7 @@ use crate::rpc::gas::GasEstimateGasLimit; use crate::rpc::miner::BlockTemplate; use crate::rpc::misc::ActorEventFilter; use crate::rpc::state::StateGetAllClaims; -use crate::rpc::types::{ApiTipsetKey, MessageFilter, MessageLookup}; +use crate::rpc::types::*; use crate::rpc::{Permission, prelude::*}; use crate::shim::actors::MarketActorStateLoad as _; use crate::shim::actors::market; @@ -827,6 +826,20 @@ fn state_tests_with_tipset( Address::SYSTEM_ACTOR, tipset.key().into(), ))?), + RpcTest::identity(StateGetActorV2::request(( + Address::SYSTEM_ACTOR, + TipsetSelector { + key: tipset.key().into(), + ..Default::default() + }, + ))?), + RpcTest::identity(StateGetID::request(( + Address::SYSTEM_ACTOR, + TipsetSelector { + key: tipset.key().into(), + ..Default::default() + }, + ))?), RpcTest::identity(StateGetRandomnessFromTickets::request(( DomainSeparationTag::ElectionProofProduction as i64, tipset.epoch(), diff --git a/src/tool/subcommands/api_cmd/test_snapshots.txt b/src/tool/subcommands/api_cmd/test_snapshots.txt index 0f1543e50108..1d654beee276 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -212,6 +212,7 @@ filecoin_statedecodeparams_1754059748144675.rpcsnap.json.zst filecoin_statedecodeparams_1754059748147929.rpcsnap.json.zst filecoin_statedecodeparams_1754059748148210.rpcsnap.json.zst filecoin_stategetactor_1737546933166397.rpcsnap.json.zst +filecoin_stategetactor_1764943330181291.rpcsnap.json.zst filecoin_stategetallallocations_1733735079961566.rpcsnap.json.zst filecoin_stategetallclaims_1733735080472880.rpcsnap.json.zst filecoin_stategetallocation_1733735082114977.rpcsnap.json.zst @@ -221,6 +222,7 @@ filecoin_stategetallocations_1733735082163772.rpcsnap.json.zst filecoin_stategetbeaconentry_1737546933208724.rpcsnap.json.zst filecoin_stategetclaim_1737546933208940.rpcsnap.json.zst filecoin_stategetclaims_1737546933208977.rpcsnap.json.zst +filecoin_stategetid_1764943126105310.rpcsnap.json.zst filecoin_stategetnetworkparams_1756890287572001.rpcsnap.json.zst filecoin_stategetrandomnessdigestfrombeacon_1737546933236534.rpcsnap.json.zst filecoin_stategetrandomnessdigestfromtickets_1737546933236558.rpcsnap.json.zst