diff --git a/client/rpc-core/src/eth.rs b/client/rpc-core/src/eth.rs index 31a584e236..fc4bd6d9f7 100644 --- a/client/rpc-core/src/eth.rs +++ b/client/rpc-core/src/eth.rs @@ -271,15 +271,15 @@ pub trait EthApi { pub trait EthFilterApi { /// Returns id of new filter. #[method(name = "eth_newFilter")] - fn new_filter(&self, filter: Filter) -> RpcResult; + async fn new_filter(&self, filter: Filter) -> RpcResult; /// Returns id of new block filter. #[method(name = "eth_newBlockFilter")] - fn new_block_filter(&self) -> RpcResult; + async fn new_block_filter(&self) -> RpcResult; /// Returns id of new block filter. #[method(name = "eth_newPendingTransactionFilter")] - fn new_pending_transaction_filter(&self) -> RpcResult; + async fn new_pending_transaction_filter(&self) -> RpcResult; /// Returns filter changes since last poll. #[method(name = "eth_getFilterChanges")] diff --git a/client/rpc/src/eth/filter.rs b/client/rpc/src/eth/filter.rs index d5383d18c0..a0b4cef5c8 100644 --- a/client/rpc/src/eth/filter.rs +++ b/client/rpc/src/eth/filter.rs @@ -107,10 +107,31 @@ where Ok(number) } - fn create_filter(&self, filter_type: FilterType) -> RpcResult { + async fn create_filter(&self, filter_type: FilterType) -> RpcResult { let info = self.client.info(); let best_hash = info.best_hash; let best_number = UniqueSaturatedInto::::unique_saturated_into(info.best_number); + // Reject log filters with block range exceeding limit (same as eth_getLogs). + if let FilterType::Log(ref filter) = filter_type { + let latest_indexed_number = self.latest_indexed_block_number().await?; + let from_num = filter + .from_block + .and_then(|b| b.to_min_block_num()) + .map(|s| s.unique_saturated_into()) + .unwrap_or(latest_indexed_number); + let to_num = filter + .to_block + .and_then(|b| b.to_min_block_num()) + .map(|s| s.unique_saturated_into()) + .unwrap_or(latest_indexed_number); + let block_range = to_num.saturating_sub(from_num); + if block_range > self.max_block_range.into() { + return Err(internal_err(format!( + "block range is too wide (maximum {})", + self.max_block_range + ))); + } + } let pool = self.filter_pool.clone(); let response = if let Ok(locked) = &mut pool.lock() { if locked.len() >= self.max_stored_filters { @@ -176,16 +197,16 @@ where BE: Backend + 'static, P: TransactionPool + 'static, { - fn new_filter(&self, filter: Filter) -> RpcResult { - self.create_filter(FilterType::Log(filter)) + async fn new_filter(&self, filter: Filter) -> RpcResult { + self.create_filter(FilterType::Log(filter)).await } - fn new_block_filter(&self) -> RpcResult { - self.create_filter(FilterType::Block) + async fn new_block_filter(&self) -> RpcResult { + self.create_filter(FilterType::Block).await } - fn new_pending_transaction_filter(&self) -> RpcResult { - self.create_filter(FilterType::PendingTransaction) + async fn new_pending_transaction_filter(&self) -> RpcResult { + self.create_filter(FilterType::PendingTransaction).await } async fn filter_changes(&self, index: Index) -> RpcResult { @@ -307,27 +328,36 @@ where .unwrap_or(last_poll); let from_number = std::cmp::max(last_poll, filter_from); - - // Update filter `last_poll` based on the same capped head we query. - // This avoids skipping blocks when best_number is ahead of indexed data. - let next_last_poll = - UniqueSaturatedInto::::unique_saturated_into(current_number) - .saturating_add(1); - locked.insert( - key, - FilterPoolItem { - last_poll: BlockNumberOrHash::Num(next_last_poll), - filter_type: pool_item.filter_type.clone(), - at_block: pool_item.at_block, - pending_transaction_hashes: HashSet::new(), - }, - ); - - // Build the response. - FuturePath::Log { - filter: filter.clone(), - from_number, - current_number, + let block_range = current_number.saturating_sub(from_number); + + // Validate block range before advancing last_poll. If we reject after + // updating last_poll, the cursor would skip logs on the next poll. + if block_range > self.max_block_range.into() { + FuturePath::Error(internal_err(format!( + "block range is too wide (maximum {})", + self.max_block_range + ))) + } else { + // Update filter `last_poll` based on the same capped head we query. + // This avoids skipping blocks when best_number is ahead of indexed data. + let next_last_poll = + UniqueSaturatedInto::::unique_saturated_into(current_number) + .saturating_add(1); + locked.insert( + key, + FilterPoolItem { + last_poll: BlockNumberOrHash::Num(next_last_poll), + filter_type: pool_item.filter_type.clone(), + at_block: pool_item.at_block, + pending_transaction_hashes: HashSet::new(), + }, + ); + // Build the response. + FuturePath::Log { + filter: filter.clone(), + from_number, + current_number, + } } } } @@ -442,6 +472,14 @@ where .map(|s| s.unique_saturated_into()) .unwrap_or(latest_number); + let block_range = current_number.saturating_sub(from_number); + if block_range > self.max_block_range.into() { + return Err(internal_err(format!( + "block range is too wide (maximum {})", + self.max_block_range + ))); + } + let logs = if backend.is_indexed() { filter_range_logs_indexed( client.as_ref(), diff --git a/ts-tests/tests/test-filter-block-range-limit.ts b/ts-tests/tests/test-filter-block-range-limit.ts new file mode 100644 index 0000000000..9e2d92018b --- /dev/null +++ b/ts-tests/tests/test-filter-block-range-limit.ts @@ -0,0 +1,109 @@ +import { expect } from "chai"; +import { step } from "mocha-steps"; + +import { createAndFinalizeBlock, describeWithFrontier, customRequest, waitForBlock } from "./util"; + +const MAX_BLOCK_RANGE = 10; +const BLOCK_RANGE_ERROR_MSG = `block range is too wide (maximum ${MAX_BLOCK_RANGE})`; + +describeWithFrontier( + "Frontier RPC (Filter/GetLogs block range limit)", + (context) => { + step("eth_getLogs with wide block range should be rejected", async function () { + // Produce enough blocks so the requested range is not capped by chain height. + // Requested range must exceed MAX_BLOCK_RANGE so the server rejects it. + const blocksToProduce = MAX_BLOCK_RANGE + 5; + for (let i = 0; i < MAX_BLOCK_RANGE + 5; i++) { + await createAndFinalizeBlock(context.web3); + } + await waitForBlock(context.web3, "0x" + blocksToProduce.toString(16), 10000); + + const r = await customRequest(context.web3, "eth_getLogs", [ + { + fromBlock: "0x0", + toBlock: "0x" + blocksToProduce.toString(16), // range = blocksToProduce > MAX_BLOCK_RANGE + }, + ]); + expect(r.error).to.not.be.undefined; + expect(r.error.message).to.include(BLOCK_RANGE_ERROR_MSG); + }); + + step("eth_newFilter with wide numeric block range should be rejected", async function () { + const r = await customRequest(context.web3, "eth_newFilter", [ + { + fromBlock: "0x0", + toBlock: "0x20", // 32 blocks > MAX_BLOCK_RANGE + address: "0x0000000000000000000000000000000000000000", + }, + ]); + expect(r.error).to.not.be.undefined; + expect(r.error.message).to.include(BLOCK_RANGE_ERROR_MSG); + }); + + step("eth_newFilter with valid block range should succeed", async function () { + const r = await customRequest(context.web3, "eth_newFilter", [ + { + fromBlock: "0x0", + toBlock: "0x5", // 5 blocks <= MAX_BLOCK_RANGE + address: "0x0000000000000000000000000000000000000000", + }, + ]); + expect(r.error).to.be.undefined; + expect(r.result).to.not.be.undefined; + }); + + step("eth_getFilterLogs with effective range exceeding limit should be rejected", async function () { + // Create filter when chain has few blocks (e.g. 2–3 after genesis). + const b = await context.web3.eth.getBlockNumber(); + const fromBlock = `0x${(b - MAX_BLOCK_RANGE).toString(16)}`; + const createFilter = await customRequest(context.web3, "eth_newFilter", [ + { + fromBlock, + toBlock: "latest", + address: "0x0000000000000000000000000000000000000000", + }, + ]); + expect(createFilter.error).to.be.undefined; + const filterId = createFilter.result; + + // Produce enough blocks so effective range (0 to latest) > MAX_BLOCK_RANGE. + for (let i = 0; i < MAX_BLOCK_RANGE; i++) { + await createAndFinalizeBlock(context.web3); + } + await waitForBlock(context.web3, "latest", 5000); + + const poll = await customRequest(context.web3, "eth_getFilterLogs", [filterId]); + expect(poll.error).to.not.be.undefined; + expect(poll.error.message).to.include(BLOCK_RANGE_ERROR_MSG); + }); + + step("eth_getFilterChanges with effective range exceeding limit should be rejected", async function () { + const b = await context.web3.eth.getBlockNumber(); + const fromBlock = `0x${(b - MAX_BLOCK_RANGE).toString(16)}`; + const createFilter = await customRequest(context.web3, "eth_newFilter", [ + { + fromBlock, + toBlock: "latest", + address: "0x0000000000000000000000000000000000000000", + }, + ]); + expect(createFilter.error).to.be.undefined; + const filterId = createFilter.result; + + // Produce one more than MAX_BLOCK_RANGE so the poll range (last_poll..latest) + // exceeds the limit. getFilterChanges uses from_number = max(last_poll, filter_from); + // at creation last_poll = best (e.g. 25), so after N blocks range = N (25 to 25+N). + // We need N > MAX_BLOCK_RANGE. + for (let i = 0; i < MAX_BLOCK_RANGE + 1; i++) { + await createAndFinalizeBlock(context.web3); + } + await waitForBlock(context.web3, "latest", 5000); + + const poll = await customRequest(context.web3, "eth_getFilterChanges", [filterId]); + expect(poll.error).to.not.be.undefined; + expect(poll.error.message).to.include(BLOCK_RANGE_ERROR_MSG); + }); + }, + undefined, + [`--max-block-range=${MAX_BLOCK_RANGE}`] +);