Skip to content

Missing Gloas beacon_attestation gossip rules #10795

@tbenr

Description

@tbenr

once we merge #10772

Summary

The Gloas beacon_attestation_{subnet_id} spec adds payload-status gossip rules that Teku does not fully implement. Today AttestationUtilGloas.validatePayloadStatus enforces only the same-slot rule, so a FULL-payload vote (index == 1) whose execution payload has not been seen is still ACCEPTed and propagated, and a FULL vote for a seen-but-invalid payload is not rejected.

This issue covers the gossip-validation layer only. The fork-choice handling of these attestations (holding the vote, withholding aggregation, re-processing on payload import) is delivered separately by the FULL-payload attestation parking work — see the two-layer note below.

Spec rules (specs/gloas/p2p-interface.md, beacon_attestation_{subnet_id})

  • [REJECT] attestation.data.index < 2 — ✅ implemented (validateCommitteeIndexValue)
  • [REJECT] attestation.data.index == 0 if block.slot == attestation.data.slot — ✅ implemented (validatePayloadStatus)
  • [IGNORE] when index == 1, the execution payload for block has been seen (MAY queue, SHOULD request by-root) — ❌ missing (this issue)
  • [REJECT] if index == 1, the execution payload for block passes validation — ❌ missing (this issue)

Two layers: where each verdict lives

A FULL-payload attestation for a not-yet-available payload flows through two independent decision points, with two different return types. Understanding the split is the whole point of this issue.

  1. Gossip validationAttestationValidator.validate(...)InternalValidationResult.
    Returned from AttestationManager.addAttestation(...) and used by the gossip topic handler to decide propagation (and peer scoring). Mapping (GossipSubValidationUtil.fromInternalValidationResult):

    • ACCEPT → gossipsub Valid (propagate)
    • SAVE_FOR_FUTURE, IGNORE → gossipsub Ignore (do not propagate, no penalty)
    • REJECT → gossipsub Invalid (do not propagate, penalize)
  2. Fork-choice processingForkChoice.onAttestation(...) (the attestationProcessor) → AttestationProcessingResult, handled by the switch in AttestationManager.onAttestation(...). This decides what happens in fork choice and how the attestation is parked/aggregated (e.g. UNKNOWN_BLOCK → park for block; UNKNOWN_EXECUTION_PAYLOAD → park for payload).

These compose because AttestationManager.processInternallyValidatedAttestation routes both ACCEPT and SAVE_FOR_FUTURE into onAttestation:

if (code == ACCEPT || code == SAVE_FOR_FUTURE) {
  onAttestation(attestation)...   // fork-choice processing + parking
}

So fork-choice processing (and therefore parking + reprocess) happens regardless of whether the gossip verdict is ACCEPT or SAVE_FOR_FUTURE; the gossip verdict only changes propagation.

Why SAVE_FOR_FUTURE, not IGNORE

The spec rule is [IGNORE], but Teku must return SAVE_FOR_FUTURE, not the IGNORE code: both map to gossipsub Ignore (no propagation), but IGNORE is dropped before onAttestation runs, so the attestation would never be parked or re-processed. SAVE_FOR_FUTURE is gossipsub-Ignore and still routes into fork-choice processing — i.e. it realises the spec's "IGNORE + MAY queue (and reprocess when the payload is retrieved)".

This mirrors the existing missing-block handling

Condition Gossip verdict (AttestationValidator) Fork-choice result (ForkChoice.onAttestation) Parking
Beacon block unknown SAVE_FOR_FUTURE (!isBlockAvailable) UNKNOWN_BLOCK attestationsWaitingForBlock, replayed on onBlockImported
FULL node absent (payload unseen) SAVE_FOR_FUTURE (this issue) UNKNOWN_EXECUTION_PAYLOAD (parking PR) attestationsWaitingForFullPayload, replayed on onExecutionPayloadImported

The gossip layer just says "not ready, don't propagate"; the fork-choice layer says why (UNKNOWN_BLOCK vs UNKNOWN_EXECUTION_PAYLOAD) and parks in the right pool.

Division of responsibility (what is already done vs this issue)

Three concerns, three mechanisms:

Concern Mechanism PR
Vote not counted before the FULL node exists record vote + resolveVoteNode/ProtoArrayScoreCalculator guard parking PR (retained from #10772)
Don't aggregate into our blocks; re-process on payload import AttestationManager parks on UNKNOWN_EXECUTION_PAYLOAD; onExecutionPayloadImported reprocess; payload requested by-root parking PR
Don't propagate to peers ([IGNORE]); penalize invalid ([REJECT]) gossip verdict SAVE_FOR_FUTURE / REJECT in AttestationValidator this issue

Implementation sketch

In AttestationValidator.singleOrAggregateAttestationChecks, after the existing block-availability SAVE_FOR_FUTURE (so the block is known), add a Gloas-gated check. Fork-gated via getFullPayloadVoteHint(index) (no-op pre-Gloas). Determine the FULL-node / payload status from fork choice + the new invalid-payload cache (below):

fullPayloadVote = forkChoiceUtil.getFullPayloadVoteHint(index)   // true only for Gloas index==1
if (fullPayloadVote):
    if (invalidExecutionPayloads.contains(beaconBlockRoot)):     // seen + invalid
        return REJECT
    if (forkChoiceStrategy.getBlockData(beaconBlockRoot, PAYLOAD_STATUS_FULL).isEmpty()):  // not seen / no FULL node
        return SAVE_FOR_FUTURE
    // FULL node present (seen + valid) -> fall through to normal validation (ACCEPT)

Prerequisite: an invalid-execution-payload cache

The [REJECT] rule needs to distinguish "payload seen and invalid" from "payload not seen yet".

  • A shared invalidBlockRoots cache exists (Map<Bytes32, BlockImportResult>, LimitedMap.createSynchronizedLRU(500) in BeaconChainController, threaded through the block/blob/data-column/execution-payload/payload-attestation gossip validators) — but it tracks invalid blocks, not invalid execution payloads.
  • Invalid payloads are currently only debug-dumped + logged (ForkChoice.reportInvalidExecutionPayload); they are not retained anywhere queryable.

So we need a new invalid-execution-payload cache keyed by beacon_block_root (analogous to invalidBlockRoots, bounded LRU), populated where invalid payloads are reported (ForkChoice.reportInvalidExecutionPayload / the failed-payload-execution path) and consulted by AttestationValidator. (Only required for [REJECT]; the [IGNORE] rule needs only the FULL-node-absent check.)

Acceptance criteria

  • beacon_attestation gossip returns SAVE_FOR_FUTURE (gossipsub-Ignore) for index == 1 when the block's FULL node / payload has not been seen — so it is not propagated, yet is still parked + reprocessed by the existing fork-choice path.
  • A bounded invalid-execution-payload cache keyed by beacon_block_root.
  • beacon_attestation gossip returns REJECT for index == 1 when the block's payload was seen and is invalid.
  • Pre-Gloas attestations are unaffected (rules gated by milestone / getFullPayloadVoteHint).
  • Tests covering: SAVE_FOR_FUTURE when payload not seen (and confirm it is parked, not dropped); REJECT on seen-invalid payload; ACCEPT when the FULL node is present.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions