From d3549511a3b0f0e6d9bbd21580fdae8f636408b9 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 30 May 2026 15:58:16 +0100 Subject: [PATCH 1/4] fix: resolve PTC committee against the referenced block state --- .../beacon-node/src/chain/regen/interface.ts | 1 + .../validation/payloadAttestationMessage.ts | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/beacon-node/src/chain/regen/interface.ts b/packages/beacon-node/src/chain/regen/interface.ts index 0c0032477e74..b1a89bbd91bd 100644 --- a/packages/beacon-node/src/chain/regen/interface.ts +++ b/packages/beacon-node/src/chain/regen/interface.ts @@ -22,6 +22,7 @@ export enum RegenCaller { validateGossipAttestation = "validateGossipAttestation", validateGossipVoluntaryExit = "validateGossipVoluntaryExit", validateGossipExecutionPayloadBid = "validateGossipExecutionPayloadBid", + validateGossipPayloadAttestationMessage = "validateGossipPayloadAttestationMessage", validateGossipProposerPreferences = "validateGossipProposerPreferences", onForkChoiceFinalized = "onForkChoiceFinalized", restApi = "restApi", diff --git a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts index 7c5d5ffe70fa..e609719e49ae 100644 --- a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts +++ b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts @@ -8,6 +8,7 @@ import {RootHex, gloas, ssz} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; import {GossipAction, PayloadAttestationError, PayloadAttestationErrorCode} from "../errors/index.js"; import {IBeaconChain} from "../index.js"; +import {RegenCaller} from "../regen/index.js"; export type PayloadAttestationValidationResult = { attDataRootHex: RootHex; @@ -61,25 +62,33 @@ async function validatePayloadAttestationMessage( // [IGNORE] The message's block `data.beacon_block_root` has been seen (via // gossip or non-gossip sources) (a client MAY queue attestation for processing // once the block is retrieved. Note a client might want to request payload after). - if (!chain.forkChoice.hasBlock(data.beaconBlockRoot)) { + const block = chain.forkChoice.getBlockHexDefaultStatus(toRootHex(data.beaconBlockRoot)); + if (block === null) { throw new PayloadAttestationError(GossipAction.IGNORE, { code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT, blockRoot: toRootHex(data.beaconBlockRoot), }); } - const state = chain.getHeadState(); - if (!isStatePostGloas(state)) { - throw new Error(`Expected gloas+ state for payload attestation validation, got fork=${state.forkName}`); - } - // [REJECT] The message's block `data.beacon_block_root` passes validation. // TODO GLOAS: implement this. Technically if we cannot get proto block from fork choice, // it is possible that the block didn't pass the validation + // Use the referenced block's branch state for the PTC committee check + const state = await chain.regen + .getBlockSlotState(block, data.slot, {dontTransferCache: true}, RegenCaller.validateGossipPayloadAttestationMessage) + .catch(() => { + throw new PayloadAttestationError(GossipAction.IGNORE, { + code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT, + blockRoot: toRootHex(data.beaconBlockRoot), + }); + }); + if (!isStatePostGloas(state)) { + throw new Error(`Expected gloas+ state for payload attestation validation, got fork=${state.forkName}`); + } + // [REJECT] The message's validator index is within the payload committee in - // `get_ptc(state, data.slot)`. The `state` is the head state corresponding to - // processing the block up to the current slot as determined by the fork choice. + // `get_ptc(state, data.slot)`. // The validator may occupy multiple PTC positions because `compute_ptc` samples // by effective balance — collect all of them so duplicate votes are counted. const validatorCommitteeIndices = state.getIndicesInPayloadTimelinessCommittee(validatorIndex, data.slot); From 3d707a8ec1190c91b086befc99b19529ef6b0e4e Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 30 May 2026 16:06:43 +0100 Subject: [PATCH 2/4] newline --- .../src/chain/validation/payloadAttestationMessage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts index e609719e49ae..0539e924287a 100644 --- a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts +++ b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts @@ -83,6 +83,7 @@ async function validatePayloadAttestationMessage( blockRoot: toRootHex(data.beaconBlockRoot), }); }); + if (!isStatePostGloas(state)) { throw new Error(`Expected gloas+ state for payload attestation validation, got fork=${state.forkName}`); } From 9fb4bb4e04ab36612eca3765534ad5ae4e1cde0e Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 30 May 2026 16:09:43 +0100 Subject: [PATCH 3/4] restore spec comment --- .../src/chain/validation/payloadAttestationMessage.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts index 0539e924287a..149784dc9b67 100644 --- a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts +++ b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts @@ -89,7 +89,8 @@ async function validatePayloadAttestationMessage( } // [REJECT] The message's validator index is within the payload committee in - // `get_ptc(state, data.slot)`. + // `get_ptc(state, data.slot)`. The `state` is the head state corresponding to + // processing the block up to the current slot as determined by the fork choice. // The validator may occupy multiple PTC positions because `compute_ptc` samples // by effective balance — collect all of them so duplicate votes are counted. const validatorCommitteeIndices = state.getIndicesInPayloadTimelinessCommittee(validatorIndex, data.slot); From 52d9dd63658d781125dda1e4ac264e3f7acc2061 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Sat, 30 May 2026 16:34:07 +0100 Subject: [PATCH 4/4] refactor --- .../src/chain/validation/payloadAttestationMessage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts index 149784dc9b67..089fe743f820 100644 --- a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts +++ b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts @@ -62,8 +62,8 @@ async function validatePayloadAttestationMessage( // [IGNORE] The message's block `data.beacon_block_root` has been seen (via // gossip or non-gossip sources) (a client MAY queue attestation for processing // once the block is retrieved. Note a client might want to request payload after). - const block = chain.forkChoice.getBlockHexDefaultStatus(toRootHex(data.beaconBlockRoot)); - if (block === null) { + const block = chain.forkChoice.getBlockDefaultStatus(data.beaconBlockRoot); + if (!block) { throw new PayloadAttestationError(GossipAction.IGNORE, { code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT, blockRoot: toRootHex(data.beaconBlockRoot),