From f778b4c6b10227e806577618904305aae27a5b8f Mon Sep 17 00:00:00 2001 From: osr21 Date: Thu, 17 Sep 2026 22:55:45 +0100 Subject: [PATCH] fix(rpc): return -32602 error for eth_getLogs with pending block tag Reth resolves `"pending"` as `head + 1` for log queries, silently returning `[]` rather than an error. Arc hides all pending-block state for public RPC nodes, so surface this as an explicit Invalid Params error instead of an empty array that misleads callers into thinking no logs exist at the current head. Fixes #426 --- crates/evm-node/src/rpc_middleware.rs | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/crates/evm-node/src/rpc_middleware.rs b/crates/evm-node/src/rpc_middleware.rs index aa01c635..15e6febb 100644 --- a/crates/evm-node/src/rpc_middleware.rs +++ b/crates/evm-node/src/rpc_middleware.rs @@ -70,6 +70,9 @@ const UNPROTECTED_TX_ERROR_MSG: &str = "only replay-protected (EIP-155) transactions allowed over RPC"; const RELAY_UNAVAILABLE_ERROR_CODE: i32 = -32010; const RELAY_UNAVAILABLE_ERROR_MSG: &str = "all transaction relay upstreams are unreachable"; +const ETH_GET_LOGS_METHOD: &str = "eth_getLogs"; +/// – Invalid params +const PENDING_LOGS_ERROR_CODE: i32 = -32602; /// Default maximum number of entries permitted in a JSON-RPC batch request. pub const ARC_RPC_MAX_BATCH_ENTRIES_DEFAULT: usize = 100; @@ -315,6 +318,47 @@ where } } +/// Returns an error if the request is `eth_getLogs` with a `"pending"` block tag. +/// +/// Reth resolves `"pending"` as `head + 1` for log queries and returns an empty +/// result set rather than coercing to `"latest"`. Arc intentionally hides all +/// pending-block state for public nodes, so surface this as an explicit +/// `-32602 Invalid params` error instead of silently returning `[]`, which +/// makes callers think no logs exist. +fn pending_logs_error<'a>(req: &Request<'a>) -> Option> { + if req.method_name() != ETH_GET_LOGS_METHOD { + return None; + } + // Parse the raw JSON rather than deserializing to alloy_rpc_types_eth::Filter + // to stay decoupled from proc-macro field names that can change across Reth + // versions. eth_getLogs accepts the filter either positionally ([{...}]) or + // under the "filter" key in object-form params. + let raw = req.params().as_str().unwrap_or(""); + let value: serde_json::Value = serde_json::from_str(raw).unwrap_or(serde_json::Value::Null); + let filter = match &value { + serde_json::Value::Array(arr) => arr.first().cloned(), + serde_json::Value::Object(obj) => obj.get("filter").cloned(), + _ => None, + }; + let has_pending = |key_camel: &str, key_snake: &str| -> bool { + filter + .as_ref() + .and_then(|f| f.get(key_camel).or_else(|| f.get(key_snake))) + .and_then(|v| v.as_str()) + .map(|s| s.eq_ignore_ascii_case("pending")) + .unwrap_or(false) + }; + if has_pending("fromBlock", "from_block") || has_pending("toBlock", "to_block") { + Some(ErrorObjectOwned::owned::<()>( + PENDING_LOGS_ERROR_CODE, + r#""pending" block tag is not supported for eth_getLogs on Arc; use "latest" instead"#, + None, + )) + } else { + None + } +} + /// Intercepts pending-tx RPCs (error or null) or forwards to the inner service. async fn intercept_or_forward<'a, S>(service: &S, req: Request<'a>) -> MethodResponse where @@ -323,6 +367,9 @@ where if let Err(err) = error_if_pending_tx_rpc(&req) { return MethodResponse::error(req.id(), err); } + if let Some(err) = pending_logs_error(&req) { + return MethodResponse::error(req.id(), err); + } if is_pool_pending_tx_lookup(&req) || is_pending_block_query(&req) { return null_response(&req); }