Amplifying go-ethereum RPC responses with an eth_simulateV1 block-number gap
A single unauthenticated eth_simulateV1 call whose blockStateCalls skip from one block number to a much higher one makes go-ethereum synthesise every intervening block — up to ~256 full header objects — and return them all, expanding a ~285-byte request into a ~434 KB response with no per-response size cap on the transport.
Why this matters
eth_simulateV1 is go-ethereum's newer transaction-simulation endpoint (its implementation of the eth_simulate method specified in ethereum/execution-apis), and it is part of the default eth namespace — so any node with the JSON-RPC surface reachable off loopback exposes it without authentication. The amplification here is not driven by an expensive computation or a large input; it comes from a documented autofill behaviour — the caller chooses a gap between two block numbers, and the server fabricates and serialises one block object per skipped height. A size- or rate-limit keyed on request bytes sees a tiny call; the cost lands on the response side and on the node's serialisation work. Public and semi-public RPC endpoints, and nodes behind a load balancer that does not cap response size or per-source request rate, are the exposed surface.
How the attack works
- Entry point: the
eth_simulateV1JSON-RPC method (defaultethnamespace). - Preconditions: the RPC endpoint is reachable by the client; no authentication is required.
- Processing path:
eth_simulateV1(internal/ethapi/simulate.go) processes theblockStateCallslist in order. Each entry may carry ablockOverrides.number. When one entry's number skips ahead of the previous block, the handler fills the gap by synthesising empty blocks for every intervening height, each serialised as a full header object in the result. The fill is bounded at approximately 256 blocks. - Boundary crossed: the number of fabricated block objects is chosen by the attacker — as the gap between two tiny call entries — and no per-response size cap is applied before the result is built and returned.
- Resulting behaviour: a sub-kilobyte request returns hundreds of kilobytes of synthesised header objects; the expansion factor is set by the requested gap, up to the ~256-block ceiling.
The mechanism is documented eth_simulateV1 block-autofill semantics, not a memory-safety bug: the node is doing what the method specifies when asked to simulate across a block-number gap. The lever is that the gap — and therefore the synthesised-object count — is caller-controlled and only loosely bounded.
Affected systems
- Severity: MEDIUM — availability / bandwidth-and-CPU only (no funds, no consensus break, no authentication bypass), per NR-2026-058.
- Chain: Ethereum.
- Implementation: go-ethereum, observed on 1.17.4, exposing
eth_simulateV1(its implementation of theethereum/execution-apiseth_simulatemethod). The behaviour follows from the documented block-autofill semantics; it is not stated as a version-specific regression, so a single tested version should not be read as the only affected one, nor as a claim about other clients. - Component / method: JSON-RPC
eth_simulateV1. - Not affected: a node that keeps
--http/--wson loopback or behind an authenticating gateway, or one that capseth_simulateV1response size / block-span per source. The advisory does not establish an affected version range beyond "go-ethereum exposingeth_simulateV1"; the behaviour is the documented autofill semantics, not a version-specific regression.
Evidence
- Publicly documented:
eth_simulateV1and itsblockStateCalls/blockOverridesblock-autofill behaviour are specified by theeth_simulatemethod inethereum/execution-apis; the ~256-block fill bound is part of the go-ethereum implementation ininternal/ethapi/simulate.go. - Independently reproduced by NullRabbit: a reproducer (
eth_simulateV1_gapblock_autofill_header_breadth_amp, familyresponse_amp,source_class: original) issues the gap request against a self-owned node; the captured bundle ships in theNullRabbit/nr-bundles-publicdataset. - Measured under controlled conditions: on self-owned go-ethereum 1.17.4 running
--devover loopback, twoblockStateCallsat block0x1and0x100produced a ~285-byte request → ~434 KB response (≈1,523×) made of 256 synthesised block objects; a control call for a single block returned ~1.7 KB. The measurement is lab-fidelity; it demonstrates the request-to-response expansion, not a production-throughput or exploitability claim. - Inferred but not reproduced: the aggregate impact of many concurrent gap requests follows from the absence of a per-source response/rate cap; it is reasoned from the single-request measurement, not separately load-tested here.
Operator actions
From the advisory (config / RPC-edge level, not a code fix — the method behaves as specified):
- Cap the block-number span / synthesised-block count for
eth_simulateV1at the RPC edge. - Rate-limit per source and cap response size for the
ethnamespace on public-facing endpoints. - Keep
--http/--wson loopback or behind an authenticating gateway; do not expose theethnamespace to untrusted clients. - Detection idea (unconfirmed): watch for
eth_simulateV1calls whoseblockStateCallscarry large gaps betweenblockOverrides.numbervalues, or whose response size is disproportionate to request size. Treat this as a monitoring heuristic, not a validated signature.
Canonical references
- NRDAX technique: NRDAX-T0329 — Unbounded RPC Response Amplification.
- NullRabbit advisory: NR-2026-058 — Ethereum (go-ethereum): eth_simulateV1 gap-block autofill response amplification (published 2026-07-15).
- Evidence bundle (Hugging Face): NullRabbit/nr-bundles-public — primitive
eth_simulateV1_gapblock_autofill_header_breadth_amp. - Upstream primary sources:
ethereum/execution-apis(theeth_simulatemethod spec);ethereum/go-ethereuminternal/ethapi/simulate.go.
Related attacks
- Same NRDAX technique (NRDAX-T0329, Unbounded RPC Response Amplification): other response-amplification primitives on go-ethereum and Zebra where a small request drives an unbounded response. This brief covers only the
eth_simulateV1gap-block-autofill instance; the shared root is a missing per-response size bound, not a shared code path.
No other technique links are asserted.
Track attacks affecting go-ethereum: subscribe to NullRabbit research updates.
Analysis plus reproducer. No weaponised proof-of-concept code.
Related Posts
How a wide-range eth_getLogs query turns a few hundred bytes into an unbounded response on exposed go-ethereum RPC nodes
eth_getLogs returns every log matching a filter across the requested block span in one JSON-RPC response, so a tiny wide-range or broad-topic request forces an exposed go-ethereum node to scan the range and serialise an unbounded matched-log set — a response-amplification DoS payable by an unauthenticated caller.
Burning go-ethereum RPC CPU with one eth_call to the BLAKE2F precompile
A single unauthenticated eth_call invoking the BLAKE2F precompile with a caller-set rounds field pins a go-ethereum RPC worker's CPU for hundreds of milliseconds from a sub-kilobyte request.
How an exposed debug_traceCall lets one unauthenticated request burn seconds of go-ethereum CPU
debug_traceCall re-runs an eth_call-style message opcode-by-opcode with a full EVM tracer attached, so a single small request converts into up to seconds of single-goroutine CPU on the target — and with no per-source rate or concurrency cap, an unauthenticated caller drives many such traces in parallel against an exposed go-ethereum node.
