Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/beacon-node/src/chain/blocks/importBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion packages/beacon-node/src/network/processor/gossipHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ({
Expand Down
9 changes: 7 additions & 2 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
12 changes: 9 additions & 3 deletions packages/fork-choice/src/forkChoice/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 80 additions & 15 deletions packages/fork-choice/src/protoArray/protoArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<RootHex, BitArray>();
private payloadTimelinessVotes = new Map<RootHex, BitArray>();

/**
* 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<RootHex, BitArray>();

constructor({
pruneThreshold,
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
}
}

Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions packages/fork-choice/test/unit/protoArray/gloas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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);
Expand All @@ -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();
});
});

Expand Down
Loading