diff --git a/CHANGELOG.md b/CHANGELOG.md index 650755d6ab3a..c1110f75fda9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ - [#6849](https://github.com/ChainSafe/forest/pull/6849): Included strict bound in blocks included for calculating gas premium `GasEstimateGasPremium`. +- [#6856](https://github.com/ChainSafe/forest/pull/6856): Return ethereum compatible error `BlockRangeExceeded` with code `-32005` when block range exceeds in the eth filter and logs API. + ## Forest v0.32.4 "Mild Inconvenience" This is a non-mandatory release for all node operators. It enables F3 finality resolution on ETH v1 RPC methods. diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index a80fb66743d2..d978338e4e59 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -3161,11 +3161,20 @@ impl RpcMethod<1> for EthGetLogs { ) -> Result { let pf = ctx .eth_event_handler - .parse_eth_filter_spec(&ctx, ð_filter)?; + .parse_eth_filter_spec(&ctx, ð_filter) + .map_err(|e| { + if e.downcast_ref::() + .is_some_and(|eth_err| matches!(eth_err, EthErrors::BlockRangeExceeded { .. })) + { + return e; + } + e.context("failed to parse events for filter") + })?; let events = ctx .eth_event_handler .get_events_for_parsed_filter(&ctx, &pf, SkipEvent::OnUnresolvedAddress) - .await?; + .await + .context("failed to get events for filter")?; Ok(eth_filter_result_from_events(&ctx, &events)?) } } @@ -3858,6 +3867,14 @@ impl RpcMethod<1> for EthTraceFilter { .await .context("cannot parse toBlock")?; + let max_block_range = ctx.eth_event_handler.max_filter_height_range; + if max_block_range > 0 && to_block.0 > from_block.0 { + let range = i64::try_from(to_block.0.saturating_sub(from_block.0)) + .context("block range overflow")?; + if range > max_block_range { + return Err(EthErrors::limit_exceeded(max_block_range as u64, range as u64).into()); + } + } Ok(trace_filter(ctx, filter, from_block, to_block, ext).await?) } } diff --git a/src/rpc/methods/eth/errors.rs b/src/rpc/methods/eth/errors.rs index 9edd08a5265b..fe0d725f060f 100644 --- a/src/rpc/methods/eth/errors.rs +++ b/src/rpc/methods/eth/errors.rs @@ -10,11 +10,20 @@ use thiserror::Error; /// This error indicates that the execution reverted while executing the message. /// Error code 3 was introduced in geth v1.9.15 and is now expected by most Ethereum ecosystem tooling for automatic ABI decoding of revert reasons from the error data field. pub const EXECUTION_REVERTED_CODE: i32 = 3; +/// This error indicates that the block range provided in the RPC exceeds the configured maximum +/// It was introduced in EIP-1474 +pub const LIMIT_EXCEEDED_CODE: i32 = -32005; #[derive(Clone, Debug, Error, Serialize)] pub enum EthErrors { #[error("{message}")] ExecutionReverted { message: String, data: String }, + #[error("{message}")] + BlockRangeExceeded { + max: u64, + given: u64, + message: String, + }, } impl EthErrors { @@ -33,18 +42,28 @@ impl EthErrors { data: format!("0x{}", hex::encode(data)), } } + + pub fn limit_exceeded(max_block_range: u64, given: u64) -> Self { + Self::BlockRangeExceeded { + max: max_block_range, + given, + message: format!("block range exceeds maximum of {max_block_range} (got {given})"), + } + } } impl RpcErrorData for EthErrors { fn error_code(&self) -> Option { match self { EthErrors::ExecutionReverted { .. } => Some(EXECUTION_REVERTED_CODE), + EthErrors::BlockRangeExceeded { .. } => Some(LIMIT_EXCEEDED_CODE), } } fn error_message(&self) -> Option { match self { EthErrors::ExecutionReverted { message, .. } => Some(message.clone()), + EthErrors::BlockRangeExceeded { message, .. } => Some(message.clone()), } } @@ -53,6 +72,38 @@ impl RpcErrorData for EthErrors { EthErrors::ExecutionReverted { data, .. } => { Some(serde_json::Value::String(data.clone())) } + EthErrors::BlockRangeExceeded { .. } => None, } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::rpc::error::ServerError; + + #[test] + fn test_block_range_exceeded_converts_to_server_error_with_correct_code() { + let err = EthErrors::limit_exceeded(100, 500); + let server_err: ServerError = err.into(); + + assert_eq!(server_err.inner().code(), LIMIT_EXCEEDED_CODE); + assert_eq!( + server_err.message(), + "block range exceeds maximum of 100 (got 500)" + ); + } + + #[test] + fn test_block_range_exceeded_via_anyhow_preserves_code() { + let eth_err = EthErrors::limit_exceeded(2880, 5000); + let anyhow_err: anyhow::Error = eth_err.into(); + let server_err: ServerError = anyhow_err.into(); + + assert_eq!(server_err.inner().code(), LIMIT_EXCEEDED_CODE); + assert_eq!( + server_err.message(), + "block range exceeds maximum of 2880 (got 5000)" + ); + } +} diff --git a/src/rpc/methods/eth/filter/mod.rs b/src/rpc/methods/eth/filter/mod.rs index 7fd1e2695965..a06922f649b7 100644 --- a/src/rpc/methods/eth/filter/mod.rs +++ b/src/rpc/methods/eth/filter/mod.rs @@ -28,6 +28,7 @@ use crate::blocks::TipsetKey; use crate::chain::index::ResolveNullTipset; use crate::cli_shared::cli::EventsConfig; use crate::rpc::eth::EVM_WORD_LENGTH; +use crate::rpc::eth::errors::EthErrors; use crate::rpc::eth::filter::event::*; use crate::rpc::eth::filter::mempool::*; use crate::rpc::eth::filter::tipset::*; @@ -552,14 +553,12 @@ fn parse_block_range( if min_height == -1 && max_height > 0 { ensure!( max_height - heaviest <= max_range, - "invalid epoch range: to block is too far in the future (maximum: {})", - max_range + EthErrors::limit_exceeded(max_range as u64, (max_height - heaviest) as u64), ); } else if min_height >= 0 && max_height == -1 { ensure!( heaviest - min_height <= max_range, - "invalid epoch range: from block is too far in the past (maximum: {})", - max_range + EthErrors::limit_exceeded(max_range as u64, (heaviest - min_height) as u64) ); } else if min_height >= 0 && max_height >= 0 { ensure!( @@ -570,8 +569,7 @@ fn parse_block_range( ); ensure!( max_height - min_height <= max_range, - "invalid epoch range: range between to and from blocks is too large (maximum: {})", - max_range + EthErrors::limit_exceeded(max_range as u64, (max_height - min_height) as u64) ); } @@ -929,6 +927,9 @@ mod tests { #[test] fn test_parse_block_range() { + use crate::rpc::error::RpcErrorData; + use crate::rpc::eth::errors::LIMIT_EXCEEDED_CODE; + let heaviest = 50; let max_range = 100; @@ -956,14 +957,17 @@ mod tests { assert_eq!(min_height, 1); // hex_str_to_epoch("0x1") = 1 assert_eq!(max_height, 10); // hex_str_to_epoch("0xA") = 10 - // Test case 3: Range too large - let result = parse_block_range( + // Test case 3: Range too large — returns BlockRangeExceeded with -32005 code + let err = parse_block_range( heaviest, Some(BlockNumberOrHash::from_str("earliest").unwrap()), - Some(BlockNumberOrHash::from_str("0x100").unwrap()), + Some(BlockNumberOrHash::from_str("0x100").unwrap()), // epoch 256, range = 256 > 100 max_range, - ); - assert!(result.is_err()); + ) + .unwrap_err(); + let eth_err = err.downcast_ref::().expect("expected EthErrors"); + assert!(matches!(eth_err, EthErrors::BlockRangeExceeded { .. })); + assert_eq!(eth_err.error_code(), Some(LIMIT_EXCEEDED_CODE)); // Test case 4: from_block = "latest", to_block = "earliest" let result = parse_block_range( @@ -1008,13 +1012,23 @@ mod tests { assert!(result.is_err()); // Test case 8: Both blocks are non-negative, order is correct, but the range is too large. - let result = parse_block_range( + let err = parse_block_range( heaviest, Some(BlockNumberOrHash::from_str("earliest").unwrap()), - Some(BlockNumberOrHash::from_str("0x65").unwrap()), + Some(BlockNumberOrHash::from_str("0x65").unwrap()), // epoch 101, range = 101 > 100 max_range, - ); - assert!(result.is_err()); + ) + .unwrap_err(); + let eth_err = err.downcast_ref::().expect("expected EthErrors"); + assert!(matches!( + eth_err, + EthErrors::BlockRangeExceeded { + max: 100, + given: 101, + .. + } + )); + assert_eq!(eth_err.error_code(), Some(LIMIT_EXCEEDED_CODE)); // Test case 9: Range exactly equal to max_range (boundary, should succeed). let result = parse_block_range( @@ -1081,6 +1095,28 @@ mod tests { let (min_height, max_height) = result.unwrap(); assert_eq!(min_height, heaviest); assert_eq!(max_height, -1); + + // Test case 15: from_block too far in past (min >= 0, max == -1, heaviest - min > max_range) + let err = parse_block_range( + 500, // heaviest + Some(BlockNumberOrHash::from_str("0x1").unwrap()), // epoch 1, range = 500 - 1 = 499 + Some(BlockNumberOrHash::from_str("latest").unwrap()), + max_range, + ) + .unwrap_err(); + let eth_err = err.downcast_ref::().expect("expected EthErrors"); + assert!(matches!( + eth_err, + EthErrors::BlockRangeExceeded { + max: 100, + given: 499, + .. + } + )); + assert_eq!( + eth_err.error_message().unwrap(), + "block range exceeds maximum of 100 (got 499)" + ); } #[test]