Skip to content

Cap self-built payload envelopes at one per valid proposer per slot - #17433

Open
potuz wants to merge 1 commit into
developfrom
fix-self-build-payload-envelope-flood
Open

Cap self-built payload envelopes at one per valid proposer per slot#17433
potuz wants to merge 1 commit into
developfrom
fix-self-build-payload-envelope-flood

Conversation

@potuz

@potuz potuz commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Problem

In queuePendingPayloadEnvelope, both capacity caps (maxPendingPayloadRoots=128, maxPendingBuildersPerRoot=2) are gated behind !isSelfBuild, and all self-build envelopes share one builder-index constant (BuilderIndexSelfBuild) so the per-root inner[builderIdx] dedup never fires across roots.

A malicious current-slot proposer holds the proposer key and can sign an unbounded number of self-build envelopes, each with a different beacon_block_root, all validly signed. Each new root both adds a pending-map entry and fires a sendBatchRootRequest goroutine to fetch the unknown block. The resulting flood of spurious block-by-root requests gets us banned by our peers.

Fix

Cap self-build at one queued payload per valid proposer index per slot, keyed on the head-state proposer for the current slot (helpers.BeaconProposerIndexAtSlot). A valid self-build signature already ties the envelope to exactly that proposer, so the key is deterministic and not attacker-controlled.

  • The seen-proposer entry is recorded only after a successful signature verification, so a non-proposer peer cannot reserve or block the real proposer's slot with a bad envelope.
  • Keying per proposer (not globally per slot) still admits one payload per distinct valid proposer in a forked scenario where the head reorgs to a branch with a different slot proposer — bounded by our own reorgs, not the attacker.
  • The existing per-slot maxSelfBuildSigFailures budget is retained to bound bad-signature floods from non-proposers.
  • queuePendingPayloadEnvelopeFromRootRequest needs no change — it already enforces the caps and never fires a root request.

Tests

Added to pending_payload_envelope_test.go:

  • Flood (one proposer, many distinct roots) → exactly one queued payload.
  • Distinct fork proposers (head reorg to a different valid slot proposer) → two queued payloads.
  • Bad-sig-does-not-reserve → a bad-signature envelope neither reserves the proposer's slot nor blocks the subsequent valid one.

Pre-existing SelfBuildBypasses* tests still pass, confirming honest self-build is never starved by the external-builder caps; the new per-proposer bound is orthogonal.

🤖 Generated with Claude Code

Self-built execution payload envelopes bypassed both queue caps
(maxPendingPayloadRoots and maxPendingBuildersPerRoot) in
queuePendingPayloadEnvelope, and all self-build envelopes share one
builder-index constant so the per-root dedup never fired across roots.
A malicious current-slot proposer could sign unbounded self-build
envelopes with distinct beacon block roots, each firing a
sendBatchRootRequest, flooding peers with spurious block-by-root
requests and getting us banned.

Dedup self-build envelopes on the head-state proposer index for the
current slot, recorded only after a successful signature check so a bad
envelope cannot reserve a proposer's slot. This caps self-build at one
queued payload (and one root request) per valid proposer per slot while
still allowing one per distinct valid proposer across forks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@potuz
potuz requested a review from a team as a code owner August 31, 2026 14:56
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.

1 participant