Skip to content

chain/ethereum: Restore eth_getLogs block range reduction under alloy - #6723

Open
datanexus-vincent wants to merge 1 commit into
graphprotocol:masterfrom
datanexus-vincent:fix/eth-getlogs-range-reduction-alloy
Open

datanexus-vincent wants to merge 1 commit into
graphprotocol:masterfrom
datanexus-vincent:fix/eth-getlogs-range-reduction-alloy

Conversation

@datanexus-vincent

@datanexus-vincent datanexus-vincent commented Sep 22, 2026

Copy link
Copy Markdown

Problem

log_stream adapts to providers that cap eth_getLogs by shrinking the block range when a request is rejected as too heavy. It identified such a rejection with a string fingerprint list:

const TOO_MANY_LOGS_FINGERPRINTS: &[&str] = &[
    "ServerError(-32005)",       // Infura
    "503 Service Unavailable",   // Alchemy
    "ServerError(-32000)",       // Alchemy
    "Try with this block range", // zKSync era
    "block range too large",     // Monad
];

ServerError(-32005) is rust-web3's Debug rendering of jsonrpc_core::ErrorCode. The alloy migration in v0.42.0 (#6063, #6317) carried the list over unchanged, but alloy renders the same error differently:

rendering
Display server returned an error response: error code -32005: Requested range exceeds maximum RPC range limit
Debug ErrorResp(ErrorPayload { code: -32005, message: "...", data: None })

Neither contains ServerError(. The 503 Service Unavailable entry broke the same way — alloy surfaces an HTTP 503 as TransportErrorKind::HttpError, rendered HTTP error 503 with empty body.

So on v0.42.0 through v0.45.0 only the two message-substring entries (zkSync Era, Monad) still match. A provider that caps a range with -32005 now causes 10 identical retries, an Unexpected RPC error warning, and a block-stream restart — in a loop — instead of the step / 10 reduction. This affects Besu (--rpc-max-logs-range, -32005 "Requested range exceeds maximum RPC range limit"), the geth/erigon/reth result cap, Infura, and Alchemy, whenever the provider's cap is below GRAPH_ETHEREUM_MAX_BLOCK_RANGE_SIZE.

Change

Replace the string list with is_too_many_logs_err, which matches the error structurally:

  • RpcError::ErrorResp with code -32005 (EIP-1474 "limit exceeded") or -32000 (geth's catch-all, used by Alchemy for size and timeout errors);
  • TransportErrorKind::HttpError with status 503, for Alchemy shedding load;
  • message fragments, kept for providers that report the same condition under a non-standard code (zkSync Era's Try with this block range, Monad's block range too large, plus geth's query returned more than for proxies that rewrite the code but keep the message).

This follows interpret_eth_call_error in call_helper.rs, which already matches RpcError::ErrorResp on code and message rather than on the formatted string.

Both places that used the old check now call the helper: the .when() retry predicate in logs_with_sigs and the reduction branch in log_stream. That matters beyond tidiness — the two see different renderings of the same error (log_stream gets it through TimeoutError, which formats with Debug; the predicate gets the bare RpcError), so a string-only fix would have had to cover both forms and stay correct across alloy bumps.

Matching -32000 on the code alone is broader than a cap error, and is what graph-node did before v0.42.0. The asymmetry justifies it: a missed cap error loops the block stream indefinitely, while a spurious match only shrinks the range for the rest of that log_stream batch, which is re-created with a fresh step for the next one. Shrinking is itself a retry, so a transient -32000 still gets further attempts. Happy to require a size/timeout message fragment alongside -32000 if you'd rather trade it the other way.

The reduction policy (step / 10, floor at a single block), GRAPH_ETHEREUM_MAX_BLOCK_RANGE_SIZE, and the eth_call deterministic-error handling are untouched.

Tests

Six unit tests in ethereum_adapter::tests covering the provider errors above, the errors that must not trigger a reduction (-32601 method not found, -32603 internal error, HTTP 429, NullResp), TimeoutError::Elapsed, and a regression guard asserting that neither alloy rendering contains the old web3 fingerprints.

test result: ok. 65 passed; 0 failed

cargo fmt --all -- --check and cargo clippy --all-targets are clean.

Notes

  • No NEWS.md entry: the file looks to be compiled by maintainers in the release PR rather than edited per bugfix. Happy to add one if you'd prefer it here.
  • Monad also returns Invalid block range for some rejections. I left it out deliberately — it is ambiguous enough that a genuinely invalid range would be retried down to a single block before erroring.

🤖 Co-authored with Claude Code

`log_stream` shrinks the block range when a provider rejects an
`eth_getLogs` request as too heavy. It recognized such a rejection by
looking for `ServerError(-32005)` / `ServerError(-32000)` in the error
string, which is rust-web3's `Debug` rendering of `jsonrpc_core::ErrorCode`.

Since the alloy migration in v0.42.0 the adapter returns
`RpcError::ErrorResp(ErrorPayload { code, .. })`, rendered as
`server returned an error response: error code -32005: ...` (`Display`)
or `ErrorResp(ErrorPayload { code: -32005, .. })` (`Debug`). Neither
contains `ServerError(`, so the code-based fingerprints stopped matching
and graph-node retried the same oversized range 10 times, logged
"Unexpected RPC error", and restarted the block stream in a loop. The
`503 Service Unavailable` entry broke the same way: alloy surfaces an
HTTP 503 as `TransportErrorKind::HttpError`, rendered `HTTP error 503
with empty body`.

Replace the string list with `is_too_many_logs_err`, which matches the
error structurally: on `ErrorPayload::code` for -32005/-32000, on HTTP
status for 503, and on message fragments for providers that use a
non-standard code (zkSync Era, Monad). This is the approach
`interpret_eth_call_error` already takes for `eth_call`. It also avoids
a trap the string check had: the two call sites see different renderings
of the same error, since `log_stream` gets it through `TimeoutError`,
which formats with `Debug`, while the retry predicate in
`logs_with_sigs` gets the bare `RpcError`.

The reduction policy (`step / 10`, down to a single block) and the
`GRAPH_ETHEREUM_MAX_BLOCK_RANGE_SIZE` defaults are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

2 participants