Skip to content
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
5 changes: 4 additions & 1 deletion packages/beacon-node/src/chain/errors/blockError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export enum BlockErrorCode {
TOO_MANY_KZG_COMMITMENTS = "BLOCK_ERROR_TOO_MANY_KZG_COMMITMENTS",
/** Bid parent block root does not match block parent root */
BID_PARENT_ROOT_MISMATCH = "BLOCK_ERROR_BID_PARENT_ROOT_MISMATCH",
/** Parent execution payload has not been seen */
PARENT_PAYLOAD_UNKNOWN = "BLOCK_ERROR_PARENT_PAYLOAD_UNKNOWN",
}

type ExecutionErrorStatus = Exclude<
Expand Down Expand Up @@ -114,7 +116,8 @@ export type BlockErrorType =
| {code: BlockErrorCode.EXECUTION_ENGINE_ERROR; execStatus: ExecutionErrorStatus; errorMessage: string}
| {code: BlockErrorCode.DATA_UNAVAILABLE}
| {code: BlockErrorCode.TOO_MANY_KZG_COMMITMENTS; blobKzgCommitmentsLen: number; commitmentLimit: number}
| {code: BlockErrorCode.BID_PARENT_ROOT_MISMATCH; bidParentRoot: RootHex; blockParentRoot: RootHex};
| {code: BlockErrorCode.BID_PARENT_ROOT_MISMATCH; bidParentRoot: RootHex; blockParentRoot: RootHex}
| {code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN; parentBlockHash: RootHex};

export class BlockGossipError extends GossipActionError<BlockErrorType> {}

Expand Down
11 changes: 11 additions & 0 deletions packages/beacon-node/src/chain/validation/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@ export async function validateGossipBlock(
});
}

// [IGNORE] The block's parent execution payload (defined by bid.parent_block_hash) has been seen
// (via gossip or non-gossip sources) (a client MAY queue blocks for processing once the parent
// payload is retrieved).
const parentBlockHashHex = toRootHex(bid.parentBlockHash);
if (!chain.forkChoice.hasExecutionPayload(parentBlockHashHex)) {
throw new BlockGossipError(GossipAction.IGNORE, {
code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN,
parentBlockHash: parentBlockHashHex,
});
}

// TODO GLOAS: [REJECT] The block's execution payload parent (defined by bid.parent_block_hash) passes all validation
// This requires execution engine integration to verify the parent block hash
}
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
17 changes: 13 additions & 4 deletions packages/beacon-node/test/spec/presets/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {AttesterSlashing, altair, bellatrix, capella, electra, gloas, phase0, ss
import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js";
import {ethereumConsensusSpecsTests} from "../specTestVersioning.js";
import {expectEqualBeaconState, inputTypeSszTreeViewDU} from "../utils/expectEqualBeaconState.js";
import {specTestIterator} from "../utils/specTestIterator.js";
import {defaultSkipOpts, specTestIterator} from "../utils/specTestIterator.js";
import {BaseSpecTest, RunnerType, TestRunnerFn, shouldVerify} from "../utils/types.js";

// Define above to re-use in sync_aggregate and sync_aggregate_random
Expand Down Expand Up @@ -187,6 +187,15 @@ const operations: TestRunnerFn<OperationsTestCase, BeaconStateAllForks> = (fork,
};
};

specTestIterator(path.join(ethereumConsensusSpecsTests.outputDir, "tests", ACTIVE_PRESET), {
operations: {type: RunnerType.default, fn: operations},
});
specTestIterator(
path.join(ethereumConsensusSpecsTests.outputDir, "tests", ACTIVE_PRESET),
{
operations: {type: RunnerType.default, fn: operations},
},
{
...defaultSkipOpts,
// builder_voluntary_exit__success: spec test vector bug — missing voluntary_exit input yield
// See: https://github.com/ethereum/consensus-specs/pull/4908
skippedTests: [...(defaultSkipOpts.skippedTests ?? []), /builder_voluntary_exit__success/],
}
);
7 changes: 6 additions & 1 deletion packages/fork-choice/src/forkChoice/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export enum InvalidAttestationCode {
* The attestation data index is invalid for a Gloas block (must be 0 or 1).
*/
INVALID_DATA_INDEX = "INVALID_DATA_INDEX",
/**
* Attestation index=1 (full node) but the payload is not known/available.
*/
UNKNOWN_PAYLOAD_STATUS = "UNKNOWN_PAYLOAD_STATUS",
}

export type InvalidAttestation =
Expand All @@ -69,7 +73,8 @@ export type InvalidAttestation =
| {code: InvalidAttestationCode.INVALID_TARGET; attestation: RootHex; local: RootHex}
| {code: InvalidAttestationCode.ATTESTS_TO_FUTURE_BLOCK; block: Slot; attestation: Slot}
| {code: InvalidAttestationCode.FUTURE_SLOT; attestationSlot: Slot; latestPermissibleSlot: Slot}
| {code: InvalidAttestationCode.INVALID_DATA_INDEX; index: number};
| {code: InvalidAttestationCode.INVALID_DATA_INDEX; index: number}
| {code: InvalidAttestationCode.UNKNOWN_PAYLOAD_STATUS; beaconBlockRoot: RootHex};

export enum ForkChoiceErrorCode {
INVALID_ATTESTATION = "FORKCHOICE_ERROR_INVALID_ATTESTATION",
Expand Down
35 changes: 32 additions & 3 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@ export class ForkChoice implements IForkChoice {
let payloadStatus: PayloadStatus;

// We need to retrieve block to check if it's Gloas and to compare slot
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/fork-choice.md#new-is_supporting_vote
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.3/specs/gloas/fork-choice.md#new-is_supporting_vote
const block = this.getBlockHexDefaultStatus(blockRootHex);

if (block && isGloasBlock(block)) {
Expand Down 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 Expand Up @@ -1056,6 +1061,14 @@ export class ForkChoice implements IForkChoice {
return this.protoArray.hasBlock(blockRoot);
}

/**
* Check if an execution payload with the given block hash has been seen.
* Spec: gloas/p2p-interface.md — parent execution payload must have been seen
*/
hasExecutionPayload(executionPayloadBlockHash: RootHex): boolean {
return this.protoArray.hasExecutionPayload(executionPayloadBlockHash);
}

/**
* Returns a MUTABLE `ProtoBlock` if the block is known **and** a descendant of the finalized root.
*/
Expand Down Expand Up @@ -1673,6 +1686,22 @@ export class ForkChoice implements IForkChoice {
});
}

// [New in Gloas:EIP7732]
// If attesting for a full node (index=1), the payload must be known (locally available)
// Spec: gloas/fork-choice.md#modified-validate_on_attestation
if (isGloasBlock(block) && attestationData.index === 1) {
const fullVariant = this.protoArray.getNodeIndexByRootAndStatus(beaconBlockRootHex, PayloadStatus.FULL);
if (fullVariant === undefined) {
throw new ForkChoiceError({
code: ForkChoiceErrorCode.INVALID_ATTESTATION,
err: {
code: InvalidAttestationCode.UNKNOWN_PAYLOAD_STATUS,
beaconBlockRoot: beaconBlockRootHex,
},
});
}
}

this.validatedAttestationDatas.add(attDataRoot);
}

Expand Down
21 changes: 16 additions & 5 deletions packages/fork-choice/src/forkChoice/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export interface IForkChoice {
* ## Specification
*
* Modified for Gloas to return ProtoNode instead of just root:
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/fork-choice.md#modified-get_ancestor
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.3/specs/gloas/fork-choice.md#modified-get_ancestor
*
* Pre-Gloas: Returns (root, PAYLOAD_STATUS_FULL)
* Gloas: Returns (root, payloadStatus) based on actual node state
Expand Down Expand Up @@ -183,20 +183,26 @@ 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.3/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
*
* ## Specification
*
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/fork-choice.md#new-on_execution_payload
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.3/specs/gloas/fork-choice.md#new-on_execution_payload
*
* @param blockRoot - The beacon block root for which the payload arrived
* @param executionPayloadBlockHash - The block hash of the execution payload
Expand Down Expand Up @@ -228,6 +234,11 @@ export interface IForkChoice {
*/
hasBlockUnsafe(blockRoot: Root): boolean;
hasBlockHexUnsafe(blockRoot: RootHex): boolean;
/**
* Check if an execution payload with the given block hash has been seen.
* For Gloas: checks if any block has a FULL variant with matching execution payload block hash.
*/
hasExecutionPayload(executionPayloadBlockHash: RootHex): boolean;
getSlotsPresent(windowStart: number): number;
/**
* Returns a `ProtoBlock` if the block is known **and** a descendant of the finalized root.
Expand Down
6 changes: 3 additions & 3 deletions packages/fork-choice/src/protoArray/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ export enum ExecutionStatus {
* Spec: gloas/fork-choice.md#constants
*/
export enum PayloadStatus {
PENDING = 0,
EMPTY = 1,
FULL = 2,
EMPTY = 0,
FULL = 1,
PENDING = 2,
}

/**
Expand Down
Loading
Loading