From 6317843a93a6bb19050ff749d8b06ab67603eac4 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 3 Nov 2025 19:01:45 +0800 Subject: [PATCH 1/4] fix(rpc): register RPC methods for different versions into different modules --- src/rpc/methods/chain.rs | 18 +++++++++++++- src/rpc/mod.rs | 43 ++++++++++++++++++--------------- src/rpc/reflect/mod.rs | 44 +++++++++++++++++++++------------- src/rpc/set_extension_layer.rs | 21 ++++++---------- 4 files changed, 75 insertions(+), 51 deletions(-) diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index 43c5c1be6811..1a84bfe752d2 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -967,7 +967,7 @@ pub enum ChainGetTipSet {} impl RpcMethod<1> for ChainGetTipSet { const NAME: &'static str = "Filecoin.ChainGetTipSet"; const PARAM_NAMES: [&'static str; 1] = ["tipsetKey"]; - const API_PATHS: BitFlags = ApiPaths::all(); + const API_PATHS: BitFlags = make_bitflags!(ApiPaths::{ V0 | V1 }); const PERMISSION: Permission = Permission::Read; const DESCRIPTION: Option<&'static str> = Some("Returns the tipset with the specified CID."); @@ -985,6 +985,22 @@ impl RpcMethod<1> for ChainGetTipSet { } } +pub enum ChainGetTipSetV2 {} +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 = (ApiTipsetKey,); + type Ok = Tipset; + + async fn handle(_: Ctx, _: Self::Params) -> Result { + Err(anyhow::anyhow!("Filecoin.ChainGetTipSet for V2 is not yet implemented").into()) + } +} + pub enum ChainSetHead {} impl RpcMethod<1> for ChainSetHead { const NAME: &'static str = "Filecoin.ChainSetHead"; diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 5a4330895c64..97539a397efa 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -15,6 +15,7 @@ mod set_extension_layer; use crate::rpc::eth::types::RandomHexStringIdProvider; use crate::shim::clock::ChainEpoch; +use clap::ValueEnum as _; pub use client::Client; pub use error::ServerError; use eth::filter::EthEventHandler; @@ -31,6 +32,7 @@ use segregation_layer::SegregationLayer; use set_extension_layer::SetExtensionLayer; mod error; mod reflect; +use ahash::HashMap; mod registry; pub mod types; @@ -73,6 +75,7 @@ macro_rules! for_each_rpc_method { $callback!($crate::rpc::chain::ChainGetParentReceipts); $callback!($crate::rpc::chain::ChainGetPath); $callback!($crate::rpc::chain::ChainGetTipSet); + $callback!($crate::rpc::chain::ChainGetTipSetV2); $callback!($crate::rpc::chain::ChainGetTipSetAfterHeight); $callback!($crate::rpc::chain::ChainGetTipSetByHeight); $callback!($crate::rpc::chain::ChainHasObj); @@ -499,7 +502,6 @@ impl RPCState { #[derive(Clone)] struct PerConnection { - methods: Methods, stop_handle: StopHandle, svc_builder: TowerServiceBuilder, keystore: Arc>, @@ -517,24 +519,26 @@ where // `Arc` is needed because we will share the state between two modules let state = Arc::new(state); let keystore = state.keystore.clone(); - let mut module = create_module(state.clone()); - - // register eth subscription APIs - let eth_pubsub = EthPubSub::new(state.clone()); - module.merge(eth_pubsub.into_rpc())?; + let mut modules = create_modules(state.clone()); let mut pubsub_module = FilRpcModule::default(); - pubsub_module.register_channel("Filecoin.ChainNotify", { let state_clone = state.clone(); move |params| chain::chain_notify(params, &state_clone) })?; - module.merge(pubsub_module)?; + + for module in modules.values_mut() { + // register eth subscription APIs + module.merge(EthPubSub::new(state.clone()).into_rpc())?; + module.merge(pubsub_module.clone())?; + } + + let methods: Arc> = + Arc::new(modules.into_iter().map(|(k, v)| (k, v.into())).collect()); let (stop_handle, _server_handle) = stop_channel(); let per_conn = PerConnection { - methods: module.into(), stop_handle: stop_handle.clone(), svc_builder: Server::builder() .set_config( @@ -574,12 +578,14 @@ where }; let svc = tower::service_fn({ + let methods = methods.clone(); let per_conn = per_conn.clone(); let filter_list = filter_list.clone(); move |req| { let is_websocket = jsonrpsee::server::ws::is_upgrade_request(&req); + let path = ApiPaths::from_uri(req.uri()).unwrap_or(ApiPaths::V1); + let methods = methods.get(&path).cloned().unwrap_or_default(); let PerConnection { - methods, stop_handle, svc_builder, keystore, @@ -588,9 +594,7 @@ where // with data from the connection such as the headers in this example let headers = req.headers().clone(); let rpc_middleware = RpcServiceBuilder::new() - .layer(SetExtensionLayer { - path: ApiPaths::from_uri(req.uri()).ok(), - }) + .layer(SetExtensionLayer { path }) .layer(SegregationLayer) .layer(FilterLayer::new(filter_list.clone())) .layer(AuthLayer { @@ -652,24 +656,25 @@ where Ok(()) } -fn create_module(state: Arc>) -> RpcModule> +fn create_modules(state: Arc>) -> HashMap>> where DB: Blockstore + Send + Sync + 'static, { - let mut module = RpcModule::from_arc(state); + let mut modules = HashMap::default(); + for api_version in ApiPaths::value_variants() { + modules.insert(*api_version, RpcModule::from_arc(state.clone())); + } macro_rules! register { ($ty:ty) => { // Register only non-subscription RPC methods. // Subscription methods are registered separately in the RPC module. if !<$ty>::SUBSCRIPTION { - <$ty>::register(&mut module, ParamStructure::ByPosition).unwrap(); - // Optionally register an alias for the method. - <$ty>::register_alias(&mut module).unwrap(); + <$ty>::register(&mut modules, ParamStructure::ByPosition).unwrap(); } }; } for_each_rpc_method!(register); - module + modules } /// If `include` is not [`None`], only methods that are listed will be returned diff --git a/src/rpc/reflect/mod.rs b/src/rpc/reflect/mod.rs index 51576f532b76..e931139c6a9b 100644 --- a/src/rpc/reflect/mod.rs +++ b/src/rpc/reflect/mod.rs @@ -25,6 +25,7 @@ use crate::lotus_json::HasLotusJson; use self::{jsonrpc_types::RequestParameters, util::Optional as _}; use super::error::ServerError as Error; +use ahash::HashMap; use anyhow::Context as _; use enumflags2::{BitFlags, bitflags, make_bitflags}; use fvm_ipld_blockstore::Blockstore; @@ -221,24 +222,20 @@ pub trait RpcMethodExt: RpcMethod { ..Default::default() } } - /// Register this method's alias with an [`RpcModule`]. - fn register_alias( - module: &mut RpcModule>, - ) -> Result<(), jsonrpsee::core::RegisterMethodError> { - if let Some(alias) = Self::NAME_ALIAS { - module.register_alias(alias, Self::NAME)? - } - Ok(()) - } /// Register a method with an [`RpcModule`]. fn register( - module: &mut RpcModule>, + modules: &mut HashMap< + ApiPaths, + RpcModule>, + >, calling_convention: ParamStructure, - ) -> Result<&mut jsonrpsee::MethodCallback, jsonrpsee::core::RegisterMethodError> + ) -> Result<(), jsonrpsee::core::RegisterMethodError> where ::LotusJson: Clone + 'static, { + use clap::ValueEnum as _; + assert!( Self::N_REQUIRED_PARAMS <= ARITY, "N_REQUIRED_PARAMS({}) can not be greater than ARITY({ARITY}) in {}", @@ -246,12 +243,25 @@ pub trait RpcMethodExt: RpcMethod { Self::NAME ); - module.register_async_method(Self::NAME, move |params, ctx, _extensions| async move { - let params = Self::parse_params(params.as_str(), calling_convention) - .map_err(|e| Error::invalid_params(e, None))?; - let ok = Self::handle(ctx, params).await?; - Result::<_, jsonrpsee::types::ErrorObjectOwned>::Ok(ok.into_lotus_json()) - }) + for api_version in ApiPaths::value_variants() { + if Self::API_PATHS.contains(*api_version) + && let Some(module) = modules.get_mut(api_version) + { + module.register_async_method( + Self::NAME, + move |params, ctx, _extensions| async move { + let params = Self::parse_params(params.as_str(), calling_convention) + .map_err(|e| Error::invalid_params(e, None))?; + let ok = Self::handle(ctx, params).await?; + Result::<_, jsonrpsee::types::ErrorObjectOwned>::Ok(ok.into_lotus_json()) + }, + )?; + if let Some(alias) = Self::NAME_ALIAS { + module.register_alias(alias, Self::NAME)? + } + } + } + Ok(()) } /// Returns [`Err`] if any of the parameters fail to serialize. fn request(params: Self::Params) -> Result, serde_json::Error> { diff --git a/src/rpc/set_extension_layer.rs b/src/rpc/set_extension_layer.rs index 86a6ab26b1bf..167ad972719d 100644 --- a/src/rpc/set_extension_layer.rs +++ b/src/rpc/set_extension_layer.rs @@ -8,9 +8,9 @@ use jsonrpsee::server::middleware::rpc::RpcServiceT; use tower::Layer; /// JSON-RPC middleware layer for setting extensions in RPC requests -#[derive(Clone, Default)] +#[derive(Clone)] pub(super) struct SetExtensionLayer { - pub path: Option, + pub path: ApiPaths, } impl Layer for SetExtensionLayer { @@ -27,13 +27,7 @@ impl Layer for SetExtensionLayer { #[derive(Clone)] pub(super) struct SetExtensionService { service: S, - path: Option, -} - -impl SetExtensionService { - fn path(&self) -> ApiPaths { - self.path.unwrap_or(ApiPaths::V1) - } + path: ApiPaths, } impl RpcServiceT for SetExtensionService @@ -48,7 +42,7 @@ where &self, mut req: jsonrpsee::types::Request<'a>, ) -> impl Future + Send + 'a { - req.extensions_mut().insert(self.path()); + req.extensions_mut().insert(self.path); self.service.call(req) } @@ -56,14 +50,13 @@ where &self, mut batch: Batch<'a>, ) -> impl Future + Send + 'a { - let path = self.path(); for req in batch.iter_mut() { match req { Ok(BatchEntry::Call(req)) => { - req.extensions_mut().insert(path); + req.extensions_mut().insert(self.path); } Ok(BatchEntry::Notification(n)) => { - n.extensions_mut().insert(path); + n.extensions_mut().insert(self.path); } Err(_) => {} } @@ -75,7 +68,7 @@ where &self, mut n: Notification<'a>, ) -> impl Future + Send + 'a { - n.extensions_mut().insert(self.path()); + n.extensions_mut().insert(self.path); self.service.notification(n) } } From 244ccee9bb9f59cd4048beca0ac035c5e789336e Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 3 Nov 2025 19:21:03 +0800 Subject: [PATCH 2/4] use unified error --- src/rpc/methods/chain.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rpc/methods/chain.rs b/src/rpc/methods/chain.rs index 1a84bfe752d2..f677d495db17 100644 --- a/src/rpc/methods/chain.rs +++ b/src/rpc/methods/chain.rs @@ -997,7 +997,7 @@ impl RpcMethod<1> for ChainGetTipSetV2 { type Ok = Tipset; async fn handle(_: Ctx, _: Self::Params) -> Result { - Err(anyhow::anyhow!("Filecoin.ChainGetTipSet for V2 is not yet implemented").into()) + Err(ServerError::unsupported_method()) } } From 17de43e8ea8e92beb88492c32b1d6af152835228 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 3 Nov 2025 22:15:35 +0800 Subject: [PATCH 3/4] fix unit test --- src/rpc/reflect/mod.rs | 15 ++++++++++++++- src/tool/subcommands/api_cmd.rs | 2 ++ src/tool/subcommands/api_cmd/test_snapshot.rs | 8 ++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/rpc/reflect/mod.rs b/src/rpc/reflect/mod.rs index e931139c6a9b..0a34015b77eb 100644 --- a/src/rpc/reflect/mod.rs +++ b/src/rpc/reflect/mod.rs @@ -116,7 +116,20 @@ pub enum Permission { /// This information is important when using [`crate::rpc::client`]. #[bitflags] #[repr(u8)] -#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd, clap::ValueEnum, EnumString)] +#[derive( + Debug, + Clone, + Copy, + Hash, + Eq, + PartialEq, + Ord, + PartialOrd, + clap::ValueEnum, + EnumString, + Deserialize, + Serialize, +)] pub enum ApiPaths { /// Only expose this method on `/rpc/v0` #[strum(ascii_case_insensitive)] diff --git a/src/tool/subcommands/api_cmd.rs b/src/tool/subcommands/api_cmd.rs index 1a604f909518..8187f8c03536 100644 --- a/src/tool/subcommands/api_cmd.rs +++ b/src/tool/subcommands/api_cmd.rs @@ -366,6 +366,8 @@ impl ApiCommands { }, index, db, + // https://github.com/ChainSafe/forest/issues/6220 + api_path: None, } }; diff --git a/src/tool/subcommands/api_cmd/test_snapshot.rs b/src/tool/subcommands/api_cmd/test_snapshot.rs index cee6a193c41a..e118341762bc 100644 --- a/src/tool/subcommands/api_cmd/test_snapshot.rs +++ b/src/tool/subcommands/api_cmd/test_snapshot.rs @@ -16,7 +16,7 @@ use crate::{ message_pool::{MessagePool, MpoolRpcProvider}, networks::{ChainConfig, NetworkChain}, rpc::{ - RPCState, RpcMethod, RpcMethodExt as _, + ApiPaths, RPCState, RpcMethod, RpcMethodExt as _, eth::{filter::EthEventHandler, types::EthHash}, }, shim::address::{CurrentNetwork, Network}, @@ -48,6 +48,8 @@ pub struct RpcTestSnapshot { pub index: Option, #[serde(with = "crate::lotus_json::base64_standard")] pub db: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub api_path: Option, } fn backfill_eth_mappings(db: &MemoryDB, index: Option) -> anyhow::Result<()> { @@ -85,10 +87,12 @@ pub async fn run_test_from_snapshot(path: &Path) -> anyhow::Result<()> { index, db: db_bytes, response: expected_response, + api_path, } = serde_json::from_slice(snapshot_bytes.as_slice())?; if chain.is_testnet() { CurrentNetwork::set_global(Network::Testnet); } + let api_path = api_path.unwrap_or(ApiPaths::V1); let db = Arc::new(ManyCar::new(MemoryDB::default()).with_read_only(AnyCar::new(db_bytes)?)?); // backfill db with index data backfill_eth_mappings(db.writer(), index)?; @@ -101,7 +105,7 @@ pub async fn run_test_from_snapshot(path: &Path) -> anyhow::Result<()> { macro_rules! run_test { ($ty:ty) => { - if method_name.as_str() == <$ty>::NAME { + if method_name.as_str() == <$ty>::NAME && <$ty>::API_PATHS.contains(api_path) { let params = <$ty>::parse_params(params_raw.clone(), ParamStructure::Either)?; let result = <$ty>::handle(ctx.clone(), params) .await From 2fcea77eb9661abe188d958852d4be980a94e5a7 Mon Sep 17 00:00:00 2001 From: hanabi1224 Date: Mon, 3 Nov 2025 23:17:04 +0800 Subject: [PATCH 4/4] update v0 only snapshots --- src/tool/subcommands/api_cmd/test_snapshots.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tool/subcommands/api_cmd/test_snapshots.txt b/src/tool/subcommands/api_cmd/test_snapshots.txt index dc97da7c3fa3..017369332bec 100644 --- a/src/tool/subcommands/api_cmd/test_snapshots.txt +++ b/src/tool/subcommands/api_cmd/test_snapshots.txt @@ -1,4 +1,4 @@ -filecoin_beacongetentry_1741270283524366.rpcsnap.json.zst +filecoin_beacongetentry_1741270283524367.rpcsnap.json.zst filecoin_chaingetblock_1736937164811210.rpcsnap.json.zst filecoin_chaingetblockmessages_1736937164799678.rpcsnap.json.zst filecoin_chaingetevents_1746450533519970.rpcsnap.json.zst @@ -266,7 +266,7 @@ filecoin_statereadstate_1748444218790807.rpcsnap.json.zst filecoin_statereadstate_1749657617007020.rpcsnap.json.zst filecoin_statereplay_1743504051038215.rpcsnap.json.zst filecoin_statesearchmsg_1741784596636715.rpcsnap.json.zst -filecoin_statesearchmsglimited_1741784596704876.rpcsnap.json.zst +filecoin_statesearchmsglimited_1741784596704877.rpcsnap.json.zst filecoin_statesectorexpiration_1741784727996672.rpcsnap.json.zst filecoin_statesectorgetinfo_1743098602783105.rpcsnap.json.zst filecoin_statesectorpartition_1737546933391247.rpcsnap.json.zst