← Back to Research
Research · August 28, 2026

Prysm's attestation pool endpoint does the BLS work before it checks the list length

Simon Morley·8 min read

Prysm's POST /eth/v2/beacon/pool/attestations decodes an attacker-sized JSON list with no body cap and no authentication, then runs a BLS G2 subgroup-check on every decoded element, so a single well-formed 59 MB request costs the beacon node 5.81 seconds of single-goroutine CPU: a compute-amplification denial of service against any Beacon API bound to a non-loopback address.

Why this matters

The Beacon API's loopback default is not remotely reachable, so this is not a "every Prysm node is exposed" finding. It matters for the deployments that must bind it wider: block explorers, staking indexers, MEV relays, and split validator-client setups where the validator talks to a beacon node across a host boundary.

For those, the cost asymmetry is the problem. The attacker spends bandwidth; the node spends CPU on a cryptographic operation, in a single goroutine, before anything decides the request was unreasonable. A request-rate limit set by intuition does not help, because the damage is not carried by the number of requests. At 1 Gbps a single host sustains only about 2 requests per second at this size, and that is already enough to pin roughly 11.78 cores' worth of CPU per second: 300% of a 4-core validator, 75% of a 16-core, 37% of a 32-core. Two requests per second looks like nothing in a rate-limit rule.

The second-order point is where the cap lives. The consensus spec bounds attestations per block at MAX_ATTESTATIONS = 128. That cap governs blocks, not this REST pool endpoint, which accepts a list of any length. A reader who assumes the spec constant protects the surface will conclude the endpoint is bounded when it is not.

How the attack works

Entry point. POST /eth/v2/beacon/pool/attestations, handled by SubmitAttestationsV2.

Preconditions. The Beacon API is reachable from the attacker, meaning bound to a non-loopback address. No credentials and no handshake are required. The attacker also needs one genuine, published BLS signature, which is public chain state.

Processing path. The route is registered in beacon-chain/rpc/endpoints.go:710-720 behind exactly three middlewares: ContentTypeHandler, AcceptHeaderHandler, and AcceptEncodingHeaderHandler. None of them authenticates, and none of them caps body size.

Inside the handler (beacon-chain/rpc/eth/beacon/handlers_pool.go), the sequence is:

  • line 152: json.NewDecoder(r.Body).Decode(&req.Data), with no http.MaxBytesReader wrapping the body.
  • line 205: json.Unmarshal into []*structs.SingleAttestation, a slice whose length the attacker chooses.
  • line 209: a zero-length check, and nothing else. There is no > MAX_ATTESTATIONS comparison.
  • lines 214-229: a loop over every element, calling sourceAtt.ToConsensus() and then bls.SignatureFromBytes(att.Signature) at line 223.

Boundary crossed. bls.SignatureFromBytes is a G2 deserialisation plus subgroup-check. It is real elliptic-curve work, and it runs per element, before any aggregate verification and before any length judgement. The request is eventually rejected downstream, but the CPU has already been spent.

The detail that makes it work. The signature has to be a real one. A random 96-byte value fast-rejects during point decompression at about 1.15 to 1.27 microseconds, because most random x-coordinates land at or above the field modulus q, so it never reaches the subgroup-check. A known-valid signature costs about 51.11 microseconds in isolation. The attacker therefore reuses any published block signature, which is public state, and every element pays the full price.

That single fact is what separates a real measurement from a wrong one here, and NullRabbit initially got it wrong in the other direction. The first pass used random bytes, measured about 19 microseconds per element, and filed the finding as MEDIUM. Operator pushback prompted a re-measurement with valid-shape signatures, which put the true cost near 60 microseconds and moved the finding to HIGH. The probe shape, not the handler, was the thing that had been mismeasured.

Affected systems

  • Chain: Ethereum (consensus layer).
  • Implementation: Prysm (OffchainLabs/prysm), v7 HEAD as measured on 2026-05-25. No CVE has been assigned.
  • Component: Beacon API pool submission, POST /eth/v2/beacon/pool/attestations (SubmitAttestationsV2).
  • Exposure condition: the Beacon API bound to a non-loopback address. The loopback default is not remotely reachable.

This brief describes the attestations endpoint only. NR-2026-022 covers two sibling endpoints with the same root cause and different per-element costs; they are separate primitives and are not measured here.

Evidence

Publicly documented. None. This is a NullRabbit original finding with no CVE and no upstream advisory.

Independently reproduced by NullRabbit. A handler-level proof of concept drives the real SubmitAttestationsV2 handler, the real BLS package, and the existing TestSubmitAttestationsV2 server mock, POSTing N attestations whose signatures are the known-valid 96-byte signature from the singleAtt fixture. NRDAX records this instance at fidelity: lab, discovery_origin: original-research.

Measured under controlled conditions. Handler-level sweep on OffchainLabs/prysm v7 HEAD, AMD Ryzen AI 9 HX 370, 24-core, valid-signature probe, 2026-05-25:

Nbody (MB)wall (ms)per attestation (us)
100.0060.6463.7
1000.066.7367.3
1,0000.5963.6763.7
10,0005.94605.6160.6
50,00029.692963.2159.3
100,00059.385887.0358.9

Scaling is linear, and the median per-element handler cost is about 60 microseconds. An isolated component breakdown attributes roughly 85% of that to bls.SignatureFromBytes alone (51.11 microseconds per call over 100,000 iterations with a valid signature, against 1.15 microseconds for random bytes and 1.26 for all-zero bytes). The remaining ~9 microseconds is JSON parsing, structural conversion, and pool-append overhead.

The same sweep repeated through an httptest.Server wrapping the real handler gives 5.81 s at N=100,000 (58.1 microseconds per element), against the handler-direct 5.89 s (58.9 microseconds). The two agree within 1.3%, which is why the HTTP-tier figure of 5.81 s is the one quoted in the advisory and used as the headline here.

Inferred but not reproduced. The attack envelope above (2 requests per second at 1 Gbps, ~11.78 cores per second, and the 4-, 16- and 32-core saturation percentages) is arithmetic on the measured per-request cost, not a separate measurement. Multi-host scaling is predicted to be linear and was not measured.

Explicitly not measured. The full beacon-chain process under sustained attack, including execution-engine polling, JWT, and concurrent gossip handlers. Those run in background goroutines outside the SubmitAttestationsV2 hot path, so the per-request cost is not expected to move materially, but that is a prediction. Also unmeasured: legitimate Beacon API client latency during an attack, sustained RSS impact, and what fraction of public Beacon APIs actually bind to non-loopback addresses. Because the handler-level measurement omits full HTTP server overhead, which adds to rather than subtracts from per-element cost, ~60 microseconds is a lower bound.

Operator actions

From the advisory's mitigation section:

  • Wrap the body. Put http.MaxBytesReader around json.NewDecoder(r.Body) on the pool-submission handlers. The spec-derived cap for this endpoint is roughly MAX_ATTESTATIONS (128) records.
  • Add an explicit list-length check after unmarshal. The structural ceilings in the spec are maxima, not per-request expectations.
  • Rate-limit pool submission per source. Note the caveat above: the sustainable attack rate is low, so a limit tuned to request count alone will not catch this. Size it against body length or element count.
  • Consider auth-gating the slashing endpoints. This one is the advisory's mitigation for the sibling attester_slashings_v2 endpoint rather than for this one, but it belongs in the same pass: real operator workflow rarely POSTs attester_slashings or proposer_slashings, and gossip handles propagation.
  • A middleware-level body-size cap alongside the existing ContentTypeHandler chain would close the class across the REST surface rather than one endpoint at a time.

For exposure checking, the question to answer first is whether your Beacon API listens anywhere other than loopback. If it does not, this does not reach you.

Canonical references

  • NRDAX technique: NRDAX-T0328 (unbounded-request-body-memory-exhaustion), instance prysm_bls_pool_decode_burn
  • NullRabbit advisory: NR-2026-022, published 2026-07-08
  • Evidence bundle: primitive prysm_bls_pool_decode_burn in the public dataset NullRabbit/nr-bundles-public
  • Source trace and measurement runs: chains/ethereum/findings/PRYSM_BLS_POOL_DECODE_BURN/
  • Upstream: OffchainLabs/prysm, beacon-chain/rpc/endpoints.go and beacon-chain/rpc/eth/beacon/handlers_pool.go

Related attacks

NRDAX-T0328 spans multiple chains under the same unbounded-request-body-memory-exhaustion technique. This instance (prysm_bls_pool_decode_burn) shares the technique with cardano_submit_api_body_memory_pin (Cardano, covered in the cardano-submit-api brief), reth_pooledtx_decode_memory_amp (Ethereum reth, covered in the reth decode-budget brief), ic_orchestrator_cup_body_bomb (Internet Computer), prysm_attester_slashing_pubkey_burn and prysm_bls_exec_change_decode_burn (both Ethereum Prysm, both in NR-2026-022), and monero_wallet_rpc_body_size_dos (Monero). This brief describes only the Prysm attestations instance.

The two Prysm siblings differ in per-element work rather than in shape: bls_to_execution_changes runs a full BLS pairing verify at about 616 microseconds per element, roughly ten times this one, and attester_slashings_v2 runs a per-index pubkey deserialisation and G1 subgroup-check at about 23 microseconds cold.

Track attacks affecting Prysm and the Ethereum consensus layer in NRDAX.

Analysis plus reproducer. No weaponised proof-of-concept code.

ethereumprysmbeacon-apicompute-amprpcinfrastructure-security
Publication policy:Why we publish these findings →

Related Posts