From e8ec8d78b871cf1b8309dabcef7bf468addadd1a Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 27 Mar 2026 15:01:15 +0700 Subject: [PATCH 1/2] fix: track PayloadExecutionStatus for forkchoice onExecutionPayload() --- .../src/chain/blocks/importBlock.ts | 35 ++--- .../chain/blocks/importExecutionPayload.ts | 35 +++-- .../beacon-node/src/chain/blocks/index.ts | 3 +- .../beacon-node/src/chain/blocks/types.ts | 39 +++-- .../blocks/verifyBlocksExecutionPayloads.ts | 8 +- .../fork-choice/src/forkChoice/forkChoice.ts | 18 ++- .../fork-choice/src/forkChoice/interface.ts | 8 +- packages/fork-choice/src/index.ts | 3 +- .../fork-choice/src/protoArray/interface.ts | 13 +- .../fork-choice/src/protoArray/protoArray.ts | 6 +- .../protoArray/executionStatusUpdates.test.ts | 4 +- .../test/unit/protoArray/gloas.test.ts | 144 ++++++++++++++++-- 12 files changed, 236 insertions(+), 80 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index c8875939bf78..e19448131f58 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -3,6 +3,7 @@ import {routes} from "@lodestar/api"; import { AncestorStatus, EpochDifference, + ExecutionStatus, ForkChoiceError, ForkChoiceErrorCode, NotReorgedReason, @@ -84,7 +85,7 @@ export async function importBlock( fullyVerifiedBlock: FullyVerifiedBlock, opts: ImportBlockOpts ): Promise { - const {blockInput, postState, parentBlockSlot, executionStatus, dataAvailabilityStatus, indexedAttestations} = + const {blockInput, postBlockState, parentBlockSlot, executionStatus, dataAvailabilityStatus, indexedAttestations} = fullyVerifiedBlock; const block = blockInput.getBlock(); const source = blockInput.getBlockSource(); @@ -96,7 +97,7 @@ export async function importBlock( const blockEpoch = computeEpochAtSlot(blockSlot); const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; const blockDelaySec = - fullyVerifiedBlock.seenTimestampSec - computeTimeAtSlot(this.config, blockSlot, postState.genesisTime); + fullyVerifiedBlock.seenTimestampSec - computeTimeAtSlot(this.config, blockSlot, postBlockState.genesisTime); const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000); const fork = this.config.getForkSeq(blockSlot); @@ -119,13 +120,13 @@ export async function importBlock( // 2. Import block to fork choice // Should compute checkpoint balances before forkchoice.onBlock - this.checkpointBalancesCache.processState(blockRootHex, postState); + this.checkpointBalancesCache.processState(blockRootHex, postBlockState); const blockSummary = this.forkChoice.onBlock( block.message, - postState, + postBlockState, blockDelaySec, currentSlot, - executionStatus, + fork >= ForkSeq.gloas ? ExecutionStatus.PayloadSeparated : executionStatus, dataAvailabilityStatus ); @@ -135,7 +136,7 @@ export async function importBlock( // Post-Gloas: blockSummary.payloadStatus is always PENDING, so payloadPresent = false (block state only, no payload processing yet) const payloadPresent = !isGloasBlock(blockSummary); // processState manages both block state and payload state variants together for memory/disk management - this.regen.processBlockState(blockRootHex, postState); + this.regen.processBlockState(blockRootHex, postBlockState); // For Gloas blocks, create PayloadEnvelopeInput so it's available for later payload import if (fork >= ForkSeq.gloas) { @@ -171,7 +172,7 @@ export async function importBlock( (opts.importAttestations !== AttestationImportOpt.Skip && blockEpoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT) ) { const attestations = block.message.body.attestations; - const rootCache = new RootCache(postState); + const rootCache = new RootCache(postBlockState); const invalidAttestationErrorsByCode = new Map(); const addAttestation = fork >= ForkSeq.electra ? addAttestationPostElectra : addAttestationPreElectra; @@ -185,7 +186,7 @@ export async function importBlock( const attDataRoot = toRootHex(ssz.phase0.AttestationData.hashTreeRoot(indexedAttestation.data)); addAttestation.call( this, - postState, + postBlockState, target, attDataRoot, attestation as Attestation, @@ -300,7 +301,7 @@ export async function importBlock( if (newHead.blockRoot !== oldHead.blockRoot) { // Set head state as strong reference - this.regen.updateHeadState(newHead, postState); + this.regen.updateHeadState(newHead, postBlockState); try { this.emitter.emit(routes.events.EventType.head, { @@ -372,7 +373,7 @@ export async function importBlock( try { this.lightClientServer?.onImportBlockHead( block.message as BeaconBlock, - postState, + postBlockState, parentBlockSlot ); } catch (e) { @@ -393,11 +394,11 @@ export async function importBlock( // and the block is weak and can potentially be reorged out. let shouldOverrideFcu = false; - if (blockSlot >= currentSlot && postState.isExecutionStateType) { + if (blockSlot >= currentSlot && postBlockState.isExecutionStateType) { let notOverrideFcuReason = NotReorgedReason.Unknown; const proposalSlot = blockSlot + 1; try { - const proposerIndex = postState.getBeaconProposer(proposalSlot); + const proposerIndex = postBlockState.getBeaconProposer(proposalSlot); const feeRecipient = this.beaconProposerCache.get(proposerIndex); if (feeRecipient) { @@ -477,20 +478,20 @@ export async function importBlock( } } - if (!postState.isStateValidatorsNodesPopulated()) { - this.logger.verbose("After importBlock caching postState without SSZ cache", {slot: postState.slot}); + if (!postBlockState.isStateValidatorsNodesPopulated()) { + this.logger.verbose("After importBlock caching postState without SSZ cache", {slot: postBlockState.slot}); } // Cache shufflings when crossing an epoch boundary const parentEpoch = computeEpochAtSlot(parentBlockSlot); if (parentEpoch < blockEpoch) { - this.shufflingCache.processState(postState); + this.shufflingCache.processState(postBlockState); this.logger.verbose("Processed shuffling for next epoch", {parentEpoch, blockEpoch, slot: blockSlot}); } if (blockSlot % SLOTS_PER_EPOCH === 0) { // Cache state to preserve epoch transition work - const checkpointState = postState; + const checkpointState = postBlockState; const cp = getCheckpointFromState(checkpointState); this.regen.addCheckpointState(cp, checkpointState, payloadPresent); // consumers should not mutate state ever @@ -584,7 +585,7 @@ export async function importBlock( this.validatorMonitor?.registerSyncAggregateInBlock( blockEpoch, (block as altair.SignedBeaconBlock).message.body.syncAggregate, - fullyVerifiedBlock.postState.currentSyncCommitteeIndexed.validatorIndices + fullyVerifiedBlock.postBlockState.currentSyncCommitteeIndexed.validatorIndices ); } diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index b7eaac604985..581df67a73f2 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -1,5 +1,6 @@ import {routes} from "@lodestar/api"; -import {ForkName} from "@lodestar/params"; +import {ExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice"; +import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; import {getExecutionPayloadEnvelopeSignatureSet} from "@lodestar/state-transition"; import {byteArrayEquals, fromHex, toRootHex} from "@lodestar/utils"; import {ExecutionPayloadStatus} from "../../execution/index.js"; @@ -51,6 +52,19 @@ export class PayloadError extends Error { } } +function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExecutionStatus { + switch (status) { + case ExecutionPayloadStatus.VALID: + return ExecutionStatus.Valid; + // TODO GLOAS: Handle optimistic import for payload + case ExecutionPayloadStatus.SYNCING: + case ExecutionPayloadStatus.ACCEPTED: + return ExecutionStatus.Syncing; + default: + throw new Error(`Unexpected execution payload status for fork choice: ${status}`); + } +} + /** * Import an execution payload envelope after all data is available. * @@ -161,12 +175,7 @@ export async function importExecutionPayload( case ExecutionPayloadStatus.ACCEPTED: case ExecutionPayloadStatus.SYNCING: - // TODO GLOAS: Handle optimistic import for payload - for now treat as error - throw new PayloadError({ - code: PayloadErrorCode.EXECUTION_ENGINE_ERROR, - execStatus: execResult.status, - errorMessage: execResult.validationError ?? "EL syncing, payload not yet validated", - }); + break; case ExecutionPayloadStatus.INVALID_BLOCK_HASH: case ExecutionPayloadStatus.ELERROR: @@ -204,14 +213,16 @@ export async function importExecutionPayload( blockRootHex, payloadInput.getBlockHashHex(), envelope.message.payload.blockNumber, - toRootHex(postPayloadStateRoot) + toRootHex(postPayloadStateRoot), + toForkChoiceExecutionStatus(execResult.status) ); // 7. Cache payload state - // TODO GLOAS: Enable when PR #8868 merged (adds processPayloadState) - // this.regen.processPayloadState(postPayloadState); - // if epoch boundary also call - // this.regen.addCheckpointState(cp, checkpointState, true); + this.regen.processPayloadState(postPayloadState); + if (postPayloadState.slot % SLOTS_PER_EPOCH === 0) { + const {checkpoint} = postPayloadState.computeAnchorCheckpoint(); + this.regen.addCheckpointState(checkpoint, postPayloadState, true); + } // 8. Record metrics for payload envelope and column sources this.metrics?.importPayload.bySource.inc({source: payloadInput.getPayloadEnvelopeSource().source}); diff --git a/packages/beacon-node/src/chain/blocks/index.ts b/packages/beacon-node/src/chain/blocks/index.ts index b9120acae350..5fca016f2c80 100644 --- a/packages/beacon-node/src/chain/blocks/index.ts +++ b/packages/beacon-node/src/chain/blocks/index.ts @@ -88,7 +88,8 @@ export async function processBlocks( const fullyVerifiedBlocks = relevantBlocks.map( (block, i): FullyVerifiedBlock => ({ blockInput: block, - postState: postStates[i], + postBlockState: postStates[i], + postEnvelopeState: null, parentBlockSlot: parentSlots[i], executionStatus: executionStatuses[i], // start supporting optimistic syncing/processing diff --git a/packages/beacon-node/src/chain/blocks/types.ts b/packages/beacon-node/src/chain/blocks/types.ts index 4b1ef4d2e954..8da3f46f8422 100644 --- a/packages/beacon-node/src/chain/blocks/types.ts +++ b/packages/beacon-node/src/chain/blocks/types.ts @@ -1,5 +1,5 @@ import type {ChainForkConfig} from "@lodestar/config"; -import {MaybeValidExecutionStatus} from "@lodestar/fork-choice"; +import {BlockExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice"; import {ForkSeq} from "@lodestar/params"; import {DataAvailabilityStatus, IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition"; import type {IndexedAttestation, Slot, fulu} from "@lodestar/types"; @@ -88,24 +88,35 @@ export type ImportBlockOpts = { seenTimestampSec?: number; }; -/** - * A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and ready to import - */ -export type FullyVerifiedBlock = { +type FullyVerifiedBlockBase = { blockInput: IBlockInput; - postState: IBeaconStateView; + postBlockState: IBeaconStateView; parentBlockSlot: Slot; proposerBalanceDelta: number; - /** - * If the execution payload couldnt be verified because of EL syncing status, - * used in optimistic sync or for merge block - */ - executionStatus: MaybeValidExecutionStatus; dataAvailabilityStatus: DataAvailabilityStatus; - /** - * Pre-computed indexed attestations from signature verification to avoid duplicate work - */ + /** Pre-computed indexed attestations from signature verification to avoid duplicate work */ indexedAttestations: IndexedAttestation[]; /** Seen timestamp seconds */ seenTimestampSec: number; }; + +/** + * A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and ready to import. + * + * Discriminated union on `postEnvelopeState`: + * - `null` → block has no pre-verified envelope; `executionStatus` is any `BlockExecutionStatus` + * - non-null → envelope was pre-verified during state transition; `executionStatus` is narrowed to + * `Valid | Syncing` (matching what `forkChoice.onExecutionPayload` expects) + */ +export type FullyVerifiedBlock = FullyVerifiedBlockBase & + ( + | { + postEnvelopeState: null; + /** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync or for merge block */ + executionStatus: BlockExecutionStatus; + } + | { + postEnvelopeState: IBeaconStateView; + executionStatus: PayloadExecutionStatus; + } + ); diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts index 0247e6760af8..fee85c9a9823 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts @@ -1,10 +1,10 @@ import {ChainForkConfig} from "@lodestar/config"; import { + BlockExecutionStatus, ExecutionStatus, IForkChoice, LVHInvalidResponse, LVHValidResponse, - MaybeValidExecutionStatus, ProtoBlock, } from "@lodestar/fork-choice"; import {ForkSeq} from "@lodestar/params"; @@ -33,7 +33,7 @@ type ExecAbortType = {blockIndex: number; execError: BlockError}; export type SegmentExecStatus = | { execAborted: null; - executionStatuses: MaybeValidExecutionStatus[]; + executionStatuses: BlockExecutionStatus[]; executionTime: number; } | {execAborted: ExecAbortType; invalidSegmentLVH?: LVHInvalidResponse}; @@ -62,7 +62,7 @@ export async function verifyBlocksExecutionPayload( signal: AbortSignal, opts: BlockProcessOpts & ImportBlockOpts ): Promise { - const executionStatuses: MaybeValidExecutionStatus[] = []; + const executionStatuses: BlockExecutionStatus[] = []; const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000); const lastBlock = blockInputs.at(-1); @@ -103,7 +103,7 @@ export async function verifyBlocksExecutionPayload( return getSegmentErrorResponse({verifyResponse, blockIndex}, parentBlock, blockInputs); } - // If we are here then its because executionStatus is one of MaybeValidExecutionStatus + // If we are here then its because executionStatus is one of BlockExecutionStatus const {executionStatus} = verifyResponse; executionStatuses.push(executionStatus); } diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 9f4c1f4ecb57..85485f4f31c2 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -29,11 +29,12 @@ import {ForkChoiceMetrics} from "../metrics.js"; import {computeDeltas} from "../protoArray/computeDeltas.js"; import {ProtoArrayError, ProtoArrayErrorCode} from "../protoArray/errors.js"; import { + BlockExecutionStatus, ExecutionStatus, HEX_ZERO_HASH, LVHExecResponse, - MaybeValidExecutionStatus, NULL_VOTE_INDEX, + PayloadExecutionStatus, PayloadStatus, ProtoBlock, ProtoNode, @@ -585,7 +586,7 @@ export class ForkChoice implements IForkChoice { state: IBeaconStateView, blockDelaySec: number, currentSlot: Slot, - executionStatus: MaybeValidExecutionStatus, + executionStatus: BlockExecutionStatus, dataAvailabilityStatus: DataAvailabilityStatus ): ProtoBlock { const {parentRoot, slot} = block; @@ -977,7 +978,8 @@ export class ForkChoice implements IForkChoice { blockRoot: RootHex, executionPayloadBlockHash: RootHex, executionPayloadNumber: number, - executionPayloadStateRoot: RootHex + executionPayloadStateRoot: RootHex, + executionStatus: PayloadExecutionStatus ): void { this.protoArray.onExecutionPayload( blockRoot, @@ -985,7 +987,8 @@ export class ForkChoice implements IForkChoice { executionPayloadBlockHash, executionPayloadNumber, executionPayloadStateRoot, - this.proposerBoostRoot + this.proposerBoostRoot, + executionStatus ); } @@ -1425,7 +1428,7 @@ export class ForkChoice implements IForkChoice { return secFromSlot * 1000 <= proposerReorgCutoff; } - private getPreMergeExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PreMerge { + private getPreMergeExecStatus(executionStatus: BlockExecutionStatus): ExecutionStatus.PreMerge { if (executionStatus !== ExecutionStatus.PreMerge) throw Error(`Invalid pre-merge execution status: expected: ${ExecutionStatus.PreMerge}, got ${executionStatus}`); return executionStatus; @@ -1440,7 +1443,7 @@ export class ForkChoice implements IForkChoice { } private getPreGloasExecStatus( - executionStatus: MaybeValidExecutionStatus + executionStatus: BlockExecutionStatus ): ExecutionStatus.Valid | ExecutionStatus.Syncing { if (executionStatus === ExecutionStatus.PreMerge || executionStatus === ExecutionStatus.PayloadSeparated) throw Error( @@ -1449,7 +1452,7 @@ export class ForkChoice implements IForkChoice { return executionStatus; } - private getPostGloasExecStatus(executionStatus: MaybeValidExecutionStatus): ExecutionStatus.PayloadSeparated { + private getPostGloasExecStatus(executionStatus: BlockExecutionStatus): ExecutionStatus.PayloadSeparated { if (executionStatus !== ExecutionStatus.PayloadSeparated) throw Error( `Invalid post-gloas execution status: expected: ${ExecutionStatus.PayloadSeparated}, got ${executionStatus}` @@ -1855,6 +1858,7 @@ export function getCommitteeFraction( * Pre-Gloas: always FULL (payload embedded in block) * Gloas: determined by state.execution_payload_availability * + * @param config - The chain fork config to determine fork at checkpoint slot * @param state - The state to check execution_payload_availability * @param checkpointEpoch - The epoch of the checkpoint */ diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index eb21e1cbc837..23162d1dce81 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -1,8 +1,9 @@ import {DataAvailabilityStatus, EffectiveBalanceIncrements, IBeaconStateView} from "@lodestar/state-transition"; import {AttesterSlashing, BeaconBlock, Epoch, IndexedAttestation, Root, RootHex, Slot} from "@lodestar/types"; import { + BlockExecutionStatus, LVHExecResponse, - MaybeValidExecutionStatus, + PayloadExecutionStatus, PayloadStatus, ProtoBlock, ProtoNode, @@ -143,7 +144,7 @@ export interface IForkChoice { state: IBeaconStateView, blockDelaySec: number, currentSlot: Slot, - executionStatus: MaybeValidExecutionStatus, + executionStatus: BlockExecutionStatus, dataAvailabilityStatus: DataAvailabilityStatus ): ProtoBlock; /** @@ -203,7 +204,8 @@ export interface IForkChoice { blockRoot: RootHex, executionPayloadBlockHash: RootHex, executionPayloadNumber: number, - executionPayloadStateRoot: RootHex + executionPayloadStateRoot: RootHex, + executionStatus: PayloadExecutionStatus ): void; /** * Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`. diff --git a/packages/fork-choice/src/index.ts b/packages/fork-choice/src/index.ts index cf4f75bceffe..b4452e7c2d64 100644 --- a/packages/fork-choice/src/index.ts +++ b/packages/fork-choice/src/index.ts @@ -31,10 +31,11 @@ export { } from "./forkChoice/store.js"; export {type ForkChoiceMetrics, getForkChoiceMetrics} from "./metrics.js"; export type { + BlockExecutionStatus, BlockExtraMeta, LVHInvalidResponse, LVHValidResponse, - MaybeValidExecutionStatus, + PayloadExecutionStatus, ProtoBlock, ProtoNode, } from "./protoArray/interface.js"; diff --git a/packages/fork-choice/src/protoArray/interface.ts b/packages/fork-choice/src/protoArray/interface.ts index 02e9890297fc..cae46fdacd55 100644 --- a/packages/fork-choice/src/protoArray/interface.ts +++ b/packages/fork-choice/src/protoArray/interface.ts @@ -64,7 +64,18 @@ export type LVHInvalidResponse = { }; export type LVHExecResponse = LVHValidResponse | LVHInvalidResponse; -export type MaybeValidExecutionStatus = Exclude; +/** + * Any execution status that is not definitively invalid. + * Pre-Gloas: Valid | Syncing | PreMerge + * Post-Gloas: execution status must be PayloadSeparated (beacon block imported before its payload arrives via SignedExecutionPayloadEnvelope) + */ +export type BlockExecutionStatus = Exclude; + +/** + * Execution status for a block whose execution payload is present and has been submitted to the EL. + * Used post-Gloas when transitioning a PayloadSeparated block to FULL via onExecutionPayload(). + */ +export type PayloadExecutionStatus = ExecutionStatus.Valid | ExecutionStatus.Syncing; export type BlockExtraMeta = | { diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index 3335ffd75b0d..d06e923be995 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -9,6 +9,7 @@ import { ExecutionStatus, HEX_ZERO_HASH, LVHExecResponse, + PayloadExecutionStatus, PayloadStatus, ProtoBlock, ProtoNode, @@ -541,7 +542,8 @@ export class ProtoArray { executionPayloadBlockHash: RootHex, executionPayloadNumber: number, executionPayloadStateRoot: RootHex, - proposerBoostRoot: RootHex | null + proposerBoostRoot: RootHex | null, + executionStatus: PayloadExecutionStatus ): void { // First check if block exists const variants = this.indices.get(blockRoot); @@ -591,7 +593,7 @@ export class ProtoArray { weight: 0, bestChild: undefined, bestDescendant: undefined, - executionStatus: ExecutionStatus.Valid, + executionStatus, executionPayloadBlockHash, executionPayloadNumber, stateRoot: executionPayloadStateRoot, diff --git a/packages/fork-choice/test/unit/protoArray/executionStatusUpdates.test.ts b/packages/fork-choice/test/unit/protoArray/executionStatusUpdates.test.ts index 8a0a544baeb4..d4cddf32c483 100644 --- a/packages/fork-choice/test/unit/protoArray/executionStatusUpdates.test.ts +++ b/packages/fork-choice/test/unit/protoArray/executionStatusUpdates.test.ts @@ -1,9 +1,9 @@ import {describe, expect, it} from "vitest"; import {DataAvailabilityStatus} from "@lodestar/state-transition"; import { + BlockExecutionStatus, BlockExtraMeta, ExecutionStatus, - MaybeValidExecutionStatus, PayloadStatus, ProtoArray, ProtoBlock, @@ -17,7 +17,7 @@ type ValidationTestCase = { executionStatus: ExecutionStatus | undefined; }; -type TestBlock = {slot: number; root: string; parent: string; executionStatus: MaybeValidExecutionStatus}; +type TestBlock = {slot: number; root: string; parent: string; executionStatus: BlockExecutionStatus}; type TestCase = [string, string | undefined, string | undefined, ExecutionStatus]; const blocks: TestBlock[] = [ {slot: 1, root: "1A", parent: "0", executionStatus: ExecutionStatus.Syncing}, diff --git a/packages/fork-choice/test/unit/protoArray/gloas.test.ts b/packages/fork-choice/test/unit/protoArray/gloas.test.ts index 26dbe09f965f..ffef0bc6d5df 100644 --- a/packages/fork-choice/test/unit/protoArray/gloas.test.ts +++ b/packages/fork-choice/test/unit/protoArray/gloas.test.ts @@ -272,7 +272,15 @@ describe("Gloas Fork Choice", () => { expect(getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.FULL)).toBeUndefined(); // Call onExecutionPayload - protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, stateRoot, null); + protoArray.onExecutionPayload( + "0x02", + gloasForkSlot, + "0x02", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ); // FULL should now exist const fullNode = getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.FULL); @@ -284,7 +292,15 @@ describe("Gloas Fork Choice", () => { const block = createTestBlock(gloasForkSlot, "0x02", genesisRoot, genesisRoot); protoArray.onBlock(block, gloasForkSlot, null); - protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, stateRoot, null); + protoArray.onExecutionPayload( + "0x02", + gloasForkSlot, + "0x02", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ); const fullNode = getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.FULL); const pendingIndex = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.PENDING); @@ -296,8 +312,24 @@ describe("Gloas Fork Choice", () => { const block = createTestBlock(gloasForkSlot, "0x02", genesisRoot, genesisRoot); protoArray.onBlock(block, gloasForkSlot, null); - protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, stateRoot, null); - protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, stateRoot, null); + protoArray.onExecutionPayload( + "0x02", + gloasForkSlot, + "0x02", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ); + protoArray.onExecutionPayload( + "0x02", + gloasForkSlot, + "0x02", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ); // Should still only have one FULL node const fullNode = getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.FULL); @@ -313,13 +345,29 @@ describe("Gloas Fork Choice", () => { // Calling onExecutionPayload should throw for pre-Gloas blocks expect(() => - protoArray.onExecutionPayload("0x02", gloasForkSlot - 1, "0x02", gloasForkSlot - 1, stateRoot, null) + protoArray.onExecutionPayload( + "0x02", + gloasForkSlot - 1, + "0x02", + gloasForkSlot - 1, + stateRoot, + null, + ExecutionStatus.Valid + ) ).toThrow(); }); it("throws for unknown block", () => { expect(() => - protoArray.onExecutionPayload("0x99", gloasForkSlot, "0x99", gloasForkSlot, stateRoot, null) + protoArray.onExecutionPayload( + "0x99", + gloasForkSlot, + "0x99", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ) ).toThrow(); }); }); @@ -384,7 +432,15 @@ describe("Gloas Fork Choice", () => { protoArray.onBlock(block, gloasForkSlot, null); // Make execution payload available by creating FULL variant - protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, stateRoot, null); + protoArray.onExecutionPayload( + "0x02", + gloasForkSlot, + "0x02", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ); // Vote yes from majority of PTC (>50%) const threshold = Math.floor(PTC_SIZE / 2) + 1; @@ -400,7 +456,15 @@ describe("Gloas Fork Choice", () => { protoArray.onBlock(block, gloasForkSlot, null); // Make execution payload available by creating FULL variant - protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, stateRoot, null); + protoArray.onExecutionPayload( + "0x02", + gloasForkSlot, + "0x02", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ); // Vote yes from exactly 50% (not >50%) const threshold = Math.floor(PTC_SIZE / 2); @@ -416,7 +480,15 @@ describe("Gloas Fork Choice", () => { protoArray.onBlock(block, gloasForkSlot, null); // Make execution payload available by creating FULL variant - protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, stateRoot, null); + protoArray.onExecutionPayload( + "0x02", + gloasForkSlot, + "0x02", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ); // Vote mixed yes/no const threshold = Math.floor(PTC_SIZE / 2) + 1; @@ -469,7 +541,15 @@ describe("Gloas Fork Choice", () => { it("intra-block: EMPTY/FULL variants have PENDING as parent", () => { const block = createTestBlock(gloasForkSlot, "0x02", genesisRoot, genesisRoot); protoArray.onBlock(block, gloasForkSlot, null); - protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, stateRoot, null); + protoArray.onExecutionPayload( + "0x02", + gloasForkSlot, + "0x02", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ); const pendingIndex = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.PENDING); const emptyNode = getNodeByPayloadStatus(protoArray, "0x02", PayloadStatus.EMPTY); @@ -483,7 +563,15 @@ describe("Gloas Fork Choice", () => { // Block A const blockA = createTestBlock(gloasForkSlot, "0x02Root", genesisRoot, genesisRoot); protoArray.onBlock(blockA, gloasForkSlot, null); - protoArray.onExecutionPayload("0x02Root", gloasForkSlot, "0x02Hash", gloasForkSlot, stateRoot, null); + protoArray.onExecutionPayload( + "0x02Root", + gloasForkSlot, + "0x02Hash", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ); // Block B extends A's FULL (parentBlockHash matches) const blockB = createTestBlock(gloasForkSlot + 1, "0x03Root", "0x02Root", "0x02Hash"); @@ -512,7 +600,7 @@ describe("Gloas Fork Choice", () => { const blockSlot = gloasForkSlot + 10; const block = createTestBlock(blockSlot, "0x02", genesisRoot, genesisRoot); protoArray.onBlock(block, blockSlot, null); - protoArray.onExecutionPayload("0x02", blockSlot, "0x02", blockSlot, stateRoot, null); + protoArray.onExecutionPayload("0x02", blockSlot, "0x02", blockSlot, stateRoot, null, ExecutionStatus.Valid); const emptyIndex = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.EMPTY); if (emptyIndex === undefined) throw new Error("Expected emptyIndex to exist"); @@ -589,7 +677,7 @@ describe("Gloas Fork Choice", () => { const blockSlot = gloasForkSlot + 10; const block = createTestBlock(blockSlot, "0x02", genesisRoot, genesisRoot); protoArray.onBlock(block, blockSlot, null); - protoArray.onExecutionPayload("0x02", blockSlot, "0x02", blockSlot, stateRoot, null); + protoArray.onExecutionPayload("0x02", blockSlot, "0x02", blockSlot, stateRoot, null, ExecutionStatus.Valid); const emptyIndex = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.EMPTY); if (emptyIndex === undefined) throw new Error("Expected emptyIndex to exist"); @@ -646,7 +734,15 @@ describe("Gloas Fork Choice", () => { protoArray.onBlock(block3, gloasForkSlot + 2, null); // Create all three variants for block1: PENDING, EMPTY, FULL - protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, stateRoot, null); + protoArray.onExecutionPayload( + "0x02", + gloasForkSlot, + "0x02", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ); // Get block1's variants indices before pruning const block1PendingBefore = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.PENDING); @@ -691,7 +787,15 @@ describe("Gloas Fork Choice", () => { protoArray.onBlock(block2, gloasForkSlot + 1, null); // Create FULL variant for block1 - protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, stateRoot, null); + protoArray.onExecutionPayload( + "0x02", + gloasForkSlot, + "0x02", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ); // Set PTC votes for block1 const threshold = Math.floor(PTC_SIZE / 2) + 1; @@ -718,7 +822,15 @@ describe("Gloas Fork Choice", () => { protoArray.onBlock(block2, gloasForkSlot + 1, null); // Create all three variants for block1 - protoArray.onExecutionPayload("0x02", gloasForkSlot, "0x02", gloasForkSlot, stateRoot, null); + protoArray.onExecutionPayload( + "0x02", + gloasForkSlot, + "0x02", + gloasForkSlot, + stateRoot, + null, + ExecutionStatus.Valid + ); // Verify all three variants exist via the public API const pendingIdx = protoArray.getNodeIndexByRootAndStatus("0x02", PayloadStatus.PENDING); From 68690137da5978fa3c2d06809ec93b17d7ef30e6 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 30 Mar 2026 15:40:17 +0700 Subject: [PATCH 2/2] chore: add comment to revisit optimistic sync later --- packages/fork-choice/src/protoArray/protoArray.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index d06e923be995..485c4011e6ef 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -593,6 +593,7 @@ export class ProtoArray { weight: 0, bestChild: undefined, bestDescendant: undefined, + // TODO GLOAS: handle optimistic sync executionStatus, executionPayloadBlockHash, executionPayloadNumber,