diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 7e61ce6671d9..5f5379ba26f5 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -247,7 +247,8 @@ export async function importBlock( this.forkChoice.notifyPtcMessages( toRootHex(payloadAttestation.data.beaconBlockRoot), ptcIndices, - payloadAttestation.data.payloadPresent + payloadAttestation.data.payloadPresent, + payloadAttestation.data.blobDataAvailable ); } } catch (e) { diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index b022a1866470..effd9ae2ca6a 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -859,7 +859,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand chain.forkChoice.notifyPtcMessages( toRootHex(payloadAttestationMessage.data.beaconBlockRoot), [validationResult.validatorCommitteeIndex], - payloadAttestationMessage.data.payloadPresent + payloadAttestationMessage.data.payloadPresent, + payloadAttestationMessage.data.blobDataAvailable ); }, [GossipType.execution_payload_bid]: async ({ diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 2d0cef22540c..f2232ff1b876 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -964,8 +964,13 @@ export class ForkChoice implements IForkChoice { * Updates the PTC votes for multiple validators attesting to a block * Spec: gloas/fork-choice.md#new-on_payload_attestation_message */ - notifyPtcMessages(blockRoot: RootHex, ptcIndices: number[], payloadPresent: boolean): void { - this.protoArray.notifyPtcMessages(blockRoot, ptcIndices, payloadPresent); + notifyPtcMessages( + blockRoot: RootHex, + ptcIndices: number[], + payloadPresent: boolean, + blobDataAvailable: boolean + ): void { + this.protoArray.notifyPtcMessages(blockRoot, ptcIndices, payloadPresent, blobDataAvailable); } /** diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index 26e7f08c2be5..a38f21895471 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -183,13 +183,19 @@ export interface IForkChoice { * * ## Specification * - * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/gloas/fork-choice.md#new-notify_ptc_messages + * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/fork-choice.md#new-on_payload_attestation_message * * @param blockRoot - The beacon block root being attested * @param ptcIndices - Array of PTC committee indices that voted - * @param payloadPresent - Whether validators attest the payload is present + * @param payloadPresent - Whether validators attest the payload is present (timeliness) + * @param blobDataAvailable - Whether validators attest blob data is available */ - notifyPtcMessages(blockRoot: RootHex, ptcIndices: number[], payloadPresent: boolean): void; + notifyPtcMessages( + blockRoot: RootHex, + ptcIndices: number[], + payloadPresent: boolean, + blobDataAvailable: boolean + ): void; /** * Notify fork choice that an execution payload has arrived (Gloas fork) * Creates the FULL variant of a Gloas block when the payload becomes available diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index 683ba6b38145..b58de09c3bd3 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -21,6 +21,12 @@ import { */ const PAYLOAD_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2); +/** + * Threshold for payload data availability (>50% of PTC must vote) + * Spec: gloas/fork-choice.md (DATA_AVAILABILITY_TIMELY_THRESHOLD = PTC_SIZE // 2) + */ +const DATA_AVAILABILITY_TIMELY_THRESHOLD = Math.floor(PTC_SIZE / 2); + export const DEFAULT_PRUNE_THRESHOLD = 0; type ProposerBoost = {root: RootHex; score: number}; @@ -61,14 +67,24 @@ export class ProtoArray { private previousProposerBoost: ProposerBoost | null = null; /** - * PTC (Payload Timeliness Committee) votes per block as bitvectors + * Payload timeliness votes per block as bitvectors * Maps block root to BitArray of PTC_SIZE bits (512 mainnet, 2 minimal) - * Spec: gloas/fork-choice.md#modified-store (line 148) + * Spec: gloas/fork-choice.md#modified-store (payload_timeliness_vote) * * Bit i is set if PTC member i voted payload_present=true * Used by is_payload_timely() to determine if payload is timely */ - private ptcVotes = new Map(); + private payloadTimelinessVotes = new Map(); + + /** + * Payload data availability votes per block as bitvectors + * Maps block root to BitArray of PTC_SIZE bits (512 mainnet, 2 minimal) + * Spec: gloas/fork-choice.md#modified-store (payload_data_availability_vote) + * + * Bit i is set if PTC member i voted blob_data_available=true + * Used by is_payload_data_available() to determine if blob data is available + */ + private payloadDataAvailabilityVotes = new Map(); constructor({ pruneThreshold, @@ -495,9 +511,20 @@ export class ProtoArray { // Update bestChild for PENDING → EMPTY edge this.maybeUpdateBestChildAndDescendant(pendingIndex, emptyIndex, currentSlot, proposerBoostRoot); - // Initialize PTC votes for this block (all false initially) - // Spec: gloas/fork-choice.md#modified-on_block (line 645) - this.ptcVotes.set(block.blockRoot, BitArray.fromBitLen(PTC_SIZE)); + // Initialize both vote maps for this block + // Spec: gloas/fork-choice.md#modified-on_block (all false for new blocks) + // Spec: gloas/fork-choice.md#modified-get_forkchoice_store (all true for anchor block) + const timelinessVotes = BitArray.fromBitLen(PTC_SIZE); + const dataAvailabilityVotes = BitArray.fromBitLen(PTC_SIZE); + if (block.blockRoot === this.finalizedRoot) { + // Anchor block: set all bits to true per get_forkchoice_store spec + for (let i = 0; i < PTC_SIZE; i++) { + timelinessVotes.set(i, true); + dataAvailabilityVotes.set(i, true); + } + } + this.payloadTimelinessVotes.set(block.blockRoot, timelinessVotes); + this.payloadDataAvailabilityVotes.set(block.blockRoot, dataAvailabilityVotes); } else { // Pre-Gloas: Only create FULL node (payload embedded in block) const node: ProtoNode = { @@ -612,11 +639,18 @@ export class ProtoArray { * * @param blockRoot - The beacon block root being attested * @param ptcIndices - Array of PTC committee indices that voted (0..PTC_SIZE-1) - * @param payloadPresent - Whether the validators attest the payload is present + * @param payloadPresent - Whether the validators attest the payload is present (timeliness) + * @param blobDataAvailable - Whether the validators attest blob data is available */ - notifyPtcMessages(blockRoot: RootHex, ptcIndices: number[], payloadPresent: boolean): void { - const votes = this.ptcVotes.get(blockRoot); - if (votes === undefined) { + notifyPtcMessages( + blockRoot: RootHex, + ptcIndices: number[], + payloadPresent: boolean, + blobDataAvailable: boolean + ): void { + const timelinessVotes = this.payloadTimelinessVotes.get(blockRoot); + const dataAvailabilityVotes = this.payloadDataAvailabilityVotes.get(blockRoot); + if (timelinessVotes === undefined || dataAvailabilityVotes === undefined) { // Block not found or not a Gloas block, ignore return; } @@ -626,7 +660,8 @@ export class ProtoArray { throw new Error(`Invalid PTC index: ${ptcIndex}, must be 0..${PTC_SIZE - 1}`); } - votes.set(ptcIndex, payloadPresent); + timelinessVotes.set(ptcIndex, payloadPresent); + dataAvailabilityVotes.set(ptcIndex, blobDataAvailable); } } @@ -642,7 +677,7 @@ export class ProtoArray { * @param blockRoot - The beacon block root to check */ isPayloadTimely(blockRoot: RootHex): boolean { - const votes = this.ptcVotes.get(blockRoot); + const votes = this.payloadTimelinessVotes.get(blockRoot); if (votes === undefined) { // Block not found or not a Gloas block return false; @@ -660,6 +695,35 @@ export class ProtoArray { return yesVotes > PAYLOAD_TIMELY_THRESHOLD; } + /** + * Check if blob data for a block is available + * Spec: gloas/fork-choice.md#new-is_payload_data_available + * + * Returns true if: + * 1. Block has data availability votes tracked + * 2. Payload is locally available (FULL variant exists in proto array) + * 3. More than DATA_AVAILABILITY_TIMELY_THRESHOLD (>50% of PTC) voted blob_data_available=true + * + * @param blockRoot - The beacon block root to check + */ + isPayloadDataAvailable(blockRoot: RootHex): boolean { + const votes = this.payloadDataAvailabilityVotes.get(blockRoot); + if (votes === undefined) { + // Block not found or not a Gloas block + return false; + } + + // If payload is not locally available, blob data is not considered available + const fullNodeIndex = this.getNodeIndexByRootAndStatus(blockRoot, PayloadStatus.FULL); + if (fullNodeIndex === undefined) { + return false; + } + + // Count votes for blob_data_available=true + const yesVotes = bitCount(votes.uint8Array); + return yesVotes > DATA_AVAILABILITY_TIMELY_THRESHOLD; + } + /** * Check if parent node is FULL * Spec: gloas/fork-choice.md#new-is_parent_node_full @@ -684,8 +748,8 @@ export class ProtoArray { * @param proposerBoostRoot - Current proposer boost root (from ForkChoice) */ shouldExtendPayload(blockRoot: RootHex, proposerBoostRoot: RootHex | null): boolean { - // Condition 1: Payload is timely - if (this.isPayloadTimely(blockRoot)) { + // Condition 1: Payload is timely AND blob data is available + if (this.isPayloadTimely(blockRoot) && this.isPayloadDataAvailable(blockRoot)) { return true; } @@ -1083,7 +1147,8 @@ export class ProtoArray { this.indices.delete(root); // Prune PTC votes for this block to prevent memory leak // Spec: gloas/fork-choice.md (implicit - finalized blocks don't need PTC votes) - this.ptcVotes.delete(root); + this.payloadTimelinessVotes.delete(root); + this.payloadDataAvailabilityVotes.delete(root); } // Store nodes prior to finalization diff --git a/packages/fork-choice/test/unit/protoArray/gloas.test.ts b/packages/fork-choice/test/unit/protoArray/gloas.test.ts index b1e5bb84d684..f8cbb8ad305f 100644 --- a/packages/fork-choice/test/unit/protoArray/gloas.test.ts +++ b/packages/fork-choice/test/unit/protoArray/gloas.test.ts @@ -347,7 +347,7 @@ describe("Gloas Fork Choice", () => { expect(protoArray.isPayloadTimely("0x02")).toBe(false); // Vote yes from validators at indices 0, 1, 2 - protoArray.notifyPtcMessages("0x02", [0, 1, 2], true); + protoArray.notifyPtcMessages("0x02", [0, 1, 2], true, true); // Still not timely (need >50% of PTC_SIZE) expect(protoArray.isPayloadTimely("0x02")).toBe(false); @@ -357,15 +357,15 @@ describe("Gloas Fork Choice", () => { const block = createTestBlock(gloasForkSlot, "0x02", genesisRoot, genesisRoot); protoArray.onBlock(block, gloasForkSlot, null); - expect(() => protoArray.notifyPtcMessages("0x02", [-1], true)).toThrow(/Invalid PTC index/); - expect(() => protoArray.notifyPtcMessages("0x02", [PTC_SIZE], true)).toThrow(/Invalid PTC index/); - expect(() => protoArray.notifyPtcMessages("0x02", [PTC_SIZE + 1], true)).toThrow(/Invalid PTC index/); - expect(() => protoArray.notifyPtcMessages("0x02", [0, 1, PTC_SIZE], true)).toThrow(/Invalid PTC index/); + expect(() => protoArray.notifyPtcMessages("0x02", [-1], true, true)).toThrow(/Invalid PTC index/); + expect(() => protoArray.notifyPtcMessages("0x02", [PTC_SIZE], true, true)).toThrow(/Invalid PTC index/); + expect(() => protoArray.notifyPtcMessages("0x02", [PTC_SIZE + 1], true, true)).toThrow(/Invalid PTC index/); + expect(() => protoArray.notifyPtcMessages("0x02", [0, 1, PTC_SIZE], true, true)).toThrow(/Invalid PTC index/); }); it("notifyPtcMessages() handles unknown block gracefully", () => { // Should not throw for unknown block - expect(() => protoArray.notifyPtcMessages("0x99", [0], true)).not.toThrow(); + expect(() => protoArray.notifyPtcMessages("0x99", [0], true, true)).not.toThrow(); }); it("isPayloadTimely() returns false when payload not locally available", () => { @@ -433,7 +433,7 @@ describe("Gloas Fork Choice", () => { expect(protoArray.isPayloadTimely("0x02")).toBe(true); // Change some yes votes to no - protoArray.notifyPtcMessages("0x02", [0, 1], false); + protoArray.notifyPtcMessages("0x02", [0, 1], false, false); // Should no longer be timely expect(protoArray.isPayloadTimely("0x02")).toBe(false); @@ -451,7 +451,7 @@ describe("Gloas Fork Choice", () => { expect(protoArray.isPayloadTimely("0x02")).toBe(false); // notifyPtcMessages should be no-op - expect(() => protoArray.notifyPtcMessages("0x02", [0], true)).not.toThrow(); + expect(() => protoArray.notifyPtcMessages("0x02", [0], true, true)).not.toThrow(); }); });