Burning go-ethereum RPC CPU with one eth_call to the BLAKE2F precompile
A single unauthenticated eth_call that invokes go-ethereum's BLAKE2F precompile with a large caller-set rounds value pins an RPC worker's CPU for hundreds of milliseconds from a sub-kilobyte request, and the JSON-RPC transport applies no per-source rate or concurrency cap — so concurrent calls each occupy a core.
Why this matters
Anyone running a go-ethereum node with the eth JSON-RPC namespace reachable off loopback exposes eth_call, which executes an attacker-influenced message synchronously on an RPC worker. The BLAKE2F precompile turns that into a precise CPU dial: the number of compression rounds is a field the caller sets, and the work scales linearly with it. Because the request stays small and the response is a normal (often error) result, there is no byte amplification to trip a size-based filter, and a naïve request-rate limit does not help — the cost is per request, and a handful of concurrent requests can saturate the RPC worker pool while looking like ordinary traffic. Public and semi-public RPC endpoints, and any node fronted by a load balancer that does not cap per-source concurrency, are the exposed surface.
How the attack works
- Entry point: the
eth_callJSON-RPC method (defaultethnamespace). - Preconditions: the RPC endpoint is reachable by the client; no authentication is required.
- Processing path:
eth_callis handled bydoCall(internal/ethapi/api.go), which runs the supplied message in the EVM bounded byrpc.gascap(default 50,000,000) andrpc.evmtimeout. When the call target is the BLAKE2F precompile at address0x09(core/vm/contracts.go, per EIP-152), the precompile performsroundscompression-function iterations, whereroundsis read directly from the first four bytes of the ~213-byte precompile input and priced at one gas per round. - Boundary crossed: the caller controls the loop count of a native compression routine executed on the RPC worker, and no per-source rate or concurrency limit is applied at the transport before that work runs.
- Resulting behaviour: a tiny request drives tens of millions of rounds of real CPU on the server before returning; per-request cost is capped by the gas/timeout ceiling, but the aggregate is that ceiling multiplied by attacker concurrency.
The mechanism is documented method and precompile semantics, not a memory-safety bug: the precompile is doing exactly what EIP-152 specifies, and the pricing (gas = rounds) is the same knob an attacker turns to buy CPU cheaply against an endpoint that meters requests but not per-request compute.
Affected systems
- Severity: LOW — availability / CPU only (no funds, no consensus break, no authentication bypass), per NR-2026-056.
- Chain: Ethereum.
- Implementation: go-ethereum, observed on 1.17.4, with
eth_callexposed (the default namespace). The behaviour follows from the documented method plus EIP-152 precompile semantics; it is not 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_callreaching the BLAKE2F precompile (0x09). - Not affected: a node that keeps
--http/--wson loopback or behind an authenticating gateway, or one that capseth_callconcurrency per source. The advisory does not establish an affected version range beyond "go-ethereum witheth_callexposed"; the behaviour is the documented method plus EIP-152 semantics, not a version-specific regression.
Evidence
- Publicly documented: the underlying precompile is specified by EIP-152; the pricing (one gas per round) and the
roundsinput field are part of that specification. No CVE is claimed — this is availability behaviour of a correctly-implemented method. - Independently reproduced by NullRabbit: a reproducer (
eth_call_blake2f_precompile_rounds_cpu_amp, familycompute_amp,source_class: original) drives the precompile througheth_callagainst 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, a call withrounds = 0x02000000(≈33.5 million) from a ~585-byte request sustained ≈405 ms of worker CPU, versus ≈1 ms for the same call atrounds = 1. The measurement is lab-fidelity; it demonstrates the linear CPU-for-bytes relationship, not a production-throughput or exploitability claim. - Inferred but not reproduced: the aggregate-saturation impact (concurrency × per-request ceiling) follows from the absence of a transport-level per-source cap; the multi-core saturation figure itself is reasoned from the single-request measurement, not separately load-tested here.
Operator actions
From the advisory (config / load-balancer level, not a code fix — the method behaves as specified):
- Rate-limit
eth_callper source and cap concurrency, not just aggregate request rate — the cost is per request. - Tighten
rpc.gascapandrpc.evmtimeoutfor public-facing endpoints to lower the per-request ceiling. - Keep
--http/--wson loopback or behind an authenticating gateway; do not expose theethnamespace to untrusted clients. - Detection idea (unconfirmed): watch for
eth_callrequests whose CPU time is disproportionate to request size, or repeated small calls targeting precompile address0x09with largeroundsfields. Treat this as a monitoring heuristic, not a validated signature.
Canonical references
- NRDAX technique: NRDAX-T0389 — Precompile Gas Underpricing CPU Burn.
- NullRabbit advisory: NR-2026-056 — Ethereum (go-ethereum): eth_call compute exhaustion (gascap interpreter loop + BLAKE2F precompile) (published 2026-07-15).
- Evidence bundle (Hugging Face): NullRabbit/nr-bundles-public — primitive
eth_call_blake2f_precompile_rounds_cpu_amp. - Upstream primary sources: EIP-152 (BLAKE2 compression precompile);
ethereum/go-ethereumcore/vm/contracts.go(BLAKE2F),internal/ethapi/api.go(doCall).
Related attacks
- Same advisory, sibling vector: NR-2026-056 also documents an
eth_call+stateOverridetight-JUMP-loop that burns the fullrpc.gascap(primitiveeth_call_stateoverride_gascap_interpreter_compute_burn). It shares the same root — attacker-influenced synchronous compute on an uncapped RPC worker — but reaches it through the interpreter rather than a precompile, and is tracked as a distinct NRDAX technique.
No other technique links are asserted; related precompile techniques on NRDAX describe different mechanisms (semantics divergence / miscomputation) and are not claimed here.
Track attacks affecting go-ethereum: subscribe to NullRabbit research updates.
Analysis plus reproducer. No weaponised proof-of-concept code.
Related Posts
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.
Amplifying go-ethereum RPC responses with an eth_simulateV1 block-number gap
A single unauthenticated eth_simulateV1 call with a gap between two block numbers makes go-ethereum synthesise up to ~256 full block headers, expanding a ~285-byte request into a ~434 KB response.
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.
