diff --git a/packages/beacon-node/src/chain/emitter.ts b/packages/beacon-node/src/chain/emitter.ts index 9de32429069f..ba68352099ae 100644 --- a/packages/beacon-node/src/chain/emitter.ts +++ b/packages/beacon-node/src/chain/emitter.ts @@ -1,7 +1,7 @@ import {EventEmitter} from "node:events"; import {StrictEventEmitter} from "strict-event-emitter-types"; import {routes} from "@lodestar/api"; -import {CheckpointWithHex} from "@lodestar/fork-choice"; +import {CheckpointWithPayload} from "@lodestar/fork-choice"; import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; import {RootHex, deneb, fulu, phase0} from "@lodestar/types"; import {PeerIdStr} from "../util/peerId.js"; @@ -83,8 +83,8 @@ export type ChainEventData = { export type IChainEvents = ApiEvents & { [ChainEvent.checkpoint]: (checkpoint: phase0.Checkpoint, state: CachedBeaconStateAllForks) => void; - [ChainEvent.forkChoiceJustified]: (checkpoint: CheckpointWithHex) => void; - [ChainEvent.forkChoiceFinalized]: (checkpoint: CheckpointWithHex) => void; + [ChainEvent.forkChoiceJustified]: (checkpoint: CheckpointWithPayload) => void; + [ChainEvent.forkChoiceFinalized]: (checkpoint: CheckpointWithPayload) => void; [ChainEvent.updateTargetCustodyGroupCount]: (targetGroupCount: number) => void; diff --git a/packages/beacon-node/src/chain/forkChoice/index.ts b/packages/beacon-node/src/chain/forkChoice/index.ts index 091183e355c4..2b080934045e 100644 --- a/packages/beacon-node/src/chain/forkChoice/index.ts +++ b/packages/beacon-node/src/chain/forkChoice/index.ts @@ -8,6 +8,7 @@ import { ProtoArray, ProtoBlock, ForkChoiceOpts as RawForkChoiceOpts, + getCheckpointPayloadStatus, } from "@lodestar/fork-choice"; import {ZERO_HASH_HEX} from "@lodestar/params"; import { @@ -107,6 +108,12 @@ export function initializeForkChoiceFromFinalizedState( const isForkPostGloas = (state as CachedBeaconStateGloas).latestBlockHash !== undefined; + // Determine justified checkpoint payload status + const justifiedPayloadStatus = getCheckpointPayloadStatus(state, justifiedCheckpoint.epoch); + + // Determine finalized checkpoint payload status + const finalizedPayloadStatus = getCheckpointPayloadStatus(state, finalizedCheckpoint.epoch); + return new forkchoiceConstructor( config, @@ -116,6 +123,8 @@ export function initializeForkChoiceFromFinalizedState( finalizedCheckpoint, justifiedBalances, justifiedBalancesGetter, + justifiedPayloadStatus, + finalizedPayloadStatus, { onJustified: (cp) => emitter.emit(ChainEvent.forkChoiceJustified, cp), onFinalized: (cp) => emitter.emit(ChainEvent.forkChoiceFinalized, cp), @@ -196,20 +205,28 @@ export function initializeForkChoiceFromUnfinalizedState( // this is not the justified state, but there is no other ways to get justified balances const justifiedBalances = getEffectiveBalanceIncrementsZeroInactive(unfinalizedState); + + const isForkPostGloas = (unfinalizedState as CachedBeaconStateGloas).latestBlockHash !== undefined; + + // For unfinalized state, use getCheckpointPayloadStatus to determine the correct status. + // It checks state.execution_payload_availability to determine EMPTY vs FULL. + const justifiedPayloadStatus = getCheckpointPayloadStatus(unfinalizedState, justifiedCheckpoint.epoch); + const finalizedPayloadStatus = getCheckpointPayloadStatus(unfinalizedState, finalizedCheckpoint.epoch); + const store = new ForkChoiceStore( currentSlot, justifiedCheckpoint, finalizedCheckpoint, justifiedBalances, justifiedBalancesGetter, + justifiedPayloadStatus, + finalizedPayloadStatus, { onJustified: (cp) => emitter.emit(ChainEvent.forkChoiceJustified, cp), onFinalized: (cp) => emitter.emit(ChainEvent.forkChoiceFinalized, cp), } ); - const isForkPostGloas = (unfinalizedState as CachedBeaconStateGloas).latestBlockHash !== undefined; - // this is the same to the finalized state const headBlock: ProtoBlock = { slot: blockHeader.slot, diff --git a/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts index 02b3f6d7f3f8..4817e5e61765 100644 --- a/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts +++ b/packages/beacon-node/test/perf/chain/opPools/aggregatedAttestationPool.test.ts @@ -1,7 +1,7 @@ import {beforeAll, bench, describe} from "@chainsafe/benchmark"; import {BitArray, toHexString} from "@chainsafe/ssz"; import {createBeaconConfig, defaultChainConfig} from "@lodestar/config"; -import {ExecutionStatus, ForkChoice, IForkChoiceStore, ProtoArray} from "@lodestar/fork-choice"; +import {ExecutionStatus, ForkChoice, IForkChoiceStore, PayloadStatus, ProtoArray} from "@lodestar/fork-choice"; import {HISTORICAL_ROOTS_LIMIT, SLOTS_PER_EPOCH} from "@lodestar/params"; import { CachedBeaconStateAltair, @@ -116,16 +116,32 @@ describe(`getAttestationsForBlock vc=${vc}`, () => { const fcStore: IForkChoiceStore = { currentSlot: originalState.slot, justified: { - checkpoint: {...justifiedCheckpoint, rootHex: toHexString(justifiedCheckpoint.root)}, + checkpoint: { + ...justifiedCheckpoint, + rootHex: toHexString(justifiedCheckpoint.root), + payloadStatus: PayloadStatus.FULL, + }, balances: originalState.epochCtx.effectiveBalanceIncrements, totalBalance, }, unrealizedJustified: { - checkpoint: {...justifiedCheckpoint, rootHex: toHexString(justifiedCheckpoint.root)}, + checkpoint: { + ...justifiedCheckpoint, + rootHex: toHexString(justifiedCheckpoint.root), + payloadStatus: PayloadStatus.FULL, + }, balances: originalState.epochCtx.effectiveBalanceIncrements, }, - finalizedCheckpoint: {...finalizedCheckpoint, rootHex: toHexString(finalizedCheckpoint.root)}, - unrealizedFinalizedCheckpoint: {...finalizedCheckpoint, rootHex: toHexString(finalizedCheckpoint.root)}, + finalizedCheckpoint: { + ...finalizedCheckpoint, + rootHex: toHexString(finalizedCheckpoint.root), + payloadStatus: PayloadStatus.FULL, + }, + unrealizedFinalizedCheckpoint: { + ...finalizedCheckpoint, + rootHex: toHexString(finalizedCheckpoint.root), + payloadStatus: PayloadStatus.FULL, + }, justifiedBalancesGetter: () => originalState.epochCtx.effectiveBalanceIncrements, equivocatingIndices: new Set(), }; diff --git a/packages/beacon-node/test/unit/chain/blocks/verifyBlocksSanityChecks.test.ts b/packages/beacon-node/test/unit/chain/blocks/verifyBlocksSanityChecks.test.ts index 6298c7e5e48e..294ce7a58c58 100644 --- a/packages/beacon-node/test/unit/chain/blocks/verifyBlocksSanityChecks.test.ts +++ b/packages/beacon-node/test/unit/chain/blocks/verifyBlocksSanityChecks.test.ts @@ -1,6 +1,6 @@ import {beforeEach, describe, expect, it} from "vitest"; import {config} from "@lodestar/config/default"; -import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; +import {IForkChoice, PayloadStatus, ProtoBlock} from "@lodestar/fork-choice"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {SignedBeaconBlock, Slot, ssz} from "@lodestar/types"; import {toHex, toRootHex} from "@lodestar/utils"; @@ -25,7 +25,12 @@ describe("chain / blocks / verifyBlocksSanityChecks", () => { block.message.slot = currentSlot; forkChoice = getMockedBeaconChain().forkChoice; - forkChoice.getFinalizedCheckpoint.mockReturnValue({epoch: 0, root: Buffer.alloc(32), rootHex: ""}); + forkChoice.getFinalizedCheckpoint.mockReturnValue({ + epoch: 0, + root: Buffer.alloc(32), + rootHex: "", + payloadStatus: PayloadStatus.FULL, + }); clock = new ClockStopped(currentSlot); modules = {config, forkChoice, clock, opts: {} as IChainOptions, blacklistedBlocks: new Map()}; // On first call, parentRoot is known @@ -48,7 +53,12 @@ describe("chain / blocks / verifyBlocksSanityChecks", () => { }); it("WOULD_REVERT_FINALIZED_SLOT", () => { - forkChoice.getFinalizedCheckpoint.mockReturnValue({epoch: 5, root: Buffer.alloc(32), rootHex: ""}); + forkChoice.getFinalizedCheckpoint.mockReturnValue({ + epoch: 5, + root: Buffer.alloc(32), + rootHex: "", + payloadStatus: PayloadStatus.FULL, + }); expectThrowsLodestarError( () => verifyBlocksSanityChecks(modules, [block], {}), BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT diff --git a/packages/beacon-node/test/unit/chain/seenCache/seenBlockInput.test.ts b/packages/beacon-node/test/unit/chain/seenCache/seenBlockInput.test.ts index 1d82643f17ef..f49cbe7ca831 100644 --- a/packages/beacon-node/test/unit/chain/seenCache/seenBlockInput.test.ts +++ b/packages/beacon-node/test/unit/chain/seenCache/seenBlockInput.test.ts @@ -1,5 +1,6 @@ import {generateKeyPair} from "@libp2p/crypto/keys"; import {beforeEach, describe, expect, it} from "vitest"; +import {PayloadStatus} from "@lodestar/fork-choice"; import {ForkName, ForkPostFulu, ForkPreGloas} from "@lodestar/params"; import {signedBlockToSignedHeader} from "@lodestar/state-transition"; import {SignedBeaconBlock} from "@lodestar/types"; @@ -218,6 +219,7 @@ describe("SeenBlockInputCache", async () => { epoch: config.DENEB_FORK_EPOCH, root, rootHex, + payloadStatus: PayloadStatus.FULL, }); expect(cache.get(childRootHex)).toBeUndefined(); expect(cache.get(parentRootHex)).toBeUndefined(); @@ -228,6 +230,7 @@ describe("SeenBlockInputCache", async () => { epoch: config.CAPELLA_FORK_EPOCH, root, rootHex, + payloadStatus: PayloadStatus.FULL, }); expect(cache.get(childRootHex)).toBe(childBlockInput); expect(cache.get(parentRootHex)).toBe(parentBlockInput); diff --git a/packages/beacon-node/test/unit/chain/validation/block.test.ts b/packages/beacon-node/test/unit/chain/validation/block.test.ts index 0139d2e59a00..1f21051fdc97 100644 --- a/packages/beacon-node/test/unit/chain/validation/block.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/block.test.ts @@ -1,7 +1,7 @@ import {Mock, Mocked, beforeEach, describe, it, vi} from "vitest"; import {createBeaconConfig, createChainForkConfig} from "@lodestar/config"; import {config as configDef} from "@lodestar/config/default"; -import {ProtoBlock} from "@lodestar/fork-choice"; +import {PayloadStatus, ProtoBlock} from "@lodestar/fork-choice"; import {ForkName, ForkPostDeneb, ForkPreFulu} from "@lodestar/params"; import {SignedBeaconBlock, ssz} from "@lodestar/types"; import {BlockErrorCode} from "../../../../src/chain/errors/index.js"; @@ -46,7 +46,12 @@ describe("gossip block validation", () => { verifySignature = chain.bls.verifySignatureSets; verifySignature.mockResolvedValue(true); - forkChoice.getFinalizedCheckpoint.mockReturnValue({epoch: 0, root: ZERO_HASH, rootHex: ""}); + forkChoice.getFinalizedCheckpoint.mockReturnValue({ + epoch: 0, + root: ZERO_HASH, + rootHex: "", + payloadStatus: PayloadStatus.FULL, + }); // Reset seen cache ( @@ -70,7 +75,12 @@ describe("gossip block validation", () => { it("WOULD_REVERT_FINALIZED_SLOT", async () => { // Set finalized epoch to be greater than block's epoch - forkChoice.getFinalizedCheckpoint.mockReturnValue({epoch: Infinity, root: ZERO_HASH, rootHex: ""}); + forkChoice.getFinalizedCheckpoint.mockReturnValue({ + epoch: Infinity, + root: ZERO_HASH, + rootHex: "", + payloadStatus: PayloadStatus.FULL, + }); await expectRejectedWithLodestarError( validateGossipBlock(config, chain, job, ForkName.phase0), diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 75237914ac7a..a6d858ac9c76 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -1,7 +1,8 @@ import {ChainForkConfig} from "@lodestar/config"; -import {SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; +import {ForkSeq, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; import { CachedBeaconStateAllForks, + CachedBeaconStateGloas, DataAvailabilityStatus, EffectiveBalanceIncrements, ZERO_HASH, @@ -53,7 +54,7 @@ import { NotReorgedReason, ShouldOverrideForkChoiceUpdateResult, } from "./interface.js"; -import {CheckpointWithHex, IForkChoiceStore, JustifiedBalances, toCheckpointWithHex} from "./store.js"; +import {CheckpointWithPayload, IForkChoiceStore, JustifiedBalances, toCheckpointWithPayload} from "./store.js"; export type ForkChoiceOpts = { proposerBoost?: boolean; @@ -556,11 +557,11 @@ export class ForkChoice implements IForkChoice { return this.protoArray.nodes; } - getFinalizedCheckpoint(): CheckpointWithHex { + getFinalizedCheckpoint(): CheckpointWithPayload { return this.fcStore.finalizedCheckpoint; } - getJustifiedCheckpoint(): CheckpointWithHex { + getJustifiedCheckpoint(): CheckpointWithPayload { return this.fcStore.justified.checkpoint; } @@ -666,10 +667,15 @@ export class ForkChoice implements IForkChoice { this.proposerBoostRoot = blockRootHex; } - const justifiedCheckpoint = toCheckpointWithHex(state.currentJustifiedCheckpoint); - const finalizedCheckpoint = toCheckpointWithHex(state.finalizedCheckpoint); + // Get justified checkpoint with payload status for Gloas + const justifiedPayloadStatus = getCheckpointPayloadStatus(state, state.currentJustifiedCheckpoint.epoch); + const justifiedCheckpoint = toCheckpointWithPayload(state.currentJustifiedCheckpoint, justifiedPayloadStatus); const stateJustifiedEpoch = justifiedCheckpoint.epoch; + // Get finalized checkpoint with payload status for Gloas + const finalizedPayloadStatus = getCheckpointPayloadStatus(state, state.finalizedCheckpoint.epoch); + const finalizedCheckpoint = toCheckpointWithPayload(state.finalizedCheckpoint, finalizedPayloadStatus); + // Justified balances for `justifiedCheckpoint` are new to the fork-choice. Compute them on demand only if // the justified checkpoint changes this.updateCheckpoints(justifiedCheckpoint, finalizedCheckpoint, () => @@ -690,29 +696,57 @@ export class ForkChoice implements IForkChoice { // This is an optimization. It should reduce the amount of times we run // `process_justification_and_finalization` by approximately 1/3rd when the chain is // performing optimally. - let unrealizedJustifiedCheckpoint: CheckpointWithHex; - let unrealizedFinalizedCheckpoint: CheckpointWithHex; + let unrealizedJustifiedCheckpoint: CheckpointWithPayload; + let unrealizedFinalizedCheckpoint: CheckpointWithPayload; if (this.opts?.computeUnrealized) { if ( parentBlock.unrealizedJustifiedEpoch === blockEpoch && parentBlock.unrealizedFinalizedEpoch + 1 >= blockEpoch ) { // reuse from parent, happens at 1/3 last blocks of epoch as monitored in mainnet + // Get payload status for unrealized justified checkpoint + const unrealizedJustifiedPayloadStatus = getCheckpointPayloadStatus( + state, + parentBlock.unrealizedJustifiedEpoch + ); unrealizedJustifiedCheckpoint = { epoch: parentBlock.unrealizedJustifiedEpoch, root: fromHex(parentBlock.unrealizedJustifiedRoot), rootHex: parentBlock.unrealizedJustifiedRoot, + payloadStatus: unrealizedJustifiedPayloadStatus, }; + // Get payload status for unrealized finalized checkpoint + const unrealizedFinalizedPayloadStatus = getCheckpointPayloadStatus( + state, + parentBlock.unrealizedFinalizedEpoch + ); unrealizedFinalizedCheckpoint = { epoch: parentBlock.unrealizedFinalizedEpoch, root: fromHex(parentBlock.unrealizedFinalizedRoot), rootHex: parentBlock.unrealizedFinalizedRoot, + payloadStatus: unrealizedFinalizedPayloadStatus, }; } else { // compute new, happens 2/3 first blocks of epoch as monitored in mainnet const unrealized = computeUnrealizedCheckpoints(state); - unrealizedJustifiedCheckpoint = toCheckpointWithHex(unrealized.justifiedCheckpoint); - unrealizedFinalizedCheckpoint = toCheckpointWithHex(unrealized.finalizedCheckpoint); + // Get payload status for unrealized justified checkpoint + const unrealizedJustifiedPayloadStatus = getCheckpointPayloadStatus( + state, + unrealized.justifiedCheckpoint.epoch + ); + unrealizedJustifiedCheckpoint = toCheckpointWithPayload( + unrealized.justifiedCheckpoint, + unrealizedJustifiedPayloadStatus + ); + // Get payload status for unrealized finalized checkpoint + const unrealizedFinalizedPayloadStatus = getCheckpointPayloadStatus( + state, + unrealized.finalizedCheckpoint.epoch + ); + unrealizedFinalizedCheckpoint = toCheckpointWithPayload( + unrealized.finalizedCheckpoint, + unrealizedFinalizedPayloadStatus + ); } } else { unrealizedJustifiedCheckpoint = justifiedCheckpoint; @@ -1041,12 +1075,8 @@ export class ForkChoice implements IForkChoice { } getJustifiedBlock(): ProtoBlock { - const {rootHex, epoch} = this.fcStore.justified.checkpoint; - // Checkpoints for pre-gloas should be FULL variant, while post-gloas should be EMPTY variant - const block = this.getBlockHex( - rootHex, - epoch >= this.config.GLOAS_FORK_EPOCH ? PayloadStatus.EMPTY : PayloadStatus.FULL - ); + const {rootHex, payloadStatus} = this.fcStore.justified.checkpoint; + const block = this.getBlockHex(rootHex, payloadStatus); if (!block) { throw new ForkChoiceError({ code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK, @@ -1057,12 +1087,8 @@ export class ForkChoice implements IForkChoice { } getFinalizedBlock(): ProtoBlock { - const {rootHex, epoch} = this.fcStore.finalizedCheckpoint; - // Checkpoints for pre-gloas should be FULL variant, while post-gloas should be EMPTY variant - const block = this.getBlockHex( - rootHex, - epoch >= this.config.GLOAS_FORK_EPOCH ? PayloadStatus.EMPTY : PayloadStatus.FULL - ); + const {rootHex, payloadStatus} = this.fcStore.finalizedCheckpoint; + const block = this.getBlockHex(rootHex, payloadStatus); if (!block) { throw new ForkChoiceError({ code: ForkChoiceErrorCode.MISSING_PROTO_ARRAY_BLOCK, @@ -1393,12 +1419,12 @@ export class ForkChoice implements IForkChoice { * * **`on_tick`** * May need the justified balances of: - * - unrealizedJustified: Already available in `CheckpointHexWithBalance` + * - unrealizedJustified: Already available in `CheckpointWithPayloadAndBalance` * Since this balances are already available the getter is just `() => balances`, without cache interaction */ private updateCheckpoints( - justifiedCheckpoint: CheckpointWithHex, - finalizedCheckpoint: CheckpointWithHex, + justifiedCheckpoint: CheckpointWithPayload, + finalizedCheckpoint: CheckpointWithPayload, getJustifiedBalances: () => JustifiedBalances ): void { // Update justified checkpoint. @@ -1418,8 +1444,8 @@ export class ForkChoice implements IForkChoice { * Update unrealized checkpoints in store if necessary */ private updateUnrealizedCheckpoints( - unrealizedJustifiedCheckpoint: CheckpointWithHex, - unrealizedFinalizedCheckpoint: CheckpointWithHex, + unrealizedJustifiedCheckpoint: CheckpointWithPayload, + unrealizedFinalizedCheckpoint: CheckpointWithPayload, getJustifiedBalances: () => JustifiedBalances ): void { if (unrealizedJustifiedCheckpoint.epoch > this.fcStore.unrealizedJustified.checkpoint.epoch) { @@ -1756,3 +1782,30 @@ export function getCommitteeFraction( const committeeWeight = Math.floor(justifiedTotalActiveBalanceByIncrement / config.slotsPerEpoch); return Math.floor((committeeWeight * config.committeePercent) / 100); } + +/** + * Get the payload status for a checkpoint. + * + * Pre-Gloas: always FULL (payload embedded in block) + * Gloas: determined by state.execution_payload_availability + * + * @param state - The state to check execution_payload_availability + * @param checkpointEpoch - The epoch of the checkpoint + */ +export function getCheckpointPayloadStatus(state: CachedBeaconStateAllForks, checkpointEpoch: number): PayloadStatus { + const fork = state.config.getForkSeq(state.slot); + + // Pre-Gloas: always FULL + if (fork < ForkSeq.gloas) { + return PayloadStatus.FULL; + } + + // For Gloas, check state.execution_payload_availability + // - For non-skipped slots at checkpoint: returns false (EMPTY) since payload hasn't arrived yet + // - For skipped slots at checkpoint: returns the actual availability status from state + const checkpointSlot = computeStartSlotAtEpoch(checkpointEpoch); + const gloasState = state as CachedBeaconStateGloas; + const payloadAvailable = gloasState.executionPayloadAvailability.get(checkpointSlot % SLOTS_PER_HISTORICAL_ROOT); + + return payloadAvailable ? PayloadStatus.FULL : PayloadStatus.EMPTY; +} diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index d036878ea536..0782322a3bb8 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -12,7 +12,7 @@ import { ProtoNode, } from "../protoArray/interface.js"; import {UpdateAndGetHeadOpt} from "./forkChoice.js"; -import {CheckpointWithHex} from "./store.js"; +import {CheckpointWithHex, CheckpointWithPayload} from "./store.js"; export type CheckpointHex = { epoch: Epoch; @@ -24,12 +24,12 @@ export type CheckpointsWithHex = { finalizedCheckpoint: CheckpointWithHex; }; -export type CheckpointHexWithBalance = { - checkpoint: CheckpointWithHex; +export type CheckpointWithPayloadAndBalance = { + checkpoint: CheckpointWithPayload; balances: EffectiveBalanceIncrements; }; -export type CheckpointHexWithTotalBalance = CheckpointHexWithBalance & { +export type CheckpointWithPayloadAndTotalBalance = CheckpointWithPayloadAndBalance & { totalBalance: number; }; diff --git a/packages/fork-choice/src/forkChoice/store.ts b/packages/fork-choice/src/forkChoice/store.ts index fda01689f96c..d71b75c64b37 100644 --- a/packages/fork-choice/src/forkChoice/store.ts +++ b/packages/fork-choice/src/forkChoice/store.ts @@ -1,7 +1,8 @@ import {CachedBeaconStateAllForks, EffectiveBalanceIncrements} from "@lodestar/state-transition"; import {RootHex, Slot, ValidatorIndex, phase0} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; -import {CheckpointHexWithBalance, CheckpointHexWithTotalBalance} from "./interface.js"; +import {PayloadStatus} from "../protoArray/interface.js"; +import {CheckpointWithPayloadAndBalance, CheckpointWithPayloadAndTotalBalance} from "./interface.js"; /** * Stores checkpoints in a hybrid format: @@ -10,6 +11,15 @@ import {CheckpointHexWithBalance, CheckpointHexWithTotalBalance} from "./interfa */ export type CheckpointWithHex = phase0.Checkpoint & {rootHex: RootHex}; +/** + * Checkpoint with payload status for Gloas fork choice. + * Used to track which variant (EMPTY or FULL) of the finalized/justified block to use. + * + * Pre-Gloas: payloadStatus is always FULL (payload embedded in block) + * Gloas: determined by state.execution_payload_availability + */ +export type CheckpointWithPayload = CheckpointWithHex & {payloadStatus: PayloadStatus}; + export type JustifiedBalances = EffectiveBalanceIncrements; /** @@ -37,11 +47,11 @@ export type JustifiedBalancesGetter = ( */ export interface IForkChoiceStore { currentSlot: Slot; - get justified(): CheckpointHexWithTotalBalance; - set justified(justified: CheckpointHexWithBalance); - unrealizedJustified: CheckpointHexWithBalance; - finalizedCheckpoint: CheckpointWithHex; - unrealizedFinalizedCheckpoint: CheckpointWithHex; + get justified(): CheckpointWithPayloadAndTotalBalance; + set justified(justified: CheckpointWithPayloadAndBalance); + unrealizedJustified: CheckpointWithPayloadAndBalance; + finalizedCheckpoint: CheckpointWithPayload; + unrealizedFinalizedCheckpoint: CheckpointWithPayload; justifiedBalancesGetter: JustifiedBalancesGetter; equivocatingIndices: Set; } @@ -50,10 +60,10 @@ export interface IForkChoiceStore { * IForkChoiceStore implementer which emits forkChoice events on updated justified and finalized checkpoints. */ export class ForkChoiceStore implements IForkChoiceStore { - private _justified: CheckpointHexWithTotalBalance; - unrealizedJustified: CheckpointHexWithBalance; - private _finalizedCheckpoint: CheckpointWithHex; - unrealizedFinalizedCheckpoint: CheckpointWithHex; + private _justified: CheckpointWithPayloadAndTotalBalance; + unrealizedJustified: CheckpointWithPayloadAndBalance; + private _finalizedCheckpoint: CheckpointWithPayload; + unrealizedFinalizedCheckpoint: CheckpointWithPayload; equivocatingIndices = new Set(); justifiedBalancesGetter: JustifiedBalancesGetter; currentSlot: Slot; @@ -64,37 +74,49 @@ export class ForkChoiceStore implements IForkChoiceStore { finalizedCheckpoint: phase0.Checkpoint, justifiedBalances: EffectiveBalanceIncrements, justifiedBalancesGetter: JustifiedBalancesGetter, + /** + * Payload status for justified checkpoint. + * Pre-Gloas: always FULL + * Gloas: determined by state.execution_payload_availability + */ + justifiedPayloadStatus: PayloadStatus, + /** + * Payload status for finalized checkpoint. + * Pre-Gloas: always FULL + * Gloas: determined by state.execution_payload_availability + */ + finalizedPayloadStatus: PayloadStatus, private readonly events?: { - onJustified: (cp: CheckpointWithHex) => void; - onFinalized: (cp: CheckpointWithHex) => void; + onJustified: (cp: CheckpointWithPayload) => void; + onFinalized: (cp: CheckpointWithPayload) => void; } ) { this.justifiedBalancesGetter = justifiedBalancesGetter; this.currentSlot = currentSlot; const justified = { - checkpoint: toCheckpointWithHex(justifiedCheckpoint), + checkpoint: toCheckpointWithPayload(justifiedCheckpoint, justifiedPayloadStatus), balances: justifiedBalances, totalBalance: computeTotalBalance(justifiedBalances), }; this._justified = justified; this.unrealizedJustified = justified; - this._finalizedCheckpoint = toCheckpointWithHex(finalizedCheckpoint); + this._finalizedCheckpoint = toCheckpointWithPayload(finalizedCheckpoint, finalizedPayloadStatus); this.unrealizedFinalizedCheckpoint = this._finalizedCheckpoint; } - get justified(): CheckpointHexWithTotalBalance { + get justified(): CheckpointWithPayloadAndTotalBalance { return this._justified; } - set justified(justified: CheckpointHexWithBalance) { + set justified(justified: CheckpointWithPayloadAndBalance) { this._justified = {...justified, totalBalance: computeTotalBalance(justified.balances)}; this.events?.onJustified(justified.checkpoint); } - get finalizedCheckpoint(): CheckpointWithHex { + get finalizedCheckpoint(): CheckpointWithPayload { return this._finalizedCheckpoint; } - set finalizedCheckpoint(checkpoint: CheckpointWithHex) { - const cp = toCheckpointWithHex(checkpoint); + set finalizedCheckpoint(checkpoint: CheckpointWithPayload) { + const cp = toCheckpointWithPayload(checkpoint, checkpoint.payloadStatus); this._finalizedCheckpoint = cp; this.events?.onFinalized(cp); } @@ -111,6 +133,16 @@ export function toCheckpointWithHex(checkpoint: phase0.Checkpoint): CheckpointWi }; } +export function toCheckpointWithPayload( + checkpoint: phase0.Checkpoint, + payloadStatus: PayloadStatus +): CheckpointWithPayload { + return { + ...toCheckpointWithHex(checkpoint), + payloadStatus, + }; +} + export function equalCheckpointWithHex(a: CheckpointWithHex, b: CheckpointWithHex): boolean { return a.epoch === b.epoch && a.rootHex === b.rootHex; } diff --git a/packages/fork-choice/src/index.ts b/packages/fork-choice/src/index.ts index 39d42df73a96..99246e5225f8 100644 --- a/packages/fork-choice/src/index.ts +++ b/packages/fork-choice/src/index.ts @@ -6,10 +6,17 @@ export { type InvalidBlock, InvalidBlockCode, } from "./forkChoice/errors.js"; -export {ForkChoice, type ForkChoiceOpts, UpdateHeadOpt} from "./forkChoice/forkChoice.js"; +export { + ForkChoice, + type ForkChoiceOpts, + UpdateHeadOpt, + getCheckpointPayloadStatus, +} from "./forkChoice/forkChoice.js"; export { type AncestorResult, AncestorStatus, + type CheckpointWithPayloadAndBalance, + type CheckpointWithPayloadAndTotalBalance, EpochDifference, type IForkChoice, NotReorgedReason, @@ -17,6 +24,7 @@ export { export * from "./forkChoice/safeBlocks.js"; export { type CheckpointWithHex, + type CheckpointWithPayload, ForkChoiceStore, type IForkChoiceStore, type JustifiedBalancesGetter, diff --git a/packages/fork-choice/test/perf/forkChoice/util.ts b/packages/fork-choice/test/perf/forkChoice/util.ts index 72e4284b6e2b..59fc71630660 100644 --- a/packages/fork-choice/test/perf/forkChoice/util.ts +++ b/packages/fork-choice/test/perf/forkChoice/util.ts @@ -47,16 +47,36 @@ export function initializeForkChoice(opts: Opts): ForkChoice { const fcStore: IForkChoiceStore = { currentSlot: genesisSlot, justified: { - checkpoint: {epoch: genesisEpoch, root: fromHexString(genesisRoot), rootHex: genesisRoot}, + checkpoint: { + epoch: genesisEpoch, + root: fromHexString(genesisRoot), + rootHex: genesisRoot, + payloadStatus: PayloadStatus.FULL, + }, balances, totalBalance: computeTotalBalance(balances), }, unrealizedJustified: { - checkpoint: {epoch: genesisEpoch, root: fromHexString(genesisRoot), rootHex: genesisRoot}, + checkpoint: { + epoch: genesisEpoch, + root: fromHexString(genesisRoot), + rootHex: genesisRoot, + payloadStatus: PayloadStatus.FULL, + }, balances, }, - finalizedCheckpoint: {epoch: genesisEpoch, root: fromHexString(genesisRoot), rootHex: genesisRoot}, - unrealizedFinalizedCheckpoint: {epoch: genesisEpoch, root: fromHexString(genesisRoot), rootHex: genesisRoot}, + finalizedCheckpoint: { + epoch: genesisEpoch, + root: fromHexString(genesisRoot), + rootHex: genesisRoot, + payloadStatus: PayloadStatus.FULL, + }, + unrealizedFinalizedCheckpoint: { + epoch: genesisEpoch, + root: fromHexString(genesisRoot), + rootHex: genesisRoot, + payloadStatus: PayloadStatus.FULL, + }, justifiedBalancesGetter: () => balances, equivocatingIndices: new Set(Array.from({length: opts.initialEquivocatedCount}, (_, i) => i)), }; diff --git a/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts b/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts index 7350f1ef2afc..5188498cfb59 100644 --- a/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts +++ b/packages/fork-choice/test/unit/forkChoice/forkChoice.test.ts @@ -55,16 +55,36 @@ describe("Forkchoice", () => { const fcStore: IForkChoiceStore = { currentSlot: genesisSlot + 1, justified: { - checkpoint: {epoch: genesisEpoch, root: fromHexString(finalizedRoot), rootHex: finalizedRoot}, + checkpoint: { + epoch: genesisEpoch, + root: fromHexString(finalizedRoot), + rootHex: finalizedRoot, + payloadStatus: PayloadStatus.FULL, + }, balances: new Uint16Array([32]), totalBalance: 32, }, unrealizedJustified: { - checkpoint: {epoch: genesisEpoch, root: fromHexString(finalizedRoot), rootHex: finalizedRoot}, + checkpoint: { + epoch: genesisEpoch, + root: fromHexString(finalizedRoot), + rootHex: finalizedRoot, + payloadStatus: PayloadStatus.FULL, + }, balances: new Uint16Array([32]), }, - finalizedCheckpoint: {epoch: genesisEpoch, root: fromHexString(finalizedRoot), rootHex: finalizedRoot}, - unrealizedFinalizedCheckpoint: {epoch: genesisEpoch, root: fromHexString(finalizedRoot), rootHex: finalizedRoot}, + finalizedCheckpoint: { + epoch: genesisEpoch, + root: fromHexString(finalizedRoot), + rootHex: finalizedRoot, + payloadStatus: PayloadStatus.FULL, + }, + unrealizedFinalizedCheckpoint: { + epoch: genesisEpoch, + root: fromHexString(finalizedRoot), + rootHex: finalizedRoot, + payloadStatus: PayloadStatus.FULL, + }, justifiedBalancesGetter: () => new Uint16Array([32]), equivocatingIndices: new Set(), }; diff --git a/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts b/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts index adcf0d0900c6..ef666a080a39 100644 --- a/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts +++ b/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts @@ -118,23 +118,35 @@ describe("Forkchoice / GetProposerHead", () => { const fcStore: IForkChoiceStore = { currentSlot: genesisSlot + 1, justified: { - checkpoint: {epoch: genesisEpoch, root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot}, + checkpoint: { + epoch: genesisEpoch, + root: fromHexString(genesisBlock.blockRoot), + rootHex: genesisBlock.blockRoot, + payloadStatus: PayloadStatus.FULL, + }, balances: new Uint16Array(Array(32).fill(150)), totalBalance: 32 * 150, }, unrealizedJustified: { - checkpoint: {epoch: genesisEpoch, root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot}, + checkpoint: { + epoch: genesisEpoch, + root: fromHexString(genesisBlock.blockRoot), + rootHex: genesisBlock.blockRoot, + payloadStatus: PayloadStatus.FULL, + }, balances: new Uint16Array(Array(32).fill(150)), }, finalizedCheckpoint: { epoch: genesisEpoch, root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot, + payloadStatus: PayloadStatus.FULL, }, unrealizedFinalizedCheckpoint: { epoch: genesisEpoch, root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot, + payloadStatus: PayloadStatus.FULL, }, justifiedBalancesGetter: () => new Uint16Array(Array(32).fill(150)), equivocatingIndices: new Set(), diff --git a/packages/fork-choice/test/unit/forkChoice/shouldOverrideForkChoiceUpdate.test.ts b/packages/fork-choice/test/unit/forkChoice/shouldOverrideForkChoiceUpdate.test.ts index eafb549c3636..7e0899fe9c46 100644 --- a/packages/fork-choice/test/unit/forkChoice/shouldOverrideForkChoiceUpdate.test.ts +++ b/packages/fork-choice/test/unit/forkChoice/shouldOverrideForkChoiceUpdate.test.ts @@ -118,23 +118,35 @@ describe("Forkchoice / shouldOverrideForkChoiceUpdate", () => { const fcStore: IForkChoiceStore = { currentSlot: genesisSlot + 1, justified: { - checkpoint: {epoch: genesisEpoch, root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot}, + checkpoint: { + epoch: genesisEpoch, + root: fromHexString(genesisBlock.blockRoot), + rootHex: genesisBlock.blockRoot, + payloadStatus: PayloadStatus.FULL, + }, balances: new Uint16Array(Array(32).fill(150)), totalBalance: 32 * 150, }, unrealizedJustified: { - checkpoint: {epoch: genesisEpoch, root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot}, + checkpoint: { + epoch: genesisEpoch, + root: fromHexString(genesisBlock.blockRoot), + rootHex: genesisBlock.blockRoot, + payloadStatus: PayloadStatus.FULL, + }, balances: new Uint16Array(Array(32).fill(150)), }, finalizedCheckpoint: { epoch: genesisEpoch, root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot, + payloadStatus: PayloadStatus.FULL, }, unrealizedFinalizedCheckpoint: { epoch: genesisEpoch, root: fromHexString(genesisBlock.blockRoot), rootHex: genesisBlock.blockRoot, + payloadStatus: PayloadStatus.FULL, }, justifiedBalancesGetter: () => new Uint16Array(Array(32).fill(150)), equivocatingIndices: new Set(),