From aa725dd0cf84462cd3bf13257f0aed8e1a9eebe4 Mon Sep 17 00:00:00 2001 From: Rose Jethani <101273941+rose2221@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:51:05 +0530 Subject: [PATCH] feat: add Historical RPC Forwarder Service (#16724) Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> Co-authored-by: Matthias Seitz --- Cargo.lock | 1 + crates/optimism/rpc/Cargo.toml | 1 + crates/optimism/rpc/src/historical.rs | 161 +++++++++++++++++++++++++- 3 files changed, 161 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 27925ad48b..92a8612a72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9389,6 +9389,7 @@ dependencies = [ "reth-tasks", "reth-transaction-pool", "revm", + "serde_json", "thiserror 2.0.12", "tokio", "tracing", diff --git a/crates/optimism/rpc/Cargo.toml b/crates/optimism/rpc/Cargo.toml index 1187076f5d..a29b2406b7 100644 --- a/crates/optimism/rpc/Cargo.toml +++ b/crates/optimism/rpc/Cargo.toml @@ -67,6 +67,7 @@ async-trait.workspace = true jsonrpsee-core.workspace = true jsonrpsee-types.workspace = true jsonrpsee.workspace = true +serde_json.workspace = true # misc eyre.workspace = true diff --git a/crates/optimism/rpc/src/historical.rs b/crates/optimism/rpc/src/historical.rs index ac9320d4ff..c364271ae3 100644 --- a/crates/optimism/rpc/src/historical.rs +++ b/crates/optimism/rpc/src/historical.rs @@ -1,10 +1,18 @@ //! Client support for optimism historical RPC requests. use crate::sequencer::Error; +use alloy_eips::BlockId; use alloy_json_rpc::{RpcRecv, RpcSend}; +use alloy_primitives::BlockNumber; use alloy_rpc_client::RpcClient; -use std::sync::Arc; -use tracing::warn; +use jsonrpsee_core::{ + middleware::{Batch, Notification, RpcServiceT}, + server::MethodResponse, +}; +use jsonrpsee_types::{Params, Request}; +use reth_storage_api::BlockReaderIdExt; +use std::{future::Future, sync::Arc}; +use tracing::{debug, warn}; /// A client that can be used to forward RPC requests for historical data to an endpoint. /// @@ -66,3 +74,152 @@ struct HistoricalRpcClientInner { historical_endpoint: String, client: RpcClient, } + +/// A service that intercepts RPC calls and forwards pre-bedrock historical requests +/// to a dedicated endpoint. +/// +/// This checks if the request is for a pre-bedrock block and forwards it via the configured +/// historical RPC client. +#[derive(Debug, Clone)] +pub struct HistoricalRpcService { + /// The inner service that handles regular RPC requests + inner: S, + /// Client used to forward historical requests + historical_client: HistoricalRpcClient, + /// Provider used to determine if a block is pre-bedrock + provider: P, + /// Bedrock transition block number + bedrock_block: BlockNumber, +} + +impl RpcServiceT for HistoricalRpcService +where + S: RpcServiceT + Send + Sync + Clone + 'static, + + P: BlockReaderIdExt + Send + Sync + Clone + 'static, +{ + type MethodResponse = S::MethodResponse; + type NotificationResponse = S::NotificationResponse; + type BatchResponse = S::BatchResponse; + + fn call<'a>(&self, req: Request<'a>) -> impl Future + Send + 'a { + let inner_service = self.inner.clone(); + let historical_client = self.historical_client.clone(); + let provider = self.provider.clone(); + let bedrock_block = self.bedrock_block; + + Box::pin(async move { + let maybe_block_id = match req.method_name() { + "eth_getBlockByNumber" | "eth_getBlockByHash" => { + parse_block_id_from_params(&req.params(), 0) + } + "eth_getBalance" | + "eth_getStorageAt" | + "eth_getCode" | + "eth_getTransactionCount" | + "eth_call" => parse_block_id_from_params(&req.params(), 1), + _ => None, + }; + + // if we've extracted a block ID, check if it's pre-Bedrock + if let Some(block_id) = maybe_block_id { + let is_pre_bedrock = if let Ok(Some(num)) = provider.block_number_for_id(block_id) { + num < bedrock_block + } else { + // If we can't convert the hash to a number, assume it's post-Bedrock + debug!(target: "rpc::historical", ?block_id, "hash unknown; not forwarding"); + false + }; + + // if the block is pre-Bedrock, forward the request to the historical client + if is_pre_bedrock { + debug!(target: "rpc::historical", method = %req.method_name(), ?block_id, params=?req.params(), "forwarding pre-Bedrock request"); + + let params = req.params(); + let params = params.as_str().unwrap_or("[]"); + if let Ok(params) = serde_json::from_str::(params) { + if let Ok(raw) = historical_client + .request::<_, serde_json::Value>(req.method_name(), params) + .await + { + let payload = + jsonrpsee_types::ResponsePayload::success(raw.to_string()).into(); + return MethodResponse::response(req.id, payload, usize::MAX); + } + } + } + } + + // handle the request with the inner service + inner_service.call(req).await + }) + } + + fn batch<'a>(&self, req: Batch<'a>) -> impl Future + Send + 'a { + self.inner.batch(req) + } + + fn notification<'a>( + &self, + n: Notification<'a>, + ) -> impl Future + Send + 'a { + self.inner.notification(n) + } +} + +/// Parses a `BlockId` from the given parameters at the specified position. +fn parse_block_id_from_params(params: &Params<'_>, position: usize) -> Option { + let values: Vec = params.parse().ok()?; + let val = values.into_iter().nth(position)?; + serde_json::from_value::(val).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_eips::{BlockId, BlockNumberOrTag}; + use jsonrpsee::types::Params; + + /// Tests that various valid id types can be parsed from the first parameter. + #[test] + fn parses_block_id_from_first_param() { + // Test with a block number + let params_num = Params::new(Some(r#"["0x64"]"#)); // 100 + assert_eq!( + parse_block_id_from_params(¶ms_num, 0).unwrap(), + BlockId::Number(BlockNumberOrTag::Number(100)) + ); + + // Test with the "earliest" tag + let params_tag = Params::new(Some(r#"["earliest"]"#)); + assert_eq!( + parse_block_id_from_params(¶ms_tag, 0).unwrap(), + BlockId::Number(BlockNumberOrTag::Earliest) + ); + } + + /// Tests that the function correctly parses from a position other than 0. + #[test] + fn parses_block_id_from_second_param() { + let params = + Params::new(Some(r#"["0x0000000000000000000000000000000000000000", "latest"]"#)); + let result = parse_block_id_from_params(¶ms, 1).unwrap(); + assert_eq!(result, BlockId::Number(BlockNumberOrTag::Latest)); + } + + /// Tests that the function returns nothing if the parameter is missing or empty. + #[test] + fn defaults_to_latest_when_param_is_missing() { + let params = Params::new(Some(r#"["0x0000000000000000000000000000000000000000"]"#)); + let result = parse_block_id_from_params(¶ms, 1); + assert!(result.is_none()); + } + + /// Tests that the function doesn't parse anyhing if the parameter is not a valid block id. + #[test] + fn returns_error_for_invalid_input() { + let params = Params::new(Some(r#"[true]"#)); + let result = parse_block_id_from_params(¶ms, 0); + assert!(result.is_none()); + } +}