Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions client/rpc-core/src/eth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<U256>;
async fn new_filter(&self, filter: Filter) -> RpcResult<U256>;

/// Returns id of new block filter.
#[method(name = "eth_newBlockFilter")]
fn new_block_filter(&self) -> RpcResult<U256>;
async fn new_block_filter(&self) -> RpcResult<U256>;

/// Returns id of new block filter.
#[method(name = "eth_newPendingTransactionFilter")]
fn new_pending_transaction_filter(&self) -> RpcResult<U256>;
async fn new_pending_transaction_filter(&self) -> RpcResult<U256>;

/// Returns filter changes since last poll.
#[method(name = "eth_getFilterChanges")]
Expand Down
94 changes: 66 additions & 28 deletions client/rpc/src/eth/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,31 @@ where
Ok(number)
}

fn create_filter(&self, filter_type: FilterType) -> RpcResult<U256> {
async fn create_filter(&self, filter_type: FilterType) -> RpcResult<U256> {
let info = self.client.info();
let best_hash = info.best_hash;
let best_number = UniqueSaturatedInto::<u64>::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 {
Expand Down Expand Up @@ -176,16 +197,16 @@ where
BE: Backend<B> + 'static,
P: TransactionPool<Block = B, Hash = B::Hash> + 'static,
{
fn new_filter(&self, filter: Filter) -> RpcResult<U256> {
self.create_filter(FilterType::Log(filter))
async fn new_filter(&self, filter: Filter) -> RpcResult<U256> {
self.create_filter(FilterType::Log(filter)).await
}

fn new_block_filter(&self) -> RpcResult<U256> {
self.create_filter(FilterType::Block)
async fn new_block_filter(&self) -> RpcResult<U256> {
self.create_filter(FilterType::Block).await
}

fn new_pending_transaction_filter(&self) -> RpcResult<U256> {
self.create_filter(FilterType::PendingTransaction)
async fn new_pending_transaction_filter(&self) -> RpcResult<U256> {
self.create_filter(FilterType::PendingTransaction).await
}

async fn filter_changes(&self, index: Index) -> RpcResult<FilterChanges> {
Expand Down Expand Up @@ -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::<u64>::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::<u64>::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,
}
}
}
}
Expand Down Expand Up @@ -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(),
Expand Down
109 changes: 109 additions & 0 deletions ts-tests/tests/test-filter-block-range-limit.ts
Original file line number Diff line number Diff line change
@@ -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}`]
);
Loading