How an exposed debug_traceCall lets one unauthenticated request burn seconds of go-ethereum CPU
debug_traceCall executes an eth_call-style message with a full EVM tracer attached, re-running the call opcode-by-opcode against the target block's state and recording per-step instrumentation — so a single small request converts into up to seconds of single-goroutine CPU on an exposed go-ethereum node, a compute-amplification denial-of-service payable by an unauthenticated caller.
Why this matters
The debug namespace is off by default, and go-ethereum's own guidance points away from exposing it publicly: its documentation notes that enabling non-default namespaces increases the attack surface, that no RPC endpoint is designed to withstand hostile clients, and that full execution traces are very resource-intensive. But operators who need tracing — block explorers, tracing/indexing backends, MEV tooling — do enable it, and where it is reachable on a routable interface it is served without per-method authentication. Tracing is materially more expensive than a plain eth_call, so an attacker who can reach the endpoint drives heavy re-execution for the cost of one request. The per-request ceilings that exist (--rpc.gascap, --rpc.evmtimeout) bound a single call but not the rate or concurrency of calls, so a naïve per-call limit does not close it: the aggregate CPU pressure is the ceiling multiplied by an attacker-chosen number of parallel traces.
How the attack works
- Entry point: the JSON-RPC
debug_traceCallmethod, reachable when thedebugnamespace is enabled (--http.api debug/--ws.api debug) on a non-loopback bind. Thedebugnamespace is not enabled by default, and go-ethereum's guidance discourages exposing it; there is no per-method auth on the transport. - Preconditions: the
debugnamespace exposed on a routable interface, with no per-source rate limit and no per-connection concurrency cap at the RPC transport. - Processing path:
debug_traceCallis served byAPI.TraceCall(eth/tracers/api.go): it reconstructs the parent block's state, builds the requested message, and executes it through the EVM with a tracer hooked into every step — the default struct/opcode logger (or a JS /callTracer/prestateTracer) records instrumentation per opcode and per memory/stack change. - Boundary crossed: availability. The attacker controls the cost by choosing a call whose execution path is long or memory-heavy, so it runs right up to the per-request ceilings.
- Resulting behaviour: each request burns up to the configured wall-clock/gas ceiling of single-goroutine CPU; because the transport applies no rate or concurrency cap, N attacker-chosen concurrent
debug_traceCalls each burn up to that ceiling in parallel, saturating node CPU.
Affected systems
- Severity: MEDIUM — availability-only (no funds, no consensus break, no authentication bypass).
- Chain: Ethereum.
- Implementation: go-ethereum (
ethereum/go-ethereum), run with thedebugnamespace exposed on a routable interface. - Component / method: JSON-RPC
debug_traceCall(thedebugnamespace over--http/--ws). - Not affected: a default node (the
debugnamespace is off by default), or a node that keepsdebugon loopback or behind auth. This advisory does not establish a specific affected version range beyond "go-ethereum with thedebugnamespace exposed"; the behaviour is the documented method semantics, not a version-specific regression.
Evidence
- Publicly documented: go-ethereum's documentation supports treating the
debugnamespace as unsafe to expose — it flags non-default namespaces as increasing the attack surface, states that no RPC endpoint is designed to withstand hostile clients, and describes full execution traces as very resource-intensive; the mechanism is cited toeth/tracers/api.go(API.TraceCall) in the advisory, and the per-request ceilings are the documented--rpc.gascap(default 50,000,000) and--rpc.evmtimeout(default 5s). - Independently reproduced by NullRabbit (lab): the published corpus reproducer (primitive
eth_debug_tracecall_compute, familycompute_amp,source_class: original) is shipped in the public datasetNullRabbit/nr-bundles-public. It captures the attack traffic — repeateddebug_traceCallrequests that drive heavy traced re-execution — contrasted with a light benigneth_call/trace to the same surface against a local self-owned node. - Measured: not established as a single constant. The per-request cost is bounded by
--rpc.evmtimeout/--rpc.gascapand maximised by a call whose traced execution is long or memory-heavy; the aggregate impact scales with attacker concurrency, which the RPC server does not cap — so the realised CPU pressure is a function of those ceilings times connection count, not a fixed multiplier. The advisory stands on the source trace and go-ethereum's own documentation of thedebugnamespace, not on a single measured CPU figure.
Operator actions
- Do not expose the
debugnamespace on a routable interface — this is go-ethereum's own guidance. Omitdebugfrom--http.api/--ws.api, or keep it on loopback or behind an authenticating gateway. - If tracing must be exposed, auth-gate it, rate-limit per source, and cap concurrency, and tighten
--rpc.evmtimeoutand--rpc.gascapbelow the defaults for the traced surface. - Serve tracing from a dedicated, isolated, capacity-planned backend rather than a validating/RPC node that also serves consensus-critical traffic.
- Exposure check: confirm whether
debugappears in your--http.api/--ws.apiset and whether that transport is bound to a routable address. Per-source rate and concurrency monitoring at the gateway is a suggested control, not a confirmed vendor fix.
Canonical references
- NRDAX technique: NRDAX-T0076 — Expensive Debug RPC Compute Amplification.
- NullRabbit advisory: NR-2026-025 — Ethereum (go-ethereum): debug_traceCall compute amplification → compute-amp DoS (published 2026-07-08).
- Evidence bundle (Hugging Face): NullRabbit/nr-bundles-public — primitive
eth_debug_tracecall_compute. - Upstream: go-ethereum JSON-RPC documentation (
debugnamespace: off by default, and geth's guidance discourages exposing it); source patheth/tracers/api.go(API.TraceCall).
Related attacks
eth_debug_tracecall_compute is a compute_amp RPC technique — the same family as other unauthenticated "one small request drives heavy server-side execution" levers on exposed RPC surfaces. Related links here are limited to the shared NRDAX technique id and the compute_amp family recorded in the registry; no further relationship is asserted beyond that.
Track attacks affecting go-ethereum and Ethereum RPC infrastructure.
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.
One prior-knowledge h2c connection multiplexes N eth_getLogs past an L4 per-connection cap on go-ethereum's JSON-RPC port
go-ethereum's JSON-RPC port terminates cleartext HTTP/2 (h2c) by prior knowledge, so one TCP connection can multiplex N eth_getLogs streams past an L4 edge that only caps connections per IP (nginx stream limit_conn) — measured 20/20 streams and ~39 MB pulled through a single connection the edge counts as one, an amplification-multiplexing bypass of a connection cap, not of an L7 request rate limit.
Bypassing go-ethereum's --http.vhosts host allowlist with an empty Host, forged by an HAProxy HTTP/2→1.1 downgrade
An HTTP/2 request with an empty :authority, downgraded to HTTP/1.1 by an HAProxy front, reaches go-ethereum with an empty Host header that its --http.vhosts allowlist accepts (200) though it rejects any concrete disallowed host (403) — a bypass of geth's anti-DNS-rebinding Host filter by an attacker with raw-HTTP/2 reach to the edge.
