Skip to content

eth/filters, rpc: add --rpc.rangelimit flag and fix filter config pass-through#2147

Merged
lucca30 merged 3 commits intov2.6.4-candidatefrom
lmartins/fix-rpc-log-query-limit
Mar 18, 2026
Merged

eth/filters, rpc: add --rpc.rangelimit flag and fix filter config pass-through#2147
lucca30 merged 3 commits intov2.6.4-candidatefrom
lmartins/fix-rpc-log-query-limit

Conversation

@lucca30
Copy link
Copy Markdown
Contributor

@lucca30 lucca30 commented Mar 18, 2026

Description

Summary

This PR cherry-picks two upstream changes and adapts them to Bor:

  • go-ethereum#33163 — introduces a --rpc.rangelimit CLI flag that caps the maximum block range (end - begin) accepted by eth_getLogs and eth_newFilter, protecting RPC nodes from expensive wide-range queries.
  • bor#2146 — fixes eth/backend.go where filters.NewFilterSystem was called with an empty filters.Config{}, silently discarding LogCacheSize, LogQueryLimit, and (now) RangeLimit values for the bor_getLogs API path.

Bor-specific adaptation

Bor exposes two log query endpoints: eth_getLogs (FilterAPI.GetLogs) and bor_getLogs (FilterAPI.GetBorBlockLogs). The range limit is enforced on both.

The flag is wired through Bor's primary flag pipeline (internal/cli/server/flags.goJsonRPCConfigethconfig.Configfilters.Config) in addition to the legacy cmd/utils/flags.go path for cmd/geth compatibility.


Changes

eth/filters/filter_system.go

  • Added RangeLimit uint64 to filters.Config (0 = unlimited).

eth/ethconfig/config.go

  • Added RPCBlockRangeLimit uint64 to ethconfig.Config (default 0 — operators opt-in).

internal/cli/server/config.go

  • Added RangeLimit uint64 field to JsonRPCConfig (HCL/TOML key: rangelimit).
  • Maps JsonRPC.RangeLimitethconfig.RPCBlockRangeLimit during config build.

internal/cli/server/flags.go (primary flag location)

  • Added --rpc.rangelimit Uint64Flag in the JsonRPC group.

cmd/utils/flags.go

  • Added RPCGlobalRangeLimitFlag (--rpc.rangelimit) alongside RPCGlobalLogQueryLimit.
  • Wired into setEthConfig via ctx.IsSet guard.
  • Updated RegisterFilterAPI to pass RangeLimit to filters.NewFilterSystem.

cmd/geth/main.go

  • Registered RPCGlobalRangeLimitFlag in the node flag list.

eth/backend.go (fix from bor#2146)

  • Replaced the empty filters.Config{} in APIs() with the full config, passing FilterLogCacheSize, RPCLogQueryLimit, and RPCBlockRangeLimit.

eth/filters/api.go

  • Added errExceedBlockRangeLimit error sentinel.
  • Added range check in GetLogs after begin/end are resolved, before filter construction.

eth/filters/bor_api.go

  • Added the same range check in GetBorBlockLogs.

eth/filters/filter_system_test.go

  • Added TestRangeLimit covering:
    • Ranges exceeding the configured limit → errExceedBlockRangeLimit for both GetLogs and GetBorBlockLogs.
    • Range within the limit → no range error.
    • RangeLimit = 0 (unlimited) → large ranges are never rejected.

Usage

# Reject eth_getLogs / bor_getLogs queries spanning more than 1000 blocks
bor --rpc.rangelimit 1000

# No limit (default)
bor --rpc.rangelimit 0

The flag is also configurable via the HCL/TOML config file:

[jsonrpc]
  rangelimit = 1000

Changes

  • Bugfix (non-breaking change that solves an issue)
  • Hotfix (change that solves an urgent issue, and requires immediate attention)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (change that is not backwards-compatible and/or changes current functionality)
  • Changes only for a subset of nodes

Breaking changes

Please complete this section if any breaking changes have been made, otherwise delete it

Nodes audience

In case this PR includes changes that must be applied only to a subset of nodes, please specify how you handled it (e.g. by adding a flag with a default value...)

Checklist

  • I have added at least 2 reviewer or the whole pos-v1 team
  • I have added sufficient documentation in code
  • I will be resolving comments - if any - by pushing each fix in a separate commit and linking the commit hash in the comment reply
  • Created a task in Jira and informed the team for implementation in Erigon client (if applicable)
  • Includes RPC methods changes, and the Notion documentation has been updated

Cross repository changes

  • This PR requires changes to heimdall
    • In case link the PR here:
  • This PR requires changes to matic-cli
    • In case link the PR here:

Testing

  • I have added unit tests
  • I have added tests to CI
  • I have tested this code manually on local environment
  • I have tested this code manually on remote devnet using express-cli
  • I have tested this code manually on amoy
  • I have created new e2e tests into express-cli

Manual tests

Please complete this section with the steps you performed if you ran manual tests for this functionality, otherwise delete it

Additional comments

Please post additional comments in this section if you have them, otherwise delete it

@lucca30 lucca30 requested review from a team and cffls March 18, 2026 19:56
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review.

@adamdossa
Copy link
Copy Markdown
Contributor

@claude review

@claude
Copy link
Copy Markdown

claude bot commented Mar 18, 2026

Claude finished @adamdossa's task in 1m 27s —— View job


Code Review: --rpc.rangelimit flag and filter config pass-through fix

  • Read all changed files
  • Review for bugs, security issues, and correctness
  • Check test coverage
  • Post review feedback

Overall this is a clean, well-structured PR. The backend.go fix is a real bug fix (config was silently discarded), and the range limit feature is straightforward. A few observations:

Issues

1. Missing begin > end validation in GetBorBlockLogs (medium)

GetLogs (api.go:488) validates begin > end before the range check:

if begin > 0 && end > 0 && begin > end {
    return nil, errInvalidBlockRange
}

GetBorBlockLogs (bor_api.go:31-47) does not have this check. This is a pre-existing gap, but the range limit check added here inherits the same assumption — if begin > end, the uint64(end - begin) cast wraps around to a very large number, which would trigger errExceedBlockRangeLimit instead of the more descriptive errInvalidBlockRange. Consider adding the same begin > end guard in bor_api.go for consistency.

Fix this →

2. eth_newFilter not covered by range limit (low, worth noting)

The PR description mentions eth_newFilter, but the range limit is only enforced in GetLogs and GetBorBlockLogs. NewFilter (api.go:407) creates a subscription filter and doesn't go through the same code path. This is consistent with upstream geth behavior (subscriptions are incremental, not historical range scans), so it's fine — but the PR description could be clarified.

3. Missing HistoryPruningCutoff check in GetBorBlockLogs (low, pre-existing)

GetLogs checks HistoryPruningCutoff before the range filter, but GetBorBlockLogs doesn't. This is pre-existing and out of scope, but worth noting for a follow-up.

Minor Observations

4. Duplicate LogQueryLimit fields in ethconfig.Config

There are two fields: LogQueryLimit (line 159, used by cmd/geth legacy path) and RPCLogQueryLimit (line 206, used by bor CLI). Both default to 1000. The backend.go fix correctly uses RPCLogQueryLimit. The new RPCBlockRangeLimit follows the RPC* naming convention — good.

5. Test quality

TestRangeLimit is thorough — it covers over-limit, within-limit, and unlimited (0) cases for both GetLogs and GetBorBlockLogs. The _ = bc / _ = bc2 at the end suppress unused warnings; this is fine for test code.

One suggestion: consider adding a boundary test case where end - begin == limit (exactly at the limit) to verify it passes, in addition to the existing limit + 1 case.

Summary

The PR is good to go with one actionable suggestion:

# Severity Issue Actionable?
1 Medium Missing begin > end check in GetBorBlockLogs Yes — add the guard
2 Low PR description mentions eth_newFilter but limit isn't enforced there Clarify description
3 Low Missing HistoryPruningCutoff in GetBorBlockLogs (pre-existing) Follow-up

The backend.go fix alone (passing real config instead of filters.Config{}) is valuable and should prevent future config fields from being silently dropped.


@kamuikatsurgi
Copy link
Copy Markdown
Member

Could you run make docs as well?

@lucca30
Copy link
Copy Markdown
Contributor Author

lucca30 commented Mar 18, 2026

@kamuikatsurgi , I did but it was updating some flags and removing others. I think if i reflect these changes here, when bringing to develop it will drop flags that exists today

Copy link
Copy Markdown
Member

@kamuikatsurgi kamuikatsurgi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Copy link
Copy Markdown
Contributor

@cffls cffls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@sonarqubecloud
Copy link
Copy Markdown

@lucca30 lucca30 merged commit 70f86c4 into v2.6.4-candidate Mar 18, 2026
5 of 8 checks passed
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 18, 2026

Codecov Report

❌ Patch coverage is 86.95652% with 6 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (v2.6.4-candidate@d72f1cf). Learn more about missing BASE report.

Files with missing lines Patch % Lines
eth/filters/api.go 88.46% 2 Missing and 1 partial ⚠️
eth/filters/bor_api.go 57.14% 2 Missing and 1 partial ⚠️

❌ Your patch status has failed because the patch coverage (86.95%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@                 Coverage Diff                 @@
##             v2.6.4-candidate    #2147   +/-   ##
===================================================
  Coverage                    ?   50.05%           
===================================================
  Files                       ?      878           
  Lines                       ?   151740           
  Branches                    ?        0           
===================================================
  Hits                        ?    75959           
  Misses                      ?    70692           
  Partials                    ?     5089           
Files with missing lines Coverage Δ
eth/backend.go 52.34% <100.00%> (ø)
eth/ethconfig/config.go 37.50% <ø> (ø)
eth/filters/filter_system.go 80.41% <ø> (ø)
internal/cli/server/config.go 63.56% <100.00%> (ø)
internal/cli/server/flags.go 100.00% <100.00%> (ø)
params/version.go 23.68% <ø> (ø)
eth/filters/api.go 53.87% <88.46%> (ø)
eth/filters/bor_api.go 36.06% <57.14%> (ø)
Files with missing lines Coverage Δ
eth/backend.go 52.34% <100.00%> (ø)
eth/ethconfig/config.go 37.50% <ø> (ø)
eth/filters/filter_system.go 80.41% <ø> (ø)
internal/cli/server/config.go 63.56% <100.00%> (ø)
internal/cli/server/flags.go 100.00% <100.00%> (ø)
params/version.go 23.68% <ø> (ø)
eth/filters/api.go 53.87% <88.46%> (ø)
eth/filters/bor_api.go 36.06% <57.14%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines +65 to +75
// - all other sentinels (latest, safe, finalized, pending) → head
// - concrete non-negative values → unchanged
func resolveBlockNumForRangeCheck(n int64, head uint64) uint64 {
if n >= 0 {
return uint64(n)
}
if n == rpc.EarliestBlockNumber.Int64() {
return 0
}
return head // latest, safe, finalized, pending all treated as head
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 resolveBlockNumForRangeCheck maps finalized (-3) and safe (-4) to head, but the actual filter in resolveSpecial (filter.go:112-123) resolves them to their real block heights via HeaderByNumber. This allows complete bypass of --rpc.rangelimit: e.g., with head=10000, finalized=9500, limit=100, a query fromBlock=finalized, toBlock=latest passes the check (range=0) but scans 500 blocks. Additionally, earliest (-5) is mapped to 0 instead of HistoryPruningCutoff(), causing false rejections on pruned nodes.

Extended reasoning...

The Bug

The newly introduced resolveBlockNumForRangeCheck function (api.go:65-75) is responsible for converting symbolic block numbers into concrete heights for the range limit guard. However, it does not match the actual resolution behavior in filter.go:resolveSpecial (lines 104-137), leading to two distinct issues:

1. Range limit bypass via finalized/safe sentinels

resolveBlockNumForRangeCheck maps FinalizedBlockNumber (-3) and SafeBlockNumber (-4) to head (line 74), treating them identically to LatestBlockNumber. However, the actual filter execution in resolveSpecial resolves these to their real block heights by calling HeaderByNumber(ctx, rpc.FinalizedBlockNumber) (line 113) and HeaderByNumber(ctx, rpc.SafeBlockNumber) (line 119), which can return values significantly behind head.

Step-by-step proof:

  1. Node has head at block 10000, finalized at block 9500, --rpc.rangelimit set to 100.
  2. User calls eth_getLogs with fromBlock: "finalized", toBlock: "latest".
  3. checkBlockRangeLimit calls resolveBlockNumForRangeCheck(-3, 10000) which returns 10000 (head), and resolveBlockNumForRangeCheck(-2, 10000) which also returns 10000.
  4. The computed range = 10000 - 10000 = 0, which is <= 100, so the check passes.
  5. The actual filter in resolveSpecial resolves finalized to 9500 (via HeaderByNumber) and latest to MaxUint64 (which maps to head=10000 during iteration).
  6. The filter scans blocks 9500-10000 = 500 blocks, which is 5x the configured 100-block limit.

On Polygon/Bor specifically, the finalized block can lag significantly behind head due to Heimdall milestone/checkpoint delays. This completely defeats the purpose of --rpc.rangelimit, which exists to protect RPC nodes from expensive wide-range log queries.

2. False rejection of earliest-based queries on pruned nodes (minor)

resolveBlockNumForRangeCheck maps EarliestBlockNumber (-5) to 0 (line 71-72). However, resolveSpecial maps it to HistoryPruningCutoff() (line 125), which on pruned nodes can be much higher than 0.

Step-by-step proof:

  1. Node is pruned with HistoryPruningCutoff = 74995000, head = 75000000, --rpc.rangelimit = 10000.
  2. User calls eth_getLogs with fromBlock: "earliest", toBlock: "latest".
  3. Range check resolves earliest to 0, latest to 75000000. Range = 75000000 > 10000 → REJECTED.
  4. Note: The HistoryPruningCutoff guard at api.go line 523 is skipped because begin = -5 fails the begin >= 0 check.
  5. But the actual filter resolves earliest to 74995000 and latest to head (75000000), scanning only 5000 blocks — well within the 10000 limit.
  6. A valid query is wrongly rejected.

This second issue is less severe (false rejection is conservative/safe, and users can work around it with concrete block numbers), but both stem from the same root cause.

Fix

resolveBlockNumForRangeCheck should resolve finalized/safe to their actual block heights via HeaderByNumber (or accept pre-resolved values), and resolve earliest to HistoryPruningCutoff() instead of 0. This requires the function to have access to the backend, so the signature would need to change.

Comment on lines 2241 to 2247
filterSystem := filters.NewFilterSystem(backend, filters.Config{
LogCacheSize: ethcfg.FilterLogCacheSize,
LogQueryLimit: ethcfg.LogQueryLimit,
RangeLimit: ethcfg.RPCBlockRangeLimit,
})

filterAPI := filters.NewFilterAPI(filterSystem, ethcfg.BorLogs)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 Pre-existing bug: RegisterFilterAPI creates two separate NewFilterAPI instances — one stored in filterAPI (line 2247) and a different one registered as the RPC service (line 2250). SetChainConfig is called on the local filterAPI variable which is then discarded, while the registered instance retains chainConfig=nil. This causes bor_getLogs calls through the cmd/geth path to always fail with "no chain config found". The fix is a one-liner: use filterAPI at line 2250 instead of creating a new instance.

Extended reasoning...

What the bug is

In cmd/utils/flags.go:RegisterFilterAPI (lines 2241-2254), two separate filters.NewFilterAPI instances are created:

  1. Line 2247: filterAPI := filters.NewFilterAPI(filterSystem, ethcfg.BorLogs) — stored in a local variable
  2. Line 2250: Service: filters.NewFilterAPI(filterSystem, ethcfg.BorLogs) — a completely different instance, registered with stack.RegisterAPIs

Then on line 2254, filterAPI.SetChainConfig(ethcfg.Genesis.Config) is called on instance #1 (the local variable). Instance #2 (the one actually serving RPC requests) never receives SetChainConfig, so its chainConfig field remains nil.

How it manifests

In eth/filters/bor_api.go:GetBorBlockLogs (line 19), the very first check is:

if api.chainConfig == nil {
    return nil, errors.New("no chain config found. Proper PublicFilterAPI initialization required")
}

Since the registered instance has chainConfig == nil, every bor_getLogs RPC call through the cmd/geth path will immediately return this error. The bor_getLogs endpoint is effectively broken for nodes started via the cmd/geth entry point.

Step-by-step proof

  1. User starts a node using cmd/geth (which calls RegisterFilterAPI)
  2. RegisterFilterAPI creates filterAPI (instance MAT-156 Span sync #1) at line 2247
  3. At line 2250, a new filters.NewFilterAPI(filterSystem, ethcfg.BorLogs) call creates instance build(deps): bump mixin-deep from 1.3.1 to 1.3.2 in /dashboard/assets #2 and registers it as the eth namespace RPC service
  4. filterAPI.SetChainConfig(ethcfg.Genesis.Config) at line 2254 sets chain config on instance MAT-156 Span sync #1
  5. Instance MAT-156 Span sync #1 (filterAPI) is never used again — it is not returned, stored, or referenced
  6. A bor_getLogs RPC request arrives and is routed to instance build(deps): bump mixin-deep from 1.3.1 to 1.3.2 in /dashboard/assets #2
  7. Instance build(deps): bump mixin-deep from 1.3.1 to 1.3.2 in /dashboard/assets #2 checks api.chainConfig == niltrue → returns error

Why existing code does not prevent it

The eth/backend.go path (used by the internal/cli/server entry point) does this correctly — it creates a single publicFilterAPI instance, calls SetChainConfig on it, and registers that same instance. However, the cmd/utils/flags.go path has this copy-paste-style duplication that was never caught.

Impact

Any Bor node started via the cmd/geth binary that receives a bor_getLogs RPC call will get an error response. The eth_getLogs path is unaffected since it does not check chainConfig. The internal/cli/server path (the primary Bor CLI) also correctly wires a single instance in eth/backend.go, so this bug primarily affects the legacy cmd/geth entry point.

How to fix

Replace the filters.NewFilterAPI(filterSystem, ethcfg.BorLogs) on line 2250 with the existing filterAPI variable:

stack.RegisterAPIs([]rpc.API{{
    Namespace: "eth",
    Service:   filterAPI,
}})

This ensures the same instance that receives SetChainConfig is the one serving RPC requests, matching the pattern already used in eth/backend.go.

Comment on lines 39 to 51
if crit.ToBlock != nil {
end = crit.ToBlock.Int64()
}
if begin > 0 && end > 0 && begin > end {
return nil, errInvalidBlockRange
}
head := api.sys.backend.CurrentHeader().Number.Uint64()
if err := checkBlockRangeLimit(begin, end, head, api.sys.cfg.RangeLimit); err != nil {
return nil, err
}
// Construct the range filter
filter = NewBorBlockLogsRangeFilter(api.sys.backend, borConfig, begin, end, crit.Addresses, crit.Topics)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 Pre-existing: GetBorBlockLogs is missing maxTopics (>4), logQueryLimit (addresses/topics per position), and HistoryPruningCutoff validations that GetLogs has. The PR adds begin>end and range limit checks to this function but omits these other guards, meaning bor_getLogs accepts queries that eth_getLogs would reject. For HistoryPruningCutoff, querying pruned ranges silently returns empty results instead of a clear PrunedHistoryError.

Extended reasoning...

What is missing

GetLogs in api.go (lines 476-490) performs three validations before constructing a filter:

  1. len(crit.Topics) > maxTopics — rejects queries with more than 4 topic positions
  2. logQueryLimit check — rejects queries where len(crit.Addresses) or any len(topics) per position exceeds the configured limit
  3. HistoryPruningCutoff check (line 523) — returns PrunedHistoryError if begin is before the pruning cutoff

GetBorBlockLogs in bor_api.go has none of these. The PR adds begin > end validation and checkBlockRangeLimit (lines 42-48) but does not add the other three checks.

Step-by-step proof

Consider a node with --rpc.logquerylimit 1000 and --bor.logs enabled:

  1. A caller sends eth_getLogs with 2000 addresses → rejected at api.go:482 with errExceedLogQueryLimit.
  2. The same caller sends bor_getLogs with 2000 addresses → passes all validation in bor_api.go, reaches NewBorBlockLogsRangeFilter, and the filter iterates every block in the range performing in-memory log matching against all 2000 addresses.

For HistoryPruningCutoff: if a node has pruned history below block 1,000,000 and a caller queries bor_getLogs with fromBlock=0, GetLogs would return PrunedHistoryError at line 523, but GetBorBlockLogs proceeds. BorBlockLogsFilter.unindexedLogs calls HeaderByNumber which returns nil for pruned blocks, causing the filter to silently return empty/partial results rather than a clear error.

Practical impact

The practical impact is low for several reasons:

  • maxTopics: EVM only supports LOG0-LOG4, so >4 topic criteria would match nothing. Still a consistency issue.
  • logQueryLimit: BorBlockLogsFilter uses simple linear block iteration (not the filtermaps index), so the cost scales linearly rather than triggering expensive index queries. The block range limit (now enforced) is the primary DoS vector.
  • HistoryPruningCutoff: The filter handles pruned blocks gracefully (no crash or data corruption), but users get silently empty results instead of an informative error.
  • bor_getLogs scope: This endpoint only covers pre-Madhugiri (PIP-74) blocks (bor_filter.go:105), further limiting the attack surface.

Why this is pre-existing

These checks were never present in GetBorBlockLogs. The PR does not introduce the gap — it adds range limit and begin > end checks while missing the opportunity to also add maxTopics, logQueryLimit, and HistoryPruningCutoff for consistency.

Suggested fix

Add the following at the top of GetBorBlockLogs, mirroring GetLogs:

if len(crit.Topics) > maxTopics {
    return nil, errExceedMaxTopics
}
if api.logQueryLimit != 0 {
    if len(crit.Addresses) > api.logQueryLimit {
        return nil, errExceedLogQueryLimit
    }
    for _, topics := range crit.Topics {
        if len(topics) > api.logQueryLimit {
            return nil, errExceedLogQueryLimit
        }
    }
}

And in the range branch, before constructing the filter:

if begin >= 0 && begin < int64(api.events.backend.HistoryPruningCutoff()) {
    return nil, &history.PrunedHistoryError{}
}

lucca30 added a commit that referenced this pull request Mar 19, 2026
…ass-through (#2147)

* Include range limit filter flag

* update docs

* apply more range checks
lucca30 added a commit that referenced this pull request Mar 19, 2026
…ass-through (#2147)

* Include range limit filter flag

* update docs

* apply more range checks
lucca30 added a commit that referenced this pull request Mar 19, 2026
…s-through (#2149)

* eth/filters, rpc: add `--rpc.rangelimit` flag and fix filter config pass-through (#2147)

* Include range limit filter flag

* update docs

* apply more range checks

* Including RPCLogQueryLimit

* make docs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants