diff --git a/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts b/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts index 1d1f37b8b8ea..60b6216c34e6 100644 --- a/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts +++ b/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts @@ -4,7 +4,7 @@ import {expect} from "vitest"; import {toHexString} from "@chainsafe/ssz"; import {createBeaconConfig} from "@lodestar/config"; import {getConfig} from "@lodestar/config/test-utils"; -import {CheckpointWithHex, ExecutionStatus, ForkChoice} from "@lodestar/fork-choice"; +import {CheckpointWithHex, ExecutionStatus, ForkChoice, getSafeExecutionBlockHash} from "@lodestar/fork-choice"; import {testLogger} from "@lodestar/logger/test-utils"; import { ACTIVE_PRESET, @@ -71,7 +71,7 @@ import {ClockStopped} from "../../mocks/clock.js"; import {getMockedBeaconDb} from "../../mocks/mockedBeaconDb.js"; import {assertCorrectProgressiveBalances} from "../config.js"; import {ethereumConsensusSpecsTests} from "../specTestVersioning.js"; -import {specTestIterator} from "../utils/specTestIterator.js"; +import {defaultSkipOpts, specTestIterator} from "../utils/specTestIterator.js"; import {RunnerType, TestRunnerFn} from "../utils/types.js"; const ANCHOR_STATE_FILE_NAME = "anchor_state"; @@ -556,21 +556,42 @@ const fastConfirmationTest = ); } - // TODO: Expose this value to to spec tests - // if (step.checks.previous_epoch_observed_justified_checkpoint) { - // } + const fcrStore = chain.forkChoice.getFastConfirmationStore(); - // TODO: Expose this value to to spec tests - // if (step.checks.current_epoch_observed_justified_checkpoint) { - // } + if (step.checks.previous_epoch_observed_justified_checkpoint) { + expect(toSpecTestCheckpoint(fcrStore.previousEpochObservedJustifiedCheckpoint)).toEqualWithMessage( + step.checks.previous_epoch_observed_justified_checkpoint, + `Invalid previous epoch observed justified checkpoint at step ${i}` + ); + } - // TODO: Expose this value to to spec tests - // if (step.checks.previous_slot_head) { - // } + if (step.checks.current_epoch_observed_justified_checkpoint) { + expect(toSpecTestCheckpoint(fcrStore.currentEpochObservedJustifiedCheckpoint)).toEqualWithMessage( + step.checks.current_epoch_observed_justified_checkpoint, + `Invalid current epoch observed justified checkpoint at step ${i}` + ); + } - // TODO: Expose this value to to spec tests - // if (step.checks.current_slot_head) { - // } + if (step.checks.previous_epoch_greatest_unrealized_checkpoint) { + expect(toSpecTestCheckpoint(fcrStore.previousEpochGreatestUnrealizedCheckpoint)).toEqualWithMessage( + step.checks.previous_epoch_greatest_unrealized_checkpoint, + `Invalid previous epoch greatest unrealized checkpoint at step ${i}` + ); + } + + if (step.checks.previous_slot_head) { + expect(fcrStore.previousSlotHead).toEqualWithMessage( + step.checks.previous_slot_head, + `Invalid previous slot head at step ${i}` + ); + } + + if (step.checks.current_slot_head) { + expect(fcrStore.currentSlotHead).toEqualWithMessage( + step.checks.current_slot_head, + `Invalid current slot head at step ${i}` + ); + } if (step.checks.confirmed_root) { expect(confirmedRoot).toEqualWithMessage( @@ -579,6 +600,13 @@ const fastConfirmationTest = ); } + if (step.checks.safe_execution_block_hash !== undefined) { + expect(getSafeExecutionBlockHash(chain.forkChoice)).toEqualWithMessage( + step.checks.safe_execution_block_hash, + `Invalid safe execution block hash at step ${i}` + ); + } + if (step.checks.should_override_forkchoice_update) { const currentSlot = Math.floor(tickTime / (config.SLOT_DURATION_MS / 1000)); const result = chain.forkChoice.shouldOverrideForkChoiceUpdate( @@ -687,17 +715,20 @@ const fastConfirmationTest = name.includes("voting_source_beyond_two_epoch") || name.includes("justified_update_always_if_better") || name.includes("justified_update_not_realized_finality") || - // TODO: lodestar's fast-confirmation rule (FCR) needs a broader overhaul. These two - // is_one_confirmed cases (new in v1.7.0-alpha.12, present in electra + fulu) currently - // fail. Unskip once the FCR is reworked. - name.includes("is_one_confirmed_fails_large_validator_slashed") || + // These vectors carry stub deposit signatures (bls_setting=2) and expect the deposit to + // be applied. Passing them requires skipping deposit signature verification inside epoch + // processing, which Lodestar does not support. Unskip if upstream signs deposits for + // real, or if full bls_setting=2 support is ever added. name.includes("is_one_confirmed_fails_recently_activated_validator_voting_in_empty_slot") || - // This case (new in v1.7.0-alpha.13, consensus-specs #5449) deposits a validator, but the - // vectors are generated with bls_setting=2 so the deposit carries a stub signature that - // only the pyspec BLS stub accepts. Lodestar verifies the deposit proof of possession - // inside processPendingDeposits, so the validator is never onboarded and the state root - // diverges once the pending deposit is applied. - name.includes("is_one_confirmed_passes_with_new_validator_activated_in_head_state"), + name.includes("is_one_confirmed_passes_with_new_validator_activated_in_head_state") || + // These vectors run `on_fast_confirmation` twice in one slot (stale GU test) or skip an + // epoch-boundary run (consecutive slots test), so a client running the handler once per + // slot cannot reproduce the expected FCR-store variables. Fixed upstream, unskip when + // the next spec-tests release (> v1.7.0-alpha.13) is picked up: + // - https://github.com/ethereum/consensus-specs/pull/5499 + // - https://github.com/ethereum/consensus-specs/pull/5498 + name.includes("fcr_no_restart_if_head_gu_is_stale") || + name.includes("is_one_confirmed_passes_with_empty_slot_and_attester_in_two_consecutive_slots_2"), }, }; }; @@ -785,9 +816,12 @@ type Checks = { previous_epoch_observed_justified_checkpoint?: SpecTestCheckpoint; current_epoch_observed_justified_checkpoint?: SpecTestCheckpoint; + previous_epoch_greatest_unrealized_checkpoint?: SpecTestCheckpoint; previous_slot_head?: string; current_slot_head?: string; confirmed_root?: string; + /** Expected response of `get_safe_execution_block_hash()`. New in Bellatrix. */ + safe_execution_block_hash?: string; // Custom attributes get_proposer_head?: string; @@ -842,6 +876,10 @@ function isCheck(step: Step): step is Checks { return typeof (step as Checks).checks === "object"; } -specTestIterator(path.join(ethereumConsensusSpecsTests.outputDir, "tests", ACTIVE_PRESET), { - fast_confirmation: {type: RunnerType.default, fn: fastConfirmationTest({onlyPredefinedResponses: false})}, -}); +specTestIterator( + path.join(ethereumConsensusSpecsTests.outputDir, "tests", ACTIVE_PRESET), + { + fast_confirmation: {type: RunnerType.default, fn: fastConfirmationTest({onlyPredefinedResponses: false})}, + }, + defaultSkipOpts +); diff --git a/packages/fork-choice/src/forkChoice/fastConfirmation/types.ts b/packages/fork-choice/src/forkChoice/fastConfirmation/types.ts index 61a4934080da..30573e2fdd53 100644 --- a/packages/fork-choice/src/forkChoice/fastConfirmation/types.ts +++ b/packages/fork-choice/src/forkChoice/fastConfirmation/types.ts @@ -15,7 +15,7 @@ export type ForkChoiceStateGetter = ( opts: {stateRoot: RootHex; checkpoint?: never} | {stateRoot?: never; checkpoint: CheckpointWithHex} ) => IBeaconStateView | null; -type IFastConfirmationSpecStore = { +export type IFastConfirmationSpecStore = { confirmedRoot: RootHex; previousEpochObservedJustifiedCheckpoint: CheckpointWithHex; currentEpochObservedJustifiedCheckpoint: CheckpointWithHex; diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 3732f848111d..5b0e261778e4 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -49,6 +49,7 @@ import { FastConfirmationRule, FastConfirmationSteps, type IFastConfirmationRule, + type IFastConfirmationSpecStore, } from "./fastConfirmation/fastConfirmationRule.ts"; import { AncestorResult, @@ -228,6 +229,17 @@ export class ForkChoice implements IForkChoice { return this.fastConfirmationRule?.getConfirmedRoot() ?? this.fcStore.justified.checkpoint.rootHex; } + getFastConfirmationStore(): IFastConfirmationSpecStore { + return { + confirmedRoot: this.getConfirmedRoot(), + previousEpochObservedJustifiedCheckpoint: this.fcStore.previousEpochObservedJustifiedCheckpoint, + currentEpochObservedJustifiedCheckpoint: this.fcStore.currentEpochObservedJustifiedCheckpoint, + previousEpochGreatestUnrealizedCheckpoint: this.fcStore.previousEpochGreatestUnrealizedCheckpoint, + previousSlotHead: this.fcStore.previousSlotHead, + currentSlotHead: this.fcStore.currentSlotHead, + }; + } + getConfirmedBlock(): ProtoBlock | null { return this.getBlockHexDefaultStatus(this.getConfirmedRoot()); } diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index 4a1b26a37d5f..db40b4a422bd 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -8,6 +8,7 @@ import { ProtoBlock, ProtoNode, } from "../protoArray/interface.js"; +import {IFastConfirmationSpecStore} from "./fastConfirmation/types.js"; import {UpdateAndGetHeadOpt} from "./forkChoice.js"; import {CheckpointWithHex} from "./store.js"; @@ -100,6 +101,8 @@ export interface IForkChoice { getHead(): ProtoBlock; getConfirmedRoot(): RootHex; getConfirmedBlock(): ProtoBlock | null; + /** Snapshot of the spec `FastConfirmationStore` fields, mirroring `get_fast_confirmation_store` */ + getFastConfirmationStore(): IFastConfirmationSpecStore; /** Resume the fast confirmation rule; restarts from the finalized root on the next slot tick */ resumeFastConfirmation(): void; /** Pause the fast confirmation rule (e.g. while syncing); pins the confirmed root to the finalized root */