From fe70c83e4fe3e4aac05c17c8c795d0adf5738217 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:38:30 -0700 Subject: [PATCH] fix: require imported payload for index==1 attestation gossip (consensus-specs #5355) Attestation gossip with data.index==1 now requires the payload to be fully imported (forkChoice.hasPayloadHexUnsafe = is_payload_verified) rather than merely seen, per consensus-specs #5355. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/chain/opPools/proposerPreferencesPool.ts | 2 +- .../src/chain/validation/attestation.ts | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/beacon-node/src/chain/opPools/proposerPreferencesPool.ts b/packages/beacon-node/src/chain/opPools/proposerPreferencesPool.ts index 56eb1d87401b..7ecdaa15853f 100644 --- a/packages/beacon-node/src/chain/opPools/proposerPreferencesPool.ts +++ b/packages/beacon-node/src/chain/opPools/proposerPreferencesPool.ts @@ -17,7 +17,7 @@ import {toRootHex} from "@lodestar/utils"; export class ProposerPreferencesPool { private readonly bySlot = new Map>(); - /** Lookup for bid validation: matches `(bid.slot, get_proposer_dependent_root(parent_state, ...))`. */ + /** Lookup for bid validation: matches `(bid.slot, get_shuffling_dependent_root(store, bid.parent_block_root, epoch))`. */ get(slot: Slot, dependentRootHex: RootHex): gloas.SignedProposerPreferences | null { return this.bySlot.get(slot)?.get(dependentRootHex) ?? null; } diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index f3b7b8863f57..bb0dae257b80 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -319,10 +319,17 @@ async function validateAttestationNoSignatureCheck( // [REJECT] If `attestation.data.index == 1` (payload present for a past // block), the execution payload for `block` passes validation. // [IGNORE] When `attestation.data.index == 1` (payload present for a past block), - // the corresponding execution payload for `block` has been seen (a client MAY queue - // attestations for processing once the payload is retrieved and SHOULD request the - // payload envelope via `ExecutionPayloadEnvelopesByRoot`). - if (block !== null && attData.index === 1 && !chain.seenPayloadEnvelope(toRootHex(attData.beaconBlockRoot))) { + // the corresponding execution payload for `block` has been fully imported, including its + // data -- i.e. `is_payload_verified(store, beacon_block_root)` returns True (consensus-specs + // #5355). `forkChoice.hasPayloadHexUnsafe` is the equivalent of `root in store.payloads`: + // it is true only once the SignedExecutionPayloadEnvelope has been imported and verified, + // not merely seen on gossip. A client MAY queue attestations until the payload is imported + // and SHOULD request the payload envelope via `ExecutionPayloadEnvelopesByRoot`. + if ( + block !== null && + attData.index === 1 && + !chain.forkChoice.hasPayloadHexUnsafe(toRootHex(attData.beaconBlockRoot)) + ) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.EXECUTION_PAYLOAD_NOT_SEEN, beaconBlockRoot: toRootHex(attData.beaconBlockRoot),