From 30b7b943962eb1293d61ef4bc6e866885b30fde7 Mon Sep 17 00:00:00 2001 From: twoeths Date: Thu, 18 Jun 2026 10:39:59 +0700 Subject: [PATCH 01/24] feat: init BeaconEngine --- .../src/chain/beaconEngine/beaconEngine.ts | 17 +++++++++++++++++ .../src/chain/beaconEngine/index.ts | 2 ++ .../src/chain/beaconEngine/interface.ts | 18 ++++++++++++++++++ packages/beacon-node/src/chain/chain.ts | 3 +++ packages/beacon-node/src/chain/interface.ts | 2 ++ 5 files changed, 42 insertions(+) create mode 100644 packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts create mode 100644 packages/beacon-node/src/chain/beaconEngine/index.ts create mode 100644 packages/beacon-node/src/chain/beaconEngine/interface.ts diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts new file mode 100644 index 000000000000..2c68393bed33 --- /dev/null +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -0,0 +1,17 @@ +import {BeaconConfig} from "@lodestar/config"; +import {IBeaconStateView} from "@lodestar/state-transition"; +import {BeaconEngineModules, IBeaconEngine} from "./interface.js"; + +/** + * JS implementation of the consensus engine. Transitional in Phase 0: constructed inside + * `BeaconChain` from the `anchorState` object; construction moves to the CLI in Phase 6. + * + * Minimal by design — collaborators, state ownership and flows migrate here in later phases. + */ +export class BeaconEngine implements IBeaconEngine { + readonly config: BeaconConfig; + + constructor(modules: BeaconEngineModules, _anchorState: IBeaconStateView) { + this.config = modules.config; + } +} diff --git a/packages/beacon-node/src/chain/beaconEngine/index.ts b/packages/beacon-node/src/chain/beaconEngine/index.ts new file mode 100644 index 000000000000..333481639797 --- /dev/null +++ b/packages/beacon-node/src/chain/beaconEngine/index.ts @@ -0,0 +1,2 @@ +export * from "./beaconEngine.js"; +export * from "./interface.js"; diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts new file mode 100644 index 000000000000..c771d20bd43a --- /dev/null +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -0,0 +1,18 @@ +import {BeaconConfig} from "@lodestar/config"; +import {Logger} from "@lodestar/utils"; +import {Metrics} from "../../metrics/index.js"; + +export type BeaconEngineModules = { + config: BeaconConfig; + logger: Logger; + metrics: Metrics | null; +}; + +/** + * The consensus engine seam. Starts minimal and transitional (JS-only); ownership of consensus + * collaborators and flows migrates here across later phases. This interface is the contract shared + * with the native engine (lodestar-z) — both the JS and native engines implement the same signatures. + */ +export interface IBeaconEngine { + readonly config: BeaconConfig; +} diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 28286f926f1e..3a318edd395e 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -67,6 +67,7 @@ import {SerializedCache} from "../util/serializedCache.js"; import {getSlotFromSignedBeaconBlockSerialized} from "../util/sszBytes.js"; import {ArchiveStore} from "./archiveStore/archiveStore.js"; import {CheckpointBalancesCache} from "./balancesCache.js"; +import {BeaconEngine} from "./beaconEngine/index.js"; import {BeaconProposerCache} from "./beaconProposerCache.js"; import {IBlockInput, isBlockInputBlobs, isBlockInputColumns} from "./blocks/blockInput/index.js"; import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js"; @@ -162,6 +163,7 @@ export class BeaconChain implements IBeaconChain { readonly anchorStateLatestBlockSlot: Slot; + readonly beaconEngine: BeaconEngine; readonly bls: IBlsVerifier; readonly forkChoice: IForkChoice; readonly clock: IClock; @@ -291,6 +293,7 @@ export class BeaconChain implements IBeaconChain { this.genesisValidatorsRoot = anchorState.genesisValidatorsRoot; this.executionEngine = executionEngine; this.executionBuilder = executionBuilder; + this.beaconEngine = new BeaconEngine({config, logger, metrics}, anchorState); const signal = this.abortController.signal; const emitter = new ChainEventEmitter(); // by default, verify signatures on both main threads and worker threads diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 077b2e87b4ba..8015981575b4 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -31,6 +31,7 @@ import {CustodyConfig} from "../util/dataColumns.js"; import {SerializedCache} from "../util/serializedCache.js"; import {IArchiveStore} from "./archiveStore/interface.js"; import {CheckpointBalancesCache} from "./balancesCache.js"; +import {IBeaconEngine} from "./beaconEngine/index.js"; import {BeaconProposerCache, ProposerPreparationData} from "./beaconProposerCache.js"; import {IBlockInput} from "./blocks/blockInput/index.js"; import {ImportBlockOpts, ImportPayloadOpts} from "./blocks/types.js"; @@ -107,6 +108,7 @@ export interface IBeaconChain { /** The initial slot that the chain is started with */ readonly anchorStateLatestBlockSlot: Slot; + readonly beaconEngine: IBeaconEngine; readonly bls: IBlsVerifier; readonly forkChoice: IForkChoice; readonly clock: IClock; From a07d28eb0b239226a461c5a8c5c382959c2c08bd Mon Sep 17 00:00:00 2001 From: twoeths Date: Thu, 18 Jun 2026 11:22:04 +0700 Subject: [PATCH 02/24] refactor: move Seen Caches + Shuffling Cache to engine --- .../src/chain/beaconEngine/beaconEngine.ts | 71 ++++++++++++++- .../src/chain/beaconEngine/interface.ts | 6 ++ .../src/chain/beaconEngine/options.ts | 27 ++++++ packages/beacon-node/src/chain/bls/index.ts | 6 +- packages/beacon-node/src/chain/chain.ts | 89 ++++++++++--------- packages/beacon-node/src/chain/options.ts | 24 +---- 6 files changed, 155 insertions(+), 68 deletions(-) create mode 100644 packages/beacon-node/src/chain/beaconEngine/options.ts diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 2c68393bed33..252581a7a57e 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -1,5 +1,23 @@ import {BeaconConfig} from "@lodestar/config"; import {IBeaconStateView} from "@lodestar/state-transition"; +import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "../bls/index.js"; +import { + AggregatedAttestationPool, + AttestationPool, + OpPool, + PayloadAttestationPool, + SyncCommitteeMessagePool, + SyncContributionAndProofPool, +} from "../opPools/index.js"; +import { + SeenAggregators, + SeenAttesters, + SeenContributionAndProof, + SeenPayloadAttesters, + SeenSyncCommitteeMessages, +} from "../seenCache/index.js"; +import {SeenAttestationDatas} from "../seenCache/seenAttestationData.js"; +import {ShufflingCache} from "../shufflingCache.js"; import {BeaconEngineModules, IBeaconEngine} from "./interface.js"; /** @@ -10,8 +28,57 @@ import {BeaconEngineModules, IBeaconEngine} from "./interface.js"; */ export class BeaconEngine implements IBeaconEngine { readonly config: BeaconConfig; + readonly bls: IBlsVerifier; + readonly shufflingCache: ShufflingCache; - constructor(modules: BeaconEngineModules, _anchorState: IBeaconStateView) { - this.config = modules.config; + // Op pools + readonly attestationPool: AttestationPool; + readonly aggregatedAttestationPool: AggregatedAttestationPool; + readonly syncCommitteeMessagePool: SyncCommitteeMessagePool; + readonly syncContributionAndProofPool: SyncContributionAndProofPool; + readonly payloadAttestationPool: PayloadAttestationPool; + readonly opPool: OpPool; + + // Consensus gossip seen-caches + readonly seenAttesters = new SeenAttesters(); + readonly seenAggregators = new SeenAggregators(); + readonly seenPayloadAttesters = new SeenPayloadAttesters(); + readonly seenSyncCommitteeMessages = new SeenSyncCommitteeMessages(); + readonly seenContributionAndProof: SeenContributionAndProof; + readonly seenAttestationDatas: SeenAttestationDatas; + + constructor(modules: BeaconEngineModules, anchorState: IBeaconStateView) { + const {opts, config, logger, metrics, clock, pubkeyCache} = modules; + this.config = config; + + // by default, verify signatures on both main threads and worker threads + this.bls = opts.blsVerifyAllMainThread + ? new BlsSingleThreadVerifier({metrics, pubkeyCache}) + : new BlsMultiThreadWorkerPool(opts, {logger, metrics, pubkeyCache}); + + this.shufflingCache = new ShufflingCache(metrics, logger, opts, [ + { + shuffling: anchorState.getPreviousShuffling(), + decisionRoot: anchorState.previousDecisionRoot, + }, + { + shuffling: anchorState.getCurrentShuffling(), + decisionRoot: anchorState.currentDecisionRoot, + }, + { + shuffling: anchorState.getNextShuffling(), + decisionRoot: anchorState.nextDecisionRoot, + }, + ]); + + this.attestationPool = new AttestationPool(config, clock, opts?.preaggregateSlotDistance, metrics); + this.aggregatedAttestationPool = new AggregatedAttestationPool(config, metrics); + this.syncCommitteeMessagePool = new SyncCommitteeMessagePool(config, clock, opts?.preaggregateSlotDistance); + this.syncContributionAndProofPool = new SyncContributionAndProofPool(config, clock, metrics, logger); + this.payloadAttestationPool = new PayloadAttestationPool(config, clock, metrics); + this.opPool = new OpPool(config); + + this.seenContributionAndProof = new SeenContributionAndProof(metrics); + this.seenAttestationDatas = new SeenAttestationDatas(metrics, opts?.attDataCacheSlotDistance); } } diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index c771d20bd43a..3b700d41e81b 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -1,11 +1,17 @@ import {BeaconConfig} from "@lodestar/config"; +import {PubkeyCache} from "@lodestar/state-transition"; import {Logger} from "@lodestar/utils"; import {Metrics} from "../../metrics/index.js"; +import {IClock} from "../../util/clock.js"; +import {IBeaconEngineOptions} from "./options.js"; export type BeaconEngineModules = { + opts: IBeaconEngineOptions; config: BeaconConfig; logger: Logger; metrics: Metrics | null; + clock: IClock; + pubkeyCache: PubkeyCache; }; /** diff --git a/packages/beacon-node/src/chain/beaconEngine/options.ts b/packages/beacon-node/src/chain/beaconEngine/options.ts new file mode 100644 index 000000000000..31081c725c52 --- /dev/null +++ b/packages/beacon-node/src/chain/beaconEngine/options.ts @@ -0,0 +1,27 @@ +import {BlsMultiThreadWorkerPoolOptions} from "../bls/index.js"; +import {ShufflingCacheOpts} from "../shufflingCache.js"; + +export type PoolOpts = { + /** + * Only preaggregate attestation/sync committee message since clockSlot - preaggregateSlotDistance + */ + preaggregateSlotDistance?: number; +}; + +export type SeenCacheOpts = { + /** + * Slot distance from current slot to cache AttestationData + */ + attDataCacheSlotDistance?: number; +}; + +/** + * Options consumed by the consensus engine and its collaborators. `IChainOptions` extends this; + * the BeaconEngine module references only `IBeaconEngineOptions` so the seam stays self-contained. + */ +export type IBeaconEngineOptions = ShufflingCacheOpts & + PoolOpts & + SeenCacheOpts & + BlsMultiThreadWorkerPoolOptions & { + blsVerifyAllMainThread?: boolean; + }; diff --git a/packages/beacon-node/src/chain/bls/index.ts b/packages/beacon-node/src/chain/bls/index.ts index f9898b13776b..45e5d0e2e60a 100644 --- a/packages/beacon-node/src/chain/bls/index.ts +++ b/packages/beacon-node/src/chain/bls/index.ts @@ -1,4 +1,8 @@ export type {IBlsVerifier} from "./interface.js"; -export type {BlsMultiThreadWorkerPoolModules, JobQueueItemType} from "./multithread/index.js"; +export type { + BlsMultiThreadWorkerPoolModules, + BlsMultiThreadWorkerPoolOptions, + JobQueueItemType, +} from "./multithread/index.js"; export {BlsMultiThreadWorkerPool} from "./multithread/index.js"; export {BlsSingleThreadVerifier} from "./singleThread.js"; diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 3a318edd395e..f6c7d8fd4ddb 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -75,7 +75,7 @@ import {PayloadEnvelopeProcessor} from "./blocks/payloadEnvelopeProcessor.js"; import {ImportPayloadOpts} from "./blocks/types.js"; import {persistBlockInput} from "./blocks/writeBlockInputToDb.js"; import {persistPayloadEnvelopeInput} from "./blocks/writePayloadEnvelopeInputToDb.js"; -import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "./bls/index.js"; +import {IBlsVerifier} from "./bls/index.js"; import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js"; import {ChainEvent, ChainEventEmitter} from "./emitter.js"; import {ForkchoiceCaller, initializeForkChoice} from "./forkChoice/index.js"; @@ -164,7 +164,9 @@ export class BeaconChain implements IBeaconChain { readonly anchorStateLatestBlockSlot: Slot; readonly beaconEngine: BeaconEngine; - readonly bls: IBlsVerifier; + get bls(): IBlsVerifier { + return this.beaconEngine.bls; + } readonly forkChoice: IForkChoice; readonly clock: IClock; readonly emitter: ChainEventEmitter; @@ -176,26 +178,50 @@ export class BeaconChain implements IBeaconChain { readonly unfinalizedPayloadEnvelopeWrites: JobItemQueue<[PayloadEnvelopeInput], void>; // Ops pool - readonly attestationPool: AttestationPool; - readonly aggregatedAttestationPool: AggregatedAttestationPool; - readonly syncCommitteeMessagePool: SyncCommitteeMessagePool; - readonly syncContributionAndProofPool; + get attestationPool(): AttestationPool { + return this.beaconEngine.attestationPool; + } + get aggregatedAttestationPool(): AggregatedAttestationPool { + return this.beaconEngine.aggregatedAttestationPool; + } + get syncCommitteeMessagePool(): SyncCommitteeMessagePool { + return this.beaconEngine.syncCommitteeMessagePool; + } + get syncContributionAndProofPool(): SyncContributionAndProofPool { + return this.beaconEngine.syncContributionAndProofPool; + } readonly executionPayloadBidPool: ExecutionPayloadBidPool; - readonly payloadAttestationPool: PayloadAttestationPool; + get payloadAttestationPool(): PayloadAttestationPool { + return this.beaconEngine.payloadAttestationPool; + } readonly proposerPreferencesPool = new ProposerPreferencesPool(); - readonly opPool: OpPool; + get opPool(): OpPool { + return this.beaconEngine.opPool; + } // Gossip seen cache - readonly seenAttesters = new SeenAttesters(); - readonly seenAggregators = new SeenAggregators(); - readonly seenPayloadAttesters = new SeenPayloadAttesters(); + get seenAttesters(): SeenAttesters { + return this.beaconEngine.seenAttesters; + } + get seenAggregators(): SeenAggregators { + return this.beaconEngine.seenAggregators; + } + get seenPayloadAttesters(): SeenPayloadAttesters { + return this.beaconEngine.seenPayloadAttesters; + } readonly seenAggregatedAttestations: SeenAggregatedAttestations; readonly seenExecutionPayloadBids = new SeenExecutionPayloadBids(); readonly seenProposerPreferences = new SeenProposerPreferences(); readonly seenBlockProposers = new SeenBlockProposers(); - readonly seenSyncCommitteeMessages = new SeenSyncCommitteeMessages(); - readonly seenContributionAndProof: SeenContributionAndProof; - readonly seenAttestationDatas: SeenAttestationDatas; + get seenSyncCommitteeMessages(): SeenSyncCommitteeMessages { + return this.beaconEngine.seenSyncCommitteeMessages; + } + get seenContributionAndProof(): SeenContributionAndProof { + return this.beaconEngine.seenContributionAndProof; + } + get seenAttestationDatas(): SeenAttestationDatas { + return this.beaconEngine.seenAttestationDatas; + } readonly seenBlockInputCache: SeenBlockInput; readonly seenPayloadEnvelopeInputCache: SeenPayloadEnvelopeInput; // Seen cache for liveness checks @@ -206,7 +232,9 @@ export class BeaconChain implements IBeaconChain { readonly beaconProposerCache: BeaconProposerCache; readonly checkpointBalancesCache: CheckpointBalancesCache; - readonly shufflingCache: ShufflingCache; + get shufflingCache(): ShufflingCache { + return this.beaconEngine.shufflingCache; + } /** * Cache produced results (ExecutionPayload, DA Data) from the local execution so that we can send @@ -293,28 +321,17 @@ export class BeaconChain implements IBeaconChain { this.genesisValidatorsRoot = anchorState.genesisValidatorsRoot; this.executionEngine = executionEngine; this.executionBuilder = executionBuilder; - this.beaconEngine = new BeaconEngine({config, logger, metrics}, anchorState); const signal = this.abortController.signal; const emitter = new ChainEventEmitter(); - // by default, verify signatures on both main threads and worker threads - const bls = opts.blsVerifyAllMainThread - ? new BlsSingleThreadVerifier({metrics, pubkeyCache}) - : new BlsMultiThreadWorkerPool(opts, {logger, metrics, pubkeyCache}); if (!clock) clock = new Clock({config, genesisTime: this.genesisTime, signal}); + this.beaconEngine = new BeaconEngine({opts, config, logger, metrics, clock, pubkeyCache}, anchorState); + this.blacklistedBlocks = new Map((opts.blacklistedBlocks ?? []).map((hex) => [hex, null])); - this.attestationPool = new AttestationPool(config, clock, this.opts?.preaggregateSlotDistance, metrics); - this.aggregatedAttestationPool = new AggregatedAttestationPool(this.config, metrics); - this.syncCommitteeMessagePool = new SyncCommitteeMessagePool(config, clock, this.opts?.preaggregateSlotDistance); - this.syncContributionAndProofPool = new SyncContributionAndProofPool(config, clock, metrics, logger); this.executionPayloadBidPool = new ExecutionPayloadBidPool(); - this.payloadAttestationPool = new PayloadAttestationPool(config, clock, metrics); - this.opPool = new OpPool(config); this.seenAggregatedAttestations = new SeenAggregatedAttestations(metrics); - this.seenContributionAndProof = new SeenContributionAndProof(metrics); - this.seenAttestationDatas = new SeenAttestationDatas(metrics, this.opts?.attDataCacheSlotDistance); const nodeId = computeNodeIdFromPrivateKey(privateKey); const initialCustodyGroupCount = opts.initialCustodyGroupCount ?? config.CUSTODY_REQUIREMENT; @@ -343,21 +360,6 @@ export class BeaconChain implements IBeaconChain { this._earliestAvailableSlot = anchorState.slot; - this.shufflingCache = new ShufflingCache(metrics, logger, this.opts, [ - { - shuffling: anchorState.getPreviousShuffling(), - decisionRoot: anchorState.previousDecisionRoot, - }, - { - shuffling: anchorState.getCurrentShuffling(), - decisionRoot: anchorState.currentDecisionRoot, - }, - { - shuffling: anchorState.getNextShuffling(), - decisionRoot: anchorState.nextDecisionRoot, - }, - ]); - // Global cache of validators pubkey/index mapping this.pubkeyCache = pubkeyCache; @@ -458,7 +460,6 @@ export class BeaconChain implements IBeaconChain { this.clock = clock; this.regen = regen; - this.bls = bls; this.emitter = emitter; this.getBlobsTracker = new GetBlobsTracker({ diff --git a/packages/beacon-node/src/chain/options.ts b/packages/beacon-node/src/chain/options.ts index b6e7058228f0..2148ad21e7be 100644 --- a/packages/beacon-node/src/chain/options.ts +++ b/packages/beacon-node/src/chain/options.ts @@ -1,9 +1,9 @@ import {defaultOptions as defaultValidatorOptions} from "@lodestar/validator"; import {DEFAULT_ARCHIVE_MODE} from "./archiveStore/constants.js"; import {ArchiveMode, ArchiveStoreOpts} from "./archiveStore/interface.js"; +import {IBeaconEngineOptions} from "./beaconEngine/options.js"; import {ForkChoiceOpts} from "./forkChoice/index.js"; import {LightClientServerOpts} from "./lightClient/index.js"; -import {ShufflingCacheOpts} from "./shufflingCache.js"; import {DEFAULT_MAX_BLOCK_STATES, FIFOBlockStateCacheOpts} from "./stateCache/fifoBlockStateCache.js"; import { DEFAULT_MAX_CP_STATE_EPOCHS_IN_MEMORY, @@ -14,18 +14,14 @@ import {ValidatorMonitorOpts} from "./validatorMonitor.js"; export {ArchiveMode, DEFAULT_ARCHIVE_MODE}; -export type IChainOptions = BlockProcessOpts & - PoolOpts & - SeenCacheOpts & +export type IChainOptions = IBeaconEngineOptions & + BlockProcessOpts & ForkChoiceOpts & ArchiveStoreOpts & FIFOBlockStateCacheOpts & PersistentCheckpointStateCacheOpts & - ShufflingCacheOpts & ValidatorMonitorOpts & LightClientServerOpts & { - blsVerifyAllMainThread?: boolean; - blsVerifyAllMultiThread?: boolean; blacklistedBlocks?: string[]; // TODO GLOAS: add similar option for execution payload envelopes? persistProducedBlocks?: boolean; @@ -83,20 +79,6 @@ export type BlockProcessOpts = { skipVerifyBlockSignatures?: boolean; }; -export type PoolOpts = { - /** - * Only preaggregate attestation/sync committee message since clockSlot - preaggregateSlotDistance - */ - preaggregateSlotDistance?: number; -}; - -export type SeenCacheOpts = { - /** - * Slot distance from current slot to cache AttestationData - */ - attDataCacheSlotDistance?: number; -}; - export const defaultChainOptions: IChainOptions = { blsVerifyAllMainThread: false, blsVerifyAllMultiThread: false, From c24604a445aaedacff6972f7fd5604ae1346cd72 Mon Sep 17 00:00:00 2001 From: twoeths Date: Thu, 18 Jun 2026 13:59:42 +0700 Subject: [PATCH 03/24] feat: move regen into BeaconEngine --- .../src/chain/beaconEngine/beaconEngine.ts | 175 ++++++++++++++- .../src/chain/beaconEngine/interface.ts | 15 ++ .../src/chain/beaconEngine/options.ts | 6 + packages/beacon-node/src/chain/chain.ts | 203 ++++-------------- packages/beacon-node/src/chain/options.ts | 7 +- packages/beacon-node/src/chain/regen/regen.ts | 2 + 6 files changed, 241 insertions(+), 167 deletions(-) diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 252581a7a57e..876a5c5c2779 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -1,6 +1,11 @@ import {BeaconConfig} from "@lodestar/config"; -import {IBeaconStateView} from "@lodestar/state-transition"; +import {CheckpointWithHex, ForkChoiceStateGetter, IForkChoice} from "@lodestar/fork-choice"; +import {EffectiveBalanceIncrements, IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition"; +import {Logger, toRootHex} from "@lodestar/utils"; +import {Metrics} from "../../metrics/index.js"; +import {CheckpointBalancesCache} from "../balancesCache.js"; import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "../bls/index.js"; +import {initializeForkChoice} from "../forkChoice/index.js"; import { AggregatedAttestationPool, AttestationPool, @@ -9,6 +14,7 @@ import { SyncCommitteeMessagePool, SyncContributionAndProofPool, } from "../opPools/index.js"; +import {QueuedStateRegenerator} from "../regen/index.js"; import { SeenAggregators, SeenAttesters, @@ -18,6 +24,9 @@ import { } from "../seenCache/index.js"; import {SeenAttestationDatas} from "../seenCache/seenAttestationData.js"; import {ShufflingCache} from "../shufflingCache.js"; +import {FIFOBlockStateCache} from "../stateCache/fifoBlockStateCache.js"; +import {PersistentCheckpointStateCache} from "../stateCache/persistentCheckpointsCache.js"; +import {BlockStateCache, CheckpointStateCache} from "../stateCache/types.js"; import {BeaconEngineModules, IBeaconEngine} from "./interface.js"; /** @@ -28,9 +37,17 @@ import {BeaconEngineModules, IBeaconEngine} from "./interface.js"; */ export class BeaconEngine implements IBeaconEngine { readonly config: BeaconConfig; + private readonly logger: Logger; + private readonly metrics: Metrics | null; readonly bls: IBlsVerifier; readonly shufflingCache: ShufflingCache; + readonly blockStateCache: BlockStateCache; + readonly checkpointStateCache: CheckpointStateCache; + readonly checkpointBalancesCache: CheckpointBalancesCache; + readonly forkChoice: IForkChoice; + readonly regen: QueuedStateRegenerator; + // Op pools readonly attestationPool: AttestationPool; readonly aggregatedAttestationPool: AggregatedAttestationPool; @@ -48,8 +65,25 @@ export class BeaconEngine implements IBeaconEngine { readonly seenAttestationDatas: SeenAttestationDatas; constructor(modules: BeaconEngineModules, anchorState: IBeaconStateView) { - const {opts, config, logger, metrics, clock, pubkeyCache} = modules; + const { + opts, + config, + logger, + metrics, + clock, + pubkeyCache, + bufferPool, + cpStateDatastore, + emitter, + signal, + db, + validatorMonitor, + seenBlockInputCache, + isAnchorStateFinalized, + } = modules; this.config = config; + this.logger = logger; + this.metrics = metrics; // by default, verify signatures on both main threads and worker threads this.bls = opts.blsVerifyAllMainThread @@ -80,5 +114,142 @@ export class BeaconEngine implements IBeaconEngine { this.seenContributionAndProof = new SeenContributionAndProof(metrics); this.seenAttestationDatas = new SeenAttestationDatas(metrics, opts?.attDataCacheSlotDistance); + + this.blockStateCache = new FIFOBlockStateCache(opts, {metrics}); + this.checkpointStateCache = new PersistentCheckpointStateCache( + {config, metrics, logger, clock, blockStateCache: this.blockStateCache, bufferPool, datastore: cpStateDatastore}, + opts + ); + + const {checkpoint} = anchorState.computeAnchorCheckpoint(); + this.blockStateCache.add(anchorState); + this.blockStateCache.setHeadState(anchorState); + this.checkpointStateCache.add(checkpoint, anchorState); + + this.checkpointBalancesCache = new CheckpointBalancesCache(); + + const forkChoiceStateGetter: ForkChoiceStateGetter = ({stateRoot, checkpoint}) => { + if (stateRoot) return this.blockStateCache.get(stateRoot); + + if (checkpoint) return this.checkpointStateCache.get({epoch: checkpoint.epoch, rootHex: checkpoint.rootHex}); + + return null; + }; + + this.forkChoice = initializeForkChoice( + config, + emitter, + clock.currentSlot, + anchorState, + isAnchorStateFinalized, + opts, + this.justifiedBalancesGetter.bind(this), + forkChoiceStateGetter, + metrics, + logger + ); + + this.regen = new QueuedStateRegenerator({ + config, + forkChoice: this.forkChoice, + blockStateCache: this.blockStateCache, + checkpointStateCache: this.checkpointStateCache, + seenBlockInputCache, + db, + metrics, + validatorMonitor, + logger, + emitter, + signal, + }); + } + + /** + * `ForkChoice.onBlock` must never throw for a block that is valid with respect to the network + * `justifiedBalancesGetter()` must never throw and it should always return a state. + * @param blockState state that declares justified checkpoint `checkpoint` + */ + private justifiedBalancesGetter( + checkpoint: CheckpointWithHex, + blockState: IBeaconStateView + ): EffectiveBalanceIncrements { + this.metrics?.balancesCache.requests.inc(); + + const effectiveBalances = this.checkpointBalancesCache.get(checkpoint); + if (effectiveBalances) { + return effectiveBalances; + } + // not expected, need metrics + this.metrics?.balancesCache.misses.inc(); + this.logger.debug("checkpointBalances cache miss", { + epoch: checkpoint.epoch, + root: checkpoint.rootHex, + }); + + const {state, stateId, shouldWarn} = this.closestJustifiedBalancesStateToCheckpoint(checkpoint, blockState); + this.metrics?.balancesCache.closestStateResult.inc({stateId}); + if (shouldWarn) { + this.logger.warn("currentJustifiedCheckpoint state not avail, using closest state", { + checkpointEpoch: checkpoint.epoch, + checkpointRoot: checkpoint.rootHex, + stateId, + stateSlot: state.slot, + stateRoot: toRootHex(state.hashTreeRoot()), + }); + } + + return state.getEffectiveBalanceIncrementsZeroInactive(); + } + + /** + * - Assumptions + invariant this function is based on: + * - Our cache can only persist X states at once to prevent OOM + * - Some old states (including to-be justified checkpoint) may / must be dropped from the cache + * - Thus, there is no guarantee that the state for a justified checkpoint will be available in the cache + * @param blockState state that declares justified checkpoint `checkpoint` + */ + private closestJustifiedBalancesStateToCheckpoint( + checkpoint: CheckpointWithHex, + blockState: IBeaconStateView + ): {state: IBeaconStateView; stateId: string; shouldWarn: boolean} { + const checkpointHex = {epoch: checkpoint.epoch, rootHex: checkpoint.rootHex}; + const state = this.regen.getCheckpointStateSync(checkpointHex); + if (state) { + return {state, stateId: "checkpoint_state", shouldWarn: false}; + } + + // Check if blockState is in the same epoch, not need to iterate the fork-choice then + if (computeEpochAtSlot(blockState.slot) === checkpoint.epoch) { + return {state: blockState, stateId: "block_state_same_epoch", shouldWarn: true}; + } + + // Find a state in the same branch of checkpoint at same epoch. Balances should exactly the same + for (const descendantBlock of this.forkChoice.forwardIterateDescendantsDefaultStatus(checkpoint.rootHex)) { + if (computeEpochAtSlot(descendantBlock.slot) === checkpoint.epoch) { + const descendantBlockState = this.regen.getStateSync(descendantBlock.stateRoot); + if (descendantBlockState) { + return {state: descendantBlockState, stateId: "descendant_state_same_epoch", shouldWarn: true}; + } + } + } + + // Check if blockState is in the next epoch, not need to iterate the fork-choice then + if (computeEpochAtSlot(blockState.slot) === checkpoint.epoch + 1) { + return {state: blockState, stateId: "block_state_next_epoch", shouldWarn: true}; + } + + // Find a state in the same branch of checkpoint at a latter epoch. Balances are not the same, but should be close + // Note: must call .forwardIterateDescendants() again since nodes are not sorted + for (const descendantBlock of this.forkChoice.forwardIterateDescendantsDefaultStatus(checkpoint.rootHex)) { + if (computeEpochAtSlot(descendantBlock.slot) > checkpoint.epoch) { + const descendantBlockState = this.regen.getStateSync(descendantBlock.stateRoot); + if (descendantBlockState) { + return {state: blockState, stateId: "descendant_state_latter_epoch", shouldWarn: true}; + } + } + } + + // If there's no state available in the same branch of checkpoint use blockState regardless of its epoch + return {state: blockState, stateId: "block_state_any_epoch", shouldWarn: true}; } } diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 3b700d41e81b..388be849f7bc 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -1,8 +1,14 @@ import {BeaconConfig} from "@lodestar/config"; import {PubkeyCache} from "@lodestar/state-transition"; import {Logger} from "@lodestar/utils"; +import {IBeaconDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; +import {BufferPool} from "../../util/bufferPool.js"; import {IClock} from "../../util/clock.js"; +import {ChainEventEmitter} from "../emitter.js"; +import {SeenBlockInput} from "../seenCache/seenGossipBlockInput.js"; +import {CPStateDatastore} from "../stateCache/datastore/types.js"; +import {ValidatorMonitor} from "../validatorMonitor.js"; import {IBeaconEngineOptions} from "./options.js"; export type BeaconEngineModules = { @@ -12,6 +18,15 @@ export type BeaconEngineModules = { metrics: Metrics | null; clock: IClock; pubkeyCache: PubkeyCache; + bufferPool: BufferPool; + cpStateDatastore: CPStateDatastore; + // TODO - beacon engine: emitter is facade infra; forkChoice/regen should not depend on it inside the engine. + emitter: ChainEventEmitter; + signal: AbortSignal; + db: IBeaconDb; + validatorMonitor: ValidatorMonitor | null; + seenBlockInputCache: SeenBlockInput; + isAnchorStateFinalized: boolean; }; /** diff --git a/packages/beacon-node/src/chain/beaconEngine/options.ts b/packages/beacon-node/src/chain/beaconEngine/options.ts index 31081c725c52..897ce6f4444e 100644 --- a/packages/beacon-node/src/chain/beaconEngine/options.ts +++ b/packages/beacon-node/src/chain/beaconEngine/options.ts @@ -1,5 +1,8 @@ import {BlsMultiThreadWorkerPoolOptions} from "../bls/index.js"; +import {ForkChoiceOpts} from "../forkChoice/index.js"; import {ShufflingCacheOpts} from "../shufflingCache.js"; +import {FIFOBlockStateCacheOpts} from "../stateCache/fifoBlockStateCache.js"; +import {PersistentCheckpointStateCacheOpts} from "../stateCache/persistentCheckpointsCache.js"; export type PoolOpts = { /** @@ -22,6 +25,9 @@ export type SeenCacheOpts = { export type IBeaconEngineOptions = ShufflingCacheOpts & PoolOpts & SeenCacheOpts & + FIFOBlockStateCacheOpts & + PersistentCheckpointStateCacheOpts & + ForkChoiceOpts & BlsMultiThreadWorkerPoolOptions & { blsVerifyAllMainThread?: boolean; }; diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index f6c7d8fd4ddb..eb59620aba5b 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -2,7 +2,7 @@ import path from "node:path"; import {PrivateKey} from "@libp2p/interface"; import {Type} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; -import {CheckpointWithHex, ForkChoiceStateGetter, IForkChoice, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice"; +import {CheckpointWithHex, IForkChoice, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice"; import {LoggerNode} from "@lodestar/logger/node"; import { EFFECTIVE_BALANCE_INCREMENT, @@ -13,7 +13,6 @@ import { isForkPostGloas, } from "@lodestar/params"; import { - EffectiveBalanceIncrements, EpochShuffling, IBeaconStateView, PubkeyCache, @@ -78,7 +77,7 @@ import {persistPayloadEnvelopeInput} from "./blocks/writePayloadEnvelopeInputToD import {IBlsVerifier} from "./bls/index.js"; import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js"; import {ChainEvent, ChainEventEmitter} from "./emitter.js"; -import {ForkchoiceCaller, initializeForkChoice} from "./forkChoice/index.js"; +import {ForkchoiceCaller} from "./forkChoice/index.js"; import {GetBlobsTracker} from "./GetBlobsTracker.js"; import {CommonBlockBody, FindHeadFnName, IBeaconChain, ProposerPreparationData, StateGetOpts} from "./interface.js"; import {LightClientServer} from "./lightClient/index.js"; @@ -119,9 +118,6 @@ import {ShufflingCache} from "./shufflingCache.js"; import {DbCPStateDatastore, checkpointToDatastoreKey} from "./stateCache/datastore/db.js"; import {FileCPStateDatastore} from "./stateCache/datastore/file.js"; import {CPStateDatastore} from "./stateCache/datastore/types.js"; -import {FIFOBlockStateCache} from "./stateCache/fifoBlockStateCache.js"; -import {PersistentCheckpointStateCache} from "./stateCache/persistentCheckpointsCache.js"; -import {CheckpointStateCache} from "./stateCache/types.js"; import {ValidatorMonitor} from "./validatorMonitor.js"; /** @@ -164,13 +160,20 @@ export class BeaconChain implements IBeaconChain { readonly anchorStateLatestBlockSlot: Slot; readonly beaconEngine: BeaconEngine; + // TODO - beacon engine: remove this? get bls(): IBlsVerifier { return this.beaconEngine.bls; } - readonly forkChoice: IForkChoice; + // TODO - beacon engine: remove this + get forkChoice(): IForkChoice { + return this.beaconEngine.forkChoice; + } readonly clock: IClock; readonly emitter: ChainEventEmitter; - readonly regen: QueuedStateRegenerator; + // TODO - beacon engine: remove this + get regen(): QueuedStateRegenerator { + return this.beaconEngine.regen; + } readonly lightClientServer?: LightClientServer; readonly reprocessController: ReprocessController; readonly archiveStore: ArchiveStore; @@ -231,7 +234,13 @@ export class BeaconChain implements IBeaconChain { readonly pubkeyCache: PubkeyCache; readonly beaconProposerCache: BeaconProposerCache; - readonly checkpointBalancesCache: CheckpointBalancesCache; + + // TODO - beacon engine: remove this + get checkpointBalancesCache(): CheckpointBalancesCache { + return this.beaconEngine.checkpointBalancesCache; + } + + // TODO - beacon engine: remove this get shufflingCache(): ShufflingCache { return this.beaconEngine.shufflingCache; } @@ -326,12 +335,9 @@ export class BeaconChain implements IBeaconChain { if (!clock) clock = new Clock({config, genesisTime: this.genesisTime, signal}); - this.beaconEngine = new BeaconEngine({opts, config, logger, metrics, clock, pubkeyCache}, anchorState); - - this.blacklistedBlocks = new Map((opts.blacklistedBlocks ?? []).map((hex) => [hex, null])); - this.executionPayloadBidPool = new ExecutionPayloadBidPool(); - - this.seenAggregatedAttestations = new SeenAggregatedAttestations(metrics); + this.bufferPool = new BufferPool(anchorState.serializedSize(), metrics); + const fileDataStore = opts.nHistoricalStatesFileDataStore ?? true; + this.cpStateDatastore = fileDataStore ? new FileCPStateDatastore(dataDir) : new DbCPStateDatastore(this.db); const nodeId = computeNodeIdFromPrivateKey(privateKey); const initialCustodyGroupCount = opts.initialCustodyGroupCount ?? config.CUSTODY_REQUIREMENT; @@ -344,9 +350,8 @@ export class BeaconChain implements IBeaconChain { initialCustodyGroupCount, }); - this.beaconProposerCache = new BeaconProposerCache(opts); - this.checkpointBalancesCache = new CheckpointBalancesCache(); this.serializedCache = new SerializedCache(); + // seenBlockInputCache stays facade-owned but is built before the engine so it can be injected into regen. this.seenBlockInputCache = new SeenBlockInput({ config, custodyConfig: this.custodyConfig, @@ -358,68 +363,39 @@ export class BeaconChain implements IBeaconChain { logger, }); - this._earliestAvailableSlot = anchorState.slot; - - // Global cache of validators pubkey/index mapping - this.pubkeyCache = pubkeyCache; - - const fileDataStore = opts.nHistoricalStatesFileDataStore ?? true; - const blockStateCache = new FIFOBlockStateCache(this.opts, {metrics}); - this.bufferPool = new BufferPool(anchorState.serializedSize(), metrics); - - this.cpStateDatastore = fileDataStore ? new FileCPStateDatastore(dataDir) : new DbCPStateDatastore(this.db); - const checkpointStateCache: CheckpointStateCache = new PersistentCheckpointStateCache( + this.beaconEngine = new BeaconEngine( { + opts, config, - metrics, logger, + metrics, clock, - blockStateCache, + pubkeyCache, bufferPool: this.bufferPool, - datastore: this.cpStateDatastore, + cpStateDatastore: this.cpStateDatastore, + emitter, + signal, + db, + validatorMonitor, + seenBlockInputCache: this.seenBlockInputCache, + isAnchorStateFinalized, }, - this.opts + anchorState ); - const {checkpoint} = anchorState.computeAnchorCheckpoint(); - blockStateCache.add(anchorState); - blockStateCache.setHeadState(anchorState); - checkpointStateCache.add(checkpoint, anchorState); + this.blacklistedBlocks = new Map((opts.blacklistedBlocks ?? []).map((hex) => [hex, null])); + this.executionPayloadBidPool = new ExecutionPayloadBidPool(); - const forkChoiceStateGetter: ForkChoiceStateGetter = ({stateRoot, checkpoint}) => { - if (stateRoot) return blockStateCache.get(stateRoot); + this.seenAggregatedAttestations = new SeenAggregatedAttestations(metrics); - if (checkpoint) return checkpointStateCache.get({epoch: checkpoint.epoch, rootHex: checkpoint.rootHex}); + this.beaconProposerCache = new BeaconProposerCache(opts); - return null; - }; + this._earliestAvailableSlot = anchorState.slot; - const forkChoice = initializeForkChoice( - config, - emitter, - clock.currentSlot, - anchorState, - isAnchorStateFinalized, - opts, - this.justifiedBalancesGetter.bind(this), - forkChoiceStateGetter, - metrics, - logger - ); + // Global cache of validators pubkey/index mapping + this.pubkeyCache = pubkeyCache; - const regen = new QueuedStateRegenerator({ - config, - forkChoice, - blockStateCache, - checkpointStateCache, - seenBlockInputCache: this.seenBlockInputCache, - db, - metrics, - validatorMonitor, - logger, - emitter, - signal, - }); + const {checkpoint} = anchorState.computeAnchorCheckpoint(); if (!opts.disableLightClientServer) { this.lightClientServer = new LightClientServer(opts, {config, clock, db, metrics, emitter, logger, signal}); @@ -430,12 +406,11 @@ export class BeaconChain implements IBeaconChain { this.blockProcessor = new BlockProcessor(this, metrics, opts, signal); this.payloadEnvelopeProcessor = new PayloadEnvelopeProcessor(this, metrics, signal); - this.forkChoice = forkChoice; - this.seenPayloadEnvelopeInputCache = new SeenPayloadEnvelopeInput({ config, clock, - forkChoice, + // TODO - beacon engine: cannot have forkchoice here + forkChoice: this.beaconEngine.forkChoice, chainEvents: emitter, signal, serializedCache: this.serializedCache, @@ -459,7 +434,6 @@ export class BeaconChain implements IBeaconChain { } this.clock = clock; - this.regen = regen; this.emitter = emitter; this.getBlobsTracker = new GetBlobsTracker({ @@ -1331,95 +1305,6 @@ export class BeaconChain implements IBeaconChain { return state.getShufflingAtEpoch(attEpoch); } - /** - * `ForkChoice.onBlock` must never throw for a block that is valid with respect to the network - * `justifiedBalancesGetter()` must never throw and it should always return a state. - * @param blockState state that declares justified checkpoint `checkpoint` - */ - private justifiedBalancesGetter( - checkpoint: CheckpointWithHex, - blockState: IBeaconStateView - ): EffectiveBalanceIncrements { - this.metrics?.balancesCache.requests.inc(); - - const effectiveBalances = this.checkpointBalancesCache.get(checkpoint); - if (effectiveBalances) { - return effectiveBalances; - } - // not expected, need metrics - this.metrics?.balancesCache.misses.inc(); - this.logger.debug("checkpointBalances cache miss", { - epoch: checkpoint.epoch, - root: checkpoint.rootHex, - }); - - const {state, stateId, shouldWarn} = this.closestJustifiedBalancesStateToCheckpoint(checkpoint, blockState); - this.metrics?.balancesCache.closestStateResult.inc({stateId}); - if (shouldWarn) { - this.logger.warn("currentJustifiedCheckpoint state not avail, using closest state", { - checkpointEpoch: checkpoint.epoch, - checkpointRoot: checkpoint.rootHex, - stateId, - stateSlot: state.slot, - stateRoot: toRootHex(state.hashTreeRoot()), - }); - } - - return state.getEffectiveBalanceIncrementsZeroInactive(); - } - - /** - * - Assumptions + invariant this function is based on: - * - Our cache can only persist X states at once to prevent OOM - * - Some old states (including to-be justified checkpoint) may / must be dropped from the cache - * - Thus, there is no guarantee that the state for a justified checkpoint will be available in the cache - * @param blockState state that declares justified checkpoint `checkpoint` - */ - private closestJustifiedBalancesStateToCheckpoint( - checkpoint: CheckpointWithHex, - blockState: IBeaconStateView - ): {state: IBeaconStateView; stateId: string; shouldWarn: boolean} { - const checkpointHex = {epoch: checkpoint.epoch, rootHex: checkpoint.rootHex}; - const state = this.regen.getCheckpointStateSync(checkpointHex); - if (state) { - return {state, stateId: "checkpoint_state", shouldWarn: false}; - } - - // Check if blockState is in the same epoch, not need to iterate the fork-choice then - if (computeEpochAtSlot(blockState.slot) === checkpoint.epoch) { - return {state: blockState, stateId: "block_state_same_epoch", shouldWarn: true}; - } - - // Find a state in the same branch of checkpoint at same epoch. Balances should exactly the same - for (const descendantBlock of this.forkChoice.forwardIterateDescendantsDefaultStatus(checkpoint.rootHex)) { - if (computeEpochAtSlot(descendantBlock.slot) === checkpoint.epoch) { - const descendantBlockState = this.regen.getStateSync(descendantBlock.stateRoot); - if (descendantBlockState) { - return {state: descendantBlockState, stateId: "descendant_state_same_epoch", shouldWarn: true}; - } - } - } - - // Check if blockState is in the next epoch, not need to iterate the fork-choice then - if (computeEpochAtSlot(blockState.slot) === checkpoint.epoch + 1) { - return {state: blockState, stateId: "block_state_next_epoch", shouldWarn: true}; - } - - // Find a state in the same branch of checkpoint at a latter epoch. Balances are not the same, but should be close - // Note: must call .forwardIterateDescendants() again since nodes are not sorted - for (const descendantBlock of this.forkChoice.forwardIterateDescendantsDefaultStatus(checkpoint.rootHex)) { - if (computeEpochAtSlot(descendantBlock.slot) > checkpoint.epoch) { - const descendantBlockState = this.regen.getStateSync(descendantBlock.stateRoot); - if (descendantBlockState) { - return {state: blockState, stateId: "descendant_state_latter_epoch", shouldWarn: true}; - } - } - } - - // If there's no state available in the same branch of checkpoint use blockState regardless of its epoch - return {state: blockState, stateId: "block_state_any_epoch", shouldWarn: true}; - } - private async persistSszObject(prefix: string, bytes: Uint8Array, root: Uint8Array, logStr?: string): Promise { const now = new Date(); // yyyy-MM-dd diff --git a/packages/beacon-node/src/chain/options.ts b/packages/beacon-node/src/chain/options.ts index 2148ad21e7be..6d7570d0f846 100644 --- a/packages/beacon-node/src/chain/options.ts +++ b/packages/beacon-node/src/chain/options.ts @@ -2,13 +2,11 @@ import {defaultOptions as defaultValidatorOptions} from "@lodestar/validator"; import {DEFAULT_ARCHIVE_MODE} from "./archiveStore/constants.js"; import {ArchiveMode, ArchiveStoreOpts} from "./archiveStore/interface.js"; import {IBeaconEngineOptions} from "./beaconEngine/options.js"; -import {ForkChoiceOpts} from "./forkChoice/index.js"; import {LightClientServerOpts} from "./lightClient/index.js"; -import {DEFAULT_MAX_BLOCK_STATES, FIFOBlockStateCacheOpts} from "./stateCache/fifoBlockStateCache.js"; +import {DEFAULT_MAX_BLOCK_STATES} from "./stateCache/fifoBlockStateCache.js"; import { DEFAULT_MAX_CP_STATE_EPOCHS_IN_MEMORY, DEFAULT_MAX_CP_STATE_ON_DISK, - PersistentCheckpointStateCacheOpts, } from "./stateCache/persistentCheckpointsCache.js"; import {ValidatorMonitorOpts} from "./validatorMonitor.js"; @@ -16,10 +14,7 @@ export {ArchiveMode, DEFAULT_ARCHIVE_MODE}; export type IChainOptions = IBeaconEngineOptions & BlockProcessOpts & - ForkChoiceOpts & ArchiveStoreOpts & - FIFOBlockStateCacheOpts & - PersistentCheckpointStateCacheOpts & ValidatorMonitorOpts & LightClientServerOpts & { blacklistedBlocks?: string[]; diff --git a/packages/beacon-node/src/chain/regen/regen.ts b/packages/beacon-node/src/chain/regen/regen.ts index a7de84a7061b..31482575f453 100644 --- a/packages/beacon-node/src/chain/regen/regen.ts +++ b/packages/beacon-node/src/chain/regen/regen.ts @@ -194,6 +194,8 @@ export class StateRegenerator implements IStateRegeneratorInternal { const protoBlocksAsc = blocksToReplay.reverse(); for (const [i, protoBlock] of protoBlocksAsc.entries()) { replaySlots[i] = protoBlock.slot; + // TODO - beacon engine: cannot have this seenBlockInputCache + // use a separate cache in BeaconEngine? const blockInput = this.modules.seenBlockInputCache.get(protoBlock.blockRoot); blockPromises[i] = blockInput?.hasBlock() ? Promise.resolve(blockInput.getBlock()) From a107e8713bf8bc095a8006fd93ea113214f5a2d5 Mon Sep 17 00:00:00 2001 From: twoeths Date: Thu, 18 Jun 2026 14:34:13 +0700 Subject: [PATCH 04/24] feat: implement IForkchoiceRead --- .../src/api/impl/beacon/blocks/utils.ts | 4 +- .../src/api/impl/beacon/pool/index.ts | 3 +- .../src/api/impl/beacon/state/utils.ts | 4 +- .../src/chain/archiveStore/archiveStore.ts | 2 +- .../chain/archiveStore/utils/archiveBlocks.ts | 4 +- .../src/chain/beaconEngine/beaconEngine.ts | 4 + .../src/chain/beaconEngine/interface.ts | 5 + .../src/chain/blocks/importBlock.ts | 8 +- .../chain/blocks/importExecutionPayload.ts | 2 +- .../beacon-node/src/chain/blocks/index.ts | 2 +- .../blocks/verifyBlocksExecutionPayloads.ts | 4 +- .../chain/blocks/verifyBlocksSanityChecks.ts | 4 +- packages/beacon-node/src/chain/chain.ts | 19 +- packages/beacon-node/src/chain/interface.ts | 4 +- .../opPools/aggregatedAttestationPool.ts | 10 +- .../chain/produceBlock/produceBlockBody.ts | 10 +- .../src/network/processor/gossipHandlers.ts | 8 +- .../src/sync/utils/remoteSyncType.ts | 6 +- .../beacon-node/src/util/dependentRoot.ts | 4 +- .../test/e2e/sync/checkpointSync.test.ts | 2 +- .../spec/presets/fast_confirmation.test.ts | 4 +- .../test/spec/presets/fork_choice.test.ts | 6 +- .../test/spec/utils/gossipValidation.ts | 14 +- .../fork-choice/src/forkChoice/interface.ts | 231 ++++++++++-------- .../fork-choice/src/forkChoice/safeBlocks.ts | 6 +- packages/fork-choice/src/index.ts | 1 + 26 files changed, 201 insertions(+), 170 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts b/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts index b27979dc4149..31525230e57a 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts @@ -1,6 +1,6 @@ import {routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; -import {IForkChoice} from "@lodestar/fork-choice"; +import {IForkChoiceRead} from "@lodestar/fork-choice"; import {blockToHeader} from "@lodestar/state-transition"; import {RootHex, SignedBeaconBlock, Slot} from "@lodestar/types"; import {IBeaconChain} from "../../../../chain/interface.js"; @@ -23,7 +23,7 @@ export function toBeaconHeaderResponse( }; } -export function resolveBlockId(forkChoice: IForkChoice, blockId: routes.beacon.BlockId): RootHex | Slot { +export function resolveBlockId(forkChoice: IForkChoiceRead, blockId: routes.beacon.BlockId): RootHex | Slot { blockId = String(blockId).toLowerCase(); if (blockId === "head") { return forkChoice.getHead().blockRoot; diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index 709d0d8d0f4e..d99693e46336 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -286,7 +286,8 @@ export function getBeaconPoolApi({ ); metrics?.opPool.payloadAttestationPool.apiInsertOutcome.inc({insertOutcome}); - chain.forkChoice.notifyPtcMessages( + // TODO - beacon engine: refactor to beaconEngine.notifyPtcMessages() + chain.beaconEngine.forkChoice.notifyPtcMessages( toRootHex(payloadAttestationMessage.data.beaconBlockRoot), payloadAttestationMessage.data.slot, validatorCommitteeIndices, diff --git a/packages/beacon-node/src/api/impl/beacon/state/utils.ts b/packages/beacon-node/src/api/impl/beacon/state/utils.ts index 44a69c87d107..0d1afa98e59e 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/utils.ts @@ -1,5 +1,5 @@ import {routes} from "@lodestar/api"; -import {CheckpointWithHex, IForkChoice} from "@lodestar/fork-choice"; +import {CheckpointWithHex, IForkChoiceRead} from "@lodestar/fork-choice"; import {GENESIS_SLOT} from "@lodestar/params"; import {IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; import {BLSPubkey, Epoch, RootHex, Slot, ValidatorIndex, getValidatorStatus, phase0} from "@lodestar/types"; @@ -8,7 +8,7 @@ import {IBeaconChain} from "../../../../chain/index.js"; import {ApiError, ValidationError} from "../../errors.js"; export function resolveStateId( - forkChoice: IForkChoice, + forkChoice: IForkChoiceRead, stateId: routes.beacon.StateId ): RootHex | Slot | CheckpointWithHex { if (stateId === "head") { diff --git a/packages/beacon-node/src/chain/archiveStore/archiveStore.ts b/packages/beacon-node/src/chain/archiveStore/archiveStore.ts index aa307adb440d..345970ed2347 100644 --- a/packages/beacon-node/src/chain/archiveStore/archiveStore.ts +++ b/packages/beacon-node/src/chain/archiveStore/archiveStore.ts @@ -230,7 +230,7 @@ export class ArchiveStore { // tasks rely on extended fork choice timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); - const prunedBlocks = this.chain.forkChoice.prune(finalized.rootHex); + const prunedBlocks = this.chain.beaconEngine.forkChoice.prune(finalized.rootHex); timer?.({source: ArchiveStoreTask.ForkchoicePrune}); timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); diff --git a/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts b/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts index 60b89bb58c08..9d20d228babf 100644 --- a/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts +++ b/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts @@ -1,7 +1,7 @@ import path from "node:path"; import {ChainForkConfig} from "@lodestar/config"; import {KeyValue} from "@lodestar/db"; -import {CheckpointWithHex, IForkChoice, PayloadStatus, ProtoBlock} from "@lodestar/fork-choice"; +import {CheckpointWithHex, IForkChoiceRead, PayloadStatus, ProtoBlock} from "@lodestar/fork-choice"; import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params"; import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {Epoch, Slot} from "@lodestar/types"; @@ -49,7 +49,7 @@ async function persistOrphanedBlock( export async function archiveBlocks( config: ChainForkConfig, db: IBeaconDb, - forkChoice: IForkChoice, + forkChoice: IForkChoiceRead, lightclientServer: LightClientServer | undefined, logger: Logger, finalizedCheckpoint: CheckpointWithHex, diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 876a5c5c2779..9823f8fe7604 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -164,6 +164,10 @@ export class BeaconEngine implements IBeaconEngine { }); } + // TODO - beacon engine: scalar state reads (getBeaconProposer, getValidator, getBalance, + // getRandaoMix, getBlockRootAtSlot, getStateRootAtSlot, getShufflingDecisionRoot). Deferred — + // signature shape (state vs stateRoot) to be decided alongside Phase 4 bytes-first. + /** * `ForkChoice.onBlock` must never throw for a block that is valid with respect to the network * `justifiedBalancesGetter()` must never throw and it should always return a state. diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 388be849f7bc..1a66472d3e4d 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -1,4 +1,5 @@ import {BeaconConfig} from "@lodestar/config"; +import {IForkChoice} from "@lodestar/fork-choice"; import {PubkeyCache} from "@lodestar/state-transition"; import {Logger} from "@lodestar/utils"; import {IBeaconDb} from "../../db/index.js"; @@ -36,4 +37,8 @@ export type BeaconEngineModules = { */ export interface IBeaconEngine { readonly config: BeaconConfig; + // Full fork choice (read + write). The engine owns it; writes are routed here while the flows that + // perform them still live facade-side. TODO - beacon engine: narrow to a read facet as those flows + // (gossip → Phase 3, migrateFinalized/prune → Phase 5) move into the engine. + readonly forkChoice: IForkChoice; } diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 7c25d2b47cdb..6c5a79cb1736 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -121,7 +121,7 @@ export async function importBlock( // from block slot. We skip proposer boost canonical check as we cannot determine the canonical proposer const expectedProposerIndex: number | null = this.getHeadState().getBeaconProposerOrNull(blockSlot); - const blockSummary = this.forkChoice.onBlock( + const blockSummary = this.beaconEngine.forkChoice.onBlock( block.message, postState, blockDelaySec, @@ -178,7 +178,7 @@ export async function importBlock( opts.importAttestations === AttestationImportOpt.Force || (target.epoch <= currentEpoch && target.epoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT) ) { - this.forkChoice.onAttestation( + this.beaconEngine.forkChoice.onAttestation( indexedAttestation, attDataRoot, opts.importAttestations === AttestationImportOpt.Force @@ -242,7 +242,7 @@ export async function importBlock( for (const slashing of block.message.body.attesterSlashings) { try { // all AttesterSlashings are valid before reaching this - this.forkChoice.onAttesterSlashing(slashing); + this.beaconEngine.forkChoice.onAttesterSlashing(slashing); } catch (e) { this.logger.warn("Error processing AttesterSlashing from block", {slot: blockSlot}, e as Error); } @@ -263,7 +263,7 @@ export async function importBlock( } if (ptcIndices.length > 0) { - this.forkChoice.notifyPtcMessages( + this.beaconEngine.forkChoice.notifyPtcMessages( toRootHex(payloadAttestation.data.beaconBlockRoot), payloadAttestation.data.slot, ptcIndices, diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index dac0b486c374..f1a592ef8579 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -233,7 +233,7 @@ export async function importExecutionPayload( // 6. Update fork choice, transitions the block's PENDING variant to FULL const execStatus = toForkChoiceExecutionStatus(execResult.status); - this.forkChoice.onExecutionPayload( + this.beaconEngine.forkChoice.onExecutionPayload( blockRootHex, blockHashHex, envelope.payload.blockNumber, diff --git a/packages/beacon-node/src/chain/blocks/index.ts b/packages/beacon-node/src/chain/blocks/index.ts index c7b60d4f5a73..097813b38703 100644 --- a/packages/beacon-node/src/chain/blocks/index.ts +++ b/packages/beacon-node/src/chain/blocks/index.ts @@ -103,7 +103,7 @@ export async function processBlocks( // and we need to further propagate if (segmentExecStatus.execAborted !== null) { if (segmentExecStatus.invalidSegmentLVH !== undefined) { - this.forkChoice.validateLatestHash(segmentExecStatus.invalidSegmentLVH); + this.beaconEngine.forkChoice.validateLatestHash(segmentExecStatus.invalidSegmentLVH); } throw segmentExecStatus.execAborted.execError; } diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts index c437437ab4d9..3f462048debb 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts @@ -2,7 +2,7 @@ import {ChainForkConfig} from "@lodestar/config"; import { BlockExecutionStatus, ExecutionStatus, - IForkChoice, + IForkChoiceRead, LVHInvalidResponse, LVHValidResponse, ProtoBlock, @@ -25,7 +25,7 @@ export type VerifyBlockExecutionPayloadModules = { clock: IClock; logger: Logger; metrics: Metrics | null; - forkChoice: IForkChoice; + forkChoice: IForkChoiceRead; config: ChainForkConfig; }; diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksSanityChecks.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksSanityChecks.ts index 94f39b9a5d70..ad00ab8e88bd 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksSanityChecks.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksSanityChecks.ts @@ -1,5 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; -import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; +import {IForkChoiceRead, ProtoBlock} from "@lodestar/fork-choice"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {RootHex, Slot, isGloasBeaconBlock} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; @@ -24,7 +24,7 @@ import {ImportBlockOpts} from "./types.js"; */ export function verifyBlocksSanityChecks( chain: { - forkChoice: IForkChoice; + forkChoice: IForkChoiceRead; clock: IClock; config: ChainForkConfig; opts: IChainOptions; diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index eb59620aba5b..c986a288b6ad 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -2,7 +2,7 @@ import path from "node:path"; import {PrivateKey} from "@libp2p/interface"; import {Type} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; -import {CheckpointWithHex, IForkChoice, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice"; +import {CheckpointWithHex, IForkChoiceRead, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice"; import {LoggerNode} from "@lodestar/logger/node"; import { EFFECTIVE_BALANCE_INCREMENT, @@ -164,8 +164,9 @@ export class BeaconChain implements IBeaconChain { get bls(): IBlsVerifier { return this.beaconEngine.bls; } - // TODO - beacon engine: remove this - get forkChoice(): IForkChoice { + // Read facet only — writes go through `beaconEngine.forkChoice`. TODO - beacon engine: remove this + // getter in Phase 6 when the facade holds no state. + get forkChoice(): IForkChoiceRead { return this.beaconEngine.forkChoice; } readonly clock: IClock; @@ -1152,7 +1153,7 @@ export class BeaconChain implements IBeaconChain { const timer = this.metrics?.forkChoice.findHead.startTimer({caller}); try { - return this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head; + return this.beaconEngine.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head; } catch (e) { this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead}); throw e; @@ -1167,7 +1168,11 @@ export class BeaconChain implements IBeaconChain { const secFromSlot = this.clock.secFromSlot(slot); try { - return this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetPredictedProposerHead, secFromSlot, slot}).head; + return this.beaconEngine.forkChoice.updateAndGetHead({ + mode: UpdateHeadOpt.GetPredictedProposerHead, + secFromSlot, + slot, + }).head; } catch (e) { this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetPredictedProposerHead}); throw e; @@ -1182,7 +1187,7 @@ export class BeaconChain implements IBeaconChain { const secFromSlot = this.clock.secFromSlot(slot); try { - const {head, isHeadTimely, notReorgedReason} = this.forkChoice.updateAndGetHead({ + const {head, isHeadTimely, notReorgedReason} = this.beaconEngine.forkChoice.updateAndGetHead({ mode: UpdateHeadOpt.GetProposerHead, secFromSlot, slot, @@ -1352,7 +1357,7 @@ export class BeaconChain implements IBeaconChain { this.processShutdownCallback(this.forkChoice.irrecoverableError); } - this.forkChoice.updateTime(slot); + this.beaconEngine.forkChoice.updateTime(slot); this.metrics?.clockSlot.set(slot); this.attestationPool.prune(slot); diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 8015981575b4..48cc396af87e 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -1,6 +1,6 @@ import {Type} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; -import {CheckpointWithHex, IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; +import {CheckpointWithHex, IForkChoiceRead, ProtoBlock} from "@lodestar/fork-choice"; import {EpochShuffling, IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; import { BeaconBlock, @@ -110,7 +110,7 @@ export interface IBeaconChain { readonly beaconEngine: IBeaconEngine; readonly bls: IBlsVerifier; - readonly forkChoice: IForkChoice; + readonly forkChoice: IForkChoiceRead; readonly clock: IClock; readonly emitter: ChainEventEmitter; readonly regen: IStateRegenerator; diff --git a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts index 55339fd566bc..96f8bc0ba0f8 100644 --- a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts @@ -1,7 +1,7 @@ import {Signature, aggregateSignatures} from "@chainsafe/blst"; import {BitArray} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; -import {IForkChoice} from "@lodestar/fork-choice"; +import {IForkChoiceRead} from "@lodestar/fork-choice"; import { ForkName, ForkSeq, @@ -209,7 +209,7 @@ export class AggregatedAttestationPool { getAttestationsForBlock( fork: ForkName, - forkChoice: IForkChoice, + forkChoice: IForkChoiceRead, shufflingCache: ShufflingCache, state: IBeaconStateView ): Attestation[] { @@ -226,7 +226,7 @@ export class AggregatedAttestationPool { */ getAttestationsForBlockElectra( fork: ForkName, - forkChoice: IForkChoice, + forkChoice: IForkChoiceRead, shufflingCache: ShufflingCache, state: IBeaconStateView ): electra.Attestation[] { @@ -802,7 +802,7 @@ export function getNotSeenValidatorsFn( * See also: https://github.com/ChainSafe/lodestar/issues/4333 */ export function getValidateAttestationDataFn( - forkChoice: IForkChoice, + forkChoice: IForkChoiceRead, state: IBeaconStateView ): ValidateAttestationDataFn { const cachedValidatedAttestationData = new Map(); @@ -848,7 +848,7 @@ export function getValidateAttestationDataFn( * Return `null` if the shuffling is valid, otherwise return an `InvalidAttestationData` reason. */ function isValidShuffling( - forkChoice: IForkChoice, + forkChoice: IForkChoiceRead, state: IBeaconStateView, blockRootHex: RootHex, targetEpoch: Epoch diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index e41268f091f8..9153bf6cbc67 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -1,5 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; -import {IForkChoice, ProtoBlock, getSafeExecutionBlockHash} from "@lodestar/fork-choice"; +import {IForkChoiceRead, ProtoBlock, getSafeExecutionBlockHash} from "@lodestar/fork-choice"; import { BUILDER_INDEX_SELF_BUILD, ForkName, @@ -700,7 +700,7 @@ export async function prepareExecutionPayload( chain: { executionEngine: IExecutionEngine; config: ChainForkConfig; - forkChoice: IForkChoice; + forkChoice: IForkChoiceRead; proposerPreferencesPool: ProposerPreferencesPool; }, logger: Logger, @@ -801,7 +801,7 @@ export function getPayloadAttributesForSSE( fork: ForkPostBellatrix, chain: { config: ChainForkConfig; - forkChoice: IForkChoice; + forkChoice: IForkChoiceRead; proposerPreferencesPool: ProposerPreferencesPool; }, { @@ -859,7 +859,7 @@ function preparePayloadAttributes( fork: ForkPostBellatrix, chain: { config: ChainForkConfig; - forkChoice: IForkChoice; + forkChoice: IForkChoiceRead; proposerPreferencesPool: ProposerPreferencesPool; }, { @@ -953,7 +953,7 @@ function preparePayloadAttributes( * (EMPTY.executionPayloadGasLimit = inherited grandparent's gas_limit). */ function getProposerTargetGasLimit( - chain: {forkChoice: IForkChoice; proposerPreferencesPool: ProposerPreferencesPool}, + chain: {forkChoice: IForkChoiceRead; proposerPreferencesPool: ProposerPreferencesPool}, prepareSlot: Slot, parentBlockRoot: Root, parentBlockHash: Bytes32 diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 040af011d81d..f8166b772b12 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -927,7 +927,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand if (!options.dontSendGossipAttestationsToForkchoice) { try { - chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex); + chain.beaconEngine.forkChoice.onAttestation(indexedAttestation, attDataRootHex); } catch (e) { logger.debug( "Error adding gossip aggregated attestation to forkchoice", @@ -953,7 +953,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand try { chain.opPool.insertAttesterSlashing(fork, attesterSlashing); - chain.forkChoice.onAttesterSlashing(attesterSlashing); + chain.beaconEngine.forkChoice.onAttesterSlashing(attesterSlashing); } catch (e) { logger.error("Error adding attesterSlashing to pool", {}, e as Error); } @@ -1219,7 +1219,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand } catch (e) { logger.error("Error adding to payloadAttestation pool", {}, e as Error); } - chain.forkChoice.notifyPtcMessages( + chain.beaconEngine.forkChoice.notifyPtcMessages( toRootHex(payloadAttestationMessage.data.beaconBlockRoot), payloadAttestationMessage.data.slot, validationResult.validatorCommitteeIndices, @@ -1337,7 +1337,7 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp if (!options.dontSendGossipAttestationsToForkchoice) { try { - chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex); + chain.beaconEngine.forkChoice.onAttestation(indexedAttestation, attDataRootHex); } catch (e) { logger.debug("Error adding gossip unaggregated attestation to forkchoice", {subnet}, e as Error); } diff --git a/packages/beacon-node/src/sync/utils/remoteSyncType.ts b/packages/beacon-node/src/sync/utils/remoteSyncType.ts index 68f958a203a6..e3fb0801090b 100644 --- a/packages/beacon-node/src/sync/utils/remoteSyncType.ts +++ b/packages/beacon-node/src/sync/utils/remoteSyncType.ts @@ -1,4 +1,4 @@ -import {IForkChoice} from "@lodestar/fork-choice"; +import {IForkChoiceRead} from "@lodestar/fork-choice"; import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {Slot, Status} from "@lodestar/types"; import {IBeaconChain} from "../../chain/interface.js"; @@ -24,7 +24,7 @@ function withinRangeOf(value: number, target: number, range: number): boolean { export function getPeerSyncType( local: Status, remote: Status, - forkChoice: IForkChoice, + forkChoice: IForkChoiceRead, slotImportTolerance: number ): PeerSyncType { // Aux vars: Inclusive boundaries of the range to consider a peer's head synced to ours. @@ -94,7 +94,7 @@ export const rangeSyncTypes = Object.keys(RangeSyncType) as RangeSyncType[]; * - The remotes finalized epoch is greater than our current finalized epoch and we have * not seen the finalized hash before */ -export function getRangeSyncType(local: Status, remote: Status, forkChoice: IForkChoice): RangeSyncType { +export function getRangeSyncType(local: Status, remote: Status, forkChoice: IForkChoiceRead): RangeSyncType { if (remote.finalizedEpoch > local.finalizedEpoch && !forkChoice.hasBlock(remote.finalizedRoot)) { return RangeSyncType.Finalized; } diff --git a/packages/beacon-node/src/util/dependentRoot.ts b/packages/beacon-node/src/util/dependentRoot.ts index 0d8c4d0242b3..50f626743058 100644 --- a/packages/beacon-node/src/util/dependentRoot.ts +++ b/packages/beacon-node/src/util/dependentRoot.ts @@ -1,4 +1,4 @@ -import {EpochDifference, IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; +import {EpochDifference, IForkChoiceRead, ProtoBlock} from "@lodestar/fork-choice"; import {Epoch, RootHex} from "@lodestar/types"; /** @@ -9,7 +9,7 @@ import {Epoch, RootHex} from "@lodestar/types"; * a dependent root of a proposal duties is 1-epoch look ahead (instead of 0 as of pre-fulu) */ export function getShufflingDependentRoot( - forkChoice: IForkChoice, + forkChoice: IForkChoiceRead, msgEpoch: Epoch, protoBlockEpoch: Epoch, protoBlock: ProtoBlock diff --git a/packages/beacon-node/test/e2e/sync/checkpointSync.test.ts b/packages/beacon-node/test/e2e/sync/checkpointSync.test.ts index 091dc7884044..ccc5aa92cce8 100644 --- a/packages/beacon-node/test/e2e/sync/checkpointSync.test.ts +++ b/packages/beacon-node/test/e2e/sync/checkpointSync.test.ts @@ -107,7 +107,7 @@ describe("sync / checkpoint sync optimistic flow for gloas", () => { if (nodeAHeadFull === null || nodeAHeadFull.executionPayloadBlockHash === null) { throw Error(`No FULL variant found on Node A for synced head root=${bn2Head.blockRoot}`); } - bn2.chain.forkChoice.onExecutionPayload( + bn2.chain.beaconEngine.forkChoice.onExecutionPayload( bn2Head.blockRoot, nodeAHeadFull.executionPayloadBlockHash, nodeAHeadFull.executionPayloadNumber, 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 b63d0cd77e21..31017d1b6c25 100644 --- a/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts +++ b/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts @@ -174,7 +174,7 @@ const fastConfirmationTest = const attDataRootHex = toHexString(sszTypesFor(fork).AttestationData.hashTreeRoot(attestation.data)); const attEpoch = computeEpochAtSlot(attestation.data.slot); const decisionRoot = headState.getShufflingDecisionRoot(attEpoch); - chain.forkChoice.onAttestation( + chain.beaconEngine.forkChoice.onAttestation( chain.shufflingCache.getIndexedAttestation(attEpoch, decisionRoot, ForkSeq[fork], attestation), attDataRootHex ); @@ -188,7 +188,7 @@ const fastConfirmationTest = }); const attesterSlashing = testcase.attesterSlashings.get(step.attester_slashing); if (!attesterSlashing) throw Error(`No attester slashing ${step.attester_slashing}`); - chain.forkChoice.onAttesterSlashing(attesterSlashing); + chain.beaconEngine.forkChoice.onAttesterSlashing(attesterSlashing); } // block step diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index cdd8cb3109cc..d4515df94305 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -194,7 +194,7 @@ const forkChoiceTest = attestation ); try { - chain.forkChoice.onAttestation(indexedAttestation, attDataRootHex); + chain.beaconEngine.forkChoice.onAttestation(indexedAttestation, attDataRootHex); if (!isValid) throw Error("Expect error since this is a negative test"); } catch (e) { if (isValid || (e as Error).message === "Expect error since this is a negative test") throw e; @@ -209,7 +209,7 @@ const forkChoiceTest = }); const attesterSlashing = testcase.attesterSlashings.get(step.attester_slashing); if (!attesterSlashing) throw Error(`No attester slashing ${step.attester_slashing}`); - chain.forkChoice.onAttesterSlashing(attesterSlashing); + chain.beaconEngine.forkChoice.onAttesterSlashing(attesterSlashing); } // payload attestation message step @@ -273,7 +273,7 @@ const forkChoiceTest = } if (!signatureValidity) throw Error("Invalid payload attestation signature"); - chain.forkChoice.notifyPtcMessages( + chain.beaconEngine.forkChoice.notifyPtcMessages( blockRoot, payloadAttestationMessage.data.slot, ptcIndices, diff --git a/packages/beacon-node/test/spec/utils/gossipValidation.ts b/packages/beacon-node/test/spec/utils/gossipValidation.ts index 448cad42b2e1..c8a8771751e3 100644 --- a/packages/beacon-node/test/spec/utils/gossipValidation.ts +++ b/packages/beacon-node/test/spec/utils/gossipValidation.ts @@ -316,7 +316,7 @@ function invalidateImportedBlock(chain: BeaconChain, blockRootHex: RootHex, pare throw new Error(`Cannot invalidate ${blockRootHex}: block has no execution payload hash`); } - chain.forkChoice.validateLatestHash({ + chain.beaconEngine.forkChoice.validateLatestHash({ executionStatus: ExecutionStatus.Invalid, latestValidExecHash: parentBlock.executionPayloadBlockHash, invalidateFromParentBlockRoot: blockRootHex, @@ -480,8 +480,8 @@ export async function runGossipValidationTest( if (blockEntry.failed) { // payload_status === "VALID" (filtered above) clock.setSlot(slot); - chain.forkChoice.updateTime(slot); - chain.forkChoice.onBlock( + chain.beaconEngine.forkChoice.updateTime(slot); + chain.beaconEngine.forkChoice.onBlock( signedBlock.message, postState, 0, @@ -496,8 +496,8 @@ export async function runGossipValidationTest( if (blockEntry.payload_status === "INVALIDATED") { clock.setSlot(slot); - chain.forkChoice.updateTime(slot); - chain.forkChoice.onBlock( + chain.beaconEngine.forkChoice.updateTime(slot); + chain.beaconEngine.forkChoice.onBlock( signedBlock.message, postState, 0, @@ -512,7 +512,7 @@ export async function runGossipValidationTest( } clock.setSlot(slot); - chain.forkChoice.updateTime(slot); + chain.beaconEngine.forkChoice.updateTime(slot); const blockImport = BlockInputPreData.createFromBlock({ forkName: fork, @@ -684,7 +684,7 @@ async function validateMessageForTopic( await validateGossipAttesterSlashing(chain, slashing); // Mirror gossip handler: insert into opPool + fork choice chain.opPool.insertAttesterSlashing(fork, slashing); - chain.forkChoice.onAttesterSlashing(slashing); + chain.beaconEngine.forkChoice.onAttesterSlashing(slashing); break; } diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index 295002242e02..3bf8c3661152 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -80,8 +80,15 @@ export type ShouldOverrideForkChoiceUpdateResult = | {shouldOverrideFcu: true; parentBlock: ProtoBlock} | {shouldOverrideFcu: false; reason: NotReorgedReason}; -export interface IForkChoice { - irrecoverableError?: Error; +/** + * Read-only facet of the fork choice. Consumers that only query the fork-choice DAG (head, blocks, + * checkpoints, ancestry, PTC votes, …) depend on this; the mutating methods live on `IForkChoice`. + * Part of the BeaconEngine seam — `chain.forkChoice` is typed as `IForkChoiceRead`; writes go + * through the engine. + * TODO - beacon engine: refine this. Cannot support iterate methods. + */ +export interface IForkChoiceRead { + readonly irrecoverableError?: Error; /** * Returns the ancestor node of `block_root` at the given `slot`. (Note: `slot` refers @@ -109,11 +116,6 @@ export interface IForkChoice { getHead(): ProtoBlock; getConfirmedRoot(): RootHex; getConfirmedBlock(): ProtoBlock | null; - updateAndGetHead(mode: UpdateAndGetHeadOpt): { - head: ProtoBlock; - isHeadTimely?: boolean; - notReorgedReason?: NotReorgedReason; - }; /** * This is called during block import when proposerBoostReorg is enabled * fcu call in `importBlock()` will be suppressed if this returns true. It is also @@ -138,98 +140,6 @@ export interface IForkChoice { getUnrealizedFinalizedCheckpoint(): CheckpointWithHex; getProposerBoostRoot(): RootHex; getPreviousProposerBoostRoot(): RootHex; - /** - * Add `block` to the fork choice DAG. - * - * ## Specification - * - * Approximates: - * - * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_block - * - * It only approximates the specification since it does not run the `state_transition` check. - * That should have already been called upstream and it's too expensive to call again. - * - * ## Notes: - * - * The supplied block **must** pass the `state_transition` function as it will not be run here. - */ - onBlock( - block: BeaconBlock, - state: IBeaconStateView, - blockDelaySec: number, - currentSlot: Slot, - executionStatus: BlockExecutionStatus, - dataAvailabilityStatus: DataAvailabilityStatus, - expectedProposerIndex: ValidatorIndex | null - ): ProtoBlock; - /** - * Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`. - * - * ## Specification - * - * Approximates: - * - * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_attestation - * - * It only approximates the specification since it does not perform - * `is_valid_indexed_attestation` since that should already have been called upstream and it's - * too expensive to call again. - * - * ## Notes: - * - * The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it - * will not be run here. - */ - onAttestation(attestation: IndexedAttestation, attDataRoot: string, forceImport?: boolean): void; - /** - * Register attester slashing in order not to consider their votes in `getHead` - * - * ## Specification - * - * https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/fork-choice.md#on_attester_slashing - */ - onAttesterSlashing(slashing: AttesterSlashing): void; - /** - * Process PTC (Payload Timeliness Committee) messages from a block - * Updates the PTC votes for the attested beacon block - * - * ## Specification - * - * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/gloas/fork-choice.md#new-notify_ptc_messages - */ - notifyPtcMessages( - blockRoot: RootHex, - slot: Slot, - 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 - * - * @param blockRoot - The beacon block root for which the payload arrived - * @param executionPayloadBlockHash - The block hash of the execution payload - * @param executionPayloadNumber - The block number of the execution payload - * @param executionPayloadGasLimit - The gas limit of the execution payload - */ - onExecutionPayload( - blockRoot: RootHex, - executionPayloadBlockHash: RootHex, - executionPayloadNumber: number, - executionPayloadGasLimit: number, - executionStatus: PayloadExecutionStatus, - dataAvailabilityStatus: DataAvailabilityStatus - ): void; - /** - * Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`. - */ - updateTime(currentSlot: Slot): void; /** * Returns current time slot. @@ -288,11 +198,6 @@ export interface IForkChoice { descendantRoot: RootHex, descendantPayloadStatus: PayloadStatus ): boolean; - /** - * Prune items up to a finalized root. - */ - prune(finalizedRoot: RootHex): ProtoBlock[]; - setPruneThreshold(threshold: number): void; /** * Iterates backwards through ancestor block summaries, starting from a block root */ @@ -343,12 +248,122 @@ export interface IForkChoice { /** Returns the distance of common ancestor of nodes to the max of the newNode and the prevNode. */ getCommonAncestorDepth(prevBlock: ProtoBlock, newBlock: ProtoBlock): AncestorResult; /** - * Optimistic sync validate till validated latest hash, invalidate any decendant branch if invalidated branch decendant provided + * A dependent root is the block root of the last block before the state transition that decided a specific shuffling */ - validateLatestHash(execResponse: LVHExecResponse): void; + getDependentRoot(block: ProtoBlock, atEpochDiff: EpochDifference): RootHex; +} +/** + * Full fork choice contract — the read facet plus the mutating methods. Writes are internal to the + * BeaconEngine; only the engine (and, transitionally, the flows not yet moved into it) hold this. + */ +export interface IForkChoice extends IForkChoiceRead { + irrecoverableError?: Error; + + updateAndGetHead(mode: UpdateAndGetHeadOpt): { + head: ProtoBlock; + isHeadTimely?: boolean; + notReorgedReason?: NotReorgedReason; + }; /** - * A dependent root is the block root of the last block before the state transition that decided a specific shuffling + * Add `block` to the fork choice DAG. + * + * ## Specification + * + * Approximates: + * + * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_block + * + * It only approximates the specification since it does not run the `state_transition` check. + * That should have already been called upstream and it's too expensive to call again. + * + * ## Notes: + * + * The supplied block **must** pass the `state_transition` function as it will not be run here. */ - getDependentRoot(block: ProtoBlock, atEpochDiff: EpochDifference): RootHex; + onBlock( + block: BeaconBlock, + state: IBeaconStateView, + blockDelaySec: number, + currentSlot: Slot, + executionStatus: BlockExecutionStatus, + dataAvailabilityStatus: DataAvailabilityStatus, + expectedProposerIndex: ValidatorIndex | null + ): ProtoBlock; + /** + * Register `attestation` with the fork choice DAG so that it may influence future calls to `getHead`. + * + * ## Specification + * + * Approximates: + * + * https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#on_attestation + * + * It only approximates the specification since it does not perform + * `is_valid_indexed_attestation` since that should already have been called upstream and it's + * too expensive to call again. + * + * ## Notes: + * + * The supplied `attestation` **must** pass the `in_valid_indexed_attestation` function as it + * will not be run here. + */ + onAttestation(attestation: IndexedAttestation, attDataRoot: string, forceImport?: boolean): void; + /** + * Register attester slashing in order not to consider their votes in `getHead` + * + * ## Specification + * + * https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/fork-choice.md#on_attester_slashing + */ + onAttesterSlashing(slashing: AttesterSlashing): void; + /** + * Process PTC (Payload Timeliness Committee) messages from a block + * Updates the PTC votes for the attested beacon block + * + * ## Specification + * + * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/gloas/fork-choice.md#new-notify_ptc_messages + */ + notifyPtcMessages( + blockRoot: RootHex, + slot: Slot, + 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 + * + * @param blockRoot - The beacon block root for which the payload arrived + * @param executionPayloadBlockHash - The block hash of the execution payload + * @param executionPayloadNumber - The block number of the execution payload + * @param executionPayloadGasLimit - The gas limit of the execution payload + */ + onExecutionPayload( + blockRoot: RootHex, + executionPayloadBlockHash: RootHex, + executionPayloadNumber: number, + executionPayloadGasLimit: number, + executionStatus: PayloadExecutionStatus, + dataAvailabilityStatus: DataAvailabilityStatus + ): void; + /** + * Call `onTick` for all slots between `fcStore.getCurrentSlot()` and the provided `currentSlot`. + */ + updateTime(currentSlot: Slot): void; + /** + * Prune items up to a finalized root. + */ + prune(finalizedRoot: RootHex): ProtoBlock[]; + setPruneThreshold(threshold: number): void; + /** + * Optimistic sync validate till validated latest hash, invalidate any decendant branch if invalidated branch decendant provided + */ + validateLatestHash(execResponse: LVHExecResponse): void; } diff --git a/packages/fork-choice/src/forkChoice/safeBlocks.ts b/packages/fork-choice/src/forkChoice/safeBlocks.ts index c8be98840e74..57ddd11c37d1 100644 --- a/packages/fork-choice/src/forkChoice/safeBlocks.ts +++ b/packages/fork-choice/src/forkChoice/safeBlocks.ts @@ -1,7 +1,7 @@ import {ZERO_HASH_HEX} from "@lodestar/params"; import {Root, RootHex} from "@lodestar/types"; import {fromHex} from "@lodestar/utils"; -import {IForkChoice} from "./interface.js"; +import {IForkChoiceRead} from "./interface.js"; /** * Under honest majority and certain network synchronicity assumptions there exists a block @@ -10,7 +10,7 @@ import {IForkChoice} from "./interface.js"; * * @deprecated The merged fast-confirmation spec only defines `get_safe_execution_block_hash`. */ -export function getSafeBeaconBlockRoot(fc: IForkChoice): Root { +export function getSafeBeaconBlockRoot(fc: IForkChoiceRead): Root { const confirmedRoot = fc.getConfirmedRoot(); if (confirmedRoot && fc.hasBlockHex(confirmedRoot)) { return fromHex(confirmedRoot); @@ -23,7 +23,7 @@ export function getSafeBeaconBlockRoot(fc: IForkChoice): Root { * * https://github.com/ethereum/consensus-specs/blob/master/fork_choice/safe-block.md#get_safe_execution_block_hash */ -export function getSafeExecutionBlockHash(forkChoice: IForkChoice): RootHex { +export function getSafeExecutionBlockHash(forkChoice: IForkChoiceRead): RootHex { const confirmedRoot = forkChoice.getConfirmedRoot(); if (confirmedRoot) { const confirmedBlock = forkChoice.getBlockHexDefaultStatus(confirmedRoot); diff --git a/packages/fork-choice/src/index.ts b/packages/fork-choice/src/index.ts index 9d5f8a54b3eb..561414b23c84 100644 --- a/packages/fork-choice/src/index.ts +++ b/packages/fork-choice/src/index.ts @@ -25,6 +25,7 @@ export { type CheckpointWithTotalBalance, EpochDifference, type IForkChoice, + type IForkChoiceRead, NotReorgedReason, } from "./forkChoice/interface.js"; export * from "./forkChoice/safeBlocks.js"; From 91c22ecf419dff460d9feb577a94b3e70963a532 Mon Sep 17 00:00:00 2001 From: twoeths Date: Thu, 18 Jun 2026 19:27:17 +0700 Subject: [PATCH 05/24] feat: move gossip validation fns to BeaconEngine --- .../src/api/impl/beacon/blocks/index.ts | 11 +- .../src/api/impl/beacon/pool/index.ts | 16 +- .../src/api/impl/validator/index.ts | 17 +- .../src/chain/beaconEngine/beaconEngine.ts | 270 +++++++++++++++++- .../src/chain/beaconEngine/interface.ts | 93 +++++- .../src/chain/beaconEngine/options.ts | 4 + packages/beacon-node/src/chain/chain.ts | 77 ++--- packages/beacon-node/src/chain/interface.ts | 9 +- packages/beacon-node/src/chain/options.ts | 2 - .../src/chain/validation/aggregateAndProof.ts | 60 ++-- .../src/chain/validation/attestation.ts | 100 ++++--- .../src/chain/validation/blobSidecar.ts | 25 +- .../beacon-node/src/chain/validation/block.ts | 41 ++- .../src/chain/validation/dataColumnSidecar.ts | 29 +- .../chain/validation/executionPayloadBid.ts | 36 +-- .../validation/executionPayloadEnvelope.ts | 28 +- .../validation/payloadAttestationMessage.ts | 30 +- .../chain/validation/proposerPreferences.ts | 22 +- .../src/chain/validation/syncCommittee.ts | 36 +-- .../syncCommitteeContributionAndProof.ts | 22 +- .../src/network/processor/gossipHandlers.ts | 93 +++--- packages/beacon-node/src/sync/unknownBlock.ts | 12 +- .../test/mocks/mockedBeaconChain.ts | 6 +- .../validation/aggregateAndProof.test.ts | 5 +- .../perf/chain/validation/attestation.test.ts | 3 +- .../test/spec/utils/gossipValidation.ts | 13 +- .../validation/aggregateAndProof.test.ts | 10 +- ...hufflingForAttestationVerification.test.ts | 12 +- .../attestation/validateAttestation.test.ts | 12 +- ...idateGossipAttestationsSameAttData.test.ts | 16 +- .../test/unit/chain/validation/block.test.ts | 32 ++- .../chain/validation/syncCommittee.test.ts | 20 +- .../test/unit/sync/unknownBlock.test.ts | 10 +- .../beacon-node/test/unit/util/kzg.test.ts | 2 +- .../test/utils/validationData/attestation.ts | 3 + 35 files changed, 791 insertions(+), 386 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 8a2c82192618..db0228f3d87b 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -48,9 +48,6 @@ import { ProduceFullFulu, ProduceFullGloas, } from "../../../../chain/produceBlock/index.js"; -import {validateGossipBlock} from "../../../../chain/validation/block.js"; -import {validateApiExecutionPayloadBid} from "../../../../chain/validation/executionPayloadBid.js"; -import {validateApiExecutionPayloadEnvelope} from "../../../../chain/validation/executionPayloadEnvelope.js"; import {OpSource} from "../../../../chain/validatorMonitor.js"; import { computePreFuluKzgCommitmentsInclusionProof, @@ -202,7 +199,9 @@ export function getBeaconBlockApi({ case routes.beacon.BroadcastValidation.gossip: { if (!blockLocallyProduced) { try { - await validateGossipBlock(config, chain, signedBlock, fork); + // TODO - engine: get block bytes from upstream + const blockBytes = config.getForkTypes(slot).SignedBeaconBlock.serialize(signedBlock); + await chain.beaconEngine.validateGossipBlock(blockBytes, signedBlock, fork); } catch (error) { if (error instanceof BlockGossipError) { switch (error.type.code) { @@ -683,7 +682,7 @@ export function getBeaconBlockApi({ throw new ApiError(400, `Envelope slot ${slot} does not match block slot ${block.slot}`); } - await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope); + await chain.beaconEngine.validateApiExecutionPayloadEnvelope(signedExecutionPayloadEnvelope); const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD; let dataColumnSidecars: gloas.DataColumnSidecar[] = []; @@ -833,7 +832,7 @@ export function getBeaconBlockApi({ throw new ApiError(400, `publishExecutionPayloadBid not supported for pre-gloas fork=${fork}`); } - await validateApiExecutionPayloadBid(chain, signedExecutionPayloadBid); + await chain.beaconEngine.validateApiExecutionPayloadBid(signedExecutionPayloadBid); try { const insertOutcome = chain.executionPayloadBidPool.add(signedExecutionPayloadBid); diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index d99693e46336..15208e94488a 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -23,11 +23,8 @@ import { } from "../../../../chain/errors/index.js"; import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js"; import {validateApiBlsToExecutionChange} from "../../../../chain/validation/blsToExecutionChange.js"; -import {toElectraSingleAttestation, validateApiAttestation} from "../../../../chain/validation/index.js"; -import {validateApiPayloadAttestationMessage} from "../../../../chain/validation/payloadAttestationMessage.js"; -import {validateGossipProposerPreferences} from "../../../../chain/validation/proposerPreferences.js"; +import {toElectraSingleAttestation} from "../../../../chain/validation/index.js"; import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js"; -import {validateApiSyncCommittee} from "../../../../chain/validation/syncCommittee.js"; import {validateApiVoluntaryExit} from "../../../../chain/validation/voluntaryExit.js"; import {validateGossipFnRetryUnknownRoot} from "../../../../network/processor/gossipHandlers.js"; import {ApiError, FailureList, IndexedError} from "../../errors.js"; @@ -81,7 +78,9 @@ export function getBeaconPoolApi({ await Promise.all( signedProposerPreferences.map(async (signed, i) => { try { - await validateGossipProposerPreferences(chain, signed); + // TODO - beacon engine: get bytes from the api + const preferencesBytes = ssz.gloas.SignedProposerPreferences.serialize(signed); + await chain.beaconEngine.validateGossipProposerPreferences(preferencesBytes, signed); chain.proposerPreferencesPool.add(signed); await network.publishProposerPreferences(signed); @@ -144,7 +143,8 @@ export function getBeaconPoolApi({ await Promise.all( signedAttestations.map(async (attestation, i) => { try { - const validateFn = () => validateApiAttestation(fork, chain, {attestation, serializedData: null}); + const validateFn = () => + chain.beaconEngine.validateApiAttestation(fork, {attestation, serializedData: null}); const {slot, beaconBlockRoot} = attestation.data; // when a validator is configured with multiple beacon node urls, this attestation data may come from another beacon node // and the block hasn't been in our forkchoice since we haven't seen / processing that block @@ -269,7 +269,7 @@ export function getBeaconPoolApi({ await Promise.all( payloadAttestationMessages.map(async (payloadAttestationMessage, i) => { try { - const validateFn = () => validateApiPayloadAttestationMessage(chain, payloadAttestationMessage); + const validateFn = () => chain.beaconEngine.validateApiPayloadAttestationMessage(payloadAttestationMessage); const {slot, beaconBlockRoot} = payloadAttestationMessage.data; const {attDataRootHex, validatorCommitteeIndices} = await validateGossipFnRetryUnknownRoot( validateFn, @@ -365,7 +365,7 @@ export function getBeaconPoolApi({ // Verify signature only, all other data is very likely to be correct, since the `signature` object is created by this node. // Worst case if `signature` is not valid, gossip peers will drop it and slightly downscore us. - await validateApiSyncCommittee(chain, state, signature); + await chain.beaconEngine.validateApiSyncCommittee(state, signature); // The same validator can appear multiple times in the sync committee. It can appear multiple times per // subnet even. First compute on which subnet the signature must be broadcasted to. diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 54516c7c2902..dc439263be19 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -73,8 +73,6 @@ import {PREPARE_NEXT_SLOT_BPS} from "../../../chain/prepareNextSlot.js"; import {BlockType, ProduceFullDeneb, ProduceFullGloas} from "../../../chain/produceBlock/index.js"; import {RegenCaller} from "../../../chain/regen/index.js"; import {CheckpointHex} from "../../../chain/stateCache/types.js"; -import {validateApiAggregateAndProof} from "../../../chain/validation/index.js"; -import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/validation/syncCommitteeContributionAndProof.js"; import {ZERO_HASH} from "../../../constants/index.js"; import {BuilderStatus, NoBidReceived} from "../../../execution/builder/http.js"; import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js"; @@ -1535,7 +1533,7 @@ export function getValidatorApi( signedAggregateAndProofs.map(async (signedAggregateAndProof, i) => { try { // TODO: Validate in batch - const validateFn = () => validateApiAggregateAndProof(fork, chain, signedAggregateAndProof); + const validateFn = () => chain.beaconEngine.validateApiAggregateAndProof(fork, signedAggregateAndProof); const {slot, beaconBlockRoot} = signedAggregateAndProof.message.aggregate.data; // when a validator is configured with multiple beacon node urls, this attestation may come from another beacon node // and the block hasn't been in our forkchoice since we haven't seen / processing that block @@ -1594,11 +1592,14 @@ export function getValidatorApi( contributionAndProofs.map(async (contributionAndProof, i) => { try { // TODO: Validate in batch - const {syncCommitteeParticipantIndices} = await validateSyncCommitteeGossipContributionAndProof( - chain, - contributionAndProof, - true // skip known participants check - ); + // TODO - engine: get bytes from api + const contributionBytes = ssz.altair.SignedContributionAndProof.serialize(contributionAndProof); + const {syncCommitteeParticipantIndices} = + await chain.beaconEngine.validateSyncCommitteeGossipContributionAndProof( + contributionBytes, + contributionAndProof, + true // skip known participants check + ); const insertOutcome = chain.syncContributionAndProofPool.add( contributionAndProof.message, syncCommitteeParticipantIndices.length, diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 9823f8fe7604..7897255da0ba 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -1,33 +1,96 @@ import {BeaconConfig} from "@lodestar/config"; -import {CheckpointWithHex, ForkChoiceStateGetter, IForkChoice} from "@lodestar/fork-choice"; -import {EffectiveBalanceIncrements, IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition"; +import {CheckpointWithHex, ForkChoiceStateGetter, IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; +import {ForkName} from "@lodestar/params"; +import { + EffectiveBalanceIncrements, + EpochShuffling, + IBeaconStateView, + PubkeyCache, + computeEpochAtSlot, + computeStartSlotAtEpoch, +} from "@lodestar/state-transition"; +import { + Epoch, + RootHex, + SignedAggregateAndProof, + SignedBeaconBlock, + SubnetID, + ValidatorIndex, + altair, + deneb, + fulu, + gloas, +} from "@lodestar/types"; import {Logger, toRootHex} from "@lodestar/utils"; import {Metrics} from "../../metrics/index.js"; +import {IClock} from "../../util/clock.js"; import {CheckpointBalancesCache} from "../balancesCache.js"; +import {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "../bls/index.js"; import {initializeForkChoice} from "../forkChoice/index.js"; import { AggregatedAttestationPool, AttestationPool, + ExecutionPayloadBidPool, OpPool, PayloadAttestationPool, + ProposerPreferencesPool, SyncCommitteeMessagePool, SyncContributionAndProofPool, } from "../opPools/index.js"; -import {QueuedStateRegenerator} from "../regen/index.js"; +import {QueuedStateRegenerator, RegenCaller} from "../regen/index.js"; import { SeenAggregators, SeenAttesters, + SeenBlockInput, + SeenBlockProposers, SeenContributionAndProof, + SeenExecutionPayloadBids, SeenPayloadAttesters, + SeenPayloadEnvelopeInput, + SeenProposerPreferences, SeenSyncCommitteeMessages, } from "../seenCache/index.js"; +import {SeenAggregatedAttestations} from "../seenCache/seenAggregateAndProof.js"; import {SeenAttestationDatas} from "../seenCache/seenAttestationData.js"; import {ShufflingCache} from "../shufflingCache.js"; import {FIFOBlockStateCache} from "../stateCache/fifoBlockStateCache.js"; import {PersistentCheckpointStateCache} from "../stateCache/persistentCheckpointsCache.js"; import {BlockStateCache, CheckpointStateCache} from "../stateCache/types.js"; +import { + AggregateAndProofValidationResult, + validateApiAggregateAndProof, + validateGossipAggregateAndProof, +} from "../validation/aggregateAndProof.js"; +import { + ApiAttestation, + AttestationValidationResult, + BatchResult, + GossipAttestation, + validateApiAttestation, + validateGossipAttestationsSameAttData, +} from "../validation/attestation.js"; +import {validateGossipBlobSidecar} from "../validation/blobSidecar.js"; +import {GossipBlockValidationResult, validateGossipBlock} from "../validation/block.js"; +import { + validateGossipFuluDataColumnSidecar, + validateGossipGloasDataColumnSidecar, +} from "../validation/dataColumnSidecar.js"; +import {validateApiExecutionPayloadBid, validateGossipExecutionPayloadBid} from "../validation/executionPayloadBid.js"; +import { + validateApiExecutionPayloadEnvelope, + validateGossipExecutionPayloadEnvelope, +} from "../validation/executionPayloadEnvelope.js"; +import { + PayloadAttestationValidationResult, + validateApiPayloadAttestationMessage, + validateGossipPayloadAttestationMessage, +} from "../validation/payloadAttestationMessage.js"; +import {validateGossipProposerPreferences} from "../validation/proposerPreferences.js"; +import {validateApiSyncCommittee, validateGossipSyncCommittee} from "../validation/syncCommittee.js"; +import {validateSyncCommitteeGossipContributionAndProof} from "../validation/syncCommitteeContributionAndProof.js"; import {BeaconEngineModules, IBeaconEngine} from "./interface.js"; +import {IBeaconEngineOptions} from "./options.js"; /** * JS implementation of the consensus engine. Transitional in Phase 0: constructed inside @@ -37,14 +100,18 @@ import {BeaconEngineModules, IBeaconEngine} from "./interface.js"; */ export class BeaconEngine implements IBeaconEngine { readonly config: BeaconConfig; + readonly opts: IBeaconEngineOptions; private readonly logger: Logger; - private readonly metrics: Metrics | null; + readonly metrics: Metrics | null; + readonly clock: IClock; + readonly pubkeyCache: PubkeyCache; readonly bls: IBlsVerifier; readonly shufflingCache: ShufflingCache; readonly blockStateCache: BlockStateCache; readonly checkpointStateCache: CheckpointStateCache; readonly checkpointBalancesCache: CheckpointBalancesCache; + // TODO - beacon engine: implement IForkchoiceRead interface readonly forkChoice: IForkChoice; readonly regen: QueuedStateRegenerator; @@ -54,6 +121,8 @@ export class BeaconEngine implements IBeaconEngine { readonly syncCommitteeMessagePool: SyncCommitteeMessagePool; readonly syncContributionAndProofPool: SyncContributionAndProofPool; readonly payloadAttestationPool: PayloadAttestationPool; + readonly executionPayloadBidPool = new ExecutionPayloadBidPool(); + readonly proposerPreferencesPool = new ProposerPreferencesPool(); readonly opPool: OpPool; // Consensus gossip seen-caches @@ -63,6 +132,16 @@ export class BeaconEngine implements IBeaconEngine { readonly seenSyncCommitteeMessages = new SeenSyncCommitteeMessages(); readonly seenContributionAndProof: SeenContributionAndProof; readonly seenAttestationDatas: SeenAttestationDatas; + readonly seenBlockProposers = new SeenBlockProposers(); + readonly seenAggregatedAttestations: SeenAggregatedAttestations; + readonly seenExecutionPayloadBids = new SeenExecutionPayloadBids(); + readonly seenProposerPreferences = new SeenProposerPreferences(); + + // Facade-owned (DA assembly), shared with the engine. `seenBlockInputCache` is injected via the + // constructor; `seenPayloadEnvelopeInputCache` is assigned by `BeaconChain` post-construction because + // it depends on the engine's own `forkChoice`. TODO - beacon engine: revisit ownership in a later phase. + readonly seenBlockInputCache: SeenBlockInput; + seenPayloadEnvelopeInputCache!: SeenPayloadEnvelopeInput; constructor(modules: BeaconEngineModules, anchorState: IBeaconStateView) { const { @@ -82,8 +161,12 @@ export class BeaconEngine implements IBeaconEngine { isAnchorStateFinalized, } = modules; this.config = config; + this.opts = opts; this.logger = logger; this.metrics = metrics; + this.clock = clock; + this.pubkeyCache = pubkeyCache; + this.seenBlockInputCache = seenBlockInputCache; // by default, verify signatures on both main threads and worker threads this.bls = opts.blsVerifyAllMainThread @@ -114,6 +197,7 @@ export class BeaconEngine implements IBeaconEngine { this.seenContributionAndProof = new SeenContributionAndProof(metrics); this.seenAttestationDatas = new SeenAttestationDatas(metrics, opts?.attDataCacheSlotDistance); + this.seenAggregatedAttestations = new SeenAggregatedAttestations(metrics); this.blockStateCache = new FIFOBlockStateCache(opts, {metrics}); this.checkpointStateCache = new PersistentCheckpointStateCache( @@ -164,6 +248,184 @@ export class BeaconEngine implements IBeaconEngine { }); } + getHeadState(): IBeaconStateView { + // head state should always exist + const head = this.forkChoice.getHead(); + const headState = this.regen.getClosestHeadState(head); + if (!headState) { + throw Error(`headState does not exist for head root=${head.blockRoot} slot=${head.slot}`); + } + return headState; + } + + /** + * Regenerate state for attestation verification, this does not happen with default chain option of maxSkipSlots = 32 . + * However, need to handle just in case. Lodestar doesn't support multiple regen state requests for attestation verification + * at the same time, bounded inside "ShufflingCache.insertPromise()" function. + * Leave this function in chain instead of attestatation verification code to make sure we're aware of its performance impact. + */ + async regenStateForAttestationVerification( + attEpoch: Epoch, + shufflingDependentRoot: RootHex, + attHeadBlock: ProtoBlock, + regenCaller: RegenCaller + ): Promise { + // this is to prevent multiple calls to get shuffling for the same epoch and dependent root + // any subsequent calls of the same epoch and dependent root will wait for this promise to resolve + this.shufflingCache.insertPromise(attEpoch, shufflingDependentRoot); + const blockEpoch = computeEpochAtSlot(attHeadBlock.slot); + + let state: IBeaconStateView; + if (blockEpoch < attEpoch - 1) { + // thanks to one epoch look ahead, we don't need to dial up to attEpoch + const targetSlot = computeStartSlotAtEpoch(attEpoch - 1); + this.metrics?.gossipAttestation.useHeadBlockStateDialedToTargetEpoch.inc({caller: regenCaller}); + state = await this.regen.getBlockSlotState(attHeadBlock, targetSlot, {dontTransferCache: true}, regenCaller); + } else if (blockEpoch > attEpoch) { + // should not happen, handled inside attestation verification code + throw Error(`Block epoch ${blockEpoch} is after attestation epoch ${attEpoch}`); + } else { + // should use either current or next shuffling of head state + // it's not likely to hit this since these shufflings are cached already + // so handle just in case + this.metrics?.gossipAttestation.useHeadBlockState.inc({caller: regenCaller}); + state = await this.regen.getState(attHeadBlock.stateRoot, regenCaller); + } + // resolve the promise to unblock other calls of the same epoch and dependent root + this.shufflingCache.processState(state); + return state.getShufflingAtEpoch(attEpoch); + } + + // Gossip validation flows. Each method takes the message's SSZ bytes first (unused by this JS impl; + // present for the native engine's bytes-first contract) and delegates to the validation logic in + // `../validation/*` rebound onto the engine. + validateGossipBlock( + _blockBytes: Uint8Array, + signedBlock: SignedBeaconBlock, + fork: ForkName + ): Promise { + return validateGossipBlock(this, signedBlock, fork); + } + + validateGossipSyncCommittee( + _syncCommitteeBytes: Uint8Array, + syncCommittee: altair.SyncCommitteeMessage, + subnet: SubnetID + ): Promise<{indicesInSubcommittee: number[]}> { + return validateGossipSyncCommittee(this, syncCommittee, subnet); + } + + validateApiSyncCommittee(headState: IBeaconStateView, syncCommittee: altair.SyncCommitteeMessage): Promise { + return validateApiSyncCommittee(this, headState, syncCommittee); + } + + validateSyncCommitteeGossipContributionAndProof( + _contributionBytes: Uint8Array, + signedContributionAndProof: altair.SignedContributionAndProof, + skipValidationKnownParticipants = false + ): Promise<{syncCommitteeParticipantIndices: ValidatorIndex[]}> { + return validateSyncCommitteeGossipContributionAndProof( + this, + signedContributionAndProof, + skipValidationKnownParticipants + ); + } + + validateGossipBlobSidecar( + _blobBytes: Uint8Array, + fork: ForkName, + blobSidecar: deneb.BlobSidecar, + subnet: SubnetID + ): Promise { + return validateGossipBlobSidecar(this, fork, blobSidecar, subnet); + } + + validateGossipFuluDataColumnSidecar( + _dataColumnBytes: Uint8Array, + dataColumnSidecar: fulu.DataColumnSidecar, + gossipSubnet: SubnetID + ): Promise { + return validateGossipFuluDataColumnSidecar(this, dataColumnSidecar, gossipSubnet, this.metrics); + } + + validateGossipGloasDataColumnSidecar( + _dataColumnBytes: Uint8Array, + payloadInput: PayloadEnvelopeInput, + dataColumnSidecar: gloas.DataColumnSidecar, + gossipSubnet: SubnetID + ): Promise { + return validateGossipGloasDataColumnSidecar(this, payloadInput, dataColumnSidecar, gossipSubnet, this.metrics); + } + + validateGossipPayloadAttestationMessage( + _payloadAttestationBytes: Uint8Array, + payloadAttestationMessage: gloas.PayloadAttestationMessage + ): Promise { + return validateGossipPayloadAttestationMessage(this, payloadAttestationMessage); + } + + validateApiPayloadAttestationMessage( + payloadAttestationMessage: gloas.PayloadAttestationMessage + ): Promise { + return validateApiPayloadAttestationMessage(this, payloadAttestationMessage); + } + + // The batch attestation validator's per-item bytes live on each `GossipAttestation.serializedData`, + // so there is no separate leading bytes parameter here. + validateGossipAttestationsSameAttData(fork: ForkName, attestations: GossipAttestation[]): Promise { + return validateGossipAttestationsSameAttData(fork, this, attestations); + } + + validateApiAttestation(fork: ForkName, attestationOrBytes: ApiAttestation): Promise { + return validateApiAttestation(fork, this, attestationOrBytes); + } + + validateGossipAggregateAndProof( + aggregateBytes: Uint8Array, + fork: ForkName, + signedAggregateAndProof: SignedAggregateAndProof + ): Promise { + return validateGossipAggregateAndProof(fork, this, signedAggregateAndProof, aggregateBytes); + } + + validateApiAggregateAndProof( + fork: ForkName, + signedAggregateAndProof: SignedAggregateAndProof + ): Promise { + return validateApiAggregateAndProof(fork, this, signedAggregateAndProof); + } + + validateGossipExecutionPayloadEnvelope( + _envelopeBytes: Uint8Array, + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope + ): Promise { + return validateGossipExecutionPayloadEnvelope(this, executionPayloadEnvelope); + } + + validateApiExecutionPayloadEnvelope(executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise { + return validateApiExecutionPayloadEnvelope(this, executionPayloadEnvelope); + } + + validateGossipExecutionPayloadBid( + _bidBytes: Uint8Array, + signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid + ): Promise<{proposerIndex: ValidatorIndex}> { + return validateGossipExecutionPayloadBid(this, signedExecutionPayloadBid); + } + + validateApiExecutionPayloadBid( + signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid + ): Promise<{proposerIndex: ValidatorIndex}> { + return validateApiExecutionPayloadBid(this, signedExecutionPayloadBid); + } + + validateGossipProposerPreferences( + _preferencesBytes: Uint8Array, + signedProposerPreferences: gloas.SignedProposerPreferences + ): Promise { + return validateGossipProposerPreferences(this, signedProposerPreferences); + } + // TODO - beacon engine: scalar state reads (getBeaconProposer, getValidator, getBalance, // getRandaoMix, getBlockRootAtSlot, getStateRootAtSlot, getShufflingDecisionRoot). Deferred — // signature shape (state vs stateRoot) to be decided alongside Phase 4 bytes-first. diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 1a66472d3e4d..bb97570e1521 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -1,14 +1,35 @@ import {BeaconConfig} from "@lodestar/config"; import {IForkChoice} from "@lodestar/fork-choice"; -import {PubkeyCache} from "@lodestar/state-transition"; +import {ForkName} from "@lodestar/params"; +import {IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; +import { + SignedAggregateAndProof, + SignedBeaconBlock, + SubnetID, + ValidatorIndex, + altair, + deneb, + fulu, + gloas, +} from "@lodestar/types"; import {Logger} from "@lodestar/utils"; import {IBeaconDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; import {BufferPool} from "../../util/bufferPool.js"; import {IClock} from "../../util/clock.js"; +import {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; import {ChainEventEmitter} from "../emitter.js"; import {SeenBlockInput} from "../seenCache/seenGossipBlockInput.js"; import {CPStateDatastore} from "../stateCache/datastore/types.js"; +import {AggregateAndProofValidationResult} from "../validation/aggregateAndProof.js"; +import { + ApiAttestation, + AttestationValidationResult, + BatchResult, + GossipAttestation, +} from "../validation/attestation.js"; +import {GossipBlockValidationResult} from "../validation/block.js"; +import {PayloadAttestationValidationResult} from "../validation/payloadAttestationMessage.js"; import {ValidatorMonitor} from "../validatorMonitor.js"; import {IBeaconEngineOptions} from "./options.js"; @@ -41,4 +62,74 @@ export interface IBeaconEngine { // perform them still live facade-side. TODO - beacon engine: narrow to a read facet as those flows // (gossip → Phase 3, migrateFinalized/prune → Phase 5) move into the engine. readonly forkChoice: IForkChoice; + + // Gossip validation flows. The first parameter is the message's SSZ bytes (unused by the JS engine, + // required by the native engine's bytes-first contract), followed by the deserialized object. + validateGossipBlock( + blockBytes: Uint8Array, + signedBlock: SignedBeaconBlock, + fork: ForkName + ): Promise; + validateGossipSyncCommittee( + syncCommitteeBytes: Uint8Array, + syncCommittee: altair.SyncCommitteeMessage, + subnet: SubnetID + ): Promise<{indicesInSubcommittee: number[]}>; + validateApiSyncCommittee(headState: IBeaconStateView, syncCommittee: altair.SyncCommitteeMessage): Promise; + validateSyncCommitteeGossipContributionAndProof( + contributionBytes: Uint8Array, + signedContributionAndProof: altair.SignedContributionAndProof, + skipValidationKnownParticipants?: boolean + ): Promise<{syncCommitteeParticipantIndices: ValidatorIndex[]}>; + validateGossipBlobSidecar( + blobBytes: Uint8Array, + fork: ForkName, + blobSidecar: deneb.BlobSidecar, + subnet: SubnetID + ): Promise; + validateGossipFuluDataColumnSidecar( + dataColumnBytes: Uint8Array, + dataColumnSidecar: fulu.DataColumnSidecar, + gossipSubnet: SubnetID + ): Promise; + validateGossipGloasDataColumnSidecar( + dataColumnBytes: Uint8Array, + payloadInput: PayloadEnvelopeInput, + dataColumnSidecar: gloas.DataColumnSidecar, + gossipSubnet: SubnetID + ): Promise; + validateGossipPayloadAttestationMessage( + payloadAttestationBytes: Uint8Array, + payloadAttestationMessage: gloas.PayloadAttestationMessage + ): Promise; + validateApiPayloadAttestationMessage( + payloadAttestationMessage: gloas.PayloadAttestationMessage + ): Promise; + validateGossipAttestationsSameAttData(fork: ForkName, attestations: GossipAttestation[]): Promise; + validateApiAttestation(fork: ForkName, attestationOrBytes: ApiAttestation): Promise; + validateGossipAggregateAndProof( + aggregateBytes: Uint8Array, + fork: ForkName, + signedAggregateAndProof: SignedAggregateAndProof + ): Promise; + validateApiAggregateAndProof( + fork: ForkName, + signedAggregateAndProof: SignedAggregateAndProof + ): Promise; + validateGossipExecutionPayloadEnvelope( + envelopeBytes: Uint8Array, + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope + ): Promise; + validateApiExecutionPayloadEnvelope(executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise; + validateGossipExecutionPayloadBid( + bidBytes: Uint8Array, + signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid + ): Promise<{proposerIndex: ValidatorIndex}>; + validateApiExecutionPayloadBid( + signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid + ): Promise<{proposerIndex: ValidatorIndex}>; + validateGossipProposerPreferences( + preferencesBytes: Uint8Array, + signedProposerPreferences: gloas.SignedProposerPreferences + ): Promise; } diff --git a/packages/beacon-node/src/chain/beaconEngine/options.ts b/packages/beacon-node/src/chain/beaconEngine/options.ts index 897ce6f4444e..efe3be9e533a 100644 --- a/packages/beacon-node/src/chain/beaconEngine/options.ts +++ b/packages/beacon-node/src/chain/beaconEngine/options.ts @@ -30,4 +30,8 @@ export type IBeaconEngineOptions = ShufflingCacheOpts & ForkChoiceOpts & BlsMultiThreadWorkerPoolOptions & { blsVerifyAllMainThread?: boolean; + /** Gossip block/blob/data-column validation observes (no longer gates) skipped slots */ + maxSkipSlots?: number; + /** Min number of same-message signature sets to batch in gossip attestation validation */ + minSameMessageSignatureSetsToBatch: number; }; diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index c986a288b6ad..814e65cc1e60 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -13,7 +13,6 @@ import { isForkPostGloas, } from "@lodestar/params"; import { - EpochShuffling, IBeaconStateView, PubkeyCache, computeEndSlotAtEpoch, @@ -194,11 +193,17 @@ export class BeaconChain implements IBeaconChain { get syncContributionAndProofPool(): SyncContributionAndProofPool { return this.beaconEngine.syncContributionAndProofPool; } - readonly executionPayloadBidPool: ExecutionPayloadBidPool; + // TODO - beacon engine: remove this + get executionPayloadBidPool(): ExecutionPayloadBidPool { + return this.beaconEngine.executionPayloadBidPool; + } get payloadAttestationPool(): PayloadAttestationPool { return this.beaconEngine.payloadAttestationPool; } - readonly proposerPreferencesPool = new ProposerPreferencesPool(); + // TODO - beacon engine: remove this + get proposerPreferencesPool(): ProposerPreferencesPool { + return this.beaconEngine.proposerPreferencesPool; + } get opPool(): OpPool { return this.beaconEngine.opPool; } @@ -213,10 +218,22 @@ export class BeaconChain implements IBeaconChain { get seenPayloadAttesters(): SeenPayloadAttesters { return this.beaconEngine.seenPayloadAttesters; } - readonly seenAggregatedAttestations: SeenAggregatedAttestations; - readonly seenExecutionPayloadBids = new SeenExecutionPayloadBids(); - readonly seenProposerPreferences = new SeenProposerPreferences(); - readonly seenBlockProposers = new SeenBlockProposers(); + // TODO - beacon engine: remove this + get seenAggregatedAttestations(): SeenAggregatedAttestations { + return this.beaconEngine.seenAggregatedAttestations; + } + // TODO - beacon engine: remove this + get seenExecutionPayloadBids(): SeenExecutionPayloadBids { + return this.beaconEngine.seenExecutionPayloadBids; + } + // TODO - beacon engine: remove this + get seenProposerPreferences(): SeenProposerPreferences { + return this.beaconEngine.seenProposerPreferences; + } + // TODO - beacon engine: remove this + get seenBlockProposers(): SeenBlockProposers { + return this.beaconEngine.seenBlockProposers; + } get seenSyncCommitteeMessages(): SeenSyncCommitteeMessages { return this.beaconEngine.seenSyncCommitteeMessages; } @@ -385,9 +402,6 @@ export class BeaconChain implements IBeaconChain { ); this.blacklistedBlocks = new Map((opts.blacklistedBlocks ?? []).map((hex) => [hex, null])); - this.executionPayloadBidPool = new ExecutionPayloadBidPool(); - - this.seenAggregatedAttestations = new SeenAggregatedAttestations(metrics); this.beaconProposerCache = new BeaconProposerCache(opts); @@ -418,6 +432,10 @@ export class BeaconChain implements IBeaconChain { metrics, logger, }); + // Facade-owned (DA assembly) but shared with the engine for gossip validation. Injected here (not via + // the engine constructor) because it depends on the engine's own forkChoice. + // TODO - beacon engine: remove this once ownership is settled in a later phase. + this.beaconEngine.seenPayloadEnvelopeInputCache = this.seenPayloadEnvelopeInputCache; const anchorBlockSlot = anchorState.latestBlockHeader.slot; if (isStatePostGloas(anchorState) && anchorBlockSlot > 0) { @@ -559,6 +577,7 @@ export class BeaconChain implements IBeaconChain { await this.opPool.toPersisted(this.db); } + // TODO - beacon engine: remove this getHeadState(): IBeaconStateView { // head state should always exist const head = this.forkChoice.getHead(); @@ -1272,44 +1291,6 @@ export class BeaconChain implements IBeaconChain { } } - /** - * Regenerate state for attestation verification, this does not happen with default chain option of maxSkipSlots = 32 . - * However, need to handle just in case. Lodestar doesn't support multiple regen state requests for attestation verification - * at the same time, bounded inside "ShufflingCache.insertPromise()" function. - * Leave this function in chain instead of attestatation verification code to make sure we're aware of its performance impact. - */ - async regenStateForAttestationVerification( - attEpoch: Epoch, - shufflingDependentRoot: RootHex, - attHeadBlock: ProtoBlock, - regenCaller: RegenCaller - ): Promise { - // this is to prevent multiple calls to get shuffling for the same epoch and dependent root - // any subsequent calls of the same epoch and dependent root will wait for this promise to resolve - this.shufflingCache.insertPromise(attEpoch, shufflingDependentRoot); - const blockEpoch = computeEpochAtSlot(attHeadBlock.slot); - - let state: IBeaconStateView; - if (blockEpoch < attEpoch - 1) { - // thanks to one epoch look ahead, we don't need to dial up to attEpoch - const targetSlot = computeStartSlotAtEpoch(attEpoch - 1); - this.metrics?.gossipAttestation.useHeadBlockStateDialedToTargetEpoch.inc({caller: regenCaller}); - state = await this.regen.getBlockSlotState(attHeadBlock, targetSlot, {dontTransferCache: true}, regenCaller); - } else if (blockEpoch > attEpoch) { - // should not happen, handled inside attestation verification code - throw Error(`Block epoch ${blockEpoch} is after attestation epoch ${attEpoch}`); - } else { - // should use either current or next shuffling of head state - // it's not likely to hit this since these shufflings are cached already - // so handle just in case - this.metrics?.gossipAttestation.useHeadBlockState.inc({caller: regenCaller}); - state = await this.regen.getState(attHeadBlock.stateRoot, regenCaller); - } - // resolve the promise to unblock other calls of the same epoch and dependent root - this.shufflingCache.processState(state); - return state.getShufflingAtEpoch(attEpoch); - } - private async persistSszObject(prefix: string, bytes: Uint8Array, root: Uint8Array, logStr?: string): Promise { const now = new Date(); // yyyy-MM-dd diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 48cc396af87e..0843c058c256 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -1,7 +1,7 @@ import {Type} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; import {CheckpointWithHex, IForkChoiceRead, ProtoBlock} from "@lodestar/fork-choice"; -import {EpochShuffling, IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; +import {IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; import { BeaconBlock, BlindedBeaconBlock, @@ -175,6 +175,7 @@ export interface IBeaconChain { validatorSeenAtEpoch(index: ValidatorIndex, epoch: Epoch): boolean; + // TODO - beacon engine: remove getHeadState* getHeadState(): IBeaconStateView; getHeadStateAtCurrentEpoch(regenCaller: RegenCaller): Promise; getHeadStateAtEpoch(epoch: Epoch, regenCaller: RegenCaller): Promise; @@ -286,12 +287,6 @@ export interface IBeaconChain { ): Promise; persistInvalidSszValue(type: Type, sszObject: T | Uint8Array, suffix?: string): void; persistInvalidSszBytes(type: string, sszBytes: Uint8Array, suffix?: string): void; - regenStateForAttestationVerification( - attEpoch: Epoch, - shufflingDependentRoot: RootHex, - attHeadBlock: ProtoBlock, - regenCaller: RegenCaller - ): Promise; updateBuilderStatus(clockSlot: Slot): void; regenCanAcceptWork(): boolean; diff --git a/packages/beacon-node/src/chain/options.ts b/packages/beacon-node/src/chain/options.ts index 6d7570d0f846..31781147a5e2 100644 --- a/packages/beacon-node/src/chain/options.ts +++ b/packages/beacon-node/src/chain/options.ts @@ -26,7 +26,6 @@ export type IChainOptions = IBeaconEngineOptions & persistOrphanedBlocksDir?: string; skipCreateStateCacheIfAvailable?: boolean; suggestedFeeRecipient: string; - maxSkipSlots?: number; /** Ensure blobs returned by the execution engine are valid */ sanityCheckExecutionEngineBlobs?: boolean; /** Max number of produced blobs by local validators to cache */ @@ -35,7 +34,6 @@ export type IChainOptions = IBeaconEngineOptions & maxCachedProducedRoots?: number; initialCustodyGroupCount?: number; broadcastValidationStrictness?: string; - minSameMessageSignatureSetsToBatch: number; archiveDateEpochs?: number; nHistoricalStatesFileDataStore?: boolean; nativeStateView?: boolean; diff --git a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts index 25cb272cb8fa..1512e1e9f193 100644 --- a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts @@ -6,8 +6,8 @@ import { } from "@lodestar/state-transition"; import {IndexedAttestation, RootHex, SignedAggregateAndProof, electra, ssz} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/index.js"; -import {IBeaconChain} from "../index.js"; import {RegenCaller} from "../regen/index.js"; import { getAttestationDataSigningRoot, @@ -27,12 +27,12 @@ export type AggregateAndProofValidationResult = { export async function validateApiAggregateAndProof( fork: ForkName, - chain: IBeaconChain, + engine: BeaconEngine, signedAggregateAndProof: SignedAggregateAndProof ): Promise { const skipValidationKnownAttesters = true; const prioritizeBls = true; - return validateAggregateAndProof(fork, chain, signedAggregateAndProof, null, { + return validateAggregateAndProof(fork, engine, signedAggregateAndProof, null, { skipValidationKnownAttesters, prioritizeBls, }); @@ -40,16 +40,16 @@ export async function validateApiAggregateAndProof( export async function validateGossipAggregateAndProof( fork: ForkName, - chain: IBeaconChain, + engine: BeaconEngine, signedAggregateAndProof: SignedAggregateAndProof, serializedData: Uint8Array ): Promise { - return validateAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData); + return validateAggregateAndProof(fork, engine, signedAggregateAndProof, serializedData); } async function validateAggregateAndProof( fork: ForkName, - chain: IBeaconChain, + engine: BeaconEngine, signedAggregateAndProof: SignedAggregateAndProof, serializedData: Uint8Array | null = null, opts: {skipValidationKnownAttesters: boolean; prioritizeBls: boolean} = { @@ -81,7 +81,7 @@ async function validateAggregateAndProof( }); } // [REJECT] `aggregate.data.index == 0` if `block.slot == aggregate.data.slot`. - const block = chain.forkChoice.getBlockDefaultStatus(attData.beaconBlockRoot); + const block = engine.forkChoice.getBlockDefaultStatus(attData.beaconBlockRoot); // If block is unknown, we don't handle it here. It will throw error later on at `verifyHeadBlockAndTargetRoot()` if (block !== null && block.slot === attData.slot && attData.index !== 0) { @@ -96,7 +96,13 @@ async function validateAggregateAndProof( // the corresponding execution payload for `block` has been seen (a client MAY queue // attestations for processing once the payload is retrieved and SHOULD request the // payload envelope via `ExecutionPayloadEnvelopesByRoot`). - if (block !== null && attData.index === 1 && !chain.seenPayloadEnvelope(toRootHex(attData.beaconBlockRoot))) { + // TODO - beacon engine: unstable uses engine.seenPayloadEnvelope() which also checks the facade-owned + // seenPayloadEnvelopeInputCache; the engine has only the forkChoice branch. + if ( + block !== null && + attData.index === 1 && + !engine.forkChoice.hasPayloadHexUnsafe(toRootHex(attData.beaconBlockRoot)) + ) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.EXECUTION_PAYLOAD_NOT_SEEN, beaconBlockRoot: toRootHex(attData.beaconBlockRoot), @@ -123,15 +129,17 @@ async function validateAggregateAndProof( } const seenAttDataKey = serializedData ? getSeenAttDataKeyFromSignedAggregateAndProof(fork, serializedData) : null; - const cachedAttData = seenAttDataKey ? chain.seenAttestationDatas.get(attSlot, committeeIndex, seenAttDataKey) : null; + const cachedAttData = seenAttDataKey + ? engine.seenAttestationDatas.get(attSlot, committeeIndex, seenAttDataKey) + : null; const attEpoch = computeEpochAtSlot(attSlot); const attTarget = attData.target; const targetEpoch = attTarget.epoch; - chain.metrics?.gossipAttestation.attestationSlotToClockSlot.observe( + engine.metrics?.gossipAttestation.attestationSlotToClockSlot.observe( {caller: RegenCaller.validateGossipAggregateAndProof}, - chain.clock.currentSlot - attSlot + engine.clock.currentSlot - attSlot ); if (!cachedAttData) { @@ -151,13 +159,13 @@ async function validateAggregateAndProof( // [IGNORE] the epoch of `aggregate.data.slot` is either the current or previous epoch // (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) // -- i.e. `compute_epoch_at_slot(aggregate.data.slot) in (get_previous_epoch(state), get_current_epoch(state))` - verifyPropagationSlotRange(fork, chain, attSlot); + verifyPropagationSlotRange(fork, engine, attSlot); } // [IGNORE] The aggregate is the first valid aggregate received for the aggregator with // index aggregate_and_proof.aggregator_index for the epoch aggregate.data.target.epoch. const aggregatorIndex = aggregateAndProof.aggregatorIndex; - if (chain.seenAggregators.isKnown(targetEpoch, aggregatorIndex)) { + if (engine.seenAggregators.isKnown(targetEpoch, aggregatorIndex)) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.AGGREGATOR_ALREADY_KNOWN, targetEpoch, @@ -172,7 +180,7 @@ async function validateAggregateAndProof( : toRootHex(ssz.phase0.AttestationData.hashTreeRoot(attData)); if ( !skipValidationKnownAttesters && - chain.seenAggregatedAttestations.isKnown(targetEpoch, committeeIndex, attDataRootHex, aggregationBits) + engine.seenAggregatedAttestations.isKnown(targetEpoch, committeeIndex, attDataRootHex, aggregationBits) ) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.ATTESTERS_ALREADY_KNOWN, @@ -189,21 +197,21 @@ async function validateAggregateAndProof( // [REJECT] The aggregate attestation's target block is an ancestor of the block named in the LMD vote // -- i.e. `get_checkpoint_block(store, aggregate.data.beacon_block_root, aggregate.data.target.epoch) == aggregate.data.target.root` const attHeadBlock = verifyHeadBlockAndTargetRoot( - chain, + engine, attData.beaconBlockRoot, attTarget.root, attSlot, attEpoch, RegenCaller.validateGossipAggregateAndProof, - chain.opts.maxSkipSlots + engine.opts.maxSkipSlots ); // [IGNORE] The current finalized_checkpoint is an ancestor of the block defined by aggregate.data.beacon_block_root // -- i.e. get_ancestor(store, aggregate.data.beacon_block_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == store.finalized_checkpoint.root - // > Altready check in `chain.forkChoice.hasBlock(attestation.data.beaconBlockRoot)` + // > Altready check in `engine.forkChoice.hasBlock(attestation.data.beaconBlockRoot)` const shuffling = await getShufflingForAttestationVerification( - chain, + engine, attEpoch, attHeadBlock, RegenCaller.validateGossipAttestation @@ -252,27 +260,27 @@ async function validateAggregateAndProof( // by the validator with index aggregate_and_proof.aggregator_index. // [REJECT] The aggregator signature, signed_aggregate_and_proof.signature, is valid. // [REJECT] The signature of aggregate is valid. - const signingRoot = cachedAttData ? cachedAttData.signingRoot : getAttestationDataSigningRoot(chain.config, attData); + const signingRoot = cachedAttData ? cachedAttData.signingRoot : getAttestationDataSigningRoot(engine.config, attData); const indexedAttestationSignatureSet = createAggregateSignatureSetFromComponents( indexedAttestation.attestingIndices, signingRoot, indexedAttestation.signature ); const signatureSets = [ - getSelectionProofSignatureSet(chain.config, attSlot, aggregatorIndex, signedAggregateAndProof), - getAggregateAndProofSignatureSet(chain.config, attEpoch, aggregatorIndex, signedAggregateAndProof), + getSelectionProofSignatureSet(engine.config, attSlot, aggregatorIndex, signedAggregateAndProof), + getAggregateAndProofSignatureSet(engine.config, attEpoch, aggregatorIndex, signedAggregateAndProof), indexedAttestationSignatureSet, ]; // no need to write to SeenAttestationDatas - if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { + if (!(await engine.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.INVALID_SIGNATURE}); } // It's important to double check that the attestation still hasn't been observed, since // there can be a race-condition if we receive two attestations at the same time and // process them in different threads. - if (chain.seenAggregators.isKnown(targetEpoch, aggregatorIndex)) { + if (engine.seenAggregators.isKnown(targetEpoch, aggregatorIndex)) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.AGGREGATOR_ALREADY_KNOWN, targetEpoch, @@ -283,7 +291,7 @@ async function validateAggregateAndProof( // Same race-condition check as above for seen aggregators if ( !skipValidationKnownAttesters && - chain.seenAggregatedAttestations.isKnown(targetEpoch, committeeIndex, attDataRootHex, aggregationBits) + engine.seenAggregatedAttestations.isKnown(targetEpoch, committeeIndex, attDataRootHex, aggregationBits) ) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.ATTESTERS_ALREADY_KNOWN, @@ -292,8 +300,8 @@ async function validateAggregateAndProof( }); } - chain.seenAggregators.add(targetEpoch, aggregatorIndex); - chain.seenAggregatedAttestations.add( + engine.seenAggregators.add(targetEpoch, aggregatorIndex); + engine.seenAggregatedAttestations.add( targetEpoch, committeeIndex, attDataRootHex, diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index f3b7b8863f57..6a43e2bac05d 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -49,8 +49,8 @@ import { getSignatureFromSingleAttestationSerialized, } from "../../util/sszBytes.js"; import {Result, wrapError} from "../../util/wrapError.js"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/index.js"; -import {IBeaconChain} from "../interface.js"; import {RegenCaller} from "../regen/index.js"; import { AttestationDataCacheEntry, @@ -102,7 +102,7 @@ export type Step0Result = AttestationValidationResult & { */ export async function validateGossipAttestationsSameAttData( fork: ForkName, - chain: IBeaconChain, + engine: BeaconEngine, attestationOrBytesArr: GossipAttestation[], // for unit test, consumers do not need to pass this step0ValidationFn = validateAttestationNoSignatureCheck @@ -118,7 +118,7 @@ export async function validateGossipAttestationsSameAttData( const step0ResultOrErrors: Result[] = []; for (const attestationOrBytes of attestationOrBytesArr) { const {subnet} = attestationOrBytes; - const resultOrError = await wrapError(step0ValidationFn(fork, chain, attestationOrBytes, subnet)); + const resultOrError = await wrapError(step0ValidationFn(fork, engine, attestationOrBytes, subnet)); step0ResultOrErrors.push(resultOrError); } @@ -139,12 +139,12 @@ export async function validateGossipAttestationsSameAttData( } let signatureValids: boolean[]; - const batchableBls = signatureSets.length >= chain.opts.minSameMessageSignatureSetsToBatch; + const batchableBls = signatureSets.length >= engine.opts.minSameMessageSignatureSetsToBatch; if (batchableBls) { // all signature sets should have same signing root since we filtered in network processor - signatureValids = await chain.bls.verifySignatureSetsSameMessage( + signatureValids = await engine.bls.verifySignatureSetsSameMessage( signatureSets.map((set) => { - const publicKey = chain.pubkeyCache.getOrThrow(set.index); + const publicKey = engine.pubkeyCache.getOrThrow(set.index); return {publicKey, signature: set.signature}; }), signatureSets[0].signingRoot @@ -152,7 +152,7 @@ export async function validateGossipAttestationsSameAttData( } else { // don't want to block the main thread if there are too few signatures signatureValids = await Promise.all( - signatureSets.map((set) => chain.bls.verifySignatureSets([set], {batchable: true})) + signatureSets.map((set) => engine.bls.verifySignatureSets([set], {batchable: true})) ); } @@ -172,7 +172,7 @@ export async function validateGossipAttestationsSameAttData( // It's important to double check that the attestation still hasn't been observed, since // there can be a race-condition if we receive two attestations at the same time and // process them in different threads. - if (chain.seenAttesters.isKnown(targetEpoch, validatorIndex)) { + if (engine.seenAttesters.isKnown(targetEpoch, validatorIndex)) { step0ResultOrErrors[oldIndex] = { err: new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN, @@ -183,7 +183,7 @@ export async function validateGossipAttestationsSameAttData( } // valid - chain.seenAttesters.add(targetEpoch, validatorIndex); + engine.seenAttesters.add(targetEpoch, validatorIndex); } else { step0ResultOrErrors[oldIndex] = { err: new AttestationError(GossipAction.REJECT, { @@ -207,20 +207,20 @@ export async function validateGossipAttestationsSameAttData( */ export async function validateApiAttestation( fork: ForkName, - chain: IBeaconChain, + engine: BeaconEngine, attestationOrBytes: ApiAttestation ): Promise { const prioritizeBls = true; const subnet = null; try { - const step0Result = await validateAttestationNoSignatureCheck(fork, chain, attestationOrBytes, subnet); + const step0Result = await validateAttestationNoSignatureCheck(fork, engine, attestationOrBytes, subnet); const {attestation, signatureSet, validatorIndex} = step0Result; - const isValid = await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}); + const isValid = await engine.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}); if (isValid) { const targetEpoch = attestation.data.target.epoch; - chain.seenAttesters.add(targetEpoch, validatorIndex); + engine.seenAttesters.add(targetEpoch, validatorIndex); return step0Result; } @@ -243,7 +243,7 @@ export async function validateApiAttestation( */ async function validateAttestationNoSignatureCheck( fork: ForkName, - chain: IBeaconChain, + engine: BeaconEngine, attestationOrBytes: AttestationOrBytes, /** Optional, to allow verifying attestations through API with unknown subnet */ subnet: SubnetID | null @@ -270,7 +270,7 @@ async function validateAttestationNoSignatureCheck( ? (getCommitteeIndexFromAttestationOrBytes(fork, attestationOrBytes) ?? 0) : PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX; const cachedAttData = - attDataKey !== null ? chain.seenAttestationDatas.get(attSlot, committeeIndexForLookup, attDataKey) : null; + attDataKey !== null ? engine.seenAttestationDatas.get(attSlot, committeeIndexForLookup, attDataKey) : null; if (cachedAttData === null) { const attestation = sszDeserializeSingleAttestation(fork, attestationOrBytes.serializedData); // only deserialize on the first AttestationData that's not cached @@ -307,7 +307,7 @@ async function validateAttestationNoSignatureCheck( } // [REJECT] `attestation.data.index == 0` if `block.slot == attestation.data.slot`. - const block = chain.forkChoice.getBlockDefaultStatus(attData.beaconBlockRoot); + const block = engine.forkChoice.getBlockDefaultStatus(attData.beaconBlockRoot); // block being null will be handled by `verifyHeadBlockAndTargetRoot` if (block !== null && block.slot === attSlot && attData.index !== 0) { @@ -322,7 +322,13 @@ async function validateAttestationNoSignatureCheck( // the corresponding execution payload for `block` has been seen (a client MAY queue // attestations for processing once the payload is retrieved and SHOULD request the // payload envelope via `ExecutionPayloadEnvelopesByRoot`). - if (block !== null && attData.index === 1 && !chain.seenPayloadEnvelope(toRootHex(attData.beaconBlockRoot))) { + // TODO - beacon engine: unstable uses engine.seenPayloadEnvelope() which also checks the facade-owned + // seenPayloadEnvelopeInputCache; the engine has only the forkChoice branch. + if ( + block !== null && + attData.index === 1 && + !engine.forkChoice.hasPayloadHexUnsafe(toRootHex(attData.beaconBlockRoot)) + ) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.EXECUTION_PAYLOAD_NOT_SEEN, beaconBlockRoot: toRootHex(attData.beaconBlockRoot), @@ -343,9 +349,9 @@ async function validateAttestationNoSignatureCheck( committeeIndex = attestationOrCache.cache.committeeIndex; } - chain.metrics?.gossipAttestation.attestationSlotToClockSlot.observe( + engine.metrics?.gossipAttestation.attestationSlotToClockSlot.observe( {caller: RegenCaller.validateGossipAttestation}, - chain.clock.currentSlot - attSlot + engine.clock.currentSlot - attSlot ); if (!attestationOrCache.cache) { @@ -367,7 +373,7 @@ async function validateAttestationNoSignatureCheck( // [IGNORE] the epoch of `attestation.data.slot` is either the current or previous epoch // (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) // -- i.e. `compute_epoch_at_slot(attestation.data.slot) in (get_previous_epoch(state), get_current_epoch(state))` - verifyPropagationSlotRange(fork, chain, attestationOrCache.attestation.data.slot); + verifyPropagationSlotRange(fork, engine, attestationOrCache.attestation.data.slot); } let aggregationBits: BitArray | null = null; @@ -411,13 +417,13 @@ async function validateAttestationNoSignatureCheck( // [IGNORE] The block being voted for (attestation.data.beacon_block_root) has been seen (via both gossip // and non-gossip sources) (a client MAY queue attestations for processing once block is retrieved). const attHeadBlock = verifyHeadBlockAndTargetRoot( - chain, + engine, attestationOrCache.attestation.data.beaconBlockRoot, attestationOrCache.attestation.data.target.root, attSlot, attEpoch, RegenCaller.validateGossipAttestation, - chain.opts.maxSkipSlots + engine.opts.maxSkipSlots ); // [REJECT] The block being voted for (attestation.data.beacon_block_root) passes validation. @@ -432,7 +438,7 @@ async function validateAttestationNoSignatureCheck( // > Altready check in `verifyHeadBlockAndTargetRoot()` const shuffling = await getShufflingForAttestationVerification( - chain, + engine, attEpoch, attHeadBlock, RegenCaller.validateGossipAttestation @@ -441,7 +447,7 @@ async function validateAttestationNoSignatureCheck( // [REJECT] The committee index is within the expected range // -- i.e. data.index < get_committee_count_per_slot(state, data.target.epoch) committeeValidatorIndices = getCommitteeValidatorIndices(shuffling, attSlot, committeeIndex); - getSigningRoot = () => getAttestationDataSigningRoot(chain.config, attData); + getSigningRoot = () => getAttestationDataSigningRoot(engine.config, attData); expectedSubnet = computeSubnetForSlot(shuffling, attSlot, committeeIndex); } @@ -504,7 +510,7 @@ async function validateAttestationNoSignatureCheck( // [IGNORE] There has been no other valid attestation seen on an attestation subnet that has an // identical attestation.data.target.epoch and participating validator index. - if (chain.seenAttesters.isKnown(targetEpoch, validatorIndex)) { + if (engine.seenAttesters.isKnown(targetEpoch, validatorIndex)) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN, targetEpoch, @@ -545,7 +551,7 @@ async function validateAttestationNoSignatureCheck( const committeeIndexKey = isForkPostElectra(fork) ? committeeIndex : PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX; - chain.seenAttestationDatas.add(attSlot, committeeIndexKey, attDataKey, { + engine.seenAttestationDatas.add(attSlot, committeeIndexKey, attDataKey, { committeeValidatorIndices, committeeIndex, signingRoot: signatureSet.signingRoot, @@ -601,9 +607,11 @@ async function validateAttestationNoSignatureCheck( * Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. * Note: We do not queue future attestations for later processing */ -export function verifyPropagationSlotRange(fork: ForkName, chain: IBeaconChain, attestationSlot: Slot): void { +export function verifyPropagationSlotRange(fork: ForkName, engine: BeaconEngine, attestationSlot: Slot): void { // slot with future tolerance of MAXIMUM_GOSSIP_CLOCK_DISPARITY - const latestPermissibleSlot = chain.clock.slotWithFutureTolerance(chain.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000); + const latestPermissibleSlot = engine.clock.slotWithFutureTolerance( + engine.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000 + ); if (attestationSlot > latestPermissibleSlot) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.FUTURE_SLOT, @@ -617,14 +625,14 @@ export function verifyPropagationSlotRange(fork: ForkName, chain: IBeaconChain, // // see: https://github.com/ethereum/consensus-specs/pull/3360 if (ForkSeq[fork] < ForkSeq.deneb) { - const currentSlot = chain.clock.currentSlot; - const withinPastDisparity = currentSlot > 0 && chain.clock.isCurrentSlotGivenGossipDisparity(currentSlot - 1); + const currentSlot = engine.clock.currentSlot; + const withinPastDisparity = currentSlot > 0 && engine.clock.isCurrentSlotGivenGossipDisparity(currentSlot - 1); const earliestPermissibleSlot = Math.max( // Pre-Deneb propagation is time-bounded: an attestation remains valid at the exact old // boundary `compute_time_at_slot(slot + range + 1) + MAXIMUM_GOSSIP_CLOCK_DISPARITY`. // Model that boundary by extending the lower slot bound by one additional slot only while // the clock still considers the previous slot current given gossip disparity. - currentSlot - chain.config.ATTESTATION_PROPAGATION_SLOT_RANGE - (withinPastDisparity ? 1 : 0), + currentSlot - engine.config.ATTESTATION_PROPAGATION_SLOT_RANGE - (withinPastDisparity ? 1 : 0), 0 ); @@ -650,7 +658,7 @@ export function verifyPropagationSlotRange(fork: ForkName, chain: IBeaconChain, // lower bound for previous epoch is same as epoch of earliestPermissibleSlot const currentEpochWithPastTolerance = computeEpochAtSlot( - chain.clock.slotWithPastTolerance(chain.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000) + engine.clock.slotWithPastTolerance(engine.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000) ); const earliestPermissiblePreviousEpoch = Math.max(currentEpochWithPastTolerance - 1, 0); @@ -670,7 +678,7 @@ export function verifyPropagationSlotRange(fork: ForkName, chain: IBeaconChain, * 2. attestation's target block is an ancestor of the block named in the LMD vote */ export function verifyHeadBlockAndTargetRoot( - chain: IBeaconChain, + engine: BeaconEngine, beaconBlockRoot: Root, targetRoot: Root, attestationSlot: Slot, @@ -678,13 +686,13 @@ export function verifyHeadBlockAndTargetRoot( caller: RegenCaller, maxSkipSlots?: number ): ProtoBlock { - const headBlock = verifyHeadBlockIsKnown(chain, beaconBlockRoot); + const headBlock = verifyHeadBlockIsKnown(engine, beaconBlockRoot); // Lighthouse rejects the attestation, however Lodestar only ignores considering it's not against the spec // it's more about a DOS protection to us // With verifyPropagationSlotRange() and maxSkipSlots = 32, it's unlikely we have to regenerate states in queue // to validate beacon_attestation and aggregate_and_proof const slotDistance = attestationSlot - headBlock.slot; - chain.metrics?.gossipAttestation.headSlotToAttestationSlot.observe({caller}, slotDistance); + engine.metrics?.gossipAttestation.headSlotToAttestationSlot.observe({caller}, slotDistance); if (maxSkipSlots !== undefined && slotDistance > maxSkipSlots) { throw new AttestationError(GossipAction.IGNORE, { @@ -710,34 +718,34 @@ export function verifyHeadBlockAndTargetRoot( * see https://github.com/ChainSafe/lodestar/blob/v1.11.3/packages/beacon-node/src/chain/validation/attestation.ts#L566 */ export async function getShufflingForAttestationVerification( - chain: IBeaconChain, + engine: BeaconEngine, attEpoch: Epoch, attHeadBlock: ProtoBlock, regenCaller: RegenCaller ): Promise { const blockEpoch = computeEpochAtSlot(attHeadBlock.slot); - const shufflingDependentRoot = getShufflingDependentRoot(chain.forkChoice, attEpoch, blockEpoch, attHeadBlock); + const shufflingDependentRoot = getShufflingDependentRoot(engine.forkChoice, attEpoch, blockEpoch, attHeadBlock); - const shuffling = await chain.shufflingCache.get(attEpoch, shufflingDependentRoot); + const shuffling = await engine.shufflingCache.get(attEpoch, shufflingDependentRoot); if (shuffling) { // most of the time, we should get the shuffling from cache - chain.metrics?.gossipAttestation.shufflingCacheHit.inc({caller: regenCaller}); + engine.metrics?.gossipAttestation.shufflingCacheHit.inc({caller: regenCaller}); return shuffling; } - chain.metrics?.gossipAttestation.shufflingCacheMiss.inc({caller: regenCaller}); + engine.metrics?.gossipAttestation.shufflingCacheMiss.inc({caller: regenCaller}); try { // for the 1st time of the same epoch and dependent root, it awaits for the regen state // from the 2nd time, it should use the same cached promise and it should reach the above code - chain.metrics?.gossipAttestation.shufflingCacheRegenHit.inc({caller: regenCaller}); - return await chain.regenStateForAttestationVerification( + engine.metrics?.gossipAttestation.shufflingCacheRegenHit.inc({caller: regenCaller}); + return await engine.regenStateForAttestationVerification( attEpoch, shufflingDependentRoot, attHeadBlock, regenCaller ); } catch (e) { - chain.metrics?.gossipAttestation.shufflingCacheRegenMiss.inc({caller: regenCaller}); + engine.metrics?.gossipAttestation.shufflingCacheRegenMiss.inc({caller: regenCaller}); throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.MISSING_STATE_TO_VERIFY_ATTESTATION, error: e as Error, @@ -759,7 +767,7 @@ export function getAttestationDataSigningRoot(config: BeaconConfig, data: phase0 } /** - * Checks if the `attestation.data.beaconBlockRoot` is known to this chain. + * Checks if the `attestation.data.beaconBlockRoot` is known to this engine. * * The block root may not be known for two reasons: * @@ -770,10 +778,10 @@ export function getAttestationDataSigningRoot(config: BeaconConfig, data: phase0 * it's still fine to ignore here because there's no need for us to handle attestations that are * already finalized. */ -function verifyHeadBlockIsKnown(chain: IBeaconChain, beaconBlockRoot: Root): ProtoBlock { +function verifyHeadBlockIsKnown(engine: BeaconEngine, beaconBlockRoot: Root): ProtoBlock { // TODO (LH): Enforce a maximum skip distance for unaggregated attestations. - const headBlock = chain.forkChoice.getBlockDefaultStatus(beaconBlockRoot); + const headBlock = engine.forkChoice.getBlockDefaultStatus(beaconBlockRoot); if (headBlock === null) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT, diff --git a/packages/beacon-node/src/chain/validation/blobSidecar.ts b/packages/beacon-node/src/chain/validation/blobSidecar.ts index 1670e3c0b5e3..264c260e3f94 100644 --- a/packages/beacon-node/src/chain/validation/blobSidecar.ts +++ b/packages/beacon-node/src/chain/validation/blobSidecar.ts @@ -14,21 +14,22 @@ import { import {BlobIndex, Root, Slot, SubnetID, deneb, ssz} from "@lodestar/types"; import {byteArrayEquals, toRootHex, verifyMerkleBranch} from "@lodestar/utils"; import {kzg} from "../../util/kzg.js"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {BlobSidecarErrorCode, BlobSidecarGossipError, BlobSidecarValidationError} from "../errors/blobSidecarError.js"; import {GossipAction} from "../errors/gossipValidation.js"; import {IBeaconChain} from "../interface.js"; import {RegenCaller} from "../regen/index.js"; export async function validateGossipBlobSidecar( + engine: BeaconEngine, fork: ForkName, - chain: IBeaconChain, blobSidecar: deneb.BlobSidecar, subnet: SubnetID ): Promise { const blobSlot = blobSidecar.signedBlockHeader.message.slot; // [REJECT] The sidecar's index is consistent with `MAX_BLOBS_PER_BLOCK` -- i.e. `blob_sidecar.index < MAX_BLOBS_PER_BLOCK`. - const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(blobSlot)); + const maxBlobsPerBlock = engine.config.getMaxBlobsPerBlock(computeEpochAtSlot(blobSlot)); if (blobSidecar.index >= maxBlobsPerBlock) { throw new BlobSidecarGossipError(GossipAction.REJECT, { code: BlobSidecarErrorCode.INDEX_TOO_LARGE, @@ -38,7 +39,7 @@ export async function validateGossipBlobSidecar( } // [REJECT] The sidecar is for the correct subnet -- i.e. `compute_subnet_for_blob_sidecar(sidecar.index) == subnet_id`. - if (computeSubnetForBlobSidecar(fork, chain.config, blobSidecar.index) !== subnet) { + if (computeSubnetForBlobSidecar(fork, engine.config, blobSidecar.index) !== subnet) { throw new BlobSidecarGossipError(GossipAction.REJECT, { code: BlobSidecarErrorCode.INVALID_INDEX, blobIdx: blobSidecar.index, @@ -49,7 +50,7 @@ export async function validateGossipBlobSidecar( // [IGNORE] The sidecar is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- // i.e. validate that sidecar.slot <= current_slot (a client MAY queue future blocks for processing at // the appropriate slot). - const currentSlotWithGossipDisparity = chain.clock.currentSlotWithGossipDisparity; + const currentSlotWithGossipDisparity = engine.clock.currentSlotWithGossipDisparity; if (currentSlotWithGossipDisparity < blobSlot) { throw new BlobSidecarGossipError(GossipAction.IGNORE, { code: BlobSidecarErrorCode.FUTURE_SLOT, @@ -60,7 +61,7 @@ export async function validateGossipBlobSidecar( // [IGNORE] The sidecar is from a slot greater than the latest finalized slot -- i.e. validate that // sidecar.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) - const finalizedCheckpoint = chain.forkChoice.getFinalizedCheckpoint(); + const finalizedCheckpoint = engine.forkChoice.getFinalizedCheckpoint(); const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch); if (blobSlot <= finalizedSlot) { throw new BlobSidecarGossipError(GossipAction.IGNORE, { @@ -78,7 +79,7 @@ export async function validateGossipBlobSidecar( // already know this block. const blockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blobSidecar.signedBlockHeader.message); const blockHex = toRootHex(blockRoot); - if (chain.forkChoice.getBlockHexDefaultStatus(blockHex) !== null) { + if (engine.forkChoice.getBlockHexDefaultStatus(blockHex) !== null) { throw new BlobSidecarGossipError(GossipAction.IGNORE, {code: BlobSidecarErrorCode.ALREADY_KNOWN, root: blockHex}); } @@ -89,7 +90,7 @@ export async function validateGossipBlobSidecar( // gossip and non-gossip sources) (a client MAY queue blocks for processing once the parent block is // retrieved). const parentRoot = toRootHex(blobSidecar.signedBlockHeader.message.parentRoot); - const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentRoot); + const parentBlock = engine.forkChoice.getBlockHexDefaultStatus(parentRoot); if (parentBlock === null) { // If fork choice does *not* consider the parent to be a descendant of the finalized block, // then there are two more cases: @@ -123,7 +124,7 @@ export async function validateGossipBlobSidecar( // this is something we should change this in the future to make the code airtight to the spec. // [IGNORE] The block's parent (defined by block.parent_root) has been seen (via both gossip and non-gossip sources) (a client MAY queue blocks for processing once the parent block is retrieved). // [REJECT] The block's parent (defined by block.parent_root) passes validation. - const blockState = await chain.regen + const blockState = await engine.regen .getBlockSlotState(parentBlock, blobSlot, {dontTransferCache: true}, RegenCaller.validateGossipBlock) .catch(() => { throw new BlobSidecarGossipError(GossipAction.IGNORE, { @@ -136,14 +137,14 @@ export async function validateGossipBlobSidecar( // [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey. const signature = blobSidecar.signedBlockHeader.signature; - if (!chain.seenBlockInputCache.isVerifiedProposerSignature(blobSlot, blockHex, signature)) { + if (!engine.seenBlockInputCache.isVerifiedProposerSignature(blobSlot, blockHex, signature)) { const signatureSet = getBlockHeaderProposerSignatureSetByParentStateSlot( - chain.config, + engine.config, blockState.slot, blobSidecar.signedBlockHeader ); // Don't batch so verification is not delayed - if (!(await chain.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { + if (!(await engine.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { throw new BlobSidecarGossipError(GossipAction.REJECT, { code: BlobSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID, blockRoot: blockHex, @@ -152,7 +153,7 @@ export async function validateGossipBlobSidecar( }); } - chain.seenBlockInputCache.markVerifiedProposerSignature(blobSlot, blockHex, signature); + engine.seenBlockInputCache.markVerifiedProposerSignature(blobSlot, blockHex, signature); } // verify if the blob inclusion proof is correct diff --git a/packages/beacon-node/src/chain/validation/block.ts b/packages/beacon-node/src/chain/validation/block.ts index 4f4ca45ac5d4..658414dbb109 100644 --- a/packages/beacon-node/src/chain/validation/block.ts +++ b/packages/beacon-node/src/chain/validation/block.ts @@ -1,4 +1,3 @@ -import {ChainForkConfig} from "@lodestar/config"; import {ExecutionStatus} from "@lodestar/fork-choice"; import {ForkName, isForkPostBellatrix, isForkPostDeneb, isForkPostGloas} from "@lodestar/params"; import { @@ -11,8 +10,8 @@ import { } from "@lodestar/state-transition"; import {SignedBeaconBlock, deneb, gloas, isGloasBeaconBlock} from "@lodestar/types"; import {byteArrayEquals, sleep, toRootHex} from "@lodestar/utils"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {BlockErrorCode, BlockGossipError, GossipAction} from "../errors/index.js"; -import {IBeaconChain} from "../interface.js"; import {RegenCaller} from "../regen/index.js"; export type GossipBlockValidationResult = { @@ -21,11 +20,11 @@ export type GossipBlockValidationResult = { }; export async function validateGossipBlock( - config: ChainForkConfig, - chain: IBeaconChain, + engine: BeaconEngine, signedBlock: SignedBeaconBlock, fork: ForkName ): Promise { + const config = engine.config; const block = signedBlock.message; const blockSlot = block.slot; const blockEpoch = computeEpochAtSlot(blockSlot); @@ -33,7 +32,7 @@ export async function validateGossipBlock( // [IGNORE] The block is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- i.e.validate // that signed_beacon_block.message.slot <= current_slot (a client MAY queue future blocks for processing at the // appropriate slot). - const currentSlotWithGossipDisparity = chain.clock.currentSlotWithGossipDisparity; + const currentSlotWithGossipDisparity = engine.clock.currentSlotWithGossipDisparity; if (currentSlotWithGossipDisparity < blockSlot) { throw new BlockGossipError(GossipAction.IGNORE, { code: BlockErrorCode.FUTURE_SLOT, @@ -44,7 +43,7 @@ export async function validateGossipBlock( // [IGNORE] The block is from a slot greater than the latest finalized slot -- i.e. validate that // signed_beacon_block.message.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) - const finalizedCheckpoint = chain.forkChoice.getFinalizedCheckpoint(); + const finalizedCheckpoint = engine.forkChoice.getFinalizedCheckpoint(); const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch); if (blockSlot <= finalizedSlot) { throw new BlockGossipError(GossipAction.IGNORE, { @@ -61,7 +60,7 @@ export async function validateGossipBlock( // check, we will load the parent and state from disk only to find out later that we // already know this block. const blockRoot = toRootHex(config.getForkTypes(blockSlot).BeaconBlock.hashTreeRoot(block)); - if (chain.forkChoice.getBlockHexDefaultStatus(blockRoot) !== null) { + if (engine.forkChoice.getBlockHexDefaultStatus(blockRoot) !== null) { throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRoot}); } @@ -70,14 +69,14 @@ export async function validateGossipBlock( // [IGNORE] The block is the first block with valid signature received for the proposer for the slot, signed_beacon_block.message.slot. const proposerIndex = block.proposerIndex; - if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) { + if (engine.seenBlockProposers.isKnown(blockSlot, proposerIndex)) { throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex}); } // [REJECT] The current finalized_checkpoint is an ancestor of block -- i.e. // get_ancestor(store, block.parent_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == store.finalized_checkpoint.root const parentRoot = toRootHex(block.parentRoot); - const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentRoot); + const parentBlock = engine.forkChoice.getBlockHexDefaultStatus(parentRoot); if (parentBlock === null) { // If fork choice does *not* consider the parent to be a descendant of the finalized block, // then there are two more cases: @@ -105,7 +104,7 @@ export async function validateGossipBlock( // (via gossip or non-gossip sources) (a client MAY queue blocks for processing once the parent payload is retrieved). if (isGloasBeaconBlock(block)) { const parentBlockHashHex = toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash); - if (chain.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHashHex) === null) { + if (engine.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHashHex) === null) { throw new BlockGossipError(GossipAction.IGNORE, { code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN, parentRoot, @@ -173,14 +172,14 @@ export async function validateGossipBlock( // this is something we should change this in the future to make the code airtight to the spec. // [IGNORE] The block's parent (defined by block.parent_root) has been seen (via both gossip and non-gossip sources) (a client MAY queue blocks for processing once the parent block is retrieved). // [REJECT] The block's parent (defined by block.parent_root) passes validation. - const blockState = await chain.regen + const blockState = await engine.regen .getPreState(block, {dontTransferCache: true}, RegenCaller.validateGossipBlock) .catch(() => { throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.PARENT_UNKNOWN, parentRoot}); }); // in forky condition, make sure to populate ShufflingCache with regened state - chain.shufflingCache.processState(blockState); + engine.shufflingCache.processState(blockState); // [REJECT] The block's execution payload timestamp is correct with respect to the slot // -- i.e. execution_payload.timestamp == compute_timestamp_at_slot(state, block.slot). @@ -188,8 +187,8 @@ export async function validateGossipBlock( if (!isExecutionBlockBodyType(block.body)) throw Error("Not execution block body type"); const executionPayload = block.body.executionPayload; if (isStatePostBellatrix(blockState) && blockState.isExecutionStateType && blockState.isExecutionEnabled(block)) { - const expectedTimestamp = computeTimeAtSlot(config, blockSlot, chain.genesisTime); - if (executionPayload.timestamp !== computeTimeAtSlot(config, blockSlot, chain.genesisTime)) { + const expectedTimestamp = computeTimeAtSlot(config, blockSlot, engine.clock.genesisTime); + if (executionPayload.timestamp !== computeTimeAtSlot(config, blockSlot, engine.clock.genesisTime)) { throw new BlockGossipError(GossipAction.REJECT, { code: BlockErrorCode.INCORRECT_TIMESTAMP, timestamp: executionPayload.timestamp, @@ -205,17 +204,17 @@ export async function validateGossipBlock( } // [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey. - if (!chain.seenBlockInputCache.isVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature)) { - const signatureSet = getBlockProposerSignatureSet(chain.config, signedBlock); + if (!engine.seenBlockInputCache.isVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature)) { + const signatureSet = getBlockProposerSignatureSet(engine.config, signedBlock); // Don't batch so verification is not delayed - if (!(await chain.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { + if (!(await engine.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { throw new BlockGossipError(GossipAction.REJECT, { code: BlockErrorCode.PROPOSAL_SIGNATURE_INVALID, blockSlot, }); } - chain.seenBlockInputCache.markVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature); + engine.seenBlockInputCache.markVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature); } // [REJECT] The block is proposed by the expected proposer_index for the block's slot in the context of the current @@ -227,20 +226,20 @@ export async function validateGossipBlock( } // Check again in case there two blocks are processed concurrently - if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) { + if (engine.seenBlockProposers.isKnown(blockSlot, proposerIndex)) { throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex}); } // Simple implementation of a pending block queue. Keeping the block here recycles the queue logic, and keeps the // gossip validation promise without any extra infrastructure. // Do the sleep at the end, since regen and signature validation can already take longer than `msToBlockSlot`. - const msToBlockSlot = computeTimeAtSlot(config, blockSlot, chain.genesisTime) * 1000 - Date.now(); + const msToBlockSlot = computeTimeAtSlot(config, blockSlot, engine.clock.genesisTime) * 1000 - Date.now(); if (msToBlockSlot <= config.MAXIMUM_GOSSIP_CLOCK_DISPARITY && msToBlockSlot > 0) { // If block is between 0 and 500 ms early, hold it in a promise. Equivalent to a pending queue. await sleep(msToBlockSlot); } - chain.seenBlockProposers.add(blockSlot, proposerIndex); + engine.seenBlockProposers.add(blockSlot, proposerIndex); return {skippedSlots}; } diff --git a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts index 2e2f05c2ba0e..5e04db4afcd3 100644 --- a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts +++ b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts @@ -16,6 +16,7 @@ import {BeaconMetrics} from "../../metrics/metrics/beacon.js"; import {Metrics} from "../../metrics/metrics.js"; import {getDataColumnSidecarSlot} from "../../util/dataColumns.js"; import {kzg} from "../../util/kzg.js"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; import { DataColumnSidecarErrorCode, @@ -29,7 +30,7 @@ import {RegenCaller} from "../regen/interface.js"; // SPEC FUNCTION // https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.4/specs/fulu/p2p-interface.md#data_column_sidecar_subnet_id export async function validateGossipFuluDataColumnSidecar( - chain: IBeaconChain, + engine: BeaconEngine, dataColumnSidecar: fulu.DataColumnSidecar, gossipSubnet: SubnetID, metrics: Metrics | null @@ -38,10 +39,10 @@ export async function validateGossipFuluDataColumnSidecar( const blockRootHex = toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader)); // 1) [REJECT] The sidecar is valid as verified by verify_data_column_sidecar - verifyFuluDataColumnSidecar(chain.config, dataColumnSidecar); + verifyFuluDataColumnSidecar(engine.config, dataColumnSidecar); // 2) [REJECT] The sidecar is for the correct subnet -- i.e. compute_subnet_for_data_column_sidecar(sidecar.index) == subnet_id - if (computeSubnetForDataColumnSidecar(chain.config, dataColumnSidecar) !== gossipSubnet) { + if (computeSubnetForDataColumnSidecar(engine.config, dataColumnSidecar) !== gossipSubnet) { throw new DataColumnSidecarGossipError(GossipAction.REJECT, { code: DataColumnSidecarErrorCode.INVALID_SUBNET, columnIndex: dataColumnSidecar.index, @@ -52,7 +53,7 @@ export async function validateGossipFuluDataColumnSidecar( // 3) [IGNORE] The sidecar is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) // -- i.e. validate that sidecar.slot <= current_slot (a client MAY queue future blocks // for processing at the appropriate slot). - const currentSlotWithGossipDisparity = chain.clock.currentSlotWithGossipDisparity; + const currentSlotWithGossipDisparity = engine.clock.currentSlotWithGossipDisparity; if (currentSlotWithGossipDisparity < blockHeader.slot) { throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { code: DataColumnSidecarErrorCode.FUTURE_SLOT, @@ -63,7 +64,7 @@ export async function validateGossipFuluDataColumnSidecar( // 4) [IGNORE] The sidecar is from a slot greater than the latest finalized slot -- i.e. validate that // sidecar.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) - const finalizedCheckpoint = chain.forkChoice.getFinalizedCheckpoint(); + const finalizedCheckpoint = engine.forkChoice.getFinalizedCheckpoint(); const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch); if (blockHeader.slot <= finalizedSlot) { throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { @@ -76,7 +77,7 @@ export async function validateGossipFuluDataColumnSidecar( // 6) [IGNORE] The sidecar's block's parent (defined by block_header.parent_root) has been seen (via gossip // or non-gossip sources) const parentRoot = toRootHex(blockHeader.parentRoot); - const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentRoot); + const parentBlock = engine.forkChoice.getBlockHexDefaultStatus(parentRoot); if (parentBlock === null) { // If fork choice does *not* consider the parent to be a descendant of the finalized block, // then there are two more cases: @@ -108,7 +109,7 @@ export async function validateGossipFuluDataColumnSidecar( // As a result, we throw an IGNORE (whereas the spec says we should REJECT for this scenario). // this is something we should change this in the future to make the code airtight to the spec. // 7) [REJECT] The sidecar's block's parent passes validation. - const blockState = await chain.regen + const blockState = await engine.regen .getBlockSlotState(parentBlock, blockHeader.slot, {dontTransferCache: true}, RegenCaller.validateGossipDataColumn) .catch(() => { throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { @@ -136,15 +137,15 @@ export async function validateGossipFuluDataColumnSidecar( // 5) [REJECT] The proposer signature of sidecar.signed_block_header, is valid with respect to the block_header.proposer_index pubkey. const signature = dataColumnSidecar.signedBlockHeader.signature; - if (!chain.seenBlockInputCache.isVerifiedProposerSignature(blockHeader.slot, blockRootHex, signature)) { + if (!engine.seenBlockInputCache.isVerifiedProposerSignature(blockHeader.slot, blockRootHex, signature)) { const signatureSet = getBlockHeaderProposerSignatureSetByParentStateSlot( - chain.config, + engine.config, blockState.slot, dataColumnSidecar.signedBlockHeader ); if ( - !(await chain.bls.verifySignatureSets([signatureSet], { + !(await engine.bls.verifySignatureSets([signatureSet], { // verify on main thread so that we only need to verify block proposer signature once per block verifyOnMainThread: true, })) @@ -157,7 +158,7 @@ export async function validateGossipFuluDataColumnSidecar( }); } - chain.seenBlockInputCache.markVerifiedProposerSignature(blockHeader.slot, blockRootHex, signature); + engine.seenBlockInputCache.markVerifiedProposerSignature(blockHeader.slot, blockRootHex, signature); } // 9) [REJECT] The current finalized_checkpoint is an ancestor of the sidecar's block @@ -208,14 +209,14 @@ export async function validateGossipFuluDataColumnSidecar( // SPEC FUNCTION // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.3/specs/gloas/p2p-interface.md#data_column_sidecar_subnet_id export async function validateGossipGloasDataColumnSidecar( - chain: IBeaconChain, + engine: BeaconEngine, payloadInput: PayloadEnvelopeInput, dataColumnSidecar: gloas.DataColumnSidecar, gossipSubnet: SubnetID, metrics: Metrics | null ): Promise { const blockRootHex = toRootHex(dataColumnSidecar.beaconBlockRoot); - const block = chain.forkChoice.getBlockHexDefaultStatus(blockRootHex); + const block = engine.forkChoice.getBlockHexDefaultStatus(blockRootHex); // [IGNORE] A valid block for the sidecar's `slot` has been seen. if (block === null) { @@ -241,7 +242,7 @@ export async function validateGossipGloasDataColumnSidecar( verifyGloasDataColumnSidecar(dataColumnSidecar, kzgCommitments); // [REJECT] The sidecar must be on the correct subnet - if (computeSubnetForDataColumnSidecar(chain.config, dataColumnSidecar) !== gossipSubnet) { + if (computeSubnetForDataColumnSidecar(engine.config, dataColumnSidecar) !== gossipSubnet) { throw new DataColumnSidecarGossipError(GossipAction.REJECT, { code: DataColumnSidecarErrorCode.INVALID_SUBNET, columnIndex: dataColumnSidecar.index, diff --git a/packages/beacon-node/src/chain/validation/executionPayloadBid.ts b/packages/beacon-node/src/chain/validation/executionPayloadBid.ts index 6129140513d4..dd184ed228d8 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadBid.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadBid.ts @@ -11,26 +11,26 @@ import { import {ValidatorIndex, gloas} from "@lodestar/types"; import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils"; import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {ExecutionPayloadBidError, ExecutionPayloadBidErrorCode, GossipAction} from "../errors/index.js"; -import {IBeaconChain} from "../index.js"; import {RegenCaller} from "../regen/index.js"; export async function validateApiExecutionPayloadBid( - chain: IBeaconChain, + engine: BeaconEngine, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid ): Promise<{proposerIndex: ValidatorIndex}> { - return validateExecutionPayloadBid(chain, signedExecutionPayloadBid); + return validateExecutionPayloadBid(engine, signedExecutionPayloadBid); } export async function validateGossipExecutionPayloadBid( - chain: IBeaconChain, + engine: BeaconEngine, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid ): Promise<{proposerIndex: ValidatorIndex}> { - return validateExecutionPayloadBid(chain, signedExecutionPayloadBid); + return validateExecutionPayloadBid(engine, signedExecutionPayloadBid); } async function validateExecutionPayloadBid( - chain: IBeaconChain, + engine: BeaconEngine, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid ): Promise<{proposerIndex: ValidatorIndex}> { const bid = signedExecutionPayloadBid.message; @@ -38,7 +38,7 @@ async function validateExecutionPayloadBid( const parentBlockHashHex = toRootHex(bid.parentBlockHash); // [IGNORE] `bid.slot` is the current slot or the next slot. - const currentSlot = chain.clock.currentSlot; + const currentSlot = engine.clock.currentSlot; if (bid.slot !== currentSlot && bid.slot !== currentSlot + 1) { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.INVALID_SLOT, @@ -50,7 +50,7 @@ async function validateExecutionPayloadBid( // [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice. // Moved earlier than the spec ordering so we can derive the proposer dependent root for the // proposer-preferences lookup below from a known fork-choice block. - const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex); + const parentBlock = engine.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex); if (parentBlock === null) { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT, @@ -81,7 +81,7 @@ async function validateExecutionPayloadBid( // letting a raw `ForkChoiceError` escape the `GossipActionError` contract. const dependentRootHex = (() => { try { - return getShufflingDependentRoot(chain.forkChoice, bidEpoch, computeEpochAtSlot(parentBlock.slot), parentBlock); + return getShufflingDependentRoot(engine.forkChoice, bidEpoch, computeEpochAtSlot(parentBlock.slot), parentBlock); } catch { return null; } @@ -98,7 +98,7 @@ async function validateExecutionPayloadBid( }); } - const proposerPreferences = chain.proposerPreferencesPool.get(bid.slot, dependentRootHex); + const proposerPreferences = engine.proposerPreferencesPool.get(bid.slot, dependentRootHex); if (proposerPreferences === null) { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES, @@ -109,7 +109,7 @@ async function validateExecutionPayloadBid( } // Use the bid's parent branch state for builder checks - const state = await chain.regen + const state = await engine.regen .getBlockSlotState(parentBlock, bid.slot, {dontTransferCache: true}, RegenCaller.validateGossipExecutionPayloadBid) .catch(() => { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { @@ -176,7 +176,7 @@ async function validateExecutionPayloadBid( // payload's hash) and EMPTY parents (EMPTY/PENDING variants carry the inherited parent // payload's hash, since the new block doesn't have its own payload). Variant carries the // executed payload's gas_limit, which we use as `parent_gas_limit` below. - const parentPayloadVariant = chain.forkChoice.getBlockHexAndBlockHash(parentBlockRootHex, parentBlockHashHex); + const parentPayloadVariant = engine.forkChoice.getBlockHexAndBlockHash(parentBlockRootHex, parentBlockHashHex); if (parentPayloadVariant === null || parentPayloadVariant.executionPayloadBlockHash === null) { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH, @@ -204,7 +204,7 @@ async function validateExecutionPayloadBid( // consensus layer -- i.e. validate that // `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`. const blobKzgCommitmentsLen = bid.blobKzgCommitments.length; - const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot)); + const maxBlobsPerBlock = engine.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot)); if (blobKzgCommitmentsLen > maxBlobsPerBlock) { throw new ExecutionPayloadBidError(GossipAction.REJECT, { code: ExecutionPayloadBidErrorCode.TOO_MANY_KZG_COMMITMENTS, @@ -214,7 +214,7 @@ async function validateExecutionPayloadBid( } // [IGNORE] this is the first signed bid seen with a valid signature from the given builder for this slot. - if (chain.seenExecutionPayloadBids.isKnown(bid.slot, bid.builderIndex)) { + if (engine.seenExecutionPayloadBids.isKnown(bid.slot, bid.builderIndex)) { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.BID_ALREADY_KNOWN, builderIndex: bid.builderIndex, @@ -226,7 +226,7 @@ async function validateExecutionPayloadBid( // [IGNORE] this bid is the highest value bid seen for the tuple // `(bid.slot, bid.parent_block_hash, bid.parent_block_root)`. - const bestBid = chain.executionPayloadBidPool.getBestBid(bid.slot, parentBlockHashHex, parentBlockRootHex); + const bestBid = engine.executionPayloadBidPool.getBestBid(bid.slot, parentBlockHashHex, parentBlockRootHex); if (bestBid !== null && bestBid.message.value >= bid.value) { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.BID_TOO_LOW, @@ -259,11 +259,11 @@ async function validateExecutionPayloadBid( // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`. const signatureSet = createSingleSignatureSetFromComponents( PublicKey.fromBytes(builder.pubkey), - getExecutionPayloadBidSigningRoot(chain.config, state.slot, bid), + getExecutionPayloadBidSigningRoot(engine.config, state.slot, bid), signedExecutionPayloadBid.signature ); - if (!(await chain.bls.verifySignatureSets([signatureSet]))) { + if (!(await engine.bls.verifySignatureSets([signatureSet]))) { throw new ExecutionPayloadBidError(GossipAction.REJECT, { code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE, builderIndex: bid.builderIndex, @@ -272,7 +272,7 @@ async function validateExecutionPayloadBid( } // Valid - chain.seenExecutionPayloadBids.add(bid.slot, bid.builderIndex); + engine.seenExecutionPayloadBids.add(bid.slot, bid.builderIndex); return {proposerIndex: proposerPreferences.message.validatorIndex}; } diff --git a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts index 9356a916b15d..8744bac328a1 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts @@ -6,26 +6,26 @@ import { } from "@lodestar/state-transition"; import {gloas, ssz} from "@lodestar/types"; import {byteArrayEquals, toRootHex} from "@lodestar/utils"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {ExecutionPayloadEnvelopeError, ExecutionPayloadEnvelopeErrorCode, GossipAction} from "../errors/index.js"; -import {IBeaconChain} from "../index.js"; import {RegenCaller} from "../regen/index.js"; export async function validateApiExecutionPayloadEnvelope( - chain: IBeaconChain, + engine: BeaconEngine, executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope ): Promise { - return validateExecutionPayloadEnvelope(chain, executionPayloadEnvelope); + return validateExecutionPayloadEnvelope(engine, executionPayloadEnvelope); } export async function validateGossipExecutionPayloadEnvelope( - chain: IBeaconChain, + engine: BeaconEngine, executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope ): Promise { - return validateExecutionPayloadEnvelope(chain, executionPayloadEnvelope); + return validateExecutionPayloadEnvelope(engine, executionPayloadEnvelope); } async function validateExecutionPayloadEnvelope( - chain: IBeaconChain, + engine: BeaconEngine, executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope ): Promise { const envelope = executionPayloadEnvelope.message; @@ -35,7 +35,7 @@ async function validateExecutionPayloadEnvelope( // [IGNORE] The envelope's block root `envelope.beacon_block_root` has been seen (via // gossip or non-gossip sources) (a client MAY queue payload for processing once // the block is retrieved). - const block = chain.forkChoice.getBlockDefaultStatus(envelope.beaconBlockRoot); + const block = engine.forkChoice.getBlockDefaultStatus(envelope.beaconBlockRoot); if (block === null) { throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { code: ExecutionPayloadEnvelopeErrorCode.BLOCK_ROOT_UNKNOWN, @@ -45,8 +45,8 @@ async function validateExecutionPayloadEnvelope( // [IGNORE] The node has not seen another valid // `SignedExecutionPayloadEnvelope` for this block root from this builder. - const envelopeBlock = chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.FULL); - const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); + const envelopeBlock = engine.forkChoice.getBlockHex(blockRootHex, PayloadStatus.FULL); + const payloadInput = engine.seenPayloadEnvelopeInputCache.get(blockRootHex); if (envelopeBlock || payloadInput?.hasPayloadEnvelope()) { throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { code: ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN, @@ -64,7 +64,7 @@ async function validateExecutionPayloadEnvelope( } // [IGNORE] The envelope is from a slot greater than or equal to the latest finalized slot -- i.e. validate that `payload.slotNumber >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)` - const finalizedCheckpoint = chain.forkChoice.getFinalizedCheckpoint(); + const finalizedCheckpoint = engine.forkChoice.getFinalizedCheckpoint(); const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch); if (payload.slotNumber < finalizedSlot) { throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { @@ -116,7 +116,7 @@ async function validateExecutionPayloadEnvelope( } // Get the block state to verify the builder's signature. - const blockState = await chain.regen + const blockState = await engine.regen .getState(block.stateRoot, RegenCaller.validateGossipPayloadEnvelope) .catch(() => { throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { @@ -132,14 +132,14 @@ async function validateExecutionPayloadEnvelope( // [REJECT] `signed_execution_payload_envelope.signature` is valid as verified // by `verify_execution_payload_envelope_signature`. const signatureSet = getExecutionPayloadEnvelopeSignatureSet( - chain.config, - chain.pubkeyCache, + engine.config, + engine.pubkeyCache, blockState, executionPayloadEnvelope, payloadInput.proposerIndex ); - if (!(await chain.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { + if (!(await engine.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE, }); diff --git a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts index 48e639017d7e..8ddae054aca3 100644 --- a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts +++ b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts @@ -6,8 +6,8 @@ import { } from "@lodestar/state-transition"; import {RootHex, gloas, ssz} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {GossipAction, PayloadAttestationError, PayloadAttestationErrorCode} from "../errors/index.js"; -import {IBeaconChain} from "../index.js"; import {RegenCaller} from "../regen/index.js"; export type PayloadAttestationValidationResult = { @@ -16,22 +16,22 @@ export type PayloadAttestationValidationResult = { }; export async function validateApiPayloadAttestationMessage( - chain: IBeaconChain, + engine: BeaconEngine, payloadAttestationMessage: gloas.PayloadAttestationMessage ): Promise { const prioritizeBls = true; - return validatePayloadAttestationMessage(chain, payloadAttestationMessage, prioritizeBls); + return validatePayloadAttestationMessage(engine, payloadAttestationMessage, prioritizeBls); } export async function validateGossipPayloadAttestationMessage( - chain: IBeaconChain, + engine: BeaconEngine, payloadAttestationMessage: gloas.PayloadAttestationMessage ): Promise { - return validatePayloadAttestationMessage(chain, payloadAttestationMessage); + return validatePayloadAttestationMessage(engine, payloadAttestationMessage); } async function validatePayloadAttestationMessage( - chain: IBeaconChain, + engine: BeaconEngine, payloadAttestationMessage: gloas.PayloadAttestationMessage, prioritizeBls = false ): Promise { @@ -39,10 +39,10 @@ async function validatePayloadAttestationMessage( const epoch = computeEpochAtSlot(data.slot); // [IGNORE] The message's slot is for the current slot (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance), i.e. `data.slot == current_slot`. - if (!chain.clock.isCurrentSlotGivenGossipDisparity(data.slot)) { + if (!engine.clock.isCurrentSlotGivenGossipDisparity(data.slot)) { throw new PayloadAttestationError(GossipAction.IGNORE, { code: PayloadAttestationErrorCode.NOT_CURRENT_SLOT, - currentSlot: chain.clock.currentSlot, + currentSlot: engine.clock.currentSlot, slot: data.slot, }); } @@ -50,7 +50,7 @@ async function validatePayloadAttestationMessage( // [IGNORE] The `payload_attestation_message` is the first valid message received // from the validator with index `payload_attestation_message.validator_index`. // A single validator can participate PTC at most once per epoch - if (chain.seenPayloadAttesters.isKnown(epoch, validatorIndex)) { + if (engine.seenPayloadAttesters.isKnown(epoch, validatorIndex)) { throw new PayloadAttestationError(GossipAction.IGNORE, { code: PayloadAttestationErrorCode.PAYLOAD_ATTESTATION_ALREADY_KNOWN, validatorIndex, @@ -62,7 +62,7 @@ async function validatePayloadAttestationMessage( // [IGNORE] The message's block `data.beacon_block_root` has been seen (via // gossip or non-gossip sources) (a client MAY queue attestation for processing // once the block is retrieved. Note a client might want to request payload after). - const block = chain.forkChoice.getBlockDefaultStatus(data.beaconBlockRoot); + const block = engine.forkChoice.getBlockDefaultStatus(data.beaconBlockRoot); if (!block) { throw new PayloadAttestationError(GossipAction.IGNORE, { code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT, @@ -86,7 +86,7 @@ async function validatePayloadAttestationMessage( // it is possible that the block didn't pass the validation // Use the referenced block's branch state for the PTC committee check - const state = await chain.regen + const state = await engine.regen .getBlockSlotState(block, data.slot, {dontTransferCache: true}, RegenCaller.validateGossipPayloadAttestationMessage) .catch(() => { throw new PayloadAttestationError(GossipAction.IGNORE, { @@ -114,7 +114,7 @@ async function validatePayloadAttestationMessage( } // [REJECT] `payload_attestation_message.signature` is valid with respect to the validator's public key. - const validatorPubkey = chain.pubkeyCache.get(validatorIndex); + const validatorPubkey = engine.pubkeyCache.get(validatorIndex); if (!validatorPubkey) { throw new PayloadAttestationError(GossipAction.REJECT, { code: PayloadAttestationErrorCode.INVALID_ATTESTER, @@ -124,18 +124,18 @@ async function validatePayloadAttestationMessage( const signatureSet = createSingleSignatureSetFromComponents( validatorPubkey, - getPayloadAttestationDataSigningRoot(chain.config, data), + getPayloadAttestationDataSigningRoot(engine.config, data), payloadAttestationMessage.signature ); - if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { + if (!(await engine.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { throw new PayloadAttestationError(GossipAction.REJECT, { code: PayloadAttestationErrorCode.INVALID_SIGNATURE, }); } // Valid - chain.seenPayloadAttesters.add(epoch, validatorIndex); + engine.seenPayloadAttesters.add(epoch, validatorIndex); return { attDataRootHex: toRootHex(ssz.gloas.PayloadAttestationData.hashTreeRoot(data)), diff --git a/packages/beacon-node/src/chain/validation/proposerPreferences.ts b/packages/beacon-node/src/chain/validation/proposerPreferences.ts index 814697d945a4..9ca61e7516ec 100644 --- a/packages/beacon-node/src/chain/validation/proposerPreferences.ts +++ b/packages/beacon-node/src/chain/validation/proposerPreferences.ts @@ -6,15 +6,15 @@ import { } from "@lodestar/state-transition"; import {ValidatorIndex, gloas} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {GossipAction, ProposerPreferencesError, ProposerPreferencesErrorCode} from "../errors/index.js"; -import {IBeaconChain} from "../index.js"; /** * Validates a gossiped `SignedProposerPreferences` per * https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#proposer_preferences */ export async function validateGossipProposerPreferences( - chain: IBeaconChain, + engine: BeaconEngine, signedProposerPreferences: gloas.SignedProposerPreferences ): Promise { const preferences = signedProposerPreferences.message; @@ -23,7 +23,7 @@ export async function validateGossipProposerPreferences( const proposalEpoch = computeEpochAtSlot(proposalSlot); // [IGNORE] `preferences.proposal_slot` is in the current or next epoch. - const currentEpoch = chain.clock.currentEpoch; + const currentEpoch = engine.clock.currentEpoch; if (proposalEpoch < currentEpoch || proposalEpoch > currentEpoch + 1) { throw new ProposerPreferencesError(GossipAction.IGNORE, { code: ProposerPreferencesErrorCode.INVALID_EPOCH, @@ -33,7 +33,7 @@ export async function validateGossipProposerPreferences( } // [IGNORE] `preferences.proposal_slot` has not already passed. - const currentSlot = chain.clock.currentSlot; + const currentSlot = engine.clock.currentSlot; if (proposalSlot <= currentSlot) { throw new ProposerPreferencesError(GossipAction.IGNORE, { code: ProposerPreferencesErrorCode.PROPOSAL_SLOT_PASSED, @@ -47,7 +47,7 @@ export async function validateGossipProposerPreferences( // the previous-root checkpoint state (populated by `processSlotsToNearestCheckpoint` for // any imported branch crossing into `proposalEpoch - 1`). The head-state path also handles // narrow timing windows where the checkpoint state isn't yet populated. - const headState = chain.getHeadState(); + const headState = engine.getHeadState(); let proposers: ValidatorIndex[] | null = null; if (headState.epoch === proposalEpoch && headState.currentDecisionRoot === dependentRootHex) { proposers = headState.currentProposers; @@ -55,7 +55,7 @@ export async function validateGossipProposerPreferences( proposers = headState.nextProposers; } else { // Sync lookup only to not trigger disk reload from gossip input. - const checkpointState = chain.regen.getCheckpointStateSync({epoch: proposalEpoch - 1, rootHex: dependentRootHex}); + const checkpointState = engine.regen.getCheckpointStateSync({epoch: proposalEpoch - 1, rootHex: dependentRootHex}); if (checkpointState !== null) { // State is at `proposalEpoch - 1`, so proposers for `proposalSlot` (next epoch from // the state's perspective) live in `nextProposers`. @@ -81,7 +81,7 @@ export async function validateGossipProposerPreferences( } // [IGNORE] First valid message for (dependent_root, proposal_slot, validator_index). - if (chain.seenProposerPreferences.isKnown(dependentRootHex, proposalSlot, validatorIndex)) { + if (engine.seenProposerPreferences.isKnown(dependentRootHex, proposalSlot, validatorIndex)) { throw new ProposerPreferencesError(GossipAction.IGNORE, { code: ProposerPreferencesErrorCode.ALREADY_KNOWN, proposalSlot, @@ -92,12 +92,12 @@ export async function validateGossipProposerPreferences( // [REJECT] `signed_proposer_preferences.signature` is valid with respect to the validator's public key. const signatureSet = createSingleSignatureSetFromComponents( - chain.pubkeyCache.getOrThrow(validatorIndex), - getProposerPreferencesSigningRoot(chain.config, preferences), + engine.pubkeyCache.getOrThrow(validatorIndex), + getProposerPreferencesSigningRoot(engine.config, preferences), signedProposerPreferences.signature ); - if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true}))) { + if (!(await engine.bls.verifySignatureSets([signatureSet], {batchable: true}))) { throw new ProposerPreferencesError(GossipAction.REJECT, { code: ProposerPreferencesErrorCode.INVALID_SIGNATURE, proposalSlot, @@ -106,5 +106,5 @@ export async function validateGossipProposerPreferences( } // Valid - chain.seenProposerPreferences.add(dependentRootHex, proposalSlot, validatorIndex); + engine.seenProposerPreferences.add(dependentRootHex, proposalSlot, validatorIndex); } diff --git a/packages/beacon-node/src/chain/validation/syncCommittee.ts b/packages/beacon-node/src/chain/validation/syncCommittee.ts index e3a793c6df17..57ccdcd672a7 100644 --- a/packages/beacon-node/src/chain/validation/syncCommittee.ts +++ b/packages/beacon-node/src/chain/validation/syncCommittee.ts @@ -2,8 +2,8 @@ import {SYNC_COMMITTEE_SUBNET_COUNT, SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar import {IBeaconStateView, isStatePostAltair} from "@lodestar/state-transition"; import {SubnetID, altair} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {GossipAction, SyncCommitteeError, SyncCommitteeErrorCode} from "../errors/index.js"; -import {IBeaconChain} from "../interface.js"; import {getSyncCommitteeSignatureSet} from "./signatureSets/index.js"; type IndexInSubcommittee = number; @@ -12,15 +12,15 @@ type IndexInSubcommittee = number; * Spec v1.1.0-alpha.8 */ export async function validateGossipSyncCommittee( - chain: IBeaconChain, + engine: BeaconEngine, syncCommittee: altair.SyncCommitteeMessage, subnet: SubnetID ): Promise<{indicesInSubcommittee: IndexInSubcommittee[]}> { const {slot, validatorIndex, beaconBlockRoot} = syncCommittee; const messageRoot = toRootHex(beaconBlockRoot); - const headState = chain.getHeadState(); - const indicesInSubcommittee = validateGossipSyncCommitteeExceptSig(chain, headState, subnet, syncCommittee); + const headState = engine.getHeadState(); + const indicesInSubcommittee = validateGossipSyncCommitteeExceptSig(engine, headState, subnet, syncCommittee); // [IGNORE] The signature's slot is for the current slot, i.e. sync_committee_signature.slot == current_slot. // > Checked in validateGossipSyncCommitteeExceptSig() @@ -32,16 +32,16 @@ export async function validateGossipSyncCommittee( // [IGNORE] There has been no other valid sync committee signature for the declared slot for the validator referenced // by sync_committee_signature.validator_index. - const prevRoot = chain.seenSyncCommitteeMessages.get(slot, subnet, validatorIndex); + const prevRoot = engine.seenSyncCommitteeMessages.get(slot, subnet, validatorIndex); if (prevRoot) { let shouldIgnore = false; if (prevRoot === messageRoot) { shouldIgnore = true; } else { - const headRoot = chain.forkChoice.getHeadRoot(); - chain.metrics?.gossipSyncCommittee.equivocationCount.inc(); + const headRoot = engine.forkChoice.getHeadRoot(); + engine.metrics?.gossipSyncCommittee.equivocationCount.inc(); if (messageRoot === headRoot) { - chain.metrics?.gossipSyncCommittee.equivocationToHeadCount.inc(); + engine.metrics?.gossipSyncCommittee.equivocationToHeadCount.inc(); } else { shouldIgnore = true; } @@ -63,34 +63,34 @@ export async function validateGossipSyncCommittee( // > Checked in validateGossipSyncCommitteeExceptSig() // [REJECT] The signature is valid for the message beacon_block_root for the validator referenced by validator_index. - await validateSyncCommitteeSigOnly(chain, headState, syncCommittee); + await validateSyncCommitteeSigOnly(engine, headState, syncCommittee); // Register this valid item as seen - chain.seenSyncCommitteeMessages.add(slot, subnet, validatorIndex, messageRoot); + engine.seenSyncCommitteeMessages.add(slot, subnet, validatorIndex, messageRoot); return {indicesInSubcommittee}; } export async function validateApiSyncCommittee( - chain: IBeaconChain, + engine: BeaconEngine, headState: IBeaconStateView, syncCommittee: altair.SyncCommitteeMessage ): Promise { const prioritizeBls = true; - return validateSyncCommitteeSigOnly(chain, headState, syncCommittee, prioritizeBls); + return validateSyncCommitteeSigOnly(engine, headState, syncCommittee, prioritizeBls); } /** * Abstracted so it can be re-used in API validation. */ async function validateSyncCommitteeSigOnly( - chain: IBeaconChain, + engine: BeaconEngine, headState: IBeaconStateView, syncCommittee: altair.SyncCommitteeMessage, prioritizeBls = false ): Promise { - const signatureSet = getSyncCommitteeSignatureSet(chain.config, headState, syncCommittee); - if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { + const signatureSet = getSyncCommitteeSignatureSet(engine.config, headState, syncCommittee); + if (!(await engine.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { throw new SyncCommitteeError(GossipAction.REJECT, { code: SyncCommitteeErrorCode.INVALID_SIGNATURE, }); @@ -101,7 +101,7 @@ async function validateSyncCommitteeSigOnly( * Spec v1.1.0-alpha.8 */ export function validateGossipSyncCommitteeExceptSig( - chain: IBeaconChain, + engine: BeaconEngine, headState: IBeaconStateView, subnet: SubnetID, data: Pick @@ -109,10 +109,10 @@ export function validateGossipSyncCommitteeExceptSig( const {slot, validatorIndex} = data; // [IGNORE] The signature's slot is for the current slot, i.e. sync_committee_signature.slot == current_slot. // (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) - if (!chain.clock.isCurrentSlotGivenGossipDisparity(slot)) { + if (!engine.clock.isCurrentSlotGivenGossipDisparity(slot)) { throw new SyncCommitteeError(GossipAction.IGNORE, { code: SyncCommitteeErrorCode.NOT_CURRENT_SLOT, - currentSlot: chain.clock.currentSlot, + currentSlot: engine.clock.currentSlot, slot, }); } diff --git a/packages/beacon-node/src/chain/validation/syncCommitteeContributionAndProof.ts b/packages/beacon-node/src/chain/validation/syncCommitteeContributionAndProof.ts index aed2867a9f0a..52850609a703 100644 --- a/packages/beacon-node/src/chain/validation/syncCommitteeContributionAndProof.ts +++ b/packages/beacon-node/src/chain/validation/syncCommitteeContributionAndProof.ts @@ -1,8 +1,8 @@ import {SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar/params"; import {IBeaconStateView, isStatePostAltair, isSyncCommitteeAggregator} from "@lodestar/state-transition"; import {ValidatorIndex, altair} from "@lodestar/types"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {GossipAction, SyncCommitteeError, SyncCommitteeErrorCode} from "../errors/index.js"; -import {IBeaconChain} from "../interface.js"; import { getContributionAndProofSignatureSet, getSyncCommitteeContributionSignatureSet, @@ -14,7 +14,7 @@ import {validateGossipSyncCommitteeExceptSig} from "./syncCommittee.js"; * Spec v1.1.0-beta.2 */ export async function validateSyncCommitteeGossipContributionAndProof( - chain: IBeaconChain, + engine: BeaconEngine, signedContributionAndProof: altair.SignedContributionAndProof, skipValidationKnownParticipants = false ): Promise<{syncCommitteeParticipantIndices: ValidatorIndex[]}> { @@ -22,8 +22,8 @@ export async function validateSyncCommitteeGossipContributionAndProof( const {contribution, aggregatorIndex} = contributionAndProof; const {subcommitteeIndex, slot} = contribution; - const headState = chain.getHeadState(); - validateGossipSyncCommitteeExceptSig(chain, headState, subcommitteeIndex, { + const headState = engine.getHeadState(); + validateGossipSyncCommitteeExceptSig(engine, headState, subcommitteeIndex, { slot, validatorIndex: contributionAndProof.aggregatorIndex, }); @@ -38,7 +38,7 @@ export async function validateSyncCommitteeGossipContributionAndProof( // _[IGNORE]_ A valid sync committee contribution with equal `slot`, `beacon_block_root` and `subcommittee_index` whose // `aggregation_bits` is non-strict superset has _not_ already been seen. - if (!skipValidationKnownParticipants && chain.seenContributionAndProof.participantsKnown(contribution)) { + if (!skipValidationKnownParticipants && engine.seenContributionAndProof.participantsKnown(contribution)) { throw new SyncCommitteeError(GossipAction.IGNORE, { code: SyncCommitteeErrorCode.SYNC_COMMITTEE_PARTICIPANTS_ALREADY_KNOWN, }); @@ -46,7 +46,7 @@ export async function validateSyncCommitteeGossipContributionAndProof( // [IGNORE] The sync committee contribution is the first valid contribution received for the aggregator with index // contribution_and_proof.aggregator_index for the slot contribution.slot and subcommittee index contribution.subcommittee_index. - if (chain.seenContributionAndProof.isAggregatorKnown(slot, subcommitteeIndex, aggregatorIndex)) { + if (engine.seenContributionAndProof.isAggregatorKnown(slot, subcommitteeIndex, aggregatorIndex)) { throw new SyncCommitteeError(GossipAction.IGNORE, { code: SyncCommitteeErrorCode.SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN, }); @@ -76,24 +76,24 @@ export async function validateSyncCommitteeGossipContributionAndProof( const signatureSets = [ // [REJECT] The contribution_and_proof.selection_proof is a valid signature of the SyncAggregatorSelectionData // derived from the contribution by the validator with index contribution_and_proof.aggregator_index. - getSyncCommitteeSelectionProofSignatureSet(chain.config, headState, contributionAndProof), + getSyncCommitteeSelectionProofSignatureSet(engine.config, headState, contributionAndProof), // [REJECT] The aggregator signature, signed_contribution_and_proof.signature, is valid. - getContributionAndProofSignatureSet(chain.config, headState, signedContributionAndProof), + getContributionAndProofSignatureSet(engine.config, headState, signedContributionAndProof), // [REJECT] The aggregate signature is valid for the message beacon_block_root and aggregate pubkey derived from // the participation info in aggregation_bits for the subcommittee specified by the contribution.subcommittee_index. - getSyncCommitteeContributionSignatureSet(chain.config, headState, contribution, syncCommitteeParticipantIndices), + getSyncCommitteeContributionSignatureSet(engine.config, headState, contribution, syncCommitteeParticipantIndices), ]; - if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true}))) { + if (!(await engine.bls.verifySignatureSets(signatureSets, {batchable: true}))) { throw new SyncCommitteeError(GossipAction.REJECT, { code: SyncCommitteeErrorCode.INVALID_SIGNATURE, }); } // no need to add to seenSyncCommittteeContributionCache here, gossip handler will do that - chain.seenContributionAndProof.add(contributionAndProof, syncCommitteeParticipantIndices.length); + engine.seenContributionAndProof.add(contributionAndProof, syncCommitteeParticipantIndices.length); return {syncCommitteeParticipantIndices}; } diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index f8166b772b12..600de99c6943 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -58,31 +58,17 @@ import { SyncCommitteeError, } from "../../chain/errors/index.js"; import {IBeaconChain} from "../../chain/interface.js"; -import {validateGossipBlobSidecar} from "../../chain/validation/blobSidecar.js"; -import { - validateGossipFuluDataColumnSidecar, - validateGossipGloasDataColumnSidecar, -} from "../../chain/validation/dataColumnSidecar.js"; -import {validateGossipExecutionPayloadBid} from "../../chain/validation/executionPayloadBid.js"; -import {validateGossipExecutionPayloadEnvelope} from "../../chain/validation/executionPayloadEnvelope.js"; import { AggregateAndProofValidationResult, GossipAttestation, toElectraSingleAttestation, - validateGossipAggregateAndProof, - validateGossipAttestationsSameAttData, validateGossipAttesterSlashing, - validateGossipBlock, validateGossipBlsToExecutionChange, validateGossipProposerSlashing, - validateGossipSyncCommittee, validateGossipVoluntaryExit, - validateSyncCommitteeGossipContributionAndProof, } from "../../chain/validation/index.js"; import {validateLightClientFinalityUpdate} from "../../chain/validation/lightClientFinalityUpdate.js"; import {validateLightClientOptimisticUpdate} from "../../chain/validation/lightClientOptimisticUpdate.js"; -import {validateGossipPayloadAttestationMessage} from "../../chain/validation/payloadAttestationMessage.js"; -import {validateGossipProposerPreferences} from "../../chain/validation/proposerPreferences.js"; import {OpSource} from "../../chain/validatorMonitor.js"; import {Metrics} from "../../metrics/index.js"; import {kzgCommitmentToVersionedHash} from "../../util/blobs.js"; @@ -149,6 +135,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand async function validateBeaconBlock( signedBlock: SignedBeaconBlock, + blockBytes: Uint8Array, fork: ForkName, peerIdStr: string, seenTimestampSec: number @@ -185,7 +172,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand peerIdStr, }); try { - const {skippedSlots} = await validateGossipBlock(config, chain, signedBlock, fork); + const {skippedSlots} = await chain.beaconEngine.validateGossipBlock(blockBytes, signedBlock, fork); if (isForkPostGloas(fork)) { chain.seenPayloadEnvelopeInputCache.add({ @@ -246,6 +233,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand async function validateBeaconBlob( blobSidecar: deneb.BlobSidecar, + blobBytes: Uint8Array, subnet: SubnetID, peerIdStr: string, seenTimestampSec: number @@ -260,7 +248,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const recvToValLatency = Date.now() / 1000 - seenTimestampSec; try { - await validateGossipBlobSidecar(fork, chain, blobSidecar, subnet); + await chain.beaconEngine.validateGossipBlobSidecar(blobBytes, fork, blobSidecar, subnet); const blockInput = chain.seenBlockInputCache.getByBlob({ blockRootHex, blobSidecar, @@ -331,7 +319,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand async function validateBeaconDataColumn( dataColumnSidecar: fulu.DataColumnSidecar, - _dataColumnBytes: Uint8Array, + dataColumnBytes: Uint8Array, gossipSubnet: SubnetID, peerIdStr: string, seenTimestampSec: number @@ -380,7 +368,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const recvToValLatency = Date.now() / 1000 - seenTimestampSec; try { - await validateGossipFuluDataColumnSidecar(chain, dataColumnSidecar, gossipSubnet, metrics); + await chain.beaconEngine.validateGossipFuluDataColumnSidecar(dataColumnBytes, dataColumnSidecar, gossipSubnet); const blockInput = chain.seenBlockInputCache.getByColumn({ blockRootHex, columnSidecar: dataColumnSidecar, @@ -442,6 +430,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand async function validatePayloadDataColumn( dataColumnSidecar: gloas.DataColumnSidecar, + dataColumnBytes: Uint8Array, gossipSubnet: SubnetID, peerIdStr: string, seenTimestampSec: number @@ -499,7 +488,12 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const recvToValLatency = Date.now() / 1000 - seenTimestampSec; try { - await validateGossipGloasDataColumnSidecar(chain, payloadInput, dataColumnSidecar, gossipSubnet, metrics); + await chain.beaconEngine.validateGossipGloasDataColumnSidecar( + dataColumnBytes, + payloadInput, + dataColumnSidecar, + gossipSubnet + ); const addedColumn = payloadInput.addColumn({ columnSidecar: dataColumnSidecar, @@ -685,7 +679,13 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const {serializedData} = gossipData; const signedBlock = sszDeserialize(topic, serializedData); - const blockInput = await validateBeaconBlock(signedBlock, topic.boundary.fork, peerIdStr, seenTimestampSec); + const blockInput = await validateBeaconBlock( + signedBlock, + serializedData, + topic.boundary.fork, + peerIdStr, + seenTimestampSec + ); chain.serializedCache.set(signedBlock, serializedData); handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec); }, @@ -704,7 +704,13 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand if (config.getForkSeq(blobSlot) < ForkSeq.deneb) { throw new GossipActionError(GossipAction.REJECT, {code: "PRE_DENEB_BLOCK"}); } - const blockInput = await validateBeaconBlob(blobSidecar, topic.subnet, peerIdStr, seenTimestampSec); + const blockInput = await validateBeaconBlob( + blobSidecar, + serializedData, + topic.subnet, + peerIdStr, + seenTimestampSec + ); chain.serializedCache.set(blobSidecar, serializedData); if (!blockInput.hasBlockAndAllData()) { const cutoffTimeMs = getCutoffTimeMs(chain, blobSlot, BLOCK_AVAILABILITY_CUTOFF_MS); @@ -756,6 +762,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand // After gloas, data columns are tracked in PayloadEnvelopeInput const payloadInput = await validatePayloadDataColumn( dataColumnSidecar, + serializedData, topic.subnet, peerIdStr, seenTimestampSec @@ -896,7 +903,11 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const {fork} = topic.boundary; try { - validationResult = await validateGossipAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData); + validationResult = await chain.beaconEngine.validateGossipAggregateAndProof( + serializedData, + fork, + signedAggregateAndProof + ); } catch (e) { if (e instanceof AttestationError && e.action === GossipAction.REJECT) { chain.persistInvalidSszValue( @@ -1002,15 +1013,14 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const contributionAndProof = sszDeserialize(topic, serializedData); - const {syncCommitteeParticipantIndices} = await validateSyncCommitteeGossipContributionAndProof( - chain, - contributionAndProof - ).catch((e) => { - if (e instanceof SyncCommitteeError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue(ssz.altair.SignedContributionAndProof, contributionAndProof, "gossip_reject"); - } - throw e; - }); + const {syncCommitteeParticipantIndices} = await chain.beaconEngine + .validateSyncCommitteeGossipContributionAndProof(serializedData, contributionAndProof) + .catch((e) => { + if (e instanceof SyncCommitteeError && e.action === GossipAction.REJECT) { + chain.persistInvalidSszValue(ssz.altair.SignedContributionAndProof, contributionAndProof, "gossip_reject"); + } + throw e; + }); // Handler chain.validatorMonitor?.registerGossipSyncContributionAndProof( @@ -1036,7 +1046,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const {subnet} = topic; let indicesInSubcommittee: number[] = [0]; try { - indicesInSubcommittee = (await validateGossipSyncCommittee(chain, syncCommittee, subnet)).indicesInSubcommittee; + indicesInSubcommittee = ( + await chain.beaconEngine.validateGossipSyncCommittee(serializedData, syncCommittee, subnet) + ).indicesInSubcommittee; } catch (e) { if (e instanceof SyncCommitteeError && e.action === GossipAction.REJECT) { chain.persistInvalidSszValue(ssz.altair.SyncCommitteeMessage, syncCommittee, "gossip_reject"); @@ -1104,7 +1116,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand // unlike BlockInput, we send the envelope into UnknownBlockInput sync // inside the sync it'll reconcile into PayloadEnvelopeInput and share the same cache with gossip try { - await validateGossipExecutionPayloadEnvelope(chain, signedEnvelope); + await chain.beaconEngine.validateGossipExecutionPayloadEnvelope(serializedData, signedEnvelope); } catch (e) { if (e instanceof ExecutionPayloadEnvelopeError) { const {beaconBlockRoot} = signedEnvelope.message; @@ -1207,7 +1219,10 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const payloadAttestationMessage = sszDeserialize(topic, serializedData); - const validationResult = await validateGossipPayloadAttestationMessage(chain, payloadAttestationMessage); + const validationResult = await chain.beaconEngine.validateGossipPayloadAttestationMessage( + serializedData, + payloadAttestationMessage + ); try { const insertOutcome = chain.payloadAttestationPool.add( @@ -1233,7 +1248,10 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const executionPayloadBid = sszDeserialize(topic, serializedData); - const {proposerIndex} = await validateGossipExecutionPayloadBid(chain, executionPayloadBid); + const {proposerIndex} = await chain.beaconEngine.validateGossipExecutionPayloadBid( + serializedData, + executionPayloadBid + ); // Handle valid payload bid by storing in a bid pool try { @@ -1256,7 +1274,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const signedProposerPreferences = sszDeserialize(topic, serializedData); - await validateGossipProposerPreferences(chain, signedProposerPreferences); + await chain.beaconEngine.validateGossipProposerPreferences(serializedData, signedProposerPreferences); chain.proposerPreferencesPool.add(signedProposerPreferences); chain.emitter.emit(routes.events.EventType.proposerPreferences, { @@ -1290,9 +1308,8 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp attDataBase64: param.gossipData.indexed, subnet: param.topic.subnet, })) as GossipAttestation[]; - const {results: validationResults, batchableBls} = await validateGossipAttestationsSameAttData( + const {results: validationResults, batchableBls} = await chain.beaconEngine.validateGossipAttestationsSameAttData( fork, - chain, validationParams ); for (const [i, validationResult] of validationResults.entries()) { diff --git a/packages/beacon-node/src/sync/unknownBlock.ts b/packages/beacon-node/src/sync/unknownBlock.ts index 7e9856ef5171..e1c901cb9921 100644 --- a/packages/beacon-node/src/sync/unknownBlock.ts +++ b/packages/beacon-node/src/sync/unknownBlock.ts @@ -3,7 +3,7 @@ import {ChainForkConfig} from "@lodestar/config"; import {ForkSeq} from "@lodestar/params"; import {RequestError, RequestErrorCode} from "@lodestar/reqresp"; import {computeTimeAtSlot} from "@lodestar/state-transition"; -import {RootHex, Slot, gloas} from "@lodestar/types"; +import {RootHex, Slot, gloas, ssz} from "@lodestar/types"; import {Logger, fromHex, prettyPrintIndices, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils"; import {isBlockInputBlobs, isBlockInputColumns} from "../chain/blocks/blockInput/blockInput.js"; import {BlockInputSource, IBlockInput} from "../chain/blocks/blockInput/types.js"; @@ -12,7 +12,6 @@ import {PayloadEnvelopeInput, PayloadEnvelopeInputSource} from "../chain/blocks/ import {BlockError, BlockErrorCode} from "../chain/errors/index.js"; import {ChainEvent, ChainEventData, IBeaconChain} from "../chain/index.js"; import {validateGloasBlockDataColumnSidecars} from "../chain/validation/dataColumnSidecar.js"; -import {validateGossipExecutionPayloadEnvelope} from "../chain/validation/executionPayloadEnvelope.js"; import {Metrics} from "../metrics/index.js"; import {INetwork, NetworkEvent, NetworkEventData, prettyPrintPeerIdStr} from "../network/index.js"; import {PeerSyncMeta} from "../network/peers/peersData.js"; @@ -896,8 +895,11 @@ export class BlockInputSync { } if (!payloadInput.hasPayloadEnvelope()) { + const envelopeBytes = + this.chain.serializedCache.get(pendingPayload.envelope) ?? + ssz.gloas.SignedExecutionPayloadEnvelope.serialize(pendingPayload.envelope); const validationResult = await wrapError( - validateGossipExecutionPayloadEnvelope(this.chain, pendingPayload.envelope) + this.chain.beaconEngine.validateGossipExecutionPayloadEnvelope(envelopeBytes, pendingPayload.envelope) ); if (validationResult.err) { this.logger.debug( @@ -1125,7 +1127,9 @@ export class BlockInputSync { } if (!payloadInput.hasPayloadEnvelope()) { - await validateGossipExecutionPayloadEnvelope(this.chain, envelope); + const envelopeBytes = + this.chain.serializedCache.get(envelope) ?? ssz.gloas.SignedExecutionPayloadEnvelope.serialize(envelope); + await this.chain.beaconEngine.validateGossipExecutionPayloadEnvelope(envelopeBytes, envelope); } let pendingPayload = this.toPendingPayloadInput(payloadInput, cacheItem, envelope); diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index ab74100273ca..721a9c3fb257 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -204,11 +204,15 @@ export type MockedBeaconChainOptions = { export function getMockedBeaconChain(opts?: Partial): MockedBeaconChain { const {clock, genesisTime, config} = opts ?? {}; // @ts-expect-error - return new BeaconChain({ + const chain = new BeaconChain({ clock: clock ?? "fake", genesisTime: genesisTime ?? 0, config: config ?? defaultConfig, }) as MockedBeaconChain; + // The gossip validators are now BeaconEngine methods that read engine collaborators. In this flat + // mock the facade doubles as the engine, so `chain.beaconEngine.forkChoice === chain.forkChoice` etc. + (chain as {beaconEngine: unknown}).beaconEngine = chain; + return chain; } export type MockedForkChoice = Mocked; diff --git a/packages/beacon-node/test/perf/chain/validation/aggregateAndProof.test.ts b/packages/beacon-node/test/perf/chain/validation/aggregateAndProof.test.ts index cdd23b07ea73..daf454a59976 100644 --- a/packages/beacon-node/test/perf/chain/validation/aggregateAndProof.test.ts +++ b/packages/beacon-node/test/perf/chain/validation/aggregateAndProof.test.ts @@ -1,6 +1,7 @@ import {bench, describe} from "@chainsafe/benchmark"; import {generateTestCachedBeaconStateOnlyValidators} from "@lodestar/state-transition/test-utils"; import {ssz} from "@lodestar/types"; +import type {BeaconEngine} from "../../../../src/chain/beaconEngine/beaconEngine.js"; import {validateApiAggregateAndProof, validateGossipAggregateAndProof} from "../../../../src/chain/validation/index.js"; import {getAggregateAndProofValidData} from "../../../utils/validationData/aggregateAndProof.js"; @@ -26,7 +27,7 @@ describe("validate gossip signedAggregateAndProof", () => { }, fn: async () => { const fork = chain.config.getForkName(stateSlot); - await validateApiAggregateAndProof(fork, chain, agg); + await validateApiAggregateAndProof(fork, chain.beaconEngine as BeaconEngine, agg); }, }); @@ -38,7 +39,7 @@ describe("validate gossip signedAggregateAndProof", () => { }, fn: async () => { const fork = chain.config.getForkName(stateSlot); - await validateGossipAggregateAndProof(fork, chain, agg, serializedData); + await validateGossipAggregateAndProof(fork, chain.beaconEngine as BeaconEngine, agg, serializedData); }, }); } diff --git a/packages/beacon-node/test/perf/chain/validation/attestation.test.ts b/packages/beacon-node/test/perf/chain/validation/attestation.test.ts index 2ddc6ba7e67f..9b2ddefaa4db 100644 --- a/packages/beacon-node/test/perf/chain/validation/attestation.test.ts +++ b/packages/beacon-node/test/perf/chain/validation/attestation.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert"; import {bench, describe} from "@chainsafe/benchmark"; import {generateTestCachedBeaconStateOnlyValidators} from "@lodestar/state-transition/test-utils"; import {ssz} from "@lodestar/types"; +import type {BeaconEngine} from "../../../../src/chain/beaconEngine/beaconEngine.js"; import {validateGossipAttestationsSameAttData} from "../../../../src/chain/validation/index.js"; import {getAttDataFromAttestationSerialized} from "../../../../src/util/sszBytes.js"; import {getAttestationValidData} from "../../../utils/validationData/attestation.js"; @@ -53,7 +54,7 @@ describe("validate gossip attestation", () => { id: `batch validate gossip attestation - vc ${vc} - chunk ${chunkSize}`, beforeEach: () => chain.seenAttesters["validatorIndexesByEpoch"].clear(), fn: async () => { - await validateGossipAttestationsSameAttData(fork, chain, attestationOrBytesArr); + await validateGossipAttestationsSameAttData(fork, chain.beaconEngine as BeaconEngine, attestationOrBytesArr); }, runsFactor: chunkSize, }); diff --git a/packages/beacon-node/test/spec/utils/gossipValidation.ts b/packages/beacon-node/test/spec/utils/gossipValidation.ts index c8a8771751e3..37ad7128949e 100644 --- a/packages/beacon-node/test/spec/utils/gossipValidation.ts +++ b/packages/beacon-node/test/spec/utils/gossipValidation.ts @@ -33,11 +33,8 @@ import {defaultChainOptions} from "../../../src/chain/options.js"; import {validateGossipAggregateAndProof} from "../../../src/chain/validation/aggregateAndProof.js"; import {GossipAttestation, validateGossipAttestationsSameAttData} from "../../../src/chain/validation/attestation.js"; import {validateGossipAttesterSlashing} from "../../../src/chain/validation/attesterSlashing.js"; -import {validateGossipBlock} from "../../../src/chain/validation/block.js"; import {validateGossipBlsToExecutionChange} from "../../../src/chain/validation/blsToExecutionChange.js"; import {validateGossipProposerSlashing} from "../../../src/chain/validation/proposerSlashing.js"; -import {validateGossipSyncCommittee} from "../../../src/chain/validation/syncCommittee.js"; -import {validateSyncCommitteeGossipContributionAndProof} from "../../../src/chain/validation/syncCommitteeContributionAndProof.js"; import {validateGossipVoluntaryExit} from "../../../src/chain/validation/voluntaryExit.js"; import {ZERO_HASH_HEX} from "../../../src/constants/constants.js"; import {ExecutionEngineMockBackend} from "../../../src/execution/engine/mock.js"; @@ -610,7 +607,7 @@ async function validateMessageForTopic( throw new GossipActionError(GossipAction.REJECT, {code: "SPEC_FINALIZED_NOT_ANCESTOR"}); } - await validateGossipBlock(chain.config, chain, signedBlock, fork); + await chain.beaconEngine.validateGossipBlock(bytes, signedBlock, fork); chain.seenBlockProposers.add(signedBlock.message.slot, signedBlock.message.proposerIndex); break; } @@ -632,7 +629,7 @@ async function validateMessageForTopic( throw new GossipActionError(GossipAction.IGNORE, {code: "SPEC_FINALIZED_NOT_ANCESTOR"}); } - await validateGossipAggregateAndProof(fork, chain, aggregate, bytes); + await validateGossipAggregateAndProof(fork, chain.beaconEngine, aggregate, bytes); break; } @@ -665,7 +662,7 @@ async function validateMessageForTopic( subnet: Number(message.subnet_id ?? 0), }; - const batchResult = await validateGossipAttestationsSameAttData(fork, chain, [gossipAttestation]); + const batchResult = await validateGossipAttestationsSameAttData(fork, chain.beaconEngine, [gossipAttestation]); const first = batchResult.results[0]; if (first?.err) throw first.err; break; @@ -700,7 +697,7 @@ async function validateMessageForTopic( const syncCommitteeMessage = rejectOnInvalidSerializedBytes(() => ssz.altair.SyncCommitteeMessage.deserialize(bytes) ); - await validateGossipSyncCommittee(chain, syncCommitteeMessage, Number(message.subnet_id ?? 0)); + await chain.beaconEngine.validateGossipSyncCommittee(bytes, syncCommitteeMessage, Number(message.subnet_id ?? 0)); break; } @@ -708,7 +705,7 @@ async function validateMessageForTopic( const signedContributionAndProof = rejectOnInvalidSerializedBytes(() => ssz.altair.SignedContributionAndProof.deserialize(bytes) ); - await validateSyncCommitteeGossipContributionAndProof(chain, signedContributionAndProof); + await chain.beaconEngine.validateSyncCommitteeGossipContributionAndProof(bytes, signedContributionAndProof); break; } diff --git a/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts b/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts index 6cf39e084adf..9dc77cf16898 100644 --- a/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts @@ -3,6 +3,7 @@ import {BitArray, toHexString} from "@chainsafe/ssz"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {generateTestCachedBeaconStateOnlyValidators} from "@lodestar/state-transition/test-utils"; import {phase0, ssz} from "@lodestar/types"; +import type {BeaconEngine} from "../../../../src/chain/beaconEngine/beaconEngine.js"; import {AttestationErrorCode} from "../../../../src/chain/errors/index.js"; import {IBeaconChain} from "../../../../src/chain/index.js"; import {validateApiAggregateAndProof, validateGossipAggregateAndProof} from "../../../../src/chain/validation/index.js"; @@ -40,7 +41,7 @@ describe("chain / validation / aggregateAndProof", () => { const {chain, signedAggregateAndProof} = getValidData({}); const fork = chain.config.getForkName(stateSlot); - await validateApiAggregateAndProof(fork, chain, signedAggregateAndProof); + await validateApiAggregateAndProof(fork, chain.beaconEngine as BeaconEngine, signedAggregateAndProof); }); it("BAD_TARGET_EPOCH", async () => { @@ -186,7 +187,12 @@ describe("chain / validation / aggregateAndProof", () => { const fork = chain.config.getForkName(stateSlot); const serializedData = ssz.phase0.SignedAggregateAndProof.serialize(signedAggregateAndProof); await expectRejectedWithLodestarError( - validateGossipAggregateAndProof(fork, chain, signedAggregateAndProof, serializedData), + validateGossipAggregateAndProof( + fork, + chain.beaconEngine as BeaconEngine, + signedAggregateAndProof, + serializedData + ), errorCode ); } diff --git a/packages/beacon-node/test/unit/chain/validation/attestation/getShufflingForAttestationVerification.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation/getShufflingForAttestationVerification.test.ts index 14f0881ce208..6cb921123855 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation/getShufflingForAttestationVerification.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation/getShufflingForAttestationVerification.test.ts @@ -51,7 +51,7 @@ describe("getShufflingForAttestationVerification", () => { return Promise.resolve(null); }); const resultShuffling = await getShufflingForAttestationVerification( - chain, + chain.beaconEngine, attEpoch, attHeadBlock as ProtoBlock, RegenCaller.validateGossipAttestation @@ -81,7 +81,7 @@ describe("getShufflingForAttestationVerification", () => { return Promise.resolve(null); }); const resultShuffling = await getShufflingForAttestationVerification( - chain, + chain.beaconEngine, attEpoch, attHeadBlock as ProtoBlock, RegenCaller.validateGossipAttestation @@ -108,10 +108,12 @@ describe("getShufflingForAttestationVerification", () => { } return Promise.resolve(null); }); - chain.regenStateForAttestationVerification.mockImplementationOnce(() => Promise.resolve(expectedShuffling)); + vi.mocked(chain.beaconEngine.regenStateForAttestationVerification).mockImplementationOnce(() => + Promise.resolve(expectedShuffling) + ); const resultShuffling = await getShufflingForAttestationVerification( - chain, + chain.beaconEngine, attEpoch, attHeadBlock as ProtoBlock, RegenCaller.validateGossipAttestation @@ -130,7 +132,7 @@ describe("getShufflingForAttestationVerification", () => { } as Partial; try { await getShufflingForAttestationVerification( - chain, + chain.beaconEngine, attEpoch, attHeadBlock as ProtoBlock, RegenCaller.validateGossipAttestation diff --git a/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts index 99c5d63c9bf2..eb90b53b138a 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts @@ -4,6 +4,7 @@ import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; import {generateTestCachedBeaconStateOnlyValidators} from "@lodestar/state-transition/test-utils"; import {ssz} from "@lodestar/types"; import {LodestarError} from "@lodestar/utils"; +import type {BeaconEngine} from "../../../../../src/chain/beaconEngine/beaconEngine.js"; import {AttestationErrorCode, GossipErrorCode} from "../../../../../src/chain/errors/index.js"; import {IBeaconChain} from "../../../../../src/chain/index.js"; import { @@ -47,7 +48,7 @@ describe("validateAttestation", () => { const {chain, attestation} = getValidData(); const fork = chain.config.getForkName(stateSlot); - await validateApiAttestation(fork, chain, {attestation, serializedData: null}); + await validateApiAttestation(fork, chain.beaconEngine as BeaconEngine, {attestation, serializedData: null}); }); it("INVALID_SERIALIZED_BYTES_ERROR_CODE", async () => { @@ -302,7 +303,10 @@ describe("validateAttestation", () => { errorCode: string ): Promise { const fork = chain.config.getForkName(stateSlot); - await expectRejectedWithLodestarError(validateApiAttestation(fork, chain, attestationOrBytes), errorCode); + await expectRejectedWithLodestarError( + validateApiAttestation(fork, chain.beaconEngine as BeaconEngine, attestationOrBytes), + errorCode + ); } async function expectGossipError( @@ -311,7 +315,9 @@ describe("validateAttestation", () => { errorCode: string ): Promise { const fork = chain.config.getForkName(stateSlot); - const {results} = await validateGossipAttestationsSameAttData(fork, chain, [attestationOrBytes]); + const {results} = await validateGossipAttestationsSameAttData(fork, chain.beaconEngine as BeaconEngine, [ + attestationOrBytes, + ]); expect(results.length).toEqual(1); expect((results[0].err as LodestarError<{code: string}>).type.code).toEqual(errorCode); } diff --git a/packages/beacon-node/test/unit/chain/validation/attestation/validateGossipAttestationsSameAttData.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation/validateGossipAttestationsSameAttData.test.ts index 7f8d858f9c51..5556d4b6a157 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation/validateGossipAttestationsSameAttData.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation/validateGossipAttestationsSameAttData.test.ts @@ -3,9 +3,9 @@ import {PublicKey, SecretKey} from "@chainsafe/blst"; import {ForkName} from "@lodestar/params"; import {SignatureSetType, createPubkeyCache} from "@lodestar/state-transition"; import {ssz} from "@lodestar/types"; +import type {BeaconEngine} from "../../../../../src/chain/beaconEngine/beaconEngine.js"; import {BlsSingleThreadVerifier} from "../../../../../src/chain/bls/singleThread.js"; import {AttestationError, AttestationErrorCode, GossipAction} from "../../../../../src/chain/errors/index.js"; -import {IBeaconChain} from "../../../../../src/chain/index.js"; import {SeenAttesters} from "../../../../../src/chain/seenCache/seenAttesters.js"; import {Step0Result, validateGossipAttestationsSameAttData} from "../../../../../src/chain/validation/index.js"; @@ -64,18 +64,18 @@ describe("validateGossipAttestationsSameAttData", () => { // Add a special keypair for invalid signatures pubkeyCache.set(2023, getKeypair(2023).publicKey.toBytes()); - let chain: IBeaconChain; + let engine: BeaconEngine; const signingRoot = Buffer.alloc(32, 1); beforeEach(() => { - chain = { + engine = { bls: new BlsSingleThreadVerifier({metrics: null, pubkeyCache}), seenAttesters: new SeenAttesters(), pubkeyCache, opts: { minSameMessageSignatureSetsToBatch: 2, - } as IBeaconChain["opts"], - } as Partial as IBeaconChain; + } as BeaconEngine["opts"], + } as Partial as BeaconEngine; }); afterEach(() => { @@ -124,12 +124,12 @@ describe("validateGossipAttestationsSameAttData", () => { callIndex++; return result; }; - await validateGossipAttestationsSameAttData(ForkName.phase0, chain, new Array(5).fill({}), phase0ValidationFn); + await validateGossipAttestationsSameAttData(ForkName.phase0, engine, new Array(5).fill({}), phase0ValidationFn); for (let validatorIndex = 0; validatorIndex < phase0Result.length; validatorIndex++) { if (seenAttesters.includes(validatorIndex)) { - expect(chain.seenAttesters.isKnown(0, validatorIndex)).toBe(true); + expect(engine.seenAttesters.isKnown(0, validatorIndex)).toBe(true); } else { - expect(chain.seenAttesters.isKnown(0, validatorIndex)).toBe(false); + expect(engine.seenAttesters.isKnown(0, validatorIndex)).toBe(false); } } }); // end test case 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 b42b78f7aaec..a2084cb70561 100644 --- a/packages/beacon-node/test/unit/chain/validation/block.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/block.test.ts @@ -33,6 +33,12 @@ describe("gossip block validation", () => { DENEB_FORK_EPOCH: 0, }); const config = createBeaconConfig(configDef, Buffer.alloc(32, 0xaa)); + // validateGossipBlock now reads the engine's BeaconConfig for both fork types and signatures, so the + // deneb tests need a deneb-configured BeaconConfig on the chain (was passed as a separate arg before). + const denebBeaconConfig = createBeaconConfig( + {...configDef, ALTAIR_FORK_EPOCH: 0, BELLATRIX_FORK_EPOCH: 0, CAPELLA_FORK_EPOCH: 0, DENEB_FORK_EPOCH: 0}, + Buffer.alloc(32, 0xaa) + ); beforeEach(() => { chain = getMockedBeaconChain({config}); @@ -67,7 +73,7 @@ describe("gossip block validation", () => { const signedBlock = {signature, message: {...block, slot: clockSlot + 1}}; await expectRejectedWithLodestarError( - validateGossipBlock(config, chain, signedBlock, ForkName.phase0), + validateGossipBlock(chain.beaconEngine, signedBlock, ForkName.phase0), BlockErrorCode.FUTURE_SLOT ); }); @@ -81,7 +87,7 @@ describe("gossip block validation", () => { }); await expectRejectedWithLodestarError( - validateGossipBlock(config, chain, job, ForkName.phase0), + validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT ); }); @@ -91,7 +97,7 @@ describe("gossip block validation", () => { forkChoice.getBlockHexDefaultStatus.mockReturnValue({} as ProtoBlock); await expectRejectedWithLodestarError( - validateGossipBlock(config, chain, job, ForkName.phase0), + validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.ALREADY_KNOWN ); }); @@ -101,7 +107,7 @@ describe("gossip block validation", () => { chain.seenBlockProposers.add(job.message.slot, job.message.proposerIndex); await expectRejectedWithLodestarError( - validateGossipBlock(config, chain, job, ForkName.phase0), + validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.REPEAT_PROPOSAL ); }); @@ -113,7 +119,7 @@ describe("gossip block validation", () => { forkChoice.getBlockHexDefaultStatus.mockReturnValueOnce(null); await expectRejectedWithLodestarError( - validateGossipBlock(config, chain, job, ForkName.phase0), + validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.PARENT_UNKNOWN ); }); @@ -125,7 +131,7 @@ describe("gossip block validation", () => { forkChoice.getBlockHexDefaultStatus.mockReturnValueOnce({slot: clockSlot + 1} as ProtoBlock); await expectRejectedWithLodestarError( - validateGossipBlock(config, chain, job, ForkName.phase0), + validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.NOT_LATER_THAN_PARENT ); }); @@ -139,7 +145,7 @@ describe("gossip block validation", () => { regen.getPreState.mockRejectedValue(undefined); await expectRejectedWithLodestarError( - validateGossipBlock(config, chain, job, ForkName.phase0), + validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.PARENT_UNKNOWN ); }); @@ -155,7 +161,7 @@ describe("gossip block validation", () => { verifySignature.mockResolvedValue(false); await expectRejectedWithLodestarError( - validateGossipBlock(config, chain, job, ForkName.phase0), + validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.PROPOSAL_SIGNATURE_INVALID ); }); @@ -174,7 +180,7 @@ describe("gossip block validation", () => { vi.spyOn(state.cachedState.epochCtx, "getBeaconProposer").mockReturnValue(proposerIndex + 1); await expectRejectedWithLodestarError( - validateGossipBlock(config, chain, job, ForkName.phase0), + validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.INCORRECT_PROPOSER ); }); @@ -192,10 +198,11 @@ describe("gossip block validation", () => { // Force proposer shuffling cache to return correct value vi.spyOn(state.cachedState.epochCtx, "getBeaconProposer").mockReturnValue(proposerIndex); - await validateGossipBlock(config, chain, job, ForkName.phase0); + await validateGossipBlock(chain.beaconEngine, job, ForkName.phase0); }); it("deneb - TOO_MANY_KZG_COMMITMENTS", async () => { + (chain as {config: typeof config}).config = denebBeaconConfig; // Fill up with kzg commitments block.body.blobKzgCommitments = Array.from( {length: denebConfig.getMaxBlobsPerBlock(denebConfig.DENEB_FORK_EPOCH)}, @@ -216,12 +223,13 @@ describe("gossip block validation", () => { (job as SignedBeaconBlock).message.body.blobKzgCommitments.push(new Uint8Array([0])); await expectRejectedWithLodestarError( - validateGossipBlock(denebConfig, chain, job, ForkName.deneb), + validateGossipBlock(chain.beaconEngine, job, ForkName.deneb), BlockErrorCode.TOO_MANY_KZG_COMMITMENTS ); }); it("deneb - valid", async () => { + (chain as {config: typeof config}).config = denebBeaconConfig; // Fill up with kzg commitments block.body.blobKzgCommitments = Array.from( {length: denebConfig.getMaxBlobsPerBlock(denebConfig.DENEB_FORK_EPOCH)}, @@ -240,6 +248,6 @@ describe("gossip block validation", () => { vi.spyOn(state.cachedState.epochCtx, "getBeaconProposer").mockReturnValue(proposerIndex); // Keep number of kzg commitments as is so it stays within the limit - await validateGossipBlock(denebConfig, chain, job, ForkName.deneb); + await validateGossipBlock(chain.beaconEngine, job, ForkName.deneb); }); }); diff --git a/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts b/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts index ec7928ffa99c..5f42d14aa97e 100644 --- a/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts @@ -59,7 +59,7 @@ describe("Sync Committee Signature validation", () => { const syncCommittee = getSyncCommitteeSignature(1, 0); await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain, syncCommittee, 0), + validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, 0), SyncCommitteeErrorCode.NOT_CURRENT_SLOT ); }); @@ -70,7 +70,7 @@ describe("Sync Committee Signature validation", () => { chain.getHeadState.mockReturnValue(headState); chain.seenSyncCommitteeMessages.get = () => toHexString(syncCommittee.beaconBlockRoot); await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain, syncCommittee, 0), + validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, 0), SyncCommitteeErrorCode.SYNC_COMMITTEE_MESSAGE_KNOWN ); }); @@ -83,7 +83,7 @@ describe("Sync Committee Signature validation", () => { chain.seenSyncCommitteeMessages.get = () => prevRoot; forkchoiceStub.getHeadRoot.mockReturnValue(prevRoot); await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain, syncCommittee, 0), + validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, 0), SyncCommitteeErrorCode.SYNC_COMMITTEE_MESSAGE_KNOWN ); }); @@ -94,7 +94,7 @@ describe("Sync Committee Signature validation", () => { chain.getHeadState.mockReturnValue(headState); await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain, syncCommittee, 0), + validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, 0), SyncCommitteeErrorCode.VALIDATOR_NOT_IN_SYNC_COMMITTEE ); }); @@ -108,7 +108,7 @@ describe("Sync Committee Signature validation", () => { const headState = new BeaconStateView(generateCachedAltairState({slot: currentSlot}, altairForkEpoch)); chain.getHeadState.mockReturnValue(headState); await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain, syncCommittee, 0), + validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, 0), SyncCommitteeErrorCode.INVALID_SUBCOMMITTEE_INDEX ); }); @@ -120,7 +120,7 @@ describe("Sync Committee Signature validation", () => { chain.getHeadState.mockReturnValue(headState); chain.bls.verifySignatureSets.mockReturnValue(false); await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain, syncCommittee, 0), + validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, 0), SyncCommitteeErrorCode.INVALID_SIGNATURE ); }); @@ -134,14 +134,14 @@ describe("Sync Committee Signature validation", () => { chain.getHeadState.mockReturnValue(headState); // "should be null" expect(chain.seenSyncCommitteeMessages.get(slot, subnet, validatorIndex)).toBeNull(); - await validateGossipSyncCommittee(chain, syncCommittee, subnet); + await validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, subnet); expect(chain.seenSyncCommitteeMessages.get(slot, subnet, validatorIndex)).toBe( toHexString(syncCommittee.beaconBlockRoot) ); // receive same message again await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain, syncCommittee, subnet), + validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, subnet), SyncCommitteeErrorCode.SYNC_COMMITTEE_MESSAGE_KNOWN ); }); @@ -159,7 +159,7 @@ describe("Sync Committee Signature validation", () => { expect(chain.seenSyncCommitteeMessages.get(slot, subnet, validatorIndex)).toBe(prevRoot); // but forkchoice head is message root forkchoiceStub.getHeadRoot.mockReturnValue(toHexString(syncCommittee.beaconBlockRoot)); - await validateGossipSyncCommittee(chain, syncCommittee, subnet); + await validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, subnet); // should accept the message and overwrite prevRoot expect(chain.seenSyncCommitteeMessages.get(slot, subnet, validatorIndex)).toBe( toHexString(syncCommittee.beaconBlockRoot) @@ -167,7 +167,7 @@ describe("Sync Committee Signature validation", () => { // receive same message again await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain, syncCommittee, subnet), + validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, subnet), SyncCommitteeErrorCode.SYNC_COMMITTEE_MESSAGE_KNOWN ); }); diff --git a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts index e661ebbd236a..e40d2dcbab54 100644 --- a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts +++ b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts @@ -667,7 +667,10 @@ describe("UnknownBlockSync", () => { custodyConfig, genesisTime: 0, metrics: null, - serializedCache: {delete: vi.fn()} as unknown as IBeaconChain["serializedCache"], + serializedCache: { + get: vi.fn().mockReturnValue(undefined), + delete: vi.fn(), + } as unknown as IBeaconChain["serializedCache"], getBlockByRoot: vi.fn().mockResolvedValue(null), processExecutionPayload: vi.fn().mockResolvedValue(undefined), seenPayloadEnvelopeInputCache: { @@ -682,6 +685,11 @@ describe("UnknownBlockSync", () => { hasBlockHex: vi.fn().mockReturnValue(false), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), } as unknown as IForkChoice, + // Envelope validation is now a BeaconEngine method; route it to the mocked module fn so the + // existing `validateGossipExecutionPayloadEnvelope` assertions still observe the calls. + beaconEngine: { + validateGossipExecutionPayloadEnvelope, + } as unknown as IBeaconChain["beaconEngine"], ...chainOverrides, } as IBeaconChain; diff --git a/packages/beacon-node/test/unit/util/kzg.test.ts b/packages/beacon-node/test/unit/util/kzg.test.ts index c6e9c9afbb08..b8b7dfecafcf 100644 --- a/packages/beacon-node/test/unit/util/kzg.test.ts +++ b/packages/beacon-node/test/unit/util/kzg.test.ts @@ -66,7 +66,7 @@ describe("KZG", () => { for (const blobSidecar of blobSidecars) { try { - await validateGossipBlobSidecar(fork, chain, blobSidecar, blobSidecar.index); + await validateGossipBlobSidecar(chain.beaconEngine, fork, blobSidecar, blobSidecar.index); } catch (_e) { // We expect some error from here // console.log(error); diff --git a/packages/beacon-node/test/utils/validationData/attestation.ts b/packages/beacon-node/test/utils/validationData/attestation.ts index 1894a9b89b39..8c161622d5eb 100644 --- a/packages/beacon-node/test/utils/validationData/attestation.ts +++ b/packages/beacon-node/test/utils/validationData/attestation.ts @@ -180,6 +180,9 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { shufflingCache, opts: defaultChainOptions, } as Partial as IBeaconChain; + // The gossip validators are BeaconEngine methods reading engine collaborators; in this flat fixture + // the facade doubles as the engine, so `chain.beaconEngine` exposes the same collaborators. + (chain as {beaconEngine: unknown}).beaconEngine = chain; return {chain, attestation, subnet, validatorIndex}; } From b2aeb953cab074a81217c2e3556b4c74f505bfc3 Mon Sep 17 00:00:00 2001 From: twoeths Date: Fri, 19 Jun 2026 14:39:45 +0700 Subject: [PATCH 06/24] fix: gossip validation fn should not throw error --- .../src/api/impl/beacon/blocks/index.ts | 54 +- .../src/api/impl/beacon/pool/index.ts | 138 ++-- .../src/api/impl/validator/index.ts | 86 ++- .../src/chain/beaconEngine/beaconEngine.ts | 101 +-- .../beaconEngine/gossipValidationResult.ts | 90 +++ .../src/chain/beaconEngine/index.ts | 1 + .../src/chain/beaconEngine/interface.ts | 58 +- .../src/network/gossip/interface.ts | 11 +- .../src/network/processor/gossipHandlers.ts | 694 +++++++++--------- .../network/processor/gossipValidatorFn.ts | 92 +-- packages/beacon-node/src/sync/unknownBlock.ts | 15 +- .../test/e2e/network/gossipsub.test.ts | 7 + .../test/spec/utils/gossipValidation.ts | 28 +- .../test/unit/sync/unknownBlock.test.ts | 8 +- 14 files changed, 756 insertions(+), 627 deletions(-) create mode 100644 packages/beacon-node/src/chain/beaconEngine/gossipValidationResult.ts diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index db0228f3d87b..795ecbd2e7e9 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -34,13 +34,14 @@ import { sszTypesFor, } from "@lodestar/types"; import {fromHex, sleep, toHex, toRootHex} from "@lodestar/utils"; +import {GossipValidationStatus} from "../../../../chain/beaconEngine/gossipValidationResult.js"; import {BlockInputSource, isBlockInputBlobs, isBlockInputColumns} from "../../../../chain/blocks/blockInput/index.js"; import {PayloadEnvelopeInputSource} from "../../../../chain/blocks/payloadEnvelopeInput/index.js"; import {ImportBlockOpts} from "../../../../chain/blocks/types.js"; import {verifyBlocksInEpoch} from "../../../../chain/blocks/verifyBlock.js"; import {BeaconChain} from "../../../../chain/chain.js"; import {ChainEvent} from "../../../../chain/emitter.js"; -import {BlockError, BlockErrorCode, BlockGossipError} from "../../../../chain/errors/index.js"; +import {BlockError, BlockErrorCode} from "../../../../chain/errors/index.js"; import { BlockType, ProduceFullBellatrix, @@ -198,33 +199,30 @@ export function getBeaconBlockApi({ switch (broadcastValidation) { case routes.beacon.BroadcastValidation.gossip: { if (!blockLocallyProduced) { - try { - // TODO - engine: get block bytes from upstream - const blockBytes = config.getForkTypes(slot).SignedBeaconBlock.serialize(signedBlock); - await chain.beaconEngine.validateGossipBlock(blockBytes, signedBlock, fork); - } catch (error) { - if (error instanceof BlockGossipError) { - switch (error.type.code) { - case BlockErrorCode.ALREADY_KNOWN: - // Block has already been seen, e.g. via gossip racing the publish API. Benign. - chain.logger.debug("Ignoring already-known block during publishing", valLogMeta); - return; - case BlockErrorCode.REPEAT_PROPOSAL: - // The proposer already produced a block for this slot. For a solo setup this is a - // notable signal (duplicate-proposal attempt). For fallback / DVT setups it is - // expected on every block where another node published first. - chain.logger.warn("Ignoring repeat-proposal block during publishing", valLogMeta); - return; - } + // TODO - engine: get block bytes from upstream + const blockBytes = config.getForkTypes(slot).SignedBeaconBlock.serialize(signedBlock); + const res = await chain.beaconEngine.validateGossipBlock(blockBytes, signedBlock, fork); + if (res.status !== GossipValidationStatus.Accept) { + switch (res.code) { + case BlockErrorCode.ALREADY_KNOWN: + // Block has already been seen, e.g. via gossip racing the publish API. Benign. + chain.logger.debug("Ignoring already-known block during publishing", valLogMeta); + return; + case BlockErrorCode.REPEAT_PROPOSAL: + // The proposer already produced a block for this slot. For a solo setup this is a + // notable signal (duplicate-proposal attempt). For fallback / DVT setups it is + // expected on every block where another node published first. + chain.logger.warn("Ignoring repeat-proposal block during publishing", valLogMeta); + return; } - chain.logger.error("Gossip validations failed while publishing the block", valLogMeta, error as Error); + chain.logger.error("Gossip validations failed while publishing the block", valLogMeta, res.error); chain.persistInvalidSszValue( chain.config.getForkTypes(slot).SignedBeaconBlock, signedBlock, "api_reject_gossip_failure" ); - throw error; + throw res.error ?? new Error(`Gossip block validation failed during publishing: ${res.code}`); } } chain.logger.debug("Gossip checks validated while publishing the block", valLogMeta); @@ -682,7 +680,14 @@ export function getBeaconBlockApi({ throw new ApiError(400, `Envelope slot ${slot} does not match block slot ${block.slot}`); } - await chain.beaconEngine.validateApiExecutionPayloadEnvelope(signedExecutionPayloadEnvelope); + const envelopeValidation = + await chain.beaconEngine.validateApiExecutionPayloadEnvelope(signedExecutionPayloadEnvelope); + if (envelopeValidation.status !== GossipValidationStatus.Accept) { + throw ( + envelopeValidation.error ?? + new ApiError(400, `Invalid execution payload envelope: ${envelopeValidation.code}`) + ); + } const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD; let dataColumnSidecars: gloas.DataColumnSidecar[] = []; @@ -832,7 +837,10 @@ export function getBeaconBlockApi({ throw new ApiError(400, `publishExecutionPayloadBid not supported for pre-gloas fork=${fork}`); } - await chain.beaconEngine.validateApiExecutionPayloadBid(signedExecutionPayloadBid); + const bidValidation = await chain.beaconEngine.validateApiExecutionPayloadBid(signedExecutionPayloadBid); + if (bidValidation.status !== GossipValidationStatus.Accept) { + throw bidValidation.error ?? new ApiError(400, `Invalid execution payload bid: ${bidValidation.code}`); + } try { const insertOutcome = chain.executionPayloadBidPool.add(signedExecutionPayloadBid); diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index 15208e94488a..585b831d39a3 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -11,15 +11,11 @@ import { import {isStatePostAltair} from "@lodestar/state-transition"; import {Epoch, SingleAttestation, isElectraAttestation, ssz, sszTypesFor} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; +import {GossipValidationStatus} from "../../../../chain/beaconEngine/gossipValidationResult.js"; import { - AttestationError, AttestationErrorCode, - GossipAction, - PayloadAttestationError, PayloadAttestationErrorCode, - ProposerPreferencesError, ProposerPreferencesErrorCode, - SyncCommitteeError, } from "../../../../chain/errors/index.js"; import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js"; import {validateApiBlsToExecutionChange} from "../../../../chain/validation/blsToExecutionChange.js"; @@ -77,10 +73,27 @@ export function getBeaconPoolApi({ await Promise.all( signedProposerPreferences.map(async (signed, i) => { + const logCtx = { + slot: signed.message.proposalSlot, + validatorIndex: signed.message.validatorIndex, + dependentRoot: toRootHex(signed.message.dependentRoot), + }; try { // TODO - beacon engine: get bytes from the api const preferencesBytes = ssz.gloas.SignedProposerPreferences.serialize(signed); - await chain.beaconEngine.validateGossipProposerPreferences(preferencesBytes, signed); + const res = await chain.beaconEngine.validateGossipProposerPreferences(preferencesBytes, signed); + if (res.status !== GossipValidationStatus.Accept) { + if (res.code === ProposerPreferencesErrorCode.ALREADY_KNOWN) { + logger.debug("Ignoring known signed proposer preferences", logCtx); + return; + } + failures.push({index: i, message: res.error?.message ?? res.code}); + logger.verbose(`Error on submitSignedProposerPreferences [${i}]`, logCtx, res.error); + if (res.status === GossipValidationStatus.Reject) { + chain.persistInvalidSszValue(ssz.gloas.SignedProposerPreferences, signed, "api_reject"); + } + return; + } chain.proposerPreferencesPool.add(signed); await network.publishProposerPreferences(signed); @@ -89,22 +102,8 @@ export function getBeaconPoolApi({ data: signed, }); } catch (e) { - const logCtx = { - slot: signed.message.proposalSlot, - validatorIndex: signed.message.validatorIndex, - dependentRoot: toRootHex(signed.message.dependentRoot), - }; - - if (e instanceof ProposerPreferencesError && e.type.code === ProposerPreferencesErrorCode.ALREADY_KNOWN) { - logger.debug("Ignoring known signed proposer preferences", logCtx); - return; - } - failures.push({index: i, message: (e as Error).message}); logger.verbose(`Error on submitSignedProposerPreferences [${i}]`, logCtx, e as Error); - if (e instanceof ProposerPreferencesError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue(ssz.gloas.SignedProposerPreferences, signed, "api_reject"); - } } }) ); @@ -142,6 +141,7 @@ export function getBeaconPoolApi({ await Promise.all( signedAttestations.map(async (attestation, i) => { + const logCtx = {slot: attestation.data.slot, index: attestation.data.index}; try { const validateFn = () => chain.beaconEngine.validateApiAttestation(fork, {attestation, serializedData: null}); @@ -149,8 +149,23 @@ export function getBeaconPoolApi({ // when a validator is configured with multiple beacon node urls, this attestation data may come from another beacon node // and the block hasn't been in our forkchoice since we haven't seen / processing that block // see https://github.com/ChainSafe/lodestar/issues/5098 + const res = await validateGossipFnRetryUnknownRoot(validateFn, network, chain, slot, beaconBlockRoot); + if (res.status !== GossipValidationStatus.Accept) { + if (res.code === AttestationErrorCode.ATTESTATION_ALREADY_KNOWN) { + // Attestations might already be published by another node as part of a fallback setup or DVT cluster + // and can reach our node by gossip before the api. Should not result in a 500 response. + logger.debug("Ignoring known attestation", logCtx); + return; + } + failures.push({index: i, message: res.error?.message ?? res.code}); + logger.verbose(`Error on submitPoolAttestations [${i}]`, logCtx, res.error); + if (res.status === GossipValidationStatus.Reject) { + chain.persistInvalidSszValue(sszTypesFor(fork).SingleAttestation, attestation, "api_reject"); + } + return; + } const {indexedAttestation, subnet, attDataRootHex, committeeIndex, validatorCommitteeIndex, committeeSize} = - await validateGossipFnRetryUnknownRoot(validateFn, network, chain, slot, beaconBlockRoot); + res.value; if (network.shouldAggregate(subnet, slot)) { const insertOutcome = chain.attestationPool.add( @@ -188,20 +203,9 @@ export function getBeaconPoolApi({ sentPeers ); } catch (e) { - const logCtx = {slot: attestation.data.slot, index: attestation.data.index}; - - if (e instanceof AttestationError && e.type.code === AttestationErrorCode.ATTESTATION_ALREADY_KNOWN) { - logger.debug("Ignoring known attestation", logCtx); - // Attestations might already be published by another node as part of a fallback setup or DVT cluster - // and can reach our node by gossip before the api. The error can be ignored and should not result in a 500 response. - return; - } - + // Validation no longer throws (handled above); this catches post-validation failures (publish/pool add). failures.push({index: i, message: (e as Error).message}); logger.verbose(`Error on submitPoolAttestations [${i}]`, logCtx, e as Error); - if (e instanceof AttestationError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue(sszTypesFor(fork).SingleAttestation, attestation, "api_reject"); - } } }) ); @@ -268,16 +272,32 @@ export function getBeaconPoolApi({ await Promise.all( payloadAttestationMessages.map(async (payloadAttestationMessage, i) => { + const logCtx = { + slot: payloadAttestationMessage.data.slot, + validatorIndex: payloadAttestationMessage.validatorIndex, + beaconBlockRoot: toRootHex(payloadAttestationMessage.data.beaconBlockRoot), + }; try { const validateFn = () => chain.beaconEngine.validateApiPayloadAttestationMessage(payloadAttestationMessage); const {slot, beaconBlockRoot} = payloadAttestationMessage.data; - const {attDataRootHex, validatorCommitteeIndices} = await validateGossipFnRetryUnknownRoot( - validateFn, - network, - chain, - slot, - beaconBlockRoot - ); + const res = await validateGossipFnRetryUnknownRoot(validateFn, network, chain, slot, beaconBlockRoot); + if (res.status !== GossipValidationStatus.Accept) { + if (res.code === PayloadAttestationErrorCode.PAYLOAD_ATTESTATION_ALREADY_KNOWN) { + logger.debug("Ignoring known payload attestation message", logCtx); + return; + } + failures.push({index: i, message: res.error?.message ?? res.code}); + logger.verbose(`Error on submitPayloadAttestationMessages [${i}]`, logCtx, res.error); + if (res.status === GossipValidationStatus.Reject) { + chain.persistInvalidSszValue( + ssz.gloas.PayloadAttestationMessage, + payloadAttestationMessage, + "api_reject" + ); + } + return; + } + const {attDataRootHex, validatorCommitteeIndices} = res.value; const insertOutcome = chain.payloadAttestationPool.add( payloadAttestationMessage, @@ -297,29 +317,8 @@ export function getBeaconPoolApi({ await network.publishPayloadAttestationMessage(payloadAttestationMessage); } catch (e) { - const logCtx = { - slot: payloadAttestationMessage.data.slot, - validatorIndex: payloadAttestationMessage.validatorIndex, - beaconBlockRoot: toRootHex(payloadAttestationMessage.data.beaconBlockRoot), - }; - - if ( - e instanceof PayloadAttestationError && - e.type.code === PayloadAttestationErrorCode.PAYLOAD_ATTESTATION_ALREADY_KNOWN - ) { - logger.debug("Ignoring known payload attestation message", logCtx); - return; - } - failures.push({index: i, message: (e as Error).message}); logger.verbose(`Error on submitPayloadAttestationMessages [${i}]`, logCtx, e as Error); - if (e instanceof PayloadAttestationError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue( - ssz.gloas.PayloadAttestationMessage, - payloadAttestationMessage, - "api_reject" - ); - } } }) ); @@ -365,7 +364,19 @@ export function getBeaconPoolApi({ // Verify signature only, all other data is very likely to be correct, since the `signature` object is created by this node. // Worst case if `signature` is not valid, gossip peers will drop it and slightly downscore us. - await chain.beaconEngine.validateApiSyncCommittee(state, signature); + const res = await chain.beaconEngine.validateApiSyncCommittee(state, signature); + if (res.status !== GossipValidationStatus.Accept) { + failures.push({index: i, message: res.error?.message ?? res.code}); + logger.verbose( + `Error on submitPoolSyncCommitteeSignatures [${i}]`, + {slot: signature.slot, validatorIndex: signature.validatorIndex}, + res.error + ); + if (res.status === GossipValidationStatus.Reject) { + chain.persistInvalidSszValue(ssz.altair.SyncCommitteeMessage, signature, "api_reject"); + } + return; + } // The same validator can appear multiple times in the sync committee. It can appear multiple times per // subnet even. First compute on which subnet the signature must be broadcasted to. @@ -401,9 +412,6 @@ export function getBeaconPoolApi({ {slot: signature.slot, validatorIndex: signature.validatorIndex}, e as Error ); - if (e instanceof SyncCommitteeError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue(ssz.altair.SyncCommitteeMessage, signature, "api_reject"); - } } }) ); diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index dc439263be19..105befd106c2 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -60,14 +60,9 @@ import { toRootHex, } from "@lodestar/utils"; import {MAX_BUILDER_BOOST_FACTOR} from "@lodestar/validator"; +import {GossipValidationStatus} from "../../../chain/beaconEngine/gossipValidationResult.js"; import {BlockInputSource} from "../../../chain/blocks/blockInput/types.js"; -import { - AttestationError, - AttestationErrorCode, - GossipAction, - SyncCommitteeError, - SyncCommitteeErrorCode, -} from "../../../chain/errors/index.js"; +import {AttestationErrorCode, SyncCommitteeErrorCode} from "../../../chain/errors/index.js"; import {ChainEvent, CommonBlockBody} from "../../../chain/index.js"; import {PREPARE_NEXT_SLOT_BPS} from "../../../chain/prepareNextSlot.js"; import {BlockType, ProduceFullDeneb, ProduceFullGloas} from "../../../chain/produceBlock/index.js"; @@ -1531,6 +1526,10 @@ export function getValidatorApi( await Promise.all( signedAggregateAndProofs.map(async (signedAggregateAndProof, i) => { + const logCtx = { + slot: signedAggregateAndProof.message.aggregate.data.slot, + index: signedAggregateAndProof.message.aggregate.data.index, + }; try { // TODO: Validate in batch const validateFn = () => chain.beaconEngine.validateApiAggregateAndProof(fork, signedAggregateAndProof); @@ -1538,8 +1537,20 @@ export function getValidatorApi( // when a validator is configured with multiple beacon node urls, this attestation may come from another beacon node // and the block hasn't been in our forkchoice since we haven't seen / processing that block // see https://github.com/ChainSafe/lodestar/issues/5098 - const {indexedAttestation, committeeValidatorIndices, attDataRootHex} = - await validateGossipFnRetryUnknownRoot(validateFn, network, chain, slot, beaconBlockRoot); + const res = await validateGossipFnRetryUnknownRoot(validateFn, network, chain, slot, beaconBlockRoot); + if (res.status !== GossipValidationStatus.Accept) { + if (res.code === AttestationErrorCode.AGGREGATOR_ALREADY_KNOWN) { + logger.debug("Ignoring known signedAggregateAndProof", logCtx); + return; // Ok to submit the same aggregate twice + } + failures.push({index: i, message: res.error?.message ?? res.code}); + logger.verbose(`Error on publishAggregateAndProofs [${i}]`, logCtx, res.error); + if (res.status === GossipValidationStatus.Reject) { + chain.persistInvalidSszValue(ssz.phase0.SignedAggregateAndProof, signedAggregateAndProof, "api_reject"); + } + return; + } + const {indexedAttestation, committeeValidatorIndices, attDataRootHex} = res.value; const insertOutcome = chain.aggregatedAttestationPool.add( signedAggregateAndProof.message.aggregate, @@ -1552,21 +1563,8 @@ export function getValidatorApi( const sentPeers = await network.publishBeaconAggregateAndProof(signedAggregateAndProof); chain.validatorMonitor?.onPoolSubmitAggregatedAttestation(seenTimestampSec, indexedAttestation, sentPeers); } catch (e) { - const logCtx = { - slot: signedAggregateAndProof.message.aggregate.data.slot, - index: signedAggregateAndProof.message.aggregate.data.index, - }; - - if (e instanceof AttestationError && e.type.code === AttestationErrorCode.AGGREGATOR_ALREADY_KNOWN) { - logger.debug("Ignoring known signedAggregateAndProof", logCtx); - return; // Ok to submit the same aggregate twice - } - failures.push({index: i, message: (e as Error).message}); logger.verbose(`Error on publishAggregateAndProofs [${i}]`, logCtx, e as Error); - if (e instanceof AttestationError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue(ssz.phase0.SignedAggregateAndProof, signedAggregateAndProof, "api_reject"); - } } }) ); @@ -1590,16 +1588,32 @@ export function getValidatorApi( await Promise.all( contributionAndProofs.map(async (contributionAndProof, i) => { + const logCtx = { + slot: contributionAndProof.message.contribution.slot, + subcommitteeIndex: contributionAndProof.message.contribution.subcommitteeIndex, + }; try { // TODO: Validate in batch // TODO - engine: get bytes from api const contributionBytes = ssz.altair.SignedContributionAndProof.serialize(contributionAndProof); - const {syncCommitteeParticipantIndices} = - await chain.beaconEngine.validateSyncCommitteeGossipContributionAndProof( - contributionBytes, - contributionAndProof, - true // skip known participants check - ); + const res = await chain.beaconEngine.validateSyncCommitteeGossipContributionAndProof( + contributionBytes, + contributionAndProof, + true // skip known participants check + ); + if (res.status !== GossipValidationStatus.Accept) { + if (res.code === SyncCommitteeErrorCode.SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN) { + logger.debug("Ignoring known contributionAndProof", logCtx); + return; // Ok to submit the same aggregate twice + } + failures.push({index: i, message: res.error?.message ?? res.code}); + logger.verbose(`Error on publishContributionAndProofs [${i}]`, logCtx, res.error); + if (res.status === GossipValidationStatus.Reject) { + chain.persistInvalidSszValue(ssz.altair.SignedContributionAndProof, contributionAndProof, "api_reject"); + } + return; + } + const {syncCommitteeParticipantIndices} = res.value; const insertOutcome = chain.syncContributionAndProofPool.add( contributionAndProof.message, syncCommitteeParticipantIndices.length, @@ -1608,24 +1622,8 @@ export function getValidatorApi( metrics?.opPool.syncContributionAndProofPool.apiInsertOutcome.inc({insertOutcome}); await network.publishContributionAndProof(contributionAndProof); } catch (e) { - const logCtx = { - slot: contributionAndProof.message.contribution.slot, - subcommitteeIndex: contributionAndProof.message.contribution.subcommitteeIndex, - }; - - if ( - e instanceof SyncCommitteeError && - e.type.code === SyncCommitteeErrorCode.SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN - ) { - logger.debug("Ignoring known contributionAndProof", logCtx); - return; // Ok to submit the same aggregate twice - } - failures.push({index: i, message: (e as Error).message}); logger.verbose(`Error on publishContributionAndProofs [${i}]`, logCtx, e as Error); - if (e instanceof SyncCommitteeError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue(ssz.altair.SignedContributionAndProof, contributionAndProof, "api_reject"); - } } }) ); diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 7897255da0ba..292b8bb73d8a 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -65,7 +65,6 @@ import { import { ApiAttestation, AttestationValidationResult, - BatchResult, GossipAttestation, validateApiAttestation, validateGossipAttestationsSameAttData, @@ -89,6 +88,7 @@ import { import {validateGossipProposerPreferences} from "../validation/proposerPreferences.js"; import {validateApiSyncCommittee, validateGossipSyncCommittee} from "../validation/syncCommittee.js"; import {validateSyncCommitteeGossipContributionAndProof} from "../validation/syncCommitteeContributionAndProof.js"; +import {GossipValidationResult, fromResult, runGossipValidation} from "./gossipValidationResult.js"; import {BeaconEngineModules, IBeaconEngine} from "./interface.js"; import {IBeaconEngineOptions} from "./options.js"; @@ -303,31 +303,32 @@ export class BeaconEngine implements IBeaconEngine { _blockBytes: Uint8Array, signedBlock: SignedBeaconBlock, fork: ForkName - ): Promise { - return validateGossipBlock(this, signedBlock, fork); + ): Promise> { + return runGossipValidation(() => validateGossipBlock(this, signedBlock, fork)); } validateGossipSyncCommittee( _syncCommitteeBytes: Uint8Array, syncCommittee: altair.SyncCommitteeMessage, subnet: SubnetID - ): Promise<{indicesInSubcommittee: number[]}> { - return validateGossipSyncCommittee(this, syncCommittee, subnet); + ): Promise> { + return runGossipValidation(() => validateGossipSyncCommittee(this, syncCommittee, subnet)); } - validateApiSyncCommittee(headState: IBeaconStateView, syncCommittee: altair.SyncCommitteeMessage): Promise { - return validateApiSyncCommittee(this, headState, syncCommittee); + validateApiSyncCommittee( + headState: IBeaconStateView, + syncCommittee: altair.SyncCommitteeMessage + ): Promise> { + return runGossipValidation(() => validateApiSyncCommittee(this, headState, syncCommittee)); } validateSyncCommitteeGossipContributionAndProof( _contributionBytes: Uint8Array, signedContributionAndProof: altair.SignedContributionAndProof, skipValidationKnownParticipants = false - ): Promise<{syncCommitteeParticipantIndices: ValidatorIndex[]}> { - return validateSyncCommitteeGossipContributionAndProof( - this, - signedContributionAndProof, - skipValidationKnownParticipants + ): Promise> { + return runGossipValidation(() => + validateSyncCommitteeGossipContributionAndProof(this, signedContributionAndProof, skipValidationKnownParticipants) ); } @@ -336,16 +337,18 @@ export class BeaconEngine implements IBeaconEngine { fork: ForkName, blobSidecar: deneb.BlobSidecar, subnet: SubnetID - ): Promise { - return validateGossipBlobSidecar(this, fork, blobSidecar, subnet); + ): Promise> { + return runGossipValidation(() => validateGossipBlobSidecar(this, fork, blobSidecar, subnet)); } validateGossipFuluDataColumnSidecar( _dataColumnBytes: Uint8Array, dataColumnSidecar: fulu.DataColumnSidecar, gossipSubnet: SubnetID - ): Promise { - return validateGossipFuluDataColumnSidecar(this, dataColumnSidecar, gossipSubnet, this.metrics); + ): Promise> { + return runGossipValidation(() => + validateGossipFuluDataColumnSidecar(this, dataColumnSidecar, gossipSubnet, this.metrics) + ); } validateGossipGloasDataColumnSidecar( @@ -353,77 +356,91 @@ export class BeaconEngine implements IBeaconEngine { payloadInput: PayloadEnvelopeInput, dataColumnSidecar: gloas.DataColumnSidecar, gossipSubnet: SubnetID - ): Promise { - return validateGossipGloasDataColumnSidecar(this, payloadInput, dataColumnSidecar, gossipSubnet, this.metrics); + ): Promise> { + return runGossipValidation(() => + validateGossipGloasDataColumnSidecar(this, payloadInput, dataColumnSidecar, gossipSubnet, this.metrics) + ); } validateGossipPayloadAttestationMessage( _payloadAttestationBytes: Uint8Array, payloadAttestationMessage: gloas.PayloadAttestationMessage - ): Promise { - return validateGossipPayloadAttestationMessage(this, payloadAttestationMessage); + ): Promise> { + return runGossipValidation(() => validateGossipPayloadAttestationMessage(this, payloadAttestationMessage)); } validateApiPayloadAttestationMessage( payloadAttestationMessage: gloas.PayloadAttestationMessage - ): Promise { - return validateApiPayloadAttestationMessage(this, payloadAttestationMessage); + ): Promise> { + return runGossipValidation(() => validateApiPayloadAttestationMessage(this, payloadAttestationMessage)); } - // The batch attestation validator's per-item bytes live on each `GossipAttestation.serializedData`, - // so there is no separate leading bytes parameter here. - validateGossipAttestationsSameAttData(fork: ForkName, attestations: GossipAttestation[]): Promise { - return validateGossipAttestationsSameAttData(fork, this, attestations); + // The batch attestation validator already returns `Result[]` (caught internally, never throws out); + // map each item to a `GossipValidationResult` so the batch is FFI-safe too. Per-item bytes live on each + // `GossipAttestation.serializedData`, so there is no separate leading bytes parameter here. + async validateGossipAttestationsSameAttData( + fork: ForkName, + attestations: GossipAttestation[] + ): Promise<{results: GossipValidationResult[]; batchableBls: boolean}> { + const {results, batchableBls} = await validateGossipAttestationsSameAttData(fork, this, attestations); + return {results: results.map(fromResult), batchableBls}; } - validateApiAttestation(fork: ForkName, attestationOrBytes: ApiAttestation): Promise { - return validateApiAttestation(fork, this, attestationOrBytes); + validateApiAttestation( + fork: ForkName, + attestationOrBytes: ApiAttestation + ): Promise> { + return runGossipValidation(() => validateApiAttestation(fork, this, attestationOrBytes)); } validateGossipAggregateAndProof( aggregateBytes: Uint8Array, fork: ForkName, signedAggregateAndProof: SignedAggregateAndProof - ): Promise { - return validateGossipAggregateAndProof(fork, this, signedAggregateAndProof, aggregateBytes); + ): Promise> { + return runGossipValidation(() => + validateGossipAggregateAndProof(fork, this, signedAggregateAndProof, aggregateBytes) + ); } validateApiAggregateAndProof( fork: ForkName, signedAggregateAndProof: SignedAggregateAndProof - ): Promise { - return validateApiAggregateAndProof(fork, this, signedAggregateAndProof); + ): Promise> { + return runGossipValidation(() => validateApiAggregateAndProof(fork, this, signedAggregateAndProof)); } validateGossipExecutionPayloadEnvelope( _envelopeBytes: Uint8Array, executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope - ): Promise { - return validateGossipExecutionPayloadEnvelope(this, executionPayloadEnvelope); + ): Promise> { + return runGossipValidation(() => validateGossipExecutionPayloadEnvelope(this, executionPayloadEnvelope)); } - validateApiExecutionPayloadEnvelope(executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise { - return validateApiExecutionPayloadEnvelope(this, executionPayloadEnvelope); + validateApiExecutionPayloadEnvelope( + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope + ): Promise> { + return runGossipValidation(() => validateApiExecutionPayloadEnvelope(this, executionPayloadEnvelope)); } validateGossipExecutionPayloadBid( _bidBytes: Uint8Array, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid - ): Promise<{proposerIndex: ValidatorIndex}> { - return validateGossipExecutionPayloadBid(this, signedExecutionPayloadBid); + ): Promise> { + return runGossipValidation(() => validateGossipExecutionPayloadBid(this, signedExecutionPayloadBid)); } validateApiExecutionPayloadBid( signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid - ): Promise<{proposerIndex: ValidatorIndex}> { - return validateApiExecutionPayloadBid(this, signedExecutionPayloadBid); + ): Promise> { + return runGossipValidation(() => validateApiExecutionPayloadBid(this, signedExecutionPayloadBid)); } validateGossipProposerPreferences( _preferencesBytes: Uint8Array, signedProposerPreferences: gloas.SignedProposerPreferences - ): Promise { - return validateGossipProposerPreferences(this, signedProposerPreferences); + ): Promise> { + return runGossipValidation(() => validateGossipProposerPreferences(this, signedProposerPreferences)); } // TODO - beacon engine: scalar state reads (getBeaconProposer, getValidator, getBalance, diff --git a/packages/beacon-node/src/chain/beaconEngine/gossipValidationResult.ts b/packages/beacon-node/src/chain/beaconEngine/gossipValidationResult.ts new file mode 100644 index 000000000000..edfb604776aa --- /dev/null +++ b/packages/beacon-node/src/chain/beaconEngine/gossipValidationResult.ts @@ -0,0 +1,90 @@ +import {Result} from "../../util/wrapError.js"; +import {GossipAction, GossipActionError} from "../errors/gossipValidation.js"; + +/** + * 3-way discriminant — maps 1:1 to libp2p `TopicValidatorResult`. Plain data, no class, no `instanceof`, + * so a native (Zig) engine can return it across FFI instead of throwing JS error objects. + */ +export enum GossipValidationStatus { + Accept = "accept", + Ignore = "ignore", + Reject = "reject", +} + +/** Shared failure arm (ignore/reject). The facade branches on `code`/`status` only — never on `error`. */ +export type GossipValidationFailure = { + status: GossipValidationStatus.Ignore | GossipValidationStatus.Reject; + /** LodestarError code — drives metrics + facade branching */ + code: string; + /** best-effort scalar logging context (FFI-friendly); do not depend on specific fields */ + data?: Record; + /** TRANSITIONAL: original JS error for rich logs while validators still throw; native leaves undefined */ + error?: Error; +}; + +/** Value-bearing result — the engine seam + facade inner helpers (`validateBeaconBlock`, …) use this. */ +export type GossipValidationResult = {status: GossipValidationStatus.Accept; value: T} | GossipValidationFailure; + +/** + * Slim verdict — the handler → verdict-mapper boundary. No `value`: the mapper only needs `status` + * (+ `code` for metrics, `error` for logs). Shares the failure arm with `GossipValidationResult`, so a + * handler holding a `GossipValidationResult` on its failure branch can `return` it as a verdict with no + * conversion, and `accept(undefined)` is assignable to the verdict's accept arm. + */ +export type GossipValidationVerdict = {status: GossipValidationStatus.Accept} | GossipValidationFailure; + +export function accept(value: T): GossipValidationResult { + return {status: GossipValidationStatus.Accept, value}; +} + +export function ignore( + code: string, + data?: Record, + error?: Error +): GossipValidationResult { + return {status: GossipValidationStatus.Ignore, code, data, error}; +} + +export function reject( + code: string, + data?: Record, + error?: Error +): GossipValidationResult { + return {status: GossipValidationStatus.Reject, code, data, error}; +} + +function failureFromGossipActionError(e: GossipActionError<{code: string}>): GossipValidationFailure { + const status = e.action === GossipAction.REJECT ? GossipValidationStatus.Reject : GossipValidationStatus.Ignore; + return {status, code: e.type.code, data: e.type as unknown as Record, error: e}; +} + +/** + * JS adapter — wraps a throwing validator and converts a caught `GossipActionError` into a result; + * rethrows anything else (truly unexpected). Called INSIDE each `BeaconEngine.validateGossip*`/`validateApi*` + * method — the facade never calls it, and the native engine builds the result struct directly. + */ +export async function runGossipValidation(fn: () => Promise): Promise> { + try { + return accept(await fn()); + } catch (e) { + if (e instanceof GossipActionError) { + return failureFromGossipActionError(e); + } + throw e; + } +} + +/** + * Batch adapter — the batch validator already returns `Result[]` (caught internally, never throws out). + * Convert each item to a `GossipValidationResult` so the batch is FFI-safe too (a `Result`'s `.err` is a + * live JS Error, which a native engine cannot produce). + */ +export function fromResult(r: Result): GossipValidationResult { + if (r.err == null) { + return accept(r.result); + } + if (r.err instanceof GossipActionError) { + return failureFromGossipActionError(r.err); + } + return ignore("UNEXPECTED_ERROR", undefined, r.err); +} diff --git a/packages/beacon-node/src/chain/beaconEngine/index.ts b/packages/beacon-node/src/chain/beaconEngine/index.ts index 333481639797..7bb8667ffb7b 100644 --- a/packages/beacon-node/src/chain/beaconEngine/index.ts +++ b/packages/beacon-node/src/chain/beaconEngine/index.ts @@ -1,2 +1,3 @@ export * from "./beaconEngine.js"; +export * from "./gossipValidationResult.js"; export * from "./interface.js"; diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index bb97570e1521..46894545d176 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -22,15 +22,11 @@ import {ChainEventEmitter} from "../emitter.js"; import {SeenBlockInput} from "../seenCache/seenGossipBlockInput.js"; import {CPStateDatastore} from "../stateCache/datastore/types.js"; import {AggregateAndProofValidationResult} from "../validation/aggregateAndProof.js"; -import { - ApiAttestation, - AttestationValidationResult, - BatchResult, - GossipAttestation, -} from "../validation/attestation.js"; +import {ApiAttestation, AttestationValidationResult, GossipAttestation} from "../validation/attestation.js"; import {GossipBlockValidationResult} from "../validation/block.js"; import {PayloadAttestationValidationResult} from "../validation/payloadAttestationMessage.js"; import {ValidatorMonitor} from "../validatorMonitor.js"; +import {GossipValidationResult} from "./gossipValidationResult.js"; import {IBeaconEngineOptions} from "./options.js"; export type BeaconEngineModules = { @@ -64,72 +60,84 @@ export interface IBeaconEngine { readonly forkChoice: IForkChoice; // Gossip validation flows. The first parameter is the message's SSZ bytes (unused by the JS engine, - // required by the native engine's bytes-first contract), followed by the deserialized object. + // required by the native engine's bytes-first contract), followed by the deserialized object. Each + // returns a `GossipValidationResult` (no throw) so the native engine can return outcomes across FFI. validateGossipBlock( blockBytes: Uint8Array, signedBlock: SignedBeaconBlock, fork: ForkName - ): Promise; + ): Promise>; validateGossipSyncCommittee( syncCommitteeBytes: Uint8Array, syncCommittee: altair.SyncCommitteeMessage, subnet: SubnetID - ): Promise<{indicesInSubcommittee: number[]}>; - validateApiSyncCommittee(headState: IBeaconStateView, syncCommittee: altair.SyncCommitteeMessage): Promise; + ): Promise>; + validateApiSyncCommittee( + headState: IBeaconStateView, + syncCommittee: altair.SyncCommitteeMessage + ): Promise>; validateSyncCommitteeGossipContributionAndProof( contributionBytes: Uint8Array, signedContributionAndProof: altair.SignedContributionAndProof, skipValidationKnownParticipants?: boolean - ): Promise<{syncCommitteeParticipantIndices: ValidatorIndex[]}>; + ): Promise>; validateGossipBlobSidecar( blobBytes: Uint8Array, fork: ForkName, blobSidecar: deneb.BlobSidecar, subnet: SubnetID - ): Promise; + ): Promise>; validateGossipFuluDataColumnSidecar( dataColumnBytes: Uint8Array, dataColumnSidecar: fulu.DataColumnSidecar, gossipSubnet: SubnetID - ): Promise; + ): Promise>; validateGossipGloasDataColumnSidecar( dataColumnBytes: Uint8Array, payloadInput: PayloadEnvelopeInput, dataColumnSidecar: gloas.DataColumnSidecar, gossipSubnet: SubnetID - ): Promise; + ): Promise>; validateGossipPayloadAttestationMessage( payloadAttestationBytes: Uint8Array, payloadAttestationMessage: gloas.PayloadAttestationMessage - ): Promise; + ): Promise>; validateApiPayloadAttestationMessage( payloadAttestationMessage: gloas.PayloadAttestationMessage - ): Promise; - validateGossipAttestationsSameAttData(fork: ForkName, attestations: GossipAttestation[]): Promise; - validateApiAttestation(fork: ForkName, attestationOrBytes: ApiAttestation): Promise; + ): Promise>; + validateGossipAttestationsSameAttData( + fork: ForkName, + attestations: GossipAttestation[] + ): Promise<{results: GossipValidationResult[]; batchableBls: boolean}>; + validateApiAttestation( + fork: ForkName, + attestationOrBytes: ApiAttestation + ): Promise>; validateGossipAggregateAndProof( aggregateBytes: Uint8Array, fork: ForkName, signedAggregateAndProof: SignedAggregateAndProof - ): Promise; + ): Promise>; validateApiAggregateAndProof( fork: ForkName, signedAggregateAndProof: SignedAggregateAndProof - ): Promise; + ): Promise>; validateGossipExecutionPayloadEnvelope( envelopeBytes: Uint8Array, executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope - ): Promise; - validateApiExecutionPayloadEnvelope(executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise; + ): Promise>; + validateApiExecutionPayloadEnvelope( + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope + ): Promise>; validateGossipExecutionPayloadBid( bidBytes: Uint8Array, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid - ): Promise<{proposerIndex: ValidatorIndex}>; + ): Promise>; validateApiExecutionPayloadBid( signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid - ): Promise<{proposerIndex: ValidatorIndex}>; + ): Promise>; validateGossipProposerPreferences( preferencesBytes: Uint8Array, signedProposerPreferences: gloas.SignedProposerPreferences - ): Promise; + ): Promise>; } diff --git a/packages/beacon-node/src/network/gossip/interface.ts b/packages/beacon-node/src/network/gossip/interface.ts index f981df5e7f6e..1c4b47405301 100644 --- a/packages/beacon-node/src/network/gossip/interface.ts +++ b/packages/beacon-node/src/network/gossip/interface.ts @@ -19,8 +19,7 @@ import { phase0, } from "@lodestar/types"; import {Logger} from "@lodestar/utils"; -import {AttestationError, AttestationErrorType} from "../../chain/errors/attestationError.js"; -import {GossipActionError} from "../../chain/errors/gossipValidation.js"; +import {GossipValidationVerdict} from "../../chain/beaconEngine/gossipValidationResult.js"; import {IBeaconChain} from "../../chain/index.js"; import {JobItemQueue} from "../../util/queue/index.js"; @@ -203,9 +202,9 @@ export type GossipHandlerParam = { seenTimestampSec: number; }; -export type GossipHandlerFn = (gossipHandlerParam: GossipHandlerParam) => Promise; +export type GossipHandlerFn = (gossipHandlerParam: GossipHandlerParam) => Promise; -export type BatchGossipHandlerFn = (gossipHandlerParam: GossipHandlerParam[]) => Promise<(null | AttestationError)[]>; +export type BatchGossipHandlerFn = (gossipHandlerParam: GossipHandlerParam[]) => Promise; export type GossipHandlerParamGeneric = { gossipData: GossipData; @@ -220,7 +219,7 @@ export type GossipHandlers = { export type SequentialGossipHandler = ( gossipHandlerParam: GossipHandlerParamGeneric -) => Promise; +) => Promise; export type SequentialGossipHandlers = { [K in SequentialGossipType]: SequentialGossipHandler; @@ -232,7 +231,7 @@ export type BatchGossipHandlers = { export type BatchGossipHandler = ( gossipHandlerParams: GossipHandlerParamGeneric[] -) => Promise<(null | GossipActionError)[]>; +) => Promise; // biome-ignore lint/suspicious/noExplicitAny: Need the usage of `any` here to infer any type export type ResolvedType Promise> = F extends (...args: any) => Promise diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 600de99c6943..a9cdfe987202 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -28,6 +28,15 @@ import { sszTypesFor, } from "@lodestar/types"; import {LogLevel, Logger, prettyBytes, toHex, toRootHex} from "@lodestar/utils"; +import { + GossipValidationResult, + GossipValidationStatus, + GossipValidationVerdict, + accept, + ignore, + reject, + runGossipValidation, +} from "../../chain/beaconEngine/gossipValidationResult.js"; import { BlockInput, BlockInputColumns, @@ -40,26 +49,16 @@ import {PayloadEnvelopeInput, PayloadEnvelopeInputSource} from "../../chain/bloc import {BlobSidecarValidation} from "../../chain/blocks/types.js"; import {ChainEvent} from "../../chain/emitter.js"; import { - AttestationError, AttestationErrorCode, BlobSidecarErrorCode, - BlobSidecarGossipError, BlockError, BlockErrorCode, - BlockGossipError, DataColumnSidecarErrorCode, - DataColumnSidecarGossipError, - ExecutionPayloadEnvelopeError, ExecutionPayloadEnvelopeErrorCode, - GossipAction, - GossipActionError, - PayloadAttestationError, PayloadAttestationErrorCode, - SyncCommitteeError, } from "../../chain/errors/index.js"; import {IBeaconChain} from "../../chain/interface.js"; import { - AggregateAndProofValidationResult, GossipAttestation, toElectraSingleAttestation, validateGossipAttesterSlashing, @@ -139,7 +138,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand fork: ForkName, peerIdStr: string, seenTimestampSec: number - ): Promise { + ): Promise> { const slot = signedBlock.message.slot; const forkTypes = config.getForkTypes(slot); const blockRootHex = toRootHex(forkTypes.BeaconBlock.hashTreeRoot(signedBlock.message)); @@ -171,64 +170,63 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand seenTimestampSec, peerIdStr, }); - try { - const {skippedSlots} = await chain.beaconEngine.validateGossipBlock(blockBytes, signedBlock, fork); - - if (isForkPostGloas(fork)) { - chain.seenPayloadEnvelopeInputCache.add({ - blockRootHex, - block: signedBlock as SignedBeaconBlock, - forkName: fork, - sampledColumns: chain.custodyConfig.sampledColumns, - custodyColumns: chain.custodyConfig.custodyColumns, - timeCreatedSec: seenTimestampSec, + const res = await chain.beaconEngine.validateGossipBlock(blockBytes, signedBlock, fork); + if (res.status !== GossipValidationStatus.Accept) { + logger.debug("Gossip block has error", {slot, root: blockShortHex, code: res.code}); + if ( + (res.code === BlockErrorCode.PARENT_UNKNOWN || res.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN) && + blockInput + ) { + chain.emitter.emit(ChainEvent.blockUnknownParent, { + blockInput, + peer: peerIdStr, + source: BlockInputSource.gossip, }); + // don't prune the blockInput + return res; } - const blockInputMeta = blockInput.getLogMeta(); + if (res.status === GossipValidationStatus.Reject) { + chain.persistInvalidSszValue(forkTypes.SignedBeaconBlock, signedBlock, `gossip_reject_slot_${slot}`); + } - const recvToValidation = Date.now() / 1000 - seenTimestampSec; - const validationTime = recvToValidation - recvToValLatency; + chain.seenBlockInputCache.prune(blockRootHex); + return res; + } - metrics?.gossipBlock.gossipValidation.recvToValidation.observe(recvToValidation); - metrics?.gossipBlock.gossipValidation.validationTime.observe(validationTime); - metrics?.gossipBlock.skippedSlots.observe(skippedSlots); + const {skippedSlots} = res.value; - logger.debug("Validated gossip block", { - ...blockInputMeta, - ...logCtx, - recvToValidation, - validationTime, - skippedSlots, + if (isForkPostGloas(fork)) { + chain.seenPayloadEnvelopeInputCache.add({ + blockRootHex, + block: signedBlock as SignedBeaconBlock, + forkName: fork, + sampledColumns: chain.custodyConfig.sampledColumns, + custodyColumns: chain.custodyConfig.custodyColumns, + timeCreatedSec: seenTimestampSec, }); + } - chain.emitter.emit(routes.events.EventType.blockGossip, {slot, block: blockRootHex}); - - return blockInput; - } catch (e) { - if (e instanceof BlockGossipError) { - logger.debug("Gossip block has error", {slot, root: blockShortHex, code: e.type.code}); - if ( - (e.type.code === BlockErrorCode.PARENT_UNKNOWN || e.type.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN) && - blockInput - ) { - chain.emitter.emit(ChainEvent.blockUnknownParent, { - blockInput, - peer: peerIdStr, - source: BlockInputSource.gossip, - }); - // throw error (don't prune the blockInput) - throw e; - } + const blockInputMeta = blockInput.getLogMeta(); - if (e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue(forkTypes.SignedBeaconBlock, signedBlock, `gossip_reject_slot_${slot}`); - } - } + const recvToValidation = Date.now() / 1000 - seenTimestampSec; + const validationTime = recvToValidation - recvToValLatency; - chain.seenBlockInputCache.prune(blockRootHex); - throw e; - } + metrics?.gossipBlock.gossipValidation.recvToValidation.observe(recvToValidation); + metrics?.gossipBlock.gossipValidation.validationTime.observe(validationTime); + metrics?.gossipBlock.skippedSlots.observe(skippedSlots); + + logger.debug("Validated gossip block", { + ...blockInputMeta, + ...logCtx, + recvToValidation, + validationTime, + skippedSlots, + }); + + chain.emitter.emit(routes.events.EventType.blockGossip, {slot, block: blockRootHex}); + + return accept(blockInput); } async function validateBeaconBlob( @@ -237,7 +235,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand subnet: SubnetID, peerIdStr: string, seenTimestampSec: number - ): Promise { + ): Promise> { const blobBlockHeader = blobSidecar.signedBlockHeader.message; const slot = blobBlockHeader.slot; const fork = config.getForkName(slot); @@ -247,74 +245,65 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec); const recvToValLatency = Date.now() / 1000 - seenTimestampSec; - try { - await chain.beaconEngine.validateGossipBlobSidecar(blobBytes, fork, blobSidecar, subnet); - const blockInput = chain.seenBlockInputCache.getByBlob({ - blockRootHex, - blobSidecar, - source: BlockInputSource.gossip, - seenTimestampSec, - peerIdStr, - }); - const recvToValidation = Date.now() / 1000 - seenTimestampSec; - const validationTime = recvToValidation - recvToValLatency; - - metrics?.gossipBlob.recvToValidation.observe(recvToValidation); - metrics?.gossipBlob.validationTime.observe(validationTime); - - if (chain.emitter.listenerCount(routes.events.EventType.blobSidecar)) { - let versionedHash: Uint8Array; - if (blockInput.hasBlock()) { - // if block hasn't arrived yet then this will throw and need to calculate the versionedHash as a 1-off - versionedHash = blockInput.getVersionedHashes()[blobSidecar.index]; - } else { - versionedHash = kzgCommitmentToVersionedHash(blobSidecar.kzgCommitment); - } - chain.emitter.emit(routes.events.EventType.blobSidecar, { - blockRoot: blockRootHex, - slot, - index: blobSidecar.index, - kzgCommitment: toHex(blobSidecar.kzgCommitment), - versionedHash: toHex(versionedHash), - }); + const res = await chain.beaconEngine.validateGossipBlobSidecar(blobBytes, fork, blobSidecar, subnet); + if (res.status !== GossipValidationStatus.Accept) { + // Don't trigger this yet if full block and blobs haven't arrived yet + if (res.code === BlobSidecarErrorCode.PARENT_UNKNOWN) { + logger.debug("Gossip blob has error", {slot, root: blockShortHex, code: res.code}); + // no need to trigger `unknownBlockParent` event here, as we already did it in `validateBeaconBlock()` } + if (res.status === GossipValidationStatus.Reject) { + chain.persistInvalidSszValue( + ssz.deneb.BlobSidecar, + blobSidecar, + `gossip_reject_slot_${slot}_index_${blobSidecar.index}` + ); + } + return res; + } - logger.debug("Received gossip blob", { - ...blockInput.getLogMeta(), - currentSlot: chain.clock.currentSlot, - peerId: peerIdStr, - delaySec, - subnet, - recvToValLatency, - recvToValidation, - validationTime, - }); + const blockInput = chain.seenBlockInputCache.getByBlob({ + blockRootHex, + blobSidecar, + source: BlockInputSource.gossip, + seenTimestampSec, + peerIdStr, + }); + const recvToValidation = Date.now() / 1000 - seenTimestampSec; + const validationTime = recvToValidation - recvToValLatency; - return blockInput; - } catch (e) { - if (e instanceof BlobSidecarGossipError) { - // Don't trigger this yet if full block and blobs haven't arrived yet - if (e.type.code === BlobSidecarErrorCode.PARENT_UNKNOWN) { - logger.debug("Gossip blob has error", {slot, root: blockShortHex, code: e.type.code}); - // no need to trigger `unknownBlockParent` event here, as we already did it in `validateBeaconBlock()` - // - // TODO(fulu): is this note above correct? Could have random blob that we see that could trigger - // unknownBlockSync. And duplicate addition of a block will be deduplicated by the - // BlockInputSync event handler. Check this!! - // events.emit(NetworkEvent.unknownBlockParent, {blockInput, peer: peerIdStr}); - } + metrics?.gossipBlob.recvToValidation.observe(recvToValidation); + metrics?.gossipBlob.validationTime.observe(validationTime); - if (e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue( - ssz.deneb.BlobSidecar, - blobSidecar, - `gossip_reject_slot_${slot}_index_${blobSidecar.index}` - ); - } + if (chain.emitter.listenerCount(routes.events.EventType.blobSidecar)) { + let versionedHash: Uint8Array; + if (blockInput.hasBlock()) { + // if block hasn't arrived yet then this will throw and need to calculate the versionedHash as a 1-off + versionedHash = blockInput.getVersionedHashes()[blobSidecar.index]; + } else { + versionedHash = kzgCommitmentToVersionedHash(blobSidecar.kzgCommitment); } - - throw e; + chain.emitter.emit(routes.events.EventType.blobSidecar, { + blockRoot: blockRootHex, + slot, + index: blobSidecar.index, + kzgCommitment: toHex(blobSidecar.kzgCommitment), + versionedHash: toHex(versionedHash), + }); } + + logger.debug("Received gossip blob", { + ...blockInput.getLogMeta(), + currentSlot: chain.clock.currentSlot, + peerId: peerIdStr, + delaySec, + subnet, + recvToValLatency, + recvToValidation, + validationTime, + }); + + return accept(blockInput); } async function validateBeaconDataColumn( @@ -323,7 +312,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand gossipSubnet: SubnetID, peerIdStr: string, seenTimestampSec: number - ): Promise { + ): Promise> { metrics?.peerDas.dataColumnSidecarProcessingRequests.inc(); const dataColumnBlockHeader = dataColumnSidecar.signedBlockHeader.message; const slot = dataColumnBlockHeader.slot; @@ -337,11 +326,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand blockRoot: blockRootHex, index: dataColumnSidecar.index, }); - throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { - code: DataColumnSidecarErrorCode.ALREADY_KNOWN, - columnIndex: dataColumnSidecar.index, - slot, - }); + return ignore(DataColumnSidecarErrorCode.ALREADY_KNOWN, {columnIndex: dataColumnSidecar.index, slot}); } // first check if we should even process this column (we may have already processed it via getBlobsV2) @@ -353,11 +338,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand ...blockInput.getLogMeta(), index: dataColumnSidecar.index, }); - throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { - code: DataColumnSidecarErrorCode.ALREADY_KNOWN, - columnIndex: dataColumnSidecar.index, - slot, - }); + return ignore(DataColumnSidecarErrorCode.ALREADY_KNOWN, {columnIndex: dataColumnSidecar.index, slot}); } } @@ -368,7 +349,23 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const recvToValLatency = Date.now() / 1000 - seenTimestampSec; try { - await chain.beaconEngine.validateGossipFuluDataColumnSidecar(dataColumnBytes, dataColumnSidecar, gossipSubnet); + const res = await chain.beaconEngine.validateGossipFuluDataColumnSidecar( + dataColumnBytes, + dataColumnSidecar, + gossipSubnet + ); + if (res.status !== GossipValidationStatus.Accept) { + if (res.status === GossipValidationStatus.Reject) { + chain.persistInvalidSszValue( + ssz.fulu.DataColumnSidecar, + dataColumnSidecar, + `gossip_reject_slot_${slot}_index_${dataColumnSidecar.index}` + ); + // no need to trigger `unknownBlockParent` event here, as we already did it in `validateBeaconBlock()` + } + return res; + } + const blockInput = chain.seenBlockInputCache.getByColumn({ blockRootHex, columnSidecar: dataColumnSidecar, @@ -406,23 +403,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand validationTime, }); - return blockInput; - } catch (e) { - if (e instanceof DataColumnSidecarGossipError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue( - ssz.fulu.DataColumnSidecar, - dataColumnSidecar, - `gossip_reject_slot_${slot}_index_${dataColumnSidecar.index}` - ); - // no need to trigger `unknownBlockParent` event here, as we already did it in `validateBeaconBlock()` - // - // TODO(fulu): is this note above correct? Could have random column that we see that could trigger - // unknownBlockSync. And duplicate addition of a block will be deduplicated by the - // BlockInputSync event handler. Check this!! - // events.emit(NetworkEvent.unknownBlockParent, {blockInput, peer: peerIdStr}); - } - - throw e; + return accept(blockInput); } finally { verificationTimer?.(); } @@ -434,7 +415,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand gossipSubnet: SubnetID, peerIdStr: string, seenTimestampSec: number - ): Promise { + ): Promise> { metrics?.peerDas.dataColumnSidecarProcessingRequests.inc(); const slot = dataColumnSidecar.slot; const blockRootHex = toRootHex(dataColumnSidecar.beaconBlockRoot); @@ -447,11 +428,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand blockRoot: blockRootHex, index: dataColumnSidecar.index, }); - throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { - code: DataColumnSidecarErrorCode.ALREADY_KNOWN, - columnIndex: dataColumnSidecar.index, - slot, - }); + return ignore(DataColumnSidecarErrorCode.ALREADY_KNOWN, {columnIndex: dataColumnSidecar.index, slot}); } const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); @@ -459,11 +436,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand if (!payloadInput) { // This should not happen for gossip because the network processor queues `data_column_sidecar` // until block import creates the corresponding PayloadEnvelopeInput. - throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { - code: DataColumnSidecarErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING, - slot, - blockRoot: blockRootHex, - }); + return ignore(DataColumnSidecarErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING, {slot, blockRoot: blockRootHex}); } // [IGNORE] The sidecar is the first sidecar for the tuple @@ -474,11 +447,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand ...payloadInput.getLogMeta(), index: dataColumnSidecar.index, }); - throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { - code: DataColumnSidecarErrorCode.ALREADY_KNOWN, - columnIndex: dataColumnSidecar.index, - slot, - }); + return ignore(DataColumnSidecarErrorCode.ALREADY_KNOWN, {columnIndex: dataColumnSidecar.index, slot}); } const verificationTimer = metrics?.peerDas.dataColumnSidecarGossipVerificationTime.startTimer(); @@ -488,12 +457,22 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const recvToValLatency = Date.now() / 1000 - seenTimestampSec; try { - await chain.beaconEngine.validateGossipGloasDataColumnSidecar( + const res = await chain.beaconEngine.validateGossipGloasDataColumnSidecar( dataColumnBytes, payloadInput, dataColumnSidecar, gossipSubnet ); + if (res.status !== GossipValidationStatus.Accept) { + if (res.status === GossipValidationStatus.Reject) { + chain.persistInvalidSszValue( + sszTypesFor(payloadInput.forkName as ForkPostGloas).DataColumnSidecar, + dataColumnSidecar, + `gossip_reject_slot_${slot}_index_${dataColumnSidecar.index}` + ); + } + return res; + } const addedColumn = payloadInput.addColumn({ columnSidecar: dataColumnSidecar, @@ -508,11 +487,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand ...payloadInput.getLogMeta(), index: dataColumnSidecar.index, }); - throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { - code: DataColumnSidecarErrorCode.ALREADY_KNOWN, - columnIndex: dataColumnSidecar.index, - slot, - }); + return ignore(DataColumnSidecarErrorCode.ALREADY_KNOWN, {columnIndex: dataColumnSidecar.index, slot}); } const recvToValidation = Date.now() / 1000 - seenTimestampSec; @@ -543,17 +518,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand validationTime, }); - return payloadInput; - } catch (e) { - if (e instanceof DataColumnSidecarGossipError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue( - sszTypesFor(payloadInput.forkName as ForkPostGloas).DataColumnSidecar, - dataColumnSidecar, - `gossip_reject_slot_${slot}_index_${dataColumnSidecar.index}` - ); - } - - throw e; + return accept(payloadInput); } finally { verificationTimer?.(); } @@ -679,15 +644,17 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const {serializedData} = gossipData; const signedBlock = sszDeserialize(topic, serializedData); - const blockInput = await validateBeaconBlock( + const res = await validateBeaconBlock( signedBlock, serializedData, topic.boundary.fork, peerIdStr, seenTimestampSec ); + if (res.status !== GossipValidationStatus.Accept) return res; chain.serializedCache.set(signedBlock, serializedData); - handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec); + handleValidBeaconBlock(res.value, peerIdStr, seenTimestampSec); + return accept(undefined); }, [GossipType.blob_sidecar]: async ({ @@ -702,15 +669,11 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const index = blobSidecar.index; if (config.getForkSeq(blobSlot) < ForkSeq.deneb) { - throw new GossipActionError(GossipAction.REJECT, {code: "PRE_DENEB_BLOCK"}); + return reject("PRE_DENEB_BLOCK"); } - const blockInput = await validateBeaconBlob( - blobSidecar, - serializedData, - topic.subnet, - peerIdStr, - seenTimestampSec - ); + const res = await validateBeaconBlob(blobSidecar, serializedData, topic.subnet, peerIdStr, seenTimestampSec); + if (res.status !== GossipValidationStatus.Accept) return res; + const blockInput = res.value; chain.serializedCache.set(blobSidecar, serializedData); if (!blockInput.hasBlockAndAllData()) { const cutoffTimeMs = getCutoffTimeMs(chain, blobSlot, BLOCK_AVAILABILITY_CUTOFF_MS); @@ -734,6 +697,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }); }); } + return accept(undefined); }, [GossipType.data_column_sidecar]: async ({ @@ -751,22 +715,19 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand if (isForkPostGloas(fork)) { if (!isGloasDataColumnSidecar(dataColumnSidecar)) { - throw new DataColumnSidecarGossipError(GossipAction.REJECT, { - code: DataColumnSidecarErrorCode.INCORRECT_TYPE, - slot: dataColumnSlot, - columnIndex: index, - fork, - }); + return reject(DataColumnSidecarErrorCode.INCORRECT_TYPE, {slot: dataColumnSlot, columnIndex: index, fork}); } // After gloas, data columns are tracked in PayloadEnvelopeInput - const payloadInput = await validatePayloadDataColumn( + const res = await validatePayloadDataColumn( dataColumnSidecar, serializedData, topic.subnet, peerIdStr, seenTimestampSec ); + if (res.status !== GossipValidationStatus.Accept) return res; + const payloadInput = res.value; chain.serializedCache.set(dataColumnSidecar, serializedData); const payloadInputMeta = payloadInput.getLogMeta(); @@ -819,77 +780,75 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }); }); } - } else { - if (config.getForkSeq(dataColumnSlot) < ForkSeq.fulu) { - throw new GossipActionError(GossipAction.REJECT, {code: "PRE_FULU_BLOCK"}); - } + return accept(undefined); + } + if (config.getForkSeq(dataColumnSlot) < ForkSeq.fulu) { + return reject("PRE_FULU_BLOCK"); + } - if (isGloasDataColumnSidecar(dataColumnSidecar)) { - throw new DataColumnSidecarGossipError(GossipAction.REJECT, { - code: DataColumnSidecarErrorCode.INCORRECT_TYPE, - slot: dataColumnSlot, - columnIndex: index, - fork, - }); - } + if (isGloasDataColumnSidecar(dataColumnSidecar)) { + return reject(DataColumnSidecarErrorCode.INCORRECT_TYPE, {slot: dataColumnSlot, columnIndex: index, fork}); + } - // Before gloas, data columns are tracked in BlockInput - const blockInput = await validateBeaconDataColumn( - dataColumnSidecar, - serializedData, - topic.subnet, - peerIdStr, - seenTimestampSec - ); - chain.serializedCache.set(dataColumnSidecar, serializedData); - const blockInputMeta = blockInput.getLogMeta(); - const {receivedColumns} = blockInputMeta; - // it's not helpful to track every single column received - // instead of that, track 1st, 8th, 16th 32th, 64th, and 128th column - switch (receivedColumns) { - case 1: - case config.SAMPLES_PER_SLOT: - case 2 * config.SAMPLES_PER_SLOT: - case NUMBER_OF_COLUMNS / 4: - case NUMBER_OF_COLUMNS / 2: - case NUMBER_OF_COLUMNS: - metrics?.dataColumns.elapsedTimeTillReceived.observe({receivedOrder: receivedColumns}, delaySec); - break; - } + // Before gloas, data columns are tracked in BlockInput + const res = await validateBeaconDataColumn( + dataColumnSidecar, + serializedData, + topic.subnet, + peerIdStr, + seenTimestampSec + ); + if (res.status !== GossipValidationStatus.Accept) return res; + const blockInput = res.value; + chain.serializedCache.set(dataColumnSidecar, serializedData); + const blockInputMeta = blockInput.getLogMeta(); + const {receivedColumns} = blockInputMeta; + // it's not helpful to track every single column received + // instead of that, track 1st, 8th, 16th 32th, 64th, and 128th column + switch (receivedColumns) { + case 1: + case config.SAMPLES_PER_SLOT: + case 2 * config.SAMPLES_PER_SLOT: + case NUMBER_OF_COLUMNS / 4: + case NUMBER_OF_COLUMNS / 2: + case NUMBER_OF_COLUMNS: + metrics?.dataColumns.elapsedTimeTillReceived.observe({receivedOrder: receivedColumns}, delaySec); + break; + } - if (!blockInput.hasComputedAllData()) { - // immediately attempt fetch of data columns from execution engine - chain.getBlobsTracker.triggerGetBlobs(blockInput); - // if we've received at least half of the columns, trigger reconstruction of the rest - if (blockInput.columnCount >= NUMBER_OF_COLUMNS / 2) { - chain.columnReconstructionTracker.triggerColumnReconstruction(blockInput); - } + if (!blockInput.hasComputedAllData()) { + // immediately attempt fetch of data columns from execution engine + chain.getBlobsTracker.triggerGetBlobs(blockInput); + // if we've received at least half of the columns, trigger reconstruction of the rest + if (blockInput.columnCount >= NUMBER_OF_COLUMNS / 2) { + chain.columnReconstructionTracker.triggerColumnReconstruction(blockInput); } + } - if (!blockInput.hasBlockAndAllData()) { - const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS); - chain.logger.debug("Received gossip data column, waiting for full data availability", { - msToWait: cutoffTimeMs, - dataColumnIndex: index, - ...blockInputMeta, - }); - // do not await here to not delay gossip validation - blockInput.waitForBlockAndAllData(cutoffTimeMs).catch((_e) => { - chain.logger.debug( - "Waited for data after receiving gossip column. Cut-off reached so attempting to fetch remainder of BlockInput", - { - dataColumnIndex: index, - ...blockInputMeta, - } - ); - chain.emitter.emit(ChainEvent.incompleteBlockInput, { - blockInput, - peer: peerIdStr, - source: BlockInputSource.gossip, - }); + if (!blockInput.hasBlockAndAllData()) { + const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS); + chain.logger.debug("Received gossip data column, waiting for full data availability", { + msToWait: cutoffTimeMs, + dataColumnIndex: index, + ...blockInputMeta, + }); + // do not await here to not delay gossip validation + blockInput.waitForBlockAndAllData(cutoffTimeMs).catch((_e) => { + chain.logger.debug( + "Waited for data after receiving gossip column. Cut-off reached so attempting to fetch remainder of BlockInput", + { + dataColumnIndex: index, + ...blockInputMeta, + } + ); + chain.emitter.emit(ChainEvent.incompleteBlockInput, { + blockInput, + peer: peerIdStr, + source: BlockInputSource.gossip, }); - } + }); } + return accept(undefined); }, [GossipType.beacon_aggregate_and_proof]: async ({ @@ -898,29 +857,27 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand seenTimestampSec, }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; - let validationResult: AggregateAndProofValidationResult; const signedAggregateAndProof = sszDeserialize(topic, serializedData); const {fork} = topic.boundary; - try { - validationResult = await chain.beaconEngine.validateGossipAggregateAndProof( - serializedData, - fork, - signedAggregateAndProof - ); - } catch (e) { - if (e instanceof AttestationError && e.action === GossipAction.REJECT) { + const res = await chain.beaconEngine.validateGossipAggregateAndProof( + serializedData, + fork, + signedAggregateAndProof + ); + if (res.status !== GossipValidationStatus.Accept) { + if (res.status === GossipValidationStatus.Reject) { chain.persistInvalidSszValue( sszTypesFor(fork).SignedAggregateAndProof, signedAggregateAndProof, "gossip_reject" ); } - throw e; + return res; } // Handler - const {indexedAttestation, committeeValidatorIndices, attDataRootHex} = validationResult; + const {indexedAttestation, committeeValidatorIndices, attDataRootHex} = res.value; chain.validatorMonitor?.registerGossipAggregatedAttestation( seenTimestampSec, signedAggregateAndProof, @@ -949,6 +906,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand } chain.emitter.emit(routes.events.EventType.attestation, signedAggregateAndProof.message.aggregate); + return accept(undefined); }, [GossipType.attester_slashing]: async ({ @@ -958,7 +916,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const {serializedData} = gossipData; const {fork} = topic.boundary; const attesterSlashing = sszDeserialize(topic, serializedData); - await validateGossipAttesterSlashing(chain, attesterSlashing); + const res = await runGossipValidation(() => validateGossipAttesterSlashing(chain, attesterSlashing)); + if (res.status !== GossipValidationStatus.Accept) return res; // Handler @@ -970,6 +929,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand } chain.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing); + return accept(undefined); }, [GossipType.proposer_slashing]: async ({ @@ -978,7 +938,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const proposerSlashing = sszDeserialize(topic, serializedData); - await validateGossipProposerSlashing(chain, proposerSlashing); + const res = await runGossipValidation(() => validateGossipProposerSlashing(chain, proposerSlashing)); + if (res.status !== GossipValidationStatus.Accept) return res; // Handler @@ -989,12 +950,14 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand } chain.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing); + return accept(undefined); }, [GossipType.voluntary_exit]: async ({gossipData, topic}: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const voluntaryExit = sszDeserialize(topic, serializedData); - await validateGossipVoluntaryExit(chain, voluntaryExit); + const res = await runGossipValidation(() => validateGossipVoluntaryExit(chain, voluntaryExit)); + if (res.status !== GossipValidationStatus.Accept) return res; // Handler @@ -1005,6 +968,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand } chain.emitter.emit(routes.events.EventType.voluntaryExit, voluntaryExit); + return accept(undefined); }, [GossipType.sync_committee_contribution_and_proof]: async ({ @@ -1013,14 +977,17 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const contributionAndProof = sszDeserialize(topic, serializedData); - const {syncCommitteeParticipantIndices} = await chain.beaconEngine - .validateSyncCommitteeGossipContributionAndProof(serializedData, contributionAndProof) - .catch((e) => { - if (e instanceof SyncCommitteeError && e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue(ssz.altair.SignedContributionAndProof, contributionAndProof, "gossip_reject"); - } - throw e; - }); + const res = await chain.beaconEngine.validateSyncCommitteeGossipContributionAndProof( + serializedData, + contributionAndProof + ); + if (res.status !== GossipValidationStatus.Accept) { + if (res.status === GossipValidationStatus.Reject) { + chain.persistInvalidSszValue(ssz.altair.SignedContributionAndProof, contributionAndProof, "gossip_reject"); + } + return res; + } + const {syncCommitteeParticipantIndices} = res.value; // Handler chain.validatorMonitor?.registerGossipSyncContributionAndProof( @@ -1038,23 +1005,21 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand } chain.emitter.emit(routes.events.EventType.contributionAndProof, contributionAndProof); + return accept(undefined); }, [GossipType.sync_committee]: async ({gossipData, topic}: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const syncCommittee = sszDeserialize(topic, serializedData); const {subnet} = topic; - let indicesInSubcommittee: number[] = [0]; - try { - indicesInSubcommittee = ( - await chain.beaconEngine.validateGossipSyncCommittee(serializedData, syncCommittee, subnet) - ).indicesInSubcommittee; - } catch (e) { - if (e instanceof SyncCommitteeError && e.action === GossipAction.REJECT) { + const res = await chain.beaconEngine.validateGossipSyncCommittee(serializedData, syncCommittee, subnet); + if (res.status !== GossipValidationStatus.Accept) { + if (res.status === GossipValidationStatus.Reject) { chain.persistInvalidSszValue(ssz.altair.SyncCommitteeMessage, syncCommittee, "gossip_reject"); } - throw e; + return res; } + const {indicesInSubcommittee} = res.value; // Handler — add for ALL positions this validator holds in the subcommittee try { @@ -1065,6 +1030,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand } catch (e) { logger.debug("Error adding to syncCommittee pool", {subnet}, e as Error); } + return accept(undefined); }, [GossipType.light_client_finality_update]: async ({ @@ -1073,7 +1039,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const lightClientFinalityUpdate = sszDeserialize(topic, serializedData); - validateLightClientFinalityUpdate(config, chain, lightClientFinalityUpdate); + return runGossipValidation(async () => + validateLightClientFinalityUpdate(config, chain, lightClientFinalityUpdate) + ); }, [GossipType.light_client_optimistic_update]: async ({ @@ -1082,7 +1050,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const lightClientOptimisticUpdate = sszDeserialize(topic, serializedData); - validateLightClientOptimisticUpdate(config, chain, lightClientOptimisticUpdate); + return runGossipValidation(async () => + validateLightClientOptimisticUpdate(config, chain, lightClientOptimisticUpdate) + ); }, // blsToExecutionChange is to be generated and validated against GENESIS_FORK_VERSION @@ -1092,7 +1062,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const blsToExecutionChange = sszDeserialize(topic, serializedData); - await validateGossipBlsToExecutionChange(chain, blsToExecutionChange); + const res = await runGossipValidation(() => validateGossipBlsToExecutionChange(chain, blsToExecutionChange)); + if (res.status !== GossipValidationStatus.Accept) return res; // Handler try { @@ -1102,6 +1073,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand } chain.emitter.emit(routes.events.EventType.blsToExecutionChange, blsToExecutionChange); + return accept(undefined); }, [GossipType.execution_payload]: async ({ gossipData, @@ -1115,24 +1087,23 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand // unlike BlockInput, we send the envelope into UnknownBlockInput sync // inside the sync it'll reconcile into PayloadEnvelopeInput and share the same cache with gossip - try { - await chain.beaconEngine.validateGossipExecutionPayloadEnvelope(serializedData, signedEnvelope); - } catch (e) { - if (e instanceof ExecutionPayloadEnvelopeError) { - const {beaconBlockRoot} = signedEnvelope.message; - const slot = signedEnvelope.message.payload.slotNumber; - logger.debug("Gossip envelope has error", {slot, root: toRootHex(beaconBlockRoot), code: e.type.code}); - - if (e.action === GossipAction.REJECT) { - chain.persistInvalidSszValue( - ssz.gloas.SignedExecutionPayloadEnvelope, - signedEnvelope, - `gossip_reject_slot_${slot}` - ); - } + const res = await chain.beaconEngine.validateGossipExecutionPayloadEnvelope(serializedData, signedEnvelope); + if (res.status !== GossipValidationStatus.Accept) { + const {beaconBlockRoot} = signedEnvelope.message; + const envelopeSlot = signedEnvelope.message.payload.slotNumber; + logger.debug("Gossip envelope has error", { + slot: envelopeSlot, + root: toRootHex(beaconBlockRoot), + code: res.code, + }); + if (res.status === GossipValidationStatus.Reject) { + chain.persistInvalidSszValue( + ssz.gloas.SignedExecutionPayloadEnvelope, + signedEnvelope, + `gossip_reject_slot_${envelopeSlot}` + ); } - - throw e; + return res; } const slot = envelope.payload.slotNumber; @@ -1154,10 +1125,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand if (!payloadInput) { // This shouldn't happen because beacon block should have been imported and thus payload input should have been created. - throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { - code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING, - blockRoot: blockRootHex, - }); + return ignore(ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING, {blockRoot: blockRootHex}); } chain.serializedCache.set(signedEnvelope, serializedData); @@ -1212,6 +1180,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand e as Error ); }); + return accept(undefined); }, [GossipType.payload_attestation_message]: async ({ gossipData, @@ -1219,16 +1188,18 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const payloadAttestationMessage = sszDeserialize(topic, serializedData); - const validationResult = await chain.beaconEngine.validateGossipPayloadAttestationMessage( + const res = await chain.beaconEngine.validateGossipPayloadAttestationMessage( serializedData, payloadAttestationMessage ); + if (res.status !== GossipValidationStatus.Accept) return res; + const {attDataRootHex, validatorCommitteeIndices} = res.value; try { const insertOutcome = chain.payloadAttestationPool.add( payloadAttestationMessage, - validationResult.attDataRootHex, - validationResult.validatorCommitteeIndices + attDataRootHex, + validatorCommitteeIndices ); metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({insertOutcome}); } catch (e) { @@ -1237,10 +1208,11 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand chain.beaconEngine.forkChoice.notifyPtcMessages( toRootHex(payloadAttestationMessage.data.beaconBlockRoot), payloadAttestationMessage.data.slot, - validationResult.validatorCommitteeIndices, + validatorCommitteeIndices, payloadAttestationMessage.data.payloadPresent, payloadAttestationMessage.data.blobDataAvailable ); + return accept(undefined); }, [GossipType.execution_payload_bid]: async ({ gossipData, @@ -1248,10 +1220,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const executionPayloadBid = sszDeserialize(topic, serializedData); - const {proposerIndex} = await chain.beaconEngine.validateGossipExecutionPayloadBid( - serializedData, - executionPayloadBid - ); + const res = await chain.beaconEngine.validateGossipExecutionPayloadBid(serializedData, executionPayloadBid); + if (res.status !== GossipValidationStatus.Accept) return res; + const {proposerIndex} = res.value; // Handle valid payload bid by storing in a bid pool try { @@ -1267,6 +1238,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand version: config.getForkName(executionPayloadBid.message.slot), data: executionPayloadBid, }); + return accept(undefined); }, [GossipType.proposer_preferences]: async ({ gossipData, @@ -1274,13 +1246,15 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const signedProposerPreferences = sszDeserialize(topic, serializedData); - await chain.beaconEngine.validateGossipProposerPreferences(serializedData, signedProposerPreferences); + const res = await chain.beaconEngine.validateGossipProposerPreferences(serializedData, signedProposerPreferences); + if (res.status !== GossipValidationStatus.Accept) return res; chain.proposerPreferencesPool.add(signedProposerPreferences); chain.emitter.emit(routes.events.EventType.proposerPreferences, { version: ForkName.gloas, data: signedProposerPreferences, }); + return accept(undefined); }, }; } @@ -1293,8 +1267,8 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp return { [GossipType.beacon_attestation]: async ( gossipHandlerParams: GossipHandlerParamGeneric[] - ): Promise<(null | AttestationError)[]> => { - const results: (null | AttestationError)[] = []; + ): Promise => { + const results: GossipValidationVerdict[] = []; const attestationCount = gossipHandlerParams.length; if (attestationCount === 0) { return results; @@ -1313,12 +1287,10 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp validationParams ); for (const [i, validationResult] of validationResults.entries()) { - if (validationResult.err) { - results.push(validationResult.err as AttestationError); + if (validationResult.status !== GossipValidationStatus.Accept) { + results.push(validationResult); continue; } - // null means no error - results.push(null); // Handler const { @@ -1328,13 +1300,14 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp committeeIndex, validatorCommitteeIndex, committeeSize, - } = validationResult.result; + } = validationResult.value; chain.validatorMonitor?.registerGossipUnaggregatedAttestation( gossipHandlerParams[i].seenTimestampSec, indexedAttestation ); - const {subnet} = validationResult.result; + const {subnet} = validationResult.value; + results.push(accept(undefined)); try { // Node may be subscribe to extra subnets (long-lived random subnets). For those, validate the messages // but don't add to attestation pool, to save CPU and RAM @@ -1392,41 +1365,40 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp * Retry a function if it throws error code UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT */ export async function validateGossipFnRetryUnknownRoot( - fn: () => Promise, + fn: () => Promise>, network: INetwork, chain: IBeaconChain, slot: Slot, blockRoot: Root -): Promise { +): Promise> { let unknownBlockRootRetries = 0; while (true) { - try { - return await fn(); - } catch (e) { - const isUnknownAttestationRoot = - e instanceof AttestationError && e.type.code === AttestationErrorCode.UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT; - const isUnknownPayloadAttestationRoot = - e instanceof PayloadAttestationError && e.type.code === PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT; - - if (isUnknownAttestationRoot || isUnknownPayloadAttestationRoot) { - if (unknownBlockRootRetries === 0) { - // Trigger unknown block root search here - const rootHex = toRootHex(blockRoot); - network.searchUnknownBlock({slot, root: rootHex}, BlockInputSource.gossip); - } + const res = await fn(); + if (res.status === GossipValidationStatus.Accept) { + return res; + } - if (unknownBlockRootRetries++ < MAX_UNKNOWN_BLOCK_ROOT_RETRIES) { - const foundBlock = await chain.waitForBlock(slot, toRootHex(blockRoot)); - // Returns true if the block was found on time. In that case, try to get it from the fork-choice again. - // Otherwise, throw the error below. - if (foundBlock) { - continue; - } - } + const isUnknownRoot = + res.code === AttestationErrorCode.UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT || + res.code === PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT; + + if (isUnknownRoot) { + if (unknownBlockRootRetries === 0) { + // Trigger unknown block root search here + const rootHex = toRootHex(blockRoot); + network.searchUnknownBlock({slot, root: rootHex}, BlockInputSource.gossip); } - throw e; + if (unknownBlockRootRetries++ < MAX_UNKNOWN_BLOCK_ROOT_RETRIES) { + const foundBlock = await chain.waitForBlock(slot, toRootHex(blockRoot)); + // Returns true if the block was found on time. In that case, try to get it from the fork-choice again. + if (foundBlock) { + continue; + } + } } + + return res; } } diff --git a/packages/beacon-node/src/network/processor/gossipValidatorFn.ts b/packages/beacon-node/src/network/processor/gossipValidatorFn.ts index 19442c7173c4..f3743fb3d3ad 100644 --- a/packages/beacon-node/src/network/processor/gossipValidatorFn.ts +++ b/packages/beacon-node/src/network/processor/gossipValidatorFn.ts @@ -1,7 +1,8 @@ import {TopicValidatorResult} from "@libp2p/gossipsub"; import {ChainForkConfig} from "@lodestar/config"; import {Logger} from "@lodestar/utils"; -import {AttestationError, GossipAction, GossipActionError} from "../../chain/errors/index.js"; +import {GossipValidationStatus, GossipValidationVerdict} from "../../chain/beaconEngine/gossipValidationResult.js"; +import {AttestationErrorCode} from "../../chain/errors/index.js"; import {Metrics} from "../../metrics/index.js"; import { BatchGossipHandlerFn, @@ -46,43 +47,34 @@ export function getGossipValidatorBatchFn( })) ); - return results.map((e, i) => { - if (e == null) { + return results.map((res, i): TopicValidatorResult => { + if (res.status === GossipValidationStatus.Accept) { return TopicValidatorResult.Accept; } const {clientAgent, clientVersion, propagationSource} = messageInfos[i]; - if (!(e instanceof AttestationError)) { - logger.debug( - `Gossip batch validation ${type} threw a non-AttestationError`, - {peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion}, - e as Error - ); - metrics?.networkProcessor.gossipValidationIgnore.inc({topic: type}); - return TopicValidatorResult.Ignore; - } - - switch (e.action) { - case GossipAction.IGNORE: + switch (res.status) { + case GossipValidationStatus.Ignore: metrics?.networkProcessor.gossipValidationIgnore.inc({topic: type}); // only beacon_attestation topic is validated in batch - metrics?.networkProcessor.gossipAttestationIgnoreByReason.inc({reason: e.type.code}); + metrics?.networkProcessor.gossipAttestationIgnoreByReason.inc({reason: res.code as AttestationErrorCode}); return TopicValidatorResult.Ignore; - case GossipAction.REJECT: + case GossipValidationStatus.Reject: metrics?.networkProcessor.gossipValidationReject.inc({topic: type}); // only beacon_attestation topic is validated in batch - metrics?.networkProcessor.gossipAttestationRejectByReason.inc({reason: e.type.code}); + metrics?.networkProcessor.gossipAttestationRejectByReason.inc({reason: res.code as AttestationErrorCode}); logger.debug( `Gossip validation ${type} rejected`, {peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion}, - e + res.error ); return TopicValidatorResult.Reject; } }); } catch (e) { - // Don't expect error here + // Handlers return a verdict per message and no longer throw on validation failure, so this is only a + // safety net for an unexpected throw (handler bug). Map the whole batch to Ignore as before. logger.debug(`Gossip batch validation ${type} threw an error`, {}, e as Error); const results: TopicValidatorResult[] = []; for (let i = 0; i < messageInfos.length; i++) { @@ -121,49 +113,45 @@ export function getGossipValidatorFn(gossipHandlers: GossipHandlers, modules: Va }) { const type = topic.type; + let result: GossipValidationVerdict; try { - await (gossipHandlers[type] as GossipHandlerFn)({ + result = await (gossipHandlers[type] as GossipHandlerFn)({ gossipData: {serializedData: msg.data, msgSlot}, topic, peerIdStr: propagationSource, seenTimestampSec, }); + } catch (e) { + // Handlers should not throw — they return a verdict. An unexpected throw maps to Ignore (mirrors the + // previous "non-GossipActionError → Ignore" behavior). Not logged as error: too noisy/dangerous. + logger.debug( + `Gossip validation ${type} threw an unexpected error`, + {peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion}, + e as Error + ); + return TopicValidatorResult.Ignore; + } - metrics?.networkProcessor.gossipValidationAccept.inc({topic: type}); + switch (result.status) { + case GossipValidationStatus.Accept: + metrics?.networkProcessor.gossipValidationAccept.inc({topic: type}); + return TopicValidatorResult.Accept; - return TopicValidatorResult.Accept; - } catch (e) { - if (!(e instanceof GossipActionError)) { - // not deserve to log error here, it looks too dangerous to users + case GossipValidationStatus.Ignore: + // Note: LodestarError.code are bounded pre-declared error messages, not from arbitrary error.message + metrics?.networkProcessor.gossipValidationError.inc({topic: type, error: result.code}); + metrics?.networkProcessor.gossipValidationIgnore.inc({topic: type}); + return TopicValidatorResult.Ignore; + + case GossipValidationStatus.Reject: + metrics?.networkProcessor.gossipValidationError.inc({topic: type, error: result.code}); + metrics?.networkProcessor.gossipValidationReject.inc({topic: type}); logger.debug( - `Gossip validation ${type} threw a non-GossipActionError`, + `Gossip validation ${type} rejected`, {peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion}, - e as Error + result.error ); - return TopicValidatorResult.Ignore; - } - - // Metrics on specific error reason - // Note: LodestarError.code are bounded pre-declared error messages, not from arbitrary error.message - metrics?.networkProcessor.gossipValidationError.inc({ - topic: type, - error: (e as GossipActionError<{code: string}>).type.code, - }); - - switch (e.action) { - case GossipAction.IGNORE: - metrics?.networkProcessor.gossipValidationIgnore.inc({topic: type}); - return TopicValidatorResult.Ignore; - - case GossipAction.REJECT: - metrics?.networkProcessor.gossipValidationReject.inc({topic: type}); - logger.debug( - `Gossip validation ${type} rejected`, - {peerId: prettyPrintPeerIdStr(propagationSource), clientAgent, clientVersion}, - e - ); - return TopicValidatorResult.Reject; - } + return TopicValidatorResult.Reject; } }; } diff --git a/packages/beacon-node/src/sync/unknownBlock.ts b/packages/beacon-node/src/sync/unknownBlock.ts index e1c901cb9921..6bf8b1c01a4f 100644 --- a/packages/beacon-node/src/sync/unknownBlock.ts +++ b/packages/beacon-node/src/sync/unknownBlock.ts @@ -5,6 +5,7 @@ import {RequestError, RequestErrorCode} from "@lodestar/reqresp"; import {computeTimeAtSlot} from "@lodestar/state-transition"; import {RootHex, Slot, gloas, ssz} from "@lodestar/types"; import {Logger, fromHex, prettyPrintIndices, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils"; +import {GossipValidationStatus} from "../chain/beaconEngine/gossipValidationResult.js"; import {isBlockInputBlobs, isBlockInputColumns} from "../chain/blocks/blockInput/blockInput.js"; import {BlockInputSource, IBlockInput} from "../chain/blocks/blockInput/types.js"; import {PayloadError, PayloadErrorCode} from "../chain/blocks/importExecutionPayload.js"; @@ -898,14 +899,15 @@ export class BlockInputSync { const envelopeBytes = this.chain.serializedCache.get(pendingPayload.envelope) ?? ssz.gloas.SignedExecutionPayloadEnvelope.serialize(pendingPayload.envelope); - const validationResult = await wrapError( - this.chain.beaconEngine.validateGossipExecutionPayloadEnvelope(envelopeBytes, pendingPayload.envelope) + const validationResult = await this.chain.beaconEngine.validateGossipExecutionPayloadEnvelope( + envelopeBytes, + pendingPayload.envelope ); - if (validationResult.err) { + if (validationResult.status !== GossipValidationStatus.Accept) { this.logger.debug( "Pending payload envelope failed validation after block import, refetching by root", {slot: pendingPayload.envelope.message.payload.slotNumber, root: rootHex}, - validationResult.err + validationResult.error ); const pendingPayloadByRoot: PendingPayloadRootHex = { @@ -1129,7 +1131,10 @@ export class BlockInputSync { if (!payloadInput.hasPayloadEnvelope()) { const envelopeBytes = this.chain.serializedCache.get(envelope) ?? ssz.gloas.SignedExecutionPayloadEnvelope.serialize(envelope); - await this.chain.beaconEngine.validateGossipExecutionPayloadEnvelope(envelopeBytes, envelope); + const res = await this.chain.beaconEngine.validateGossipExecutionPayloadEnvelope(envelopeBytes, envelope); + if (res.status !== GossipValidationStatus.Accept) { + throw res.error ?? new Error(`Execution payload envelope validation failed: ${res.code}`); + } } let pendingPayload = this.toPendingPayloadInput(payloadInput, cacheItem, envelope); diff --git a/packages/beacon-node/test/e2e/network/gossipsub.test.ts b/packages/beacon-node/test/e2e/network/gossipsub.test.ts index abe852a09f36..8b1019f6a5d8 100644 --- a/packages/beacon-node/test/e2e/network/gossipsub.test.ts +++ b/packages/beacon-node/test/e2e/network/gossipsub.test.ts @@ -3,6 +3,7 @@ import {createChainForkConfig, defaultChainConfig} from "@lodestar/config"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {ssz} from "@lodestar/types"; import {sleep} from "@lodestar/utils"; +import {accept} from "../../../src/chain/beaconEngine/gossipValidationResult.js"; import {GossipHandlerParamGeneric, GossipHandlers, GossipType} from "../../../src/network/gossip/index.js"; import {Network} from "../../../src/network/index.js"; import {connect, onPeerConnect} from "../../utils/network.js"; @@ -70,6 +71,7 @@ function runTests({useWorker}: {useWorker: boolean}): void { const {netA, netB} = await mockModules({ [GossipType.voluntary_exit]: async ({gossipData}: GossipHandlerParamGeneric) => { onVoluntaryExit(gossipData.serializedData); + return accept(undefined); }, }); @@ -109,6 +111,7 @@ function runTests({useWorker}: {useWorker: boolean}): void { gossipData, }: GossipHandlerParamGeneric) => { onBlsToExecutionChange(gossipData.serializedData); + return accept(undefined); }, }); @@ -145,6 +148,7 @@ function runTests({useWorker}: {useWorker: boolean}): void { const {netA, netB} = await mockModules({ [GossipType.attester_slashing]: async ({gossipData}: GossipHandlerParamGeneric) => { onAttesterSlashingChange(gossipData.serializedData); + return accept(undefined); }, }); @@ -179,6 +183,7 @@ function runTests({useWorker}: {useWorker: boolean}): void { const {netA, netB} = await mockModules({ [GossipType.proposer_slashing]: async ({gossipData}: GossipHandlerParamGeneric) => { onProposerSlashingChange(gossipData.serializedData); + return accept(undefined); }, }); @@ -215,6 +220,7 @@ function runTests({useWorker}: {useWorker: boolean}): void { gossipData, }: GossipHandlerParamGeneric) => { onLightClientOptimisticUpdate(gossipData.serializedData); + return accept(undefined); }, }); @@ -254,6 +260,7 @@ function runTests({useWorker}: {useWorker: boolean}): void { gossipData, }: GossipHandlerParamGeneric) => { onLightClientFinalityUpdate(gossipData.serializedData); + return accept(undefined); }, }); diff --git a/packages/beacon-node/test/spec/utils/gossipValidation.ts b/packages/beacon-node/test/spec/utils/gossipValidation.ts index 37ad7128949e..1a9fbaa47c34 100644 --- a/packages/beacon-node/test/spec/utils/gossipValidation.ts +++ b/packages/beacon-node/test/spec/utils/gossipValidation.ts @@ -25,6 +25,10 @@ import { } from "@lodestar/state-transition"; import {RootHex, SignedBeaconBlock, ssz, sszTypesFor} from "@lodestar/types"; import {fromHex, loadYaml, toHex, toRootHex} from "@lodestar/utils"; +import { + GossipValidationResult, + GossipValidationStatus, +} from "../../../src/chain/beaconEngine/gossipValidationResult.js"; import {BlockInputPreData, BlockInputSource} from "../../../src/chain/blocks/blockInput/index.js"; import {AttestationImportOpt, BlobSidecarValidation} from "../../../src/chain/blocks/types.js"; import {GossipAction, GossipActionError} from "../../../src/chain/errors/gossipValidation.js"; @@ -334,6 +338,16 @@ function isDescendantAtFinalizedCheckpoint( } } +/** + * Engine validate methods now return a GossipValidationResult instead of throwing. Re-throw the failure + * (as a GossipActionError keyed on status/code) so the surrounding throw-based test flow is preserved. + */ +function assertAccepted(res: GossipValidationResult): void { + if (res.status === GossipValidationStatus.Accept) return; + const action = res.status === GossipValidationStatus.Reject ? GossipAction.REJECT : GossipAction.IGNORE; + throw res.error ?? new GossipActionError(action, {code: res.code}); +} + function mapErrorToResult(e: unknown): "valid" | "ignore" | "reject" { if (e instanceof GossipActionError) { return e.action === GossipAction.IGNORE ? "ignore" : "reject"; @@ -607,7 +621,7 @@ async function validateMessageForTopic( throw new GossipActionError(GossipAction.REJECT, {code: "SPEC_FINALIZED_NOT_ANCESTOR"}); } - await chain.beaconEngine.validateGossipBlock(bytes, signedBlock, fork); + assertAccepted(await chain.beaconEngine.validateGossipBlock(bytes, signedBlock, fork)); chain.seenBlockProposers.add(signedBlock.message.slot, signedBlock.message.proposerIndex); break; } @@ -697,7 +711,13 @@ async function validateMessageForTopic( const syncCommitteeMessage = rejectOnInvalidSerializedBytes(() => ssz.altair.SyncCommitteeMessage.deserialize(bytes) ); - await chain.beaconEngine.validateGossipSyncCommittee(bytes, syncCommitteeMessage, Number(message.subnet_id ?? 0)); + assertAccepted( + await chain.beaconEngine.validateGossipSyncCommittee( + bytes, + syncCommitteeMessage, + Number(message.subnet_id ?? 0) + ) + ); break; } @@ -705,7 +725,9 @@ async function validateMessageForTopic( const signedContributionAndProof = rejectOnInvalidSerializedBytes(() => ssz.altair.SignedContributionAndProof.deserialize(bytes) ); - await chain.beaconEngine.validateSyncCommitteeGossipContributionAndProof(bytes, signedContributionAndProof); + assertAccepted( + await chain.beaconEngine.validateSyncCommitteeGossipContributionAndProof(bytes, signedContributionAndProof) + ); break; } diff --git a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts index e40d2dcbab54..37f3de4360e0 100644 --- a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts +++ b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts @@ -9,6 +9,7 @@ import {ForkName} from "@lodestar/params"; import {RequestError, RequestErrorCode} from "@lodestar/reqresp"; import {SignedBeaconBlock, gloas, ssz} from "@lodestar/types"; import {notNullish, sleep, toRootHex} from "@lodestar/utils"; +import {accept} from "../../../src/chain/beaconEngine/gossipValidationResult.js"; import {BlockInputNoData} from "../../../src/chain/blocks/blockInput/blockInput.js"; import {BlockInputSource, DAType, IBlockInput} from "../../../src/chain/blocks/blockInput/types.js"; import {PayloadError, PayloadErrorCode} from "../../../src/chain/blocks/importExecutionPayload.js"; @@ -730,7 +731,11 @@ describe("UnknownBlockSync", () => { beforeEach(() => { vi.useFakeTimers({shouldAdvanceTime: true}); - vi.mocked(validateGossipExecutionPayloadEnvelope).mockReset().mockResolvedValue(undefined); + // Routed as the BeaconEngine method (see setupPayloadSyncTest), which returns a GossipValidationResult. + // The mock keeps the free-fn `Promise` type, so cast the engine-shaped value through void. + vi.mocked(validateGossipExecutionPayloadEnvelope) + .mockReset() + .mockResolvedValue(accept(undefined) as unknown as void); vi.mocked(validateGloasBlockDataColumnSidecars).mockReset().mockResolvedValue(undefined); }); @@ -980,6 +985,7 @@ describe("UnknownBlockSync", () => { if (!blockImported) { throw new Error("EXECUTION_PAYLOAD_ENVELOPE_ERROR_BLOCK_ROOT_UNKNOWN"); } + return accept(undefined) as unknown as void; }); ({emitter} = setupPayloadSyncTest({ From f3580dcda0a0d323b5e82ef85f012d14573ef22b Mon Sep 17 00:00:00 2001 From: twoeths Date: Fri, 19 Jun 2026 20:38:49 +0700 Subject: [PATCH 07/24] feat: move verifyBlockStateTransition() and importBlock() to BeaconEngine --- .../src/chain/beaconEngine/beaconEngine.ts | 602 +++++++++++++++++- .../src/chain/beaconEngine/interface.ts | 58 +- .../src/chain/blocks/importBlock.ts | 565 ++-------------- .../beacon-node/src/chain/blocks/index.ts | 118 ++-- .../beacon-node/src/chain/blocks/types.ts | 28 +- .../src/chain/blocks/verifyBlock.ts | 99 +-- .../blocks/verifyBlocksExecutionPayloads.ts | 28 +- packages/beacon-node/src/chain/chain.ts | 4 + 8 files changed, 801 insertions(+), 701 deletions(-) diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 292b8bb73d8a..1761df130c39 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -1,16 +1,48 @@ +import {BitArray} from "@chainsafe/ssz"; +import {routes} from "@lodestar/api"; import {BeaconConfig} from "@lodestar/config"; -import {CheckpointWithHex, ForkChoiceStateGetter, IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; -import {ForkName} from "@lodestar/params"; import { + AncestorStatus, + BlockExecutionStatus, + CheckpointWithHex, + EpochDifference, + ExecutionStatus, + ForkChoiceError, + ForkChoiceErrorCode, + ForkChoiceStateGetter, + IForkChoice, + PayloadExecutionStatus, + ProtoBlock, + UpdateHeadOpt, +} from "@lodestar/fork-choice"; +import { + ForkName, + ForkPostAltair, + ForkPostElectra, + ForkSeq, + GENESIS_SLOT, + MAX_SEED_LOOKAHEAD, + SLOTS_PER_EPOCH, +} from "@lodestar/params"; +import { + DataAvailabilityStatus, EffectiveBalanceIncrements, EpochShuffling, IBeaconStateView, PubkeyCache, + RootCache, computeEpochAtSlot, computeStartSlotAtEpoch, + computeTimeAtSlot, + isStatePostAltair, + isStatePostBellatrix, } from "@lodestar/state-transition"; import { + Attestation, + BeaconBlock, Epoch, + IndexedAttestation, + Root, RootHex, SignedAggregateAndProof, SignedBeaconBlock, @@ -18,16 +50,31 @@ import { ValidatorIndex, altair, deneb, + electra, fulu, gloas, + isGloasBeaconBlock, + phase0, + ssz, } from "@lodestar/types"; import {Logger, toRootHex} from "@lodestar/utils"; import {Metrics} from "../../metrics/index.js"; import {IClock} from "../../util/clock.js"; +import {callInNextEventLoop} from "../../util/eventLoop.js"; +import {isOptimisticBlock} from "../../util/forkChoice.js"; import {CheckpointBalancesCache} from "../balancesCache.js"; +import {isBlockInputBlobs, isBlockInputColumns} from "../blocks/blockInput/blockInput.js"; +import {IBlockInput} from "../blocks/blockInput/index.js"; import {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; +import {AttestationImportOpt, ImportBlockOpts} from "../blocks/types.js"; +import {getCheckpointFromState} from "../blocks/utils/checkpoint.js"; +import {verifyBlocksSignatures} from "../blocks/verifyBlocksSignatures.js"; +import {verifyBlocksStateTransitionOnly} from "../blocks/verifyBlocksStateTransitionOnly.js"; import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "../bls/index.js"; -import {initializeForkChoice} from "../forkChoice/index.js"; +import {ChainEvent, ChainEventEmitter, ReorgEventData} from "../emitter.js"; +import {BlockError, BlockErrorCode} from "../errors/index.js"; +import {ForkchoiceCaller, initializeForkChoice} from "../forkChoice/index.js"; +import {LightClientServer} from "../lightClient/index.js"; import { AggregatedAttestationPool, AttestationPool, @@ -38,6 +85,7 @@ import { SyncCommitteeMessagePool, SyncContributionAndProofPool, } from "../opPools/index.js"; +import {BlockProcessOpts} from "../options.js"; import {QueuedStateRegenerator, RegenCaller} from "../regen/index.js"; import { SeenAggregators, @@ -55,7 +103,7 @@ import {SeenAggregatedAttestations} from "../seenCache/seenAggregateAndProof.js" import {SeenAttestationDatas} from "../seenCache/seenAttestationData.js"; import {ShufflingCache} from "../shufflingCache.js"; import {FIFOBlockStateCache} from "../stateCache/fifoBlockStateCache.js"; -import {PersistentCheckpointStateCache} from "../stateCache/persistentCheckpointsCache.js"; +import {PersistentCheckpointStateCache, toCheckpointHex} from "../stateCache/persistentCheckpointsCache.js"; import {BlockStateCache, CheckpointStateCache} from "../stateCache/types.js"; import { AggregateAndProofValidationResult, @@ -88,10 +136,20 @@ import { import {validateGossipProposerPreferences} from "../validation/proposerPreferences.js"; import {validateApiSyncCommittee, validateGossipSyncCommittee} from "../validation/syncCommittee.js"; import {validateSyncCommitteeGossipContributionAndProof} from "../validation/syncCommitteeContributionAndProof.js"; +import {ValidatorMonitor} from "../validatorMonitor.js"; import {GossipValidationResult, fromResult, runGossipValidation} from "./gossipValidationResult.js"; -import {BeaconEngineModules, IBeaconEngine} from "./interface.js"; +import {BeaconEngineModules, IBeaconEngine, ImportBlockResult} from "./interface.js"; import {IBeaconEngineOptions} from "./options.js"; +type VerifiedBlockBundle = { + postState: IBeaconStateView; + blockInput: IBlockInput; + indexedAttestations: IndexedAttestation[]; + proposerBalanceDelta: number; + parentBlockSlot: number; + seenTimestampSec: number; +}; + /** * JS implementation of the consensus engine. Transitional in Phase 0: constructed inside * `BeaconChain` from the `anchorState` object; construction moves to the CLI in Phase 6. @@ -142,6 +200,10 @@ export class BeaconEngine implements IBeaconEngine { // it depends on the engine's own `forkChoice`. TODO - beacon engine: revisit ownership in a later phase. readonly seenBlockInputCache: SeenBlockInput; seenPayloadEnvelopeInputCache!: SeenPayloadEnvelopeInput; + readonly validatorMonitor: ValidatorMonitor | null; + lightClientServer: LightClientServer | undefined; + private readonly verifiedBlocks = new Map(); + private readonly emitter: ChainEventEmitter; constructor(modules: BeaconEngineModules, anchorState: IBeaconStateView) { const { @@ -167,6 +229,9 @@ export class BeaconEngine implements IBeaconEngine { this.clock = clock; this.pubkeyCache = pubkeyCache; this.seenBlockInputCache = seenBlockInputCache; + this.validatorMonitor = validatorMonitor; + this.lightClientServer = modules.lightClientServer; + this.emitter = emitter; // by default, verify signatures on both main threads and worker threads this.bls = opts.blsVerifyAllMainThread @@ -443,6 +508,533 @@ export class BeaconEngine implements IBeaconEngine { return runGossipValidation(() => validateGossipProposerPreferences(this, signedProposerPreferences)); } + async verifyBlocks( + _blockBytes: Uint8Array[], + parentBlock: ProtoBlock, + blockInputs: IBlockInput[], + opts: BlockProcessOpts & ImportBlockOpts, + signal: AbortSignal + ): Promise<{verifyStateTime: number; verifySignaturesTime: number}> { + const blocks = blockInputs.map((bi) => bi.getBlock()); + const block0 = blocks[0]; + if (!block0) throw Error("Empty blockInputs"); + const block0Epoch = computeEpochAtSlot(block0.message.slot); + const fork = this.config.getForkSeq(block0.message.slot); + + const preState0 = await this.regen + .getPreState(block0.message, {dontTransferCache: false}, RegenCaller.processBlocksInEpoch) + .catch((e) => { + throw new BlockError(block0, {code: BlockErrorCode.PRESTATE_MISSING, error: e as Error}); + }); + + this.shufflingCache.processState(preState0); + + if (block0Epoch !== computeEpochAtSlot(preState0.slot)) { + throw Error(`preState at slot ${preState0.slot} must be dialed to block epoch ${block0Epoch}`); + } + + const indexedAttestationsByBlock: IndexedAttestation[][] = []; + for (const [i, block] of blocks.entries()) { + indexedAttestationsByBlock[i] = block.message.body.attestations.map((attestation) => { + const attEpoch = computeEpochAtSlot(attestation.data.slot); + const decisionRoot = preState0.getShufflingDecisionRoot(attEpoch); + return this.shufflingCache.getIndexedAttestation(attEpoch, decisionRoot, fork, attestation); + }); + } + + const [{postStates, proposerBalanceDeltas, verifyStateTime}, {verifySignaturesTime}] = await Promise.all([ + verifyBlocksStateTransitionOnly( + preState0, + blockInputs, + blocks.map(() => DataAvailabilityStatus.Available), + this.logger, + this.metrics, + this.validatorMonitor, + signal, + opts + ), + opts.skipVerifyBlockSignatures !== true + ? verifyBlocksSignatures( + this.config, + this.bls, + this.logger, + this.metrics, + preState0, + blocks, + indexedAttestationsByBlock, + opts + ) + : Promise.resolve({verifySignaturesTime: Date.now()}), + ]); + + for (let i = 0; i < blockInputs.length; i++) { + const blockInput = blockInputs[i]; + const blockRootHex = blockInput.blockRootHex; + this.verifiedBlocks.set(blockRootHex, { + postState: postStates[i], + blockInput, + indexedAttestations: indexedAttestationsByBlock[i], + proposerBalanceDelta: proposerBalanceDeltas[i], + parentBlockSlot: i === 0 ? parentBlock.slot : blocks[i - 1].message.slot, + seenTimestampSec: opts.seenTimestampSec ?? Math.floor(Date.now() / 1000), + }); + } + + return {verifyStateTime, verifySignaturesTime}; + } + + async importBlock( + blockRoot: Root, + executionStatus: BlockExecutionStatus | PayloadExecutionStatus, + dataAvailabilityStatus: DataAvailabilityStatus, + opts: ImportBlockOpts + ): Promise { + // bytes-first input (native engine FFI); the JS cache is keyed by hex. + const blockRootHex = toRootHex(blockRoot); + const v = this.verifiedBlocks.get(blockRootHex); + if (!v) throw Error(`No verified block found for root ${blockRootHex}`); + + try { + return await this._importBlock(v, executionStatus, dataAvailabilityStatus, opts); + } finally { + this.verifiedBlocks.delete(blockRootHex); + } + } + + private async _importBlock( + v: VerifiedBlockBundle, + executionStatus: BlockExecutionStatus | PayloadExecutionStatus, + dataAvailabilityStatus: DataAvailabilityStatus, + opts: ImportBlockOpts + ): Promise { + const {blockInput, postState, parentBlockSlot, indexedAttestations, proposerBalanceDelta, seenTimestampSec} = v; + let execStatus = executionStatus; + const block = blockInput.getBlock(); + const source = blockInput.getBlockSource(); + const {slot: blockSlot} = block.message; + const blockRootHex = blockInput.blockRootHex; + const currentSlot = this.forkChoice.getTime(); + const currentEpoch = computeEpochAtSlot(currentSlot); + const blockEpoch = computeEpochAtSlot(blockSlot); + const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; + const blockDelaySec = seenTimestampSec - computeTimeAtSlot(this.config, blockSlot, postState.genesisTime); + const fork = this.config.getForkSeq(blockSlot); + const isExecutionState = isStatePostBellatrix(postState) && postState.isExecutionStateType; + + // 2. Import block to fork choice + this.checkpointBalancesCache.processState(blockRootHex, postState); + if (fork >= ForkSeq.gloas) { + const parentRootHex = toRootHex(block.message.parentRoot); + const parentBlock = this.forkChoice.getBlockHexDefaultStatus(parentRootHex); + if (parentBlock === null) { + throw Error(`Parent block not found in forkChoice, parentRoot=${parentRootHex}`); + } + if (parentBlock.executionStatus === ExecutionStatus.Invalid) { + throw Error(`Parent block has invalid execution status, parentRoot=${parentRootHex}`); + } + execStatus = parentBlock.executionStatus; + } + + // getBeaconProposerOrNull returns null if the head state is more than one epoch away from the + // block slot; we skip the proposer-boost canonical check as we cannot determine the proposer. + const expectedProposerIndex: number | null = this.getHeadState().getBeaconProposerOrNull(blockSlot); + + const blockSummary = this.forkChoice.onBlock( + block.message, + postState, + blockDelaySec, + currentSlot, + execStatus, + dataAvailabilityStatus, + expectedProposerIndex + ); + + this.regen.processState(blockRootHex, postState); + this.metrics?.importBlock.bySource.inc({source: source.source}); + this.logger.verbose("Added block to forkchoice and state cache", {slot: blockSlot, root: blockRootHex}); + + // 3. Import attestations to fork choice + const FORK_CHOICE_ATT_EPOCH_LIMIT = 1; + const attestationsResult: {blockEpoch: number; attestingIndices: number[]}[] = []; + + if ( + opts.importAttestations === AttestationImportOpt.Force || + (opts.importAttestations !== AttestationImportOpt.Skip && + blockEpoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT) + ) { + const attestations = block.message.body.attestations; + const rootCache = new RootCache(postState); + const invalidAttestationErrorsByCode = new Map(); + + const addAttestation = + fork >= ForkSeq.electra ? this.addAttestationPostElectra.bind(this) : this.addAttestationPreElectra.bind(this); + + for (let i = 0; i < attestations.length; i++) { + const attestation = attestations[i]; + try { + const indexedAttestation = indexedAttestations[i]; + const {target, beaconBlockRoot} = attestation.data; + const attDataRoot = toRootHex(ssz.phase0.AttestationData.hashTreeRoot(indexedAttestation.data)); + + addAttestation( + postState, + target, + attDataRoot, + attestation as Attestation, + indexedAttestation + ); + + if ( + opts.importAttestations === AttestationImportOpt.Force || + (target.epoch <= currentEpoch && target.epoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT) + ) { + this.forkChoice.onAttestation( + indexedAttestation, + attDataRoot, + opts.importAttestations === AttestationImportOpt.Force + ); + } + + attestationsResult.push({blockEpoch, attestingIndices: Array.from(indexedAttestation.attestingIndices)}); + + const correctHead = ssz.Root.equals(rootCache.getBlockRootAtSlot(attestation.data.slot), beaconBlockRoot); + const missedSlotVote = + attestation.data.slot > GENESIS_SLOT && + ssz.Root.equals( + rootCache.getBlockRootAtSlot(attestation.data.slot - 1), + rootCache.getBlockRootAtSlot(attestation.data.slot) + ); + this.validatorMonitor?.registerAttestationInBlock( + indexedAttestation, + parentBlockSlot, + correctHead, + missedSlotVote, + blockRootHex, + blockSlot + ); + } catch (e) { + if (e instanceof ForkChoiceError && e.type.code === ForkChoiceErrorCode.INVALID_ATTESTATION) { + let errWithCount = invalidAttestationErrorsByCode.get(e.type.err.code); + if (errWithCount === undefined) { + errWithCount = {error: e as Error, count: 1}; + invalidAttestationErrorsByCode.set(e.type.err.code, errWithCount); + } else { + errWithCount.count++; + } + } else { + this.logger.warn("Error processing attestation from block", {slot: blockSlot}, e as Error); + } + } + } + + for (const {error, count} of invalidAttestationErrorsByCode.values()) { + this.logger.warn( + "Error processing attestations from block", + {slot: blockSlot, erroredAttestations: count}, + error + ); + } + } + + // 4. Import attester slashings + if ( + opts.importAttestations === AttestationImportOpt.Force || + (opts.importAttestations !== AttestationImportOpt.Skip && + blockEpoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT - 1 - MAX_SEED_LOOKAHEAD) + ) { + for (const slashing of block.message.body.attesterSlashings) { + try { + this.forkChoice.onAttesterSlashing(slashing); + } catch (e) { + this.logger.warn("Error processing AttesterSlashing from block", {slot: blockSlot}, e as Error); + } + } + } + + // 4.5. Import payload attestations (Gloas) + if (isGloasBeaconBlock(block.message)) { + for (const payloadAttestation of block.message.body.payloadAttestations) { + try { + const ptcIndices: number[] = []; + for (let i = 0; i < payloadAttestation.aggregationBits.bitLen; i++) { + if (payloadAttestation.aggregationBits.get(i)) { + ptcIndices.push(i); + } + } + if (ptcIndices.length > 0) { + this.forkChoice.notifyPtcMessages( + toRootHex(payloadAttestation.data.beaconBlockRoot), + payloadAttestation.data.slot, + ptcIndices, + payloadAttestation.data.payloadPresent, + payloadAttestation.data.blobDataAvailable + ); + } + } catch (e) { + this.logger.warn("Error processing PayloadAttestation from block", {slot: blockSlot}, e as Error); + } + } + } + + // 5. Compute head + const oldHead = this.forkChoice.getHead(); + const oldHeadBlockRoot = oldHead.blockRoot; + + this.metrics?.forkChoice.requests.inc(); + const newHeadTimer = this.metrics?.forkChoice.findHead.startTimer({caller: ForkchoiceCaller.importBlock}); + let newHead: ProtoBlock; + try { + newHead = this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head; + } catch (e) { + this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead}); + throw e; + } finally { + newHeadTimer?.(); + } + + const currFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; + + let headChanged = false; + let headResult: ImportBlockResult["head"] = null; + let reorg: ReorgEventData | null = null; + + if (newHead.blockRoot !== oldHead.blockRoot) { + headChanged = true; + + this.regen.updateHeadState(newHead, postState); + + try { + headResult = { + block: newHead.blockRoot, + epochTransition: computeStartSlotAtEpoch(computeEpochAtSlot(newHead.slot)) === newHead.slot, + slot: newHead.slot, + state: newHead.stateRoot, + previousDutyDependentRoot: this.forkChoice.getDependentRoot(newHead, EpochDifference.previous), + currentDutyDependentRoot: this.forkChoice.getDependentRoot(newHead, EpochDifference.current), + executionOptimistic: isOptimisticBlock(newHead), + }; + } catch (e) { + this.logger.debug("Error building head event data", {slot: newHead.slot, root: newHead.blockRoot}, e as Error); + } + + const delaySec = this.clock.secFromSlot(newHead.slot); + this.logger.verbose("New chain head", {slot: newHead.slot, root: newHead.blockRoot, delaySec}); + + if (this.metrics) { + this.metrics.headSlot.set(newHead.slot); + if (delaySec < (SLOTS_PER_EPOCH * this.config.SLOT_DURATION_MS) / 1000) { + this.metrics.importBlock.elapsedTimeTillBecomeHead.observe(delaySec); + const cutOffSec = this.config.getAttestationDueMs(this.config.getForkName(blockSlot)) / 1000; + if (delaySec > cutOffSec) { + this.metrics.importBlock.setHeadAfterCutoff.inc(); + } + } + } + + this.syncContributionAndProofPool.prune(newHead.slot); + this.seenContributionAndProof.prune(newHead.slot); + + this.metrics?.forkChoice.changedHead.inc(); + + const ancestorResult = this.forkChoice.getCommonAncestorDepth(oldHead, newHead); + if (ancestorResult.code === AncestorStatus.CommonAncestor) { + reorg = { + depth: ancestorResult.depth, + epoch: computeEpochAtSlot(newHead.slot), + slot: newHead.slot, + newHeadBlock: newHead.blockRoot, + oldHeadBlock: oldHead.blockRoot, + newHeadState: newHead.stateRoot, + oldHeadState: oldHead.stateRoot, + executionOptimistic: isOptimisticBlock(newHead), + }; + this.metrics?.forkChoice.reorg.inc(); + this.metrics?.forkChoice.reorgDistance.observe(ancestorResult.depth); + } + + if (blockEpoch >= this.config.ALTAIR_FORK_EPOCH) { + callInNextEventLoop(() => { + try { + if (isStatePostAltair(postState)) { + this.lightClientServer?.onImportBlockHead( + block.message as BeaconBlock, + postState, + parentBlockSlot + ); + } + } catch (e) { + this.logger.verbose("Error lightClientServer.onImportBlock", {slot: blockSlot}, e as Error); + } + }); + } + } + + const parentEpoch = computeEpochAtSlot(parentBlockSlot); + if (parentEpoch < blockEpoch) { + this.shufflingCache.processState(postState); + this.logger.verbose("Processed shuffling for next epoch", {parentEpoch, blockEpoch, slot: blockSlot}); + } + + if (blockSlot % SLOTS_PER_EPOCH === 0) { + const checkpointState = postState; + const cp = getCheckpointFromState(checkpointState); + this.regen.addCheckpointState(cp, checkpointState); + this.emitter.emit(ChainEvent.checkpoint, cp, checkpointState); + this.logger.verbose("Checkpoint processed", toCheckpointHex(cp)); + + const activeValidatorsCount = checkpointState.activeValidatorCount; + this.metrics?.currentActiveValidators.set(activeValidatorsCount); + this.metrics?.currentValidators.set({status: "active"}, activeValidatorsCount); + + const parentBlockSummary = isGloasBeaconBlock(block.message) + ? this.forkChoice.getBlockHexAndBlockHash( + toRootHex(checkpointState.latestBlockHeader.parentRoot), + toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash) + ) + : this.forkChoice.getBlockDefaultStatus(checkpointState.latestBlockHeader.parentRoot); + + if (parentBlockSummary) { + const justifiedCheckpoint = checkpointState.currentJustifiedCheckpoint; + const justifiedEpoch = justifiedCheckpoint.epoch; + const preJustifiedEpoch = parentBlockSummary.justifiedEpoch; + if (justifiedEpoch > preJustifiedEpoch) { + this.logger.verbose("Checkpoint justified", toCheckpointHex(justifiedCheckpoint)); + this.metrics?.previousJustifiedEpoch.set(checkpointState.previousJustifiedCheckpoint.epoch); + this.metrics?.currentJustifiedEpoch.set(justifiedCheckpoint.epoch); + } + const finalizedCheckpoint = checkpointState.finalizedCheckpoint; + const finalizedEpoch = finalizedCheckpoint.epoch; + const preFinalizedEpoch = parentBlockSummary.finalizedEpoch; + if (finalizedEpoch > preFinalizedEpoch) { + this.emitter.emit(routes.events.EventType.finalizedCheckpoint, { + block: toRootHex(finalizedCheckpoint.root), + epoch: finalizedCheckpoint.epoch, + state: toRootHex(checkpointState.hashTreeRoot()), + executionOptimistic: false, + }); + this.logger.verbose("Checkpoint finalized", toCheckpointHex(finalizedCheckpoint)); + this.metrics?.finalizedEpoch.set(finalizedCheckpoint.epoch); + } + } + } + + let proposerIndexNextSlot: number | null = null; + if (blockSlot >= currentSlot && isExecutionState) { + try { + proposerIndexNextSlot = postState.getBeaconProposer(blockSlot + 1); + } catch { + // epoch boundary, proposer shuffle not yet stable + } + } + + if (!postState.isStateValidatorsNodesPopulated()) { + this.logger.verbose("After importBlock caching postState without SSZ cache", {slot: postState.slot}); + } + + this.metrics?.parentBlockDistance.observe(blockSlot - parentBlockSlot); + this.metrics?.proposerBalanceDeltaAny.observe(proposerBalanceDelta); + this.validatorMonitor?.registerImportedBlock(block.message, {proposerBalanceDelta}); + if (isStatePostAltair(postState)) { + this.validatorMonitor?.registerSyncAggregateInBlock( + blockEpoch, + (block as altair.SignedBeaconBlock).message.body.syncAggregate, + postState.currentSyncCommitteeIndexed.validatorIndices + ); + } + + if (isBlockInputColumns(blockInput)) { + for (const {source} of blockInput.getSampledColumnsWithSource()) { + this.metrics?.dataColumns.bySource.inc({source}); + } + } else if (isBlockInputBlobs(blockInput)) { + for (const {source} of blockInput.getAllBlobsWithSource()) { + this.metrics?.importBlock.blobsBySource.inc({blobsSource: source}); + } + } + + return { + headChanged, + head: headResult, + reorg, + blockSummary, + proposerIndexNextSlot, + isExecutionState, + prevFinalizedEpoch, + currFinalizedEpoch, + oldHeadBlockRoot, + newHeadBlockRoot: newHead.blockRoot, + attestations: attestationsResult, + blockMeta: { + slot: blockSlot, + blockRootHex, + proposerBalanceDelta, + parentBlockSlot, + seenTimestampSec, + }, + }; + } + + private addAttestationPreElectra( + _state: IBeaconStateView, + target: phase0.Checkpoint, + attDataRoot: string, + attestation: Attestation, + indexedAttestation: phase0.IndexedAttestation + ): void { + this.seenAggregatedAttestations.add( + target.epoch, + attestation.data.index, + attDataRoot, + {aggregationBits: attestation.aggregationBits, trueBitCount: indexedAttestation.attestingIndices.length}, + true + ); + } + + private addAttestationPostElectra( + state: IBeaconStateView, + target: phase0.Checkpoint, + attDataRoot: string, + attestation: Attestation, + indexedAttestation: electra.IndexedAttestation + ): void { + const committeeIndices = attestation.committeeBits.getTrueBitIndexes(); + if (committeeIndices.length === 1) { + this.seenAggregatedAttestations.add( + target.epoch, + committeeIndices[0], + attDataRoot, + {aggregationBits: attestation.aggregationBits, trueBitCount: indexedAttestation.attestingIndices.length}, + true + ); + } else { + const attSlot = attestation.data.slot; + const attEpoch = computeEpochAtSlot(attSlot); + const decisionRoot = state.getShufflingDecisionRoot(attEpoch); + const committees = this.shufflingCache.getBeaconCommittees(attEpoch, decisionRoot, attSlot, committeeIndices); + const aggregationBools = attestation.aggregationBits.toBoolArray(); + let offset = 0; + for (let i = 0; i < committees.length; i++) { + const committee = committees[i]; + const aggregationBits = BitArray.fromBoolArray(aggregationBools.slice(offset, offset + committee.length)); + const trueBitCount = aggregationBits.getTrueBitIndexes().length; + offset += committee.length; + this.seenAggregatedAttestations.add( + target.epoch, + committeeIndices[i], + attDataRoot, + {aggregationBits, trueBitCount}, + true + ); + } + } + } + + discardVerifiedBlocks(blockRootHexes: string[]): void { + for (const r of blockRootHexes) { + this.verifiedBlocks.delete(r); + } + } + // TODO - beacon engine: scalar state reads (getBeaconProposer, getValidator, getBalance, // getRandaoMix, getBlockRootAtSlot, getStateRootAtSlot, getShufflingDecisionRoot). Deferred — // signature shape (state vs stateRoot) to be decided alongside Phase 4 bytes-first. diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 46894545d176..24b583c8b0c0 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -1,8 +1,10 @@ import {BeaconConfig} from "@lodestar/config"; -import {IForkChoice} from "@lodestar/fork-choice"; +import {BlockExecutionStatus, IForkChoice, PayloadExecutionStatus, ProtoBlock} from "@lodestar/fork-choice"; import {ForkName} from "@lodestar/params"; -import {IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; +import {DataAvailabilityStatus, IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; import { + Root, + RootHex, SignedAggregateAndProof, SignedBeaconBlock, SubnetID, @@ -17,8 +19,12 @@ import {IBeaconDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; import {BufferPool} from "../../util/bufferPool.js"; import {IClock} from "../../util/clock.js"; +import {IBlockInput} from "../blocks/blockInput/index.js"; import {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; -import {ChainEventEmitter} from "../emitter.js"; +import {ImportBlockOpts} from "../blocks/types.js"; +import {ChainEventEmitter, ReorgEventData} from "../emitter.js"; +import {LightClientServer} from "../lightClient/index.js"; +import {BlockProcessOpts} from "../options.js"; import {SeenBlockInput} from "../seenCache/seenGossipBlockInput.js"; import {CPStateDatastore} from "../stateCache/datastore/types.js"; import {AggregateAndProofValidationResult} from "../validation/aggregateAndProof.js"; @@ -45,6 +51,36 @@ export type BeaconEngineModules = { validatorMonitor: ValidatorMonitor | null; seenBlockInputCache: SeenBlockInput; isAnchorStateFinalized: boolean; + lightClientServer?: LightClientServer; +}; + +export type ImportBlockResult = { + headChanged: boolean; + head: { + block: string; + slot: number; + state: string; + epochTransition: boolean; + previousDutyDependentRoot: string; + currentDutyDependentRoot: string; + executionOptimistic: boolean; + } | null; + reorg: ReorgEventData | null; + blockSummary: ProtoBlock | null; + proposerIndexNextSlot: number | null; + isExecutionState: boolean; + prevFinalizedEpoch: number; + currFinalizedEpoch: number; + oldHeadBlockRoot: string; + newHeadBlockRoot: string; + attestations: {blockEpoch: number; attestingIndices: number[]}[]; + blockMeta: { + slot: number; + blockRootHex: string; + proposerBalanceDelta: number; + parentBlockSlot: number; + seenTimestampSec: number; + }; }; /** @@ -140,4 +176,20 @@ export interface IBeaconEngine { preferencesBytes: Uint8Array, signedProposerPreferences: gloas.SignedProposerPreferences ): Promise>; + verifyBlocks( + _blockBytes: Uint8Array[], + parentBlock: ProtoBlock, + blockInputs: IBlockInput[], + opts: BlockProcessOpts & ImportBlockOpts, + signal: AbortSignal + ): Promise<{verifyStateTime: number; verifySignaturesTime: number}>; + // `blockRoot` is the SSZ root as raw bytes (the verify output handle, import input) — bytes-first for + // the native engine FFI. The JS engine keys its internal cache by hex (converted here). + importBlock( + blockRoot: Root, + executionStatus: BlockExecutionStatus | PayloadExecutionStatus, + dataAvailabilityStatus: DataAvailabilityStatus, + opts: ImportBlockOpts + ): Promise; + discardVerifiedBlocks(blockRootHexes: RootHex[]): void; } diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 6c5a79cb1736..ef016d2359d8 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -1,44 +1,22 @@ -import {BitArray} from "@chainsafe/ssz"; import {routes} from "@lodestar/api"; import { - AncestorStatus, - EpochDifference, - ExecutionStatus, - ForkChoiceError, - ForkChoiceErrorCode, + BlockExecutionStatus, NotReorgedReason, + PayloadExecutionStatus, getSafeExecutionBlockHash, } from "@lodestar/fork-choice"; -import {ForkPostAltair, ForkPostElectra, ForkSeq, MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params"; -import { - IBeaconStateView, - RootCache, - computeEpochAtSlot, - computeStartSlotAtEpoch, - computeTimeAtSlot, - isStartSlotOfEpoch, - isStatePostAltair, - isStatePostBellatrix, -} from "@lodestar/state-transition"; -import {Attestation, BeaconBlock, altair, capella, electra, isGloasBeaconBlock, phase0, ssz} from "@lodestar/types"; -import {isErrorAborted, toRootHex} from "@lodestar/utils"; -import {GENESIS_SLOT, ZERO_HASH_HEX} from "../../constants/index.js"; +import {DataAvailabilityStatus, isStartSlotOfEpoch} from "@lodestar/state-transition"; +import {capella} from "@lodestar/types"; +import {fromHex, isErrorAborted} from "@lodestar/utils"; +import {ZERO_HASH_HEX} from "../../constants/index.js"; import {callInNextEventLoop} from "../../util/eventLoop.js"; import {isOptimisticBlock} from "../../util/forkChoice.js"; import {isQueueErrorAborted} from "../../util/queue/index.js"; import type {BeaconChain} from "../chain.js"; -import {ChainEvent, ReorgEventData} from "../emitter.js"; -import {ForkchoiceCaller} from "../forkChoice/index.js"; import {REPROCESS_MIN_TIME_TO_NEXT_SLOT_SEC} from "../reprocess.js"; -import {toCheckpointHex} from "../stateCache/persistentCheckpointsCache.js"; -import {isBlockInputBlobs, isBlockInputColumns} from "./blockInput/blockInput.js"; -import {AttestationImportOpt, FullyVerifiedBlock, ImportBlockOpts} from "./types.js"; -import {getCheckpointFromState} from "./utils/checkpoint.js"; +import {IBlockInput} from "./blockInput/index.js"; +import {ImportBlockOpts} from "./types.js"; -/** - * Fork-choice allows to import attestations from current (0) or past (1) epoch. - */ -const FORK_CHOICE_ATT_EPOCH_LIMIT = 1; /** * Emit eventstream events for block contents events only for blocks that are recent enough to clock */ @@ -46,44 +24,17 @@ const EVENTSTREAM_EMIT_RECENT_BLOCK_SLOTS = 64; /** * Imports a fully verified block into the chain state. Produces multiple permanent side-effects. - * - * ImportBlock order of operations must guarantee that BeaconNode does not end in an unknown state: - * - * 1. Persist block to hot DB (pre-emptively) - * - Done before importing block to fork-choice to guarantee that blocks in the fork-choice *always* are persisted - * in the DB. Otherwise the beacon node may end up in an unrecoverable state. If a block is persisted in the hot - * db but is unknown by the fork-choice, then it will just use some extra disk space. On restart is will be - * pruned regardless. - * - Note that doing a disk write first introduces a small delay before setting the head. An improvement where disk - * write happens latter requires the ability to roll back a fork-choice head change if disk write fails - * - * 2. Import block to fork-choice - * 3. Import attestations to fork-choice - * 4. Import attester slashings to fork-choice - * 5. Compute head. If new head, immediately stateCache.setHeadState() - * 6. Queue notifyForkchoiceUpdate to engine api - * 7. Add post state to stateCache */ export async function importBlock( this: BeaconChain, - fullyVerifiedBlock: FullyVerifiedBlock, + blockInput: IBlockInput, + executionStatus: BlockExecutionStatus | PayloadExecutionStatus, + dataAvailabilityStatus: DataAvailabilityStatus, opts: ImportBlockOpts ): Promise { - const {blockInput, postState, parentBlockSlot, dataAvailabilityStatus, indexedAttestations} = fullyVerifiedBlock; - let {executionStatus} = fullyVerifiedBlock; const block = blockInput.getBlock(); - const source = blockInput.getBlockSource(); const {slot: blockSlot} = block.message; - const blockRoot = this.config.getForkTypes(blockSlot).BeaconBlock.hashTreeRoot(block.message); - const blockRootHex = toRootHex(blockRoot); - const currentSlot = this.forkChoice.getTime(); - const currentEpoch = computeEpochAtSlot(currentSlot); - const blockEpoch = computeEpochAtSlot(blockSlot); - const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; - const blockDelaySec = - fullyVerifiedBlock.seenTimestampSec - computeTimeAtSlot(this.config, blockSlot, postState.genesisTime); const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000); - const fork = this.config.getForkSeq(blockSlot); // this is just a type assertion since blockinput with dataPromise type will not end up here if (!blockInput.hasAllData) { @@ -91,9 +42,6 @@ export async function importBlock( } // 1. Persist block to hot DB (performed asynchronously to avoid blocking head selection) - // Wait for space in the write queue to apply backpressure during sync. - // Without this, a supernode syncing from behind can accumulate many blocks worth of column - // data in memory (up to 128 columns per block) causing OOM before persistence catches up. await this.unfinalizedBlockWrites.waitForSpace(); this.unfinalizedBlockWrites.push(blockInput).catch((e) => { if (!isQueueErrorAborted(e)) { @@ -101,309 +49,62 @@ export async function importBlock( } }); - // 2. Import block to fork choice - - // Should compute checkpoint balances before forkchoice.onBlock - this.checkpointBalancesCache.processState(blockRootHex, postState); - if (fork >= ForkSeq.gloas) { - const parentRootHex = toRootHex(block.message.parentRoot); - const parentBlock = this.forkChoice.getBlockHexDefaultStatus(parentRootHex); - if (parentBlock === null) { - throw Error(`Parent block not found in forkChoice, parentRoot=${parentRootHex}`); - } - if (parentBlock.executionStatus === ExecutionStatus.Invalid) { - throw Error(`Parent block has invalid execution status, parentRoot=${parentRootHex}`); - } - executionStatus = parentBlock.executionStatus; - } - - // getBeaconProposerOrNull will return null if head state is more than one epoch away - // from block slot. We skip proposer boost canonical check as we cannot determine the canonical proposer - const expectedProposerIndex: number | null = this.getHeadState().getBeaconProposerOrNull(blockSlot); - - const blockSummary = this.beaconEngine.forkChoice.onBlock( - block.message, - postState, - blockDelaySec, - currentSlot, + // 2-5. Delegate to engine: state transition, fork choice, head computation. + // Engine takes the root as bytes (native-engine bytes-first contract). + const r = await this.beaconEngine.importBlock( + fromHex(blockInput.blockRootHex), executionStatus, dataAvailabilityStatus, - expectedProposerIndex + opts ); - // This adds the state necessary to process the next block - // Some block event handlers require state being in state cache so need to do this before emitting EventType.block - this.regen.processState(blockRootHex, postState); - - this.metrics?.importBlock.bySource.inc({source: source.source}); - this.logger.verbose("Added block to forkchoice and state cache", {slot: blockSlot, root: blockRootHex}); - - // 3. Import attestations to fork choice - // - // - For each attestation - // - Get indexed attestation - // - Register attestation with fork-choice - // - Register attestation with validator monitor (only after sync) - // Only process attestations of blocks with relevant attestations for the fork-choice: - // If current epoch is N, and block is epoch X, block may include attestations for epoch X or X - 1. - // The latest block that is useful is at epoch N - 1 which may include attestations for epoch N - 1 or N - 2. - if ( - opts.importAttestations === AttestationImportOpt.Force || - (opts.importAttestations !== AttestationImportOpt.Skip && blockEpoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT) - ) { - const attestations = block.message.body.attestations; - const rootCache = new RootCache(postState); - const invalidAttestationErrorsByCode = new Map(); - - const addAttestation = fork >= ForkSeq.electra ? addAttestationPostElectra : addAttestationPreElectra; - - for (let i = 0; i < attestations.length; i++) { - const attestation = attestations[i]; - try { - const indexedAttestation = indexedAttestations[i]; - const {target, beaconBlockRoot} = attestation.data; - - const attDataRoot = toRootHex(ssz.phase0.AttestationData.hashTreeRoot(indexedAttestation.data)); - addAttestation.call( - this, - postState, - target, - attDataRoot, - attestation as Attestation, - indexedAttestation - ); - // Duplicated logic from fork-choice onAttestation validation logic. - // Attestations outside of this range will be dropped as Errors, so no need to import - if ( - opts.importAttestations === AttestationImportOpt.Force || - (target.epoch <= currentEpoch && target.epoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT) - ) { - this.beaconEngine.forkChoice.onAttestation( - indexedAttestation, - attDataRoot, - opts.importAttestations === AttestationImportOpt.Force - ); - } - - // Note: To avoid slowing down sync, only register attestations within FORK_CHOICE_ATT_EPOCH_LIMIT - this.seenBlockAttesters.addIndices(blockEpoch, indexedAttestation.attestingIndices); - - const correctHead = ssz.Root.equals(rootCache.getBlockRootAtSlot(attestation.data.slot), beaconBlockRoot); - const missedSlotVote = - attestation.data.slot > GENESIS_SLOT && - ssz.Root.equals( - rootCache.getBlockRootAtSlot(attestation.data.slot - 1), - rootCache.getBlockRootAtSlot(attestation.data.slot) - ); - this.validatorMonitor?.registerAttestationInBlock( - indexedAttestation, - parentBlockSlot, - correctHead, - missedSlotVote, - blockRootHex, - blockSlot - ); - } catch (e) { - // a block has a lot of attestations and it may has same error, we don't want to log all of them - if (e instanceof ForkChoiceError && e.type.code === ForkChoiceErrorCode.INVALID_ATTESTATION) { - let errWithCount = invalidAttestationErrorsByCode.get(e.type.err.code); - if (errWithCount === undefined) { - errWithCount = {error: e as Error, count: 1}; - invalidAttestationErrorsByCode.set(e.type.err.code, errWithCount); - } else { - errWithCount.count++; - } - } else { - // always log other errors - this.logger.warn("Error processing attestation from block", {slot: blockSlot}, e as Error); - } - } - } - - for (const {error, count} of invalidAttestationErrorsByCode.values()) { - this.logger.warn( - "Error processing attestations from block", - {slot: blockSlot, erroredAttestations: count}, - error - ); - } + // Register attesters seen in this block + for (const {blockEpoch, attestingIndices} of r.attestations) { + this.seenBlockAttesters.addIndices(blockEpoch, attestingIndices); } - // 4. Import attester slashings to fork choice - // - // FORK_CHOICE_ATT_EPOCH_LIMIT is for attestation to become valid - // but AttesterSlashing could be found before that time and still able to submit valid attestations - // until slashed validator become inactive, see computeActivationExitEpoch() function - if ( - opts.importAttestations === AttestationImportOpt.Force || - (opts.importAttestations !== AttestationImportOpt.Skip && - blockEpoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT - 1 - MAX_SEED_LOOKAHEAD) - ) { - for (const slashing of block.message.body.attesterSlashings) { - try { - // all AttesterSlashings are valid before reaching this - this.beaconEngine.forkChoice.onAttesterSlashing(slashing); - } catch (e) { - this.logger.warn("Error processing AttesterSlashing from block", {slot: blockSlot}, e as Error); - } - } - } - - // 4.5. Import payload attestations to fork choice (Gloas) - // - if (isGloasBeaconBlock(block.message)) { - for (const payloadAttestation of block.message.body.payloadAttestations) { - try { - // Extract PTC indices from aggregation bits - const ptcIndices: number[] = []; - for (let i = 0; i < payloadAttestation.aggregationBits.bitLen; i++) { - if (payloadAttestation.aggregationBits.get(i)) { - ptcIndices.push(i); - } - } - - if (ptcIndices.length > 0) { - this.beaconEngine.forkChoice.notifyPtcMessages( - toRootHex(payloadAttestation.data.beaconBlockRoot), - payloadAttestation.data.slot, - ptcIndices, - payloadAttestation.data.payloadPresent, - payloadAttestation.data.blobDataAvailable - ); - } - } catch (e) { - this.logger.warn("Error processing PayloadAttestation from block", {slot: blockSlot}, e as Error); - } - } - } - - // 5. Compute head. If new head, immediately stateCache.setHeadState() - - const oldHead = this.forkChoice.getHead(); - const newHead = this.recomputeForkChoiceHead(ForkchoiceCaller.importBlock); - const currFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; - - if (newHead.blockRoot !== oldHead.blockRoot) { - // Set head state as strong reference - this.regen.updateHeadState(newHead, postState); - + // Emit head event (engine already pruned pools and set head state) + if (r.head !== null) { try { - this.emitter.emit(routes.events.EventType.head, { - block: newHead.blockRoot, - epochTransition: computeStartSlotAtEpoch(computeEpochAtSlot(newHead.slot)) === newHead.slot, - slot: newHead.slot, - state: newHead.stateRoot, - previousDutyDependentRoot: this.forkChoice.getDependentRoot(newHead, EpochDifference.previous), - currentDutyDependentRoot: this.forkChoice.getDependentRoot(newHead, EpochDifference.current), - executionOptimistic: isOptimisticBlock(newHead), - }); + this.emitter.emit(routes.events.EventType.head, r.head); } catch (e) { - // getDependentRoot() may fail with error: "No block for root" as we can see in holesky non-finality issue - this.logger.debug("Error emitting head event", {slot: newHead.slot, root: newHead.blockRoot}, e as Error); - } - - const delaySec = this.clock.secFromSlot(newHead.slot); - this.logger.verbose("New chain head", { - slot: newHead.slot, - root: newHead.blockRoot, - delaySec, - }); - - if (this.metrics) { - this.metrics.headSlot.set(newHead.slot); - // Only track "recent" blocks. Otherwise sync can distort this metrics heavily. - // We want to track recent blocks coming from gossip, unknown block sync, and API. - if (delaySec < (SLOTS_PER_EPOCH * this.config.SLOT_DURATION_MS) / 1000) { - this.metrics.importBlock.elapsedTimeTillBecomeHead.observe(delaySec); - const cutOffSec = this.config.getAttestationDueMs(this.config.getForkName(blockSlot)) / 1000; - if (delaySec > cutOffSec) { - this.metrics.importBlock.setHeadAfterCutoff.inc(); - } - } - } - - this.onNewHead(newHead); - - this.metrics?.forkChoice.changedHead.inc(); - - const ancestorResult = this.forkChoice.getCommonAncestorDepth(oldHead, newHead); - if (ancestorResult.code === AncestorStatus.CommonAncestor) { - // CommonAncestor = chain reorg, old head and new head not direct descendants - - const forkChoiceReorgEventData: ReorgEventData = { - depth: ancestorResult.depth, - epoch: computeEpochAtSlot(newHead.slot), - slot: newHead.slot, - newHeadBlock: newHead.blockRoot, - oldHeadBlock: oldHead.blockRoot, - newHeadState: newHead.stateRoot, - oldHeadState: oldHead.stateRoot, - executionOptimistic: isOptimisticBlock(newHead), - }; - - this.emitter.emit(routes.events.EventType.chainReorg, forkChoiceReorgEventData); - this.logger.verbose("Chain reorg", forkChoiceReorgEventData); - - this.metrics?.forkChoice.reorg.inc(); - this.metrics?.forkChoice.reorgDistance.observe(ancestorResult.depth); + this.logger.debug("Error emitting head event", {slot: r.head.slot, root: r.head.block}, e as Error); } + } - // Lightclient server support (only after altair) - // - Persist state witness - // - Use block's syncAggregate - if (blockEpoch >= this.config.ALTAIR_FORK_EPOCH) { - // we want to import block asap so do this in the next event loop - callInNextEventLoop(() => { - try { - if (isStatePostAltair(postState)) { - this.lightClientServer?.onImportBlockHead( - block.message as BeaconBlock, - postState, - parentBlockSlot - ); - } - } catch (e) { - this.logger.verbose("Error lightClientServer.onImportBlock", {slot: blockSlot}, e as Error); - } - }); - } + // Emit chain reorg event + if (r.reorg !== null) { + this.emitter.emit(routes.events.EventType.chainReorg, r.reorg); + this.logger.verbose("Chain reorg", r.reorg); } // 6. Queue notifyForkchoiceUpdate to engine api - // - // NOTE: forkChoice.fsStore.finalizedCheckpoint MUST only change in response to an onBlock event - // Notifying EL of head and finalized updates as below is usually done within the 1st 4s of the slot. - // If there is an advanced payload generation in the next slot, we'll notify EL again 4s before next - // slot via PrepareNextSlotScheduler. There is no harm updating the ELs with same data, it will just ignore it. - - // Suppress fcu call if shouldOverrideFcu is true. This only happens if we have proposer boost reorg enabled - // and the block is weak and can potentially be reorged out. let shouldOverrideFcu = false; - if (blockSlot >= currentSlot && isStatePostBellatrix(postState) && postState.isExecutionStateType) { + if (r.isExecutionState && r.blockMeta.slot >= this.forkChoice.getTime()) { let notOverrideFcuReason = NotReorgedReason.Unknown; - const proposalSlot = blockSlot + 1; + const proposalSlot = r.blockMeta.slot + 1; try { - const proposerIndex = postState.getBeaconProposer(proposalSlot); - const feeRecipient = this.beaconProposerCache.get(proposerIndex); - - if (feeRecipient) { - // We would set this to true if - // 1) This is a gossip block - // 2) We are proposer of next slot - // 3) Proposer boost reorg related flag is turned on (this is checked inside the function) - // 4) Block meets the criteria of being re-orged out (this is also checked inside the function) - const result = this.forkChoice.shouldOverrideForkChoiceUpdate( - blockSummary, - this.clock.secFromSlot(currentSlot), - currentSlot - ); - shouldOverrideFcu = result.shouldOverrideFcu; - if (!result.shouldOverrideFcu) { - notOverrideFcuReason = result.reason; + const proposerIndex = r.proposerIndexNextSlot; + if (proposerIndex !== null) { + const feeRecipient = this.beaconProposerCache.get(proposerIndex); + if (feeRecipient && r.blockSummary !== null) { + const result = this.forkChoice.shouldOverrideForkChoiceUpdate( + r.blockSummary, + this.clock.secFromSlot(this.forkChoice.getTime()), + this.forkChoice.getTime() + ); + shouldOverrideFcu = result.shouldOverrideFcu; + if (!result.shouldOverrideFcu) { + notOverrideFcuReason = result.reason; + } + } else { + notOverrideFcuReason = NotReorgedReason.NotProposerOfNextSlot; } } else { - notOverrideFcuReason = NotReorgedReason.NotProposerOfNextSlot; + if (isStartSlotOfEpoch(proposalSlot)) { + notOverrideFcuReason = NotReorgedReason.NotShufflingStable; + } } } catch (e) { if (isStartSlotOfEpoch(proposalSlot)) { @@ -415,14 +116,14 @@ export async function importBlock( if (shouldOverrideFcu) { this.logger.verbose("Weak block detected. Skip fcu call in importBlock", { - blockRoot: blockRootHex, - slot: blockSlot, + blockRoot: r.blockMeta.blockRootHex, + slot: r.blockMeta.slot, }); } else { this.metrics?.importBlock.notOverrideFcuReason.inc({reason: notOverrideFcuReason}); this.logger.verbose("Strong block detected. Not override fcu call", { - blockRoot: blockRootHex, - slot: blockSlot, + blockRoot: r.blockMeta.blockRootHex, + slot: r.blockMeta.slot, reason: notOverrideFcuReason, }); } @@ -430,22 +131,10 @@ export async function importBlock( if ( !this.opts.disableImportExecutionFcU && - (newHead.blockRoot !== oldHead.blockRoot || currFinalizedEpoch !== prevFinalizedEpoch) && + (r.newHeadBlockRoot !== r.oldHeadBlockRoot || r.currFinalizedEpoch !== r.prevFinalizedEpoch) && !shouldOverrideFcu ) { - /** - * On post BELLATRIX_EPOCH but pre TTD, blocks include empty execution payload with a zero block hash. - * The consensus clients must not send notifyForkchoiceUpdate before TTD since the execution client will error. - * So we must check that: - * - `headBlockHash !== null` -> Pre BELLATRIX_EPOCH - * - `headBlockHash !== ZERO_HASH` -> Pre TTD - */ const headBlockHash = this.forkChoice.getHead().executionPayloadBlockHash ?? ZERO_HASH_HEX; - /** - * After BELLATRIX_EPOCH and TTD it's okay to send a zero hash block hash for the finalized block. This will happen if - * the current finalized block does not contain any execution payload at all (pre MERGE_EPOCH) or if it contains a - * zero block hash (pre TTD) - */ const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; if (headBlockHash !== ZERO_HASH_HEX) { @@ -464,70 +153,11 @@ export async function importBlock( } } - if (!postState.isStateValidatorsNodesPopulated()) { - this.logger.verbose("After importBlock caching postState without SSZ cache", {slot: postState.slot}); - } - - // Cache shufflings when crossing an epoch boundary - const parentEpoch = computeEpochAtSlot(parentBlockSlot); - if (parentEpoch < blockEpoch) { - this.shufflingCache.processState(postState); - 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 cp = getCheckpointFromState(checkpointState); - this.regen.addCheckpointState(cp, checkpointState); - // consumers should not mutate state ever - this.emitter.emit(ChainEvent.checkpoint, cp, checkpointState); - - // Note: in-lined code from previos handler of ChainEvent.checkpoint - this.logger.verbose("Checkpoint processed", toCheckpointHex(cp)); - - const activeValidatorsCount = checkpointState.activeValidatorCount; - this.metrics?.currentActiveValidators.set(activeValidatorsCount); - this.metrics?.currentValidators.set({status: "active"}, activeValidatorsCount); + const blockRootHex = r.blockMeta.blockRootHex; + const blockSummary = r.blockSummary; - const parentBlockSummary = isGloasBeaconBlock(block.message) - ? this.forkChoice.getBlockHexAndBlockHash( - toRootHex(checkpointState.latestBlockHeader.parentRoot), - toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash) - ) - : this.forkChoice.getBlockDefaultStatus(checkpointState.latestBlockHeader.parentRoot); - - if (parentBlockSummary) { - const justifiedCheckpoint = checkpointState.currentJustifiedCheckpoint; - const justifiedEpoch = justifiedCheckpoint.epoch; - const preJustifiedEpoch = parentBlockSummary.justifiedEpoch; - if (justifiedEpoch > preJustifiedEpoch) { - this.logger.verbose("Checkpoint justified", toCheckpointHex(justifiedCheckpoint)); - this.metrics?.previousJustifiedEpoch.set(checkpointState.previousJustifiedCheckpoint.epoch); - this.metrics?.currentJustifiedEpoch.set(justifiedCheckpoint.epoch); - } - const finalizedCheckpoint = checkpointState.finalizedCheckpoint; - const finalizedEpoch = finalizedCheckpoint.epoch; - const preFinalizedEpoch = parentBlockSummary.finalizedEpoch; - if (finalizedEpoch > preFinalizedEpoch) { - this.emitter.emit(routes.events.EventType.finalizedCheckpoint, { - block: toRootHex(finalizedCheckpoint.root), - epoch: finalizedCheckpoint.epoch, - state: toRootHex(checkpointState.hashTreeRoot()), - executionOptimistic: false, - }); - this.logger.verbose("Checkpoint finalized", toCheckpointHex(finalizedCheckpoint)); - this.metrics?.finalizedEpoch.set(finalizedCheckpoint.epoch); - } - } - } - - // Send block events, only for recent enough blocks - - if (currentSlot - blockSlot < EVENTSTREAM_EMIT_RECENT_BLOCK_SLOTS) { - // We want to import block asap so call all event handler in the next event loop + if (this.clock.currentSlot - blockSlot < EVENTSTREAM_EMIT_RECENT_BLOCK_SLOTS) { callInNextEventLoop(() => { - // NOTE: Skip emitting if there are no listeners from the API if (this.emitter.listenerCount(routes.events.EventType.block)) { this.emitter.emit(routes.events.EventType.block, { block: blockRootHex, @@ -563,32 +193,7 @@ export async function importBlock( }); } - // Register stat metrics about the block after importing it - this.metrics?.parentBlockDistance.observe(blockSlot - parentBlockSlot); - this.metrics?.proposerBalanceDeltaAny.observe(fullyVerifiedBlock.proposerBalanceDelta); - this.validatorMonitor?.registerImportedBlock(block.message, fullyVerifiedBlock); - if (isStatePostAltair(fullyVerifiedBlock.postState)) { - this.validatorMonitor?.registerSyncAggregateInBlock( - blockEpoch, - (block as altair.SignedBeaconBlock).message.body.syncAggregate, - fullyVerifiedBlock.postState.currentSyncCommitteeIndexed.validatorIndices - ); - } - - if (isBlockInputColumns(blockInput)) { - for (const {source} of blockInput.getSampledColumnsWithSource()) { - this.metrics?.dataColumns.bySource.inc({source}); - } - } else if (isBlockInputBlobs(blockInput)) { - for (const {source} of blockInput.getAllBlobsWithSource()) { - this.metrics?.importBlock.blobsBySource.inc({blobsSource: source}); - } - } - const advancedSlot = this.clock.slotWithFutureTolerance(REPROCESS_MIN_TIME_TO_NEXT_SLOT_SEC); - - // Gossip blocks need to be imported as soon as possible, waiting attestations could be processed - // in the next event loop. See https://github.com/ChainSafe/lodestar/issues/4789 callInNextEventLoop(() => { this.reprocessController.onBlockImported({slot: blockSlot, root: blockRootHex}, advancedSlot); }); @@ -609,61 +214,3 @@ export async function importBlock( delaySec: this.clock.secFromSlot(blockSlot), }); } - -export function addAttestationPreElectra( - this: BeaconChain, - // added to have the same signature as addAttestationPostElectra - _: IBeaconStateView, - target: phase0.Checkpoint, - attDataRoot: string, - attestation: Attestation, - indexedAttestation: phase0.IndexedAttestation -): void { - this.seenAggregatedAttestations.add( - target.epoch, - attestation.data.index, - attDataRoot, - {aggregationBits: attestation.aggregationBits, trueBitCount: indexedAttestation.attestingIndices.length}, - true - ); -} - -export function addAttestationPostElectra( - this: BeaconChain, - state: IBeaconStateView, - target: phase0.Checkpoint, - attDataRoot: string, - attestation: Attestation, - indexedAttestation: electra.IndexedAttestation -): void { - const committeeIndices = attestation.committeeBits.getTrueBitIndexes(); - if (committeeIndices.length === 1) { - this.seenAggregatedAttestations.add( - target.epoch, - committeeIndices[0], - attDataRoot, - {aggregationBits: attestation.aggregationBits, trueBitCount: indexedAttestation.attestingIndices.length}, - true - ); - } else { - const attSlot = attestation.data.slot; - const attEpoch = computeEpochAtSlot(attSlot); - const decisionRoot = state.getShufflingDecisionRoot(attEpoch); - const committees = this.shufflingCache.getBeaconCommittees(attEpoch, decisionRoot, attSlot, committeeIndices); - const aggregationBools = attestation.aggregationBits.toBoolArray(); - let offset = 0; - for (let i = 0; i < committees.length; i++) { - const committee = committees[i]; - const aggregationBits = BitArray.fromBoolArray(aggregationBools.slice(offset, offset + committee.length)); - const trueBitCount = aggregationBits.getTrueBitIndexes().length; - offset += committee.length; - this.seenAggregatedAttestations.add( - target.epoch, - committeeIndices[i], - attDataRoot, - {aggregationBits, trueBitCount}, - true - ); - } - } -} diff --git a/packages/beacon-node/src/chain/blocks/index.ts b/packages/beacon-node/src/chain/blocks/index.ts index 097813b38703..d85c4993c4f0 100644 --- a/packages/beacon-node/src/chain/blocks/index.ts +++ b/packages/beacon-node/src/chain/blocks/index.ts @@ -1,3 +1,5 @@ +import {BlockExecutionStatus} from "@lodestar/fork-choice"; +import {DataAvailabilityStatus} from "@lodestar/state-transition"; import {SignedBeaconBlock, Slot} from "@lodestar/types"; import {isErrorAborted, toRootHex} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; @@ -10,7 +12,7 @@ import {IBlockInput} from "./blockInput/types.js"; import {importBlock} from "./importBlock.js"; import {importExecutionPayload} from "./importExecutionPayload.js"; import {PayloadEnvelopeInput} from "./payloadEnvelopeInput/payloadEnvelopeInput.js"; -import {FullyVerifiedBlock, ImportBlockOpts} from "./types.js"; +import {ImportBlockOpts} from "./types.js"; import {assertLinearChainSegment} from "./utils/chainSegment.js"; import {verifyBlocksInEpoch} from "./verifyBlock.js"; import {verifyBlocksSanityChecks} from "./verifyBlocksSanityChecks.js"; @@ -65,7 +67,7 @@ export async function processBlocks( } try { - const {relevantBlocks, parentSlots, parentBlock} = verifyBlocksSanityChecks(this, blocks, payloadEnvelopes, opts); + const {relevantBlocks, parentBlock} = verifyBlocksSanityChecks(this, blocks, payloadEnvelopes, opts); // No relevant blocks, skip verifyBlocksInEpoch() if (relevantBlocks.length === 0 || parentBlock === null) { @@ -90,69 +92,69 @@ export async function processBlocks( // Fully verify a block to be imported immediately after. Does not produce any side-effects besides adding intermediate // states in the state cache through regen. - const { - postStates, - blockDAStatuses, - payloadDAStatuses, - proposerBalanceDeltas, - segmentExecStatus, - indexedAttestationsByBlock, - } = await verifyBlocksInEpoch.call(this, parentBlock, relevantBlocks, payloadEnvelopes, opts); - - // If segmentExecStatus has lvhForkchoice then, the entire segment should be invalid - // and we need to further propagate - if (segmentExecStatus.execAborted !== null) { - if (segmentExecStatus.invalidSegmentLVH !== undefined) { - this.beaconEngine.forkChoice.validateLatestHash(segmentExecStatus.invalidSegmentLVH); - } - throw segmentExecStatus.execAborted.execError; - } + const {segmentExecStatus, blockDAStatuses, payloadDAStatuses} = await verifyBlocksInEpoch.call( + this, + parentBlock, + relevantBlocks, + payloadEnvelopes, + opts + ); - const {executionStatuses} = segmentExecStatus; - const verifiedBlocksBySlot = new Map(); - for (let i = 0; i < relevantBlocks.length; i++) { - const block = relevantBlocks[i]; - verifiedBlocksBySlot.set(block.getBlock().message.slot, { - blockInput: block, - postState: postStates[i], - parentBlockSlot: parentSlots[i], - executionStatus: executionStatuses[i], - // start supporting optimistic syncing/processing - dataAvailabilityStatus: blockDAStatuses[i], - proposerBalanceDelta: proposerBalanceDeltas[i], - indexedAttestations: indexedAttestationsByBlock[i], - // TODO: Make this param mandatory and capture in gossip - seenTimestampSec: opts.seenTimestampSec ?? Math.floor(Date.now() / 1000), - }); - } + // verifyBlocksInEpoch cached the verified consensus bundles in the engine (by root). Ensure they + // are always evicted — on the execAborted throw below, on a mid-loop import error, and on success + // (importBlock deletes each as it consumes it, so the sweep is a no-op cleanup of any remainder). + const verifiedBlockRoots = relevantBlocks.map((b) => b.blockRootHex); + try { + // If segmentExecStatus has lvhForkchoice then, the entire segment should be invalid + // and we need to further propagate + if (segmentExecStatus.execAborted !== null) { + if (segmentExecStatus.invalidSegmentLVH !== undefined) { + this.beaconEngine.forkChoice.validateLatestHash(segmentExecStatus.invalidSegmentLVH); + } + throw segmentExecStatus.execAborted.execError; + } - const slotSet = new Set(blocks.map((b) => b.getBlock().message.slot)); - if (payloadEnvelopes) { - for (const slot of payloadEnvelopes.keys()) slotSet.add(slot); - } - const slots = Array.from(slotSet).sort((a, b) => a - b); - for (const slot of slots) { - const fullyVerifiedBlock = verifiedBlocksBySlot.get(slot); - if (fullyVerifiedBlock !== undefined) { - // TODO: Consider batching importBlock too if it takes significant time - await importBlock.call(this, fullyVerifiedBlock, opts); + const {executionStatuses} = segmentExecStatus; + const blockInputsBySlot = new Map< + Slot, + {blockInput: IBlockInput; executionStatus: BlockExecutionStatus; blockDAStatus: DataAvailabilityStatus} + >(); + for (let i = 0; i < relevantBlocks.length; i++) { + const block = relevantBlocks[i]; + blockInputsBySlot.set(block.getBlock().message.slot, { + blockInput: block, + executionStatus: executionStatuses[i], + blockDAStatus: blockDAStatuses[i], + }); } - const payloadInput = payloadEnvelopes?.get(slot); - if (payloadInput?.hasPayloadEnvelope()) { - if (!payloadInput.isComplete()) { - // we validated DA before reaching this - throw new Error(`Payload envelope for slot ${slot} not complete after DA verification`); + const slotSet = new Set(blocks.map((b) => b.getBlock().message.slot)); + if (payloadEnvelopes) { + for (const slot of payloadEnvelopes.keys()) slotSet.add(slot); + } + const slots = Array.from(slotSet).sort((a, b) => a - b); + for (const slot of slots) { + const entry = blockInputsBySlot.get(slot); + if (entry !== undefined) { + await importBlock.call(this, entry.blockInput, entry.executionStatus, entry.blockDAStatus, opts); } - // we already awaited DA in verifyBlocksInEpoch for this segment - const payloadDA = payloadDAStatuses.get(slot); - if (payloadDA === undefined) { - throw new Error(`Missing payload DA status for slot ${slot}`); + + const payloadInput = payloadEnvelopes?.get(slot); + if (payloadInput?.hasPayloadEnvelope()) { + if (!payloadInput.isComplete()) { + throw new Error(`Payload envelope for slot ${slot} not complete after DA verification`); + } + const payloadDA = payloadDAStatuses.get(slot); + if (payloadDA === undefined) { + throw new Error(`Missing payload DA status for slot ${slot}`); + } + await importExecutionPayload.call(this, payloadInput, payloadDA, {validSignature: false}); } - await importExecutionPayload.call(this, payloadInput, payloadDA, {validSignature: false}); - } - await nextEventLoop(); + await nextEventLoop(); + } + } finally { + this.beaconEngine.discardVerifiedBlocks(verifiedBlockRoots); } } catch (e) { if (isErrorAborted(e) || isQueueErrorAborted(e) || isBlockErrorAborted(e)) { diff --git a/packages/beacon-node/src/chain/blocks/types.ts b/packages/beacon-node/src/chain/blocks/types.ts index 63892b0d4ce3..e8fee5ab8754 100644 --- a/packages/beacon-node/src/chain/blocks/types.ts +++ b/packages/beacon-node/src/chain/blocks/types.ts @@ -1,9 +1,7 @@ import type {ChainForkConfig} from "@lodestar/config"; -import type {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"; -import {IBlockInput} from "./blockInput/types.js"; +import {computeEpochAtSlot} from "@lodestar/state-transition"; +import type {Slot, fulu} from "@lodestar/types"; export enum GossipedInputType { block = "block", @@ -88,25 +86,3 @@ export type ImportBlockOpts = { /** Seen timestamp seconds */ seenTimestampSec?: number; }; - -/** - * A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and ready to import. - * - * `executionStatus` reflects the outcome of execution payload verification at block-import time: - * - pre-gloas: Valid | Syncing | PreMerge (from EL notifyNewPayload against the in-block payload) - * - post-gloas: inherited from parent's chain (Valid/Syncing) by importBlock; payload arrives - * separately as an envelope and creates the FULL variant later via onExecutionPayload - */ -export type FullyVerifiedBlock = { - blockInput: IBlockInput; - postState: IBeaconStateView; - parentBlockSlot: Slot; - proposerBalanceDelta: number; - dataAvailabilityStatus: DataAvailabilityStatus; - /** Pre-computed indexed attestations from signature verification to avoid duplicate work */ - indexedAttestations: IndexedAttestation[]; - /** Seen timestamp seconds */ - seenTimestampSec: number; - /** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync */ - executionStatus: BlockExecutionStatus | PayloadExecutionStatus; -}; diff --git a/packages/beacon-node/src/chain/blocks/verifyBlock.ts b/packages/beacon-node/src/chain/blocks/verifyBlock.ts index c420c661cc73..bfe411d792eb 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlock.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlock.ts @@ -1,12 +1,10 @@ import {ExecutionStatus, ProtoBlock} from "@lodestar/fork-choice"; import {ForkName, ForkSeq, isForkPostFulu} from "@lodestar/params"; -import {DataAvailabilityStatus, IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition"; -import {IndexedAttestation, Slot, deneb} from "@lodestar/types"; +import {DataAvailabilityStatus, computeEpochAtSlot} from "@lodestar/state-transition"; +import {Slot, deneb, sszTypesFor} from "@lodestar/types"; import {getBlobKzgCommitments} from "../../util/dataColumns.js"; import type {BeaconChain} from "../chain.js"; -import {BlockError, BlockErrorCode} from "../errors/index.js"; import {BlockProcessOpts} from "../options.js"; -import {RegenCaller} from "../regen/index.js"; import {DAType, IBlockInput} from "./blockInput/index.js"; import {PayloadEnvelopeInput} from "./payloadEnvelopeInput/payloadEnvelopeInput.js"; import {ImportBlockOpts} from "./types.js"; @@ -16,8 +14,6 @@ import {CAPELLA_OWL_BANNER} from "./utils/ownBanner.js"; import {FULU_ZEBRA_BANNER} from "./utils/zebraBanner.js"; import {verifyBlocksDataAvailability} from "./verifyBlocksDataAvailability.js"; import {SegmentExecStatus, verifyBlocksExecutionPayload} from "./verifyBlocksExecutionPayloads.js"; -import {verifyBlocksSignatures} from "./verifyBlocksSignatures.js"; -import {verifyBlocksStateTransitionOnly} from "./verifyBlocksStateTransitionOnly.js"; import {verifyPayloadsDataAvailability} from "./verifyPayloadsDataAvailability.js"; /** @@ -38,12 +34,9 @@ export async function verifyBlocksInEpoch( payloadEnvelopes: Map | null, opts: BlockProcessOpts & ImportBlockOpts ): Promise<{ - postStates: IBeaconStateView[]; - proposerBalanceDeltas: number[]; segmentExecStatus: SegmentExecStatus; blockDAStatuses: DataAvailabilityStatus[]; payloadDAStatuses: Map; - indexedAttestationsByBlock: IndexedAttestation[][]; }> { const blocks = blockInputs.map((blockInput) => blockInput.getBlock()); const lastBlock = blocks.at(-1); @@ -65,33 +58,12 @@ export async function verifyBlocksInEpoch( // All blocks are in the same epoch const fork = this.config.getForkSeq(block0.message.slot); - // TODO: Skip in process chain segment - // Retrieve preState from cache (regen) - const preState0 = await this.regen - // transfer cache to process faster, postState will be in block state cache - .getPreState(block0.message, {dontTransferCache: false}, RegenCaller.processBlocksInEpoch) - .catch((e) => { - throw new BlockError(block0, {code: BlockErrorCode.PRESTATE_MISSING, error: e as Error}); - }); - - // in forky condition, make sure to populate ShufflingCache with regened state - // otherwise it may fail to get indexed attestations from shuffling cache later - this.shufflingCache.processState(preState0); - - if (!preState0.isStateValidatorsNodesPopulated()) { - this.logger.verbose("verifyBlocksInEpoch preState0 SSZ cache stats", { - slot: preState0.slot, - cache: preState0.isStateValidatorsNodesPopulated(), - clonedCount: preState0.clonedCount, - clonedCountWithTransferCache: preState0.clonedCountWithTransferCache, - createdWithTransferCache: preState0.createdWithTransferCache, - }); - } - - // Ensure the state is in the same epoch as block0 - if (block0Epoch !== computeEpochAtSlot(preState0.slot)) { - throw Error(`preState at slot ${preState0.slot} must be dialed to block epoch ${block0Epoch}`); - } + // Per-block SSZ bytes for the engine's bytes-first contract (unused by the JS engine). Gossip + sync + // populate serializedCache; fall back to serializing on a cache miss so all call sites pass real bytes. + const blockBytes = blockInputs.map((blockInput) => { + const block = blockInput.getBlock(); + return this.serializedCache.get(block) ?? sszTypesFor(blockInput.forkName).SignedBeaconBlock.serialize(block); + }); const abortController = new AbortController(); @@ -99,22 +71,12 @@ export async function verifyBlocksInEpoch( // Start execution payload verification first (async request to execution client) const verifyExecutionPayloadsPromise = opts.skipVerifyExecutionPayload !== true - ? verifyBlocksExecutionPayload(this, parentBlock, blockInputs, preState0, abortController.signal, opts) + ? verifyBlocksExecutionPayload(this, parentBlock, blockInputs, abortController.signal, opts) : Promise.resolve({ execAborted: null, executionStatuses: blocks.map((_blk) => ExecutionStatus.Syncing), } as SegmentExecStatus); - // Store indexed attestations for each block to avoid recomputing them during import - const indexedAttestationsByBlock: IndexedAttestation[][] = []; - for (const [i, block] of blocks.entries()) { - indexedAttestationsByBlock[i] = block.message.body.attestations.map((attestation) => { - const attEpoch = computeEpochAtSlot(attestation.data.slot); - const decisionRoot = preState0.getShufflingDecisionRoot(attEpoch); - return this.shufflingCache.getIndexedAttestation(attEpoch, decisionRoot, fork, attestation); - }); - } - // Pick the data-availability source by fork: // - Pre-Gloas: blob/Fulu-column data lives in IBlockInput → verifyBlocksDataAvailability. // - Post-Gloas: verifyPayloadsDataAvailability (payload-level DA, keyed by slot). @@ -163,44 +125,14 @@ export async function verifyBlocksInEpoch( const [ segmentExecStatus, {blockDAStatuses, payloadDAStatuses, availableTime}, - {postStates, proposerBalanceDeltas, verifyStateTime}, - {verifySignaturesTime}, + {verifyStateTime, verifySignaturesTime}, ] = await Promise.all([ verifyExecutionPayloadsPromise, // data availability (fork-specific; see daAvailabilityPromise above) daAvailabilityPromise, - // Run state transition only - // TODO: Ensure it yields to allow flushing to workers and engine API - verifyBlocksStateTransitionOnly( - preState0, - blockInputs, - // hack availability for state transition eval as availability is separately determined - blocks.map(() => DataAvailabilityStatus.Available), - this.logger, - this.metrics, - this.validatorMonitor, - abortController.signal, - opts - ), - - // All signatures at once - opts.skipVerifyBlockSignatures !== true - ? verifyBlocksSignatures( - this.config, - this.bls, - this.logger, - this.metrics, - preState0, - blocks, - indexedAttestationsByBlock, - opts - ) - : Promise.resolve({verifySignaturesTime: Date.now()}), - - // TODO GLOAS: can verify payload signatures in batch too - // maybe chain with the above verifyBlocksSignatures() + this.beaconEngine.verifyBlocks(blockBytes, parentBlock, blockInputs, opts, abortController.signal), ]); if (opts.verifyOnly !== true) { @@ -283,14 +215,7 @@ export async function verifyBlocksInEpoch( ); } - return { - postStates, - blockDAStatuses, - payloadDAStatuses, - proposerBalanceDeltas, - segmentExecStatus, - indexedAttestationsByBlock, - }; + return {segmentExecStatus, blockDAStatuses, payloadDAStatuses}; } finally { abortController.abort(); } diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts index 3f462048debb..0992574c7b69 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts @@ -8,7 +8,7 @@ import { ProtoBlock, } from "@lodestar/fork-choice"; import {ForkSeq} from "@lodestar/params"; -import {IBeaconStateView, isExecutionBlockBodyType, isStatePostBellatrix} from "@lodestar/state-transition"; +import {isExecutionBlockBodyType} from "@lodestar/state-transition"; import {bellatrix, electra} from "@lodestar/types"; import {ErrorAborted, Logger, toRootHex} from "@lodestar/utils"; import {ExecutionPayloadStatus, IExecutionEngine} from "../../execution/engine/interface.js"; @@ -57,7 +57,6 @@ export async function verifyBlocksExecutionPayload( chain: VerifyBlockExecutionPayloadModules, parentBlock: ProtoBlock, blockInputs: IBlockInput[], - preState0: IBeaconStateView, signal: AbortSignal, opts: BlockProcessOpts & ImportBlockOpts ): Promise { @@ -95,7 +94,7 @@ export async function verifyBlocksExecutionPayload( if (signal.aborted) { throw new ErrorAborted("verifyBlockExecutionPayloads"); } - const verifyResponse = await verifyBlockExecutionPayload(chain, blockInput, preState0); + const verifyResponse = await verifyBlockExecutionPayload(chain, blockInput); // If execError has happened, then we need to extract the segmentExecStatus and return if (verifyResponse.execError !== null) { @@ -139,8 +138,7 @@ export async function verifyBlocksExecutionPayload( */ export async function verifyBlockExecutionPayload( chain: VerifyBlockExecutionPayloadModules, - blockInput: IBlockInput, - preState0: IBeaconStateView + blockInput: IBlockInput ): Promise { const block = blockInput.getBlock(); @@ -150,14 +148,18 @@ export async function verifyBlockExecutionPayload( return {executionStatus: ExecutionStatus.Syncing, lvhResponse: undefined, execError: null}; } - /** Not null if execution is enabled */ - const executionPayloadEnabled = - isStatePostBellatrix(preState0) && - preState0.isExecutionStateType && - isExecutionBlockBodyType(block.message.body) && - preState0.isExecutionEnabled(block.message) - ? block.message.body.executionPayload - : null; + // TODO - beacon engine: this caused failed spec tests? + // the BeaconEngine cannot provide a BeaconState + // const executionPayloadEnabled = + // isStatePostBellatrix(preState0) && + // preState0.isExecutionStateType && + // isExecutionBlockBodyType(block.message.body) && + // preState0.isExecutionEnabled(block.message) + // ? block.message.body.executionPayload + // : null; + const executionPayloadEnabled = isExecutionBlockBodyType(block.message.body) + ? block.message.body.executionPayload + : null; if (!executionPayloadEnabled) { // Pre-merge block, no execution payload to verify diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 814e65cc1e60..6d79c92239a6 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -436,6 +436,9 @@ export class BeaconChain implements IBeaconChain { // the engine constructor) because it depends on the engine's own forkChoice. // TODO - beacon engine: remove this once ownership is settled in a later phase. this.beaconEngine.seenPayloadEnvelopeInputCache = this.seenPayloadEnvelopeInputCache; + // Inject lightClientServer post-construction (created after engine, depends on nothing from engine) + // TODO - beacon engine + this.beaconEngine.lightClientServer = this.lightClientServer; const anchorBlockSlot = anchorState.latestBlockHeader.slot; if (isStatePostGloas(anchorState) && anchorBlockSlot > 0) { @@ -548,6 +551,7 @@ export class BeaconChain implements IBeaconChain { // Caller must check that epoch is not older that current epoch - 1 // else the caches for that epoch may already be pruned. + // TODO - beacon engine: consider move all of these caches to BeaconEngine return ( // Dedicated cache for liveness checks, registers attesters seen through blocks. // Note: this check should be cheaper + overlap with counting participants of aggregates from gossip. From 2a5e82ec6f9178fe1ab533425e62120d045a7867 Mon Sep 17 00:00:00 2001 From: twoeths Date: Tue, 23 Jun 2026 19:17:42 +0700 Subject: [PATCH 08/24] feat: implement and consume BeaconEngine.produceBlockBase() --- .../src/api/impl/beacon/blocks/index.ts | 22 +- .../src/api/impl/validator/index.ts | 99 ++-- .../src/chain/beaconEngine/beaconEngine.ts | 318 ++++++++++++- .../src/chain/beaconEngine/interface.ts | 64 ++- packages/beacon-node/src/chain/chain.ts | 104 ++--- .../beacon-node/src/chain/prepareNextSlot.ts | 24 +- .../chain/produceBlock/produceBlockBody.ts | 428 ++++++++---------- packages/beacon-node/src/chain/regen/regen.ts | 1 + .../validation/executionPayloadEnvelope.ts | 70 ++- .../src/network/processor/gossipHandlers.ts | 25 +- packages/beacon-node/src/sync/unknownBlock.ts | 15 +- .../test/mocks/mockedBeaconChain.ts | 1 + .../produceBlock/produceBlockBody.test.ts | 27 +- .../api/impl/validator/produceBlockV3.test.ts | 79 ++-- 14 files changed, 828 insertions(+), 449 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 795ecbd2e7e9..196bb31eb1cd 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -680,8 +680,18 @@ export function getBeaconBlockApi({ throw new ApiError(400, `Envelope slot ${slot} does not match block slot ${block.slot}`); } - const envelopeValidation = - await chain.beaconEngine.validateApiExecutionPayloadEnvelope(signedExecutionPayloadEnvelope); + // Facade owns the DA cache; look up the bid scalars and pass them to the engine. + const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); + if (!payloadInput) { + throw new ApiError(404, `Execution payload envelope input not found for beacon block root ${blockRootHex}`); + } + const envelopeValidation = await chain.beaconEngine.validateApiExecutionPayloadEnvelope( + signedExecutionPayloadEnvelope, + payloadInput.proposerIndex, + payloadInput.getBuilderIndex(), + payloadInput.getBlockHashHex(), + payloadInput.getBid().executionRequestsRoot + ); if (envelopeValidation.status !== GossipValidationStatus.Accept) { throw ( envelopeValidation.error ?? @@ -728,14 +738,6 @@ export function getBeaconBlockApi({ await sleep(msToBlockSlot); } - const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); - if (!payloadInput) { - // The block is awaited above (queuing if the envelope arrived first), and both the API and - // gossip import paths seed the PayloadEnvelopeInput before importing the block, so the input - // should exist here. - throw new ApiError(404, `PayloadEnvelopeInput not found for block root ${blockRootHex}`); - } - payloadInput.addPayloadEnvelope({ envelope: signedExecutionPayloadEnvelope, source: PayloadEnvelopeInputSource.api, diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 105befd106c2..0bb8cef3ee3a 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -51,7 +51,6 @@ import { } from "@lodestar/types"; import { TimeoutError, - defer, formatWeiToEth, fromHex, prettyWeiToEth, @@ -65,7 +64,12 @@ import {BlockInputSource} from "../../../chain/blocks/blockInput/types.js"; import {AttestationErrorCode, SyncCommitteeErrorCode} from "../../../chain/errors/index.js"; import {ChainEvent, CommonBlockBody} from "../../../chain/index.js"; import {PREPARE_NEXT_SLOT_BPS} from "../../../chain/prepareNextSlot.js"; -import {BlockType, ProduceFullDeneb, ProduceFullGloas} from "../../../chain/produceBlock/index.js"; +import { + BlockType, + type PreparedBlockScalars, + ProduceFullDeneb, + ProduceFullGloas, +} from "../../../chain/produceBlock/index.js"; import {RegenCaller} from "../../../chain/regen/index.js"; import {CheckpointHex} from "../../../chain/stateCache/types.js"; import {ZERO_HASH} from "../../../constants/index.js"; @@ -73,7 +77,6 @@ import {BuilderStatus, NoBidReceived} from "../../../execution/builder/http.js"; import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js"; import {CommitteeSubscription} from "../../../network/subnets/index.js"; import {SyncState} from "../../../sync/index.js"; -import {callInNextEventLoop} from "../../../util/eventLoop.js"; import {isOptimisticBlock} from "../../../util/forkChoice.js"; import {getDefaultGraffiti, toGraffitiBytes} from "../../../util/graffiti.js"; import {getLodestarClientVersion} from "../../../util/metadata.js"; @@ -411,9 +414,11 @@ export function getValidatorApi( { commonBlockBodyPromise, parentBlock, + prepared, }: Omit & { commonBlockBodyPromise: Promise; parentBlock: ProtoBlock; + prepared: PreparedBlockScalars; } ): Promise { const version = config.getForkName(slot); @@ -448,6 +453,7 @@ export function getValidatorApi( randaoReveal, graffiti, commonBlockBodyPromise, + ...prepared, }); metrics?.blockProductionSuccess.inc({source}); @@ -480,9 +486,11 @@ export function getValidatorApi( strictFeeRecipientCheck, commonBlockBodyPromise, parentBlock, + prepared, }: Omit & { commonBlockBodyPromise: Promise; parentBlock: ProtoBlock; + prepared: PreparedBlockScalars; } ): Promise { const source = ProducedBlockSource.engine; @@ -498,6 +506,7 @@ export function getValidatorApi( graffiti, feeRecipient, commonBlockBodyPromise, + ...prepared, }); const version = config.getForkName(block.slot); if (strictFeeRecipientCheck && feeRecipient && isForkPostBellatrix(version)) { @@ -566,12 +575,6 @@ export function getValidatorApi( notWhileSyncing(); await waitForSlot(slot); // Must never request for a future slot > currentSlot - const parentBlock = chain.getProposerHead(slot); - const {blockRoot: parentBlockRootHex, slot: parentSlot} = parentBlock; - const parentBlockRoot = fromHex(parentBlockRootHex); - notOnOutOfRangeData(parentBlockRoot); - metrics?.blockProductionSlotDelta.set(slot - parentSlot); - const fork = config.getForkName(slot); // set some sensible opts // builderSelection will be deprecated and will run in mode MaxProfit if builder is enabled @@ -582,6 +585,22 @@ export function getValidatorApi( throw new ApiError(400, `Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR`); } + const graffitiBytes = toGraffitiBytes( + graffiti ?? getDefaultGraffiti(getLodestarClientVersion(opts), chain.executionEngine.clientVersion, opts) + ); + // Engine computes the shared head once (proposer head, forkChoice exec hashes, base-state scalars) and + // kicks off the deferred common-body packing. builderBid is gloas-only — discarded on the V3 path. + const { + parentBlock, + builderBid: _builderBid, + commonBlockBodyPromise, + ...prepared + } = await chain.beaconEngine.produceBlockBase({slot, randaoReveal, graffiti: graffitiBytes}); + const {blockRoot: parentBlockRootHex, slot: parentSlot} = parentBlock; + const parentBlockRoot = fromHex(parentBlockRootHex); + notOnOutOfRangeData(parentBlockRoot); + metrics?.blockProductionSlotDelta.set(slot - parentSlot); + const isBuilderEnabled = ForkSeq[fork] >= ForkSeq.bellatrix && chain.executionBuilder !== undefined && @@ -603,10 +622,6 @@ export function getValidatorApi( ); } - const graffitiBytes = toGraffitiBytes( - graffiti ?? getDefaultGraffiti(getLodestarClientVersion(opts), chain.executionEngine.clientVersion, opts) - ); - const loggerContext = { slot, parentSlot, @@ -622,10 +637,6 @@ export function getValidatorApi( logger.verbose("Assembling block with produceEngineOrBuilderBlock", loggerContext); - // Defer common block body production to make sure we sent async builder and engine requests before - const deferredCommonBlockBody = defer(); - const commonBlockBodyPromise = deferredCommonBlockBody.promise; - // use abort controller to stop waiting for both block sources const controller = new AbortController(); @@ -637,6 +648,7 @@ export function getValidatorApi( strictFeeRecipientCheck: false, commonBlockBodyPromise, parentBlock, + prepared, }) : Promise.reject(new Error("Builder disabled")); @@ -646,6 +658,7 @@ export function getValidatorApi( strictFeeRecipientCheck, commonBlockBodyPromise, parentBlock, + prepared, }).then((engineBlock) => { // Once the engine returns a block, in the event of either: // - suspected builder censorship @@ -677,30 +690,6 @@ export function getValidatorApi( signal: controller.signal, }); - // Ensure builder and engine HTTP requests are sent before starting common block body production - // by deferring the call to next event loop iteration, allowing pending I/O operations like - // HTTP requests to be processed first and sent out early in slot. - callInNextEventLoop(() => { - logger.verbose("Producing common block body", loggerContext); - const commonBlockBodyStartedAt = Date.now(); - - chain - .produceCommonBlockBody({ - slot, - parentBlock, - randaoReveal, - graffiti: graffitiBytes, - }) - .then((commonBlockBody) => { - deferredCommonBlockBody.resolve(commonBlockBody); - logger.verbose("Produced common block body", { - ...loggerContext, - durationMs: Date.now() - commonBlockBodyStartedAt, - }); - }) - .catch(deferredCommonBlockBody.reject); - }); - const [builder, engine] = await blockProductionRacePromise; if (builder.status === "pending" && engine.status === "pending") { @@ -907,12 +896,6 @@ export function getValidatorApi( notWhileSyncing(); await waitForSlot(slot); - const parentBlock = chain.getProposerHead(slot); - const {blockRoot: parentBlockRootHex, slot: parentSlot} = parentBlock; - const parentBlockRoot = fromHex(parentBlockRootHex); - notOnOutOfRangeData(parentBlockRoot); - metrics?.blockProductionSlotDelta.set(slot - parentSlot); - const graffitiBytes = toGraffitiBytes( graffiti ?? getDefaultGraffiti(getLodestarClientVersion(opts), chain.executionEngine.clientVersion, opts) ); @@ -920,9 +903,17 @@ export function getValidatorApi( // TODO GLOAS: respect builderSelection (MaxProfit, BuilderAlways, ExecutionAlways, etc.) to let // the user control bid source preferences and value comparison. Also add external builder api // support when it is implemented. - const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot); - const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash; - const builderBid = chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex); + // The engine computes the shared head once (proposer head, shouldBuildOnFull, best builder bid, + // forkChoice exec hashes, base-state scalars) and kicks off common-body packing. + const {parentBlock, builderBid, commonBlockBodyPromise, ...prepared} = await chain.beaconEngine.produceBlockBase({ + slot, + randaoReveal, + graffiti: graffitiBytes, + }); + const {blockRoot: parentBlockRootHex, slot: parentSlot} = parentBlock; + const parentBlockRoot = fromHex(parentBlockRootHex); + notOnOutOfRangeData(parentBlockRoot); + metrics?.blockProductionSlotDelta.set(slot - parentSlot); const logCtx = { slot, @@ -939,13 +930,6 @@ export function getValidatorApi( : {}), }; - const commonBlockBodyPromise = chain.produceCommonBlockBody({ - slot, - parentBlock, - randaoReveal, - graffiti: graffitiBytes, - }); - const baseAttrs = { slot, parentBlock, @@ -953,6 +937,7 @@ export function getValidatorApi( graffiti: graffitiBytes, feeRecipient, commonBlockBodyPromise, + ...prepared, }; metrics?.blockProductionRequests.inc({source: ProducedBlockSource.engine}); diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 1761df130c39..62eebbb663c5 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -14,6 +14,7 @@ import { PayloadExecutionStatus, ProtoBlock, UpdateHeadOpt, + getSafeExecutionBlockHash, } from "@lodestar/fork-choice"; import { ForkName, @@ -23,6 +24,7 @@ import { GENESIS_SLOT, MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH, + isForkPostGloas, } from "@lodestar/params"; import { DataAvailabilityStatus, @@ -36,16 +38,21 @@ import { computeTimeAtSlot, isStatePostAltair, isStatePostBellatrix, + isStatePostCapella, + isStatePostGloas, } from "@lodestar/state-transition"; import { Attestation, + BLSSignature, BeaconBlock, + Bytes32, Epoch, IndexedAttestation, Root, RootHex, SignedAggregateAndProof, SignedBeaconBlock, + Slot, SubnetID, ValidatorIndex, altair, @@ -57,7 +64,9 @@ import { phase0, ssz, } from "@lodestar/types"; -import {Logger, toRootHex} from "@lodestar/utils"; +import {Logger, fromHex, toRootHex} from "@lodestar/utils"; +import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js"; +import {IBeaconDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; import {IClock} from "../../util/clock.js"; import {callInNextEventLoop} from "../../util/eventLoop.js"; @@ -74,6 +83,7 @@ import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from ". import {ChainEvent, ChainEventEmitter, ReorgEventData} from "../emitter.js"; import {BlockError, BlockErrorCode} from "../errors/index.js"; import {ForkchoiceCaller, initializeForkChoice} from "../forkChoice/index.js"; +import {CommonBlockBody, FindHeadFnName} from "../interface.js"; import {LightClientServer} from "../lightClient/index.js"; import { AggregatedAttestationPool, @@ -86,6 +96,12 @@ import { SyncContributionAndProofPool, } from "../opPools/index.js"; import {BlockProcessOpts} from "../options.js"; +import { + BlockAttributes, + BlockProductionStep, + BlockType, + PayloadAttributesWithdrawals, +} from "../produceBlock/produceBlockBody.js"; import {QueuedStateRegenerator, RegenCaller} from "../regen/index.js"; import { SeenAggregators, @@ -95,7 +111,6 @@ import { SeenContributionAndProof, SeenExecutionPayloadBids, SeenPayloadAttesters, - SeenPayloadEnvelopeInput, SeenProposerPreferences, SeenSyncCommitteeMessages, } from "../seenCache/index.js"; @@ -138,7 +153,7 @@ import {validateApiSyncCommittee, validateGossipSyncCommittee} from "../validati import {validateSyncCommitteeGossipContributionAndProof} from "../validation/syncCommitteeContributionAndProof.js"; import {ValidatorMonitor} from "../validatorMonitor.js"; import {GossipValidationResult, fromResult, runGossipValidation} from "./gossipValidationResult.js"; -import {BeaconEngineModules, IBeaconEngine, ImportBlockResult} from "./interface.js"; +import {BeaconEngineModules, IBeaconEngine, ImportBlockResult, ProduceBlockBaseResult} from "./interface.js"; import {IBeaconEngineOptions} from "./options.js"; type VerifiedBlockBundle = { @@ -195,12 +210,12 @@ export class BeaconEngine implements IBeaconEngine { readonly seenExecutionPayloadBids = new SeenExecutionPayloadBids(); readonly seenProposerPreferences = new SeenProposerPreferences(); - // Facade-owned (DA assembly), shared with the engine. `seenBlockInputCache` is injected via the - // constructor; `seenPayloadEnvelopeInputCache` is assigned by `BeaconChain` post-construction because - // it depends on the engine's own `forkChoice`. TODO - beacon engine: revisit ownership in a later phase. + // TODO - beacon engine: how to remove this? readonly seenBlockInputCache: SeenBlockInput; - seenPayloadEnvelopeInputCache!: SeenPayloadEnvelopeInput; readonly validatorMonitor: ValidatorMonitor | null; + // Engine reads execution payload envelopes for block production (getParentExecutionRequests). Writes + // / archival / network handlers still use the shared `db` directly until the DB-ownership phase. + readonly db: IBeaconDb; lightClientServer: LightClientServer | undefined; private readonly verifiedBlocks = new Map(); private readonly emitter: ChainEventEmitter; @@ -225,6 +240,7 @@ export class BeaconEngine implements IBeaconEngine { this.config = config; this.opts = opts; this.logger = logger; + this.db = db; this.metrics = metrics; this.clock = clock; this.pubkeyCache = pubkeyCache; @@ -361,6 +377,260 @@ export class BeaconEngine implements IBeaconEngine { return state.getShufflingAtEpoch(attEpoch); } + getProposerHead(slot: Slot): ProtoBlock { + this.metrics?.forkChoice.requests.inc(); + const timer = this.metrics?.forkChoice.findHead.startTimer({caller: FindHeadFnName.getProposerHead}); + const secFromSlot = this.clock.secFromSlot(slot); + + try { + const {head, isHeadTimely, notReorgedReason} = this.forkChoice.updateAndGetHead({ + mode: UpdateHeadOpt.GetProposerHead, + secFromSlot, + slot, + }); + + if (isHeadTimely && notReorgedReason !== undefined) { + this.metrics?.forkChoice.notReorgedReason.inc({reason: notReorgedReason}); + } + return head; + } catch (e) { + this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetProposerHead}); + throw e; + } finally { + timer?.(); + } + } + + /** DB read of an execution payload envelope. Callers that hold the DA cache check it first. */ + async getExecutionPayloadEnvelope( + blockSlot: Slot, + blockRootHex: string + ): Promise { + return ( + (await this.db.executionPayloadEnvelope.get(fromHex(blockRootHex))) ?? + (await this.db.executionPayloadEnvelopeArchive.get(blockSlot)) ?? + null + ); + } + + /** + * Parent execution requests for block production. DB-only: the facade-owned DA cache is not visible + * here, so callers that need cache-only (DB-write-pending) envelopes check the cache first + * (`chain.getParentExecutionRequests`). At `produceBlockBase` time the parent is FULL and its + * envelope's async DB write has normally completed. + */ + async getParentExecutionRequests( + parentBlockSlot: Slot, + parentBlockRootHex: RootHex + ): Promise { + // at the fork boundary, parent is pre-gloas + if (!isForkPostGloas(this.config.getForkName(parentBlockSlot))) { + return ssz.gloas.ExecutionRequests.defaultValue(); + } + const envelope = await this.getExecutionPayloadEnvelope(parentBlockSlot, parentBlockRootHex); + if (envelope === null) { + throw Error(`Parent execution payload envelope not found slot=${parentBlockSlot}, root=${parentBlockRootHex}`); + } + return envelope.message.executionRequests; + } + + /** + * Compute the shared head of block production once: proposer head, builder-bid lookup, forkChoice + * exec hashes, and the base-state scalars the downstream flow needs. No `BeaconState` crosses — the + * fetched state is consumed here and the (cheap, cache-hit) re-regen happens per path downstream. + */ + async produceBlockBase({ + slot, + randaoReveal, + graffiti, + }: { + slot: Slot; + randaoReveal: BLSSignature; + graffiti: Bytes32; + }): Promise { + const parentBlock = this.getProposerHead(slot); + const fork = this.config.getForkName(slot); + + const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); + const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; + + // gloas: decide build-on-full, look up the best builder bid, and collect the parent block's payload + // attestations (slot - 1) — all from engine-owned pools. Same args across both production paths. + let isBuildingOnFull = false; + let builderBid: gloas.SignedExecutionPayloadBid | null = null; + let payloadAttestations: gloas.PayloadAttestation[] = []; + if (isForkPostGloas(fork)) { + isBuildingOnFull = this.forkChoice.shouldBuildOnFull(parentBlock, slot); + const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash; + builderBid = this.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlock.blockRoot); + payloadAttestations = this.payloadAttestationPool.getPayloadAttestationsForBlock(parentBlock.blockRoot, slot - 1); + } + + const state = await this.regen.getBlockSlotState( + parentBlock, + slot, + {dontTransferCache: true}, + RegenCaller.produceBlock + ); + const proposerIndex = state.getBeaconProposer(slot); + const proposerPubKey = this.pubkeyCache.getOrThrow(proposerIndex).toBytes(); + const prevRandao = state.getRandaoMix(state.epoch); + + let parentBlockHash: Bytes32; + // parent execution gas limit: gloas keeps it on the bid (UintBn64 → cast), pre-gloas on the header + let parentGasLimit: number; + if (isStatePostGloas(state)) { + parentBlockHash = isBuildingOnFull + ? state.latestExecutionPayloadBid.blockHash + : state.latestExecutionPayloadBid.parentBlockHash; + parentGasLimit = Number(state.latestExecutionPayloadBid.gasLimit); + } else if (isStatePostBellatrix(state)) { + parentBlockHash = state.latestExecutionPayloadHeader.blockHash; + parentGasLimit = state.latestExecutionPayloadHeader.gasLimit; + } else { + parentBlockHash = ZERO_HASH; // pre-bellatrix: unused + parentGasLimit = 0; + } + + // Apply the parent execution payload ONCE here (gloas, building-on-full) so the common body and the + // payload-attribute withdrawals are produced from the exact state the block transitions through. + // `getParentExecutionRequests` is only called when `isBuildingOnFull` (parent is FULL in forkChoice → + // its envelope is available); on build-on-empty the base state is used. Both production paths + // (self-build / builder-bid) agree because the builder-bid lookup is gated by `isBuildingOnFull`. + let parentExecutionRequests = ssz.gloas.ExecutionRequests.defaultValue(); + let stateForProduction: IBeaconStateView = state; + if (isBuildingOnFull && isStatePostGloas(state)) { + parentExecutionRequests = await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot); + stateForProduction = state.withParentPayloadApplied(parentExecutionRequests); + } + + // Payload attributes, resolved once from the production state. `prevRandao` (above) is unaffected by + // the parent payload, so it is reused for both the EL request and the gloas self-bid. gloas + // withdrawals: building-on-full → applied-state `getExpectedWithdrawals`; build-on-empty → + // `payloadExpectedWithdrawals` (a batch already deducted from CL balances but never delivered on the + // EL, which the next payload must carry to keep CL/EL consistent). + const timestamp = computeTimeAtSlot(this.config, state.slot, state.genesisTime); + let withdrawals: PayloadAttributesWithdrawals | undefined; + if (isStatePostGloas(stateForProduction)) { + withdrawals = isBuildingOnFull + ? stateForProduction.getExpectedWithdrawals().expectedWithdrawals + : stateForProduction.payloadExpectedWithdrawals; + } else if (isStatePostCapella(stateForProduction)) { + withdrawals = stateForProduction.getExpectedWithdrawals().expectedWithdrawals; + } + + // Build the common body from `stateForProduction` (its voluntaryExits / blsToExecutionChanges are + // already valid against the applied state — no per-path re-filter downstream). Deferred to the next + // event loop so the downstream EL request goes out first. + const commonBlockBodyPromise = new Promise((resolve, reject) => { + callInNextEventLoop(() => { + try { + resolve( + this.assembleCommonBlockBody(BlockType.Full, stateForProduction, { + slot, + parentBlock, + randaoReveal, + graffiti, + }) + ); + } catch (e) { + reject(e as Error); + } + }); + }); + + return { + parentBlock, + proposerIndex, + proposerPubKey, + safeBlockHash, + finalizedBlockHash, + timestamp, + prevRandao, + parentBlockHash, + parentGasLimit, + isBuildingOnFull, + builderBid, + parentExecutionRequests, + payloadAttestations, + withdrawals, + commonBlockBodyPromise, + }; + } + + /** + * Produce the fork-agnostic part of a block body (attestations, slashings, exits, sync aggregate). + * Fetches the block-slot state internally; the result is reused across the self-build and builder-bid + * production paths. + */ + async produceCommonBlockBody(blockAttributes: BlockAttributes): Promise { + const {slot, parentBlock} = blockAttributes; + const state = await this.regen.getBlockSlotState( + parentBlock, + slot, + {dontTransferCache: true}, + RegenCaller.produceBlock + ); + return this.assembleCommonBlockBody(BlockType.Full, state, blockAttributes); + } + + private assembleCommonBlockBody( + blockType: BlockType, + currentState: IBeaconStateView, + {randaoReveal, graffiti, slot, parentBlock}: BlockAttributes + ): CommonBlockBody { + const stepsMetrics = + blockType === BlockType.Full + ? this.metrics?.executionBlockProductionTimeSteps + : this.metrics?.builderBlockProductionTimeSteps; + + const fork = this.config.getForkName(slot); + + const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] = + this.opPool.getSlashingsAndExits(currentState, blockType, this.metrics); + + const endAttestations = stepsMetrics?.startTimer(); + const attestations = this.aggregatedAttestationPool.getAttestationsForBlock( + fork, + this.forkChoice, + this.shufflingCache, + currentState + ); + endAttestations?.({step: BlockProductionStep.attestations}); + + const blockBody: Omit = { + randaoReveal, + graffiti, + // Eth1 data voting is no longer required since electra + eth1Data: currentState.eth1Data, + proposerSlashings, + attesterSlashings, + attestations, + // Since electra, deposits are processed by the execution layer, + // we no longer support handling deposits from earlier forks. + deposits: [], + voluntaryExits, + }; + + if (ForkSeq[fork] >= ForkSeq.capella) { + (blockBody as CommonBlockBody).blsToExecutionChanges = blsToExecutionChanges; + } + + const endSyncAggregate = stepsMetrics?.startTimer(); + if (ForkSeq[fork] >= ForkSeq.altair) { + const parentBlockRoot = fromHex(parentBlock.blockRoot); + const previousSlot = slot - 1; + const syncAggregate = this.syncContributionAndProofPool.getAggregate(previousSlot, parentBlockRoot); + this.metrics?.production.producedSyncAggregateParticipants.observe( + syncAggregate.syncCommitteeBits.getTrueBitIndexes().length + ); + (blockBody as CommonBlockBody).syncAggregate = syncAggregate; + } + endSyncAggregate?.({step: BlockProductionStep.syncAggregate}); + + return blockBody as CommonBlockBody; + } + // Gossip validation flows. Each method takes the message's SSZ bytes first (unused by this JS impl; // present for the native engine's bytes-first contract) and delegates to the validation logic in // `../validation/*` rebound onto the engine. @@ -477,15 +747,41 @@ export class BeaconEngine implements IBeaconEngine { validateGossipExecutionPayloadEnvelope( _envelopeBytes: Uint8Array, - executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope, + proposerIndex: ValidatorIndex, + bidBuilderIndex: ValidatorIndex, + bidBlockHashHex: RootHex, + bidExecutionRequestsRoot: Root ): Promise> { - return runGossipValidation(() => validateGossipExecutionPayloadEnvelope(this, executionPayloadEnvelope)); + return runGossipValidation(() => + validateGossipExecutionPayloadEnvelope( + this, + executionPayloadEnvelope, + proposerIndex, + bidBuilderIndex, + bidBlockHashHex, + bidExecutionRequestsRoot + ) + ); } validateApiExecutionPayloadEnvelope( - executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope, + proposerIndex: ValidatorIndex, + bidBuilderIndex: ValidatorIndex, + bidBlockHashHex: RootHex, + bidExecutionRequestsRoot: Root ): Promise> { - return runGossipValidation(() => validateApiExecutionPayloadEnvelope(this, executionPayloadEnvelope)); + return runGossipValidation(() => + validateApiExecutionPayloadEnvelope( + this, + executionPayloadEnvelope, + proposerIndex, + bidBuilderIndex, + bidBlockHashHex, + bidExecutionRequestsRoot + ) + ); } validateGossipExecutionPayloadBid( diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 24b583c8b0c0..5128e57600f0 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -3,10 +3,14 @@ import {BlockExecutionStatus, IForkChoice, PayloadExecutionStatus, ProtoBlock} f import {ForkName} from "@lodestar/params"; import {DataAvailabilityStatus, IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; import { + BLSPubkey, + BLSSignature, + Bytes32, Root, RootHex, SignedAggregateAndProof, SignedBeaconBlock, + Slot, SubnetID, ValidatorIndex, altair, @@ -23,8 +27,10 @@ import {IBlockInput} from "../blocks/blockInput/index.js"; import {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; import {ImportBlockOpts} from "../blocks/types.js"; import {ChainEventEmitter, ReorgEventData} from "../emitter.js"; +import {CommonBlockBody} from "../interface.js"; import {LightClientServer} from "../lightClient/index.js"; import {BlockProcessOpts} from "../options.js"; +import {BlockAttributes, PayloadAttributesWithdrawals} from "../produceBlock/produceBlockBody.js"; import {SeenBlockInput} from "../seenCache/seenGossipBlockInput.js"; import {CPStateDatastore} from "../stateCache/datastore/types.js"; import {AggregateAndProofValidationResult} from "../validation/aggregateAndProof.js"; @@ -83,6 +89,38 @@ export type ImportBlockResult = { }; }; +/** + * Shared head of block production, computed once by `produceBlockBase` and reused across the + * self-build and builder-bid paths. Holds no `BeaconState` — only scalar fields the downstream flow + * needs plus the (still-pending) common block body. + */ +export type ProduceBlockBaseResult = { + parentBlock: ProtoBlock; + proposerIndex: ValidatorIndex; + proposerPubKey: BLSPubkey; + safeBlockHash: RootHex; + finalizedBlockHash: RootHex; + /** EL payload-attribute timestamp (computeTimeAtSlot) */ + timestamp: number; + prevRandao: Bytes32; + /** self-build parent execution block hash (gloas: from the bid; bellatrix: from the header) */ + parentBlockHash: Bytes32; + /** parent execution gas limit (gloas: from the bid; bellatrix: from the header) */ + parentGasLimit: number; + /** gloas: forkChoice.shouldBuildOnFull(parentBlock, slot); false pre-gloas */ + isBuildingOnFull: boolean; + /** gloas: best bid from the (engine-owned) executionPayloadBidPool; null otherwise */ + builderBid: gloas.SignedExecutionPayloadBid | null; + /** gloas: parent execution requests applied to produce the common body (default = empty / build-on-empty) */ + parentExecutionRequests: gloas.ExecutionRequests; + /** gloas: payload attestations for the parent block's payload (slot - 1); empty pre-gloas */ + payloadAttestations: gloas.PayloadAttestation[]; + /** payload-attribute withdrawals resolved from the parent-payload-applied state (post-capella) */ + withdrawals?: PayloadAttributesWithdrawals; + /** common body produced from the parent-payload-applied state — its voluntaryExits are already valid */ + commonBlockBodyPromise: Promise; +}; + /** * The consensus engine seam. Starts minimal and transitional (JS-only); ownership of consensus * collaborators and flows migrates here across later phases. This interface is the contract shared @@ -95,6 +133,18 @@ export interface IBeaconEngine { // (gossip → Phase 3, migrateFinalized/prune → Phase 5) move into the engine. readonly forkChoice: IForkChoice; + // Block production. `produceCommonBlockBody` builds the fork-agnostic body part (reused across the + // self-build / builder-bid paths). `produceBlockBase` computes the shared head once (proposer head, + // builder-bid lookup, forkChoice exec hashes, base-state scalars) so they are not recomputed per path. + // More of the production flow migrates here in later steps. + getProposerHead(slot: Slot): ProtoBlock; + getExecutionPayloadEnvelope( + blockSlot: Slot, + blockRootHex: string + ): Promise; + produceCommonBlockBody(blockAttributes: BlockAttributes): Promise; + produceBlockBase(attrs: {slot: Slot; randaoReveal: BLSSignature; graffiti: Bytes32}): Promise; + // Gossip validation flows. The first parameter is the message's SSZ bytes (unused by the JS engine, // required by the native engine's bytes-first contract), followed by the deserialized object. Each // returns a `GossipValidationResult` (no throw) so the native engine can return outcomes across FFI. @@ -158,12 +208,22 @@ export interface IBeaconEngine { fork: ForkName, signedAggregateAndProof: SignedAggregateAndProof ): Promise>; + // The bid scalars + proposerIndex are looked up facade-side from the `PayloadEnvelopeInput` (the engine + // no longer touches the DA seen cache) and passed in. validateGossipExecutionPayloadEnvelope( envelopeBytes: Uint8Array, - executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope, + proposerIndex: ValidatorIndex, + bidBuilderIndex: ValidatorIndex, + bidBlockHashHex: RootHex, + bidExecutionRequestsRoot: Root ): Promise>; validateApiExecutionPayloadEnvelope( - executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope, + proposerIndex: ValidatorIndex, + bidBuilderIndex: ValidatorIndex, + bidBlockHashHex: RootHex, + bidExecutionRequestsRoot: Root ): Promise>; validateGossipExecutionPayloadBid( bidBytes: Uint8Array, diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 6d79c92239a6..632d56012b10 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -94,7 +94,7 @@ import {IChainOptions} from "./options.js"; import {PrepareNextSlotScheduler} from "./prepareNextSlot.js"; import {computeNewStateRoot} from "./produceBlock/computeNewStateRoot.js"; import {AssembledBlockType, BlockType, ProduceResult} from "./produceBlock/index.js"; -import {BlockAttributes, produceBlockBody, produceCommonBlockBody} from "./produceBlock/produceBlockBody.js"; +import {BlockAttributes, PreparedBlockScalars, produceBlockBody} from "./produceBlock/produceBlockBody.js"; import {QueuedStateRegenerator, RegenCaller} from "./regen/index.js"; import {ReprocessController} from "./reprocess.js"; import { @@ -432,10 +432,6 @@ export class BeaconChain implements IBeaconChain { metrics, logger, }); - // Facade-owned (DA assembly) but shared with the engine for gossip validation. Injected here (not via - // the engine constructor) because it depends on the engine's own forkChoice. - // TODO - beacon engine: remove this once ownership is settled in a later phase. - this.beaconEngine.seenPayloadEnvelopeInputCache = this.seenPayloadEnvelopeInputCache; // Inject lightClientServer post-construction (created after engine, depends on nothing from engine) // TODO - beacon engine this.beaconEngine.lightClientServer = this.lightClientServer; @@ -911,16 +907,12 @@ export class BeaconChain implements IBeaconChain { blockSlot: Slot, blockRootHex: string ): Promise { + // Facade owns the DA cache; check it before the engine's DB read. const payloadInput = this.seenPayloadEnvelopeInputCache.get(blockRootHex); if (payloadInput?.hasPayloadEnvelope()) { return payloadInput.getPayloadEnvelope(); } - - return ( - (await this.db.executionPayloadEnvelope.get(fromHex(blockRootHex))) ?? - (await this.db.executionPayloadEnvelopeArchive.get(blockSlot)) ?? - null - ); + return this.beaconEngine.getExecutionPayloadEnvelope(blockSlot, blockRootHex); } async getParentExecutionRequests( @@ -1019,21 +1011,14 @@ export class BeaconChain implements IBeaconChain { } async produceCommonBlockBody(blockAttributes: BlockAttributes): Promise { - const {slot, parentBlock} = blockAttributes; - const state = await this.regen.getBlockSlotState( - parentBlock, - slot, - {dontTransferCache: true}, - RegenCaller.produceBlock - ); - - // TODO: To avoid breaking changes for metric define this attribute - const blockType = BlockType.Full; - - return produceCommonBlockBody.call(this, blockType, state, blockAttributes); + return this.beaconEngine.produceCommonBlockBody(blockAttributes); } - produceBlock(blockAttributes: BlockAttributes & {commonBlockBodyPromise: Promise}): Promise<{ + produceBlock( + blockAttributes: BlockAttributes & { + commonBlockBodyPromise: Promise; + } & PreparedBlockScalars + ): Promise<{ block: BeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Wei; @@ -1042,7 +1027,11 @@ export class BeaconChain implements IBeaconChain { return this.produceBlockWrapper(BlockType.Full, blockAttributes); } - produceBlindedBlock(blockAttributes: BlockAttributes & {commonBlockBodyPromise: Promise}): Promise<{ + produceBlindedBlock( + blockAttributes: BlockAttributes & { + commonBlockBodyPromise: Promise; + } & PreparedBlockScalars + ): Promise<{ block: BlindedBeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Wei; @@ -1060,26 +1049,30 @@ export class BeaconChain implements IBeaconChain { commonBlockBodyPromise, parentBlock, builderBid, - }: BlockAttributes & {commonBlockBodyPromise: Promise} + // All scalars are precomputed once by produceBlockBase (both V3 and V4) and threaded through, so + // produceBlockBody no longer needs a BeaconState here. + proposerIndex, + proposerPubKey, + safeBlockHash, + finalizedBlockHash, + timestamp, + prevRandao, + parentBlockHash, + parentGasLimit, + isBuildingOnFull, + parentExecutionRequests, + payloadAttestations, + withdrawals, + }: BlockAttributes & {commonBlockBodyPromise: Promise} & PreparedBlockScalars ): Promise<{ block: AssembledBlockType; executionPayloadValue: Wei; consensusBlockValue: Wei; shouldOverrideBuilder?: boolean; }> { - const state = await this.regen.getBlockSlotState( - parentBlock, - slot, - {dontTransferCache: true}, - RegenCaller.produceBlock - ); - const proposerIndex = state.getBeaconProposer(slot); - const proposerPubKey = this.pubkeyCache.getOrThrow(proposerIndex).toBytes(); - const {body, produceResult, executionPayloadValue, shouldOverrideBuilder} = await produceBlockBody.call( this, blockType, - state, { randaoReveal, graffiti, @@ -1087,6 +1080,16 @@ export class BeaconChain implements IBeaconChain { feeRecipient, parentBlock, proposerIndex, + safeBlockHash, + finalizedBlockHash, + timestamp, + prevRandao, + parentBlockHash, + parentGasLimit, + isBuildingOnFull, + parentExecutionRequests, + payloadAttestations, + withdrawals, proposerPubKey, commonBlockBodyPromise, builderBid, @@ -1114,6 +1117,13 @@ export class BeaconChain implements IBeaconChain { body, } as AssembledBlockType; + // TODO - beacon engine: remove this + const state = await this.regen.getBlockSlotState( + parentBlock, + slot, + {dontTransferCache: true}, + RegenCaller.produceBlock + ); const {newStateRoot, proposerReward} = computeNewStateRoot(this.metrics, state, block); block.stateRoot = newStateRoot; const blockRoot = @@ -1205,27 +1215,7 @@ export class BeaconChain implements IBeaconChain { } getProposerHead(slot: Slot): ProtoBlock { - this.metrics?.forkChoice.requests.inc(); - const timer = this.metrics?.forkChoice.findHead.startTimer({caller: FindHeadFnName.getProposerHead}); - const secFromSlot = this.clock.secFromSlot(slot); - - try { - const {head, isHeadTimely, notReorgedReason} = this.beaconEngine.forkChoice.updateAndGetHead({ - mode: UpdateHeadOpt.GetProposerHead, - secFromSlot, - slot, - }); - - if (isHeadTimely && notReorgedReason !== undefined) { - this.metrics?.forkChoice.notReorgedReason.inc({reason: notReorgedReason}); - } - return head; - } catch (e) { - this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetProposerHead}); - throw e; - } finally { - timer?.(); - } + return this.beaconEngine.getProposerHead(slot); } /** diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index ec6474c0d046..67d133101d74 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -20,7 +20,11 @@ import {ClockEvent} from "../util/clock.js"; import {isQueueErrorAborted} from "../util/queue/index.js"; import {ForkchoiceCaller} from "./forkChoice/index.js"; import {IBeaconChain} from "./interface.js"; -import {getPayloadAttributesForSSE, prepareExecutionPayload} from "./produceBlock/produceBlockBody.js"; +import { + getPayloadAttributesForSSE, + prepareExecutionPayload, + resolvePayloadAttributesInput, +} from "./produceBlock/produceBlockBody.js"; import {RegenCaller} from "./regen/index.js"; // TODO GLOAS: re-evaluate this timing @@ -207,7 +211,13 @@ export class PrepareNextSlotScheduler { parentBlockHash, safeBlockHash, finalizedBlockHash, - stateAfterParentPayload, + prepareSlot, + resolvePayloadAttributesInput( + this.config, + fork as ForkPostBellatrix, + stateAfterParentPayload, + parentBlockHash + ), feeRecipient ); this.logger.verbose("PrepareNextSlotScheduler prepared new payload", { @@ -236,11 +246,19 @@ export class PrepareNextSlotScheduler { this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes) ) { const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, { - prepareState: stateAfterParentPayload, prepareSlot, parentBlockRoot: fromHex(updatedHead.blockRoot), parentBlockHash, feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000", + proposerIndex: stateAfterParentPayload.getBeaconProposer(prepareSlot), + parentBlockNumberPreGloas: + ForkSeq[fork] >= ForkSeq.gloas ? undefined : stateAfterParentPayload.payloadBlockNumber, + ...resolvePayloadAttributesInput( + this.config, + fork as ForkPostBellatrix, + stateAfterParentPayload, + parentBlockHash + ), }); this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork}); } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 9153bf6cbc67..864fb5f2251b 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -1,5 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; -import {IForkChoiceRead, ProtoBlock, getSafeExecutionBlockHash} from "@lodestar/fork-choice"; +import {IForkChoiceRead, ProtoBlock} from "@lodestar/fork-choice"; import { BUILDER_INDEX_SELF_BUILD, ForkName, @@ -16,12 +16,10 @@ import { } from "@lodestar/params"; import { G2_POINT_AT_INFINITY, - IBeaconStateView, type IBeaconStateViewBellatrix, computeEpochAtSlot, computeTimeAtSlot, getExpectedGasLimit, - isStatePostBellatrix, isStatePostCapella, isStatePostGloas, } from "@lodestar/state-transition"; @@ -51,7 +49,6 @@ import { ssz, } from "@lodestar/types"; import {GWEI_TO_WEI, Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils"; -import {ZERO_HASH_HEX} from "../../constants/index.js"; import {numToQuantity} from "../../execution/engine/utils.js"; import {IExecutionBuilder, IExecutionEngine, PayloadAttributes, PayloadId} from "../../execution/index.js"; import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; @@ -95,6 +92,29 @@ export type BlockAttributes = { builderBid?: gloas.SignedExecutionPayloadBid; }; +/** + * Scalars precomputed once by `BeaconEngine.produceBlockBase` and threaded through production so the + * downstream flow does not recompute the same forkChoice / base-state reads. Optional: pre-gloas + * callers (`produceBlockV3`) don't supply them yet, so `produceBlockBody` falls back to computing them. + */ +export type PreparedBlockScalars = { + proposerIndex: ValidatorIndex; + proposerPubKey: BLSPubkey; + safeBlockHash: RootHex; + finalizedBlockHash: RootHex; + timestamp: number; + prevRandao: Bytes32; + parentBlockHash: Bytes32; + parentGasLimit: number; + isBuildingOnFull: boolean; + // gloas: parent execution requests applied to produce the (already-filtered) common body, and the + // payload-attribute withdrawals resolved from that same applied state. + parentExecutionRequests: gloas.ExecutionRequests; + // gloas: payload attestations for the parent block's payload (slot - 1), collected from the pool once. + payloadAttestations: gloas.PayloadAttestation[]; + withdrawals?: PayloadAttributesWithdrawals; +}; + export enum BlockType { Full = "Full", Blinded = "Blinded", @@ -152,37 +172,14 @@ export type ProduceResult = | ProduceFullPhase0 | ProduceBlinded; -/** - * Drop voluntary exits that `parent_execution_requests` have invalidated (e.g. a withdrawal - * request initiating an exit on the same validator). Op pool selected against the unapplied - * state, so re-validate against the post-apply state to avoid producing an invalid block. - * - * `getStateAfterParentPayload` is a thunk so the post-apply state is only materialized when - * actually needed (i.e. when extending the parent payload and there are exits to filter). - */ -function maybeFilterInvalidatedVoluntaryExits( - commonBlockBody: CommonBlockBody, - isExtendingPayload: boolean, - getStateAfterParentPayload: () => IBeaconStateViewBellatrix -): CommonBlockBody["voluntaryExits"] { - if (!isExtendingPayload || commonBlockBody.voluntaryExits.length === 0) { - return commonBlockBody.voluntaryExits; - } - const state = getStateAfterParentPayload(); - return commonBlockBody.voluntaryExits.filter((signedVoluntaryExit) => - state.isValidVoluntaryExit(signedVoluntaryExit, false) - ); -} - export async function produceBlockBody( this: BeaconChain, blockType: T, - currentState: IBeaconStateView, + // All scalars are precomputed once by `BeaconEngine.produceBlockBase` (both V3 and V4 routes) and + // threaded here, so `produceBlockBody` no longer needs the `BeaconState` — it reads the scalars directly. blockAttr: BlockAttributes & { - proposerIndex: ValidatorIndex; - proposerPubKey: BLSPubkey; commonBlockBodyPromise: Promise; - } + } & PreparedBlockScalars ): Promise<{ body: AssembledBodyType; produceResult: ProduceResult; @@ -197,6 +194,17 @@ export async function produceBlockBody( proposerPubKey, commonBlockBodyPromise, builderBid, + // Precomputed by produceBlockBase (gloas/V4); undefined on the V3 path → computed via `??` below + safeBlockHash: preparedSafeBlockHash, + finalizedBlockHash: preparedFinalizedBlockHash, + timestamp: preparedTimestamp, + prevRandao: preparedPrevRandao, + parentBlockHash: preparedParentBlockHash, + parentGasLimit: preparedParentGasLimit, + isBuildingOnFull: preparedIsBuildingOnFull, + parentExecutionRequests: preparedParentExecutionRequests, + payloadAttestations: preparedPayloadAttestations, + withdrawals: preparedWithdrawals, } = blockAttr; let executionPayloadValue: Wei; let blockBody: AssembledBodyType; @@ -218,30 +226,16 @@ export async function produceBlockBody( this.logger.verbose("Producing beacon block body", logMeta); if (builderBid !== undefined) { - if (!isStatePostGloas(currentState)) { - throw new Error("Expected Gloas state for builder bid block production"); - } - - const isExtendingPayload = byteArrayEquals( - builderBid.message.parentBlockHash, - currentState.latestExecutionPayloadBid.blockHash - ); - const parentExecutionRequests = isExtendingPayload - ? await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot) - : ssz.gloas.ExecutionRequests.defaultValue(); + // parentExecutionRequests was applied by produceBlockBase to build the common body; the bid + // lookup is gated by isBuildingOnFull so it agrees with this bid's extend decision. executionPayloadValue = BigInt(builderBid.message.value) * GWEI_TO_WEI; const commonBlockBody = await commonBlockBodyPromise; const gloasBody = Object.assign({}, commonBlockBody) as gloas.BeaconBlockBody; gloasBody.signedExecutionPayloadBid = builderBid; - gloasBody.payloadAttestations = this.payloadAttestationPool.getPayloadAttestationsForBlock( - parentBlock.blockRoot, - blockSlot - 1 - ); - gloasBody.parentExecutionRequests = parentExecutionRequests; - gloasBody.voluntaryExits = maybeFilterInvalidatedVoluntaryExits(commonBlockBody, isExtendingPayload, () => - currentState.withParentPayloadApplied(parentExecutionRequests) - ); + gloasBody.payloadAttestations = preparedPayloadAttestations; + gloasBody.parentExecutionRequests = preparedParentExecutionRequests; + // gloasBody.voluntaryExits keep the common body's exits — already valid against the applied state. blockBody = gloasBody as AssembledBodyType; this.logger.verbose("Produced block with builder bid", { @@ -251,18 +245,11 @@ export async function produceBlockBody( parentBlockHash: toRootHex(builderBid.message.parentBlockHash), parentBlockRoot: toRootHex(builderBid.message.parentBlockRoot), blockHash: toRootHex(builderBid.message.blockHash), - isExtendingPayload, }); } else if (isForkPostGloas(fork)) { - if (!isStatePostGloas(currentState)) { - throw new Error("Expected Gloas state for Gloas block production"); - } - // TODO GLOAS: support non self-building here, the block type differentiation between // full and blinded no longer makes sense in gloas, it might be a good idea to move // this into a completely separate function and have pre/post gloas more separated - const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); - const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; // TODO GLOAS: post-Gloas, proposer feeRecipient is also carried (signed) in // ProposerPreferencesPool. Consider using this unified cache instead // see https://github.com/ChainSafe/lodestar/issues/9379 @@ -270,32 +257,20 @@ export async function produceBlockBody( const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer(); - // Get execution payload from EL - let parentBlockHash: Bytes32; - let parentExecutionRequests: gloas.ExecutionRequests; - // Apply parent payload once here as it's reused by EL prep and voluntary exit filtering below - let stateAfterParentPayload: IBeaconStateViewBellatrix = currentState; - // Spec: should_build_on_full(store, head). `parentBlock` is the proposer's head - // (set by chain.getProposerHead(slot)). Returns false when the PTC majority signalled - // the blob data is not available or the payload was not timely, forcing a build on EMPTY (reorg). - const isBuildingOnFull = this.forkChoice.shouldBuildOnFull(parentBlock, blockSlot); - if (isBuildingOnFull) { - parentBlockHash = currentState.latestExecutionPayloadBid.blockHash; - parentExecutionRequests = await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot); - stateAfterParentPayload = currentState.withParentPayloadApplied(parentExecutionRequests); - } else { - parentBlockHash = currentState.latestExecutionPayloadBid.parentBlockHash; - parentExecutionRequests = ssz.gloas.ExecutionRequests.defaultValue(); - } const prepareRes = await prepareExecutionPayload( this, this.logger, fork, parentBlockRoot, - parentBlockHash, - safeBlockHash, - finalizedBlockHash ?? ZERO_HASH_HEX, - stateAfterParentPayload, + preparedParentBlockHash, + preparedSafeBlockHash, + preparedFinalizedBlockHash, + blockSlot, + { + timestamp: preparedTimestamp, + prevRandao: preparedPrevRandao, + withdrawals: preparedWithdrawals, + }, feeRecipient ); @@ -305,11 +280,11 @@ export async function produceBlockBody( this.logger.verbose("Prepared execution payload from engine", { slot: blockSlot, parentBlockRoot: toRootHex(parentBlockRoot), - parentBlockHash: toRootHex(parentBlockHash), + parentBlockHash: toRootHex(preparedParentBlockHash), feeRecipient, prepType, payloadId, - isBuildingOnFull, + isBuildingOnFull: preparedIsBuildingOnFull, }); if (prepType !== PayloadPreparationType.Cached) { @@ -339,10 +314,10 @@ export async function produceBlockBody( // Create self-build execution payload bid const bid: gloas.ExecutionPayloadBid = { - parentBlockHash, + parentBlockHash: preparedParentBlockHash, parentBlockRoot, blockHash: executionPayload.blockHash, - prevRandao: currentState.getRandaoMix(currentState.epoch), + prevRandao: preparedPrevRandao, feeRecipient: executionPayload.feeRecipient, gasLimit: executionPayload.gasLimit, builderIndex: BUILDER_INDEX_SELF_BUILD, @@ -360,16 +335,9 @@ export async function produceBlockBody( const commonBlockBody = await commonBlockBodyPromise; const gloasBody = Object.assign({}, commonBlockBody) as gloas.BeaconBlockBody; gloasBody.signedExecutionPayloadBid = signedBid; - gloasBody.payloadAttestations = this.payloadAttestationPool.getPayloadAttestationsForBlock( - parentBlock.blockRoot, - blockSlot - 1 - ); - gloasBody.parentExecutionRequests = parentExecutionRequests; - gloasBody.voluntaryExits = maybeFilterInvalidatedVoluntaryExits( - commonBlockBody, - isBuildingOnFull, - () => stateAfterParentPayload - ); + gloasBody.payloadAttestations = preparedPayloadAttestations; + gloasBody.parentExecutionRequests = preparedParentExecutionRequests; + // gloasBody.voluntaryExits keep the common body's exits — already valid against the applied state. blockBody = gloasBody as AssembledBodyType; // Store execution payload data required to construct execution payload envelope later @@ -399,12 +367,6 @@ export async function produceBlockBody( shouldOverrideBuilder, }); } else if (isForkPostBellatrix(fork)) { - if (!isStatePostBellatrix(currentState)) { - throw new Error("Expected Bellatrix state for execution block production"); - } - - const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); - const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex); const feeRecipientType = requestedFeeRecipient ? "requested" @@ -414,6 +376,12 @@ export async function produceBlockBody( Object.assign(logMeta, {feeRecipientType, feeRecipient}); + const payloadAttributesInput: PayloadAttributesInput = { + timestamp: preparedTimestamp, + prevRandao: preparedPrevRandao, + withdrawals: preparedWithdrawals, + }; + if (blockType === BlockType.Blinded) { if (!this.executionBuilder) throw Error("External builder not configured"); const executionBuilder = this.executionBuilder; @@ -429,10 +397,11 @@ export async function produceBlockBody( this.logger, fork, parentBlockRoot, - currentState.latestExecutionPayloadHeader.blockHash, - safeBlockHash, - finalizedBlockHash ?? ZERO_HASH_HEX, - currentState, + preparedParentBlockHash, + preparedSafeBlockHash, + preparedFinalizedBlockHash, + blockSlot, + payloadAttributesInput, executionBuilder.issueLocalFcUWithFeeRecipient ); } @@ -444,7 +413,13 @@ export async function produceBlockBody( slot: blockSlot, proposerPubKey: toHex(proposerPubKey), }); - const headerRes = await prepareExecutionPayloadHeader(this, fork, currentState, proposerPubKey); + const headerRes = await prepareExecutionPayloadHeader( + this, + fork, + preparedParentBlockHash, + blockSlot, + proposerPubKey + ); endExecutionPayloadHeader?.({ step: BlockProductionStep.executionPayload, @@ -479,11 +454,10 @@ export async function produceBlockBody( }); } else { const headerGasLimit = builderRes.header.gasLimit; - const parentGasLimit = currentState.latestExecutionPayloadHeader.gasLimit; - const expectedGasLimit = getExpectedGasLimit(parentGasLimit, targetGasLimit); + const expectedGasLimit = getExpectedGasLimit(preparedParentGasLimit, targetGasLimit); - const lowerBound = Math.min(parentGasLimit, expectedGasLimit); - const upperBound = Math.max(parentGasLimit, expectedGasLimit); + const lowerBound = Math.min(preparedParentGasLimit, expectedGasLimit); + const upperBound = Math.max(preparedParentGasLimit, expectedGasLimit); if (headerGasLimit < lowerBound || headerGasLimit > upperBound) { throw Error( @@ -496,7 +470,7 @@ export async function produceBlockBody( slot: blockSlot, headerGasLimit, expectedGasLimit, - parentGasLimit, + parentGasLimit: preparedParentGasLimit, targetGasLimit, }); } @@ -538,10 +512,11 @@ export async function produceBlockBody( this.logger, fork, parentBlockRoot, - currentState.latestExecutionPayloadHeader.blockHash, - safeBlockHash, - finalizedBlockHash ?? ZERO_HASH_HEX, - currentState, + preparedParentBlockHash, + preparedSafeBlockHash, + preparedFinalizedBlockHash, + blockSlot, + payloadAttributesInput, feeRecipient ); @@ -709,15 +684,15 @@ export async function prepareExecutionPayload( parentBlockHash: Bytes32, safeBlockHash: RootHex, finalizedBlockHash: RootHex, + prepareSlot: Slot, /** - * Post-gloas, when extending a full parent, callers must apply - * parent execution payload first (see `withParentPayloadApplied`). + * Post-gloas, when extending a full parent, the input must be resolved from a state with parent + * execution payload applied first (see `withParentPayloadApplied`). */ - state: IBeaconStateViewBellatrix, + payloadAttributesInput: PayloadAttributesInput, suggestedFeeRecipient: string ): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> { - const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime); - const prevRandao = state.getRandaoMix(state.epoch); + const {timestamp, prevRandao, withdrawals} = payloadAttributesInput; const payloadIdCached = chain.executionEngine.payloadIdCache.get({ headBlockHash: toRootHex(parentBlockHash), @@ -746,11 +721,13 @@ export async function prepareExecutionPayload( } const attributes: PayloadAttributes = preparePayloadAttributes(fork, chain, { - prepareState: state, - prepareSlot: state.slot, + prepareSlot, parentBlockRoot, parentBlockHash, feeRecipient: suggestedFeeRecipient, + timestamp, + prevRandao, + withdrawals, }); payloadId = await chain.executionEngine.notifyForkchoiceUpdate( @@ -781,7 +758,8 @@ async function prepareExecutionPayloadHeader( config: ChainForkConfig; }, fork: ForkPostBellatrix, - state: IBeaconStateViewBellatrix, + parentHash: Bytes32, + slot: Slot, proposerPubKey: BLSPubkey ): Promise<{ header: ExecutionPayloadHeader; @@ -793,8 +771,7 @@ async function prepareExecutionPayloadHeader( throw Error("executionBuilder required"); } - const parentHash = state.latestExecutionPayloadHeader.blockHash; - return chain.executionBuilder.getHeader(fork, state.slot, parentHash, proposerPubKey); + return chain.executionBuilder.getHeader(fork, slot, parentHash, proposerPubKey); } export function getPayloadAttributesForSSE( @@ -805,29 +782,34 @@ export function getPayloadAttributesForSSE( proposerPreferencesPool: ProposerPreferencesPool; }, { - prepareState, prepareSlot, parentBlockRoot, parentBlockHash, feeRecipient, + timestamp, + prevRandao, + withdrawals, + proposerIndex, + parentBlockNumberPreGloas, }: { - /** - * Post-gloas, when extending a full parent, callers must apply - * parent execution payload first (see `withParentPayloadApplied`). - */ - prepareState: IBeaconStateViewBellatrix; prepareSlot: Slot; parentBlockRoot: Root; parentBlockHash: Bytes32; feeRecipient: string; - } + /** proposer at the prepare slot (SSE event) */ + proposerIndex: ValidatorIndex; + /** pre-gloas only: `state.payloadBlockNumber` for the SSE parentBlockNumber */ + parentBlockNumberPreGloas?: number; + } & PayloadAttributesInput ): SSEPayloadAttributes { const payloadAttributes = preparePayloadAttributes(fork, chain, { - prepareState, prepareSlot, parentBlockRoot, parentBlockHash, feeRecipient, + timestamp, + prevRandao, + withdrawals, }); let parentBlockNumber: number; @@ -841,11 +823,14 @@ export function getPayloadAttributesForSSE( } parentBlockNumber = parentBlock.executionPayloadNumber; } else { - parentBlockNumber = prepareState.payloadBlockNumber; + if (parentBlockNumberPreGloas === undefined) { + throw Error("Expected parentBlockNumberPreGloas for pre-gloas SSE payload attributes"); + } + parentBlockNumber = parentBlockNumberPreGloas; } const ssePayloadAttributes: SSEPayloadAttributes = { - proposerIndex: prepareState.getBeaconProposer(prepareSlot), + proposerIndex, proposalSlot: prepareSlot, parentBlockNumber, parentBlockRoot, @@ -855,6 +840,68 @@ export function getPayloadAttributesForSSE( return ssePayloadAttributes; } +export type PayloadAttributesWithdrawals = capella.SSEPayloadAttributes["payloadAttributes"]["withdrawals"]; + +/** + * Plain (BeaconState-free) inputs for execution payload attributes. Produced by the single state + * reader `resolvePayloadAttributesInput`; consumed by `preparePayloadAttributes` / + * `prepareExecutionPayload` / `getPayloadAttributesForSSE`. + */ +export type PayloadAttributesInput = { + timestamp: number; + prevRandao: Bytes32; + /** undefined pre-capella */ + withdrawals?: PayloadAttributesWithdrawals; +}; + +/** + * The single place that reads `BeaconState` for execution payload attributes. Everything downstream + * consumes the returned plain fields and never touches the state. + * + * Post-gloas, when extending a full parent, callers must apply parent execution payload first + * (see `withParentPayloadApplied`) before calling this. + * // TODO - beacon engine: should be a private function of BeaconEngine because it needs BeaconState + */ +export function resolvePayloadAttributesInput( + config: ChainForkConfig, + fork: ForkPostBellatrix, + state: IBeaconStateViewBellatrix, + parentBlockHash: Bytes32 +): PayloadAttributesInput { + const timestamp = computeTimeAtSlot(config, state.slot, state.genesisTime); + const prevRandao = state.getRandaoMix(state.epoch); + + let withdrawals: PayloadAttributesWithdrawals | undefined; + if (ForkSeq[fork] >= ForkSeq.capella) { + if (!isStatePostCapella(state)) { + throw new Error("Expected Capella state for withdrawals"); + } + + if (isStatePostGloas(state)) { + const isExtendingPayload = byteArrayEquals(parentBlockHash, state.latestExecutionPayloadBid.blockHash); + if (isExtendingPayload) { + // applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch + // here means the caller did not apply parent payload to state + if (!byteArrayEquals(state.latestBlockHash, state.latestExecutionPayloadBid.blockHash)) { + throw new Error("Expected state with parent execution payload applied for withdrawals"); + } + withdrawals = state.getExpectedWithdrawals().expectedWithdrawals; + } else { + // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch + // already deducted from CL balances but never credited on the EL (the envelope + // was not delivered). The next payload must carry those same withdrawals to + // restore CL/EL consistency, otherwise validators permanently lose that balance. + withdrawals = state.payloadExpectedWithdrawals; + } + } else { + // withdrawals logic is now fork aware as it changes on electra fork post capella + withdrawals = state.getExpectedWithdrawals().expectedWithdrawals; + } + } + + return {timestamp, prevRandao, withdrawals}; +} + function preparePayloadAttributes( fork: ForkPostBellatrix, chain: { @@ -863,25 +910,23 @@ function preparePayloadAttributes( proposerPreferencesPool: ProposerPreferencesPool; }, { - prepareState, prepareSlot, parentBlockRoot, parentBlockHash, feeRecipient, + timestamp, + prevRandao, + withdrawals, }: { - /** - * Post-gloas, when extending a full parent, callers must apply - * parent execution payload first (see `withParentPayloadApplied`). - */ - prepareState: IBeaconStateViewBellatrix; prepareSlot: Slot; parentBlockRoot: Root; parentBlockHash: Bytes32; feeRecipient: string; + timestamp: number; + prevRandao: Bytes32; + withdrawals?: PayloadAttributesWithdrawals; } ): SSEPayloadAttributes["payloadAttributes"] { - const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime); - const prevRandao = prepareState.getRandaoMix(prepareState.epoch); const payloadAttributes = { timestamp, prevRandao, @@ -889,33 +934,10 @@ function preparePayloadAttributes( }; if (ForkSeq[fork] >= ForkSeq.capella) { - if (!isStatePostCapella(prepareState)) { - throw new Error("Expected Capella state for withdrawals"); - } - - if (isStatePostGloas(prepareState)) { - const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash); - if (isExtendingPayload) { - // applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch - // here means the caller did not apply parent payload to prepareState - if (!byteArrayEquals(prepareState.latestBlockHash, prepareState.latestExecutionPayloadBid.blockHash)) { - throw new Error("Expected state with parent execution payload applied for withdrawals"); - } - (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = - prepareState.getExpectedWithdrawals().expectedWithdrawals; - } else { - // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch - // already deducted from CL balances but never credited on the EL (the envelope - // was not delivered). The next payload must carry those same withdrawals to - // restore CL/EL consistency, otherwise validators permanently lose that balance. - (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = - prepareState.payloadExpectedWithdrawals; - } - } else { - // withdrawals logic is now fork aware as it changes on electra fork post capella - (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = - prepareState.getExpectedWithdrawals().expectedWithdrawals; + if (withdrawals === undefined) { + throw new Error("Expected withdrawals for post-capella payload attributes"); } + (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals = withdrawals; } if (ForkSeq[fork] >= ForkSeq.deneb) { @@ -923,9 +945,6 @@ function preparePayloadAttributes( } if (ForkSeq[fork] >= ForkSeq.gloas) { - if (!isStatePostGloas(prepareState)) { - throw new Error("Expected Gloas state for Gloas payload attributes"); - } (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).slotNumber = prepareSlot; (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).targetGasLimit = getProposerTargetGasLimit( chain, @@ -989,76 +1008,3 @@ function getProposerTargetGasLimit( } return parentPayloadVariant.executionPayloadGasLimit; } - -export async function produceCommonBlockBody( - this: BeaconChain, - blockType: T, - currentState: IBeaconStateView, - {randaoReveal, graffiti, slot, parentBlock}: BlockAttributes -): Promise { - const stepsMetrics = - blockType === BlockType.Full - ? this.metrics?.executionBlockProductionTimeSteps - : this.metrics?.builderBlockProductionTimeSteps; - - const fork = this.config.getForkName(slot); - - // TODO: - // Iterate through the naive aggregation pool and ensure all the attestations from there - // are included in the operation pool. - // for (const attestation of db.attestationPool.getAll()) { - // try { - // opPool.insertAttestation(attestation); - // } catch (e) { - // // Don't stop block production if there's an error, just create a log. - // logger.error("Attestation did not transfer to op pool", {}, e); - // } - // } - const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] = - this.opPool.getSlashingsAndExits(currentState, blockType, this.metrics); - - const endAttestations = stepsMetrics?.startTimer(); - const attestations = this.aggregatedAttestationPool.getAttestationsForBlock( - fork, - this.forkChoice, - this.shufflingCache, - currentState - ); - endAttestations?.({ - step: BlockProductionStep.attestations, - }); - - const blockBody: Omit = { - randaoReveal, - graffiti, - // Eth1 data voting is no longer required since electra - eth1Data: currentState.eth1Data, - proposerSlashings, - attesterSlashings, - attestations, - // Since electra, deposits are processed by the execution layer, - // we no longer support handling deposits from earlier forks. - deposits: [], - voluntaryExits, - }; - - if (ForkSeq[fork] >= ForkSeq.capella) { - (blockBody as CommonBlockBody).blsToExecutionChanges = blsToExecutionChanges; - } - - const endSyncAggregate = stepsMetrics?.startTimer(); - if (ForkSeq[fork] >= ForkSeq.altair) { - const parentBlockRoot = fromHex(parentBlock.blockRoot); - const previousSlot = slot - 1; - const syncAggregate = this.syncContributionAndProofPool.getAggregate(previousSlot, parentBlockRoot); - this.metrics?.production.producedSyncAggregateParticipants.observe( - syncAggregate.syncCommitteeBits.getTrueBitIndexes().length - ); - (blockBody as CommonBlockBody).syncAggregate = syncAggregate; - } - endSyncAggregate?.({ - step: BlockProductionStep.syncAggregate, - }); - - return blockBody as CommonBlockBody; -} diff --git a/packages/beacon-node/src/chain/regen/regen.ts b/packages/beacon-node/src/chain/regen/regen.ts index 31482575f453..13de4f34f952 100644 --- a/packages/beacon-node/src/chain/regen/regen.ts +++ b/packages/beacon-node/src/chain/regen/regen.ts @@ -27,6 +27,7 @@ export type RegenModules = { forkChoice: IForkChoice; blockStateCache: BlockStateCache; checkpointStateCache: CheckpointStateCache; + // TODO - beacon engine: remove this, use db instead seenBlockInputCache: SeenBlockInput; config: ChainForkConfig; emitter: ChainEventEmitter; diff --git a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts index 8744bac328a1..cbacdb47b344 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts @@ -4,29 +4,57 @@ import { getExecutionPayloadEnvelopeSignatureSet, isStatePostGloas, } from "@lodestar/state-transition"; -import {gloas, ssz} from "@lodestar/types"; +import {Root, RootHex, ValidatorIndex, gloas, ssz} from "@lodestar/types"; import {byteArrayEquals, toRootHex} from "@lodestar/utils"; import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {ExecutionPayloadEnvelopeError, ExecutionPayloadEnvelopeErrorCode, GossipAction} from "../errors/index.js"; import {RegenCaller} from "../regen/index.js"; +// The `bid*` / `proposerIndex` values are read facade-side from the `PayloadEnvelopeInput` (owned by +// BeaconChain) and passed in as scalars — the engine no longer touches the DA seen cache. export async function validateApiExecutionPayloadEnvelope( engine: BeaconEngine, - executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope, + proposerIndex: ValidatorIndex, + bidBuilderIndex: ValidatorIndex, + bidBlockHashHex: RootHex, + bidExecutionRequestsRoot: Root ): Promise { - return validateExecutionPayloadEnvelope(engine, executionPayloadEnvelope); + return validateExecutionPayloadEnvelope( + engine, + executionPayloadEnvelope, + proposerIndex, + bidBuilderIndex, + bidBlockHashHex, + bidExecutionRequestsRoot + ); } export async function validateGossipExecutionPayloadEnvelope( engine: BeaconEngine, - executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope, + proposerIndex: ValidatorIndex, + bidBuilderIndex: ValidatorIndex, + bidBlockHashHex: RootHex, + bidExecutionRequestsRoot: Root ): Promise { - return validateExecutionPayloadEnvelope(engine, executionPayloadEnvelope); + return validateExecutionPayloadEnvelope( + engine, + executionPayloadEnvelope, + proposerIndex, + bidBuilderIndex, + bidBlockHashHex, + bidExecutionRequestsRoot + ); } async function validateExecutionPayloadEnvelope( engine: BeaconEngine, - executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope + executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope, + proposerIndex: ValidatorIndex, + bidBuilderIndex: ValidatorIndex, + bidBlockHashHex: RootHex, + bidExecutionRequestsRoot: Root ): Promise { const envelope = executionPayloadEnvelope.message; const {payload} = envelope; @@ -46,8 +74,12 @@ async function validateExecutionPayloadEnvelope( // [IGNORE] The node has not seen another valid // `SignedExecutionPayloadEnvelope` for this block root from this builder. const envelopeBlock = engine.forkChoice.getBlockHex(blockRootHex, PayloadStatus.FULL); - const payloadInput = engine.seenPayloadEnvelopeInputCache.get(blockRootHex); - if (envelopeBlock || payloadInput?.hasPayloadEnvelope()) { + + // const payloadInput = engine.seenPayloadEnvelopeInputCache.get(blockRootHex); + // if (envelopeBlock || payloadInput?.hasPayloadEnvelope()) { + // TODO - beacon engine: unstable also check seenPayloadEnvelopeInputCache but we should not do it + // see also https://github.com/ethereum/consensus-specs/pull/5355 + if (envelopeBlock) { throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { code: ExecutionPayloadEnvelopeErrorCode.ENVELOPE_ALREADY_KNOWN, blockRoot: blockRootHex, @@ -55,14 +87,6 @@ async function validateExecutionPayloadEnvelope( }); } - if (!payloadInput) { - // PayloadEnvelopeInput should have been created during block import - throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { - code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING, - blockRoot: blockRootHex, - }); - } - // [IGNORE] The envelope is from a slot greater than or equal to the latest finalized slot -- i.e. validate that `payload.slotNumber >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)` const finalizedCheckpoint = engine.forkChoice.getFinalizedCheckpoint(); const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch); @@ -88,30 +112,30 @@ async function validateExecutionPayloadEnvelope( } // [REJECT] `envelope.builder_index == bid.builder_index` - if (envelope.builderIndex !== payloadInput.getBuilderIndex()) { + if (envelope.builderIndex !== bidBuilderIndex) { throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { code: ExecutionPayloadEnvelopeErrorCode.BUILDER_INDEX_MISMATCH, envelopeBuilderIndex: envelope.builderIndex, - bidBuilderIndex: payloadInput.getBuilderIndex(), + bidBuilderIndex, }); } // [REJECT] `payload.block_hash == bid.block_hash` - if (toRootHex(payload.blockHash) !== payloadInput.getBlockHashHex()) { + if (toRootHex(payload.blockHash) !== bidBlockHashHex) { throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { code: ExecutionPayloadEnvelopeErrorCode.BLOCK_HASH_MISMATCH, envelopeBlockHash: toRootHex(payload.blockHash), - bidBlockHash: payloadInput.getBlockHashHex(), + bidBlockHash: bidBlockHashHex, }); } // [REJECT] `hash_tree_root(envelope.execution_requests) == bid.execution_requests_root` const requestsRoot = ssz.gloas.ExecutionRequests.hashTreeRoot(envelope.executionRequests); - if (!byteArrayEquals(requestsRoot, payloadInput.getBid().executionRequestsRoot)) { + if (!byteArrayEquals(requestsRoot, bidExecutionRequestsRoot)) { throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_ROOT_MISMATCH, envelopeRequestsRoot: toRootHex(requestsRoot), - bidRequestsRoot: toRootHex(payloadInput.getBid().executionRequestsRoot), + bidRequestsRoot: toRootHex(bidExecutionRequestsRoot), }); } @@ -136,7 +160,7 @@ async function validateExecutionPayloadEnvelope( engine.pubkeyCache, blockState, executionPayloadEnvelope, - payloadInput.proposerIndex + proposerIndex ); if (!(await engine.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index a9cdfe987202..6b1d2c6714f1 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -1087,7 +1087,22 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand // unlike BlockInput, we send the envelope into UnknownBlockInput sync // inside the sync it'll reconcile into PayloadEnvelopeInput and share the same cache with gossip - const res = await chain.beaconEngine.validateGossipExecutionPayloadEnvelope(serializedData, signedEnvelope); + const blockRootHex = toRootHex(envelope.beaconBlockRoot); + // The facade owns the DA cache; look up the bid scalars here and pass them to the engine. If the + // input is missing we can't validate the envelope (no bid), so return early without the engine. + const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); + if (!payloadInput) { + // This shouldn't happen because beacon block should have been imported and thus payload input should have been created. + return ignore(ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING, {blockRoot: blockRootHex}); + } + const res = await chain.beaconEngine.validateGossipExecutionPayloadEnvelope( + serializedData, + signedEnvelope, + payloadInput.proposerIndex, + payloadInput.getBuilderIndex(), + payloadInput.getBlockHashHex(), + payloadInput.getBid().executionRequestsRoot + ); if (res.status !== GossipValidationStatus.Accept) { const {beaconBlockRoot} = signedEnvelope.message; const envelopeSlot = signedEnvelope.message.payload.slotNumber; @@ -1120,14 +1135,6 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand metrics?.gossipExecutionPayloadEnvelope.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec); chain.validatorMonitor?.registerExecutionPayloadEnvelope(OpSource.gossip, delaySec, signedEnvelope); - const blockRootHex = toRootHex(envelope.beaconBlockRoot); - const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); - - if (!payloadInput) { - // This shouldn't happen because beacon block should have been imported and thus payload input should have been created. - return ignore(ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING, {blockRoot: blockRootHex}); - } - chain.serializedCache.set(signedEnvelope, serializedData); payloadInput.addPayloadEnvelope({ diff --git a/packages/beacon-node/src/sync/unknownBlock.ts b/packages/beacon-node/src/sync/unknownBlock.ts index 6bf8b1c01a4f..e3fc551a5053 100644 --- a/packages/beacon-node/src/sync/unknownBlock.ts +++ b/packages/beacon-node/src/sync/unknownBlock.ts @@ -901,7 +901,11 @@ export class BlockInputSync { ssz.gloas.SignedExecutionPayloadEnvelope.serialize(pendingPayload.envelope); const validationResult = await this.chain.beaconEngine.validateGossipExecutionPayloadEnvelope( envelopeBytes, - pendingPayload.envelope + pendingPayload.envelope, + payloadInput.proposerIndex, + payloadInput.getBuilderIndex(), + payloadInput.getBlockHashHex(), + payloadInput.getBid().executionRequestsRoot ); if (validationResult.status !== GossipValidationStatus.Accept) { this.logger.debug( @@ -1131,7 +1135,14 @@ export class BlockInputSync { if (!payloadInput.hasPayloadEnvelope()) { const envelopeBytes = this.chain.serializedCache.get(envelope) ?? ssz.gloas.SignedExecutionPayloadEnvelope.serialize(envelope); - const res = await this.chain.beaconEngine.validateGossipExecutionPayloadEnvelope(envelopeBytes, envelope); + const res = await this.chain.beaconEngine.validateGossipExecutionPayloadEnvelope( + envelopeBytes, + envelope, + payloadInput.proposerIndex, + payloadInput.getBuilderIndex(), + payloadInput.getBlockHashHex(), + payloadInput.getBid().executionRequestsRoot + ); if (res.status !== GossipValidationStatus.Accept) { throw res.error ?? new Error(`Execution payload envelope validation failed: ${res.code}`); } diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index 721a9c3fb257..09816479889b 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -161,6 +161,7 @@ vi.mock("../../src/chain/chain.js", async (importActual) => { shufflingCache: new ShufflingCache(), pubkeyCache: createPubkeyCache(), produceCommonBlockBody: vi.fn(), + produceBlockBase: vi.fn(), getProposerHead: vi.fn(), produceBlock: vi.fn(), produceBlindedBlock: vi.fn(), diff --git a/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts index 9f503b56648d..c73b124958db 100644 --- a/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts +++ b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts @@ -2,16 +2,13 @@ import {generateKeyPair} from "@libp2p/crypto/keys"; import {afterAll, beforeAll, bench, describe} from "@chainsafe/benchmark"; import {LevelDbController} from "@lodestar/db/controller/level"; import {testLogger} from "@lodestar/logger/test-utils"; -import {BeaconStateView, CachedBeaconStateElectra} from "@lodestar/state-transition"; +import {BeaconStateView, CachedBeaconStateElectra, computeTimeAtSlot} from "@lodestar/state-transition"; import {generatePerfTestCachedStateElectra} from "@lodestar/state-transition/test-utils"; +import {ssz} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; import {defaultOptions as defaultValidatorOptions} from "@lodestar/validator"; import {BeaconChain} from "../../../../src/chain/index.js"; -import { - BlockType, - produceBlockBody, - produceCommonBlockBody, -} from "../../../../src/chain/produceBlock/produceBlockBody.js"; +import {BlockType, produceBlockBody} from "../../../../src/chain/produceBlock/produceBlockBody.js"; import {getExecutionEngineFromBackend} from "../../../../src/execution/engine/index.js"; import {ExecutionEngineMockBackend} from "../../../../src/execution/engine/mock.js"; import {ArchiveMode, BeaconDb} from "../../../../src/index.js"; @@ -93,14 +90,18 @@ describe("produceBlockBody", () => { fn: async ({chain, state, head, proposerIndex, proposerPubKey}) => { const slot = state.slot; - const commonBlockBodyPromise = produceCommonBlockBody.call(chain, BlockType.Full, state, { + const commonBlockBodyPromise = chain.beaconEngine.produceCommonBlockBody({ slot: slot + 1, graffiti: Buffer.alloc(32), randaoReveal: Buffer.alloc(96), parentBlock: head, }); - await produceBlockBody.call(chain, BlockType.Full, state, { + // Scalars normally precomputed by produceBlockBase; mirror the genesis exec hash so the EL mock + // (headBlockHash/finalizedBlockHash ∈ validBlocks) accepts the forkchoice update. + const parentBlockHash = state.latestExecutionPayloadHeader.blockHash; + const parentHashHex = toRootHex(parentBlockHash); + await produceBlockBody.call(chain, BlockType.Full, { slot: slot + 1, graffiti: Buffer.alloc(32), randaoReveal: Buffer.alloc(96), @@ -108,6 +109,16 @@ describe("produceBlockBody", () => { proposerIndex, proposerPubKey, commonBlockBodyPromise, + safeBlockHash: parentHashHex, + finalizedBlockHash: parentHashHex, + timestamp: computeTimeAtSlot(chain.config, slot + 1, state.genesisTime), + prevRandao: state.getRandaoMix(state.epoch), + parentBlockHash, + parentGasLimit: state.latestExecutionPayloadHeader.gasLimit, + isBuildingOnFull: false, + parentExecutionRequests: ssz.gloas.ExecutionRequests.defaultValue(), + payloadAttestations: [], + withdrawals: state.getExpectedWithdrawals().expectedWithdrawals, }); }, }); diff --git a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts index aedb4eb09899..981ac2796b94 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts @@ -97,18 +97,22 @@ describe("api/validator - produceBlockV3", () => { } as ProtoBlock); modules.chain.getProposerHead.mockReturnValue({blockRoot: toRootHex(fullBlock.parentRoot)} as ProtoBlock); modules.chain.forkChoice.getBlockDefaultStatus.mockReturnValue(zeroProtoBlock); - modules.chain.produceCommonBlockBody.mockResolvedValue({ - attestations: fullBlock.body.attestations, - attesterSlashings: fullBlock.body.attesterSlashings, - deposits: fullBlock.body.deposits, - proposerSlashings: fullBlock.body.proposerSlashings, - eth1Data: fullBlock.body.eth1Data, - graffiti: fullBlock.body.graffiti, - randaoReveal: fullBlock.body.randaoReveal, - voluntaryExits: fullBlock.body.voluntaryExits, - blsToExecutionChanges: [], - syncAggregate: fullBlock.body.syncAggregate, - }); + vi.mocked(modules.chain.beaconEngine.produceBlockBase).mockResolvedValue({ + parentBlock: {blockRoot: toRootHex(fullBlock.parentRoot)} as ProtoBlock, + builderBid: null, + commonBlockBodyPromise: Promise.resolve({ + attestations: fullBlock.body.attestations, + attesterSlashings: fullBlock.body.attesterSlashings, + deposits: fullBlock.body.deposits, + proposerSlashings: fullBlock.body.proposerSlashings, + eth1Data: fullBlock.body.eth1Data, + graffiti: fullBlock.body.graffiti, + randaoReveal: fullBlock.body.randaoReveal, + voluntaryExits: fullBlock.body.voluntaryExits, + blsToExecutionChanges: [], + syncAggregate: fullBlock.body.syncAggregate, + }), + } as unknown as Awaited>); if (enginePayloadValue !== null) { modules.chain.produceBlock.mockResolvedValue({ @@ -191,18 +195,24 @@ describe("api/validator - produceBlockV3", () => { executionPayloadValue, consensusBlockValue, }); - modules.chain.produceCommonBlockBody.mockResolvedValue({ - attestations: fullBlock.body.attestations, - attesterSlashings: fullBlock.body.attesterSlashings, - deposits: fullBlock.body.deposits, - proposerSlashings: fullBlock.body.proposerSlashings, - eth1Data: fullBlock.body.eth1Data, - graffiti: fullBlock.body.graffiti, - randaoReveal: fullBlock.body.randaoReveal, - voluntaryExits: fullBlock.body.voluntaryExits, - blsToExecutionChanges: [], - syncAggregate: fullBlock.body.syncAggregate, - }); + // Return only parentBlock/builderBid/commonBlockBodyPromise so `...prepared` is empty and the + // produceBlock call args below match exactly. + vi.mocked(modules.chain.beaconEngine.produceBlockBase).mockResolvedValue({ + parentBlock, + builderBid: null, + commonBlockBodyPromise: Promise.resolve({ + attestations: fullBlock.body.attestations, + attesterSlashings: fullBlock.body.attesterSlashings, + deposits: fullBlock.body.deposits, + proposerSlashings: fullBlock.body.proposerSlashings, + eth1Data: fullBlock.body.eth1Data, + graffiti: fullBlock.body.graffiti, + randaoReveal: fullBlock.body.randaoReveal, + voluntaryExits: fullBlock.body.voluntaryExits, + blsToExecutionChanges: [], + syncAggregate: fullBlock.body.syncAggregate, + }), + } as unknown as Awaited>); // check if expectedFeeRecipient is passed to produceBlock await api.produceBlockV3({slot, randaoReveal, graffiti, feeRecipient}); @@ -270,8 +280,23 @@ describe("api/validator - produceBlockV3", () => { graffiti: toGraffitiBytes(graffiti), }); + // produceBlockBody now reads all inputs from the prepared scalars (no BeaconState). These mirror the + // values produceBlockBase would derive for this bellatrix state. + const preparedScalars = { + safeBlockHash: ZERO_HASH_HEX, + finalizedBlockHash: ZERO_HASH_HEX, + timestamp: computeTimeAtSlot(modules.config, state.slot, state.genesisTime), + prevRandao: new Uint8Array(32), + parentBlockHash: new Uint8Array(32), + parentGasLimit: 0, + isBuildingOnFull: false, + parentExecutionRequests: ssz.gloas.ExecutionRequests.defaultValue(), + payloadAttestations: [], + withdrawals: undefined, + }; + // use fee recipient passed in produceBlockBody call for payload gen in engine notifyForkchoiceUpdate - await produceBlockBody.call(modules.chain as unknown as BeaconChain, BlockType.Full, state, { + await produceBlockBody.call(modules.chain as unknown as BeaconChain, BlockType.Full, { randaoReveal, graffiti: toGraffitiBytes(graffiti), slot, @@ -280,6 +305,7 @@ describe("api/validator - produceBlockV3", () => { proposerIndex: 0, proposerPubKey: new Uint8Array(32).fill(1), commonBlockBodyPromise: createCommonBlockBodyPromise(), + ...preparedScalars, }); expect(modules.chain["executionEngine"].notifyForkchoiceUpdate).toBeCalledWith( @@ -297,7 +323,7 @@ describe("api/validator - produceBlockV3", () => { // use fee recipient set in beaconProposerCacheStub if none passed modules.chain["beaconProposerCache"].getOrDefault.mockReturnValue("0x fee recipient address"); - await produceBlockBody.call(modules.chain as unknown as BeaconChain, BlockType.Full, state, { + await produceBlockBody.call(modules.chain as unknown as BeaconChain, BlockType.Full, { randaoReveal, graffiti: toGraffitiBytes(graffiti), slot, @@ -305,6 +331,7 @@ describe("api/validator - produceBlockV3", () => { proposerIndex: 0, proposerPubKey: new Uint8Array(32).fill(1), commonBlockBodyPromise: createCommonBlockBodyPromise(), + ...preparedScalars, }); expect(modules.chain["executionEngine"].notifyForkchoiceUpdate).toBeCalledWith( From 207b272e882b99d7b706a7c5c325678d6fbe7d18 Mon Sep 17 00:00:00 2001 From: twoeths Date: Thu, 25 Jun 2026 13:53:57 +0700 Subject: [PATCH 09/24] feat: BeaconEngine.computeNewStateRoot() --- .../src/chain/beaconEngine/beaconEngine.ts | 30 +++++++++++++++++++ .../computeNewStateRoot.ts | 0 .../src/chain/beaconEngine/interface.ts | 9 ++++++ packages/beacon-node/src/chain/chain.ts | 20 +++++-------- packages/beacon-node/src/chain/interface.ts | 1 - .../src/chain/produceBlock/index.ts | 1 - 6 files changed, 46 insertions(+), 15 deletions(-) rename packages/beacon-node/src/chain/{produceBlock => beaconEngine}/computeNewStateRoot.ts (100%) diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 62eebbb663c5..bb913452744c 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -45,8 +45,10 @@ import { Attestation, BLSSignature, BeaconBlock, + BlindedBeaconBlock, Bytes32, Epoch, + Gwei, IndexedAttestation, Root, RootHex, @@ -152,6 +154,7 @@ import {validateGossipProposerPreferences} from "../validation/proposerPreferenc import {validateApiSyncCommittee, validateGossipSyncCommittee} from "../validation/syncCommittee.js"; import {validateSyncCommitteeGossipContributionAndProof} from "../validation/syncCommitteeContributionAndProof.js"; import {ValidatorMonitor} from "../validatorMonitor.js"; +import {computeNewStateRoot} from "./computeNewStateRoot.js"; import {GossipValidationResult, fromResult, runGossipValidation} from "./gossipValidationResult.js"; import {BeaconEngineModules, IBeaconEngine, ImportBlockResult, ProduceBlockBaseResult} from "./interface.js"; import {IBeaconEngineOptions} from "./options.js"; @@ -418,6 +421,8 @@ export class BeaconEngine implements IBeaconEngine { * here, so callers that need cache-only (DB-write-pending) envelopes check the cache first * (`chain.getParentExecutionRequests`). At `produceBlockBase` time the parent is FULL and its * envelope's async DB write has normally completed. + * TODO - beacon engine: here BeaconEngine owns the payloadEnvelope db because we need state transition for block production + * see if we have any down sides with this */ async getParentExecutionRequests( parentBlockSlot: Slot, @@ -574,6 +579,31 @@ export class BeaconEngine implements IBeaconEngine { return this.assembleCommonBlockBody(BlockType.Full, state, blockAttributes); } + /** + * Compute the post-state root (and proposer reward) for a produced block. Resolves the parent + * `ProtoBlock` from `block.parentRoot` and regens the block-slot state internally (cache hit after + * `produceBlockBase`), so the facade passes no `BeaconState`. The JS engine uses the `block` POJO and + * ignores `_blockBytes` / `_blinded` (these feed the native engine's bytes-first deserialize). + */ + async computeNewStateRoot( + block: BeaconBlock | BlindedBeaconBlock, + _blockBytes: Uint8Array, + _blinded: boolean + ): Promise<{newStateRoot: Root; proposerReward: Gwei}> { + const parentBlock = this.forkChoice.getBlockDefaultStatus(block.parentRoot); + if (parentBlock === null) { + throw Error(`Parent block not found for computeNewStateRoot root=${toRootHex(block.parentRoot)}`); + } + const state = await this.regen.getBlockSlotState( + parentBlock, + block.slot, + {dontTransferCache: true}, + RegenCaller.produceBlock + ); + const {newStateRoot, proposerReward} = computeNewStateRoot(this.metrics, state, block); + return {newStateRoot, proposerReward}; + } + private assembleCommonBlockBody( blockType: BlockType, currentState: IBeaconStateView, diff --git a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts b/packages/beacon-node/src/chain/beaconEngine/computeNewStateRoot.ts similarity index 100% rename from packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts rename to packages/beacon-node/src/chain/beaconEngine/computeNewStateRoot.ts diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 5128e57600f0..bd3d3fe32311 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -5,7 +5,10 @@ import {DataAvailabilityStatus, IBeaconStateView, PubkeyCache} from "@lodestar/s import { BLSPubkey, BLSSignature, + BeaconBlock, + BlindedBeaconBlock, Bytes32, + Gwei, Root, RootHex, SignedAggregateAndProof, @@ -144,6 +147,12 @@ export interface IBeaconEngine { ): Promise; produceCommonBlockBody(blockAttributes: BlockAttributes): Promise; produceBlockBase(attrs: {slot: Slot; randaoReveal: BLSSignature; graffiti: Bytes32}): Promise; + // Compute the post-state root + proposer reward for a produced block. + computeNewStateRoot( + block: BeaconBlock | BlindedBeaconBlock, + blockBytes: Uint8Array, + blinded: boolean + ): Promise<{newStateRoot: Root; proposerReward: Gwei}>; // Gossip validation flows. The first parameter is the message's SSZ bytes (unused by the JS engine, // required by the native engine's bytes-first contract), followed by the deserialized object. Each diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 632d56012b10..e600c91de6fc 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -92,7 +92,6 @@ import { } from "./opPools/index.js"; import {IChainOptions} from "./options.js"; import {PrepareNextSlotScheduler} from "./prepareNextSlot.js"; -import {computeNewStateRoot} from "./produceBlock/computeNewStateRoot.js"; import {AssembledBlockType, BlockType, ProduceResult} from "./produceBlock/index.js"; import {BlockAttributes, PreparedBlockScalars, produceBlockBody} from "./produceBlock/produceBlockBody.js"; import {QueuedStateRegenerator, RegenCaller} from "./regen/index.js"; @@ -1010,10 +1009,6 @@ export class BeaconChain implements IBeaconChain { return sidecarsFinalized; } - async produceCommonBlockBody(blockAttributes: BlockAttributes): Promise { - return this.beaconEngine.produceCommonBlockBody(blockAttributes); - } - produceBlock( blockAttributes: BlockAttributes & { commonBlockBodyPromise: Promise; @@ -1117,14 +1112,13 @@ export class BeaconChain implements IBeaconChain { body, } as AssembledBlockType; - // TODO - beacon engine: remove this - const state = await this.regen.getBlockSlotState( - parentBlock, - slot, - {dontTransferCache: true}, - RegenCaller.produceBlock - ); - const {newStateRoot, proposerReward} = computeNewStateRoot(this.metrics, state, block); + // Serialize the (zero-stateRoot) block for the bytes-first seam. The JS engine uses the POJO; the + // serialized bytes + `blinded` flag are for the future native engine. + const blinded = produceResult.type === BlockType.Blinded; + const blockBytes = blinded + ? this.config.getPostBellatrixForkTypes(slot).BlindedBeaconBlock.serialize(block as BlindedBeaconBlock) + : this.config.getForkTypes(slot).BeaconBlock.serialize(block as BeaconBlock); + const {newStateRoot, proposerReward} = await this.beaconEngine.computeNewStateRoot(block, blockBytes, blinded); block.stateRoot = newStateRoot; const blockRoot = produceResult.type === BlockType.Full diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 0843c058c256..99a88fe693ec 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -240,7 +240,6 @@ export interface IBeaconChain { ): Promise; getParentExecutionRequests(parentBlockSlot: Slot, parentBlockRootHex: RootHex): Promise; - produceCommonBlockBody(blockAttributes: BlockAttributes): Promise; produceBlock(blockAttributes: BlockAttributes & {commonBlockBodyPromise: Promise}): Promise<{ block: BeaconBlock; executionPayloadValue: Wei; diff --git a/packages/beacon-node/src/chain/produceBlock/index.ts b/packages/beacon-node/src/chain/produceBlock/index.ts index 8e637ecb8fd6..541fb04fab24 100644 --- a/packages/beacon-node/src/chain/produceBlock/index.ts +++ b/packages/beacon-node/src/chain/produceBlock/index.ts @@ -1,2 +1 @@ -export * from "./computeNewStateRoot.js"; export * from "./produceBlockBody.js"; From 85cf31e9351b5e90e14d6305fbbc900c8419af83 Mon Sep 17 00:00:00 2001 From: twoeths Date: Fri, 26 Jun 2026 14:11:03 +0700 Subject: [PATCH 10/24] feat: implement duties apis in BeaconEngine --- .../src/api/impl/beacon/state/index.ts | 4 +- .../src/api/impl/beacon/state/utils.ts | 1 + .../src/api/impl/validator/index.ts | 301 ++---------------- .../src/api/impl/validator/utils.ts | 34 +- .../src/chain/beaconEngine/beaconEngine.ts | 261 ++++++++++++++- .../src/chain/beaconEngine/duties.ts | 29 ++ .../src/chain/beaconEngine/interface.ts | 31 ++ packages/beacon-node/src/chain/chain.ts | 8 +- packages/beacon-node/src/chain/interface.ts | 2 - .../test/mocks/mockedBeaconChain.ts | 16 + .../spec/presets/fast_confirmation.test.ts | 7 +- .../impl/validator/duties/proposer.test.ts | 25 +- 12 files changed, 388 insertions(+), 331 deletions(-) create mode 100644 packages/beacon-node/src/chain/beaconEngine/duties.ts diff --git a/packages/beacon-node/src/api/impl/beacon/state/index.ts b/packages/beacon-node/src/api/impl/beacon/state/index.ts index 85bb748e417b..2bd60136f711 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/index.ts @@ -28,6 +28,7 @@ export function getBeaconStateApi({ async function getState( stateId: routes.beacon.StateId ): Promise<{state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean}> { + // TODO - beacon engine: this api will be dropped for BeaconEngine const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, stateId); return { @@ -253,7 +254,8 @@ export function getBeaconStateApi({ } const decisionRoot = state.getShufflingDecisionRoot(epoch); - const shuffling = await chain.shufflingCache.get(epoch, decisionRoot); + // TODO - engine: should not access shufflingCache, directly. It belongs to BeaconEngine + const shuffling = await chain.beaconEngine.shufflingCache.get(epoch, decisionRoot); if (!shuffling) { throw new ApiError( 500, diff --git a/packages/beacon-node/src/api/impl/beacon/state/utils.ts b/packages/beacon-node/src/api/impl/beacon/state/utils.ts index 0d1afa98e59e..5e65aabfd2a0 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/utils.ts @@ -40,6 +40,7 @@ export function resolveStateId( return blockSlot; } +// TODO - beacon engine: move it over there. Do not support returning the whole IBeaconStateView. export async function getStateResponseWithRegen( chain: IBeaconChain, inStateId: routes.beacon.StateId diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 0bb8cef3ee3a..77d0888b01be 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -7,9 +7,7 @@ import { ForkPostBellatrix, ForkPreGloas, ForkSeq, - GENESIS_SLOT, SLOTS_PER_EPOCH, - SLOTS_PER_HISTORICAL_ROOT, SYNC_COMMITTEE_SUBNET_SIZE, isForkPostBellatrix, isForkPostDeneb, @@ -19,16 +17,11 @@ import { } from "@lodestar/params"; import { DataAvailabilityStatus, - IBeaconStateView, beaconBlockToBlinded, - calculateCommitteeAssignments, computeEpochAtSlot, computeStartSlotAtEpoch, computeTimeAtSlot, getCurrentSlot, - isStatePostAltair, - isStatePostGloas, - proposerShufflingDecisionRoot, } from "@lodestar/state-transition"; import { BLSSignature, @@ -41,12 +34,10 @@ import { ProducedBlockSource, Root, Slot, - ValidatorIndex, Wei, bellatrix, getValidatorStatus, gloas, - phase0, ssz, } from "@lodestar/types"; import { @@ -62,7 +53,7 @@ import {MAX_BUILDER_BOOST_FACTOR} from "@lodestar/validator"; import {GossipValidationStatus} from "../../../chain/beaconEngine/gossipValidationResult.js"; import {BlockInputSource} from "../../../chain/blocks/blockInput/types.js"; import {AttestationErrorCode, SyncCommitteeErrorCode} from "../../../chain/errors/index.js"; -import {ChainEvent, CommonBlockBody} from "../../../chain/index.js"; +import {CommonBlockBody} from "../../../chain/index.js"; import {PREPARE_NEXT_SLOT_BPS} from "../../../chain/prepareNextSlot.js"; import { BlockType, @@ -71,8 +62,6 @@ import { ProduceFullGloas, } from "../../../chain/produceBlock/index.js"; import {RegenCaller} from "../../../chain/regen/index.js"; -import {CheckpointHex} from "../../../chain/stateCache/types.js"; -import {ZERO_HASH} from "../../../constants/index.js"; import {BuilderStatus, NoBidReceived} from "../../../execution/builder/http.js"; import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js"; import {CommitteeSubscription} from "../../../network/subnets/index.js"; @@ -81,10 +70,9 @@ import {isOptimisticBlock} from "../../../util/forkChoice.js"; import {getDefaultGraffiti, toGraffitiBytes} from "../../../util/graffiti.js"; import {getLodestarClientVersion} from "../../../util/metadata.js"; import {ApiOptions} from "../../options.js"; -import {getStateResponseWithRegen} from "../beacon/state/utils.js"; import {ApiError, FailureList, IndexedError, NodeIsSyncing, OnlySupportedByDVT} from "../errors.js"; import {ApiModules} from "../types.js"; -import {computeSubnetForCommitteesAtSlot, getPubkeysForIndices, selectBlockProductionSource} from "./utils.js"; +import {computeSubnetForCommitteesAtSlot, selectBlockProductionSource} from "./utils.js"; /** * If the node is within this many epochs from the head, we declare it to be synced regardless of @@ -172,8 +160,6 @@ export function getValidatorApi( opts: ApiOptions, {chain, config, logger, metrics, network, sync}: ApiModules ): ApplicationMethods { - let genesisBlockRoot: Root | null = null; - /** * Validator clock may be advanced from beacon's clock. If the validator requests a resource in a * future slot, wait some time instead of rejecting the request because it's in the future. @@ -186,29 +172,6 @@ export function getValidatorApi( ); const MAX_API_CLOCK_DISPARITY_MS = MAX_API_CLOCK_DISPARITY_SEC * 1000; - /** Compute and cache the genesis block root */ - async function getGenesisBlockRoot(state: IBeaconStateView): Promise { - if (!genesisBlockRoot) { - // Close to genesis the genesis block may not be available in the DB - if (state.slot === GENESIS_SLOT) { - genesisBlockRoot = state.computeAnchorCheckpoint().checkpoint.root; - } else if (state.slot < SLOTS_PER_HISTORICAL_ROOT) { - genesisBlockRoot = state.getBlockRootAtSlot(GENESIS_SLOT); - } - - const blockRes = await chain.getCanonicalBlockAtSlot(GENESIS_SLOT); - if (blockRes) { - genesisBlockRoot = config - .getForkTypes(blockRes.block.message.slot) - .SignedBeaconBlock.hashTreeRoot(blockRes.block); - } - } - - // If for some reason the genesisBlockRoot is not able don't prevent validators from - // proposing or attesting. If the genesisBlockRoot is wrong, at worst it may trigger a re-fetch of the duties - return genesisBlockRoot || ZERO_HASH; - } - /** * If advancing the local clock `MAX_API_CLOCK_DISPARITY_MS` ticks to the requested slot, wait for its start * Prevents the validator from getting errors from the API if the clock is a bit advanced @@ -287,53 +250,6 @@ export function getValidatorApi( }; } - /** - * This function is called 1s before next epoch, usually at that time PrepareNextSlotScheduler finishes - * so we should have checkpoint state, otherwise wait for up to the slot 1 of epoch. - * slot epoch 0 1 - * |------------|------------| - * ^ ^ - * | | - * | | - * | waitForCheckpointState (1s before slot 0 of epoch, wait until slot 1 of epoch) - * | - * prepareNextSlot (4s before next slot) - */ - async function waitForCheckpointState(cpHex: CheckpointHex): Promise { - const cpState = chain.regen.getCheckpointStateSync(cpHex); - if (cpState) { - return cpState; - } - const cp = { - epoch: cpHex.epoch, - root: fromHex(cpHex.rootHex), - }; - const slot0 = computeStartSlotAtEpoch(cp.epoch); - // if not, wait for ChainEvent.checkpoint event until slot 1 of epoch - let listener: ((eventCp: phase0.Checkpoint) => void) | null = null; - const foundCPState = await Promise.race([ - new Promise((resolve) => { - listener = (eventCp) => { - resolve(ssz.phase0.Checkpoint.equals(eventCp, cp)); - }; - chain.emitter.once(ChainEvent.checkpoint, listener); - }), - // in rare case, checkpoint state cache may happen up to 6s of slot 0 of epoch - // so we wait for it until the slot 1 of epoch - chain.clock.waitForSlot(slot0 + 1), - ]); - - if (listener != null) { - chain.emitter.off(ChainEvent.checkpoint, listener); - } - - if (foundCPState === true) { - return chain.regen.getCheckpointStateSync(cpHex); - } - - return null; - } - /** * Reject any request while the node is syncing */ @@ -1191,116 +1107,24 @@ export function getValidatorApi( ); } - const head = chain.forkChoice.getHead(); - let state: IBeaconStateView | undefined = undefined; - // validators may request next epoch's duties when it's close to next epoch - // this is to avoid missed block proposal due to 0 epoch look ahead. - // Post-Fulu, `nextEpoch + 1` is served from the same upcoming-epoch (`nextEpoch`) - // checkpoint state via its `nextProposers` (deterministic proposer lookahead). - if (nearNextEpoch && (epoch === nextEpoch || (isPostFulu && epoch === nextEpoch + 1))) { - // wait for maximum 1 slot for cp state which is the timeout of validator api - const cpState = await waitForCheckpointState({ - rootHex: head.blockRoot, - epoch: nextEpoch, - }); - if (cpState) { - state = cpState; - metrics?.duties.requestNextEpochProposalDutiesHit.inc(); - } else { - metrics?.duties.requestNextEpochProposalDutiesMiss.inc(); - } - } - - if (!state) { - if (epoch >= currentEpoch - 1) { - // Cached beacon state stores proposers for previous, current and next epoch. The - // requested epoch is within that range, we can use the head state at current epoch - state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.getDuties); - } else { - const res = await getStateResponseWithRegen(chain, startSlot); - - state = res.state instanceof Uint8Array ? chain.getHeadState().loadOtherState(res.state) : res.state; - - if (state.epoch !== epoch) { - throw Error(`Loaded state epoch ${state.epoch} does not match requested epoch ${epoch}`); - } - } - } - - const stateEpoch = state.epoch; - let indexes: ValidatorIndex[] = []; - - switch (epoch) { - case stateEpoch: - indexes = state.currentProposers; - break; - - case stateEpoch + 1: { - // make sure shuffling is calculated and ready for next call to calculate nextProposers - const nextEpoch = state.epoch + 1; - await chain.shufflingCache.get(nextEpoch, state.nextDecisionRoot); - // Requesting duties for next epoch is allowed since they can be predicted with high probabilities. - // @see `epochCtx.getBeaconProposersNextEpoch` JSDocs for rationale. - indexes = state.nextProposers; - break; - } - - case stateEpoch - 1: { - const indexesPrevEpoch = state.previousProposers; - if (indexesPrevEpoch === null) { - // Should not happen as previous proposer duties should be initialized for head state - // and if we load state from `Uint8Array` it will always be the state of requested epoch - throw Error(`Proposer duties for previous epoch ${epoch} not yet initialized`); - } - indexes = indexesPrevEpoch; - break; - } + // near the boundary, let the engine wait briefly for the next-epoch checkpoint state so the + // upcoming epoch's duties can be served early. The clock deadline (wait until slot 1 of the next + // epoch, the validator-api timeout) is passed in as a relative duration — the engine holds no clock. + const checkpointWaitTimeoutMs = + nearNextEpoch && (epoch === nextEpoch || (isPostFulu && epoch === nextEpoch + 1)) + ? computeTimeAtSlot(config, computeStartSlotAtEpoch(nextEpoch) + 1, chain.genesisTime) * 1000 - Date.now() + : undefined; - default: - // Should never happen, epoch is checked to be in bounds above - throw Error(`Proposer duties for epoch ${epoch} not supported, current epoch ${stateEpoch}`); - } - - // NOTE: this is the fastest way of getting compressed pubkeys. - // See benchmark -> packages/lodestar/test/perf/api/impl/validator/attester.test.ts - // After dropping the flat caches attached to the CachedBeaconState it's no longer available. - // TODO: Add a flag to just send 0x00 as pubkeys since the Lodestar validator does not need them. - const pubkeys = getPubkeysForIndices(state, indexes); - - const duties: routes.validator.ProposerDuty[] = []; - for (let i = 0; i < SLOTS_PER_EPOCH; i++) { - duties.push({slot: startSlot + i, validatorIndex: indexes[i], pubkey: pubkeys[i]}); - } - - // In v2 the dependent root is different after fulu due to deterministic proposer lookahead - let dependentRoot = proposerShufflingDecisionRoot( - opts?.v2 ? config.getForkName(startSlot) : ForkName.phase0, - state, - epoch - ); - const logCtx = { + const {data, dependentRoot, head} = await chain.beaconEngine.getProposerDuties( epoch, - stateSlot: state.slot, - stateEpoch: state.epoch, - v2: opts?.v2 ?? false, - }; - if (dependentRoot === null) { - // fallback to get_proposer_duties() v1, also in lodestar v1.43 - logger.verbose("Proposer duties decision root not in state, falling back to state epoch", logCtx); - dependentRoot = proposerShufflingDecisionRoot(ForkName.phase0, state, state.epoch); - } - if (dependentRoot === null) { - logger.verbose("Proposer duties decision root not in state, falling back to genesis block root", logCtx); - dependentRoot = await getGenesisBlockRoot(state); - } - - const dependentRootHex = toRootHex(dependentRoot); - logger.verbose("Computed proposer duties decision root", {...logCtx, dependentRoot: dependentRootHex}); + {currentEpoch, checkpointWaitTimeoutMs}, + opts?.v2 ?? false + ); return { - data: duties, + data, meta: { - dependentRoot: dependentRootHex, + dependentRoot: toRootHex(dependentRoot), executionOptimistic: isOptimisticBlock(head), }, }; @@ -1326,43 +1150,14 @@ export function getValidatorApi( throw new ApiError(400, "Cannot get duties for epoch more than one ahead"); } - const head = chain.forkChoice.getHead(); - const state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.getDuties); - - // TODO: Determine what the current epoch would be if we fast-forward our system clock by - // `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. - // - // Most of the time, `tolerantCurrentEpoch` will be equal to `currentEpoch`. However, during - // the first `MAXIMUM_GOSSIP_CLOCK_DISPARITY` duration of the epoch `tolerantCurrentEpoch` - // will equal `currentEpoch + 1` - - // Check that all validatorIndex belong to the state before calling getCommitteeAssignments() - const pubkeys = getPubkeysForIndices(state, indices); - const decisionRoot = state.getShufflingDecisionRoot(epoch); - const shuffling = await chain.shufflingCache.get(epoch, decisionRoot); - if (!shuffling) { - throw new ApiError( - 500, - `No shuffling found to calculate committee assignments for epoch: ${epoch} and decisionRoot: ${decisionRoot}` - ); - } - const committeeAssignments = calculateCommitteeAssignments(shuffling, indices); - const duties: routes.validator.AttesterDuty[] = []; - for (let i = 0, len = indices.length; i < len; i++) { - const validatorIndex = indices[i]; - const duty = committeeAssignments.get(validatorIndex) as routes.validator.AttesterDuty | undefined; - if (duty) { - // Mutate existing object instead of re-creating another new object with spread operator - // Should be faster and require less memory - duty.pubkey = pubkeys[i]; - duties.push(duty); - } - } - - const dependentRoot = fromHex(state.getShufflingDecisionRoot(epoch)) || (await getGenesisBlockRoot(state)); + const {data, dependentRoot, head} = await chain.beaconEngine.getAttesterDuties( + epoch, + indices, + chain.clock.currentEpoch + ); return { - data: duties, + data, meta: { dependentRoot: toRootHex(dependentRoot), executionOptimistic: isOptimisticBlock(head), @@ -1389,29 +1184,14 @@ export function getValidatorApi( throw new ApiError(400, "Cannot get PTC duties for epoch more than one ahead"); } - const head = chain.forkChoice.getHead(); - const state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.getDuties); - if (!isStatePostGloas(state)) { - throw new ApiError(400, `PTC duties are not available before Gloas fork=${state.forkName}`); - } - - const pubkeys = getPubkeysForIndices(state, indices); - const ptcs = state.getEpochPTCs(epoch); - const duties: routes.validator.PtcDuty[] = []; - for (let i = 0, len = indices.length; i < len; i++) { - const validatorIndex = indices[i]; - for (let j = 0; j < SLOTS_PER_EPOCH; j++) { - if (ptcs[j].indexOf(validatorIndex) !== -1) { - duties.push({pubkey: pubkeys[i], validatorIndex, slot: j + startSlot}); - break; - } - } - } - - const dependentRoot = fromHex(state.getShufflingDecisionRoot(epoch)) || (await getGenesisBlockRoot(state)); + const {data, dependentRoot, head} = await chain.beaconEngine.getPtcDuties( + epoch, + indices, + chain.clock.currentEpoch + ); return { - data: duties, + data, meta: { dependentRoot: toRootHex(dependentRoot), executionOptimistic: isOptimisticBlock(head), @@ -1448,33 +1228,10 @@ export function getValidatorApi( // sync committee duties have a lookahead of 1 day. Assuming the validator only requests duties for upcoming // epochs, the head state will very likely have the duties available for the requested epoch. // Note: does not support requesting past duties - const head = chain.forkChoice.getHead(); - const state = chain.getHeadState(); - if (!isStatePostAltair(state)) { - throw new ApiError(400, "Sync committee duties are not available before Altair"); - } - - // Check that all validatorIndex belong to the state before calling getCommitteeAssignments() - const pubkeys = getPubkeysForIndices(state, indices); - // Ensures `epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD <= current_epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + 1` - const syncCommitteeCache = state.getIndexedSyncCommitteeAtEpoch(epoch); - const validatorSyncCommitteeIndexMap = syncCommitteeCache.validatorIndexMap; - - const duties: routes.validator.SyncDuty[] = []; - for (let i = 0, len = indices.length; i < len; i++) { - const validatorIndex = indices[i]; - const validatorSyncCommitteeIndices = validatorSyncCommitteeIndexMap.get(validatorIndex); - if (validatorSyncCommitteeIndices) { - duties.push({ - pubkey: pubkeys[i], - validatorIndex, - validatorSyncCommitteeIndices, - }); - } - } + const {data, head} = chain.beaconEngine.getSyncCommitteeDuties(epoch, indices); return { - data: duties, + data, meta: {executionOptimistic: isOptimisticBlock(head)}, }; }, diff --git a/packages/beacon-node/src/api/impl/validator/utils.ts b/packages/beacon-node/src/api/impl/validator/utils.ts index 81717fcbfd50..2620a55dcf12 100644 --- a/packages/beacon-node/src/api/impl/validator/utils.ts +++ b/packages/beacon-node/src/api/impl/validator/utils.ts @@ -1,10 +1,13 @@ import {routes} from "@lodestar/api"; import {ATTESTATION_SUBNET_COUNT} from "@lodestar/params"; -import {IBeaconStateView, computeSlotsSinceEpochStart} from "@lodestar/state-transition"; -import {BLSPubkey, CommitteeIndex, ProducedBlockSource, Slot, SubnetID, ValidatorIndex} from "@lodestar/types"; +import {computeSlotsSinceEpochStart} from "@lodestar/state-transition"; +import {CommitteeIndex, ProducedBlockSource, Slot, SubnetID} from "@lodestar/types"; import {MAX_BUILDER_BOOST_FACTOR} from "@lodestar/validator"; import {BlockSelectionResult, BuilderBlockSelectionReason, EngineBlockSelectionReason} from "./index.js"; +// Pubkey precompute lives with the duty flows in the engine now; re-exported here for existing callers/tests. +export {getPubkeysForIndices} from "../../../chain/beaconEngine/duties.js"; + export function computeSubnetForCommitteesAtSlot( slot: Slot, committeesAtSlot: number, @@ -15,33 +18,6 @@ export function computeSubnetForCommitteesAtSlot( return (committeesSinceEpochStart + committeeIndex) % ATTESTATION_SUBNET_COUNT; } -/** - * Precompute all pubkeys for given `validatorIndices`. Ensures that all `validatorIndices` are known - * before doing other expensive logic. - * - * Uses special BranchNodeStruct state.validators data structure to optimize getting pubkeys. - * Type-unsafe: assumes state.validators[i] is of BranchNodeStruct type. - * Note: This is the fastest way of getting compressed pubkeys. - * See benchmark -> packages/beacon-node/test/perf/api/impl/validator/attester.test.ts - */ -export function getPubkeysForIndices(state: IBeaconStateView, indexes: ValidatorIndex[]): BLSPubkey[] { - const validatorsLen = state.validatorCount; // Get once, it's expensive - - const pubkeys: BLSPubkey[] = []; - for (let i = 0, len = indexes.length; i < len; i++) { - const index = indexes[i]; - if (index >= validatorsLen) { - throw Error(`validatorIndex ${index} too high. Current validator count ${validatorsLen}`); - } - - // NOTE: This could be optimized further by traversing the tree optimally with .getNodes() - const validator = state.getValidator(index); - pubkeys.push(validator.pubkey); - } - - return pubkeys; -} - export function selectBlockProductionSource({ builderSelection, engineExecutionPayloadValue, diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index bb913452744c..3500af3768e2 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -24,6 +24,7 @@ import { GENESIS_SLOT, MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH, + SLOTS_PER_HISTORICAL_ROOT, isForkPostGloas, } from "@lodestar/params"; import { @@ -33,6 +34,7 @@ import { IBeaconStateView, PubkeyCache, RootCache, + calculateCommitteeAssignments, computeEpochAtSlot, computeStartSlotAtEpoch, computeTimeAtSlot, @@ -40,6 +42,7 @@ import { isStatePostBellatrix, isStatePostCapella, isStatePostGloas, + proposerShufflingDecisionRoot, } from "@lodestar/state-transition"; import { Attestation, @@ -66,7 +69,7 @@ import { phase0, ssz, } from "@lodestar/types"; -import {Logger, fromHex, toRootHex} from "@lodestar/utils"; +import {Logger, fromHex, sleep, toRootHex} from "@lodestar/utils"; import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js"; import {IBeaconDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; @@ -121,7 +124,7 @@ import {SeenAttestationDatas} from "../seenCache/seenAttestationData.js"; import {ShufflingCache} from "../shufflingCache.js"; import {FIFOBlockStateCache} from "../stateCache/fifoBlockStateCache.js"; import {PersistentCheckpointStateCache, toCheckpointHex} from "../stateCache/persistentCheckpointsCache.js"; -import {BlockStateCache, CheckpointStateCache} from "../stateCache/types.js"; +import {BlockStateCache, CheckpointHex, CheckpointStateCache} from "../stateCache/types.js"; import { AggregateAndProofValidationResult, validateApiAggregateAndProof, @@ -155,6 +158,7 @@ import {validateApiSyncCommittee, validateGossipSyncCommittee} from "../validati import {validateSyncCommitteeGossipContributionAndProof} from "../validation/syncCommitteeContributionAndProof.js"; import {ValidatorMonitor} from "../validatorMonitor.js"; import {computeNewStateRoot} from "./computeNewStateRoot.js"; +import {getPubkeysForIndices} from "./duties.js"; import {GossipValidationResult, fromResult, runGossipValidation} from "./gossipValidationResult.js"; import {BeaconEngineModules, IBeaconEngine, ImportBlockResult, ProduceBlockBaseResult} from "./interface.js"; import {IBeaconEngineOptions} from "./options.js"; @@ -222,6 +226,8 @@ export class BeaconEngine implements IBeaconEngine { lightClientServer: LightClientServer | undefined; private readonly verifiedBlocks = new Map(); private readonly emitter: ChainEventEmitter; + // Genesis block root is invariant; computed lazily from state for the genesis-shuffling duty fallback. + private cachedGenesisBlockRoot: Root | null = null; constructor(modules: BeaconEngineModules, anchorState: IBeaconStateView) { const { @@ -604,6 +610,257 @@ export class BeaconEngine implements IBeaconEngine { return {newStateRoot, proposerReward}; } + /** + * Proposer duties for `epoch`. + */ + async getProposerDuties( + epoch: Epoch, + {currentEpoch, checkpointWaitTimeoutMs}: {currentEpoch: Epoch; checkpointWaitTimeoutMs?: number}, + v2: boolean + ): Promise<{data: routes.validator.ProposerDuty[]; dependentRoot: Root; head: ProtoBlock}> { + const startSlot = computeStartSlotAtEpoch(epoch); + const head = this.forkChoice.getHead(); + + let state: IBeaconStateView | undefined; + if (checkpointWaitTimeoutMs !== undefined) { + const cpState = await this.waitForCheckpointState( + {rootHex: head.blockRoot, epoch: currentEpoch + 1}, + checkpointWaitTimeoutMs + ); + if (cpState) { + state = cpState; + this.metrics?.duties.requestNextEpochProposalDutiesHit.inc(); + } else { + this.metrics?.duties.requestNextEpochProposalDutiesMiss.inc(); + } + } + if (!state) { + if (epoch >= currentEpoch - 1) { + // TODO - beacon engine: not sure if we need to do this, just conform to the current unstable behavior for now + state = await this.getHeadStateAtEpoch(currentEpoch, RegenCaller.getDuties); + } else { + // TODO - beacon engine: currently unstable call getStateResponseWithRegen() + // the engine (Phase 5). Throw for now; serve it here once the engine owns the archive. + throw Error(`Proposer duties for past epoch ${epoch} not supported yet (engine has no archive access)`); + } + } + + const stateEpoch = state.epoch; + let indexes: ValidatorIndex[]; + switch (epoch) { + case stateEpoch: + indexes = state.currentProposers; + break; + + case stateEpoch + 1: + // make sure shuffling is calculated and ready for the call to calculate nextProposers + await this.shufflingCache.get(stateEpoch + 1, state.nextDecisionRoot); + indexes = state.nextProposers; + break; + + case stateEpoch - 1: { + const indexesPrevEpoch = state.previousProposers; + if (indexesPrevEpoch === null) { + // Should not happen as previous proposer duties should be initialized for head state + throw Error(`Proposer duties for previous epoch ${epoch} not yet initialized`); + } + indexes = indexesPrevEpoch; + break; + } + + default: + throw Error(`Proposer duties for epoch ${epoch} not supported, current epoch ${stateEpoch}`); + } + + const pubkeys = getPubkeysForIndices(state, indexes); + const data: routes.validator.ProposerDuty[] = []; + for (let i = 0; i < SLOTS_PER_EPOCH; i++) { + data.push({slot: startSlot + i, validatorIndex: indexes[i], pubkey: pubkeys[i]}); + } + + // In v2 the dependent root is different after fulu due to deterministic proposer lookahead + let dependentRoot = proposerShufflingDecisionRoot( + v2 ? this.config.getForkName(startSlot) : ForkName.phase0, + state, + epoch + ); + const logCtx = {epoch, stateSlot: state.slot, stateEpoch, v2}; + if (dependentRoot === null) { + // fallback to get_proposer_duties() v1, also in lodestar v1.43 + this.logger.verbose("Proposer duties decision root not in state, falling back to state epoch", logCtx); + dependentRoot = proposerShufflingDecisionRoot(ForkName.phase0, state, stateEpoch); + } + if (dependentRoot === null) { + this.logger.verbose("Proposer duties decision root not in state, falling back to genesis block root", logCtx); + dependentRoot = this.genesisBlockRoot(state); + } + this.logger.verbose("Computed proposer duties decision root", {...logCtx, dependentRoot: toRootHex(dependentRoot)}); + + return {data, dependentRoot, head}; + } + + /** Attester duties for `validatorIndices` at `epoch`. State resolved at `currentEpoch` (passed-in clock). */ + async getAttesterDuties( + epoch: Epoch, + indices: ValidatorIndex[], + currentEpoch: Epoch + ): Promise<{data: routes.validator.AttesterDuty[]; dependentRoot: Root; head: ProtoBlock}> { + const head = this.forkChoice.getHead(); + const state = await this.getHeadStateAtEpoch(currentEpoch, RegenCaller.getDuties); + + // Check that all validatorIndex belong to the state before calling getCommitteeAssignments() + const pubkeys = getPubkeysForIndices(state, indices); + const decisionRoot = state.getShufflingDecisionRoot(epoch); + const shuffling = await this.shufflingCache.get(epoch, decisionRoot); + if (!shuffling) { + throw Error( + `No shuffling found to calculate committee assignments for epoch: ${epoch} and decisionRoot: ${decisionRoot}` + ); + } + const committeeAssignments = calculateCommitteeAssignments(shuffling, indices); + const data: routes.validator.AttesterDuty[] = []; + for (let i = 0, len = indices.length; i < len; i++) { + const validatorIndex = indices[i]; + const duty = committeeAssignments.get(validatorIndex) as routes.validator.AttesterDuty | undefined; + if (duty) { + // Mutate existing object instead of re-creating another new object with spread operator + // Should be faster and require less memory + duty.pubkey = pubkeys[i]; + data.push(duty); + } + } + + const dependentRoot = fromHex(state.getShufflingDecisionRoot(epoch)) || this.genesisBlockRoot(state); + return {data, dependentRoot, head}; + } + + /** Sync committee duties for `validatorIndices` at `epoch`. Synchronous — served from head state. */ + getSyncCommitteeDuties( + epoch: Epoch, + indices: ValidatorIndex[] + ): {data: routes.validator.SyncDuty[]; head: ProtoBlock} { + const head = this.forkChoice.getHead(); + const state = this.getHeadState(); + if (!isStatePostAltair(state)) { + throw Error("Sync committee duties are not available before Altair"); + } + + // Check that all validatorIndex belong to the state before calling getCommitteeAssignments() + const pubkeys = getPubkeysForIndices(state, indices); + // Ensures `epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD <= current_epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + 1` + const validatorSyncCommitteeIndexMap = state.getIndexedSyncCommitteeAtEpoch(epoch).validatorIndexMap; + + const data: routes.validator.SyncDuty[] = []; + for (let i = 0, len = indices.length; i < len; i++) { + const validatorIndex = indices[i]; + const validatorSyncCommitteeIndices = validatorSyncCommitteeIndexMap.get(validatorIndex); + if (validatorSyncCommitteeIndices) { + data.push({pubkey: pubkeys[i], validatorIndex, validatorSyncCommitteeIndices}); + } + } + + return {data, head}; + } + + /** gloas: PTC duties for `validatorIndices` at `epoch`. State resolved at `currentEpoch` (passed-in clock). */ + async getPtcDuties( + epoch: Epoch, + indices: ValidatorIndex[], + currentEpoch: Epoch + ): Promise<{data: routes.validator.PtcDuty[]; dependentRoot: Root; head: ProtoBlock}> { + const startSlot = computeStartSlotAtEpoch(epoch); + const head = this.forkChoice.getHead(); + const state = await this.getHeadStateAtEpoch(currentEpoch, RegenCaller.getDuties); + if (!isStatePostGloas(state)) { + throw Error("PTC duties are not available before Gloas"); + } + + const pubkeys = getPubkeysForIndices(state, indices); + const ptcs = state.getEpochPTCs(epoch); + const data: routes.validator.PtcDuty[] = []; + for (let i = 0, len = indices.length; i < len; i++) { + const validatorIndex = indices[i]; + for (let j = 0; j < SLOTS_PER_EPOCH; j++) { + if (ptcs[j].indexOf(validatorIndex) !== -1) { + data.push({pubkey: pubkeys[i], validatorIndex, slot: j + startSlot}); + break; + } + } + } + + const dependentRoot = fromHex(state.getShufflingDecisionRoot(epoch)) || this.genesisBlockRoot(state); + return {data, dependentRoot, head}; + } + + /** + * Head state advanced to `epoch` (mirrors the former `chain.getHeadStateAtEpoch`; keeps `RegenCaller.getDuties`). + * No clock: the caller passes the target epoch. + */ + private async getHeadStateAtEpoch(epoch: Epoch, regenCaller: RegenCaller): Promise { + // using getHeadState() means we'll use checkpointStateCache if it's available + const headState = this.getHeadState(); + // head state is in the same epoch, or we pulled up head state already from past epoch + if (epoch <= computeEpochAtSlot(headState.slot)) { + // should go to this most of the time + return headState; + } + // only use regen queue if necessary, it'll cache in checkpointStateCache if regen gets through epoch transition + const head = this.forkChoice.getHead(); + const startSlot = computeStartSlotAtEpoch(epoch); + return this.regen.getBlockSlotState(head, startSlot, {dontTransferCache: true}, regenCaller); + } + + /** + * Wait briefly for the next-epoch checkpoint state to be computed by the prepare-next-slot scheduler so + * the upcoming epoch's proposer duties can be served slightly early. Regen-centric; clock-free — the + * `clock.waitForSlot` deadline is passed in as a relative `timeoutMs` (a plain timer, not a slot-clock). + */ + private async waitForCheckpointState(cpHex: CheckpointHex, timeoutMs: number): Promise { + const cpState = this.regen.getCheckpointStateSync(cpHex); + if (cpState) { + return cpState; + } + const cp = {epoch: cpHex.epoch, root: fromHex(cpHex.rootHex)}; + // if not, wait for ChainEvent.checkpoint event until the relative timeout elapses + let listener: ((eventCp: phase0.Checkpoint) => void) | null = null; + const foundCPState = await Promise.race([ + new Promise((resolve) => { + listener = (eventCp) => { + resolve(ssz.phase0.Checkpoint.equals(eventCp, cp)); + }; + this.emitter.once(ChainEvent.checkpoint, listener); + }), + sleep(timeoutMs), + ]); + + if (listener != null) { + this.emitter.off(ChainEvent.checkpoint, listener); + } + + if (foundCPState === true) { + return this.regen.getCheckpointStateSync(cpHex); + } + + return null; + } + + /** + * Compute and cache the genesis block root from internal state (genesis-shuffling duty fallback). + * Drops the former DB-block read; near genesis the state-derived root is authoritative, and if it is + * ever unavailable returning ZERO_HASH at worst triggers a duties re-fetch. + */ + private genesisBlockRoot(state: IBeaconStateView): Root { + if (!this.cachedGenesisBlockRoot) { + // Close to genesis the genesis block may not be available in the DB + if (state.slot === GENESIS_SLOT) { + this.cachedGenesisBlockRoot = state.computeAnchorCheckpoint().checkpoint.root; + } else if (state.slot < SLOTS_PER_HISTORICAL_ROOT) { + this.cachedGenesisBlockRoot = state.getBlockRootAtSlot(GENESIS_SLOT); + } + } + return this.cachedGenesisBlockRoot || ZERO_HASH; + } + private assembleCommonBlockBody( blockType: BlockType, currentState: IBeaconStateView, diff --git a/packages/beacon-node/src/chain/beaconEngine/duties.ts b/packages/beacon-node/src/chain/beaconEngine/duties.ts new file mode 100644 index 000000000000..c05f3c6c574d --- /dev/null +++ b/packages/beacon-node/src/chain/beaconEngine/duties.ts @@ -0,0 +1,29 @@ +import {IBeaconStateView} from "@lodestar/state-transition"; +import {BLSPubkey, ValidatorIndex} from "@lodestar/types"; + +/** + * Precompute all pubkeys for given `validatorIndices`. Ensures that all `validatorIndices` are known + * before doing other expensive logic. + * + * Uses special BranchNodeStruct state.validators data structure to optimize getting pubkeys. + * Type-unsafe: assumes state.validators[i] is of BranchNodeStruct type. + * Note: This is the fastest way of getting compressed pubkeys. + * See benchmark -> packages/beacon-node/test/perf/api/impl/validator/attester.test.ts + */ +export function getPubkeysForIndices(state: IBeaconStateView, indexes: ValidatorIndex[]): BLSPubkey[] { + const validatorsLen = state.validatorCount; // Get once, it's expensive + + const pubkeys: BLSPubkey[] = []; + for (let i = 0, len = indexes.length; i < len; i++) { + const index = indexes[i]; + if (index >= validatorsLen) { + throw Error(`validatorIndex ${index} too high. Current validator count ${validatorsLen}`); + } + + // NOTE: This could be optimized further by traversing the tree optimally with .getNodes() + const validator = state.getValidator(index); + pubkeys.push(validator.pubkey); + } + + return pubkeys; +} diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index bd3d3fe32311..920b6f864068 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -1,3 +1,4 @@ +import {routes} from "@lodestar/api"; import {BeaconConfig} from "@lodestar/config"; import {BlockExecutionStatus, IForkChoice, PayloadExecutionStatus, ProtoBlock} from "@lodestar/fork-choice"; import {ForkName} from "@lodestar/params"; @@ -8,6 +9,7 @@ import { BeaconBlock, BlindedBeaconBlock, Bytes32, + Epoch, Gwei, Root, RootHex, @@ -35,6 +37,7 @@ import {LightClientServer} from "../lightClient/index.js"; import {BlockProcessOpts} from "../options.js"; import {BlockAttributes, PayloadAttributesWithdrawals} from "../produceBlock/produceBlockBody.js"; import {SeenBlockInput} from "../seenCache/seenGossipBlockInput.js"; +import {ShufflingCache} from "../shufflingCache.js"; import {CPStateDatastore} from "../stateCache/datastore/types.js"; import {AggregateAndProofValidationResult} from "../validation/aggregateAndProof.js"; import {ApiAttestation, AttestationValidationResult, GossipAttestation} from "../validation/attestation.js"; @@ -135,6 +138,9 @@ export interface IBeaconEngine { // perform them still live facade-side. TODO - beacon engine: narrow to a read facet as those flows // (gossip → Phase 3, migrateFinalized/prune → Phase 5) move into the engine. readonly forkChoice: IForkChoice; + // TODO - beacon engine: the engine owns the shuffling cache; exposed for the committees API + // (`getEpochCommittees`) until that read becomes an engine method. + readonly shufflingCache: ShufflingCache; // Block production. `produceCommonBlockBody` builds the fork-agnostic body part (reused across the // self-build / builder-bid paths). `produceBlockBase` computes the shared head once (proposer head, @@ -154,6 +160,31 @@ export interface IBeaconEngine { blinded: boolean ): Promise<{newStateRoot: Root; proposerReward: Gwei}>; + // Validator duties. The engine resolves state internally and returns fully-populated duties (with + // pubkeys) + dependentRoot; no `IBeaconStateView` crosses. Clock-derived values are passed in (the + // engine holds no clock): `currentEpoch` for head-state resolution, and `checkpointWaitTimeoutMs` (a + // relative deadline, set only near the boundary) for the proposer next-epoch checkpoint wait. The + // past-epoch proposer state crosses as `pastStateBytes` (facade-owned archive until the DB phase). + getProposerDuties( + epoch: Epoch, + timing: {currentEpoch: Epoch; checkpointWaitTimeoutMs?: number}, + v2: boolean + ): Promise<{data: routes.validator.ProposerDuty[]; dependentRoot: Root; head: ProtoBlock}>; + getAttesterDuties( + epoch: Epoch, + indices: ValidatorIndex[], + currentEpoch: Epoch + ): Promise<{data: routes.validator.AttesterDuty[]; dependentRoot: Root; head: ProtoBlock}>; + getSyncCommitteeDuties( + epoch: Epoch, + indices: ValidatorIndex[] + ): {data: routes.validator.SyncDuty[]; head: ProtoBlock}; + getPtcDuties( + epoch: Epoch, + indices: ValidatorIndex[], + currentEpoch: Epoch + ): Promise<{data: routes.validator.PtcDuty[]; dependentRoot: Root; head: ProtoBlock}>; + // Gossip validation flows. The first parameter is the message's SSZ bytes (unused by the JS engine, // required by the native engine's bytes-first contract), followed by the deserialized object. Each // returns a `GossipValidationResult` (no throw) so the native engine can return outcomes across FFI. diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index e600c91de6fc..dc1dae87fc6f 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -112,7 +112,6 @@ import {SeenAggregatedAttestations} from "./seenCache/seenAggregateAndProof.js"; import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js"; import {SeenBlockAttesters} from "./seenCache/seenBlockAttesters.js"; import {SeenBlockInput} from "./seenCache/seenGossipBlockInput.js"; -import {ShufflingCache} from "./shufflingCache.js"; import {DbCPStateDatastore, checkpointToDatastoreKey} from "./stateCache/datastore/db.js"; import {FileCPStateDatastore} from "./stateCache/datastore/file.js"; import {CPStateDatastore} from "./stateCache/datastore/types.js"; @@ -257,11 +256,6 @@ export class BeaconChain implements IBeaconChain { return this.beaconEngine.checkpointBalancesCache; } - // TODO - beacon engine: remove this - get shufflingCache(): ShufflingCache { - return this.beaconEngine.shufflingCache; - } - /** * Cache produced results (ExecutionPayload, DA Data) from the local execution so that we can send * and get signed/published blinded versions which beacon node can @@ -1379,7 +1373,7 @@ export class BeaconChain implements IBeaconChain { private onCheckpoint(this: BeaconChain, _checkpoint: phase0.Checkpoint, state: IBeaconStateView): void { // Defer to not block other checkpoint event handlers, which can cause lightclient update delays callInNextEventLoop(() => { - this.shufflingCache.processState(state); + this.beaconEngine.shufflingCache.processState(state); }); } diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 99a88fe693ec..7b3e8402d240 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -70,7 +70,6 @@ import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js"; import {SeenBlockAttesters} from "./seenCache/seenBlockAttesters.js"; import {SeenBlockInput} from "./seenCache/seenGossipBlockInput.js"; import {PayloadEnvelopeInput, SeenPayloadEnvelopeInput} from "./seenCache/seenPayloadEnvelopeInput.js"; -import {ShufflingCache} from "./shufflingCache.js"; import {ValidatorMonitor} from "./validatorMonitor.js"; export {BlockType, type AssembledBlockType}; @@ -150,7 +149,6 @@ export interface IBeaconChain { readonly blockProductionCache: Map; - readonly shufflingCache: ShufflingCache; readonly blacklistedBlocks: Map; // Cache for serialized objects readonly serializedCache: SerializedCache; diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index 09816479889b..0de753f76b9a 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -4,6 +4,7 @@ import {config as defaultConfig} from "@lodestar/config/default"; import {EpochDifference, ForkChoice, ProtoBlock} from "@lodestar/fork-choice"; import {createPubkeyCache} from "@lodestar/state-transition"; import {Logger} from "@lodestar/utils"; +import {BeaconEngine} from "../../src/chain/beaconEngine/beaconEngine.js"; import {BeaconProposerCache} from "../../src/chain/beaconProposerCache.js"; import {BeaconChain} from "../../src/chain/chain.js"; import {ChainEventEmitter} from "../../src/chain/emitter.js"; @@ -213,6 +214,21 @@ export function getMockedBeaconChain(opts?: Partial): // The gossip validators are now BeaconEngine methods that read engine collaborators. In this flat // mock the facade doubles as the engine, so `chain.beaconEngine.forkChoice === chain.forkChoice` etc. (chain as {beaconEngine: unknown}).beaconEngine = chain; + // Duty flows are real BeaconEngine methods; bind the real implementations (and the private helpers + // they call) so duty tests exercise real logic against the mock's collaborators. + for (const name of [ + "getProposerDuties", + "getAttesterDuties", + "getSyncCommitteeDuties", + "getPtcDuties", + "getHeadStateAtEpoch", + "waitForCheckpointState", + "genesisBlockRoot", + ] as const) { + (chain as unknown as Record)[name] = ( + BeaconEngine.prototype as unknown as Record + )[name]; + } return chain; } 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 31017d1b6c25..717f4a634b1a 100644 --- a/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts +++ b/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts @@ -175,7 +175,12 @@ const fastConfirmationTest = const attEpoch = computeEpochAtSlot(attestation.data.slot); const decisionRoot = headState.getShufflingDecisionRoot(attEpoch); chain.beaconEngine.forkChoice.onAttestation( - chain.shufflingCache.getIndexedAttestation(attEpoch, decisionRoot, ForkSeq[fork], attestation), + chain.beaconEngine.shufflingCache.getIndexedAttestation( + attEpoch, + decisionRoot, + ForkSeq[fork], + attestation + ), attDataRootHex ); } diff --git a/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts b/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts index 82eb0485847c..700c030489a2 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts @@ -33,7 +33,7 @@ describe("get proposers api impl", () => { initializeState(currentSlot); - modules.chain.getHeadStateAtCurrentEpoch.mockResolvedValue(new BeaconStateView(cachedState)); + modules.chain.getHeadState.mockReturnValue(new BeaconStateView(cachedState)); modules.forkChoice.getHead.mockReturnValue(zeroProtoBlock); modules.forkChoice.getFinalizedBlock.mockReturnValue(zeroProtoBlock); modules.db.block.get.mockResolvedValue({message: {stateRoot: Buffer.alloc(32)}} as any); @@ -125,7 +125,8 @@ describe("get proposers api impl", () => { it("should get v1 proposers for next epoch from a mid-epoch head state", async () => { vi.advanceTimersByTime(15 * config.SLOT_DURATION_MS); initializeState(currentSlot + 15); - modules.chain.getHeadStateAtCurrentEpoch.mockResolvedValue(new BeaconStateView(cachedState)); + // engine resolves duty state via getHeadStateAtEpoch -> getHeadState (sync) + modules.chain.getHeadState.mockReturnValue(new BeaconStateView(cachedState)); const v1 = (await api.getProposerDuties({epoch: currentEpoch + 1})) as {meta: {dependentRoot: string}}; @@ -133,24 +134,14 @@ describe("get proposers api impl", () => { expect(v1.meta.dependentRoot).toBe(expected); }); - it("should get proposers for historical epoch", async () => { + // TODO - beacon engine: historical proposer duties (epochs < currentEpoch - 1) are temporarily + // unsupported — the engine has no archive access until the states/archive DB moves into it (Phase 5). + // Restore serving + this assertion then. + it("should throw for historical epoch (no engine archive access yet)", async () => { const historicalEpoch = currentEpoch - 2; initializeState(currentSlot - 2 * SLOTS_PER_EPOCH + 1); - modules.chain.getStateBySlot.mockResolvedValue({ - state: new BeaconStateView(cachedState), - executionOptimistic: false, - finalized: true, - }); - const {data: result} = (await api.getProposerDutiesV2({epoch: historicalEpoch})) as { - data: routes.validator.ProposerDutyList; - }; - - expect(result.length).toBe(SLOTS_PER_EPOCH); - // Spy won't be called as `getProposerDuties` will create a new cached beacon state - expect(result.map((p) => p.slot)).toEqual( - Array.from({length: SLOTS_PER_EPOCH}, (_, i) => historicalEpoch * SLOTS_PER_EPOCH + i) - ); + await expect(api.getProposerDutiesV2({epoch: historicalEpoch})).rejects.toThrow("not supported yet"); }); it("should raise error for more than one epoch in the future", async () => { From 452cc42db7f95fed0dcd46620fc33eb74adc5472 Mon Sep 17 00:00:00 2001 From: twoeths Date: Mon, 29 Jun 2026 13:54:05 +0700 Subject: [PATCH 11/24] feat: implement BeaconEngine.prepareForNextSlot() --- .../src/chain/beaconEngine/beaconEngine.ts | 431 +++++++++++++++++- .../src/chain/beaconEngine/interface.ts | 48 +- .../src/chain/beaconEngine/options.ts | 4 + .../src/chain/blocks/importBlock.ts | 1 + packages/beacon-node/src/chain/chain.ts | 6 +- packages/beacon-node/src/chain/options.ts | 1 - .../beacon-node/src/chain/prepareNextSlot.ts | 284 +++--------- .../chain/produceBlock/produceBlockBody.ts | 229 ++-------- .../test/mocks/mockedBeaconChain.ts | 7 + 9 files changed, 572 insertions(+), 439 deletions(-) diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 3500af3768e2..abdd1e72d909 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -19,12 +19,14 @@ import { import { ForkName, ForkPostAltair, + ForkPostBellatrix, ForkPostElectra, ForkSeq, GENESIS_SLOT, MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT, + isForkPostBellatrix, isForkPostGloas, } from "@lodestar/params"; import { @@ -32,8 +34,10 @@ import { EffectiveBalanceIncrements, EpochShuffling, IBeaconStateView, + IBeaconStateViewBellatrix, PubkeyCache, RootCache, + StateHashTreeRootSource, calculateCommitteeAssignments, computeEpochAtSlot, computeStartSlotAtEpoch, @@ -55,6 +59,7 @@ import { IndexedAttestation, Root, RootHex, + SSEPayloadAttributes, SignedAggregateAndProof, SignedBeaconBlock, Slot, @@ -69,14 +74,16 @@ import { phase0, ssz, } from "@lodestar/types"; -import {Logger, fromHex, sleep, toRootHex} from "@lodestar/utils"; +import {Logger, byteArrayEquals, fromHex, sleep, toRootHex} from "@lodestar/utils"; import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js"; import {IBeaconDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; import {IClock} from "../../util/clock.js"; +import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; import {callInNextEventLoop} from "../../util/eventLoop.js"; import {isOptimisticBlock} from "../../util/forkChoice.js"; import {CheckpointBalancesCache} from "../balancesCache.js"; +import {BeaconProposerCache} from "../beaconProposerCache.js"; import {isBlockInputBlobs, isBlockInputColumns} from "../blocks/blockInput/blockInput.js"; import {IBlockInput} from "../blocks/blockInput/index.js"; import {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; @@ -105,7 +112,9 @@ import { BlockAttributes, BlockProductionStep, BlockType, + PayloadAttributesInput, PayloadAttributesWithdrawals, + preparePayloadAttributes, } from "../produceBlock/produceBlockBody.js"; import {QueuedStateRegenerator, RegenCaller} from "../regen/index.js"; import { @@ -160,7 +169,13 @@ import {ValidatorMonitor} from "../validatorMonitor.js"; import {computeNewStateRoot} from "./computeNewStateRoot.js"; import {getPubkeysForIndices} from "./duties.js"; import {GossipValidationResult, fromResult, runGossipValidation} from "./gossipValidationResult.js"; -import {BeaconEngineModules, IBeaconEngine, ImportBlockResult, ProduceBlockBaseResult} from "./interface.js"; +import { + BeaconEngineModules, + IBeaconEngine, + ImportBlockResult, + PrepareNextSlotResult, + ProduceBlockBaseResult, +} from "./interface.js"; import {IBeaconEngineOptions} from "./options.js"; type VerifiedBlockBundle = { @@ -172,6 +187,9 @@ type VerifiedBlockBundle = { seenTimestampSec: number; }; +/* We don't want to do more epoch transition than this in prepareForNextSlot */ +const PREPARE_EPOCH_LIMIT = 1; + /** * JS implementation of the consensus engine. Transitional in Phase 0: constructed inside * `BeaconChain` from the `anchorState` object; construction moves to the CLI in Phase 6. @@ -204,6 +222,7 @@ export class BeaconEngine implements IBeaconEngine { readonly executionPayloadBidPool = new ExecutionPayloadBidPool(); readonly proposerPreferencesPool = new ProposerPreferencesPool(); readonly opPool: OpPool; + readonly beaconProposerCache: BeaconProposerCache; // Consensus gossip seen-caches readonly seenAttesters = new SeenAttesters(); @@ -284,6 +303,7 @@ export class BeaconEngine implements IBeaconEngine { this.syncContributionAndProofPool = new SyncContributionAndProofPool(config, clock, metrics, logger); this.payloadAttestationPool = new PayloadAttestationPool(config, clock, metrics); this.opPool = new OpPool(config); + this.beaconProposerCache = new BeaconProposerCache(opts); this.seenContributionAndProof = new SeenContributionAndProof(metrics); this.seenAttestationDatas = new SeenAttestationDatas(metrics, opts?.attDataCacheSlotDistance); @@ -428,7 +448,7 @@ export class BeaconEngine implements IBeaconEngine { * (`chain.getParentExecutionRequests`). At `produceBlockBase` time the parent is FULL and its * envelope's async DB write has normally completed. * TODO - beacon engine: here BeaconEngine owns the payloadEnvelope db because we need state transition for block production - * see if we have any down sides with this + * and PrepareNextSlot. It means we always have to reach db instead of cache. See if we have any down sides with this */ async getParentExecutionRequests( parentBlockSlot: Slot, @@ -530,6 +550,12 @@ export class BeaconEngine implements IBeaconEngine { withdrawals = stateForProduction.getExpectedWithdrawals().expectedWithdrawals; } + // gloas: resolve the proposer target gas limit here (engine-owned fork choice / proposer-preferences + // pool) so the facade self-build path passes it to `prepareExecutionPayload` as plain data. + const targetGasLimit = isForkPostGloas(fork) + ? this.getProposerTargetGasLimit(slot, fromHex(parentBlock.blockRoot), parentBlockHash) + : undefined; + // Build the common body from `stateForProduction` (its voluntaryExits / blsToExecutionChanges are // already valid against the applied state — no per-path re-filter downstream). Deferred to the next // event loop so the downstream EL request goes out first. @@ -565,6 +591,7 @@ export class BeaconEngine implements IBeaconEngine { parentExecutionRequests, payloadAttestations, withdrawals, + targetGasLimit, commonBlockBodyPromise, }; } @@ -610,6 +637,404 @@ export class BeaconEngine implements IBeaconEngine { return {newStateRoot, proposerReward}; } + /** + * Advance preparation for the next slot. Does all consensus work internally (recompute head, regen the + * prepare-state, predict a proposer-boost-reorg, precompute the epoch transition, cache `hashTreeRoot`) + * and returns the side-effects the facade must perform (EL advance-prep + builder, DA prune, SSE emit). + * The sleep timing and try/catch error reporting stay in the facade scheduler. Returns `null` when + * there is nothing for the facade to do. + */ + async prepareForNextSlot(clockSlot: Slot): Promise { + const prepareSlot = clockSlot + 1; + const nextEpoch = computeEpochAtSlot(clockSlot) + 1; + const isEpochTransition = computeEpochAtSlot(prepareSlot) === nextEpoch; + const fork = this.config.getForkName(prepareSlot); + + // calling updateHead() here before we produce a block to reduce reorg possibility + const headBlock = this.recomputeForkChoiceHead(); + const {slot: headSlot, blockRoot: headRoot} = headBlock; + // may be updated below if we predict a proposer-boost-reorg + let updatedHead = headBlock; + + // PS: previously this was comparing slots, but that gave no leway on the skipped + // slots on epoch bounday. Making it more fluid. + if (prepareSlot - headSlot > PREPARE_EPOCH_LIMIT * SLOTS_PER_EPOCH) { + this.metrics?.precomputeNextEpochTransition.count.inc({result: "skip"}, 1); + this.logger.debug("Skipping PrepareNextSlotScheduler - head slot is too behind current slot", { + nextEpoch, + headSlot, + clockSlot, + }); + return null; + } + + this.logger.verbose("Running prepareForNextSlot", {nextEpoch, prepareSlot, headSlot, headRoot, isEpochTransition}); + const precomputeEpochTransitionTimer = isEpochTransition + ? this.metrics?.precomputeNextEpochTransition.duration.startTimer() + : null; + const start = Date.now(); + // No need to wait for this or the clock drift + // Pre Bellatrix: we only do precompute state transition for the last slot of epoch + // For Bellatrix, we always do the `processSlots()` to prepare payload for the next slot + const prepareState = await this.regen.getBlockSlotState( + headBlock, + prepareSlot, + // the slot 0 of next epoch will likely use this Previous Root Checkpoint state for state transition so we transfer cache here + // the resulting state with cache will be cached in Checkpoint State Cache which is used for the upcoming block processing + // for other slots dontTransferCached=true because we don't run state transition on this state + {dontTransferCache: !isEpochTransition}, + RegenCaller.precomputeEpoch + ); + + let elPrep: PrepareNextSlotResult["elPrep"] = null; + let daPruneParent: ProtoBlock | null = null; + let sse: PrepareNextSlotResult["sse"] = null; + + if (isForkPostBellatrix(fork)) { + const proposerIndex = prepareState.getBeaconProposer(prepareSlot); + // set iff WE propose next slot — gates builder status / EL prep, exactly as before + const feeRecipient = this.beaconProposerCache.get(proposerIndex); + let updatedPrepareState = prepareState; + + if (feeRecipient) { + // If we are proposing next slot, we need to predict if we can proposer-boost-reorg or not + const proposerHead = this.predictProposerHead(clockSlot); + const {slot: proposerHeadSlot, blockRoot: proposerHeadRoot} = proposerHead; + + // If we predict we can reorg, update prepareState with proposer head block + if (proposerHeadRoot !== headRoot || proposerHeadSlot !== headSlot) { + this.logger.verbose("Weak head detected. May build on parent block instead", { + proposerHeadSlot, + proposerHeadRoot, + headSlot, + headRoot, + }); + this.metrics?.weakHeadDetected.inc(); + updatedPrepareState = await this.regen.getBlockSlotState( + proposerHead, + prepareSlot, + // only transfer cache if epoch transition because that's the state we will use to stateTransition() the 1st block of epoch + {dontTransferCache: !isEpochTransition}, + RegenCaller.predictProposerHead + ); + updatedHead = proposerHead; + } + } + + if (!isStatePostBellatrix(updatedPrepareState)) { + throw new Error("Expected Bellatrix state for payload attributes"); + } + + let parentBlockHash: Bytes32; + // Apply parent payload once here as it's reused by EL prep and SSE emit below + let stateAfterParentPayload: IBeaconStateViewBellatrix = updatedPrepareState; + if (isStatePostGloas(updatedPrepareState)) { + // Spec: should_build_on_full(store, head) — see produceBlockBody.ts for context. + if (this.forkChoice.shouldBuildOnFull(updatedHead, prepareSlot)) { + parentBlockHash = updatedPrepareState.latestExecutionPayloadBid.blockHash; + // Skip applying parent payload unless we're proposing the next slot or have to emit payload_attributes events + if (feeRecipient !== undefined || this.opts.emitPayloadAttributes === true) { + const parentExecutionRequests = await this.getParentExecutionRequests( + updatedHead.slot, + updatedHead.blockRoot + ); + stateAfterParentPayload = updatedPrepareState.withParentPayloadApplied(parentExecutionRequests); + } + } else { + parentBlockHash = updatedPrepareState.latestExecutionPayloadBid.parentBlockHash; + } + } else { + parentBlockHash = updatedPrepareState.latestExecutionPayloadHeader.blockHash; + } + + // EL advance-prep inputs — ONLY when WE propose next slot. Facade does builder status + EL. + if (feeRecipient) { + const parentBlockRoot = fromHex(updatedHead.blockRoot); + const preparationTime = + computeTimeAtSlot(this.config, prepareSlot, prepareState.genesisTime) - Date.now() / 1000; + this.metrics?.blockPayload.payloadAdvancePrepTime.observe(preparationTime); + elPrep = { + fork: fork as ForkPostBellatrix, + proposerIndex, + feeRecipient, + parentBlockRoot, + parentBlockHash, + safeBlockHash: getSafeExecutionBlockHash(this.forkChoice), + finalizedBlockHash: this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX, + prepareSlot, + payloadAttributesInput: this.resolvePayloadAttributesInput( + fork as ForkPostBellatrix, + stateAfterParentPayload, + parentBlockHash + ), + // the only engine-owned value the facade EL path needs (gloas); resolved here so the facade + // `prepareExecutionPayload` stays off fork choice / the proposer-preferences pool + targetGasLimit: + ForkSeq[fork] >= ForkSeq.gloas + ? this.getProposerTargetGasLimit(prepareSlot, parentBlockRoot, parentBlockHash) + : undefined, + }; + } + + if (ForkSeq[fork] >= ForkSeq.gloas) { + // Cutoff = slot of the parent of the block we'll actually build on (post-reorg). + // Steady state: cache holds just 2 entries — head (parent for next-slot production) + // and head.parent (proposer-boost-reorg fallback). Anything older is evicted. + daPruneParent = this.forkChoice.getBlockHexDefaultStatus(updatedHead.parentRoot) ?? null; + } + + this.computeStateHashTreeRoot(updatedPrepareState, isEpochTransition); + + // If emitPayloadAttributes is true emit a SSE payloadAttributes event for + // every slot. Without the flag, only emit the event if we are proposing in the next slot. + if ( + (feeRecipient || this.opts.emitPayloadAttributes === true) && + this.emitter.listenerCount(routes.events.EventType.payloadAttributes) + ) { + const data = this.getPayloadAttributesForSSE(fork as ForkPostBellatrix, { + prepareSlot, + parentBlockRoot: fromHex(updatedHead.blockRoot), + parentBlockHash, + feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000", + proposerIndex: stateAfterParentPayload.getBeaconProposer(prepareSlot), + parentBlockNumberPreGloas: + ForkSeq[fork] >= ForkSeq.gloas ? undefined : stateAfterParentPayload.payloadBlockNumber, + ...this.resolvePayloadAttributesInput(fork as ForkPostBellatrix, stateAfterParentPayload, parentBlockHash), + }); + sse = {data, version: fork}; + } + } else { + this.computeStateHashTreeRoot(prepareState, isEpochTransition); + } + + // assuming there is no reorg, it caches the checkpoint state & helps avoid doing a full state transition in the next slot + // + when gossip block comes, we need to validate and run state transition + // + if next slot is a skipped slot, it'd help getting target checkpoint state faster to validate attestations + if (isEpochTransition) { + this.metrics?.precomputeNextEpochTransition.count.inc({result: "success"}, 1); + const previousHits = this.regen.updatePreComputedCheckpoint(headRoot, nextEpoch); + if (previousHits === 0) { + this.metrics?.precomputeNextEpochTransition.waste.inc(); + } + this.metrics?.precomputeNextEpochTransition.hits.set(previousHits ?? 0); + + this.logger.verbose("Completed PrepareNextSlotScheduler epoch transition", { + nextEpoch, + headSlot, + prepareSlot, + previousHits, + durationMs: Date.now() - start, + }); + + precomputeEpochTransitionTimer?.(); + } + + return elPrep || daPruneParent || sse ? {elPrep, daPruneParent, sse} : null; + } + + /** Canonical head recompute used before block production (mirrors chain.ts facade method). */ + private recomputeForkChoiceHead(): ProtoBlock { + this.metrics?.forkChoice.requests.inc(); + const timer = this.metrics?.forkChoice.findHead.startTimer({caller: ForkchoiceCaller.prepareNextSlot}); + try { + return this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head; + } catch (e) { + this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead}); + throw e; + } finally { + timer?.(); + } + } + + /** Predicted proposer head for proposer-boost-reorg (mirrors chain.ts facade method). */ + private predictProposerHead(slot: Slot): ProtoBlock { + this.metrics?.forkChoice.requests.inc(); + const timer = this.metrics?.forkChoice.findHead.startTimer({caller: FindHeadFnName.predictProposerHead}); + const secFromSlot = this.clock.secFromSlot(slot); + try { + return this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetPredictedProposerHead, secFromSlot, slot}).head; + } catch (e) { + this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetPredictedProposerHead}); + throw e; + } finally { + timer?.(); + } + } + + /** + * Cache HashObjects for faster hashTreeRoot() later, especially for computeNewStateRoot() if we need to + * produce a block at slot 0 of epoch. See https://github.com/ChainSafe/lodestar/issues/6194 + */ + private computeStateHashTreeRoot(state: IBeaconStateView, isEpochTransition: boolean): void { + const hashTreeRootTimer = this.metrics?.stateHashTreeRootTime.startTimer({ + source: isEpochTransition ? StateHashTreeRootSource.prepareNextEpoch : StateHashTreeRootSource.prepareNextSlot, + }); + state.hashTreeRoot(); + hashTreeRootTimer?.(); + } + + /** + * The single place that reads `BeaconState` for execution payload attributes. Everything downstream + * consumes the returned plain fields and never touches the state. + * + * Post-gloas, when extending a full parent, callers must apply parent execution payload first + * (see `withParentPayloadApplied`) before calling this. + */ + private resolvePayloadAttributesInput( + fork: ForkPostBellatrix, + state: IBeaconStateViewBellatrix, + parentBlockHash: Bytes32 + ): PayloadAttributesInput { + const timestamp = computeTimeAtSlot(this.config, state.slot, state.genesisTime); + const prevRandao = state.getRandaoMix(state.epoch); + + let withdrawals: PayloadAttributesWithdrawals | undefined; + if (ForkSeq[fork] >= ForkSeq.capella) { + if (!isStatePostCapella(state)) { + throw new Error("Expected Capella state for withdrawals"); + } + + if (isStatePostGloas(state)) { + const isExtendingPayload = byteArrayEquals(parentBlockHash, state.latestExecutionPayloadBid.blockHash); + if (isExtendingPayload) { + // applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch + // here means the caller did not apply parent payload to state + if (!byteArrayEquals(state.latestBlockHash, state.latestExecutionPayloadBid.blockHash)) { + throw new Error("Expected state with parent execution payload applied for withdrawals"); + } + withdrawals = state.getExpectedWithdrawals().expectedWithdrawals; + } else { + // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch + // already deducted from CL balances but never credited on the EL (the envelope + // was not delivered). The next payload must carry those same withdrawals to + // restore CL/EL consistency, otherwise validators permanently lose that balance. + withdrawals = state.payloadExpectedWithdrawals; + } + } else { + // withdrawals logic is now fork aware as it changes on electra fork post capella + withdrawals = state.getExpectedWithdrawals().expectedWithdrawals; + } + } + + return {timestamp, prevRandao, withdrawals}; + } + + /** + * Build the SSE `payloadAttributes` event payload for the prepare slot. Reads engine-owned fork choice + * (parent block number) and resolves the gloas `targetGasLimit` internally. + */ + private getPayloadAttributesForSSE( + fork: ForkPostBellatrix, + { + prepareSlot, + parentBlockRoot, + parentBlockHash, + feeRecipient, + timestamp, + prevRandao, + withdrawals, + proposerIndex, + parentBlockNumberPreGloas, + }: { + prepareSlot: Slot; + parentBlockRoot: Root; + parentBlockHash: Bytes32; + feeRecipient: string; + /** proposer at the prepare slot (SSE event) */ + proposerIndex: ValidatorIndex; + /** pre-gloas only: `state.payloadBlockNumber` for the SSE parentBlockNumber */ + parentBlockNumberPreGloas?: number; + } & PayloadAttributesInput + ): SSEPayloadAttributes { + const targetGasLimit = isForkPostGloas(fork) + ? this.getProposerTargetGasLimit(prepareSlot, parentBlockRoot, parentBlockHash) + : undefined; + const payloadAttributes = preparePayloadAttributes(fork, targetGasLimit, { + prepareSlot, + parentBlockRoot, + feeRecipient, + timestamp, + prevRandao, + withdrawals, + }); + + let parentBlockNumber: number; + if (isForkPostGloas(fork)) { + const parentBlock = this.forkChoice.getBlockHexAndBlockHash( + toRootHex(parentBlockRoot), + toRootHex(parentBlockHash) + ); + if (parentBlock?.executionPayloadBlockHash == null) { + throw Error(`Parent block not found in fork choice root=${toRootHex(parentBlockRoot)}`); + } + parentBlockNumber = parentBlock.executionPayloadNumber; + } else { + if (parentBlockNumberPreGloas === undefined) { + throw Error("Expected parentBlockNumberPreGloas for pre-gloas SSE payload attributes"); + } + parentBlockNumber = parentBlockNumberPreGloas; + } + + return { + proposerIndex, + proposalSlot: prepareSlot, + parentBlockNumber, + parentBlockRoot, + parentBlockHash, + payloadAttributes, + }; + } + + /** + * Resolve the proposer's preferred (target) gas limit for the Gloas `PayloadAttributesV4` + * `targetGasLimit` field (consensus-specs#5235, execution-apis#796). + * + * Sourced from the `SignedProposerPreferences` the proposer's VC submitted to the pool + * (same `(slot, dependent_root)` lookup as gossip bid validation). When no matching + * preferences are pooled, target the parent payload's gas limit so the gas limit stays + * unchanged (`is_gas_limit_target_compatible` then requires `gas_limit == parent_gas_limit`). + * + * The parent payload's gas_limit is read from fork choice — the variant matching + * `(parentBlockRoot, parentBlockHash)` carries the correct value for both FULL parents + * (FULL.executionPayloadGasLimit = delivered payload's gas_limit) and EMPTY parents + * (EMPTY.executionPayloadGasLimit = inherited grandparent's gas_limit). + */ + private getProposerTargetGasLimit(prepareSlot: Slot, parentBlockRoot: Root, parentBlockHash: Bytes32): number { + const parentBlockRootHex = toRootHex(parentBlockRoot); + const parentBlock = this.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex); + const dependentRootHex = (() => { + if (parentBlock === null) { + return null; + } + try { + return getShufflingDependentRoot( + this.forkChoice, + computeEpochAtSlot(prepareSlot), + computeEpochAtSlot(parentBlock.slot), + parentBlock + ); + } catch { + return null; + } + })(); + + const pref = dependentRootHex !== null ? this.proposerPreferencesPool.get(prepareSlot, dependentRootHex) : null; + if (pref !== null) { + return pref.message.targetGasLimit; + } + + const parentPayloadVariant = this.forkChoice.getBlockHexAndBlockHash( + parentBlockRootHex, + toRootHex(parentBlockHash) + ); + if (parentPayloadVariant === null || parentPayloadVariant.executionPayloadBlockHash === null) { + throw new Error( + `Cannot resolve parent payload gas_limit for proposer targetGasLimit fallback parentBlockRoot=${parentBlockRootHex} parentBlockHash=${toRootHex(parentBlockHash)}` + ); + } + return parentPayloadVariant.executionPayloadGasLimit; + } + /** * Proposer duties for `epoch`. */ diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 920b6f864068..dcdd5692b1d4 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -1,7 +1,7 @@ import {routes} from "@lodestar/api"; import {BeaconConfig} from "@lodestar/config"; import {BlockExecutionStatus, IForkChoice, PayloadExecutionStatus, ProtoBlock} from "@lodestar/fork-choice"; -import {ForkName} from "@lodestar/params"; +import {ForkName, ForkPostBellatrix} from "@lodestar/params"; import {DataAvailabilityStatus, IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; import { BLSPubkey, @@ -13,6 +13,7 @@ import { Gwei, Root, RootHex, + SSEPayloadAttributes, SignedAggregateAndProof, SignedBeaconBlock, Slot, @@ -35,7 +36,11 @@ import {ChainEventEmitter, ReorgEventData} from "../emitter.js"; import {CommonBlockBody} from "../interface.js"; import {LightClientServer} from "../lightClient/index.js"; import {BlockProcessOpts} from "../options.js"; -import {BlockAttributes, PayloadAttributesWithdrawals} from "../produceBlock/produceBlockBody.js"; +import { + BlockAttributes, + PayloadAttributesInput, + PayloadAttributesWithdrawals, +} from "../produceBlock/produceBlockBody.js"; import {SeenBlockInput} from "../seenCache/seenGossipBlockInput.js"; import {ShufflingCache} from "../shufflingCache.js"; import {CPStateDatastore} from "../stateCache/datastore/types.js"; @@ -123,10 +128,44 @@ export type ProduceBlockBaseResult = { payloadAttestations: gloas.PayloadAttestation[]; /** payload-attribute withdrawals resolved from the parent-payload-applied state (post-capella) */ withdrawals?: PayloadAttributesWithdrawals; + /** gloas: proposer target gas limit, resolved engine-side so the facade self-build EL call passes it + * as plain data (mirrors the prepareForNextSlot path); undefined pre-gloas */ + targetGasLimit?: number; /** common body produced from the parent-payload-applied state — its voluntaryExits are already valid */ commonBlockBodyPromise: Promise; }; +/** + * Result of `prepareForNextSlot`. The engine does all consensus work internally (head recompute, regen, + * proposer-boost-reorg prediction, `hashTreeRoot`, epoch-transition precompute) and returns only the + * side-effects the facade must perform (EL advance-prep + builder, DA prune, SSE emit). `null` = nothing + * for the facade to do. + */ +export type PrepareNextSlotResult = { + /** + * Non-null ONLY when this node proposes the next slot (the `beaconProposerCache` fee-recipient + * resolves). When null the facade does NO builder status and NO EL `prepareExecutionPayload`. + */ + elPrep: { + fork: ForkPostBellatrix; + proposerIndex: ValidatorIndex; + feeRecipient: string; + parentBlockRoot: Root; + parentBlockHash: Bytes32; + safeBlockHash: RootHex; + finalizedBlockHash: RootHex; + prepareSlot: Slot; + payloadAttributesInput: PayloadAttributesInput; + /** gloas: proposer target gas limit, resolved engine-side so the facade EL call never reaches back + * into engine internals (fork choice / proposer-preferences pool); undefined pre-gloas */ + targetGasLimit?: number; + } | null; + /** gloas: parent ProtoBlock of the (possibly reorged) head; facade prunes the DA seen cache below it */ + daPruneParent: ProtoBlock | null; + /** built only when (proposing || emitPayloadAttributes) AND the emitter has listeners; facade emits it */ + sse: {data: SSEPayloadAttributes; version: ForkName} | null; +}; + /** * The consensus engine seam. Starts minimal and transitional (JS-only); ownership of consensus * collaborators and flows migrates here across later phases. This interface is the contract shared @@ -160,6 +199,11 @@ export interface IBeaconEngine { blinded: boolean ): Promise<{newStateRoot: Root; proposerReward: Gwei}>; + // Advance preparation for the next slot: recompute head, regen the prepare-state, predict a + // proposer-boost-reorg, precompute the epoch transition and cache `hashTreeRoot`. Returns the + // side-effects the facade performs (EL advance-prep + builder, DA prune, SSE emit); `null` = no-op. + prepareForNextSlot(clockSlot: Slot): Promise; + // Validator duties. The engine resolves state internally and returns fully-populated duties (with // pubkeys) + dependentRoot; no `IBeaconStateView` crosses. Clock-derived values are passed in (the // engine holds no clock): `currentEpoch` for head-state resolution, and `checkpointWaitTimeoutMs` (a diff --git a/packages/beacon-node/src/chain/beaconEngine/options.ts b/packages/beacon-node/src/chain/beaconEngine/options.ts index efe3be9e533a..797ea3e02dd8 100644 --- a/packages/beacon-node/src/chain/beaconEngine/options.ts +++ b/packages/beacon-node/src/chain/beaconEngine/options.ts @@ -34,4 +34,8 @@ export type IBeaconEngineOptions = ShufflingCacheOpts & maxSkipSlots?: number; /** Min number of same-message signature sets to batch in gossip attestation validation */ minSameMessageSignatureSetsToBatch: number; + /** Default fee recipient used by the engine-owned beaconProposerCache */ + suggestedFeeRecipient: string; + /** Emit an SSE payloadAttributes event every slot (not only when proposing) */ + emitPayloadAttributes?: boolean; }; diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index ef016d2359d8..0b9d4f5777c8 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -87,6 +87,7 @@ export async function importBlock( try { const proposerIndex = r.proposerIndexNextSlot; if (proposerIndex !== null) { + // TODO - beacon engine: move this there const feeRecipient = this.beaconProposerCache.get(proposerIndex); if (feeRecipient && r.blockSummary !== null) { const result = this.forkChoice.shouldOverrideForkChoiceUpdate( diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index dc1dae87fc6f..a33302497fb8 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -249,7 +249,9 @@ export class BeaconChain implements IBeaconChain { // Global state caches readonly pubkeyCache: PubkeyCache; - readonly beaconProposerCache: BeaconProposerCache; + get beaconProposerCache(): BeaconProposerCache { + return this.beaconEngine.beaconProposerCache; + } // TODO - beacon engine: remove this get checkpointBalancesCache(): CheckpointBalancesCache { @@ -396,8 +398,6 @@ export class BeaconChain implements IBeaconChain { this.blacklistedBlocks = new Map((opts.blacklistedBlocks ?? []).map((hex) => [hex, null])); - this.beaconProposerCache = new BeaconProposerCache(opts); - this._earliestAvailableSlot = anchorState.slot; // Global cache of validators pubkey/index mapping diff --git a/packages/beacon-node/src/chain/options.ts b/packages/beacon-node/src/chain/options.ts index 31781147a5e2..75b6e71d913c 100644 --- a/packages/beacon-node/src/chain/options.ts +++ b/packages/beacon-node/src/chain/options.ts @@ -25,7 +25,6 @@ export type IChainOptions = IBeaconEngineOptions & persistOrphanedBlocks?: boolean; persistOrphanedBlocksDir?: string; skipCreateStateCacheIfAvailable?: boolean; - suggestedFeeRecipient: string; /** Ensure blobs returned by the execution engine are valid */ sanityCheckExecutionEngineBlobs?: boolean; /** Max number of produced blobs by local validators to cache */ diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index 67d133101d74..10c20f3f9a9a 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -1,39 +1,21 @@ import {routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; -import {getSafeExecutionBlockHash} from "@lodestar/fork-choice"; -import {ForkPostBellatrix, ForkSeq, SLOTS_PER_EPOCH, isForkPostBellatrix} from "@lodestar/params"; -import { - IBeaconStateView, - IBeaconStateViewBellatrix, - StateHashTreeRootSource, - computeEpochAtSlot, - computeTimeAtSlot, - isStatePostBellatrix, - isStatePostGloas, -} from "@lodestar/state-transition"; -import {Bytes32, Slot} from "@lodestar/types"; -import {Logger, fromHex, isErrorAborted, sleep} from "@lodestar/utils"; -import {GENESIS_SLOT, ZERO_HASH_HEX} from "../constants/constants.js"; +import {ForkSeq} from "@lodestar/params"; +import {computeEpochAtSlot} from "@lodestar/state-transition"; +import {Slot} from "@lodestar/types"; +import {Logger, isErrorAborted, sleep} from "@lodestar/utils"; +import {GENESIS_SLOT} from "../constants/constants.js"; import {BuilderStatus} from "../execution/builder/http.js"; import {Metrics} from "../metrics/index.js"; import {ClockEvent} from "../util/clock.js"; import {isQueueErrorAborted} from "../util/queue/index.js"; -import {ForkchoiceCaller} from "./forkChoice/index.js"; import {IBeaconChain} from "./interface.js"; -import { - getPayloadAttributesForSSE, - prepareExecutionPayload, - resolvePayloadAttributesInput, -} from "./produceBlock/produceBlockBody.js"; -import {RegenCaller} from "./regen/index.js"; +import {prepareExecutionPayload} from "./produceBlock/produceBlockBody.js"; // TODO GLOAS: re-evaluate this timing /* With 12s slot times, this scheduler will run 4s before the start of each slot (`12 - 0.6667 * 12 = 4`). */ export const PREPARE_NEXT_SLOT_BPS = 6667; -/* We don't want to do more epoch transition than this */ -const PREPARE_EPOCH_LIMIT = 1; - /** * At Bellatrix, if we are responsible for proposing in next slot, we want to prepare payload * 4s before the start of next slot at PREPARE_NEXT_SLOT_BPS of the current slot. @@ -44,6 +26,9 @@ const PREPARE_EPOCH_LIMIT = 1; * + validators propose blocks on time * + For Bellatrix, to compute proposers of next epoch so that we can prepare new payloads * + * The consensus work is owned by `BeaconEngine.prepareForNextSlot`; this scheduler owns the clock + * listener, the sleep timing, and the facade side-effects the engine returns (EL advance-prep + builder + * status, DA seen-cache prune, SSE payloadAttributes emit). */ export class PrepareNextSlotScheduler { constructor( @@ -84,208 +69,63 @@ export class PrepareNextSlotScheduler { // or precompute epoch transition await sleep(this.config.getSlotComponentDurationMs(PREPARE_NEXT_SLOT_BPS), this.signal); - // calling updateHead() here before we produce a block to reduce reorg possibility - const headBlock = this.chain.recomputeForkChoiceHead(ForkchoiceCaller.prepareNextSlot); - const {slot: headSlot, blockRoot: headRoot} = headBlock; - // may be updated below if we predict a proposer-boost-reorg - let updatedHead = headBlock; - - // PS: previously this was comparing slots, but that gave no leway on the skipped - // slots on epoch bounday. Making it more fluid. - if (prepareSlot - headSlot > PREPARE_EPOCH_LIMIT * SLOTS_PER_EPOCH) { - this.metrics?.precomputeNextEpochTransition.count.inc({result: "skip"}, 1); - this.logger.debug("Skipping PrepareNextSlotScheduler - head slot is too behind current slot", { - nextEpoch, - headSlot, - clockSlot, - }); - + const result = await this.chain.beaconEngine.prepareForNextSlot(clockSlot); + if (result === null) { return; } - this.logger.verbose("Running prepareForNextSlot", { - nextEpoch, - prepareSlot, - headSlot, - headRoot, - isEpochTransition, - }); - const precomputeEpochTransitionTimer = isEpochTransition - ? this.metrics?.precomputeNextEpochTransition.duration.startTimer() - : null; - const start = Date.now(); - // No need to wait for this or the clock drift - // Pre Bellatrix: we only do precompute state transition for the last slot of epoch - // For Bellatrix, we always do the `processSlots()` to prepare payload for the next slot - const prepareState = await this.chain.regen.getBlockSlotState( - headBlock, - prepareSlot, - // the slot 0 of next epoch will likely use this Previous Root Checkpoint state for state transition so we transfer cache here - // the resulting state with cache will be cached in Checkpoint State Cache which is used for the upcoming block processing - // for other slots dontTransferCached=true because we don't run state transition on this state - {dontTransferCache: !isEpochTransition}, - RegenCaller.precomputeEpoch - ); - - if (isForkPostBellatrix(fork)) { - const proposerIndex = prepareState.getBeaconProposer(prepareSlot); - const feeRecipient = this.chain.beaconProposerCache.get(proposerIndex); - let updatedPrepareState = prepareState; - - if (feeRecipient) { - // If we are proposing next slot, we need to predict if we can proposer-boost-reorg or not - const proposerHead = this.chain.predictProposerHead(clockSlot); - const {slot: proposerHeadSlot, blockRoot: proposerHeadRoot} = proposerHead; - - // If we predict we can reorg, update prepareState with proposer head block - if (proposerHeadRoot !== headRoot || proposerHeadSlot !== headSlot) { - this.logger.verbose("Weak head detected. May build on parent block instead", { - proposerHeadSlot, - proposerHeadRoot, - headSlot, - headRoot, - }); - this.metrics?.weakHeadDetected.inc(); - updatedPrepareState = await this.chain.regen.getBlockSlotState( - proposerHead, - prepareSlot, - // only transfer cache if epoch transition because that's the state we will use to stateTransition() the 1st block of epoch - {dontTransferCache: !isEpochTransition}, - RegenCaller.predictProposerHead - ); - updatedHead = proposerHead; - } - - // Update the builder status, if enabled shoot an api call to check status - this.chain.updateBuilderStatus(clockSlot); - if (this.chain.executionBuilder?.status === BuilderStatus.enabled) { - this.chain.executionBuilder.checkStatus().catch((e) => { - this.logger.error("Builder disabled as the check status api failed", {prepareSlot}, e as Error); - }); - } - } - - if (!isStatePostBellatrix(updatedPrepareState)) { - throw new Error("Expected Bellatrix state for payload attributes"); - } - - let parentBlockHash: Bytes32; - // Apply parent payload once here as it's reused by EL prep and SSE emit below - let stateAfterParentPayload: IBeaconStateViewBellatrix = updatedPrepareState; - if (isStatePostGloas(updatedPrepareState)) { - // Spec: should_build_on_full(store, head) — see produceBlockBody.ts for context. - if (this.chain.forkChoice.shouldBuildOnFull(updatedHead, prepareSlot)) { - parentBlockHash = updatedPrepareState.latestExecutionPayloadBid.blockHash; - // Skip applying parent payload unless we're proposing the next slot or have to emit payload_attributes events - if (feeRecipient !== undefined || this.chain.opts.emitPayloadAttributes === true) { - const parentExecutionRequests = await this.chain.getParentExecutionRequests( - updatedHead.slot, - updatedHead.blockRoot - ); - stateAfterParentPayload = updatedPrepareState.withParentPayloadApplied(parentExecutionRequests); - } - } else { - parentBlockHash = updatedPrepareState.latestExecutionPayloadBid.parentBlockHash; - } - } else { - parentBlockHash = updatedPrepareState.latestExecutionPayloadHeader.blockHash; - } - - if (feeRecipient) { - const preparationTime = - computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000; - this.metrics?.blockPayload.payloadAdvancePrepTime.observe(preparationTime); - - const safeBlockHash = getSafeExecutionBlockHash(this.chain.forkChoice); - const finalizedBlockHash = - this.chain.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; - - // awaiting here instead of throwing an async call because there is no other task - // left for scheduler and this gives nice semantics to catch and log errors in the - // try/catch wrapper here. - await prepareExecutionPayload( - this.chain, - this.logger, - fork as ForkPostBellatrix, // State is of execution type - fromHex(updatedHead.blockRoot), - parentBlockHash, - safeBlockHash, - finalizedBlockHash, - prepareSlot, - resolvePayloadAttributesInput( - this.config, - fork as ForkPostBellatrix, - stateAfterParentPayload, - parentBlockHash - ), - feeRecipient - ); - this.logger.verbose("PrepareNextSlotScheduler prepared new payload", { - prepareSlot, - proposerIndex, - feeRecipient, - }); - } - - if (ForkSeq[fork] >= ForkSeq.gloas) { - // Cutoff = slot of the parent of the block we'll actually build on (post-reorg). - // Steady state: cache holds just 2 entries — head (parent for next-slot production) - // and head.parent (proposer-boost-reorg fallback). Anything older is evicted. - const updatedHeadParent = this.chain.forkChoice.getBlockHexDefaultStatus(updatedHead.parentRoot); - if (updatedHeadParent) { - this.chain.seenPayloadEnvelopeInputCache.pruneBelowParent(updatedHeadParent); - } - } - - this.computeStateHashTreeRoot(updatedPrepareState, isEpochTransition); - - // If emitPayloadAttributes is true emit a SSE payloadAttributes event for - // every slot. Without the flag, only emit the event if we are proposing in the next slot. - if ( - (feeRecipient || this.chain.opts.emitPayloadAttributes === true) && - this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes) - ) { - const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, { - prepareSlot, - parentBlockRoot: fromHex(updatedHead.blockRoot), - parentBlockHash, - feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000", - proposerIndex: stateAfterParentPayload.getBeaconProposer(prepareSlot), - parentBlockNumberPreGloas: - ForkSeq[fork] >= ForkSeq.gloas ? undefined : stateAfterParentPayload.payloadBlockNumber, - ...resolvePayloadAttributesInput( - this.config, - fork as ForkPostBellatrix, - stateAfterParentPayload, - parentBlockHash - ), + // Only when we are proposing the next slot: update builder status and fire the EL advance-prep. + if (result.elPrep) { + const { + fork, + proposerIndex, + feeRecipient, + parentBlockRoot, + parentBlockHash, + safeBlockHash, + finalizedBlockHash, + prepareSlot, + payloadAttributesInput, + targetGasLimit, + } = result.elPrep; + + // Update the builder status, if enabled shoot an api call to check status + this.chain.updateBuilderStatus(clockSlot); + if (this.chain.executionBuilder?.status === BuilderStatus.enabled) { + this.chain.executionBuilder.checkStatus().catch((e) => { + this.logger.error("Builder disabled as the check status api failed", {prepareSlot}, e as Error); }); - this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork}); - } - } else { - this.computeStateHashTreeRoot(prepareState, isEpochTransition); - } - - // assuming there is no reorg, it caches the checkpoint state & helps avoid doing a full state transition in the next slot - // + when gossip block comes, we need to validate and run state transition - // + if next slot is a skipped slot, it'd help getting target checkpoint state faster to validate attestations - if (isEpochTransition) { - this.metrics?.precomputeNextEpochTransition.count.inc({result: "success"}, 1); - const previousHits = this.chain.regen.updatePreComputedCheckpoint(headRoot, nextEpoch); - if (previousHits === 0) { - this.metrics?.precomputeNextEpochTransition.waste.inc(); } - this.metrics?.precomputeNextEpochTransition.hits.set(previousHits ?? 0); - this.logger.verbose("Completed PrepareNextSlotScheduler epoch transition", { - nextEpoch, - headSlot, + // awaiting here instead of throwing an async call because there is no other task + // left for scheduler and this gives nice semantics to catch and log errors in the + // try/catch wrapper here. + await prepareExecutionPayload( + this.chain, + this.logger, + fork, + parentBlockRoot, + parentBlockHash, + safeBlockHash, + finalizedBlockHash, prepareSlot, - previousHits, - durationMs: Date.now() - start, + payloadAttributesInput, + feeRecipient, + targetGasLimit + ); + this.logger.verbose("PrepareNextSlotScheduler prepared new payload", { + prepareSlot, + proposerIndex, + feeRecipient, }); + } - precomputeEpochTransitionTimer?.(); + if (result.daPruneParent) { + this.chain.seenPayloadEnvelopeInputCache.pruneBelowParent(result.daPruneParent); + } + + if (result.sse) { + this.chain.emitter.emit(routes.events.EventType.payloadAttributes, result.sse); } } catch (e) { if (!isErrorAborted(e) && !isQueueErrorAborted(e)) { @@ -294,14 +134,4 @@ export class PrepareNextSlotScheduler { } } }; - - computeStateHashTreeRoot(state: IBeaconStateView, isEpochTransition: boolean): void { - // cache HashObjects for faster hashTreeRoot() later, especially for computeNewStateRoot() if we need to produce a block at slot 0 of epoch - // see https://github.com/ChainSafe/lodestar/issues/6194 - const hashTreeRootTimer = this.metrics?.stateHashTreeRootTime.startTimer({ - source: isEpochTransition ? StateHashTreeRootSource.prepareNextEpoch : StateHashTreeRootSource.prepareNextSlot, - }); - state.hashTreeRoot(); - hashTreeRootTimer?.(); - } } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 864fb5f2251b..6641342294a6 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -1,5 +1,5 @@ import {ChainForkConfig} from "@lodestar/config"; -import {IForkChoiceRead, ProtoBlock} from "@lodestar/fork-choice"; +import {ProtoBlock} from "@lodestar/fork-choice"; import { BUILDER_INDEX_SELF_BUILD, ForkName, @@ -14,15 +14,7 @@ import { isForkPostBellatrix, isForkPostGloas, } from "@lodestar/params"; -import { - G2_POINT_AT_INFINITY, - type IBeaconStateViewBellatrix, - computeEpochAtSlot, - computeTimeAtSlot, - getExpectedGasLimit, - isStatePostCapella, - isStatePostGloas, -} from "@lodestar/state-transition"; +import {G2_POINT_AT_INFINITY, computeTimeAtSlot, getExpectedGasLimit} from "@lodestar/state-transition"; import { BLSPubkey, BLSSignature, @@ -48,15 +40,13 @@ import { gloas, ssz, } from "@lodestar/types"; -import {GWEI_TO_WEI, Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils"; +import {GWEI_TO_WEI, Logger, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils"; import {numToQuantity} from "../../execution/engine/utils.js"; import {IExecutionBuilder, IExecutionEngine, PayloadAttributes, PayloadId} from "../../execution/index.js"; -import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; import {fromGraffitiBytes} from "../../util/graffiti.js"; import {kzg} from "../../util/kzg.js"; import type {BeaconChain} from "../chain.js"; import {CommonBlockBody} from "../interface.js"; -import {ProposerPreferencesPool} from "../opPools/index.js"; import {validateBlobsAndKzgCommitments, validateCellsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js"; // Time to provide the EL to generate a payload from new payload id @@ -113,6 +103,8 @@ export type PreparedBlockScalars = { // gloas: payload attestations for the parent block's payload (slot - 1), collected from the pool once. payloadAttestations: gloas.PayloadAttestation[]; withdrawals?: PayloadAttributesWithdrawals; + // gloas: proposer target gas limit resolved by produceBlockBase; passed to prepareExecutionPayload. + targetGasLimit?: number; }; export enum BlockType { @@ -205,6 +197,7 @@ export async function produceBlockBody( parentExecutionRequests: preparedParentExecutionRequests, payloadAttestations: preparedPayloadAttestations, withdrawals: preparedWithdrawals, + targetGasLimit: preparedTargetGasLimit, } = blockAttr; let executionPayloadValue: Wei; let blockBody: AssembledBodyType; @@ -271,7 +264,8 @@ export async function produceBlockBody( prevRandao: preparedPrevRandao, withdrawals: preparedWithdrawals, }, - feeRecipient + feeRecipient, + preparedTargetGasLimit ); const {prepType, payloadId} = prepareRes; @@ -402,7 +396,8 @@ export async function produceBlockBody( preparedFinalizedBlockHash, blockSlot, payloadAttributesInput, - executionBuilder.issueLocalFcUWithFeeRecipient + executionBuilder.issueLocalFcUWithFeeRecipient, + preparedTargetGasLimit ); } @@ -517,7 +512,8 @@ export async function produceBlockBody( preparedFinalizedBlockHash, blockSlot, payloadAttributesInput, - feeRecipient + feeRecipient, + preparedTargetGasLimit ); const {prepType, payloadId} = prepareRes; @@ -675,8 +671,6 @@ export async function prepareExecutionPayload( chain: { executionEngine: IExecutionEngine; config: ChainForkConfig; - forkChoice: IForkChoiceRead; - proposerPreferencesPool: ProposerPreferencesPool; }, logger: Logger, fork: ForkPostBellatrix, @@ -690,7 +684,9 @@ export async function prepareExecutionPayload( * execution payload applied first (see `withParentPayloadApplied`). */ payloadAttributesInput: PayloadAttributesInput, - suggestedFeeRecipient: string + suggestedFeeRecipient: string, + /** gloas: resolved engine-side (`getProposerTargetGasLimit`) and passed in as plain data; undefined pre-gloas */ + targetGasLimit?: number ): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> { const {timestamp, prevRandao, withdrawals} = payloadAttributesInput; @@ -720,10 +716,9 @@ export async function prepareExecutionPayload( prepType = PayloadPreparationType.Fresh; } - const attributes: PayloadAttributes = preparePayloadAttributes(fork, chain, { + const attributes: PayloadAttributes = preparePayloadAttributes(fork, targetGasLimit, { prepareSlot, parentBlockRoot, - parentBlockHash, feeRecipient: suggestedFeeRecipient, timestamp, prevRandao, @@ -774,77 +769,11 @@ async function prepareExecutionPayloadHeader( return chain.executionBuilder.getHeader(fork, slot, parentHash, proposerPubKey); } -export function getPayloadAttributesForSSE( - fork: ForkPostBellatrix, - chain: { - config: ChainForkConfig; - forkChoice: IForkChoiceRead; - proposerPreferencesPool: ProposerPreferencesPool; - }, - { - prepareSlot, - parentBlockRoot, - parentBlockHash, - feeRecipient, - timestamp, - prevRandao, - withdrawals, - proposerIndex, - parentBlockNumberPreGloas, - }: { - prepareSlot: Slot; - parentBlockRoot: Root; - parentBlockHash: Bytes32; - feeRecipient: string; - /** proposer at the prepare slot (SSE event) */ - proposerIndex: ValidatorIndex; - /** pre-gloas only: `state.payloadBlockNumber` for the SSE parentBlockNumber */ - parentBlockNumberPreGloas?: number; - } & PayloadAttributesInput -): SSEPayloadAttributes { - const payloadAttributes = preparePayloadAttributes(fork, chain, { - prepareSlot, - parentBlockRoot, - parentBlockHash, - feeRecipient, - timestamp, - prevRandao, - withdrawals, - }); - - let parentBlockNumber: number; - if (isForkPostGloas(fork)) { - const parentBlock = chain.forkChoice.getBlockHexAndBlockHash( - toRootHex(parentBlockRoot), - toRootHex(parentBlockHash) - ); - if (parentBlock?.executionPayloadBlockHash == null) { - throw Error(`Parent block not found in fork choice root=${toRootHex(parentBlockRoot)}`); - } - parentBlockNumber = parentBlock.executionPayloadNumber; - } else { - if (parentBlockNumberPreGloas === undefined) { - throw Error("Expected parentBlockNumberPreGloas for pre-gloas SSE payload attributes"); - } - parentBlockNumber = parentBlockNumberPreGloas; - } - - const ssePayloadAttributes: SSEPayloadAttributes = { - proposerIndex, - proposalSlot: prepareSlot, - parentBlockNumber, - parentBlockRoot, - parentBlockHash, - payloadAttributes, - }; - return ssePayloadAttributes; -} - export type PayloadAttributesWithdrawals = capella.SSEPayloadAttributes["payloadAttributes"]["withdrawals"]; /** - * Plain (BeaconState-free) inputs for execution payload attributes. Produced by the single state - * reader `resolvePayloadAttributesInput`; consumed by `preparePayloadAttributes` / + * Plain (BeaconState-free) inputs for execution payload attributes. Produced by the engine's single + * state reader `BeaconEngine.resolvePayloadAttributesInput`; consumed by `preparePayloadAttributes` / * `prepareExecutionPayload` / `getPayloadAttributesForSSE`. */ export type PayloadAttributesInput = { @@ -854,65 +783,14 @@ export type PayloadAttributesInput = { withdrawals?: PayloadAttributesWithdrawals; }; -/** - * The single place that reads `BeaconState` for execution payload attributes. Everything downstream - * consumes the returned plain fields and never touches the state. - * - * Post-gloas, when extending a full parent, callers must apply parent execution payload first - * (see `withParentPayloadApplied`) before calling this. - * // TODO - beacon engine: should be a private function of BeaconEngine because it needs BeaconState - */ -export function resolvePayloadAttributesInput( - config: ChainForkConfig, +export function preparePayloadAttributes( fork: ForkPostBellatrix, - state: IBeaconStateViewBellatrix, - parentBlockHash: Bytes32 -): PayloadAttributesInput { - const timestamp = computeTimeAtSlot(config, state.slot, state.genesisTime); - const prevRandao = state.getRandaoMix(state.epoch); - - let withdrawals: PayloadAttributesWithdrawals | undefined; - if (ForkSeq[fork] >= ForkSeq.capella) { - if (!isStatePostCapella(state)) { - throw new Error("Expected Capella state for withdrawals"); - } - - if (isStatePostGloas(state)) { - const isExtendingPayload = byteArrayEquals(parentBlockHash, state.latestExecutionPayloadBid.blockHash); - if (isExtendingPayload) { - // applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch - // here means the caller did not apply parent payload to state - if (!byteArrayEquals(state.latestBlockHash, state.latestExecutionPayloadBid.blockHash)) { - throw new Error("Expected state with parent execution payload applied for withdrawals"); - } - withdrawals = state.getExpectedWithdrawals().expectedWithdrawals; - } else { - // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch - // already deducted from CL balances but never credited on the EL (the envelope - // was not delivered). The next payload must carry those same withdrawals to - // restore CL/EL consistency, otherwise validators permanently lose that balance. - withdrawals = state.payloadExpectedWithdrawals; - } - } else { - // withdrawals logic is now fork aware as it changes on electra fork post capella - withdrawals = state.getExpectedWithdrawals().expectedWithdrawals; - } - } - - return {timestamp, prevRandao, withdrawals}; -} - -function preparePayloadAttributes( - fork: ForkPostBellatrix, - chain: { - config: ChainForkConfig; - forkChoice: IForkChoiceRead; - proposerPreferencesPool: ProposerPreferencesPool; - }, + // gloas: resolved by the engine (see `BeaconEngine.getProposerTargetGasLimit`) and passed in as plain + // data so this shared helper does not touch fork choice / the proposer-preferences pool; undefined pre-gloas + targetGasLimit: number | undefined, { prepareSlot, parentBlockRoot, - parentBlockHash, feeRecipient, timestamp, prevRandao, @@ -920,7 +798,6 @@ function preparePayloadAttributes( }: { prepareSlot: Slot; parentBlockRoot: Root; - parentBlockHash: Bytes32; feeRecipient: string; timestamp: number; prevRandao: Bytes32; @@ -945,66 +822,12 @@ function preparePayloadAttributes( } if (ForkSeq[fork] >= ForkSeq.gloas) { + if (targetGasLimit === undefined) { + throw new Error("Expected targetGasLimit for post-gloas payload attributes"); + } (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).slotNumber = prepareSlot; - (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).targetGasLimit = getProposerTargetGasLimit( - chain, - prepareSlot, - parentBlockRoot, - parentBlockHash - ); + (payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).targetGasLimit = targetGasLimit; } return payloadAttributes; } - -/** - * Resolve the proposer's preferred (target) gas limit for the Gloas `PayloadAttributesV4` - * `targetGasLimit` field (consensus-specs#5235, execution-apis#796). - * - * Sourced from the `SignedProposerPreferences` the proposer's VC submitted to the pool - * (same `(slot, dependent_root)` lookup as gossip bid validation). When no matching - * preferences are pooled, target the parent payload's gas limit so the gas limit stays - * unchanged (`is_gas_limit_target_compatible` then requires `gas_limit == parent_gas_limit`). - * - * The parent payload's gas_limit is read from fork choice — the variant matching - * `(parentBlockRoot, parentBlockHash)` carries the correct value for both FULL parents - * (FULL.executionPayloadGasLimit = delivered payload's gas_limit) and EMPTY parents - * (EMPTY.executionPayloadGasLimit = inherited grandparent's gas_limit). - */ -function getProposerTargetGasLimit( - chain: {forkChoice: IForkChoiceRead; proposerPreferencesPool: ProposerPreferencesPool}, - prepareSlot: Slot, - parentBlockRoot: Root, - parentBlockHash: Bytes32 -): number { - const parentBlockRootHex = toRootHex(parentBlockRoot); - const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex); - const dependentRootHex = (() => { - if (parentBlock === null) { - return null; - } - try { - return getShufflingDependentRoot( - chain.forkChoice, - computeEpochAtSlot(prepareSlot), - computeEpochAtSlot(parentBlock.slot), - parentBlock - ); - } catch { - return null; - } - })(); - - const pref = dependentRootHex !== null ? chain.proposerPreferencesPool.get(prepareSlot, dependentRootHex) : null; - if (pref !== null) { - return pref.message.targetGasLimit; - } - - const parentPayloadVariant = chain.forkChoice.getBlockHexAndBlockHash(parentBlockRootHex, toRootHex(parentBlockHash)); - if (parentPayloadVariant === null || parentPayloadVariant.executionPayloadBlockHash === null) { - throw new Error( - `Cannot resolve parent payload gas_limit for proposer targetGasLimit fallback parentBlockRoot=${parentBlockRootHex} parentBlockHash=${toRootHex(parentBlockHash)}` - ); - } - return parentPayloadVariant.executionPayloadGasLimit; -} diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index 0de753f76b9a..68f5c5635131 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -216,6 +216,8 @@ export function getMockedBeaconChain(opts?: Partial): (chain as {beaconEngine: unknown}).beaconEngine = chain; // Duty flows are real BeaconEngine methods; bind the real implementations (and the private helpers // they call) so duty tests exercise real logic against the mock's collaborators. + // `prepareForNextSlot` is a real BeaconEngine method too; `recomputeForkChoiceHead` / `predictProposerHead` + // stay as mock fns above so tests can control head selection. for (const name of [ "getProposerDuties", "getAttesterDuties", @@ -224,6 +226,11 @@ export function getMockedBeaconChain(opts?: Partial): "getHeadStateAtEpoch", "waitForCheckpointState", "genesisBlockRoot", + "prepareForNextSlot", + "computeStateHashTreeRoot", + "resolvePayloadAttributesInput", + "getPayloadAttributesForSSE", + "getProposerTargetGasLimit", ] as const) { (chain as unknown as Record)[name] = ( BeaconEngine.prototype as unknown as Record From cc46c047b003468b548230665a8a328de733c4ea Mon Sep 17 00:00:00 2001 From: twoeths Date: Mon, 29 Jun 2026 16:07:25 +0700 Subject: [PATCH 12/24] fix: api to pass bytes to BeaconEngine --- .../src/api/impl/beacon/blocks/index.ts | 30 +++++-- .../src/api/impl/beacon/pool/index.ts | 34 ++++++-- .../src/api/impl/validator/index.ts | 25 ++++-- .../src/chain/beaconEngine/beaconEngine.ts | 8 +- .../src/chain/beaconEngine/interface.ts | 12 ++- .../src/chain/validation/syncCommittee.ts | 3 +- packages/beacon-node/src/util/sszBytes.ts | 81 +++++++++++++++++++ .../test/unit/util/sszBytes.test.ts | 74 ++++++++++++++++- 8 files changed, 239 insertions(+), 28 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 196bb31eb1cd..af895e2ab4bd 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -31,6 +31,7 @@ import { fulu, gloas, isDenebBlockContents, + ssz, sszTypesFor, } from "@lodestar/types"; import {fromHex, sleep, toHex, toRootHex} from "@lodestar/utils"; @@ -64,6 +65,7 @@ import { import {isOptimisticBlock} from "../../../../util/forkChoice.js"; import {kzg} from "../../../../util/kzg.js"; import {promiseAllMaybeAsync} from "../../../../util/promises.js"; +import {getSignedBlockBytesFromSignedBlockContentsSerialized} from "../../../../util/sszBytes.js"; import {ApiModules} from "../../types.js"; import {assertUniqueItems} from "../../utils.js"; import {getBlockResponse, toBeaconHeaderResponse} from "./utils.js"; @@ -93,7 +95,7 @@ export function getBeaconBlockApi({ >): ApplicationMethods { const publishBlockV2: ApplicationMethods["publishBlockV2"] = async ( {signedBlockContents, broadcastValidation}, - _context, + context, opts: PublishBlockOpts = {} ) => { const seenTimestampSec = Date.now() / 1000; @@ -199,8 +201,15 @@ export function getBeaconBlockApi({ switch (broadcastValidation) { case routes.beacon.BroadcastValidation.gossip: { if (!blockLocallyProduced) { - // TODO - engine: get block bytes from upstream - const blockBytes = config.getForkTypes(slot).SignedBeaconBlock.serialize(signedBlock); + let blockBytes: Uint8Array; + if (context?.sszBytes) { + blockBytes = + isForkPostDeneb(fork) && !isForkPostGloas(fork) + ? getSignedBlockBytesFromSignedBlockContentsSerialized(context.sszBytes) + : context.sszBytes; + } else { + blockBytes = config.getForkTypes(slot).SignedBeaconBlock.serialize(signedBlock); + } const res = await chain.beaconEngine.validateGossipBlock(blockBytes, signedBlock, fork); if (res.status !== GossipValidationStatus.Accept) { switch (res.code) { @@ -648,7 +657,7 @@ export function getBeaconBlockApi({ publishBlockV2, publishBlindedBlockV2, - async publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope}) { + async publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope}, context) { const seenTimestampSec = Date.now() / 1000; const envelope = signedExecutionPayloadEnvelope.message; const slot = envelope.payload.slotNumber; @@ -685,7 +694,11 @@ export function getBeaconBlockApi({ if (!payloadInput) { throw new ApiError(404, `Execution payload envelope input not found for beacon block root ${blockRootHex}`); } + // SSZ request: the body IS the envelope. JSON request (no sszBytes): serialize once. + const envelopeBytes = + context?.sszBytes ?? ssz.gloas.SignedExecutionPayloadEnvelope.serialize(signedExecutionPayloadEnvelope); const envelopeValidation = await chain.beaconEngine.validateApiExecutionPayloadEnvelope( + envelopeBytes, signedExecutionPayloadEnvelope, payloadInput.proposerIndex, payloadInput.getBuilderIndex(), @@ -830,7 +843,7 @@ export function getBeaconBlockApi({ }); }, - async publishExecutionPayloadBid({signedExecutionPayloadBid}) { + async publishExecutionPayloadBid({signedExecutionPayloadBid}, context) { const bid = signedExecutionPayloadBid.message; const slot = bid.slot; const fork = config.getForkName(slot); @@ -839,7 +852,12 @@ export function getBeaconBlockApi({ throw new ApiError(400, `publishExecutionPayloadBid not supported for pre-gloas fork=${fork}`); } - const bidValidation = await chain.beaconEngine.validateApiExecutionPayloadBid(signedExecutionPayloadBid); + // SSZ request: the body IS the bid. JSON request (no sszBytes): serialize once. + const bidBytes = context?.sszBytes ?? ssz.gloas.SignedExecutionPayloadBid.serialize(signedExecutionPayloadBid); + const bidValidation = await chain.beaconEngine.validateApiExecutionPayloadBid( + bidBytes, + signedExecutionPayloadBid + ); if (bidValidation.status !== GossipValidationStatus.Accept) { throw bidValidation.error ?? new ApiError(400, `Invalid execution payload bid: ${bidValidation.code}`); } diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index 585b831d39a3..eeb1e22ba46b 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -23,6 +23,7 @@ import {toElectraSingleAttestation} from "../../../../chain/validation/index.js" import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js"; import {validateApiVoluntaryExit} from "../../../../chain/validation/voluntaryExit.js"; import {validateGossipFnRetryUnknownRoot} from "../../../../network/processor/gossipHandlers.js"; +import {getFixedListElementBytes} from "../../../../util/sszBytes.js"; import {ApiError, FailureList, IndexedError} from "../../errors.js"; import {ApiModules} from "../../types.js"; @@ -68,9 +69,15 @@ export function getBeaconPoolApi({ return {data: chain.proposerPreferencesPool.getAll(slot), meta: {version: fork}}; }, - async submitSignedProposerPreferences({signedProposerPreferences}) { + async submitSignedProposerPreferences({signedProposerPreferences}, context) { const failures: FailureList = []; + // SSZ request: slice each entry out of the (fixed-size element) list body. JSON request + // (no sszBytes): serialize once. + const preferencesBytes = context?.sszBytes + ? getFixedListElementBytes(context.sszBytes, ssz.gloas.SignedProposerPreferences.fixedSize) + : signedProposerPreferences.map((s) => ssz.gloas.SignedProposerPreferences.serialize(s)); + await Promise.all( signedProposerPreferences.map(async (signed, i) => { const logCtx = { @@ -79,9 +86,7 @@ export function getBeaconPoolApi({ dependentRoot: toRootHex(signed.message.dependentRoot), }; try { - // TODO - beacon engine: get bytes from the api - const preferencesBytes = ssz.gloas.SignedProposerPreferences.serialize(signed); - const res = await chain.beaconEngine.validateGossipProposerPreferences(preferencesBytes, signed); + const res = await chain.beaconEngine.validateGossipProposerPreferences(preferencesBytes[i], signed); if (res.status !== GossipValidationStatus.Accept) { if (res.code === ProposerPreferencesErrorCode.ALREADY_KNOWN) { logger.debug("Ignoring known signed proposer preferences", logCtx); @@ -267,9 +272,15 @@ export function getBeaconPoolApi({ } }, - async submitPayloadAttestationMessages({payloadAttestationMessages}) { + async submitPayloadAttestationMessages({payloadAttestationMessages}, context) { const failures: FailureList = []; + // SSZ request: slice each message out of the (fixed-size element) list body. JSON request + // (no sszBytes): serialize once. + const messageBytes = context?.sszBytes + ? getFixedListElementBytes(context.sszBytes, ssz.gloas.PayloadAttestationMessage.fixedSize) + : payloadAttestationMessages.map((m) => ssz.gloas.PayloadAttestationMessage.serialize(m)); + await Promise.all( payloadAttestationMessages.map(async (payloadAttestationMessage, i) => { const logCtx = { @@ -278,7 +289,8 @@ export function getBeaconPoolApi({ beaconBlockRoot: toRootHex(payloadAttestationMessage.data.beaconBlockRoot), }; try { - const validateFn = () => chain.beaconEngine.validateApiPayloadAttestationMessage(payloadAttestationMessage); + const validateFn = () => + chain.beaconEngine.validateApiPayloadAttestationMessage(messageBytes[i], payloadAttestationMessage); const {slot, beaconBlockRoot} = payloadAttestationMessage.data; const res = await validateGossipFnRetryUnknownRoot(validateFn, network, chain, slot, beaconBlockRoot); if (res.status !== GossipValidationStatus.Accept) { @@ -338,7 +350,7 @@ export function getBeaconPoolApi({ * * https://github.com/ethereum/beacon-APIs/pull/135 */ - async submitPoolSyncCommitteeSignatures({signatures}) { + async submitPoolSyncCommitteeSignatures({signatures}, context) { // Fetch states for all slots of the `signatures` const slots = new Set(); for (const signature of signatures) { @@ -351,6 +363,12 @@ export function getBeaconPoolApi({ throw new ApiError(400, "Sync committee pool is not supported before Altair"); } + // SSZ request: slice each message out of the (fixed-size element) list body. JSON request + // (no sszBytes): serialize once. Never re-serialize on the SSZ path. + const signatureBytes = context?.sszBytes + ? getFixedListElementBytes(context.sszBytes, ssz.altair.SyncCommitteeMessage.fixedSize) + : signatures.map((s) => ssz.altair.SyncCommitteeMessage.serialize(s)); + const failures: FailureList = []; await Promise.all( @@ -364,7 +382,7 @@ export function getBeaconPoolApi({ // Verify signature only, all other data is very likely to be correct, since the `signature` object is created by this node. // Worst case if `signature` is not valid, gossip peers will drop it and slightly downscore us. - const res = await chain.beaconEngine.validateApiSyncCommittee(state, signature); + const res = await chain.beaconEngine.validateApiSyncCommittee(signatureBytes[i], signature); if (res.status !== GossipValidationStatus.Accept) { failures.push({index: i, message: res.error?.message ?? res.code}); logger.verbose( diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 77d0888b01be..34d80640a924 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -39,6 +39,7 @@ import { getValidatorStatus, gloas, ssz, + sszTypesFor, } from "@lodestar/types"; import { TimeoutError, @@ -69,6 +70,7 @@ import {SyncState} from "../../../sync/index.js"; import {isOptimisticBlock} from "../../../util/forkChoice.js"; import {getDefaultGraffiti, toGraffitiBytes} from "../../../util/graffiti.js"; import {getLodestarClientVersion} from "../../../util/metadata.js"; +import {getFixedListElementBytes, getVariableListElementBytes} from "../../../util/sszBytes.js"; import {ApiOptions} from "../../options.js"; import {ApiError, FailureList, IndexedError, NodeIsSyncing, OnlySupportedByDVT} from "../errors.js"; import {ApiModules} from "../types.js"; @@ -1259,13 +1261,19 @@ export function getValidatorApi( }; }, - async publishAggregateAndProofsV2({signedAggregateAndProofs}) { + async publishAggregateAndProofsV2({signedAggregateAndProofs}, context) { notWhileSyncing(); const seenTimestampSec = Date.now() / 1000; const failures: FailureList = []; const fork = chain.config.getForkName(chain.clock.currentSlot); + // SSZ request: slice each aggregate out of the (variable-size element) list body. JSON request + // (no sszBytes): serialize once. Never re-serialize on the SSZ path. + const aggregateBytes = context?.sszBytes + ? getVariableListElementBytes(context.sszBytes) + : signedAggregateAndProofs.map((agg) => sszTypesFor(fork).SignedAggregateAndProof.serialize(agg)); + await Promise.all( signedAggregateAndProofs.map(async (signedAggregateAndProof, i) => { const logCtx = { @@ -1274,7 +1282,8 @@ export function getValidatorApi( }; try { // TODO: Validate in batch - const validateFn = () => chain.beaconEngine.validateApiAggregateAndProof(fork, signedAggregateAndProof); + const validateFn = () => + chain.beaconEngine.validateApiAggregateAndProof(aggregateBytes[i], fork, signedAggregateAndProof); const {slot, beaconBlockRoot} = signedAggregateAndProof.message.aggregate.data; // when a validator is configured with multiple beacon node urls, this attestation may come from another beacon node // and the block hasn't been in our forkchoice since we haven't seen / processing that block @@ -1323,11 +1332,17 @@ export function getValidatorApi( * * https://github.com/ethereum/beacon-APIs/pull/137 */ - async publishContributionAndProofs({contributionAndProofs}) { + async publishContributionAndProofs({contributionAndProofs}, context) { notWhileSyncing(); const failures: FailureList = []; + // SSZ request: slice each entry out of the (fixed-size element) list body. JSON request + // (no sszBytes): serialize once. + const contributionBytes = context?.sszBytes + ? getFixedListElementBytes(context.sszBytes, ssz.altair.SignedContributionAndProof.fixedSize) + : contributionAndProofs.map((c) => ssz.altair.SignedContributionAndProof.serialize(c)); + await Promise.all( contributionAndProofs.map(async (contributionAndProof, i) => { const logCtx = { @@ -1336,10 +1351,8 @@ export function getValidatorApi( }; try { // TODO: Validate in batch - // TODO - engine: get bytes from api - const contributionBytes = ssz.altair.SignedContributionAndProof.serialize(contributionAndProof); const res = await chain.beaconEngine.validateSyncCommitteeGossipContributionAndProof( - contributionBytes, + contributionBytes[i], contributionAndProof, true // skip known participants check ); diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index abdd1e72d909..8db87810fa92 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -1363,10 +1363,10 @@ export class BeaconEngine implements IBeaconEngine { } validateApiSyncCommittee( - headState: IBeaconStateView, + _syncCommitteeBytes: Uint8Array, syncCommittee: altair.SyncCommitteeMessage ): Promise> { - return runGossipValidation(() => validateApiSyncCommittee(this, headState, syncCommittee)); + return runGossipValidation(() => validateApiSyncCommittee(this, syncCommittee)); } validateSyncCommitteeGossipContributionAndProof( @@ -1417,6 +1417,7 @@ export class BeaconEngine implements IBeaconEngine { } validateApiPayloadAttestationMessage( + _payloadAttestationBytes: Uint8Array, payloadAttestationMessage: gloas.PayloadAttestationMessage ): Promise> { return runGossipValidation(() => validateApiPayloadAttestationMessage(this, payloadAttestationMessage)); @@ -1451,6 +1452,7 @@ export class BeaconEngine implements IBeaconEngine { } validateApiAggregateAndProof( + _aggregateBytes: Uint8Array, fork: ForkName, signedAggregateAndProof: SignedAggregateAndProof ): Promise> { @@ -1478,6 +1480,7 @@ export class BeaconEngine implements IBeaconEngine { } validateApiExecutionPayloadEnvelope( + _envelopeBytes: Uint8Array, executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope, proposerIndex: ValidatorIndex, bidBuilderIndex: ValidatorIndex, @@ -1504,6 +1507,7 @@ export class BeaconEngine implements IBeaconEngine { } validateApiExecutionPayloadBid( + _bidBytes: Uint8Array, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid ): Promise> { return runGossipValidation(() => validateApiExecutionPayloadBid(this, signedExecutionPayloadBid)); diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index dcdd5692b1d4..fc8fdc1df23f 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -2,7 +2,7 @@ import {routes} from "@lodestar/api"; import {BeaconConfig} from "@lodestar/config"; import {BlockExecutionStatus, IForkChoice, PayloadExecutionStatus, ProtoBlock} from "@lodestar/fork-choice"; import {ForkName, ForkPostBellatrix} from "@lodestar/params"; -import {DataAvailabilityStatus, IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; +import {DataAvailabilityStatus, PubkeyCache} from "@lodestar/state-transition"; import { BLSPubkey, BLSSignature, @@ -229,8 +229,8 @@ export interface IBeaconEngine { currentEpoch: Epoch ): Promise<{data: routes.validator.PtcDuty[]; dependentRoot: Root; head: ProtoBlock}>; - // Gossip validation flows. The first parameter is the message's SSZ bytes (unused by the JS engine, - // required by the native engine's bytes-first contract), followed by the deserialized object. Each + // Gossip + API validation flows. The first parameter is the message's SSZ bytes (unused by the JS + // engine, required by the native engine's bytes-first contract), followed by the deserialized object. Each // returns a `GossipValidationResult` (no throw) so the native engine can return outcomes across FFI. validateGossipBlock( blockBytes: Uint8Array, @@ -243,7 +243,7 @@ export interface IBeaconEngine { subnet: SubnetID ): Promise>; validateApiSyncCommittee( - headState: IBeaconStateView, + syncCommitteeBytes: Uint8Array, syncCommittee: altair.SyncCommitteeMessage ): Promise>; validateSyncCommitteeGossipContributionAndProof( @@ -273,6 +273,7 @@ export interface IBeaconEngine { payloadAttestationMessage: gloas.PayloadAttestationMessage ): Promise>; validateApiPayloadAttestationMessage( + payloadAttestationBytes: Uint8Array, payloadAttestationMessage: gloas.PayloadAttestationMessage ): Promise>; validateGossipAttestationsSameAttData( @@ -289,6 +290,7 @@ export interface IBeaconEngine { signedAggregateAndProof: SignedAggregateAndProof ): Promise>; validateApiAggregateAndProof( + aggregateBytes: Uint8Array, fork: ForkName, signedAggregateAndProof: SignedAggregateAndProof ): Promise>; @@ -303,6 +305,7 @@ export interface IBeaconEngine { bidExecutionRequestsRoot: Root ): Promise>; validateApiExecutionPayloadEnvelope( + envelopeBytes: Uint8Array, executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope, proposerIndex: ValidatorIndex, bidBuilderIndex: ValidatorIndex, @@ -314,6 +317,7 @@ export interface IBeaconEngine { signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid ): Promise>; validateApiExecutionPayloadBid( + bidBytes: Uint8Array, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid ): Promise>; validateGossipProposerPreferences( diff --git a/packages/beacon-node/src/chain/validation/syncCommittee.ts b/packages/beacon-node/src/chain/validation/syncCommittee.ts index 57ccdcd672a7..ece83579c103 100644 --- a/packages/beacon-node/src/chain/validation/syncCommittee.ts +++ b/packages/beacon-node/src/chain/validation/syncCommittee.ts @@ -73,9 +73,10 @@ export async function validateGossipSyncCommittee( export async function validateApiSyncCommittee( engine: BeaconEngine, - headState: IBeaconStateView, syncCommittee: altair.SyncCommitteeMessage ): Promise { + // Resolve head state internally — no IBeaconStateView crosses the engine seam. + const headState = engine.getHeadState(); const prioritizeBls = true; return validateSyncCommitteeSigOnly(engine, headState, syncCommittee, prioritizeBls); } diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index fce206edac0a..a193ac0b6b01 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -851,3 +851,84 @@ export function getBlobKzgCommitmentsCountFromSignedBeaconBlockSerialized( return Math.round(((end > blockBytes.byteLength ? blockBytes.byteLength : end) - start) / commitmentSize); } + +/** Read a 4-byte little-endian SSZ offset at `position`. */ +function readOffset(data: Uint8Array, position: number): number { + return (data[position] | (data[position + 1] << 8) | (data[position + 2] << 16) | (data[position + 3] << 24)) >>> 0; +} + +/** + * Slice each element's raw bytes out of a serialized SSZ List of FIXED-size elements (e.g. + * SyncCommitteeMessage, SingleAttestation, PayloadAttestationMessage). A fixed-element list serializes as + * the elements concatenated, so element `i` is the `elementSize`-wide window at `i * elementSize`. + * Returned slices are zero-copy `.subarray()` views into `listBytes` — no allocation per element. + */ +export function getFixedListElementBytes(listBytes: Uint8Array, elementSize: number | null): Uint8Array[] { + // `elementSize` is typically `.fixedSize`, which is `number | null`; null means a variable-size + // element type (wrong slicer — use getVariableListElementBytes). + if (elementSize === null || elementSize <= 0) { + throw new Error(`Invalid SSZ fixed element size ${elementSize}`); + } + if (listBytes.length % elementSize !== 0) { + throw new Error(`Serialized list length ${listBytes.length} is not a multiple of element size ${elementSize}`); + } + const count = listBytes.length / elementSize; + const elements = new Array(count); + for (let i = 0; i < count; i++) { + elements[i] = listBytes.subarray(i * elementSize, (i + 1) * elementSize); + } + return elements; +} + +/** + * Slice each element's raw bytes out of a serialized SSZ List of VARIABLE-size elements (e.g. + * SignedAggregateAndProof). Such a list begins with a 4-byte offset table (one entry per element); the + * first offset equals `4 * count`, element `i` spans `[offset[i], offset[i+1])`, and the last element + * ends at `listBytes.length`. Returned slices are zero-copy `.subarray()` views into `listBytes`. + */ +export function getVariableListElementBytes(listBytes: Uint8Array): Uint8Array[] { + if (listBytes.length === 0) { + return []; + } + if (listBytes.length < VARIABLE_FIELD_OFFSET) { + throw new Error(`Serialized variable list too short: ${listBytes.length}`); + } + const firstOffset = readOffset(listBytes, 0); + if (firstOffset % VARIABLE_FIELD_OFFSET !== 0 || firstOffset === 0 || firstOffset > listBytes.length) { + throw new Error(`Invalid first offset ${firstOffset} for serialized variable list of length ${listBytes.length}`); + } + const count = firstOffset / VARIABLE_FIELD_OFFSET; + const elements = new Array(count); + for (let i = 0; i < count; i++) { + const start = readOffset(listBytes, i * VARIABLE_FIELD_OFFSET); + const end = i === count - 1 ? listBytes.length : readOffset(listBytes, (i + 1) * VARIABLE_FIELD_OFFSET); + if (start > end || end > listBytes.length) { + throw new Error( + `Invalid element range [${start}, ${end}) for serialized variable list of length ${listBytes.length}` + ); + } + elements[i] = listBytes.subarray(start, end); + } + return elements; +} + +/** + * Slice the `signedBlock` field's bytes out of a serialized post-deneb (pre-gloas) `SignedBlockContents` + * request body. `SignedBlockContents = {signedBlock, kzgProofs, blobs}` is all variable-size, so the body + * opens with a 3-entry offset table and `signedBlock` (the first field) spans `[offset[0], offset[1])`. + * Lets the publish API reuse the request body as gossip block bytes without re-serializing. Zero-copy + * `.subarray()` view. + */ +export function getSignedBlockBytesFromSignedBlockContentsSerialized(contentsBytes: Uint8Array): Uint8Array { + if (contentsBytes.length < 2 * VARIABLE_FIELD_OFFSET) { + throw new Error(`Serialized SignedBlockContents too short: ${contentsBytes.length}`); + } + const start = readOffset(contentsBytes, 0); + const end = readOffset(contentsBytes, VARIABLE_FIELD_OFFSET); + if (start > end || end > contentsBytes.length) { + throw new Error( + `Invalid signedBlock range [${start}, ${end}) in serialized SignedBlockContents of length ${contentsBytes.length}` + ); + } + return contentsBytes.subarray(start, end); +} diff --git a/packages/beacon-node/test/unit/util/sszBytes.test.ts b/packages/beacon-node/test/unit/util/sszBytes.test.ts index a24a30ca0260..805a076f44ed 100644 --- a/packages/beacon-node/test/unit/util/sszBytes.test.ts +++ b/packages/beacon-node/test/unit/util/sszBytes.test.ts @@ -1,5 +1,5 @@ import {describe, expect, it} from "vitest"; -import {BitArray} from "@chainsafe/ssz"; +import {BitArray, ListCompositeType} from "@chainsafe/ssz"; import {createChainForkConfig} from "@lodestar/config"; import {ForkName, MAX_COMMITTEES_PER_SLOT} from "@lodestar/params"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; @@ -38,6 +38,7 @@ import { getCommitteeIndexFromSingleAttestationSerialized, getDataIndexFromSignedAggregateAndProofSerialized, getDataIndexFromSingleAttestationSerialized, + getFixedListElementBytes, getLastProcessedSlotFromBeaconStateSerialized, getParentBlockHashFromGloasSignedBeaconBlockSerialized, getParentBlockHashFromSignedExecutionPayloadBidSerialized, @@ -46,6 +47,7 @@ import { getPayloadPresentFromPayloadAttestationMessageSerialized, getSignatureFromAttestationSerialized, getSignatureFromSingleAttestationSerialized, + getSignedBlockBytesFromSignedBlockContentsSerialized, getSlotFromAttestationSerialized, getSlotFromBeaconStateSerialized, getSlotFromBlobSidecarSerialized, @@ -56,6 +58,7 @@ import { getSlotFromSignedBeaconBlockSerialized, getSlotFromSignedExecutionPayloadBidSerialized, getSlotFromSingleAttestationSerialized, + getVariableListElementBytes, } from "../../../src/util/sszBytes.js"; import {generateRandomBlob} from "../../utils/kzg.js"; @@ -743,6 +746,75 @@ describe("SignedExecutionPayloadBid SSZ serialized picking", () => { }); }); +describe("getFixedListElementBytes", () => { + const SyncCommitteeMessageList = new ListCompositeType(ssz.altair.SyncCommitteeMessage, 1024); + const elementSize = ssz.altair.SyncCommitteeMessage.fixedSize as number; + + for (const count of [0, 1, 5]) { + it(`round-trips ${count} fixed-size elements`, () => { + const items = Array.from({length: count}, (_, i) => { + const msg = ssz.altair.SyncCommitteeMessage.defaultValue(); + msg.slot = i; + msg.validatorIndex = i * 7; + msg.beaconBlockRoot = new Uint8Array(32).fill(i + 1); + return msg; + }); + const listBytes = SyncCommitteeMessageList.serialize(items); + const slices = getFixedListElementBytes(listBytes, elementSize); + expect(slices.length).toBe(count); + for (let i = 0; i < count; i++) { + expect(slices[i]).toEqual(ssz.altair.SyncCommitteeMessage.serialize(items[i])); + expect(ssz.altair.SyncCommitteeMessage.deserialize(slices[i])).toEqual(items[i]); + } + }); + } + + it("throws when length is not a multiple of element size", () => { + expect(() => getFixedListElementBytes(Buffer.alloc(elementSize + 1), elementSize)).toThrow(); + }); +}); + +describe("getVariableListElementBytes", () => { + const SignedAggregateAndProofList = new ListCompositeType(ssz.phase0.SignedAggregateAndProof, 1024); + + for (const count of [0, 1, 4]) { + it(`round-trips ${count} variable-size elements`, () => { + const items = Array.from({length: count}, (_, i) => { + const agg = ssz.phase0.SignedAggregateAndProof.defaultValue(); + agg.message.aggregatorIndex = i; + // distinct, differently-sized aggregationBits so element offsets differ + agg.message.aggregate.aggregationBits = BitArray.fromBoolArray(Array.from({length: i + 1}, () => true)); + agg.message.aggregate.data.slot = i; + return agg; + }); + const listBytes = SignedAggregateAndProofList.serialize(items); + const slices = getVariableListElementBytes(listBytes); + expect(slices.length).toBe(count); + for (let i = 0; i < count; i++) { + expect(slices[i]).toEqual(ssz.phase0.SignedAggregateAndProof.serialize(items[i])); + expect(ssz.phase0.SignedAggregateAndProof.deserialize(slices[i])).toEqual(items[i]); + } + }); + } +}); + +describe("getSignedBlockBytesFromSignedBlockContentsSerialized", () => { + for (const blobCount of [0, 2]) { + it(`slices signedBlock out of SignedBlockContents with ${blobCount} blobs`, () => { + const contents = ssz.deneb.SignedBlockContents.defaultValue(); + contents.signedBlock.message.slot = 123; + contents.signedBlock.message.proposerIndex = 4; + contents.kzgProofs = Array.from({length: blobCount}, () => new Uint8Array(48).fill(7)); + contents.blobs = Array.from({length: blobCount}, () => generateRandomBlob()); + const contentsBytes = ssz.deneb.SignedBlockContents.serialize(contents); + + const blockBytes = getSignedBlockBytesFromSignedBlockContentsSerialized(contentsBytes); + expect(blockBytes).toEqual(ssz.deneb.SignedBeaconBlock.serialize(contents.signedBlock)); + expect(ssz.deneb.SignedBeaconBlock.deserialize(blockBytes)).toEqual(contents.signedBlock); + }); + } +}); + function payloadAttestationMessageFromValues(slot: Slot, blockRoot: RootHex): gloas.PayloadAttestationMessage { const msg = ssz.gloas.PayloadAttestationMessage.defaultValue(); msg.data.slot = slot; From b89e51725765952c900437659d011b3fd628c477 Mon Sep 17 00:00:00 2001 From: twoeths Date: Thu, 2 Jul 2026 15:54:19 +0700 Subject: [PATCH 13/24] feat: archive blocks + states by BeaconEngine --- .../src/chain/archiveStore/archiveStore.ts | 89 ++++-------- .../src/chain/archiveStore/interface.ts | 10 ++ .../chain/archiveStore/utils/archiveBlocks.ts | 130 +++++++++++------- .../src/chain/beaconEngine/beaconEngine.ts | 76 ++++++++++ .../src/chain/beaconEngine/interface.ts | 64 ++++++++- .../src/chain/beaconEngine/options.ts | 5 + .../src/chain/blocks/importBlock.ts | 4 + packages/beacon-node/src/chain/chain.ts | 65 ++++++++- .../beacon-node/src/chain/forkChoice/index.ts | 11 +- packages/beacon-node/src/chain/options.ts | 2 - .../src/metrics/metrics/lodestar.ts | 2 +- .../chain/archiveStore/blockArchiver.test.ts | 50 ++++++- packages/fork-choice/src/forkChoice/store.ts | 10 +- 13 files changed, 380 insertions(+), 138 deletions(-) diff --git a/packages/beacon-node/src/chain/archiveStore/archiveStore.ts b/packages/beacon-node/src/chain/archiveStore/archiveStore.ts index 345970ed2347..849c5a56a6a0 100644 --- a/packages/beacon-node/src/chain/archiveStore/archiveStore.ts +++ b/packages/beacon-node/src/chain/archiveStore/archiveStore.ts @@ -10,9 +10,8 @@ import {ChainEvent} from "../emitter.js"; import {IBeaconChain} from "../interface.js"; import {PROCESS_FINALIZED_CHECKPOINT_QUEUE_LENGTH} from "./constants.js"; import {HistoricalStateRegen} from "./historicalState/historicalStateRegen.js"; -import {ArchiveMode, ArchiveStoreOpts, StateArchiveStrategy} from "./interface.js"; -import {FrequencyStateArchiveStrategy} from "./strategies/frequencyStateArchiveStrategy.js"; -import {archiveBlocks} from "./utils/archiveBlocks.js"; +import {ArchiveStoreOpts, ArchiveStoreTask} from "./interface.js"; +import {migrateFinalizedDA} from "./utils/archiveBlocks.js"; import {pruneHistory} from "./utils/pruneHistory.js"; import {updateBackfillRange} from "./utils/updateBackfillRange.js"; @@ -25,25 +24,14 @@ type ArchiveStoreModules = { type ArchiveStoreInitOpts = ArchiveStoreOpts & {dbName: string; anchorState: {finalizedCheckpoint: Checkpoint}}; -export enum ArchiveStoreTask { - ArchiveBlocks = "archive_blocks", - PruneHistory = "prune_history", - OnFinalizedCheckpoint = "on_finalized_checkpoint", - MaybeArchiveState = "maybe_archive_state", - ForkchoicePrune = "forkchoice_prune", - UpdateBackfillRange = "update_backfill_range", -} - /** * Used for running tasks that depends on some events or are executed * periodically. */ export class ArchiveStore { - private archiveMode: ArchiveMode; private jobQueue: JobItemQueue<[CheckpointWithHex], void>; private archiveDataEpochs?: number; - private readonly statesArchiverStrategy: StateArchiveStrategy; private readonly chain: IBeaconChain; private readonly db: IBeaconDb; private readonly logger: LoggerNode; @@ -60,7 +48,6 @@ export class ArchiveStore { this.metrics = modules.metrics; this.opts = opts; this.signal = signal; - this.archiveMode = opts.archiveMode; this.archiveDataEpochs = opts.archiveDataEpochs; this.jobQueue = new JobItemQueue<[CheckpointWithHex], void>(this.processFinalizedCheckpoint, { @@ -68,17 +55,8 @@ export class ArchiveStore { signal, }); - if (opts.archiveMode === ArchiveMode.Frequency) { - this.statesArchiverStrategy = new FrequencyStateArchiveStrategy( - this.chain.regen, - this.db, - this.logger, - opts, - this.chain.bufferPool - ); - } else { - throw new Error(`State archive strategy "${opts.archiveMode}" currently not supported.`); - } + // State archival (finalized + temp + shutdown) is engine-owned; the ArchiveStore keeps only the + // event wiring and the DA/light-client cleanup driven by migrateFinalized's snapshot. if (!opts.disableArchiveOnCheckpoint) { this.chain.emitter.on(ChainEvent.forkChoiceFinalized, this.onFinalizedCheckpoint); @@ -159,7 +137,7 @@ export class ArchiveStore { * Archive latest finalized state * */ async persistToDisk(): Promise { - return this.statesArchiverStrategy.archiveState(this.chain.forkChoice.getFinalizedCheckpoint()); + return this.chain.beaconEngine.persistFinalizedStateToDisk(); } //------------------------------------------------------------------------- @@ -174,15 +152,9 @@ export class ArchiveStore { }; private onCheckpoint = (): void => { - const headStateRoot = this.chain.forkChoice.getHead().stateRoot; - this.chain.regen.pruneOnCheckpoint( - this.chain.forkChoice.getFinalizedCheckpoint().epoch, - this.chain.forkChoice.getJustifiedCheckpoint().epoch, - headStateRoot - ); - - this.statesArchiverStrategy.onCheckpoint(headStateRoot, this.metrics).catch((err) => { - this.logger.error("Error during state archive", {archiveMode: this.archiveMode}, err); + // Engine prunes regen caches and archives a temp checkpoint state (states DB is engine-owned). + this.chain.beaconEngine.archiveStateOnCheckpoint().catch((err) => { + this.logger.error("Error during state archive", {}, err); }); }; @@ -191,23 +163,12 @@ export class ArchiveStore { const finalizedEpoch = finalized.epoch; this.logger.verbose("Start processing finalized checkpoint", {epoch: finalizedEpoch, rootHex: finalized.rootHex}); - let timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); - await archiveBlocks( - this.chain.config, - this.db, - this.chain.forkChoice, - this.chain.lightClientServer, - this.logger, - finalized, - this.chain.clock.currentEpoch, - this.archiveDataEpochs, - this.chain.opts.persistOrphanedBlocks, - this.chain.opts.persistOrphanedBlocksDir - ); - timer?.({source: ArchiveStoreTask.ArchiveBlocks}); + // Engine-owned persistence, consolidated into one method: canonical blocks hot→cold, finalized + // state archive, fork-choice prune. Returns the snapshot the facade uses for DA/light-client cleanup. + const {snapshot, prunedBlocks} = await this.chain.beaconEngine.migrateFinalized(finalized); if (this.opts.pruneHistory) { - timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); + const timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); await pruneHistory( this.chain.config, this.db, @@ -219,19 +180,19 @@ export class ArchiveStore { timer?.({source: ArchiveStoreTask.PruneHistory}); } - timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); - await this.statesArchiverStrategy.onFinalizedCheckpoint(finalized, this.metrics); - timer?.({source: ArchiveStoreTask.OnFinalizedCheckpoint}); - - // should be after ArchiveBlocksTask to handle restart cleanly - timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); - await this.statesArchiverStrategy.maybeArchiveState(finalized, this.metrics); - timer?.({source: ArchiveStoreTask.MaybeArchiveState}); - - // tasks rely on extended fork choice - timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); - const prunedBlocks = this.chain.beaconEngine.forkChoice.prune(finalized.rootHex); - timer?.({source: ArchiveStoreTask.ForkchoicePrune}); + // Facade cleans the DA / light-client artifacts it still owns, driven by the snapshot. + let timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); + await migrateFinalizedDA( + this.chain.config, + this.db, + this.chain.lightClientServer, + this.logger, + snapshot, + finalizedEpoch, + this.chain.clock.currentEpoch, + this.archiveDataEpochs + ); + timer?.({source: ArchiveStoreTask.ArchiveBlocks}); timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); await updateBackfillRange({chain: this.chain, db: this.db, logger: this.logger}, finalized); diff --git a/packages/beacon-node/src/chain/archiveStore/interface.ts b/packages/beacon-node/src/chain/archiveStore/interface.ts index 51307ddc3f5a..af12fd4d752f 100644 --- a/packages/beacon-node/src/chain/archiveStore/interface.ts +++ b/packages/beacon-node/src/chain/archiveStore/interface.ts @@ -9,6 +9,16 @@ export enum ArchiveMode { // Differential = "diff", } +/** `processFinalizedCheckpoint.durationByTask` label — some tasks now run inside `engine.migrateFinalized`. */ +export enum ArchiveStoreTask { + ArchiveBlocks = "archive_blocks", + PruneHistory = "prune_history", + OnFinalizedCheckpoint = "on_finalized_checkpoint", + MaybeArchiveState = "maybe_archive_state", + ForkchoicePrune = "forkchoice_prune", + UpdateBackfillRange = "update_backfill_range", +} + export interface StatesArchiveOpts { /** * Minimum number of epochs between archived states diff --git a/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts b/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts index 9d20d228babf..9e2fd2dd503c 100644 --- a/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts +++ b/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts @@ -1,7 +1,7 @@ import path from "node:path"; import {ChainForkConfig} from "@lodestar/config"; import {KeyValue} from "@lodestar/db"; -import {CheckpointWithHex, IForkChoiceRead, PayloadStatus, ProtoBlock} from "@lodestar/fork-choice"; +import {CheckpointWithHex, PayloadStatus, ProtoBlock} from "@lodestar/fork-choice"; import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params"; import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {Epoch, Slot} from "@lodestar/types"; @@ -10,6 +10,7 @@ import {IBeaconDb} from "../../../db/index.js"; import {BlockArchiveBatchPutBinaryItem} from "../../../db/repositories/index.js"; import {ensureDir, writeIfNotExist} from "../../../util/file.js"; import {BlockRootHex} from "../../../util/sszBytes.js"; +import {FinalizedBlockSnapshot, FinalizedProtoSummary} from "../../beaconEngine/interface.js"; import {LightClientServer} from "../../lightClient/index.js"; // Process in chunks to avoid OOM @@ -38,37 +39,25 @@ async function persistOrphanedBlock( } /** - * Archives finalized blocks from active bucket to archive bucket. + * Engine-owned block persistence for a finalized checkpoint: migrate canonical blocks hot→cold and + * delete (optionally persist) non-canonical blocks. Blocks are engine-owned; the DA sidecars tied to + * these blocks are cleaned facade-side (`migrateFinalizedDA`) from the returned snapshot. * - * Only archive blocks on the same chain to the finalized checkpoint. - * Each run should move all finalized blocks to blockArhive db to make it consistent - * to stateArchive, so that the node always work well when we restart. - * Note that the finalized block still stay in forkchoice to check finalize checkpoint of next onBlock calls, - * the next run should not reprocess finalzied block of this run. + * Only archive blocks on the same chain to the finalized checkpoint. Each run should move all finalized + * blocks to blockArchive db to keep it consistent with stateArchive, so the node always works well on + * restart. The finalized block stays in fork choice to check the finalized checkpoint of the next onBlock + * calls; the next run should not reprocess a finalized block of this run. */ -export async function archiveBlocks( - config: ChainForkConfig, +export async function migrateFinalizedBlocks( db: IBeaconDb, - forkChoice: IForkChoiceRead, - lightclientServer: LightClientServer | undefined, logger: Logger, + finalizedCanonicalBlocks: ProtoBlock[], + finalizedNonCanonicalBlocks: ProtoBlock[], finalizedCheckpoint: CheckpointWithHex, currentEpoch: Epoch, - archiveDataEpochs?: number, persistOrphanedBlocks?: boolean, persistOrphanedBlocksDir?: string ): Promise { - // Use fork choice to determine the blocks to archive and delete. - // `ancestors` is the canonical walk back from the finalized root, including the previous finalized - // block as its last element. - const {ancestors: finalizedCanonicalBlocks, nonAncestors: finalizedNonCanonicalBlocks} = - forkChoice.getAllAncestorAndNonAncestorBlocksDefaultStatus(finalizedCheckpoint.rootHex); - - // NOTE: The finalized block will be exactly the first block of `epoch` or previous - const finalizedPostDeneb = finalizedCheckpoint.epoch >= config.DENEB_FORK_EPOCH; - const finalizedPostFulu = finalizedCheckpoint.epoch >= config.FULU_FORK_EPOCH; - const finalizedPostGloas = finalizedCheckpoint.epoch >= config.GLOAS_FORK_EPOCH; - const finalizedCanonicalBlockRoots: BlockRootSlot[] = finalizedCanonicalBlocks.map((block) => ({ slot: block.slot, root: fromHex(block.blockRoot), @@ -86,7 +75,69 @@ export async function archiveBlocks( migratedEntries: migratedSlots.length, slotRange: prettyPrintIndices(migratedSlots), }); + } + + const nonCanonicalBlockRoots = finalizedNonCanonicalBlocks.map((summary) => fromHex(summary.blockRoot)); + if (nonCanonicalBlockRoots.length > 0) { + if (persistOrphanedBlocks) { + // Persist orphaned blocks to disk before deleting them from hot db + await Promise.all( + nonCanonicalBlockRoots.map(async (root, index) => { + const block = finalizedNonCanonicalBlocks[index]; + const blockBytes = await db.block.getBinary(root); + const blockLogCtx = {slot: block.slot, root: block.blockRoot}; + if (blockBytes) { + await persistOrphanedBlock(block.slot, block.blockRoot, blockBytes, { + persistOrphanedBlocksDir: persistOrphanedBlocksDir ?? "orphaned_blocks", + }); + logger.verbose("Persisted orphaned block", {...logCtx, ...blockLogCtx}); + } else { + logger.warn("Tried to persist orphaned block but no block found", {...logCtx, ...blockLogCtx}); + } + }) + ); + } + + const nonCanonicalSlots = finalizedNonCanonicalBlocks.map((summary) => summary.slot).sort((a, b) => a - b); + await db.block.batchDelete(nonCanonicalBlockRoots); + logger.verbose("Deleted non canonical blocks from hot DB", { + ...logCtx, + count: nonCanonicalBlockRoots.length, + slotRange: prettyPrintIndices(nonCanonicalSlots), + }); + } +} + +/** + * Facade-owned DA + light-client cleanup for a finalized checkpoint. Everything it needs comes from the + * engine's returned `snapshot` (canonical/non-canonical block summaries) — it never reads fork choice, + * which the engine already pruned against. Blocks + states are engine-owned (see `migrateFinalizedBlocks`); + * here we migrate/prune the blob/data-column/payload-envelope sidecars and prune light-client data. + */ +export async function migrateFinalizedDA( + config: ChainForkConfig, + db: IBeaconDb, + lightclientServer: LightClientServer | undefined, + logger: Logger, + snapshot: FinalizedBlockSnapshot, + finalizedEpoch: Epoch, + currentEpoch: Epoch, + archiveDataEpochs?: number +): Promise { + const {canonical: finalizedCanonicalBlocks, nonCanonical: finalizedNonCanonicalBlocks} = snapshot; + + const finalizedPostDeneb = finalizedEpoch >= config.DENEB_FORK_EPOCH; + const finalizedPostFulu = finalizedEpoch >= config.FULU_FORK_EPOCH; + const finalizedPostGloas = finalizedEpoch >= config.GLOAS_FORK_EPOCH; + const finalizedCanonicalBlockRoots: BlockRootSlot[] = finalizedCanonicalBlocks.map((block) => ({ + slot: block.slot, + root: fromHex(block.blockRoot), + })); + + const logCtx = {currentEpoch, finalizedEpoch}; + + if (finalizedCanonicalBlockRoots.length > 0) { if (finalizedPostDeneb) { const migratedEntries = await migrateBlobSidecarsFromHotToColdDb( config, @@ -113,6 +164,8 @@ export async function archiveBlocks( }); } + // TODO - beacon engine: move payload db to BeaconEngine (execution payload envelopes are + // consensus data consumed by block production / prepareNextSlot / STF, not DA like blobs/columns). if (finalizedPostGloas) { const migratedSlots = await migrateExecutionPayloadEnvelopesFromHotToColdDb( config, @@ -128,30 +181,8 @@ export async function archiveBlocks( } } - // deleteNonCanonicalBlocks - // loop through forkchoice single time - const nonCanonicalBlockRoots = finalizedNonCanonicalBlocks.map((summary) => fromHex(summary.blockRoot)); if (nonCanonicalBlockRoots.length > 0) { - if (persistOrphanedBlocks) { - // Persist orphaned blocks to disk before deleting them from hot db - await Promise.all( - nonCanonicalBlockRoots.map(async (root, index) => { - const block = finalizedNonCanonicalBlocks[index]; - const blockBytes = await db.block.getBinary(root); - const blockLogCtx = {slot: block.slot, root: block.blockRoot}; - if (blockBytes) { - await persistOrphanedBlock(block.slot, block.blockRoot, blockBytes, { - persistOrphanedBlocksDir: persistOrphanedBlocksDir ?? "orphaned_blocks", - }); - logger.verbose("Persisted orphaned block", {...logCtx, ...blockLogCtx}); - } else { - logger.warn("Tried to persist orphaned block but no block found", {...logCtx, ...blockLogCtx}); - } - }) - ); - } - const nonCanonicalSlots = finalizedNonCanonicalBlocks.map((summary) => summary.slot).sort((a, b) => a - b); const nonCanonicalLogCtx = { ...logCtx, @@ -159,9 +190,6 @@ export async function archiveBlocks( slotRange: prettyPrintIndices(nonCanonicalSlots), }; - await db.block.batchDelete(nonCanonicalBlockRoots); - logger.verbose("Deleted non canonical blocks from hot DB", nonCanonicalLogCtx); - if (finalizedPostDeneb) { await db.blobSidecars.batchDelete(nonCanonicalBlockRoots); logger.verbose("Deleted non canonical blobSidecars from hot DB", nonCanonicalLogCtx); @@ -249,7 +277,7 @@ export async function archiveBlocks( await lightclientServer.pruneNonCheckpointData(nonCheckpointBlockRoots); } - logger.verbose("Archiving of finalized blocks complete", { + logger.verbose("Archiving of finalized block DA complete", { ...logCtx, totalArchived: finalizedCanonicalBlocks.length, }); @@ -376,7 +404,7 @@ async function migrateDataColumnSidecarsFromHotToColdDb( config: ChainForkConfig, db: IBeaconDb, logger: Logger, - canonicalBlocks: ProtoBlock[], + canonicalBlocks: FinalizedProtoSummary[], currentEpoch: Epoch ): Promise { const columnBlocks = canonicalBlocks.filter( @@ -449,7 +477,7 @@ async function migrateExecutionPayloadEnvelopesFromHotToColdDb( config: ChainForkConfig, db: IBeaconDb, logger: Logger, - canonicalBlocks: ProtoBlock[] + canonicalBlocks: FinalizedProtoSummary[] ): Promise { const payloadBlocks = canonicalBlocks.filter( (block) => config.getForkSeq(block.slot) < ForkSeq.gloas || block.payloadStatus === PayloadStatus.FULL diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 8db87810fa92..921342da86ee 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -82,6 +82,9 @@ import {IClock} from "../../util/clock.js"; import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; import {callInNextEventLoop} from "../../util/eventLoop.js"; import {isOptimisticBlock} from "../../util/forkChoice.js"; +import {ArchiveMode, ArchiveStoreTask, StateArchiveStrategy} from "../archiveStore/interface.js"; +import {FrequencyStateArchiveStrategy} from "../archiveStore/strategies/frequencyStateArchiveStrategy.js"; +import {migrateFinalizedBlocks} from "../archiveStore/utils/archiveBlocks.js"; import {CheckpointBalancesCache} from "../balancesCache.js"; import {BeaconProposerCache} from "../beaconProposerCache.js"; import {isBlockInputBlobs, isBlockInputColumns} from "../blocks/blockInput/blockInput.js"; @@ -171,8 +174,10 @@ import {getPubkeysForIndices} from "./duties.js"; import {GossipValidationResult, fromResult, runGossipValidation} from "./gossipValidationResult.js"; import { BeaconEngineModules, + FinalizedProtoSummary, IBeaconEngine, ImportBlockResult, + MigrateFinalizedResult, PrepareNextSlotResult, ProduceBlockBaseResult, } from "./interface.js"; @@ -242,6 +247,9 @@ export class BeaconEngine implements IBeaconEngine { // Engine reads execution payload envelopes for block production (getParentExecutionRequests). Writes // / archival / network handlers still use the shared `db` directly until the DB-ownership phase. readonly db: IBeaconDb; + // Engine owns state archival (states DB). Finalized-state archive runs inside migrateFinalized; the + // facade delegates temp-state (onCheckpoint) and shutdown persistence to engine methods. + private readonly stateArchiveStrategy: StateArchiveStrategy; lightClientServer: LightClientServer | undefined; private readonly verifiedBlocks = new Map(); private readonly emitter: ChainEventEmitter; @@ -356,6 +364,13 @@ export class BeaconEngine implements IBeaconEngine { emitter, signal, }); + + // Engine owns the states DB, so it owns the state-archive strategy (finalized + temp + shutdown). + if (opts.archiveMode === ArchiveMode.Frequency) { + this.stateArchiveStrategy = new FrequencyStateArchiveStrategy(this.regen, db, logger, opts, bufferPool); + } else { + throw new Error(`State archive strategy "${opts.archiveMode}" currently not supported.`); + } } getHeadState(): IBeaconStateView { @@ -1975,6 +1990,7 @@ export class BeaconEngine implements IBeaconEngine { currFinalizedEpoch, oldHeadBlockRoot, newHeadBlockRoot: newHead.blockRoot, + newHead, attestations: attestationsResult, blockMeta: { slot: blockSlot, @@ -2047,6 +2063,61 @@ export class BeaconEngine implements IBeaconEngine { } } + // --- DB ownership (blocks + states) --- + + async migrateFinalized(finalized: CheckpointWithHex): Promise { + // Snapshot from the engine's own fork choice: canonical ancestors (newest→oldest, last = previous + // finalized boundary) + non-canonical (orphaned) blocks. The facade cleans DA/light-client from this. + const {ancestors, nonAncestors} = this.forkChoice.getAllAncestorAndNonAncestorBlocksDefaultStatus( + finalized.rootHex + ); + const snapshot = { + canonical: ancestors.map(toFinalizedProtoSummary), + nonCanonical: nonAncestors.map(toFinalizedProtoSummary), + }; + + // 1. Migrate canonical blocks hot→cold + delete non-canonical blocks (blocks are engine-owned). + let timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); + await migrateFinalizedBlocks( + this.db, + this.logger, + ancestors, + nonAncestors, + finalized, + this.clock.currentEpoch, + this.opts.persistOrphanedBlocks, + this.opts.persistOrphanedBlocksDir + ); + timer?.({source: ArchiveStoreTask.ArchiveBlocks}); + + // 2. Archive the finalized state (states are engine-owned). MUST run after block migration so a + // restart never sees an archived state whose canonical blocks are still only in hot db. + timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); + await this.stateArchiveStrategy.maybeArchiveState(finalized, this.metrics); + timer?.({source: ArchiveStoreTask.MaybeArchiveState}); + + // 3. Prune fork choice (in-memory) after the DB writes committed. + timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); + const prunedBlocks = this.forkChoice.prune(finalized.rootHex); + timer?.({source: ArchiveStoreTask.ForkchoicePrune}); + + return {snapshot, prunedBlocks}; + } + + async persistFinalizedStateToDisk(): Promise { + return this.stateArchiveStrategy.archiveState(this.forkChoice.getFinalizedCheckpoint()); + } + + async archiveStateOnCheckpoint(): Promise { + const headStateRoot = this.forkChoice.getHead().stateRoot; + this.regen.pruneOnCheckpoint( + this.forkChoice.getFinalizedCheckpoint().epoch, + this.forkChoice.getJustifiedCheckpoint().epoch, + headStateRoot + ); + await this.stateArchiveStrategy.onCheckpoint(headStateRoot, this.metrics); + } + // TODO - beacon engine: scalar state reads (getBeaconProposer, getValidator, getBalance, // getRandaoMix, getBlockRootAtSlot, getStateRootAtSlot, getShufflingDecisionRoot). Deferred — // signature shape (state vs stateRoot) to be decided alongside Phase 4 bytes-first. @@ -2140,3 +2211,8 @@ export class BeaconEngine implements IBeaconEngine { return {state: blockState, stateId: "block_state_any_epoch", shouldWarn: true}; } } + +/** Reduce a ProtoBlock to the plain scalars the facade needs for DA/light-client cleanup. */ +function toFinalizedProtoSummary(block: ProtoBlock): FinalizedProtoSummary { + return {slot: block.slot, blockRoot: block.blockRoot, payloadStatus: block.payloadStatus}; +} diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index fc8fdc1df23f..1e6963b160f4 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -1,6 +1,13 @@ import {routes} from "@lodestar/api"; import {BeaconConfig} from "@lodestar/config"; -import {BlockExecutionStatus, IForkChoice, PayloadExecutionStatus, ProtoBlock} from "@lodestar/fork-choice"; +import { + BlockExecutionStatus, + CheckpointWithHex, + IForkChoice, + PayloadExecutionStatus, + PayloadStatus, + ProtoBlock, +} from "@lodestar/fork-choice"; import {ForkName, ForkPostBellatrix} from "@lodestar/params"; import {DataAvailabilityStatus, PubkeyCache} from "@lodestar/state-transition"; import { @@ -84,6 +91,12 @@ export type ImportBlockResult = { } | null; reorg: ReorgEventData | null; blockSummary: ProtoBlock | null; + /** + * Head ProtoBlock after this import (result of updateAndGetHead). The facade caches it and derives + * the finalized/justified transition (its `finalized*`/`justified*` fields) + `getStatus` from it, + * instead of the engine emitting `forkChoiceFinalized`/`forkChoiceJustified` (FFI-honest). + */ + newHead: ProtoBlock; proposerIndexNextSlot: number | null; isExecutionState: boolean; prevFinalizedEpoch: number; @@ -166,6 +179,36 @@ export type PrepareNextSlotResult = { sse: {data: SSEPayloadAttributes; version: ForkName} | null; }; +/** + * Plain-scalar summary of a finalized proto-block returned from `migrateFinalized`. No `BeaconState` + * and no live `ProtoBlock` handle crosses the seam — only the fields the facade needs to migrate/prune + * the DA + light-client artifacts it still owns. + */ +export type FinalizedProtoSummary = { + slot: Slot; + /** hex, as ProtoBlock carries it; the facade does `fromHex()` where it needs bytes */ + blockRoot: RootHex; + /** gloas FULL-gate for column/payload-envelope migration (`getForkSeq(slot) < gloas || FULL`) */ + payloadStatus: PayloadStatus; +}; + +/** + * Canonical/non-canonical block snapshot for a finalized checkpoint, computed by the engine from its + * own fork choice. The facade drives all DA/light-client cleanup from this (never re-reading fork choice). + */ +export type FinalizedBlockSnapshot = { + /** canonical ancestors of the finalized root, newest→oldest; last = previous-finalized boundary block */ + canonical: FinalizedProtoSummary[]; + /** non-canonical (orphaned) blocks pruned by this finalization */ + nonCanonical: FinalizedProtoSummary[]; +}; + +export type MigrateFinalizedResult = { + snapshot: FinalizedBlockSnapshot; + /** fork-choice prune result; facade only reads `.length` for its log */ + prunedBlocks: ProtoBlock[]; +}; + /** * The consensus engine seam. Starts minimal and transitional (JS-only); ownership of consensus * collaborators and flows migrates here across later phases. This interface is the contract shared @@ -340,4 +383,23 @@ export interface IBeaconEngine { opts: ImportBlockOpts ): Promise; discardVerifiedBlocks(blockRootHexes: RootHex[]): void; + + // --- DB ownership (blocks + states) --- + + /** + * Process a newly finalized checkpoint: migrate canonical blocks hot→cold, archive the finalized + * state, prune fork choice — all engine-owned persistence, consolidated into one method. Returns the + * canonical/non-canonical block snapshot the facade uses to clean the DA + light-client artifacts it + * owns. State bytes never cross; archival is internal. + */ + migrateFinalized(finalized: CheckpointWithHex): Promise; + + /** Persist the latest finalized state to the (engine-owned) archive DB — used on graceful shutdown. */ + persistFinalizedStateToDisk(): Promise; + + /** + * On a new checkpoint: prune regen caches and archive a temp checkpoint state (engine-owned states DB). + * The facade keeps only the event listener + error handling. + */ + archiveStateOnCheckpoint(): Promise; } diff --git a/packages/beacon-node/src/chain/beaconEngine/options.ts b/packages/beacon-node/src/chain/beaconEngine/options.ts index 797ea3e02dd8..df8147fabad3 100644 --- a/packages/beacon-node/src/chain/beaconEngine/options.ts +++ b/packages/beacon-node/src/chain/beaconEngine/options.ts @@ -1,3 +1,4 @@ +import {StatesArchiveOpts} from "../archiveStore/interface.js"; import {BlsMultiThreadWorkerPoolOptions} from "../bls/index.js"; import {ForkChoiceOpts} from "../forkChoice/index.js"; import {ShufflingCacheOpts} from "../shufflingCache.js"; @@ -28,6 +29,7 @@ export type IBeaconEngineOptions = ShufflingCacheOpts & FIFOBlockStateCacheOpts & PersistentCheckpointStateCacheOpts & ForkChoiceOpts & + StatesArchiveOpts & BlsMultiThreadWorkerPoolOptions & { blsVerifyAllMainThread?: boolean; /** Gossip block/blob/data-column validation observes (no longer gates) skipped slots */ @@ -38,4 +40,7 @@ export type IBeaconEngineOptions = ShufflingCacheOpts & suggestedFeeRecipient: string; /** Emit an SSE payloadAttributes event every slot (not only when proposing) */ emitPayloadAttributes?: boolean; + /** Persist orphaned (non-canonical finalized) blocks to disk before deleting them on finalization */ + persistOrphanedBlocks?: boolean; + persistOrphanedBlocksDir?: string; }; diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 0b9d4f5777c8..587d38e07566 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -78,6 +78,10 @@ export async function importBlock( this.logger.verbose("Chain reorg", r.reorg); } + // Refresh the facade head cache and emit forkChoiceJustified/forkChoiceFinalized from the new head. + // The engine no longer emits these (FFI-honest); they are derived facade-side from the head ProtoBlock. + this.updateHeadAndEmitCheckpointEvents(r.newHead); + // 6. Queue notifyForkchoiceUpdate to engine api let shouldOverrideFcu = false; diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index a33302497fb8..36b793aeba42 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -166,6 +166,10 @@ export class BeaconChain implements IBeaconChain { get forkChoice(): IForkChoiceRead { return this.beaconEngine.forkChoice; } + // Cached head ProtoBlock, updated from importBlock's result. Lets the facade serve getStatus and + // detect finalized/justified transitions (it carries finalized*/justified* checkpoints) without the + // engine emitting into the JS emitter (FFI-honest). Not the source of truth for all head reads yet. + private headProtoBlock: ProtoBlock; readonly clock: IClock; readonly emitter: ChainEventEmitter; // TODO - beacon engine: remove this @@ -395,6 +399,8 @@ export class BeaconChain implements IBeaconChain { }, anchorState ); + // Seed the facade head cache from the engine's initial head (one-time read); refreshed on each import. + this.headProtoBlock = this.beaconEngine.forkChoice.getHead(); this.blacklistedBlocks = new Map((opts.blacklistedBlocks ?? []).map((hex) => [hex, null])); @@ -1150,8 +1156,9 @@ export class BeaconChain implements IBeaconChain { } getStatus(): Status { - const head = this.forkChoice.getHead(); - const finalizedCheckpoint = this.forkChoice.getFinalizedCheckpoint(); + // Read the cached head instead of the engine — its finalized* fields ARE the head state's + // finalized checkpoint (exactly what Status wants), so no separate getFinalizedCheckpoint() call. + const head = this.headProtoBlock; const boundary = this.config.getForkBoundaryAtEpoch(this.clock.currentEpoch); return { // fork_digest: The node's ForkDigest (compute_fork_digest(current_fork_version, genesis_validators_root)) where @@ -1160,8 +1167,8 @@ export class BeaconChain implements IBeaconChain { // - epoch of fork boundary is used to get blob parameters of current Blob Parameter Only (BPO) fork forkDigest: this.config.forkBoundary2ForkDigest(boundary), // finalized_root: state.finalized_checkpoint.root for the state corresponding to the head block (Note this defaults to Root(b'\x00' * 32) for the genesis finalized checkpoint). - finalizedRoot: finalizedCheckpoint.epoch === GENESIS_EPOCH ? ZERO_HASH : finalizedCheckpoint.root, - finalizedEpoch: finalizedCheckpoint.epoch, + finalizedRoot: head.finalizedEpoch === GENESIS_EPOCH ? ZERO_HASH : fromHex(head.finalizedRoot), + finalizedEpoch: head.finalizedEpoch, // TODO: PERFORMANCE: Memoize to prevent re-computing every time headRoot: fromHex(head.blockRoot), headSlot: head.slot, @@ -1169,12 +1176,42 @@ export class BeaconChain implements IBeaconChain { }; } + /** + * Single choke point for refreshing the cached head: pass the new canonical head here from ANY flow + * that updates it (importBlock, recomputeForkChoiceHead, getProposerHead, predictProposerHead). It + * emits fork-choice justified/finalized transitions facade-side — the engine no longer emits these + * (a native engine can't emit into the JS emitter); they are derived from the head ProtoBlock's + * justified and finalized checkpoints. Head-derived: a checkpoint that advances only via an + * epoch-boundary tick is emitted on the next head update. + */ + updateHeadAndEmitCheckpointEvents(newHead: ProtoBlock): void { + const prevHead = this.headProtoBlock; + this.headProtoBlock = newHead; + + if (newHead.justifiedEpoch !== prevHead.justifiedEpoch || newHead.justifiedRoot !== prevHead.justifiedRoot) { + this.emitter.emit( + ChainEvent.forkChoiceJustified, + protoCheckpointToWithHex(newHead.justifiedEpoch, newHead.justifiedRoot) + ); + } + if (newHead.finalizedEpoch !== prevHead.finalizedEpoch || newHead.finalizedRoot !== prevHead.finalizedRoot) { + this.emitter.emit( + ChainEvent.forkChoiceFinalized, + protoCheckpointToWithHex(newHead.finalizedEpoch, newHead.finalizedRoot) + ); + } + } + recomputeForkChoiceHead(caller: ForkchoiceCaller): ProtoBlock { this.metrics?.forkChoice.requests.inc(); const timer = this.metrics?.forkChoice.findHead.startTimer({caller}); try { - return this.beaconEngine.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head; + // GetCanonicalHead runs updateHead(); the returned head IS the new canonical head — cache it + // (and emit any justified/finalized transition it carries) via the single choke point. + const head = this.beaconEngine.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head; + this.updateHeadAndEmitCheckpointEvents(head); + return head; } catch (e) { this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead}); throw e; @@ -1189,11 +1226,15 @@ export class BeaconChain implements IBeaconChain { const secFromSlot = this.clock.secFromSlot(slot); try { - return this.beaconEngine.forkChoice.updateAndGetHead({ + const predictedHead = this.beaconEngine.forkChoice.updateAndGetHead({ mode: UpdateHeadOpt.GetPredictedProposerHead, secFromSlot, slot, }).head; + // The returned value is a proposer-boost-reorg prediction, NOT the canonical head — sync the + // cache to the canonical head (getHead()) through the choke point (may emit justified/finalized). + this.updateHeadAndEmitCheckpointEvents(this.beaconEngine.forkChoice.getHead()); + return predictedHead; } catch (e) { this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetPredictedProposerHead}); throw e; @@ -1203,7 +1244,12 @@ export class BeaconChain implements IBeaconChain { } getProposerHead(slot: Slot): ProtoBlock { - return this.beaconEngine.getProposerHead(slot); + // engine.getProposerHead runs updateHead() (GetProposerHead) then returns the proposer head, which + // may be the parent for a reorg. Sync the cache to the canonical head (not the returned value) + // through the choke point so any justified/finalized transition is emitted. + const proposerHead = this.beaconEngine.getProposerHead(slot); + this.updateHeadAndEmitCheckpointEvents(this.beaconEngine.forkChoice.getHead()); + return proposerHead; } /** @@ -1557,3 +1603,8 @@ export class BeaconChain implements IBeaconChain { return preState.computeSyncCommitteeRewards(block, validatorIds ?? []); } } + +/** Build a CheckpointWithHex from a ProtoBlock's `{epoch, rootHex}` checkpoint fields (no re-hashing). */ +function protoCheckpointToWithHex(epoch: Epoch, rootHex: RootHex): CheckpointWithHex { + return {epoch, root: fromHex(rootHex), rootHex}; +} diff --git a/packages/beacon-node/src/chain/forkChoice/index.ts b/packages/beacon-node/src/chain/forkChoice/index.ts index 86c19f0bf9d7..9a267ac1d39d 100644 --- a/packages/beacon-node/src/chain/forkChoice/index.ts +++ b/packages/beacon-node/src/chain/forkChoice/index.ts @@ -24,7 +24,7 @@ import {Slot, ssz} from "@lodestar/types"; import {Logger, toRootHex} from "@lodestar/utils"; import {GENESIS_SLOT} from "../../constants/index.js"; import {Metrics} from "../../metrics/index.js"; -import {ChainEvent, ChainEventEmitter} from "../emitter.js"; +import {ChainEventEmitter} from "../emitter.js"; export type ForkChoiceOpts = RawForkChoiceOpts & { // for testing only @@ -120,8 +120,8 @@ export function initializeForkChoiceFromFinalizedState( justifiedBalancesGetter, stateGetter, { - onJustified: (cp) => emitter.emit(ChainEvent.forkChoiceJustified, cp), - onFinalized: (cp) => emitter.emit(ChainEvent.forkChoiceFinalized, cp), + // justified/finalized are emitted facade-side from the head ProtoBlock (FFI-honest — a native + // engine can't emit into the JS emitter). Only fast-confirmation stays wired here for now. onFastConfirmation: ({block, slot, currentSlot}) => emitter.emit(routes.events.EventType.fastConfirmation, {block, slot, currentSlot}), } @@ -215,8 +215,9 @@ export function initializeForkChoiceFromUnfinalizedState( justifiedBalancesGetter, stateGetter, { - onJustified: (cp) => emitter.emit(ChainEvent.forkChoiceJustified, cp), - onFinalized: (cp) => emitter.emit(ChainEvent.forkChoiceFinalized, cp), + // justified/finalized are emitted facade-side from the head ProtoBlock (FFI-honest — a native + // engine can't emit into the JS emitter). Only fast-confirmation stays wired here for now. + // TODO - beacon engine: remove this onFastConfirmation: ({block, slot, currentSlot}) => emitter.emit(routes.events.EventType.fastConfirmation, {block, slot, currentSlot}), } diff --git a/packages/beacon-node/src/chain/options.ts b/packages/beacon-node/src/chain/options.ts index 75b6e71d913c..cfe54b1616e3 100644 --- a/packages/beacon-node/src/chain/options.ts +++ b/packages/beacon-node/src/chain/options.ts @@ -22,8 +22,6 @@ export type IChainOptions = IBeaconEngineOptions & persistProducedBlocks?: boolean; persistInvalidSszObjects?: boolean; persistInvalidSszObjectsDir?: string; - persistOrphanedBlocks?: boolean; - persistOrphanedBlocksDir?: string; skipCreateStateCacheIfAvailable?: boolean; /** Ensure blobs returned by the execution engine are valid */ sanityCheckExecutionEngineBlobs?: boolean; diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index 745223553f59..300b17524c3b 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -1,6 +1,6 @@ /** biome-ignore-all lint/suspicious/noTemplateCurlyInString: The metric templates requires to have `${}` in a normal string */ import {NotReorgedReason} from "@lodestar/fork-choice"; -import {ArchiveStoreTask} from "../../chain/archiveStore/archiveStore.js"; +import {ArchiveStoreTask} from "../../chain/archiveStore/interface.js"; import {FrequencyStateArchiveStep} from "../../chain/archiveStore/strategies/frequencyStateArchiveStrategy.js"; import {BlockInputSource} from "../../chain/blocks/blockInput/index.js"; import {PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js"; diff --git a/packages/beacon-node/test/unit/chain/archiveStore/blockArchiver.test.ts b/packages/beacon-node/test/unit/chain/archiveStore/blockArchiver.test.ts index 3c2aaf52eebd..189215c4b175 100644 --- a/packages/beacon-node/test/unit/chain/archiveStore/blockArchiver.test.ts +++ b/packages/beacon-node/test/unit/chain/archiveStore/blockArchiver.test.ts @@ -1,17 +1,61 @@ import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"; import {fromHexString, toHexString} from "@chainsafe/ssz"; -import {createChainForkConfig} from "@lodestar/config"; +import {ChainForkConfig, createChainForkConfig} from "@lodestar/config"; import {config as defaultConfig} from "@lodestar/config/default"; -import {PayloadStatus} from "@lodestar/fork-choice"; +import {CheckpointWithHex, IForkChoiceRead, PayloadStatus, ProtoBlock} from "@lodestar/fork-choice"; import {testLogger} from "@lodestar/logger/test-utils"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {ssz} from "@lodestar/types"; -import {archiveBlocks} from "../../../../src/chain/archiveStore/utils/archiveBlocks.js"; +import {Logger} from "@lodestar/utils"; +import {migrateFinalizedBlocks, migrateFinalizedDA} from "../../../../src/chain/archiveStore/utils/archiveBlocks.js"; +import {LightClientServer} from "../../../../src/chain/lightClient/index.js"; import {ZERO_HASH_HEX} from "../../../../src/constants/index.js"; +import {IBeaconDb} from "../../../../src/db/index.js"; import {MockedBeaconChain, getMockedBeaconChain} from "../../../mocks/mockedBeaconChain.js"; import {MockedBeaconDb, getMockedBeaconDb} from "../../../mocks/mockedBeaconDb.js"; import {generateProtoBlock} from "../../../utils/typeGenerator.js"; +/** + * Test shim composing engine block-migration + facade DA cleanup exactly as + * `ArchiveStore.processFinalizedCheckpoint` does, so the assertions below stay unchanged. + */ +async function archiveBlocks( + config: ChainForkConfig, + db: IBeaconDb, + forkChoice: IForkChoiceRead, + lightclientServer: LightClientServer | undefined, + logger: Logger, + finalized: CheckpointWithHex, + currentEpoch: number, + archiveDataEpochs?: number, + persistOrphanedBlocks?: boolean, + persistOrphanedBlocksDir?: string +): Promise { + const {ancestors, nonAncestors} = forkChoice.getAllAncestorAndNonAncestorBlocksDefaultStatus(finalized.rootHex); + await migrateFinalizedBlocks( + db, + logger, + ancestors, + nonAncestors, + finalized, + currentEpoch, + persistOrphanedBlocks, + persistOrphanedBlocksDir + ); + const toSummary = (b: ProtoBlock) => ({slot: b.slot, blockRoot: b.blockRoot, payloadStatus: b.payloadStatus}); + const snapshot = {canonical: ancestors.map(toSummary), nonCanonical: nonAncestors.map(toSummary)}; + await migrateFinalizedDA( + config, + db, + lightclientServer, + logger, + snapshot, + finalized.epoch, + currentEpoch, + archiveDataEpochs + ); +} + function toAsyncIterable(items: T[]): AsyncIterable { return { async *[Symbol.asyncIterator]() { diff --git a/packages/fork-choice/src/forkChoice/store.ts b/packages/fork-choice/src/forkChoice/store.ts index 915bfb3214d1..2bd518137b8e 100644 --- a/packages/fork-choice/src/forkChoice/store.ts +++ b/packages/fork-choice/src/forkChoice/store.ts @@ -82,8 +82,10 @@ export class ForkChoiceStore implements IForkChoiceStore { justifiedBalancesGetter: JustifiedBalancesGetter, stateGetter: ForkChoiceStateGetter, private readonly events?: { - onJustified: (cp: CheckpointWithHex) => void; - onFinalized: (cp: CheckpointWithHex) => void; + // Optional: a native engine can't emit JS events, so justified/finalized transitions are + // detected facade-side (from the head ProtoBlock) rather than pushed from here. + onJustified?: (cp: CheckpointWithHex) => void; + onFinalized?: (cp: CheckpointWithHex) => void; onFastConfirmation?: (data: {block: RootHex; slot: Slot; currentSlot: Slot}) => void; } ) { @@ -122,7 +124,7 @@ export class ForkChoiceStore implements IForkChoiceStore { } set justified(justified: CheckpointWithBalance) { this._justified = {...justified, totalBalance: computeTotalBalance(justified.balances)}; - this.events?.onJustified(justified.checkpoint); + this.events?.onJustified?.(justified.checkpoint); } get finalizedCheckpoint(): CheckpointWithHex { @@ -131,7 +133,7 @@ export class ForkChoiceStore implements IForkChoiceStore { set finalizedCheckpoint(checkpoint: CheckpointWithHex) { const cp = toCheckpointWithHex(checkpoint); this._finalizedCheckpoint = cp; - this.events?.onFinalized(cp); + this.events?.onFinalized?.(cp); } /** From 40169aa01e5693bf8cb62031cb19a438f307b640 Mon Sep 17 00:00:00 2001 From: twoeths Date: Fri, 3 Jul 2026 10:29:58 +0700 Subject: [PATCH 14/24] feat: move ExecutionPayload db to BeaconEngine --- .../src/api/impl/beacon/blocks/index.ts | 2 +- .../chain/archiveStore/utils/archiveBlocks.ts | 75 ++++++++++++------- .../src/chain/beaconEngine/beaconEngine.ts | 71 +++++++++++++++++- .../src/chain/beaconEngine/interface.ts | 9 ++- .../blocks/writePayloadEnvelopeInputToDb.ts | 12 ++- packages/beacon-node/src/chain/chain.ts | 30 ++++---- packages/beacon-node/src/chain/interface.ts | 5 +- .../executionPayloadEnvelopesByRange.ts | 65 ++++++---------- .../executionPayloadEnvelopesByRoot.ts | 2 +- .../src/network/reqresp/handlers/index.ts | 2 +- .../utils/dataColumnResponseValidation.ts | 3 +- packages/beacon-node/src/util/sszBytes.ts | 2 + 12 files changed, 179 insertions(+), 99 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index af895e2ab4bd..1e15a938b7bd 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -902,7 +902,7 @@ export function getBeaconBlockApi({ const blockRootHex = toRootHex(blockRoot); const data = context?.returnBytes - ? await chain.getSerializedExecutionPayloadEnvelope(slot, blockRootHex) + ? await chain.getSerializedExecutionPayloadEnvelope(slot, blockRoot) : await chain.getExecutionPayloadEnvelope(slot, blockRootHex); if (!data) { diff --git a/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts b/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts index 9e2fd2dd503c..bc3c407bb52c 100644 --- a/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts +++ b/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts @@ -9,7 +9,7 @@ import {Logger, fromAsync, fromHex, prettyPrintIndices, toRootHex} from "@lodest import {IBeaconDb} from "../../../db/index.js"; import {BlockArchiveBatchPutBinaryItem} from "../../../db/repositories/index.js"; import {ensureDir, writeIfNotExist} from "../../../util/file.js"; -import {BlockRootHex} from "../../../util/sszBytes.js"; +import {BlockRootHex, BlockRootSlot} from "../../../util/sszBytes.js"; import {FinalizedBlockSnapshot, FinalizedProtoSummary} from "../../beaconEngine/interface.js"; import {LightClientServer} from "../../lightClient/index.js"; @@ -19,8 +19,6 @@ import {LightClientServer} from "../../lightClient/index.js"; const BLOCK_BATCH_SIZE = 256; const BLOB_SIDECAR_BATCH_SIZE = 32; -type BlockRootSlot = {slot: Slot; root: Uint8Array}; - /** * Persist orphaned block to disk */ @@ -108,11 +106,56 @@ export async function migrateFinalizedBlocks( } } +/** + * Engine-owned migration of finalized execution payload envelopes (gloas): canonical blocks hot→cold and + * non-canonical delete. Envelopes are consensus data owned by BeaconEngine (not DA like blobs/columns), + * so this runs inside `engine.migrateFinalized`, driven by the same `snapshot` as `migrateFinalizedBlocks`. + */ +export async function migrateFinalizedExecutionPayloadEnvelopes( + config: ChainForkConfig, + db: IBeaconDb, + logger: Logger, + snapshot: FinalizedBlockSnapshot, + finalizedEpoch: Epoch, + currentEpoch: Epoch +): Promise { + if (finalizedEpoch < config.GLOAS_FORK_EPOCH) return; + + const {canonical: finalizedCanonicalBlocks, nonCanonical: finalizedNonCanonicalBlocks} = snapshot; + const logCtx = {currentEpoch, finalizedEpoch}; + + if (finalizedCanonicalBlocks.length > 0) { + const migratedSlots = await migrateExecutionPayloadEnvelopesFromHotToColdDb( + config, + db, + logger, + finalizedCanonicalBlocks + ); + logger.verbose("Migrated executionPayloadEnvelopes from hot DB to cold DB", { + ...logCtx, + migratedEntries: migratedSlots.length, + slotRange: prettyPrintIndices(migratedSlots), + }); + } + + const nonCanonicalBlockRoots = finalizedNonCanonicalBlocks.map((summary) => fromHex(summary.blockRoot)); + if (nonCanonicalBlockRoots.length > 0) { + await db.executionPayloadEnvelope.batchDelete(nonCanonicalBlockRoots); + const nonCanonicalSlots = finalizedNonCanonicalBlocks.map((summary) => summary.slot).sort((a, b) => a - b); + logger.verbose("Deleted non canonical executionPayloadEnvelopes from hot DB", { + ...logCtx, + count: nonCanonicalBlockRoots.length, + slotRange: prettyPrintIndices(nonCanonicalSlots), + }); + } +} + /** * Facade-owned DA + light-client cleanup for a finalized checkpoint. Everything it needs comes from the * engine's returned `snapshot` (canonical/non-canonical block summaries) — it never reads fork choice, - * which the engine already pruned against. Blocks + states are engine-owned (see `migrateFinalizedBlocks`); - * here we migrate/prune the blob/data-column/payload-envelope sidecars and prune light-client data. + * which the engine already pruned against. Blocks + states + payload envelopes are engine-owned (see + * `migrateFinalizedBlocks` / `migrateFinalizedExecutionPayloadEnvelopes`); here we migrate/prune the + * blob + data-column sidecars and prune light-client data. */ export async function migrateFinalizedDA( config: ChainForkConfig, @@ -128,7 +171,6 @@ export async function migrateFinalizedDA( const finalizedPostDeneb = finalizedEpoch >= config.DENEB_FORK_EPOCH; const finalizedPostFulu = finalizedEpoch >= config.FULU_FORK_EPOCH; - const finalizedPostGloas = finalizedEpoch >= config.GLOAS_FORK_EPOCH; const finalizedCanonicalBlockRoots: BlockRootSlot[] = finalizedCanonicalBlocks.map((block) => ({ slot: block.slot, @@ -163,22 +205,6 @@ export async function migrateFinalizedDA( slotRange: prettyPrintIndices(migratedSlots), }); } - - // TODO - beacon engine: move payload db to BeaconEngine (execution payload envelopes are - // consensus data consumed by block production / prepareNextSlot / STF, not DA like blobs/columns). - if (finalizedPostGloas) { - const migratedSlots = await migrateExecutionPayloadEnvelopesFromHotToColdDb( - config, - db, - logger, - finalizedCanonicalBlocks - ); - logger.verbose("Migrated executionPayloadEnvelopes from hot DB to cold DB", { - ...logCtx, - migratedEntries: migratedSlots.length, - slotRange: prettyPrintIndices(migratedSlots), - }); - } } const nonCanonicalBlockRoots = finalizedNonCanonicalBlocks.map((summary) => fromHex(summary.blockRoot)); @@ -199,11 +225,6 @@ export async function migrateFinalizedDA( await db.dataColumnSidecar.deleteMany(nonCanonicalBlockRoots); logger.verbose("Deleted non canonical dataColumnSidecars from hot DB", nonCanonicalLogCtx); } - - if (finalizedPostGloas) { - await db.executionPayloadEnvelope.batchDelete(nonCanonicalBlockRoots); - logger.verbose("Deleted non canonical executionPayloadEnvelopes from hot DB", nonCanonicalLogCtx); - } } // Delete expired blobs diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 921342da86ee..90963f65e1b0 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -12,6 +12,7 @@ import { ForkChoiceStateGetter, IForkChoice, PayloadExecutionStatus, + PayloadStatus, ProtoBlock, UpdateHeadOpt, getSafeExecutionBlockHash, @@ -82,9 +83,13 @@ import {IClock} from "../../util/clock.js"; import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; import {callInNextEventLoop} from "../../util/eventLoop.js"; import {isOptimisticBlock} from "../../util/forkChoice.js"; +import {BlockRootSlot} from "../../util/sszBytes.js"; import {ArchiveMode, ArchiveStoreTask, StateArchiveStrategy} from "../archiveStore/interface.js"; import {FrequencyStateArchiveStrategy} from "../archiveStore/strategies/frequencyStateArchiveStrategy.js"; -import {migrateFinalizedBlocks} from "../archiveStore/utils/archiveBlocks.js"; +import { + migrateFinalizedBlocks, + migrateFinalizedExecutionPayloadEnvelopes, +} from "../archiveStore/utils/archiveBlocks.js"; import {CheckpointBalancesCache} from "../balancesCache.js"; import {BeaconProposerCache} from "../beaconProposerCache.js"; import {isBlockInputBlobs, isBlockInputColumns} from "../blocks/blockInput/blockInput.js"; @@ -448,15 +453,63 @@ export class BeaconEngine implements IBeaconEngine { /** DB read of an execution payload envelope. Callers that hold the DA cache check it first. */ async getExecutionPayloadEnvelope( blockSlot: Slot, - blockRootHex: string + blockRoot: Uint8Array ): Promise { return ( - (await this.db.executionPayloadEnvelope.get(fromHex(blockRootHex))) ?? + (await this.db.executionPayloadEnvelope.get(blockRoot)) ?? (await this.db.executionPayloadEnvelopeArchive.get(blockSlot)) ?? null ); } + /** Serialized DB read of an execution payload envelope (byte sibling of getExecutionPayloadEnvelope). */ + async getSerializedExecutionPayloadEnvelope(blockSlot: Slot, blockRoot: Uint8Array): Promise { + return ( + (await this.db.executionPayloadEnvelope.getBinary(blockRoot)) ?? + (await this.db.executionPayloadEnvelopeArchive.getBinary(blockSlot)) ?? + null + ); + } + + /** Serialized read of a finalized (cold-archive, keyed by slot) execution payload envelope. */ + async getSerializedFinalizedExecutionPayloadEnvelope(slot: Slot): Promise { + return (await this.db.executionPayloadEnvelopeArchive.getBinary(slot)) ?? null; + } + + /** Persist a hot execution payload envelope, bytes-first (key = beaconBlockRoot). */ + async persistExecutionPayloadEnvelope(blockRoot: Uint8Array, serializedBytes: Uint8Array): Promise { + await this.db.executionPayloadEnvelope.putBinary(blockRoot, serializedBytes); + } + + /** + * Thin serving refs for ExecutionPayloadEnvelopesByRange: fork-choice reads + FULL filter stay inside + * the engine; only slot + raw root cross. Returns the archive boundary (`finalizedSlot`) and the + * canonical FULL blocks in `(finalizedSlot - 1, endSlot)`. + */ + getFullBlockRootSlotsByRange(startSlot: Slot, endSlot: Slot): {finalizedSlot: Slot; nonFinalized: BlockRootSlot[]} { + // The finalized block's envelope stays in hot db until the next finalization run → archive tops out at finalizedSlot - 1 + const finalizedSlot = this.forkChoice.getFinalizedBlock().slot; + const archiveMaxSlot = finalizedSlot - 1; + const nonFinalized: BlockRootSlot[] = []; + if (endSlot > archiveMaxSlot) { + const head = this.forkChoice.getHead(); + // newest→oldest; iterate ascending + const headChain = this.forkChoice.getAllAncestorBlocks(head.blockRoot, head.payloadStatus); + for (let i = headChain.length - 1; i >= 0; i--) { + const block = headChain[i]; + if (block.slot > archiveMaxSlot && block.slot >= startSlot && block.slot < endSlot) { + // Only FULL blocks have an envelope; skip EMPTY/PENDING + if (block.payloadStatus === PayloadStatus.FULL) { + nonFinalized.push({slot: block.slot, root: fromHex(block.blockRoot)}); + } + } else if (block.slot >= endSlot) { + break; + } + } + } + return {finalizedSlot, nonFinalized}; + } + /** * Parent execution requests for block production. DB-only: the facade-owned DA cache is not visible * here, so callers that need cache-only (DB-write-pending) envelopes check the cache first @@ -473,7 +526,7 @@ export class BeaconEngine implements IBeaconEngine { if (!isForkPostGloas(this.config.getForkName(parentBlockSlot))) { return ssz.gloas.ExecutionRequests.defaultValue(); } - const envelope = await this.getExecutionPayloadEnvelope(parentBlockSlot, parentBlockRootHex); + const envelope = await this.getExecutionPayloadEnvelope(parentBlockSlot, fromHex(parentBlockRootHex)); if (envelope === null) { throw Error(`Parent execution payload envelope not found slot=${parentBlockSlot}, root=${parentBlockRootHex}`); } @@ -2088,6 +2141,16 @@ export class BeaconEngine implements IBeaconEngine { this.opts.persistOrphanedBlocks, this.opts.persistOrphanedBlocksDir ); + // Migrate canonical payload envelopes hot→cold + delete non-canonical (envelopes are engine-owned, + // consensus data — not DA). Tied to canonical blocks, so run right after block migration. + await migrateFinalizedExecutionPayloadEnvelopes( + this.config, + this.db, + this.logger, + snapshot, + finalized.epoch, + this.clock.currentEpoch + ); timer?.({source: ArchiveStoreTask.ArchiveBlocks}); // 2. Archive the finalized state (states are engine-owned). MUST run after block migration so a diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 1e6963b160f4..8f0349e1e219 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -36,6 +36,7 @@ import {IBeaconDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; import {BufferPool} from "../../util/bufferPool.js"; import {IClock} from "../../util/clock.js"; +import {BlockRootSlot} from "../../util/sszBytes.js"; import {IBlockInput} from "../blocks/blockInput/index.js"; import {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; import {ImportBlockOpts} from "../blocks/types.js"; @@ -229,10 +230,16 @@ export interface IBeaconEngine { // builder-bid lookup, forkChoice exec hashes, base-state scalars) so they are not recomputed per path. // More of the production flow migrates here in later steps. getProposerHead(slot: Slot): ProtoBlock; + // Execution payload envelope DB (engine-owned). Roots cross as raw bytes (FFI-honest). getExecutionPayloadEnvelope( blockSlot: Slot, - blockRootHex: string + blockRoot: Uint8Array ): Promise; + getSerializedExecutionPayloadEnvelope(blockSlot: Slot, blockRoot: Uint8Array): Promise; + getSerializedFinalizedExecutionPayloadEnvelope(slot: Slot): Promise; + persistExecutionPayloadEnvelope(blockRoot: Uint8Array, serializedBytes: Uint8Array): Promise; + // Thin serving refs for ExecutionPayloadEnvelopesByRange — fork choice read engine-side, no ProtoBlock crosses. + getFullBlockRootSlotsByRange(startSlot: Slot, endSlot: Slot): {finalizedSlot: Slot; nonFinalized: BlockRootSlot[]}; produceCommonBlockBody(blockAttributes: BlockAttributes): Promise; produceBlockBase(attrs: {slot: Slot; randaoReveal: BLSSignature; graffiti: Bytes32}): Promise; // Compute the post-state root + proposer reward for a produced block. diff --git a/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts b/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts index 3421e6bd40ed..e7a3b487997d 100644 --- a/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts +++ b/packages/beacon-node/src/chain/blocks/writePayloadEnvelopeInputToDb.ts @@ -1,3 +1,4 @@ +import {ssz} from "@lodestar/types"; import {BeaconChain} from "../chain.js"; import {PayloadEnvelopeInput} from "../seenCache/seenPayloadEnvelopeInput.js"; import {writeDataColumnsToDb} from "./writeBlockInputToDb.js"; @@ -16,10 +17,13 @@ export async function writePayloadEnvelopeInputToDb( const envelope = payloadInput.getPayloadEnvelope(); const blockRootHex = payloadInput.blockRootHex; - const envelopeBytes = this.serializedCache.get(envelope); - const envelopePromise = envelopeBytes - ? this.db.executionPayloadEnvelope.putBinary(this.db.executionPayloadEnvelope.getId(envelope), envelopeBytes) - : this.db.executionPayloadEnvelope.add(envelope); + // Envelope DB is engine-owned; write through the engine (bytes-first). Serialize on a cache miss. + const envelopeBytes = + this.serializedCache.get(envelope) ?? ssz.gloas.SignedExecutionPayloadEnvelope.serialize(envelope); + const envelopePromise = this.beaconEngine.persistExecutionPayloadEnvelope( + envelope.message.beaconBlockRoot, + envelopeBytes + ); // Write envelope and data columns in parallel (reuses shared column writing logic) await Promise.all([envelopePromise, writeDataColumnsToDb.call(this, payloadInput)]); diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 36b793aeba42..b2522cb2ffa5 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -62,7 +62,7 @@ import {ensureDir, writeIfNotExist} from "../util/file.js"; import {isOptimisticBlock} from "../util/forkChoice.js"; import {JobItemQueue} from "../util/queue/itemQueue.js"; import {SerializedCache} from "../util/serializedCache.js"; -import {getSlotFromSignedBeaconBlockSerialized} from "../util/sszBytes.js"; +import {BlockRootSlot, getSlotFromSignedBeaconBlockSerialized} from "../util/sszBytes.js"; import {ArchiveStore} from "./archiveStore/archiveStore.js"; import {CheckpointBalancesCache} from "./balancesCache.js"; import {BeaconEngine} from "./beaconEngine/index.js"; @@ -884,22 +884,24 @@ export class BeaconChain implements IBeaconChain { return null; } - async getSerializedExecutionPayloadEnvelope(blockSlot: Slot, blockRootHex: string): Promise { - const payloadInput = this.seenPayloadEnvelopeInputCache.get(blockRootHex); + async getSerializedExecutionPayloadEnvelope(blockSlot: Slot, blockRoot: Uint8Array): Promise { + // Facade owns the DA cache (keyed by hex); check it before the engine's DB read. + const payloadInput = this.seenPayloadEnvelopeInputCache.get(toRootHex(blockRoot)); if (payloadInput?.hasPayloadEnvelope()) { const envelope = payloadInput.getPayloadEnvelope(); - const serialized = this.serializedCache.get(envelope); - if (serialized) { - return serialized; - } - return ssz.gloas.SignedExecutionPayloadEnvelope.serialize(envelope); + return this.serializedCache.get(envelope) ?? ssz.gloas.SignedExecutionPayloadEnvelope.serialize(envelope); } + return this.beaconEngine.getSerializedExecutionPayloadEnvelope(blockSlot, blockRoot); + } - return ( - (await this.db.executionPayloadEnvelope.getBinary(fromHex(blockRootHex))) ?? - (await this.db.executionPayloadEnvelopeArchive.getBinary(blockSlot)) ?? - null - ); + /** Engine passthrough: serialized finalized (cold-archive) envelope by slot. */ + getSerializedFinalizedExecutionPayloadEnvelope(slot: Slot): Promise { + return this.beaconEngine.getSerializedFinalizedExecutionPayloadEnvelope(slot); + } + + /** Engine passthrough: thin ByRange serving refs (fork choice read engine-side). */ + getFullBlockRootSlotsByRange(startSlot: Slot, endSlot: Slot): {finalizedSlot: Slot; nonFinalized: BlockRootSlot[]} { + return this.beaconEngine.getFullBlockRootSlotsByRange(startSlot, endSlot); } async getExecutionPayloadEnvelope( @@ -911,7 +913,7 @@ export class BeaconChain implements IBeaconChain { if (payloadInput?.hasPayloadEnvelope()) { return payloadInput.getPayloadEnvelope(); } - return this.beaconEngine.getExecutionPayloadEnvelope(blockSlot, blockRootHex); + return this.beaconEngine.getExecutionPayloadEnvelope(blockSlot, fromHex(blockRootHex)); } async getParentExecutionRequests( diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 7b3e8402d240..90a1a0a2b1aa 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -29,6 +29,7 @@ import {BufferPool} from "../util/bufferPool.js"; import {IClock} from "../util/clock.js"; import {CustodyConfig} from "../util/dataColumns.js"; import {SerializedCache} from "../util/serializedCache.js"; +import {BlockRootSlot} from "../util/sszBytes.js"; import {IArchiveStore} from "./archiveStore/interface.js"; import {CheckpointBalancesCache} from "./balancesCache.js"; import {IBeaconEngine} from "./beaconEngine/index.js"; @@ -231,7 +232,9 @@ export interface IBeaconChain { blockRootHex: string, indices: number[] ): Promise<(Uint8Array | undefined)[]>; - getSerializedExecutionPayloadEnvelope(blockSlot: Slot, blockRootHex: string): Promise; + getSerializedExecutionPayloadEnvelope(blockSlot: Slot, blockRoot: Uint8Array): Promise; + getSerializedFinalizedExecutionPayloadEnvelope(slot: Slot): Promise; + getFullBlockRootSlotsByRange(startSlot: Slot, endSlot: Slot): {finalizedSlot: Slot; nonFinalized: BlockRootSlot[]}; getExecutionPayloadEnvelope( blockSlot: Slot, blockRootHex: string diff --git a/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts index 1797552512d8..fcaf0fb1542b 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts @@ -1,18 +1,15 @@ import {PeerId} from "@libp2p/interface"; import {ChainConfig} from "@lodestar/config"; -import {PayloadStatus} from "@lodestar/fork-choice"; import {GENESIS_SLOT} from "@lodestar/params"; import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp"; import {computeEpochAtSlot} from "@lodestar/state-transition"; import {gloas} from "@lodestar/types"; import {IBeaconChain} from "../../../chain/index.js"; -import {IBeaconDb} from "../../../db/index.js"; import {prettyPrintPeerId} from "../../util.js"; export async function* onExecutionPayloadEnvelopesByRange( request: gloas.ExecutionPayloadEnvelopesByRangeRequest, chain: IBeaconChain, - db: IBeaconDb, peerId: PeerId, peerClient: string ): AsyncIterable { @@ -35,19 +32,18 @@ export async function* onExecutionPayloadEnvelopesByRange( ); } - const finalized = db.executionPayloadEnvelopeArchive; - // Use the finalized block's actual slot as the checkpoint epoch-boundary slot may be skipped - const finalizedSlot = chain.forkChoice.getFinalizedBlock().slot; - // The finalized block's envelope stays in the hot db until the next finalization run + // Fork choice is read inside the engine; only thin refs cross (slot + raw root), no ProtoBlock. + // The finalized block's envelope stays in hot db until the next finalization run, so the archive + // tops out at finalizedSlot - 1. + const {finalizedSlot, nonFinalized} = chain.getFullBlockRootSlotsByRange(startSlot, endSlot); const archiveMaxSlot = finalizedSlot - 1; - // Finalized range of envelopes + // Finalized range: point-read the cold archive per slot; skipped slots have no envelope. if (startSlot <= archiveMaxSlot) { - for await (const {key, value: envelopeBytes} of finalized.binaryEntriesStream({ - gte: startSlot, - lt: Math.min(endSlot, archiveMaxSlot + 1), - })) { - const slot = finalized.decodeKey(key); + const lt = Math.min(endSlot, archiveMaxSlot + 1); + for (let slot = startSlot; slot < lt; slot++) { + const envelopeBytes = await chain.getSerializedFinalizedExecutionPayloadEnvelope(slot); + if (!envelopeBytes) continue; yield { data: envelopeBytes, boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(slot)), @@ -55,38 +51,19 @@ export async function* onExecutionPayloadEnvelopesByRange( } } - // Non-finalized range of envelopes - if (endSlot > archiveMaxSlot) { - const headBlock = chain.forkChoice.getHead(); - const headRoot = headBlock.blockRoot; - const headChain = chain.forkChoice.getAllAncestorBlocks(headRoot, headBlock.payloadStatus); - - // Iterate head chain with ascending block numbers - for (let i = headChain.length - 1; i >= 0; i--) { - const block = headChain[i]; - - if (block.slot > archiveMaxSlot && block.slot >= startSlot && block.slot < endSlot) { - // Skip EMPTY blocks - if (block.payloadStatus !== PayloadStatus.FULL) { - continue; - } - - const envelopeBytes = await chain.getSerializedExecutionPayloadEnvelope(block.slot, block.blockRoot); - if (!envelopeBytes) { - throw new ResponseError( - RespStatus.SERVER_ERROR, - `No envelope for root ${block.blockRoot} slot ${block.slot}, startSlot=${startSlot} endSlot=${endSlot} finalizedSlot=${finalizedSlot}` - ); - } - - yield { - data: envelopeBytes, - boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(block.slot)), - }; - } else if (block.slot >= endSlot) { - break; - } + // Non-finalized range: point-read each canonical FULL block's envelope by (slot, root). + for (const {slot, root} of nonFinalized) { + const envelopeBytes = await chain.getSerializedExecutionPayloadEnvelope(slot, root); + if (!envelopeBytes) { + throw new ResponseError( + RespStatus.SERVER_ERROR, + `No envelope for slot ${slot}, startSlot=${startSlot} endSlot=${endSlot} finalizedSlot=${finalizedSlot}` + ); } + yield { + data: envelopeBytes, + boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(slot)), + }; } } diff --git a/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts index 510fed4b6e3b..01d5575f981c 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts @@ -35,7 +35,7 @@ export async function* onExecutionPayloadEnvelopesByRoot( continue; } - const envelopeBytes = await chain.getSerializedExecutionPayloadEnvelope(slot, rootHex); + const envelopeBytes = await chain.getSerializedExecutionPayloadEnvelope(slot, root); if (envelopeBytes) { yield { data: envelopeBytes, diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/beacon-node/src/network/reqresp/handlers/index.ts index 4035aa508009..0ff2f8698eb6 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/index.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/index.ts @@ -76,7 +76,7 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh }, [ReqRespMethod.ExecutionPayloadEnvelopesByRange]: (req, peerId, peerClient) => { const body = ssz.gloas.ExecutionPayloadEnvelopesByRangeRequest.deserialize(req.data); - return onExecutionPayloadEnvelopesByRange(body, chain, db, peerId, peerClient); + return onExecutionPayloadEnvelopesByRange(body, chain, peerId, peerClient); }, [ReqRespMethod.LightClientBootstrap]: (req) => { diff --git a/packages/beacon-node/src/network/reqresp/utils/dataColumnResponseValidation.ts b/packages/beacon-node/src/network/reqresp/utils/dataColumnResponseValidation.ts index 451b0d87a08c..acb3f493937d 100644 --- a/packages/beacon-node/src/network/reqresp/utils/dataColumnResponseValidation.ts +++ b/packages/beacon-node/src/network/reqresp/utils/dataColumnResponseValidation.ts @@ -41,7 +41,8 @@ export async function handleColumnSidecarUnavailability({ // Post-gloas, columns exist only for FULL blocks; a finalized block is FULL if its envelope was // archived. Bid blobsCount is unreliable here since an EMPTY block's bid may still commit to blobs if (blockRoot === undefined && chain.config.getForkSeq(slot) >= ForkSeq.gloas) { - const envelopeBytes = await db.executionPayloadEnvelopeArchive.getBinary(slot); + // Envelope DB is engine-owned; read the finalized (cold-archive) envelope via the engine. + const envelopeBytes = await chain.getSerializedFinalizedExecutionPayloadEnvelope(slot); if (!envelopeBytes) return; } diff --git a/packages/beacon-node/src/util/sszBytes.ts b/packages/beacon-node/src/util/sszBytes.ts index a193ac0b6b01..3ce660dce4e5 100644 --- a/packages/beacon-node/src/util/sszBytes.ts +++ b/packages/beacon-node/src/util/sszBytes.ts @@ -13,6 +13,8 @@ import { import {BLSSignature, CommitteeIndex, RootHex, Slot, ValidatorIndex, ssz} from "@lodestar/types"; export type BlockRootHex = RootHex; +/** A block's slot + its raw (bytes) root. Shared by the archive migration + envelope serving paths. */ +export type BlockRootSlot = {slot: Slot; root: Uint8Array}; // pre-electra, AttestationData is used to cache attestations export type AttDataBase64 = string; // electra, CommitteeBits From b5e4eea12ed247bb7d27c97f704d91233ff3c2dc Mon Sep 17 00:00:00 2001 From: twoeths Date: Fri, 3 Jul 2026 15:32:41 +0700 Subject: [PATCH 15/24] feat: separate IBeaconChainDb vs IBeaconEngineDb --- .../src/api/impl/beacon/blocks/index.ts | 21 ++- .../src/chain/archiveStore/archiveStore.ts | 24 +-- .../historicalState/getHistoricalState.ts | 6 +- .../historicalState/historicalStateRegen.ts | 1 + .../frequencyStateArchiveStrategy.ts | 4 +- .../chain/archiveStore/utils/archiveBlocks.ts | 20 ++- .../chain/archiveStore/utils/pruneHistory.ts | 4 +- .../archiveStore/utils/updateBackfillRange.ts | 4 +- .../src/chain/beaconEngine/beaconEngine.ts | 125 +++++++++++++++- .../src/chain/beaconEngine/interface.ts | 36 ++++- .../src/chain/blocks/writeBlockInputToDb.ts | 9 +- packages/beacon-node/src/chain/chain.ts | 137 +++++++++++------- packages/beacon-node/src/chain/interface.ts | 12 +- .../src/chain/lightClient/index.ts | 6 +- .../beacon-node/src/chain/opPools/opPool.ts | 7 +- packages/beacon-node/src/chain/regen/regen.ts | 4 +- .../src/chain/stateCache/datastore/db.ts | 4 +- packages/beacon-node/src/db/index.ts | 2 +- packages/beacon-node/src/db/interface.ts | 31 ++-- packages/beacon-node/src/network/network.ts | 4 +- .../src/network/processor/gossipHandlers.ts | 1 + .../src/network/processor/index.ts | 4 +- .../reqresp/handlers/beaconBlocksByHead.ts | 4 +- .../reqresp/handlers/beaconBlocksByRange.ts | 87 ++++------- .../reqresp/handlers/beaconBlocksByRoot.ts | 4 +- .../reqresp/handlers/blobSidecarsByRange.ts | 4 +- .../handlers/dataColumnSidecarsByRange.ts | 6 +- .../handlers/dataColumnSidecarsByRoot.ts | 5 +- .../executionPayloadEnvelopesByRoot.ts | 4 +- .../src/network/reqresp/handlers/index.ts | 10 +- .../utils/dataColumnResponseValidation.ts | 8 +- .../beacon-node/src/sync/backfill/backfill.ts | 99 ++++++------- .../test/mocks/mockedBeaconChain.ts | 5 + .../beacon/blocks/getBlockHeaders.test.ts | 13 +- .../handlers/beaconBlocksByHead.test.ts | 4 +- 35 files changed, 441 insertions(+), 278 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 1e15a938b7bd..0ff1d3925bc1 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -65,7 +65,10 @@ import { import {isOptimisticBlock} from "../../../../util/forkChoice.js"; import {kzg} from "../../../../util/kzg.js"; import {promiseAllMaybeAsync} from "../../../../util/promises.js"; -import {getSignedBlockBytesFromSignedBlockContentsSerialized} from "../../../../util/sszBytes.js"; +import { + getSignedBlockBytesFromSignedBlockContentsSerialized, + getSlotFromSignedBeaconBlockSerialized, +} from "../../../../util/sszBytes.js"; import {ApiModules} from "../../types.js"; import {assertUniqueItems} from "../../utils.js"; import {getBlockResponse, toBeaconHeaderResponse} from "./utils.js"; @@ -88,11 +91,7 @@ export function getBeaconBlockApi({ config, metrics, network, - db, -}: Pick< - ApiModules, - "chain" | "config" | "metrics" | "network" | "db" ->): ApplicationMethods { +}: Pick): ApplicationMethods { const publishBlockV2: ApplicationMethods["publishBlockV2"] = async ( {signedBlockContents, broadcastValidation}, context, @@ -487,8 +486,14 @@ export function getBeaconBlockApi({ const result: routes.beacon.BlockHeaderResponse[] = []; if (parentRoot) { - const finalizedBlock = await db.blockArchive.getByParentRoot(fromHex(parentRoot)); - if (finalizedBlock) { + // Block DB is engine-owned; read finalized block bytes via the engine and deserialize here. + const finalizedBlockBytes = await chain.getSerializedFinalizedBlockByParentRoot(fromHex(parentRoot)); + if (finalizedBlockBytes) { + const blockSlot = getSlotFromSignedBeaconBlockSerialized(finalizedBlockBytes); + if (blockSlot === null) { + throw Error(`Invalid block bytes from archive for parentRoot=${parentRoot}`); + } + const finalizedBlock = config.getForkTypes(blockSlot).SignedBeaconBlock.deserialize(finalizedBlockBytes); result.push(toBeaconHeaderResponse(config, finalizedBlock, true)); } const nonFinalizedBlocks = chain.forkChoice.getBlockSummariesByParentRoot(parentRoot); diff --git a/packages/beacon-node/src/chain/archiveStore/archiveStore.ts b/packages/beacon-node/src/chain/archiveStore/archiveStore.ts index 849c5a56a6a0..0c97832ec80c 100644 --- a/packages/beacon-node/src/chain/archiveStore/archiveStore.ts +++ b/packages/beacon-node/src/chain/archiveStore/archiveStore.ts @@ -2,7 +2,7 @@ import {CheckpointWithHex} from "@lodestar/fork-choice"; import {LoggerNode} from "@lodestar/logger/node"; import {Checkpoint} from "@lodestar/types/phase0"; import {callFnWhenAwait} from "@lodestar/utils"; -import {IBeaconDb} from "../../db/index.js"; +import {IBeaconChainDb} from "../../db/index.js"; import {Metrics} from "../../metrics/metrics.js"; import {isOptimisticBlock} from "../../util/forkChoice.js"; import {JobItemQueue, isQueueErrorAborted} from "../../util/queue/index.js"; @@ -12,12 +12,11 @@ import {PROCESS_FINALIZED_CHECKPOINT_QUEUE_LENGTH} from "./constants.js"; import {HistoricalStateRegen} from "./historicalState/historicalStateRegen.js"; import {ArchiveStoreOpts, ArchiveStoreTask} from "./interface.js"; import {migrateFinalizedDA} from "./utils/archiveBlocks.js"; -import {pruneHistory} from "./utils/pruneHistory.js"; import {updateBackfillRange} from "./utils/updateBackfillRange.js"; type ArchiveStoreModules = { chain: IBeaconChain; - db: IBeaconDb; + db: IBeaconChainDb; logger: LoggerNode; metrics: Metrics | null; }; @@ -33,7 +32,7 @@ export class ArchiveStore { private archiveDataEpochs?: number; private readonly chain: IBeaconChain; - private readonly db: IBeaconDb; + private readonly db: IBeaconChainDb; private readonly logger: LoggerNode; private readonly metrics: Metrics | null; private readonly opts: ArchiveStoreInitOpts; @@ -77,12 +76,9 @@ export class ArchiveStore { if (this.opts.pruneHistory) { // prune ALL stale data before starting this.logger.info("Pruning historical data"); + // Block + state DB is engine-owned; prune through the engine. await callFnWhenAwait( - pruneHistory( - this.chain.config, - this.db, - this.logger, - this.metrics, + this.chain.beaconEngine.pruneHistory( this.opts.anchorState.finalizedCheckpoint.epoch, this.chain.clock.currentEpoch ), @@ -169,14 +165,8 @@ export class ArchiveStore { if (this.opts.pruneHistory) { const timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer(); - await pruneHistory( - this.chain.config, - this.db, - this.logger, - this.metrics, - finalizedEpoch, - this.chain.clock.currentEpoch - ); + // Block + state DB is engine-owned; prune through the engine. + await this.chain.beaconEngine.pruneHistory(finalizedEpoch, this.chain.clock.currentEpoch); timer?.({source: ArchiveStoreTask.PruneHistory}); } diff --git a/packages/beacon-node/src/chain/archiveStore/historicalState/getHistoricalState.ts b/packages/beacon-node/src/chain/archiveStore/historicalState/getHistoricalState.ts index b24b09021cfd..34b5905a5113 100644 --- a/packages/beacon-node/src/chain/archiveStore/historicalState/getHistoricalState.ts +++ b/packages/beacon-node/src/chain/archiveStore/historicalState/getHistoricalState.ts @@ -6,7 +6,7 @@ import { createBeaconStateViewForHistoricalRegen, } from "@lodestar/state-transition"; import {byteArrayEquals} from "@lodestar/utils"; -import {IBeaconDb} from "../../../db/index.js"; +import {IBeaconEngineDb} from "../../../db/index.js"; import {HistoricalStateRegenMetrics} from "./metrics.js"; import {RegenErrorType} from "./types.js"; @@ -16,7 +16,7 @@ import {RegenErrorType} from "./types.js"; export async function getNearestState( slot: number, config: BeaconConfig, - db: IBeaconDb, + db: IBeaconEngineDb, nativeStateView: boolean ): Promise { const stateBytesArr = await db.stateArchive.binaries({limit: 1, lte: slot, reverse: true}); @@ -36,7 +36,7 @@ export async function getNearestState( export async function getHistoricalState( slot: number, config: BeaconConfig, - db: IBeaconDb, + db: IBeaconEngineDb, nativeStateView: boolean, metrics?: HistoricalStateRegenMetrics ): Promise { diff --git a/packages/beacon-node/src/chain/archiveStore/historicalState/historicalStateRegen.ts b/packages/beacon-node/src/chain/archiveStore/historicalState/historicalStateRegen.ts index aabcb669aa68..03b7eaf69089 100644 --- a/packages/beacon-node/src/chain/archiveStore/historicalState/historicalStateRegen.ts +++ b/packages/beacon-node/src/chain/archiveStore/historicalState/historicalStateRegen.ts @@ -15,6 +15,7 @@ const WORKER_DIR = process.env.NODE_ENV === "test" ? "../../../../lib/chain/arch /** * HistoricalStateRegen limits the damage from recreating historical states * by running regen in a separate worker thread. + * TODO - beacon engine: may move this completely to BeaconEngine */ export class HistoricalStateRegen implements HistoricalStateWorkerApi { private readonly api: ModuleThread; diff --git a/packages/beacon-node/src/chain/archiveStore/strategies/frequencyStateArchiveStrategy.ts b/packages/beacon-node/src/chain/archiveStore/strategies/frequencyStateArchiveStrategy.ts index 94cfff11e548..c89940f825d5 100644 --- a/packages/beacon-node/src/chain/archiveStore/strategies/frequencyStateArchiveStrategy.ts +++ b/packages/beacon-node/src/chain/archiveStore/strategies/frequencyStateArchiveStrategy.ts @@ -3,7 +3,7 @@ import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {Epoch, RootHex, Slot} from "@lodestar/types"; import {Logger} from "@lodestar/utils"; -import {IBeaconDb} from "../../../db/index.js"; +import {IBeaconEngineDb} from "../../../db/index.js"; import {Metrics} from "../../../metrics/metrics.js"; import {AllocSource, BufferPool} from "../../../util/bufferPool.js"; import {getStateSlotFromBytes} from "../../../util/multifork.js"; @@ -34,7 +34,7 @@ export enum FrequencyStateArchiveStep { export class FrequencyStateArchiveStrategy implements StateArchiveStrategy { constructor( private readonly regen: IStateRegenerator, - private readonly db: IBeaconDb, + private readonly db: IBeaconEngineDb, private readonly logger: Logger, private readonly opts: StatesArchiveOpts, private readonly bufferPool?: BufferPool | null diff --git a/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts b/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts index bc3c407bb52c..b75ee15bfe07 100644 --- a/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts +++ b/packages/beacon-node/src/chain/archiveStore/utils/archiveBlocks.ts @@ -6,7 +6,7 @@ import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params"; import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {Epoch, Slot} from "@lodestar/types"; import {Logger, fromAsync, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils"; -import {IBeaconDb} from "../../../db/index.js"; +import {IBeaconChainDb, IBeaconEngineDb} from "../../../db/index.js"; import {BlockArchiveBatchPutBinaryItem} from "../../../db/repositories/index.js"; import {ensureDir, writeIfNotExist} from "../../../util/file.js"; import {BlockRootHex, BlockRootSlot} from "../../../util/sszBytes.js"; @@ -47,7 +47,7 @@ async function persistOrphanedBlock( * calls; the next run should not reprocess a finalized block of this run. */ export async function migrateFinalizedBlocks( - db: IBeaconDb, + db: IBeaconEngineDb, logger: Logger, finalizedCanonicalBlocks: ProtoBlock[], finalizedNonCanonicalBlocks: ProtoBlock[], @@ -113,7 +113,7 @@ export async function migrateFinalizedBlocks( */ export async function migrateFinalizedExecutionPayloadEnvelopes( config: ChainForkConfig, - db: IBeaconDb, + db: IBeaconEngineDb, logger: Logger, snapshot: FinalizedBlockSnapshot, finalizedEpoch: Epoch, @@ -159,7 +159,7 @@ export async function migrateFinalizedExecutionPayloadEnvelopes( */ export async function migrateFinalizedDA( config: ChainForkConfig, - db: IBeaconDb, + db: IBeaconChainDb, lightclientServer: LightClientServer | undefined, logger: Logger, snapshot: FinalizedBlockSnapshot, @@ -304,7 +304,11 @@ export async function migrateFinalizedDA( }); } -async function migrateBlocksFromHotToColdDb(db: IBeaconDb, logger: Logger, blocks: BlockRootSlot[]): Promise { +async function migrateBlocksFromHotToColdDb( + db: IBeaconEngineDb, + logger: Logger, + blocks: BlockRootSlot[] +): Promise { // The input includes the previous finalized block as the last ancestor; its SignedBeaconBlock // was archived on a previous run and is no longer in hot db. `getBinary` returning null for any // block in the batch is therefore treated as "already migrated, skip" rather than an error. @@ -357,7 +361,7 @@ async function migrateBlocksFromHotToColdDb(db: IBeaconDb, logger: Logger, block */ async function migrateBlobSidecarsFromHotToColdDb( config: ChainForkConfig, - db: IBeaconDb, + db: IBeaconChainDb, logger: Logger, blocks: BlockRootSlot[], currentEpoch: Epoch @@ -423,7 +427,7 @@ async function migrateBlobSidecarsFromHotToColdDb( */ async function migrateDataColumnSidecarsFromHotToColdDb( config: ChainForkConfig, - db: IBeaconDb, + db: IBeaconChainDb, logger: Logger, canonicalBlocks: FinalizedProtoSummary[], currentEpoch: Epoch @@ -496,7 +500,7 @@ async function migrateDataColumnSidecarsFromHotToColdDb( */ async function migrateExecutionPayloadEnvelopesFromHotToColdDb( config: ChainForkConfig, - db: IBeaconDb, + db: IBeaconEngineDb, logger: Logger, canonicalBlocks: FinalizedProtoSummary[] ): Promise { diff --git a/packages/beacon-node/src/chain/archiveStore/utils/pruneHistory.ts b/packages/beacon-node/src/chain/archiveStore/utils/pruneHistory.ts index aefaa40732ad..2f08c804e057 100644 --- a/packages/beacon-node/src/chain/archiveStore/utils/pruneHistory.ts +++ b/packages/beacon-node/src/chain/archiveStore/utils/pruneHistory.ts @@ -2,12 +2,12 @@ import {ChainConfig} from "@lodestar/config"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {Epoch} from "@lodestar/types"; import {Logger} from "@lodestar/utils"; -import {IBeaconDb} from "../../../db/interface.js"; +import {IBeaconEngineDb} from "../../../db/interface.js"; import {Metrics} from "../../../metrics/index.js"; export async function pruneHistory( config: ChainConfig, - db: IBeaconDb, + db: IBeaconEngineDb, logger: Logger, metrics: Metrics | null | undefined, finalizedEpoch: Epoch, diff --git a/packages/beacon-node/src/chain/archiveStore/utils/updateBackfillRange.ts b/packages/beacon-node/src/chain/archiveStore/utils/updateBackfillRange.ts index 1ae4d5540ace..c17fd87941f4 100644 --- a/packages/beacon-node/src/chain/archiveStore/utils/updateBackfillRange.ts +++ b/packages/beacon-node/src/chain/archiveStore/utils/updateBackfillRange.ts @@ -1,6 +1,6 @@ import {CheckpointWithHex} from "@lodestar/fork-choice"; import {Logger} from "@lodestar/logger"; -import {IBeaconDb} from "../../../db/interface.js"; +import {IBeaconChainDb} from "../../../db/interface.js"; import {IBeaconChain} from "../../interface.js"; /** @@ -14,7 +14,7 @@ import {IBeaconChain} from "../../interface.js"; * slot. */ export async function updateBackfillRange( - {chain, db, logger}: {chain: IBeaconChain; db: IBeaconDb; logger: Logger}, + {chain, db, logger}: {chain: IBeaconChain; db: IBeaconChainDb; logger: Logger}, finalized: CheckpointWithHex ): Promise { try { diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 90963f65e1b0..c4b8e9f75c5c 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -77,19 +77,20 @@ import { } from "@lodestar/types"; import {Logger, byteArrayEquals, fromHex, sleep, toRootHex} from "@lodestar/utils"; import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js"; -import {IBeaconDb} from "../../db/index.js"; +import {IBeaconEngineDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; import {IClock} from "../../util/clock.js"; import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; import {callInNextEventLoop} from "../../util/eventLoop.js"; import {isOptimisticBlock} from "../../util/forkChoice.js"; -import {BlockRootSlot} from "../../util/sszBytes.js"; +import {BlockRootSlot, getSlotFromSignedBeaconBlockSerialized} from "../../util/sszBytes.js"; import {ArchiveMode, ArchiveStoreTask, StateArchiveStrategy} from "../archiveStore/interface.js"; import {FrequencyStateArchiveStrategy} from "../archiveStore/strategies/frequencyStateArchiveStrategy.js"; import { migrateFinalizedBlocks, migrateFinalizedExecutionPayloadEnvelopes, } from "../archiveStore/utils/archiveBlocks.js"; +import {pruneHistory} from "../archiveStore/utils/pruneHistory.js"; import {CheckpointBalancesCache} from "../balancesCache.js"; import {BeaconProposerCache} from "../beaconProposerCache.js"; import {isBlockInputBlobs, isBlockInputColumns} from "../blocks/blockInput/blockInput.js"; @@ -251,7 +252,7 @@ export class BeaconEngine implements IBeaconEngine { readonly validatorMonitor: ValidatorMonitor | null; // Engine reads execution payload envelopes for block production (getParentExecutionRequests). Writes // / archival / network handlers still use the shared `db` directly until the DB-ownership phase. - readonly db: IBeaconDb; + readonly db: IBeaconEngineDb; // Engine owns state archival (states DB). Finalized-state archive runs inside migrateFinalized; the // facade delegates temp-state (onCheckpoint) and shutdown persistence to engine methods. private readonly stateArchiveStrategy: StateArchiveStrategy; @@ -510,6 +511,109 @@ export class BeaconEngine implements IBeaconEngine { return {finalizedSlot, nonFinalized}; } + // --- Block DB (engine-owned). Bytes-only; no SignedBeaconBlock crosses the seam. --- + + /** Serialized block by root: hot then cold. `finalized` reflects a cold-db (archive) hit. */ + async getSerializedBlockByRoot( + root: Uint8Array + ): Promise<{bytes: Uint8Array; slot: Slot; finalized: boolean} | null> { + const hot = await this.db.block.getBinary(root); + if (hot) { + const slot = getSlotFromSignedBeaconBlockSerialized(hot); + if (slot === null) throw Error(`Invalid block data stored in DB for root: ${toRootHex(root)}`); + return {bytes: hot, slot, finalized: false}; + } + const cold = await this.db.blockArchive.getBinaryEntryByRoot(root); + return cold && {bytes: cold.value, slot: cold.key, finalized: true}; + } + + /** Serialized finalized block by slot (cold archive). */ + async getSerializedFinalizedBlockBySlot(slot: Slot): Promise { + return (await this.db.blockArchive.getBinary(slot)) ?? null; + } + + /** Finalized block slot by root (cold archive index). */ + async getFinalizedBlockSlotByRoot(root: Uint8Array): Promise { + return this.db.blockArchive.getSlotByRoot(root); + } + + /** Serialized finalized block by parent root (cold archive index). */ + async getSerializedFinalizedBlockByParentRoot(parentRoot: Uint8Array): Promise { + const slot = await this.db.blockArchive.getSlotByParentRoot(parentRoot); + return slot !== null ? ((await this.db.blockArchive.getBinary(slot)) ?? null) : null; + } + + /** + * Thin serve refs for BeaconBlocksByRange: fork-choice reads stay inside the engine; only slot + raw + * root cross. Returns the archive boundary (`finalizedSlot`, inclusive — blocks incl. the finalized + * block migrate at finalization) and every canonical block in `(finalizedSlot, endSlot)` (no filter). + */ + getCanonicalBlockRootSlotsByRange( + startSlot: Slot, + endSlot: Slot + ): {finalizedSlot: Slot; nonFinalized: BlockRootSlot[]} { + const finalizedSlot = this.forkChoice.getFinalizedCheckpointSlot(); + const archiveMaxSlot = finalizedSlot; + const nonFinalized: BlockRootSlot[] = []; + if (endSlot > archiveMaxSlot) { + const head = this.forkChoice.getHead(); + // newest→oldest; iterate ascending + const headChain = this.forkChoice.getAllAncestorBlocks(head.blockRoot, head.payloadStatus); + for (let i = headChain.length - 1; i >= 0; i--) { + const block = headChain[i]; + if (block.slot > archiveMaxSlot && block.slot >= startSlot && block.slot < endSlot) { + nonFinalized.push({slot: block.slot, root: fromHex(block.blockRoot)}); + } else if (block.slot >= endSlot) { + break; + } + } + } + return {finalizedSlot, nonFinalized}; + } + + /** Persist a hot block, bytes-first (key = block root). */ + async persistBlock(root: Uint8Array, serializedBytes: Uint8Array): Promise { + await this.db.block.putBinary(root, serializedBytes); + } + + // --- Cold block archive writes/reverse-lookup (backfill; engine-owned). Bytes-first + root/parent indexes. --- + + async persistArchiveBlock( + slot: Slot, + serializedBytes: Uint8Array, + blockRoot: Uint8Array, + parentRoot: Uint8Array + ): Promise { + await this.db.blockArchive.batchPutBinary([{key: slot, value: serializedBytes, slot, blockRoot, parentRoot}]); + } + + async batchPersistArchiveBlocks( + entries: {slot: Slot; bytes: Uint8Array; blockRoot: Uint8Array; parentRoot: Uint8Array}[] + ): Promise { + await this.db.blockArchive.batchPutBinary( + entries.map((e) => ({ + key: e.slot, + value: e.bytes, + slot: e.slot, + blockRoot: e.blockRoot, + parentRoot: e.parentRoot, + })) + ); + } + + /** Nearest archived block strictly below `slot` (reverse range, limit 1) — serialized. */ + async getSerializedArchiveBlockBefore(slot: Slot): Promise<{slot: Slot; bytes: Uint8Array} | null> { + for await (const {key, value} of this.db.blockArchive.binaryEntriesStream({lt: slot, reverse: true, limit: 1})) { + return {slot: this.db.blockArchive.decodeKey(key), bytes: value}; + } + return null; + } + + /** Serialized state by state root (cold archive). */ + async getSerializedStateByRoot(stateRoot: Uint8Array): Promise { + return (await this.db.stateArchive.getBinaryByRoot(stateRoot)) ?? null; + } + /** * Parent execution requests for block production. DB-only: the facade-owned DA cache is not visible * here, so callers that need cache-only (DB-write-pending) envelopes check the cache first @@ -2181,6 +2285,21 @@ export class BeaconEngine implements IBeaconEngine { await this.stateArchiveStrategy.onCheckpoint(headStateRoot, this.metrics); } + /** Prune finalized blocks + states below the retention window (engine-owned block/state DB). */ + async pruneHistory(finalizedEpoch: Epoch, currentEpoch: Epoch): Promise { + await pruneHistory(this.config, this.db, this.logger, this.metrics, finalizedEpoch, currentEpoch); + } + + /** Populate the (engine-owned) op pool from its persisted DB repos. Call once on startup. */ + async loadOpPoolFromDisk(): Promise { + await this.opPool.fromPersisted(this.db); + } + + /** Persist the (engine-owned) op pool to its DB repos. Call before stopping. */ + async persistOpPoolToDisk(): Promise { + await this.opPool.toPersisted(this.db); + } + // TODO - beacon engine: scalar state reads (getBeaconProposer, getValidator, getBalance, // getRandaoMix, getBlockRootAtSlot, getStateRootAtSlot, getShufflingDecisionRoot). Deferred — // signature shape (state vs stateRoot) to be decided alongside Phase 4 bytes-first. diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 8f0349e1e219..630a455c944b 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -32,7 +32,7 @@ import { gloas, } from "@lodestar/types"; import {Logger} from "@lodestar/utils"; -import {IBeaconDb} from "../../db/index.js"; +import {IBeaconEngineDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; import {BufferPool} from "../../util/bufferPool.js"; import {IClock} from "../../util/clock.js"; @@ -72,7 +72,7 @@ export type BeaconEngineModules = { // TODO - beacon engine: emitter is facade infra; forkChoice/regen should not depend on it inside the engine. emitter: ChainEventEmitter; signal: AbortSignal; - db: IBeaconDb; + db: IBeaconEngineDb; validatorMonitor: ValidatorMonitor | null; seenBlockInputCache: SeenBlockInput; isAnchorStateFinalized: boolean; @@ -240,6 +240,31 @@ export interface IBeaconEngine { persistExecutionPayloadEnvelope(blockRoot: Uint8Array, serializedBytes: Uint8Array): Promise; // Thin serving refs for ExecutionPayloadEnvelopesByRange — fork choice read engine-side, no ProtoBlock crosses. getFullBlockRootSlotsByRange(startSlot: Slot, endSlot: Slot): {finalizedSlot: Slot; nonFinalized: BlockRootSlot[]}; + + // Block + state DB (engine-owned). Bytes-only — no `SignedBeaconBlock`/`BeaconState` crosses the seam. + getSerializedBlockByRoot(root: Uint8Array): Promise<{bytes: Uint8Array; slot: Slot; finalized: boolean} | null>; + getSerializedFinalizedBlockBySlot(slot: Slot): Promise; + getFinalizedBlockSlotByRoot(root: Uint8Array): Promise; + getSerializedFinalizedBlockByParentRoot(parentRoot: Uint8Array): Promise; + // Thin serving refs for BeaconBlocksByRange (all canonical blocks; inclusive finalized boundary). + getCanonicalBlockRootSlotsByRange( + startSlot: Slot, + endSlot: Slot + ): {finalizedSlot: Slot; nonFinalized: BlockRootSlot[]}; + persistBlock(root: Uint8Array, serializedBytes: Uint8Array): Promise; + getSerializedStateByRoot(stateRoot: Uint8Array): Promise; + // Cold block-archive writes + reverse-lookup (backfill). + persistArchiveBlock( + slot: Slot, + serializedBytes: Uint8Array, + blockRoot: Uint8Array, + parentRoot: Uint8Array + ): Promise; + batchPersistArchiveBlocks( + entries: {slot: Slot; bytes: Uint8Array; blockRoot: Uint8Array; parentRoot: Uint8Array}[] + ): Promise; + getSerializedArchiveBlockBefore(slot: Slot): Promise<{slot: Slot; bytes: Uint8Array} | null>; + produceCommonBlockBody(blockAttributes: BlockAttributes): Promise; produceBlockBase(attrs: {slot: Slot; randaoReveal: BLSSignature; graffiti: Bytes32}): Promise; // Compute the post-state root + proposer reward for a produced block. @@ -409,4 +434,11 @@ export interface IBeaconEngine { * The facade keeps only the event listener + error handling. */ archiveStateOnCheckpoint(): Promise; + + /** Prune finalized blocks + states below the retention window (engine-owned block/state DB). */ + pruneHistory(finalizedEpoch: Epoch, currentEpoch: Epoch): Promise; + + /** Load / persist the (engine-owned) op pool from/to its DB repos. Facade lifecycle delegates here. */ + loadOpPoolFromDisk(): Promise; + persistOpPoolToDisk(): Promise; } diff --git a/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts b/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts index 173044a2824f..d4f50abffb6b 100644 --- a/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts +++ b/packages/beacon-node/src/chain/blocks/writeBlockInputToDb.ts @@ -36,15 +36,20 @@ async function writeBlockAndBlobsToDb(this: BeaconChain, blockInput: IBlockInput : undefined; const fnPromises: Promise[] = []; + // Block DB is engine-owned; write through the engine (bytes-first). Serialize on a cache miss. const blockBytes = this.serializedCache.get(block); if (blockBytes) { // skip serializing data if we already have it this.metrics?.importBlock.persistBlockWithSerializedDataCount.inc(); - fnPromises.push(this.db.block.putBinary(this.db.block.getId(block), blockBytes)); } else { this.metrics?.importBlock.persistBlockNoSerializedDataCount.inc(); - fnPromises.push(this.db.block.add(block)); } + fnPromises.push( + this.beaconEngine.persistBlock( + blockRoot, + blockBytes ?? this.config.getForkTypes(slot).SignedBeaconBlock.serialize(block) + ) + ); this.logger.debug("Persist block to hot DB", {slot, root: blockRootHex, inputType: blockInput.type, numBlobs}); diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index b2522cb2ffa5..957ea634aed6 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -48,7 +48,7 @@ import { import {Logger, fromHex, gweiToWei, isErrorAborted, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils"; import {ProcessShutdownCallback} from "@lodestar/validator"; import {GENESIS_EPOCH, ZERO_HASH} from "../constants/index.js"; -import {IBeaconDb} from "../db/index.js"; +import {IBeaconChainDb, IBeaconDb} from "../db/index.js"; import {BLOB_SIDECARS_IN_WRAPPER_INDEX} from "../db/repositories/blobSidecars.js"; import {BuilderStatus} from "../execution/builder/http.js"; import {IExecutionBuilder, IExecutionEngine} from "../execution/index.js"; @@ -62,7 +62,7 @@ import {ensureDir, writeIfNotExist} from "../util/file.js"; import {isOptimisticBlock} from "../util/forkChoice.js"; import {JobItemQueue} from "../util/queue/itemQueue.js"; import {SerializedCache} from "../util/serializedCache.js"; -import {BlockRootSlot, getSlotFromSignedBeaconBlockSerialized} from "../util/sszBytes.js"; +import {BlockRootSlot} from "../util/sszBytes.js"; import {ArchiveStore} from "./archiveStore/archiveStore.js"; import {CheckpointBalancesCache} from "./balancesCache.js"; import {BeaconEngine} from "./beaconEngine/index.js"; @@ -280,7 +280,9 @@ export class BeaconChain implements IBeaconChain { protected readonly blockProcessor: BlockProcessor; protected readonly payloadEnvelopeProcessor: PayloadEnvelopeProcessor; - protected readonly db: IBeaconDb; + // Facade owns DA + light-client + backfill stores only; block/state/envelope/op-pool repos are + // engine-owned (reached via `beaconEngine`). Bootstrap passes the full `IBeaconDb`, narrowed here. + protected readonly db: IBeaconChainDb; // this is only available if nHistoricalStates is enabled private readonly cpStateDatastore?: CPStateDatastore; private abortController = new AbortController(); @@ -354,7 +356,8 @@ export class BeaconChain implements IBeaconChain { this.bufferPool = new BufferPool(anchorState.serializedSize(), metrics); const fileDataStore = opts.nHistoricalStatesFileDataStore ?? true; - this.cpStateDatastore = fileDataStore ? new FileCPStateDatastore(dataDir) : new DbCPStateDatastore(this.db); + // checkpointState is an engine repo; build the datastore from the bootstrap `db` (union) param. + this.cpStateDatastore = fileDataStore ? new FileCPStateDatastore(dataDir) : new DbCPStateDatastore(db); const nodeId = computeNodeIdFromPrivateKey(privateKey); const initialCustodyGroupCount = opts.initialCustodyGroupCount ?? config.CUSTODY_REQUIREMENT; @@ -567,13 +570,13 @@ export class BeaconChain implements IBeaconChain { /** Populate in-memory caches with persisted data. Call at least once on startup */ async loadFromDisk(): Promise { await this.regen.init(); - await this.opPool.fromPersisted(this.db); + await this.beaconEngine.loadOpPoolFromDisk(); } /** Persist in-memory data to the DB. Call at least once before stopping the process */ async persistToDisk(): Promise { await this.archiveStore.persistToDisk(); - await this.opPool.toPersisted(this.db); + await this.beaconEngine.persistOpPoolToDisk(); } // TODO - beacon engine: remove this @@ -691,7 +694,7 @@ export class BeaconChain implements IBeaconChain { } // this is mostly useful for a node with `--chain.archiveStateEpochFrequency 1` - const data = await this.db.stateArchive.getBinaryByRoot(fromHex(stateRoot)); + const data = await this.beaconEngine.getSerializedStateByRoot(fromHex(stateRoot)); return data && {state: data, executionOptimistic: false, finalized: true}; } @@ -760,88 +763,114 @@ export class BeaconChain implements IBeaconChain { if (block) { // Block found in fork-choice. // It may be in the block input cache, awaiting full DA reconstruction, check there first - // Otherwise (most likely), check the hot db + // Otherwise (most likely), the engine reads the hot db (falling back to cold on an archive race) const blockInput = this.seenBlockInputCache.get(block.blockRoot); if (blockInput?.hasBlock()) { return {block: blockInput.getBlock(), executionOptimistic: isOptimisticBlock(block), finalized: false}; } - const data = await this.db.block.get(fromHex(block.blockRoot)); - if (data) { - return {block: data, executionOptimistic: isOptimisticBlock(block), finalized: false}; + const res = await this.beaconEngine.getSerializedBlockByRoot(fromHex(block.blockRoot)); + if (res) { + return { + block: this.config.getForkTypes(res.slot).SignedBeaconBlock.deserialize(res.bytes), + executionOptimistic: isOptimisticBlock(block), + finalized: res.finalized, + }; } } // A non-finalized slot expected to be found in the hot db, could be archived during - // this function runtime, so if not found in the hot db, fallback to the cold db - // TODO: Add a lock to the archiver to have determinstic behaviour on where are blocks + // this function runtime, so if not found, fallback to the cold db (by slot) } - const data = await this.db.blockArchive.get(slot); - return data && {block: data, executionOptimistic: false, finalized: true}; + const bytes = await this.beaconEngine.getSerializedFinalizedBlockBySlot(slot); + return ( + bytes && { + block: this.config.getForkTypes(slot).SignedBeaconBlock.deserialize(bytes), + executionOptimistic: false, + finalized: true, + } + ); } async getBlockByRoot( root: string ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { - const block = this.forkChoice.getBlockHexDefaultStatus(root); - if (block) { + const protoBlock = this.forkChoice.getBlockHexDefaultStatus(root); + if (protoBlock) { // Block found in fork-choice. // It may be in the block input cache, awaiting full DA reconstruction, check there first - // Otherwise (most likely), check the hot db - const blockInput = this.seenBlockInputCache.get(block.blockRoot); + // Otherwise (most likely), the engine reads the hot db (falling back to cold on an archive race) + const blockInput = this.seenBlockInputCache.get(protoBlock.blockRoot); if (blockInput?.hasBlock()) { - return {block: blockInput.getBlock(), executionOptimistic: isOptimisticBlock(block), finalized: false}; - } - const data = await this.db.block.get(fromHex(root)); - if (data) { - return {block: data, executionOptimistic: isOptimisticBlock(block), finalized: false}; + return {block: blockInput.getBlock(), executionOptimistic: isOptimisticBlock(protoBlock), finalized: false}; } - // If block is not found in hot db, try cold db since there could be an archive cycle happening - // TODO: Add a lock to the archiver to have deterministic behavior on where are blocks } - const data = await this.db.blockArchive.getByRoot(fromHex(root)); - return data && {block: data, executionOptimistic: false, finalized: true}; + // Engine reads hot→cold and reports which; deserialize facade-side (no object crosses the seam) + const res = await this.beaconEngine.getSerializedBlockByRoot(fromHex(root)); + return ( + res && { + block: this.config.getForkTypes(res.slot).SignedBeaconBlock.deserialize(res.bytes), + executionOptimistic: protoBlock != null && isOptimisticBlock(protoBlock), + finalized: res.finalized, + } + ); } async getSerializedBlockByRoot( - root: string + root: Uint8Array ): Promise<{block: Uint8Array; executionOptimistic: boolean; finalized: boolean; slot: Slot} | null> { - const block = this.forkChoice.getBlockHexDefaultStatus(root); - if (block) { + // TODO - beacon engine: make a separate call, need to be lightweight + const protoBlock = this.forkChoice.getBlockHexDefaultStatus(toRootHex(root)); + if (protoBlock) { // Block found in fork-choice. // It may be in the block input cache, awaiting full DA reconstruction, check there first - // Otherwise (most likely), check the hot db - const blockInput = this.seenBlockInputCache.get(block.blockRoot); + // Otherwise (most likely), the engine reads the hot db (falling back to cold on an archive race) + const blockInput = this.seenBlockInputCache.get(protoBlock.blockRoot); if (blockInput?.hasBlock()) { const signedBlock = blockInput.getBlock(); - const serialized = this.serializedCache.get(signedBlock); - if (serialized) { - return { - block: serialized, - executionOptimistic: isOptimisticBlock(block), - finalized: false, - slot: blockInput.slot, - }; - } return { - block: sszTypesFor(blockInput.forkName).SignedBeaconBlock.serialize(signedBlock), - executionOptimistic: isOptimisticBlock(block), + block: + this.serializedCache.get(signedBlock) ?? + sszTypesFor(blockInput.forkName).SignedBeaconBlock.serialize(signedBlock), + executionOptimistic: isOptimisticBlock(protoBlock), finalized: false, slot: blockInput.slot, }; } - const data = await this.db.block.getBinary(fromHex(root)); - if (data) { - const slot = getSlotFromSignedBeaconBlockSerialized(data); - if (slot === null) throw new Error(`Invalid block data stored in DB for root: ${root}`); - return {block: data, executionOptimistic: isOptimisticBlock(block), finalized: false, slot}; - } - // If block is not found in hot db, try cold db since there could be an archive cycle happening - // TODO: Add a lock to the archiver to have deterministic behavior on where are blocks } - const data = await this.db.blockArchive.getBinaryEntryByRoot(fromHex(root)); - return data && {block: data.value, executionOptimistic: false, finalized: true, slot: data.key}; + const res = await this.beaconEngine.getSerializedBlockByRoot(root); + return ( + res && { + block: res.bytes, + executionOptimistic: protoBlock != null && isOptimisticBlock(protoBlock), + finalized: res.finalized, + slot: res.slot, + } + ); + } + + /** Engine passthrough: serialized finalized block by slot (cold archive). */ + getSerializedFinalizedBlockBySlot(slot: Slot): Promise { + return this.beaconEngine.getSerializedFinalizedBlockBySlot(slot); + } + + /** Engine passthrough: thin BeaconBlocksByRange serving refs (fork choice read engine-side). */ + getCanonicalBlockRootSlotsByRange( + startSlot: Slot, + endSlot: Slot + ): {finalizedSlot: Slot; nonFinalized: BlockRootSlot[]} { + return this.beaconEngine.getCanonicalBlockRootSlotsByRange(startSlot, endSlot); + } + + /** Engine passthrough: finalized block slot by root (cold archive index). */ + getFinalizedBlockSlotByRoot(root: Uint8Array): Promise { + return this.beaconEngine.getFinalizedBlockSlotByRoot(root); + } + + /** Engine passthrough: serialized finalized block by parent root (cold archive index). */ + getSerializedFinalizedBlockByParentRoot(parentRoot: Uint8Array): Promise { + return this.beaconEngine.getSerializedFinalizedBlockByParentRoot(parentRoot); } async getBlobSidecars(blockSlot: Slot, blockRootHex: string): Promise { diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 90a1a0a2b1aa..46cd483200c3 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -213,10 +213,10 @@ export interface IBeaconChain { slot: Slot ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null>; /** - * Get local block by root, does not fetch from the network + * Get local serialized block by root (bytes-first), does not fetch from the network */ getSerializedBlockByRoot( - root: RootHex + root: Uint8Array ): Promise<{block: Uint8Array; executionOptimistic: boolean; finalized: boolean; slot: Slot} | null>; /** * Get local block by root, does not fetch from the network @@ -224,6 +224,14 @@ export interface IBeaconChain { getBlockByRoot( root: RootHex ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null>; + // Engine passthroughs (block DB is engine-owned) — used by p2p handlers / reqresp utils / api. + getSerializedFinalizedBlockBySlot(slot: Slot): Promise; + getCanonicalBlockRootSlotsByRange( + startSlot: Slot, + endSlot: Slot + ): {finalizedSlot: Slot; nonFinalized: BlockRootSlot[]}; + getFinalizedBlockSlotByRoot(root: Uint8Array): Promise; + getSerializedFinalizedBlockByParentRoot(parentRoot: Uint8Array): Promise; getBlobSidecars(blockSlot: Slot, blockRootHex: string): Promise; getSerializedBlobSidecars(blockSlot: Slot, blockRootHex: string): Promise; getDataColumnSidecars(blockSlot: Slot, blockRootHex: string): Promise; diff --git a/packages/beacon-node/src/chain/lightClient/index.ts b/packages/beacon-node/src/chain/lightClient/index.ts index c8c5ce18905d..62b46baf2a58 100644 --- a/packages/beacon-node/src/chain/lightClient/index.ts +++ b/packages/beacon-node/src/chain/lightClient/index.ts @@ -48,7 +48,7 @@ import { } from "@lodestar/types"; import {Logger, MapDef, byteArrayEquals, pruneSetToMax, toRootHex} from "@lodestar/utils"; import {ZERO_HASH} from "../../constants/index.js"; -import {IBeaconDb} from "../../db/index.js"; +import {IBeaconChainDb} from "../../db/index.js"; import {NUM_WITNESS, NUM_WITNESS_ELECTRA} from "../../db/repositories/lightclientSyncCommitteeWitness.js"; import {Metrics} from "../../metrics/index.js"; import {IClock} from "../../util/clock.js"; @@ -82,7 +82,7 @@ export type SyncAttestedData = { type LightClientServerModules = { config: ChainForkConfig; clock: IClock; - db: IBeaconDb; + db: IBeaconChainDb; metrics: Metrics | null; emitter: ChainEventEmitter; logger: Logger; @@ -193,7 +193,7 @@ const MAX_PREV_HEAD_DATA = 32; * Storing 4 witness per epoch costs `6848 * 4 * 32 = 876544 ~ 0.9 MB/m`. */ export class LightClientServer { - private readonly db: IBeaconDb; + private readonly db: IBeaconChainDb; private readonly config: ChainForkConfig; private readonly metrics: Metrics | null; private readonly emitter: ChainEventEmitter; diff --git a/packages/beacon-node/src/chain/opPools/opPool.ts b/packages/beacon-node/src/chain/opPools/opPool.ts index d6554f5fbdfb..e9f70554b633 100644 --- a/packages/beacon-node/src/chain/opPools/opPool.ts +++ b/packages/beacon-node/src/chain/opPools/opPool.ts @@ -26,7 +26,7 @@ import { sszTypesFor, } from "@lodestar/types"; import {fromHex, toHex, toRootHex} from "@lodestar/utils"; -import {IBeaconDb} from "../../db/index.js"; +import {IBeaconEngineDb} from "../../db/index.js"; import {Metrics} from "../../metrics/metrics.js"; import {SignedBLSToExecutionChangeVersioned} from "../../util/types.js"; import {BlockType} from "../interface.js"; @@ -39,6 +39,7 @@ type AttesterSlashingCached = { intersectingIndices: number[]; }; +// TODO - beacon engine: This is owned by BeaconEngine, make sure this is populated by BeaconEngine during gossip validation export class OpPool { /** Map of uniqueId(AttesterSlashing) -> AttesterSlashing */ private readonly attesterSlashings = new Map(); @@ -68,7 +69,7 @@ export class OpPool { return this.blsToExecutionChanges.size; } - async fromPersisted(db: IBeaconDb): Promise { + async fromPersisted(db: IBeaconEngineDb): Promise { const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] = await Promise.all([ db.attesterSlashing.entries(), db.proposerSlashing.values(), @@ -90,7 +91,7 @@ export class OpPool { } } - async toPersisted(db: IBeaconDb): Promise { + async toPersisted(db: IBeaconEngineDb): Promise { await Promise.all([ persistDiff( db.attesterSlashing, diff --git a/packages/beacon-node/src/chain/regen/regen.ts b/packages/beacon-node/src/chain/regen/regen.ts index 13de4f34f952..e65f39e81175 100644 --- a/packages/beacon-node/src/chain/regen/regen.ts +++ b/packages/beacon-node/src/chain/regen/regen.ts @@ -11,7 +11,7 @@ import { } from "@lodestar/state-transition"; import {BeaconBlock, RootHex, SignedBeaconBlock, Slot} from "@lodestar/types"; import {Logger, fromHex, toRootHex} from "@lodestar/utils"; -import {IBeaconDb} from "../../db/index.js"; +import {IBeaconEngineDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; import {nextEventLoop} from "../../util/eventLoop.js"; import {getCheckpointFromState} from "../blocks/utils/checkpoint.js"; @@ -23,7 +23,7 @@ import {RegenError, RegenErrorCode} from "./errors.js"; import {IStateRegeneratorInternal, RegenCaller, StateRegenerationOpts} from "./interface.js"; export type RegenModules = { - db: IBeaconDb; + db: IBeaconEngineDb; forkChoice: IForkChoice; blockStateCache: BlockStateCache; checkpointStateCache: CheckpointStateCache; diff --git a/packages/beacon-node/src/chain/stateCache/datastore/db.ts b/packages/beacon-node/src/chain/stateCache/datastore/db.ts index 64c893c9bdd7..2fb9e36b0920 100644 --- a/packages/beacon-node/src/chain/stateCache/datastore/db.ts +++ b/packages/beacon-node/src/chain/stateCache/datastore/db.ts @@ -1,7 +1,7 @@ import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {Epoch, phase0, ssz} from "@lodestar/types"; import {MapDef} from "@lodestar/utils"; -import {IBeaconDb} from "../../../db/interface.js"; +import {IBeaconEngineDb} from "../../../db/interface.js"; import { getLastProcessedSlotFromBeaconStateSerialized, getSlotFromBeaconStateSerialized, @@ -12,7 +12,7 @@ import {CPStateDatastore, DatastoreKey} from "./types.js"; * Implementation of CPStateDatastore using db. */ export class DbCPStateDatastore implements CPStateDatastore { - constructor(private readonly db: IBeaconDb) {} + constructor(private readonly db: IBeaconEngineDb) {} async write(cpKey: phase0.Checkpoint, stateBytes: Uint8Array): Promise { const serializedCheckpoint = checkpointToDatastoreKey(cpKey); diff --git a/packages/beacon-node/src/db/index.ts b/packages/beacon-node/src/db/index.ts index ecd83a045154..5c23858a40c1 100644 --- a/packages/beacon-node/src/db/index.ts +++ b/packages/beacon-node/src/db/index.ts @@ -1,3 +1,3 @@ export {BeaconDb} from "./beacon.js"; export {Bucket} from "./buckets.js"; -export type {IBeaconDb} from "./interface.js"; +export type {IBeaconChainDb, IBeaconDb, IBeaconEngineDb} from "./interface.js"; diff --git a/packages/beacon-node/src/db/interface.ts b/packages/beacon-node/src/db/interface.ts index 15691ba5944d..c31030d1fc8b 100644 --- a/packages/beacon-node/src/db/interface.ts +++ b/packages/beacon-node/src/db/interface.ts @@ -24,32 +24,39 @@ import { /** * The DB service manages the data layer of the beacon chain * The exposed methods do not refer to the underlying data engine, - * but instead expose relevant beacon chain objects + * but instead expose relevant beacon chain objects. + * + * Ownership is split by owner (BeaconEngine seam): `IBeaconEngineDb` holds the consensus stores the + * engine owns (blocks, states, checkpoint states, payload envelopes, op-pool persistence); `IBeaconChainDb` + * holds the DA + light-client + backfill stores the facade owns. `IBeaconDb` is their composition, held + * only by the bootstrap (CLI / `BeaconNode`). */ -export interface IBeaconDb { +export interface IBeaconEngineDb { // unfinalized blocks block: BlockRepository; // finalized blocks blockArchive: BlockArchiveRepository; - blobSidecars: BlobSidecarsRepository; - blobSidecarsArchive: BlobSidecarsArchiveRepository; - dataColumnSidecar: DataColumnSidecarRepository; - dataColumnSidecarArchive: DataColumnSidecarArchiveRepository; - - executionPayloadEnvelope: ExecutionPayloadEnvelopeRepository; - executionPayloadEnvelopeArchive: ExecutionPayloadEnvelopeArchiveRepository; - // finalized states stateArchive: StateArchiveRepository; // checkpoint states checkpointState: CheckpointStateRepository; - // op pool + executionPayloadEnvelope: ExecutionPayloadEnvelopeRepository; + executionPayloadEnvelopeArchive: ExecutionPayloadEnvelopeArchiveRepository; + + // op pool persistence (the OpPool is engine-owned) voluntaryExit: VoluntaryExitRepository; proposerSlashing: ProposerSlashingRepository; attesterSlashing: AttesterSlashingRepository; blsToExecutionChange: BLSToExecutionChangeRepository; +} + +export interface IBeaconChainDb { + blobSidecars: BlobSidecarsRepository; + blobSidecarsArchive: BlobSidecarsArchiveRepository; + dataColumnSidecar: DataColumnSidecarRepository; + dataColumnSidecarArchive: DataColumnSidecarArchiveRepository; // lightclient bestLightClientUpdate: BestLightClientUpdateRepository; @@ -58,7 +65,9 @@ export interface IBeaconDb { syncCommitteeWitness: SyncCommitteeWitnessRepository; backfilledRanges: BackfilledRanges; +} +export interface IBeaconDb extends IBeaconChainDb, IBeaconEngineDb { pruneHotDb(): Promise; deleteDeprecatedEth1Data(): Promise; diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index 8b2f9bab4fcc..db2def4ce921 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -33,7 +33,7 @@ import {prettyPrintIndices, sleep} from "@lodestar/utils"; import {BlockInputSource} from "../chain/blocks/blockInput/types.js"; import {ChainEvent, IBeaconChain} from "../chain/index.js"; import {computeSubnetForDataColumnSidecar} from "../chain/validation/dataColumnSidecar.js"; -import {IBeaconDb} from "../db/interface.js"; +import {IBeaconChainDb} from "../db/interface.js"; import {Metrics, RegistryMetricCreator} from "../metrics/index.js"; import {IClock} from "../util/clock.js"; import {CustodyConfig} from "../util/dataColumns.js"; @@ -87,7 +87,7 @@ export type NetworkInitModules = { logger: LoggerNode; metrics: Metrics | null; chain: IBeaconChain; - db: IBeaconDb; + db: IBeaconChainDb; getReqRespHandler: GetReqRespHandlerFn; // Optionally pass custom GossipHandlers, for testing gossipHandlers?: GossipHandlers; diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 6b1d2c6714f1..7ab0adfc4590 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -923,6 +923,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand try { chain.opPool.insertAttesterSlashing(fork, attesterSlashing); + // TODO - beacon engine chain.beaconEngine.forkChoice.onAttesterSlashing(attesterSlashing); } catch (e) { logger.error("Error adding attesterSlashing to pool", {}, e as Error); diff --git a/packages/beacon-node/src/network/processor/index.ts b/packages/beacon-node/src/network/processor/index.ts index 357bb967e48e..81940b07d5f1 100644 --- a/packages/beacon-node/src/network/processor/index.ts +++ b/packages/beacon-node/src/network/processor/index.ts @@ -7,7 +7,7 @@ import {BlockInputSource} from "../../chain/blocks/blockInput/types.js"; import {ChainEvent} from "../../chain/emitter.js"; import {GossipErrorCode} from "../../chain/errors/gossipValidation.js"; import {IBeaconChain} from "../../chain/interface.js"; -import {IBeaconDb} from "../../db/interface.js"; +import {IBeaconChainDb} from "../../db/interface.js"; import {Metrics} from "../../metrics/metrics.js"; import {ClockEvent} from "../../util/clock.js"; import {callInNextEventLoop} from "../../util/eventLoop.js"; @@ -41,7 +41,7 @@ export * from "./types.js"; export type NetworkProcessorModules = ValidatorFnsModules & ValidatorFnModules & { chain: IBeaconChain; - db: IBeaconDb; + db: IBeaconChainDb; events: NetworkEventBus; logger: Logger; metrics: Metrics | null; diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByHead.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByHead.ts index 5d22e8fa2d20..81cc1b9dbd75 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByHead.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByHead.ts @@ -4,7 +4,7 @@ import {ForkName, GENESIS_EPOCH, GENESIS_SLOT, isForkPostDeneb} from "@lodestar/ import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp"; import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {fulu} from "@lodestar/types"; -import {toRootHex} from "@lodestar/utils"; +import {fromHex, toRootHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../chain/index.js"; import {getParentRootFromSignedBeaconBlockSerialized} from "../../../util/sszBytes.js"; import {prettyPrintPeerId} from "../../util.js"; @@ -28,7 +28,7 @@ export async function* onBeaconBlocksByHead( const minimumRequestSlot = computeStartSlotAtEpoch(minimumRequestEpoch); for (let blocksSent = 0; blocksSent < count; blocksSent++) { - const blockBytes = await chain.getSerializedBlockByRoot(blockRootHex); + const blockBytes = await chain.getSerializedBlockByRoot(fromHex(blockRootHex)); if (!blockBytes) { if (blocksSent === 0) { throw new ResponseError(RespStatus.RESOURCE_UNAVAILABLE, `Unknown block root ${requestedRootHex}`); diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts index 02d4ac844c40..b4fb14de21bd 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRange.ts @@ -5,30 +5,17 @@ import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp"; import {computeEpochAtSlot} from "@lodestar/state-transition"; import {deneb, phase0} from "@lodestar/types"; import {IBeaconChain} from "../../../chain/index.js"; -import {IBeaconDb} from "../../../db/index.js"; import {prettyPrintPeerId} from "../../util.js"; -// TODO: Unit test - export async function* onBeaconBlocksByRange( request: phase0.BeaconBlocksByRangeRequest, chain: IBeaconChain, - db: IBeaconDb, peerId: PeerId, peerClient: string ): AsyncIterable { const {startSlot, count} = validateBeaconBlocksByRangeRequest(chain.config, request); const endSlot = startSlot + count; - const finalized = db.blockArchive; - // in the case of initializing from a non-finalized state, we don't have the finalized block so this api does not work - // chain.forkChoice.getFinalizeBlock().slot - const finalizedSlot = chain.forkChoice.getFinalizedCheckpointSlot(); - // Blocks are migrated to blockArchive at finalization (including the finalized block itself), - // so the archive loop serves up to AND INCLUDING finalizedSlot and the headChain loop - // starts above it to avoid duplicate yields. See archiveBlocks.ts for the migration logic. - const archiveMaxSlot = finalizedSlot; - // endSlot is exclusive, so highest served slot is endSlot - 1. // Throw only when the entire requested range is below earliestAvailableSlot. if (endSlot - 1 < chain.earliestAvailableSlot) { @@ -45,59 +32,41 @@ export async function* onBeaconBlocksByRange( ); } - // Finalized range of blocks + // Fork choice is read inside the engine; only thin refs cross (slot + raw root), no ProtoBlock. + // Blocks incl. the finalized block are migrated to the archive at finalization, so the archive loop + // serves up to AND INCLUDING finalizedSlot and the non-finalized refs start above it. + const {finalizedSlot, nonFinalized} = chain.getCanonicalBlockRootSlotsByRange(startSlot, endSlot); + const archiveMaxSlot = finalizedSlot; + + // Finalized range: point-read the cold archive per slot; skipped slots return null. if (startSlot <= archiveMaxSlot) { - // Chain of blobs won't change - for await (const {key, value} of finalized.binaryEntriesStream({ - gte: startSlot, - lt: Math.min(endSlot, archiveMaxSlot + 1), - })) { + const lt = Math.min(endSlot, archiveMaxSlot + 1); + for (let slot = startSlot; slot < lt; slot++) { + const blockBytes = await chain.getSerializedFinalizedBlockBySlot(slot); + if (!blockBytes) continue; yield { - data: value, - boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(finalized.decodeKey(key))), + data: blockBytes, + boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(slot)), }; } } - // Non-finalized range of blocks - if (endSlot > archiveMaxSlot) { - const headBlock = chain.forkChoice.getHead(); - const headRoot = headBlock.blockRoot; - // TODO DENEB: forkChoice should mantain an array of canonical blocks, and change only on reorg - const headChain = chain.forkChoice.getAllAncestorBlocks(headRoot, headBlock.payloadStatus); - // `getAllAncestorBlocks` includes both the head and the previous-finalized boundary. - - // Iterate head chain with ascending block numbers - for (let i = headChain.length - 1; i >= 0; i--) { - const block = headChain[i]; - - // Must include only blocks in the range requested, and skip anything the archive loop - // above already served via the block.slot > archiveMaxSlot filter. - if (block.slot > archiveMaxSlot && block.slot >= startSlot && block.slot < endSlot) { - // Note: Here the forkChoice head may change due to a re-org, so the headChain reflects the canonical chain - // at the time of the start of the request. Spec is clear the chain of blobs must be consistent, but on - // re-org there's no need to abort the request - // Spec: https://github.com/ethereum/consensus-specs/blob/a1e46d1ae47dd9d097725801575b46907c12a1f8/specs/eip4844/p2p-interface.md#blobssidecarsbyrange-v1 - - const blockBytes = await chain.getSerializedBlockByRoot(block.blockRoot); - if (!blockBytes) { - throw new ResponseError( - RespStatus.SERVER_ERROR, - `No block for root ${block.blockRoot} slot ${block.slot}, startSlot=${startSlot} endSlot=${endSlot} finalizedSlot=${finalizedSlot}` - ); - } - - yield { - data: blockBytes.block, - boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(block.slot)), - }; - } - - // If block is after endSlot, stop iterating - else if (block.slot >= endSlot) { - break; - } + // Non-finalized range: point-read each canonical block by root (hot→cold via the engine). + // Note: the fork-choice head may change due to a re-org, so `nonFinalized` reflects the canonical chain + // at the time the refs were taken. Spec requires the chain of blocks be consistent, but on re-org there's + // no need to abort the request. + for (const {slot, root} of nonFinalized) { + const res = await chain.getSerializedBlockByRoot(root); + if (!res) { + throw new ResponseError( + RespStatus.SERVER_ERROR, + `No block for slot ${slot}, startSlot=${startSlot} endSlot=${endSlot} finalizedSlot=${finalizedSlot}` + ); } + yield { + data: res.block, + boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(slot)), + }; } } diff --git a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts index 05c036c69f16..766ee61362c3 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/beaconBlocksByRoot.ts @@ -1,6 +1,5 @@ import {ResponseOutgoing} from "@lodestar/reqresp"; import {computeEpochAtSlot} from "@lodestar/state-transition"; -import {toRootHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../chain/index.js"; import {BeaconBlocksByRootRequest} from "../../../util/types.js"; @@ -12,8 +11,7 @@ export async function* onBeaconBlocksByRoot( // Archival nodes may still serve older retained blocks to allow genesis sync. for (const blockRoot of requestBody) { - const root = blockRoot; - const block = await chain.getSerializedBlockByRoot(toRootHex(root)); + const block = await chain.getSerializedBlockByRoot(blockRoot); if (block) { yield { diff --git a/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRange.ts index fcc20db2c57c..ba914d027591 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRange.ts @@ -5,13 +5,13 @@ import {computeEpochAtSlot} from "@lodestar/state-transition"; import {Epoch, Slot, deneb} from "@lodestar/types"; import {fromHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../chain/index.js"; -import {IBeaconDb} from "../../../db/index.js"; +import {IBeaconChainDb} from "../../../db/index.js"; import {BLOB_SIDECARS_IN_WRAPPER_INDEX} from "../../../db/repositories/blobSidecars.js"; export async function* onBlobSidecarsByRange( request: deneb.BlobSidecarsByRangeRequest, chain: IBeaconChain, - db: IBeaconDb + db: IBeaconChainDb ): AsyncIterable { // Non-finalized range of blobs const {startSlot, count} = validateBlobSidecarsByRangeRequest(chain.config, chain.clock.currentEpoch, request); diff --git a/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts index 8e4ea818f19e..8d8496314db6 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts @@ -7,7 +7,7 @@ import {computeEpochAtSlot} from "@lodestar/state-transition"; import {ColumnIndex, Epoch, fulu} from "@lodestar/types"; import {fromHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../chain/index.js"; -import {IBeaconDb} from "../../../db/index.js"; +import {IBeaconChainDb} from "../../../db/index.js"; import {prettyPrintPeerId} from "../../util.js"; import { handleColumnSidecarUnavailability, @@ -17,7 +17,7 @@ import { export async function* onDataColumnSidecarsByRange( request: fulu.DataColumnSidecarsByRangeRequest, chain: IBeaconChain, - db: IBeaconDb, + db: IBeaconChainDb, peerId: PeerId, peerClient: string ): AsyncIterable { @@ -87,7 +87,6 @@ export async function* onDataColumnSidecarsByRange( if (unavailableColumnIndices.length) { await handleColumnSidecarUnavailability({ chain, - db, metrics: chain.metrics, unavailableColumnIndices, slot, @@ -148,7 +147,6 @@ export async function* onDataColumnSidecarsByRange( if (unavailableColumnIndices.length) { await handleColumnSidecarUnavailability({ chain, - db, metrics: chain.metrics, unavailableColumnIndices, blockRoot: fromHex(block.blockRoot), diff --git a/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts index 82b14eaa89a0..5e4105932747 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts @@ -4,7 +4,6 @@ import {computeEpochAtSlot} from "@lodestar/state-transition"; import {ColumnIndex} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../chain/index.js"; -import {IBeaconDb} from "../../../db/index.js"; import {DataColumnSidecarsByRootRequest} from "../../../util/types.js"; import {prettyPrintPeerId} from "../../util.js"; import { @@ -15,7 +14,6 @@ import { export async function* onDataColumnSidecarsByRoot( requestBody: DataColumnSidecarsByRootRequest, chain: IBeaconChain, - db: IBeaconDb, peerId: PeerId, peerClient: string ): AsyncIterable { @@ -36,7 +34,7 @@ export async function* onDataColumnSidecarsByRoot( const blockRootHex = toRootHex(blockRoot); const block = chain.forkChoice.getBlockHexDefaultStatus(blockRootHex); // If the block is not in fork choice, it may be finalized. Attempt to find its slot in block archive - const slot = block ? block.slot : await db.blockArchive.getSlotByRoot(blockRoot); + const slot = block ? block.slot : await chain.getFinalizedBlockSlotByRoot(blockRoot); if (slot === null) { // We haven't seen the block @@ -83,7 +81,6 @@ export async function* onDataColumnSidecarsByRoot( if (unavailableColumnIndices.length) { await handleColumnSidecarUnavailability({ chain, - db, metrics: chain.metrics, slot, blockRoot, diff --git a/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts index 01d5575f981c..7b0221546712 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts @@ -3,14 +3,12 @@ import {ResponseOutgoing} from "@lodestar/reqresp"; import {computeEpochAtSlot} from "@lodestar/state-transition"; import {toRootHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../chain/index.js"; -import {IBeaconDb} from "../../../db/index.js"; import {ExecutionPayloadEnvelopesByRootRequest} from "../../../util/types.js"; import {prettyPrintPeerId} from "../../util.js"; export async function* onExecutionPayloadEnvelopesByRoot( requestBody: ExecutionPayloadEnvelopesByRootRequest, chain: IBeaconChain, - db: IBeaconDb, peerId: PeerId, peerClient: string ): AsyncIterable { @@ -21,7 +19,7 @@ export async function* onExecutionPayloadEnvelopesByRoot( const rootHex = toRootHex(root); const block = chain.forkChoice.getBlockHexDefaultStatus(rootHex); // If the block is not in fork choice, it may be finalized. Attempt to find its slot in block archive - const slot = block ? block.slot : await db.blockArchive.getSlotByRoot(root); + const slot = block ? block.slot : await chain.getFinalizedBlockSlotByRoot(root); if (slot === null) { chain.logger.debug( diff --git a/packages/beacon-node/src/network/reqresp/handlers/index.ts b/packages/beacon-node/src/network/reqresp/handlers/index.ts index 0ff2f8698eb6..ef9305b74705 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/index.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/index.ts @@ -1,7 +1,7 @@ import {ProtocolHandler} from "@lodestar/reqresp"; import {ssz} from "@lodestar/types"; import {IBeaconChain} from "../../../chain/index.js"; -import {IBeaconDb} from "../../../db/index.js"; +import {IBeaconChainDb} from "../../../db/index.js"; import { BeaconBlocksByRootRequestType, BlobSidecarsByRootRequestType, @@ -33,7 +33,7 @@ function notImplemented(method: ReqRespMethod): ProtocolHandler { * The ReqRespHandler module handles app-level requests / responses from other peers, * fetching state from the chain and database as needed. */ -export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconChain}): GetReqRespHandlerFn { +export function getReqRespHandlers({db, chain}: {db: IBeaconChainDb; chain: IBeaconChain}): GetReqRespHandlerFn { const handlers: Record = { [ReqRespMethod.Status]: notImplemented(ReqRespMethod.Status), [ReqRespMethod.Goodbye]: notImplemented(ReqRespMethod.Goodbye), @@ -41,7 +41,7 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh [ReqRespMethod.Metadata]: notImplemented(ReqRespMethod.Metadata), [ReqRespMethod.BeaconBlocksByRange]: (req, peerId, peerClient) => { const body = ssz.phase0.BeaconBlocksByRangeRequest.deserialize(req.data); - return onBeaconBlocksByRange(body, chain, db, peerId, peerClient); + return onBeaconBlocksByRange(body, chain, peerId, peerClient); }, [ReqRespMethod.BeaconBlocksByRoot]: (req) => { const fork = chain.config.getForkName(chain.clock.currentSlot); @@ -67,12 +67,12 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh }, [ReqRespMethod.DataColumnSidecarsByRoot]: (req, peerId, peerClient) => { const body = DataColumnSidecarsByRootRequestType(chain.config).deserialize(req.data); - return onDataColumnSidecarsByRoot(body, chain, db, peerId, peerClient); + return onDataColumnSidecarsByRoot(body, chain, peerId, peerClient); }, [ReqRespMethod.ExecutionPayloadEnvelopesByRoot]: (req, peerId, peerClient) => { const body = ExecutionPayloadEnvelopesByRootRequestType(chain.config).deserialize(req.data); - return onExecutionPayloadEnvelopesByRoot(body, chain, db, peerId, peerClient); + return onExecutionPayloadEnvelopesByRoot(body, chain, peerId, peerClient); }, [ReqRespMethod.ExecutionPayloadEnvelopesByRange]: (req, peerId, peerClient) => { const body = ssz.gloas.ExecutionPayloadEnvelopesByRangeRequest.deserialize(req.data); diff --git a/packages/beacon-node/src/network/reqresp/utils/dataColumnResponseValidation.ts b/packages/beacon-node/src/network/reqresp/utils/dataColumnResponseValidation.ts index acb3f493937d..7281d79ecff9 100644 --- a/packages/beacon-node/src/network/reqresp/utils/dataColumnResponseValidation.ts +++ b/packages/beacon-node/src/network/reqresp/utils/dataColumnResponseValidation.ts @@ -3,13 +3,11 @@ import {ForkSeq} from "@lodestar/params"; import {ColumnIndex, Slot} from "@lodestar/types"; import {prettyBytes, prettyPrintIndices, toRootHex} from "@lodestar/utils"; import {IBeaconChain} from "../../../chain/interface.js"; -import {IBeaconDb} from "../../../db/interface.js"; import {Metrics} from "../../../metrics/metrics.js"; import {getBlobKzgCommitmentsCountFromSignedBeaconBlockSerialized} from "../../../util/sszBytes.js"; export async function handleColumnSidecarUnavailability({ chain, - db, metrics, unavailableColumnIndices, requestedColumns, @@ -18,7 +16,6 @@ export async function handleColumnSidecarUnavailability({ blockRoot, }: { chain: IBeaconChain; - db: IBeaconDb; metrics: Metrics | null; slot: Slot; blockRoot?: Uint8Array; @@ -46,7 +43,10 @@ export async function handleColumnSidecarUnavailability({ if (!envelopeBytes) return; } - const blockBytes = blockRoot ? await db.block.getBinary(blockRoot) : await db.blockArchive.getBinary(slot); + // Block DB is engine-owned; read hot-by-root or finalized-by-slot via the engine. + const blockBytes = blockRoot + ? ((await chain.getSerializedBlockByRoot(blockRoot))?.block ?? null) + : await chain.getSerializedFinalizedBlockBySlot(slot); if (!blockBytes) { chain.logger.verbose( `Expected ${blockRoot ? "unfinalized" : "finalized"} block not found while handling unavailable dataColumnSidecar`, diff --git a/packages/beacon-node/src/sync/backfill/backfill.ts b/packages/beacon-node/src/sync/backfill/backfill.ts index 2e2f4eb0df65..ffdb662c8c40 100644 --- a/packages/beacon-node/src/sync/backfill/backfill.ts +++ b/packages/beacon-node/src/sync/backfill/backfill.ts @@ -7,7 +7,7 @@ import {Root, SignedBeaconBlock, Slot, phase0, ssz} from "@lodestar/types"; import {ErrorAborted, Logger, byteArrayEquals, sleep, toRootHex} from "@lodestar/utils"; import {IBeaconChain} from "../../chain/index.js"; import {GENESIS_SLOT, ZERO_HASH} from "../../constants/index.js"; -import {IBeaconDb} from "../../db/index.js"; +import {IBeaconChainDb} from "../../db/index.js"; import {Metrics} from "../../metrics/metrics.js"; import {INetwork, NetworkEvent, NetworkEventData, PeerAction} from "../../network/index.js"; import {ItTrigger} from "../../util/itTrigger.js"; @@ -24,7 +24,7 @@ const DB_READ_BREATHER_TIMEOUT = 1000; export type BackfillSyncModules = { chain: IBeaconChain; - db: IBeaconDb; + db: IBeaconChainDb; network: INetwork; config: BeaconConfig; logger: Logger; @@ -105,7 +105,7 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} private readonly chain: IBeaconChain; private readonly network: INetwork; - private readonly db: IBeaconDb; + private readonly db: IBeaconChainDb; private readonly config: BeaconConfig; private readonly logger: Logger; private readonly metrics: Metrics | null; @@ -229,7 +229,7 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} opts: BackfillSyncOpts, modules: BackfillSyncModules ): Promise { - const {config, anchorState, db, wsCheckpoint, logger} = modules; + const {config, anchorState, chain, db, wsCheckpoint, logger} = modules; const {checkpoint: anchorCp} = anchorState.computeAnchorCheckpoint(); const anchorSlot = anchorState.latestBlockHeader.slot; @@ -259,7 +259,7 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} ? {root: wsCheckpoint.root, slot: wsCheckpoint.epoch * SLOTS_PER_EPOCH} : null; // Load a previous finalized or wsCheckpoint slot from DB below anchorSlot - const prevFinalizedCheckpointBlock = await extractPreviousFinOrWsCheckpoint(config, db, anchorSlot, logger); + const prevFinalizedCheckpointBlock = await extractPreviousFinOrWsCheckpoint(config, chain, anchorSlot, logger); return new BackfillSync(opts, { syncAnchor, @@ -360,14 +360,14 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} // a validated connected segment having the slots of previous wsCheckpoint // or finalized as keys if (this.syncAnchor.lastBackSyncedBlock.slot !== GENESIS_SLOT) { - await this.db.blockArchive.put( + await this.putArchiveBlock( this.syncAnchor.lastBackSyncedBlock.slot, this.syncAnchor.lastBackSyncedBlock.block ); } this.prevFinalizedCheckpointBlock = await extractPreviousFinOrWsCheckpoint( this.config, - this.db, + this.chain, this.syncAnchor.lastBackSyncedBlock.slot, this.logger ); @@ -381,7 +381,7 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} )}` ); } - await this.db.blockArchive.put(GENESIS_SLOT, this.syncAnchor.lastBackSyncedBlock.block); + await this.putArchiveBlock(GENESIS_SLOT, this.syncAnchor.lastBackSyncedBlock.block); } if (this.wsCheckpointHeader && !this.wsValidated) { @@ -512,7 +512,8 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} // Checkpoint root should be in db now , in case there are string of orphaned/missed // slots before/leading up to checkpoint, the block just backsynced before the // wsCheckpointHeader.slot will have the checkpoint root - const wsDbCheckpointBlock = await this.db.blockArchive.getByRoot(this.wsCheckpointHeader.root); + const wsDbCheckpointBlock = + (await this.chain.getBlockByRoot(toRootHex(this.wsCheckpointHeader.root)))?.block ?? null; if ( !wsDbCheckpointBlock || // The only validation we can do here is that wsDbCheckpointBlock is found at/before @@ -560,7 +561,7 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} if (jumpBackTo < lastBackSyncedSlot) { validSequence = true; - const anchorBlock = await this.db.blockArchive.get(jumpBackTo); + const anchorBlock = (await this.chain.getCanonicalBlockAtSlot(jumpBackTo))?.block ?? null; if (!anchorBlock) { validSequence = false; this.logger.warn( @@ -575,7 +576,8 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} // Everything saved in db between a backfilled range is a connected sequence // we only need to check if prevFinalizedCheckpointBlock is in db - const prevBackfillCpBlock = await this.db.blockArchive.getByRoot(this.prevFinalizedCheckpointBlock.root); + const prevBackfillCpBlock = + (await this.chain.getBlockByRoot(toRootHex(this.prevFinalizedCheckpointBlock.root)))?.block ?? null; if ( prevBackfillCpBlock != null && this.prevFinalizedCheckpointBlock.slot === prevBackfillCpBlock.message.slot @@ -620,7 +622,7 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} // finalized or wsCheckpoint behind the new lastBackSyncedBlock this.prevFinalizedCheckpointBlock = await extractPreviousFinOrWsCheckpoint( this.config, - this.db, + this.chain, jumpBackTo, this.logger ); @@ -661,7 +663,7 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} anchorBlockRoot = this.syncAnchor.anchorBlockRoot; expectedSlot = this.syncAnchor.anchorSlot; } - let anchorBlock = await this.db.blockArchive.getByRoot(anchorBlockRoot); + let anchorBlock = (await this.chain.getBlockByRoot(toRootHex(anchorBlockRoot)))?.block ?? null; if (!anchorBlock) return false; if (expectedSlot !== null && anchorBlock.message.slot !== expectedSlot) @@ -679,7 +681,7 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} while ( backCount !== this.opts.backfillBatchSize && // biome-ignore lint/suspicious/noAssignInExpressions: May be refactored later - (parentBlock = await this.db.blockArchive.getByRoot(anchorBlock.message.parentRoot)) + (parentBlock = (await this.chain.getBlockByRoot(toRootHex(anchorBlock.message.parentRoot)))?.block ?? null) ) { // Before moving anchorBlock back, we need check for prevFinalizedCheckpointBlock if (anchorBlock.message.slot < this.prevFinalizedCheckpointBlock.slot) { @@ -712,7 +714,7 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} // Extract new prevFinalizedCheckpointBlock below anchorBlock this.prevFinalizedCheckpointBlock = await extractPreviousFinOrWsCheckpoint( this.config, - this.db, + this.chain, anchorBlock.message.slot, this.logger ); @@ -742,6 +744,16 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} return true; } + /** Persist a finalized block to the engine-owned cold archive (bytes-first, indexed by root/parent). */ + private async putArchiveBlock(slot: Slot, block: SignedBeaconBlock): Promise { + await this.chain.beaconEngine.persistArchiveBlock( + slot, + this.chain.serializedCache.get(block) ?? this.config.getForkTypes(slot).SignedBeaconBlock.serialize(block), + this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block.message), + block.message.parentRoot + ); + } + private async syncBlockByRoot(peer: PeerIdStr, anchorBlockRoot: Root): Promise { const res = await this.network.sendBeaconBlocksByRoot(peer, [anchorBlockRoot]); if (res.length < 1) throw new Error("InvalidBlockSyncedFromPeer"); @@ -755,12 +767,7 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} // we will need to go make checks on the top of sync loop before writing as it might // override prevFinalizedCheckpointBlock if (this.prevFinalizedCheckpointBlock.slot < anchorBlock.message.slot) { - const serialized = this.chain.serializedCache.get(anchorBlock); - if (serialized) { - await this.db.blockArchive.putBinary(anchorBlock.message.slot, serialized); - } else { - await this.db.blockArchive.put(anchorBlock.message.slot, anchorBlock); - } + await this.putArchiveBlock(anchorBlock.message.slot, anchorBlock); } this.syncAnchor = { @@ -831,30 +838,19 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} ? verifiedBlocks : verifiedBlocks.slice(0, verifiedBlocks.length - 1); - const binaryPuts = []; - const nonBinaryPuts = []; - - for (const block of blocksToPut) { - const serialized = this.chain.serializedCache.get(block); - const item = { - key: block.message.slot, - slot: block.message.slot, - blockRoot: this.config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message), + // Block archive is engine-owned; persist bytes-first (serialize on a serializedCache miss). + const entries = blocksToPut.map((block) => { + const slot = block.message.slot; + return { + slot, + bytes: + this.chain.serializedCache.get(block) ?? this.config.getForkTypes(slot).SignedBeaconBlock.serialize(block), + blockRoot: this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block.message), parentRoot: block.message.parentRoot, }; - - if (serialized) { - binaryPuts.push({...item, value: serialized}); - } else { - nonBinaryPuts.push({...item, value: block}); - } - } - - if (binaryPuts.length > 0) { - await this.db.blockArchive.batchPutBinary(binaryPuts); - } - if (nonBinaryPuts.length > 0) { - await this.db.blockArchive.batchPut(nonBinaryPuts); + }); + if (entries.length > 0) { + await this.chain.beaconEngine.batchPersistArchiveBlocks(entries); } this.metrics?.backfillSync.totalBlocks.inc({method: BackfillSyncMethod.rangesync}, verifiedBlocks.length); @@ -878,25 +874,22 @@ export class BackfillSync extends (EventEmitter as {new (): BackfillSyncEmitter} async function extractPreviousFinOrWsCheckpoint( config: ChainForkConfig, - db: IBeaconDb, + chain: IBeaconChain, belowSlot: Slot, logger?: Logger ): Promise { // Anything below genesis block is just zero hash if (belowSlot <= GENESIS_SLOT) return {root: ZERO_HASH, slot: belowSlot - 1}; - // To extract the next prevFinalizedCheckpointBlock, we just need to look back in DB - // Any saved previous finalized or ws checkpoint, will also have a corresponding block + // To extract the next prevFinalizedCheckpointBlock, we just need to look back in the engine-owned + // block archive. Any saved previous finalized or ws checkpoint will also have a corresponding block // saved in DB, as we make sure of that // 1. When we archive new finalized state and blocks // 2. When we backfill from a wsCheckpoint - const nextPrevFinOrWsBlock = ( - await db.blockArchive.values({ - lt: belowSlot, - reverse: true, - limit: 1, - }) - )[0]; + const before = await chain.beaconEngine.getSerializedArchiveBlockBefore(belowSlot); + const nextPrevFinOrWsBlock = before + ? config.getForkTypes(before.slot).SignedBeaconBlock.deserialize(before.bytes) + : undefined; let prevFinalizedCheckpointBlock: BackfillBlockHeader; if (nextPrevFinOrWsBlock != null) { diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index 68f5c5635131..4f529252b5d5 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -168,6 +168,11 @@ vi.mock("../../src/chain/chain.js", async (importActual) => { produceBlindedBlock: vi.fn(), getCanonicalBlockAtSlot: vi.fn(), getBlockByRoot: vi.fn(), + getSerializedBlockByRoot: vi.fn(), + getSerializedFinalizedBlockBySlot: vi.fn(), + getCanonicalBlockRootSlotsByRange: vi.fn(), + getFinalizedBlockSlotByRoot: vi.fn(), + getSerializedFinalizedBlockByParentRoot: vi.fn(), recomputeForkChoiceHead: vi.fn(), predictProposerHead: vi.fn(), getHeadStateAtCurrentEpoch: vi.fn(), diff --git a/packages/beacon-node/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts b/packages/beacon-node/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts index d7fe25bb7a4e..cf1aabeb6e95 100644 --- a/packages/beacon-node/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts +++ b/packages/beacon-node/test/unit/api/impl/beacon/blocks/getBlockHeaders.test.ts @@ -12,11 +12,12 @@ describe("api - beacon - getBlockHeaders", () => { let api: ReturnType; const parentRoot = toHexString(Buffer.alloc(32, 1)); + // Serialized default block the engine passthrough returns (api deserializes it). + const finalizedBlockBytes = ssz.phase0.SignedBeaconBlock.serialize(ssz.phase0.SignedBeaconBlock.defaultValue()); + beforeEach(() => { modules = getApiTestModules(); api = getBeaconBlockApi(modules); - - vi.spyOn(modules.db.blockArchive, "getByParentRoot"); }); afterEach(() => { @@ -83,7 +84,7 @@ describe("api - beacon - getBlockHeaders", () => { }); it.skip("parent root filter - both finalized and non finalized results", async () => { - modules.db.blockArchive.getByParentRoot.mockResolvedValue(ssz.phase0.SignedBeaconBlock.defaultValue()); + modules.chain.getSerializedFinalizedBlockByParentRoot.mockResolvedValue(finalizedBlockBytes); modules.forkChoice.getBlockSummariesByParentRoot.mockReturnValue([ generateProtoBlock({slot: 2}), generateProtoBlock({slot: 1}), @@ -111,7 +112,7 @@ describe("api - beacon - getBlockHeaders", () => { }); it("parent root - no finalized block", async () => { - modules.db.blockArchive.getByParentRoot.mockResolvedValue(null); + modules.chain.getSerializedFinalizedBlockByParentRoot.mockResolvedValue(null); modules.forkChoice.getBlockSummariesByParentRoot.mockReturnValue([generateProtoBlock({slot: 1})]); when(modules.forkChoice.getCanonicalBlockAtSlot).calledWith(1).thenReturn(generateProtoBlock()); modules.chain.getBlockByRoot.mockResolvedValue({ @@ -125,14 +126,14 @@ describe("api - beacon - getBlockHeaders", () => { }); it("parent root - no non finalized blocks", async () => { - modules.db.blockArchive.getByParentRoot.mockResolvedValue(ssz.phase0.SignedBeaconBlock.defaultValue()); + modules.chain.getSerializedFinalizedBlockByParentRoot.mockResolvedValue(finalizedBlockBytes); modules.forkChoice.getBlockSummariesByParentRoot.mockReturnValue([]); const {data: blockHeaders} = await api.getBlockHeaders({parentRoot}); expect(blockHeaders.length).toBe(1); }); it("parent root + slot filter", async () => { - modules.db.blockArchive.getByParentRoot.mockResolvedValue(ssz.phase0.SignedBeaconBlock.defaultValue()); + modules.chain.getSerializedFinalizedBlockByParentRoot.mockResolvedValue(finalizedBlockBytes); modules.forkChoice.getBlockSummariesByParentRoot.mockReturnValue([ generateProtoBlock({slot: 2}), generateProtoBlock({slot: 1}), diff --git a/packages/beacon-node/test/unit/network/reqresp/handlers/beaconBlocksByHead.test.ts b/packages/beacon-node/test/unit/network/reqresp/handlers/beaconBlocksByHead.test.ts index 7498290f42c9..bc3360c10226 100644 --- a/packages/beacon-node/test/unit/network/reqresp/handlers/beaconBlocksByHead.test.ts +++ b/packages/beacon-node/test/unit/network/reqresp/handlers/beaconBlocksByHead.test.ts @@ -189,8 +189,8 @@ function createChain(blocks: TestBlock[], currentEpoch = 0, testConfig = config) return { config: testConfig, clock: {currentEpoch, currentSlot: computeStartSlotAtEpoch(currentEpoch)}, - getSerializedBlockByRoot: vi.fn(async (rootHex: string) => { - const block = blocksByRoot.get(rootHex); + getSerializedBlockByRoot: vi.fn(async (root: Uint8Array) => { + const block = blocksByRoot.get(toRootHex(root)); return block ? {block: block.bytes, executionOptimistic: false, finalized: false, slot: block.slot} : null; }), logger: {verbose: vi.fn()}, From f1618783c90a1b71d0c9b4cff8c60d9fb5f5bd14 Mon Sep 17 00:00:00 2001 From: twoeths Date: Tue, 7 Jul 2026 10:44:55 +0700 Subject: [PATCH 16/24] feat: remove BeaconEngine caches from BeaconChain --- .../src/api/impl/beacon/blocks/index.ts | 2 +- .../src/api/impl/beacon/pool/index.ts | 41 +- .../src/api/impl/validator/index.ts | 8 +- .../src/chain/beaconEngine/beaconEngine.ts | 360 +++++++++++++++++- .../src/chain/beaconEngine/interface.ts | 97 ++++- .../src/chain/blocks/importBlock.ts | 9 +- packages/beacon-node/src/chain/chain.ts | 203 ++-------- packages/beacon-node/src/chain/interface.ts | 56 +-- .../chain/produceBlock/produceBlockBody.ts | 16 +- .../src/chain/validation/attesterSlashing.ts | 24 +- .../chain/validation/blsToExecutionChange.ts | 20 +- .../src/chain/validation/proposerSlashing.ts | 22 +- .../src/chain/validation/voluntaryExit.ts | 20 +- .../src/network/processor/gossipHandlers.ts | 65 +--- packages/beacon-node/src/sync/unknownBlock.ts | 2 +- .../test/e2e/api/lodestar/lodestar.test.ts | 19 +- .../test/mocks/mockedBeaconChain.ts | 48 +++ .../produceBlock/produceBlockBody.test.ts | 2 + .../test/spec/utils/gossipValidation.ts | 26 +- .../api/impl/validator/produceBlockV3.test.ts | 7 +- .../chain/validation/attesterSlashing.test.ts | 6 +- .../test/unit/chain/validation/block.test.ts | 2 +- .../validation/blsToExecutionChange.test.ts | 12 +- .../chain/validation/proposerSlashing.test.ts | 6 +- .../chain/validation/syncCommittee.test.ts | 7 +- .../chain/validation/voluntaryExit.test.ts | 14 +- .../test/unit/sync/unknownBlock.test.ts | 16 +- .../utils/validationData/aggregateAndProof.ts | 9 +- .../test/utils/validationData/attestation.ts | 18 +- 29 files changed, 681 insertions(+), 456 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 0ff1d3925bc1..9b7f2db50267 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -868,7 +868,7 @@ export function getBeaconBlockApi({ } try { - const insertOutcome = chain.executionPayloadBidPool.add(signedExecutionPayloadBid); + const insertOutcome = chain.beaconEngine.addExecutionPayloadBid(signedExecutionPayloadBid); metrics?.opPool.executionPayloadBidPool.apiInsertOutcome.inc({insertOutcome}); } catch (e) { chain.logger.error("Error adding to executionPayloadBid pool", {}, e as Error); diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index eeb1e22ba46b..9ff310c34b35 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -17,11 +17,7 @@ import { PayloadAttestationErrorCode, ProposerPreferencesErrorCode, } from "../../../../chain/errors/index.js"; -import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js"; -import {validateApiBlsToExecutionChange} from "../../../../chain/validation/blsToExecutionChange.js"; import {toElectraSingleAttestation} from "../../../../chain/validation/index.js"; -import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js"; -import {validateApiVoluntaryExit} from "../../../../chain/validation/voluntaryExit.js"; import {validateGossipFnRetryUnknownRoot} from "../../../../network/processor/gossipHandlers.js"; import {getFixedListElementBytes} from "../../../../util/sszBytes.js"; import {ApiError, FailureList, IndexedError} from "../../errors.js"; @@ -36,7 +32,7 @@ export function getBeaconPoolApi({ return { async getPoolAttestationsV2({slot, committeeIndex}) { // Already filtered by slot - let attestations = chain.aggregatedAttestationPool.getAll(slot); + let attestations = chain.beaconEngine.getPoolAggregatedAttestations(slot); const fork = chain.config.getForkName(slot ?? attestations[0]?.data.slot ?? chain.clock.currentSlot); const isPostElectra = isForkPostElectra(fork); @@ -57,7 +53,7 @@ export function getBeaconPoolApi({ throw new ApiError(400, `Payload attestation pool is not supported before Gloas fork=${fork}`); } - return {data: chain.payloadAttestationPool.getAll(slot), meta: {version: fork}}; + return {data: chain.beaconEngine.getPoolPayloadAttestations(slot), meta: {version: fork}}; }, async getPoolProposerPreferences({slot}) { @@ -66,7 +62,7 @@ export function getBeaconPoolApi({ throw new ApiError(400, `Proposer preferences pool is not supported before Gloas fork=${fork}`); } - return {data: chain.proposerPreferencesPool.getAll(slot), meta: {version: fork}}; + return {data: chain.beaconEngine.getPoolProposerPreferences(slot), meta: {version: fork}}; }, async submitSignedProposerPreferences({signedProposerPreferences}, context) { @@ -100,7 +96,7 @@ export function getBeaconPoolApi({ return; } - chain.proposerPreferencesPool.add(signed); + // Engine inserted into its (internal) proposerPreferencesPool on Accept above. await network.publishProposerPreferences(signed); chain.emitter.emit(routes.events.EventType.proposerPreferences, { version: ForkName.gloas, @@ -120,19 +116,19 @@ export function getBeaconPoolApi({ async getPoolAttesterSlashingsV2() { const fork = chain.config.getForkName(chain.clock.currentSlot); - return {data: chain.opPool.getAllAttesterSlashings(), meta: {version: fork}}; + return {data: chain.beaconEngine.getPoolAttesterSlashings(), meta: {version: fork}}; }, async getPoolProposerSlashings() { - return {data: chain.opPool.getAllProposerSlashings()}; + return {data: chain.beaconEngine.getPoolProposerSlashings()}; }, async getPoolVoluntaryExits() { - return {data: chain.opPool.getAllVoluntaryExits()}; + return {data: chain.beaconEngine.getPoolVoluntaryExits()}; }, async getPoolBLSToExecutionChanges() { - return {data: chain.opPool.getAllBlsToExecutionChanges().map(({data}) => data)}; + return {data: chain.beaconEngine.getPoolBlsToExecutionChanges()}; }, async submitPoolAttestationsV2({signedAttestations}) { @@ -173,7 +169,7 @@ export function getBeaconPoolApi({ res.value; if (network.shouldAggregate(subnet, slot)) { - const insertOutcome = chain.attestationPool.add( + const insertOutcome = chain.beaconEngine.addAttestationToPool( committeeIndex, attestation, attDataRootHex, @@ -221,21 +217,19 @@ export function getBeaconPoolApi({ }, async submitPoolAttesterSlashingsV2({attesterSlashing}) { - await validateApiAttesterSlashing(chain, attesterSlashing); const fork = chain.config.getForkName(Number(attesterSlashing.attestation1.data.slot)); - chain.opPool.insertAttesterSlashing(fork, attesterSlashing); + // Engine validates + inserts into its (internal) opPool; facade only publishes. + await chain.beaconEngine.validateApiAttesterSlashing(attesterSlashing, fork); await network.publishAttesterSlashing(attesterSlashing); }, async submitPoolProposerSlashings({proposerSlashing}) { - await validateApiProposerSlashing(chain, proposerSlashing); - chain.opPool.insertProposerSlashing(proposerSlashing); + await chain.beaconEngine.validateApiProposerSlashing(proposerSlashing); await network.publishProposerSlashing(proposerSlashing); }, async submitPoolVoluntaryExit({signedVoluntaryExit}) { - await validateApiVoluntaryExit(chain, signedVoluntaryExit); - chain.opPool.insertVoluntaryExit(signedVoluntaryExit); + await chain.beaconEngine.validateApiVoluntaryExit(signedVoluntaryExit); chain.emitter.emit(routes.events.EventType.voluntaryExit, signedVoluntaryExit); await network.publishVoluntaryExit(signedVoluntaryExit); }, @@ -246,10 +240,9 @@ export function getBeaconPoolApi({ await Promise.all( blsToExecutionChanges.map(async (blsToExecutionChange, i) => { try { - // Ignore even if the change exists and reprocess - await validateApiBlsToExecutionChange(chain, blsToExecutionChange); const preCapella = chain.clock.currentEpoch < chain.config.CAPELLA_FORK_EPOCH; - chain.opPool.insertBlsToExecutionChange(blsToExecutionChange, preCapella); + // Ignore even if the change exists and reprocess; engine validates + inserts into its opPool. + await chain.beaconEngine.validateApiBlsToExecutionChange(blsToExecutionChange, preCapella); chain.emitter.emit(routes.events.EventType.blsToExecutionChange, blsToExecutionChange); @@ -311,7 +304,7 @@ export function getBeaconPoolApi({ } const {attDataRootHex, validatorCommitteeIndices} = res.value; - const insertOutcome = chain.payloadAttestationPool.add( + const insertOutcome = chain.beaconEngine.addPayloadAttestation( payloadAttestationMessage, attDataRootHex, validatorCommitteeIndices @@ -407,7 +400,7 @@ export function getBeaconPoolApi({ // Sync committee subnet members are just sequential in the order they appear in SyncCommitteeIndexes array const subnet = Math.floor(indexInCommittee / SYNC_COMMITTEE_SUBNET_SIZE); const indexInSubcommittee = indexInCommittee % SYNC_COMMITTEE_SUBNET_SIZE; - chain.syncCommitteeMessagePool.add(subnet, signature, indexInSubcommittee, priority); + chain.beaconEngine.addSyncCommitteeMessage(subnet, signature, indexInSubcommittee, priority); // Cheap de-duplication code to avoid using a Set. indexesInCommittee is always sorted if (subnets.length === 0 || subnets.at(-1) !== subnet) { diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 34d80640a924..45f4e7a7d95d 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1072,7 +1072,7 @@ export function getValidatorApi( notOnOptimisticBlockRoot(beaconBlockRoot); notOnOutOfRangeData(beaconBlockRoot); - const contribution = chain.syncCommitteeMessagePool.getContribution(subcommitteeIndex, slot, beaconBlockRoot); + const contribution = chain.beaconEngine.getSyncCommitteeContribution(subcommitteeIndex, slot, beaconBlockRoot); if (!contribution) { throw new ApiError( 404, @@ -1244,7 +1244,7 @@ export function getValidatorApi( await waitForSlot(slot); // Must never request for a future slot > currentSlot const dataRootHex = toRootHex(attestationDataRoot); - const aggregate = chain.attestationPool.getAggregate(slot, dataRootHex, committeeIndex); + const aggregate = chain.beaconEngine.getAttestationAggregate(slot, dataRootHex, committeeIndex); if (!aggregate) { throw new ApiError( @@ -1303,7 +1303,7 @@ export function getValidatorApi( } const {indexedAttestation, committeeValidatorIndices, attDataRootHex} = res.value; - const insertOutcome = chain.aggregatedAttestationPool.add( + const insertOutcome = chain.beaconEngine.addAggregatedAttestation( signedAggregateAndProof.message.aggregate, attDataRootHex, indexedAttestation.attestingIndices.length, @@ -1369,7 +1369,7 @@ export function getValidatorApi( return; } const {syncCommitteeParticipantIndices} = res.value; - const insertOutcome = chain.syncContributionAndProofPool.add( + const insertOutcome = chain.beaconEngine.addSyncContributionAndProof( contributionAndProof.message, syncCommitteeParticipantIndices.length, true diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index c4b8e9f75c5c..09f9a88ded31 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -51,10 +51,12 @@ import { } from "@lodestar/state-transition"; import { Attestation, + AttesterSlashing, BLSSignature, BeaconBlock, BlindedBeaconBlock, Bytes32, + CommitteeIndex, Epoch, Gwei, IndexedAttestation, @@ -63,10 +65,13 @@ import { SSEPayloadAttributes, SignedAggregateAndProof, SignedBeaconBlock, + SingleAttestation, Slot, + SubcommitteeIndex, SubnetID, ValidatorIndex, altair, + capella, deneb, electra, fulu, @@ -79,7 +84,8 @@ import {Logger, byteArrayEquals, fromHex, sleep, toRootHex} from "@lodestar/util import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js"; import {IBeaconEngineDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; -import {IClock} from "../../util/clock.js"; +import {BufferPool} from "../../util/bufferPool.js"; +import {ClockEvent, IClock} from "../../util/clock.js"; import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; import {callInNextEventLoop} from "../../util/eventLoop.js"; import {isOptimisticBlock} from "../../util/forkChoice.js"; @@ -92,7 +98,7 @@ import { } from "../archiveStore/utils/archiveBlocks.js"; import {pruneHistory} from "../archiveStore/utils/pruneHistory.js"; import {CheckpointBalancesCache} from "../balancesCache.js"; -import {BeaconProposerCache} from "../beaconProposerCache.js"; +import {BeaconProposerCache, ProposerPreparationData} from "../beaconProposerCache.js"; import {isBlockInputBlobs, isBlockInputColumns} from "../blocks/blockInput/blockInput.js"; import {IBlockInput} from "../blocks/blockInput/index.js"; import {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; @@ -116,6 +122,7 @@ import { SyncCommitteeMessagePool, SyncContributionAndProofPool, } from "../opPools/index.js"; +import {InsertOutcome} from "../opPools/types.js"; import {BlockProcessOpts} from "../options.js"; import { BlockAttributes, @@ -139,6 +146,7 @@ import { } from "../seenCache/index.js"; import {SeenAggregatedAttestations} from "../seenCache/seenAggregateAndProof.js"; import {SeenAttestationDatas} from "../seenCache/seenAttestationData.js"; +import {SeenBlockAttesters} from "../seenCache/seenBlockAttesters.js"; import {ShufflingCache} from "../shufflingCache.js"; import {FIFOBlockStateCache} from "../stateCache/fifoBlockStateCache.js"; import {PersistentCheckpointStateCache, toCheckpointHex} from "../stateCache/persistentCheckpointsCache.js"; @@ -155,8 +163,13 @@ import { validateApiAttestation, validateGossipAttestationsSameAttData, } from "../validation/attestation.js"; +import {validateApiAttesterSlashing, validateGossipAttesterSlashing} from "../validation/attesterSlashing.js"; import {validateGossipBlobSidecar} from "../validation/blobSidecar.js"; import {GossipBlockValidationResult, validateGossipBlock} from "../validation/block.js"; +import { + validateApiBlsToExecutionChange, + validateGossipBlsToExecutionChange, +} from "../validation/blsToExecutionChange.js"; import { validateGossipFuluDataColumnSidecar, validateGossipGloasDataColumnSidecar, @@ -172,12 +185,19 @@ import { validateGossipPayloadAttestationMessage, } from "../validation/payloadAttestationMessage.js"; import {validateGossipProposerPreferences} from "../validation/proposerPreferences.js"; +import {validateApiProposerSlashing, validateGossipProposerSlashing} from "../validation/proposerSlashing.js"; import {validateApiSyncCommittee, validateGossipSyncCommittee} from "../validation/syncCommittee.js"; import {validateSyncCommitteeGossipContributionAndProof} from "../validation/syncCommitteeContributionAndProof.js"; +import {validateApiVoluntaryExit, validateGossipVoluntaryExit} from "../validation/voluntaryExit.js"; import {ValidatorMonitor} from "../validatorMonitor.js"; import {computeNewStateRoot} from "./computeNewStateRoot.js"; import {getPubkeysForIndices} from "./duties.js"; -import {GossipValidationResult, fromResult, runGossipValidation} from "./gossipValidationResult.js"; +import { + GossipValidationResult, + GossipValidationStatus, + fromResult, + runGossipValidation, +} from "./gossipValidationResult.js"; import { BeaconEngineModules, FinalizedProtoSummary, @@ -214,6 +234,8 @@ export class BeaconEngine implements IBeaconEngine { readonly metrics: Metrics | null; readonly clock: IClock; readonly pubkeyCache: PubkeyCache; + // NodeJS-specific memory pool for state serialization; engine-internal (not on IBeaconEngine). + readonly bufferPool: BufferPool; readonly bls: IBlsVerifier; readonly shufflingCache: ShufflingCache; @@ -243,6 +265,8 @@ export class BeaconEngine implements IBeaconEngine { readonly seenContributionAndProof: SeenContributionAndProof; readonly seenAttestationDatas: SeenAttestationDatas; readonly seenBlockProposers = new SeenBlockProposers(); + // Liveness cache: attesters seen through imported blocks (populated in _importBlock). + readonly seenBlockAttesters = new SeenBlockAttesters(); readonly seenAggregatedAttestations: SeenAggregatedAttestations; readonly seenExecutionPayloadBids = new SeenExecutionPayloadBids(); readonly seenProposerPreferences = new SeenProposerPreferences(); @@ -270,7 +294,6 @@ export class BeaconEngine implements IBeaconEngine { metrics, clock, pubkeyCache, - bufferPool, cpStateDatastore, emitter, signal, @@ -286,6 +309,8 @@ export class BeaconEngine implements IBeaconEngine { this.metrics = metrics; this.clock = clock; this.pubkeyCache = pubkeyCache; + // Engine owns the buffer pool; sized from the anchor state (NodeJS-specific, engine-internal). + this.bufferPool = new BufferPool(anchorState.serializedSize(), metrics); this.seenBlockInputCache = seenBlockInputCache; this.validatorMonitor = validatorMonitor; this.lightClientServer = modules.lightClientServer; @@ -325,7 +350,15 @@ export class BeaconEngine implements IBeaconEngine { this.blockStateCache = new FIFOBlockStateCache(opts, {metrics}); this.checkpointStateCache = new PersistentCheckpointStateCache( - {config, metrics, logger, clock, blockStateCache: this.blockStateCache, bufferPool, datastore: cpStateDatastore}, + { + config, + metrics, + logger, + clock, + blockStateCache: this.blockStateCache, + bufferPool: this.bufferPool, + datastore: cpStateDatastore, + }, opts ); @@ -373,10 +406,47 @@ export class BeaconEngine implements IBeaconEngine { // Engine owns the states DB, so it owns the state-archive strategy (finalized + temp + shutdown). if (opts.archiveMode === ArchiveMode.Frequency) { - this.stateArchiveStrategy = new FrequencyStateArchiveStrategy(this.regen, db, logger, opts, bufferPool); + this.stateArchiveStrategy = new FrequencyStateArchiveStrategy(this.regen, db, logger, opts, this.bufferPool); } else { throw new Error(`State archive strategy "${opts.archiveMode}" currently not supported.`); } + + // The engine prunes its OWN caches on the clock tick (the facade no longer reaches in). More + // engine-owned pools/caches move under this listener in later slices. + clock.addListener(ClockEvent.slot, (slot: Slot) => { + this.attestationPool.prune(slot); + this.aggregatedAttestationPool.prune(slot); + this.syncCommitteeMessagePool.prune(slot); + this.payloadAttestationPool.prune(slot); + this.executionPayloadBidPool.prune(slot); + this.seenProposerPreferences.prune(slot); + this.proposerPreferencesPool.prune(slot); + this.seenSyncCommitteeMessages.prune(slot); + this.seenExecutionPayloadBids.prune(slot); + this.seenAttestationDatas.onSlot(slot); + }); + clock.addListener(ClockEvent.epoch, (epoch: Epoch) => { + this.seenAttesters.prune(epoch); + this.seenAggregators.prune(epoch); + this.seenPayloadAttesters.prune(epoch); + this.seenAggregatedAttestations.prune(epoch); + this.seenBlockAttesters.prune(epoch); + this.beaconProposerCache.prune(epoch); + }); + + // The engine reports metrics for its own (internal) op pools. + if (metrics) { + metrics.clockSlot.addCollect(() => { + metrics.opPool.attesterSlashingPoolSize.set(this.opPool.attesterSlashingsSize); + metrics.opPool.proposerSlashingPoolSize.set(this.opPool.proposerSlashingsSize); + metrics.opPool.voluntaryExitPoolSize.set(this.opPool.voluntaryExitsSize); + metrics.opPool.blsToExecutionChangePoolSize.set(this.opPool.blsToExecutionChangeSize); + metrics.opPool.attestationPool.size.set(this.attestationPool.getAttestationCount()); + metrics.opPool.syncCommitteeMessagePoolSize.set(this.syncCommitteeMessagePool.size); + metrics.opPool.payloadAttestationPool.size.set(this.payloadAttestationPool.size); + metrics.opPool.executionPayloadBidPool.size.set(this.executionPayloadBidPool.size); + }); + } } getHeadState(): IBeaconStateView { @@ -389,6 +459,11 @@ export class BeaconEngine implements IBeaconEngine { return headState; } + /** Head state advanced to the current wall-clock epoch (mirrors the former `chain.getHeadStateAtCurrentEpoch`). */ + async getHeadStateAtCurrentEpoch(regenCaller: RegenCaller): Promise { + return this.getHeadStateAtEpoch(this.clock.currentEpoch, regenCaller); + } + /** * Regenerate state for attestation verification, this does not happen with default chain option of maxSkipSlots = 32 . * However, need to handle just in case. Lodestar doesn't support multiple regen state requests for attestation verification @@ -679,6 +754,13 @@ export class BeaconEngine implements IBeaconEngine { const proposerPubKey = this.pubkeyCache.getOrThrow(proposerIndex).toBytes(); const prevRandao = state.getRandaoMix(state.epoch); + // Resolve the proposer fee recipient once (engine-owned cache); the API-requested one, if any, + // overrides it downstream. `feeRecipientCached` distinguishes a registered value from the default + // for block-production logging — so produceBlockBody never reaches back into the engine. + const cachedFeeRecipient = this.beaconProposerCache.get(proposerIndex); + const defaultFeeRecipient = this.beaconProposerCache.getOrDefault(proposerIndex); + const feeRecipientCached = cachedFeeRecipient !== undefined; + let parentBlockHash: Bytes32; // parent execution gas limit: gloas keeps it on the bid (UintBn64 → cast), pre-gloas on the header let parentGasLimit: number; @@ -752,6 +834,8 @@ export class BeaconEngine implements IBeaconEngine { parentBlock, proposerIndex, proposerPubKey, + defaultFeeRecipient, + feeRecipientCached, safeBlockHash, finalizedBlockHash, timestamp, @@ -1685,11 +1769,266 @@ export class BeaconEngine implements IBeaconEngine { return runGossipValidation(() => validateApiExecutionPayloadBid(this, signedExecutionPayloadBid)); } - validateGossipProposerPreferences( + async validateGossipProposerPreferences( _preferencesBytes: Uint8Array, signedProposerPreferences: gloas.SignedProposerPreferences ): Promise> { - return runGossipValidation(() => validateGossipProposerPreferences(this, signedProposerPreferences)); + const res = await runGossipValidation(() => validateGossipProposerPreferences(this, signedProposerPreferences)); + // Insert on Accept — the pool is engine-internal, so the facade (gossip handler / API) no longer adds. + if (res.status === GossipValidationStatus.Accept) { + this.proposerPreferencesPool.add(signedProposerPreferences); + } + return res; + } + + // --- Op pool: gossip validate (+ insert on Accept) / API validate (throws, + insert) / REST reads --- + // The pools are engine-internal (not on IBeaconEngine's cache surface); the facade reaches them only + // through these narrow methods. gossip = no-throw result; API = throws (preserves REST error shape). + + async validateGossipAttesterSlashing( + attesterSlashing: AttesterSlashing, + fork: ForkName + ): Promise> { + const res = await runGossipValidation(() => validateGossipAttesterSlashing(this, attesterSlashing)); + if (res.status === GossipValidationStatus.Accept) { + // Contain insert failures — a pool error must not flip the gossip verdict (matches prior handler). + try { + this.opPool.insertAttesterSlashing(fork, attesterSlashing); + this.forkChoice.onAttesterSlashing(attesterSlashing); + } catch (e) { + this.logger.error("Error adding attesterSlashing to pool", {}, e as Error); + } + } + return res; + } + + async validateApiAttesterSlashing(attesterSlashing: AttesterSlashing, fork: ForkName): Promise { + await validateApiAttesterSlashing(this, attesterSlashing); + this.opPool.insertAttesterSlashing(fork, attesterSlashing); + } + + async validateGossipProposerSlashing( + proposerSlashing: phase0.ProposerSlashing + ): Promise> { + const res = await runGossipValidation(() => validateGossipProposerSlashing(this, proposerSlashing)); + if (res.status === GossipValidationStatus.Accept) { + try { + this.opPool.insertProposerSlashing(proposerSlashing); + } catch (e) { + this.logger.error("Error adding proposerSlashing to pool", {}, e as Error); + } + } + return res; + } + + async validateApiProposerSlashing(proposerSlashing: phase0.ProposerSlashing): Promise { + await validateApiProposerSlashing(this, proposerSlashing); + this.opPool.insertProposerSlashing(proposerSlashing); + } + + async validateGossipVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise> { + const res = await runGossipValidation(() => validateGossipVoluntaryExit(this, voluntaryExit)); + if (res.status === GossipValidationStatus.Accept) { + try { + this.opPool.insertVoluntaryExit(voluntaryExit); + } catch (e) { + this.logger.error("Error adding voluntaryExit to pool", {}, e as Error); + } + } + return res; + } + + async validateApiVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise { + await validateApiVoluntaryExit(this, voluntaryExit); + this.opPool.insertVoluntaryExit(voluntaryExit); + } + + async validateGossipBlsToExecutionChange( + blsToExecutionChange: capella.SignedBLSToExecutionChange + ): Promise> { + const res = await runGossipValidation(() => validateGossipBlsToExecutionChange(this, blsToExecutionChange)); + if (res.status === GossipValidationStatus.Accept) { + try { + this.opPool.insertBlsToExecutionChange(blsToExecutionChange); + } catch (e) { + this.logger.error("Error adding blsToExecutionChange to pool", {}, e as Error); + } + } + return res; + } + + async validateApiBlsToExecutionChange( + blsToExecutionChange: capella.SignedBLSToExecutionChange, + preCapella: boolean + ): Promise { + await validateApiBlsToExecutionChange(this, blsToExecutionChange); + this.opPool.insertBlsToExecutionChange(blsToExecutionChange, preCapella); + } + + getPoolProposerPreferences(slot?: Slot): gloas.SignedProposerPreferences[] { + return this.proposerPreferencesPool.getAll(slot); + } + + getPoolAttesterSlashings(): AttesterSlashing[] { + return this.opPool.getAllAttesterSlashings(); + } + + getPoolProposerSlashings(): phase0.ProposerSlashing[] { + return this.opPool.getAllProposerSlashings(); + } + + getPoolVoluntaryExits(): phase0.SignedVoluntaryExit[] { + return this.opPool.getAllVoluntaryExits(); + } + + getPoolBlsToExecutionChanges(): capella.SignedBLSToExecutionChange[] { + return this.opPool.getAllBlsToExecutionChanges().map(({data}) => data); + } + + // --- Op pools (engine-internal): narrow add/read bridges for gossip handlers + REST/validator API --- + // The pools are engine-owned; the facade reaches them only through these methods (never the objects). + + addAttestationToPool( + committeeIndex: CommitteeIndex, + attestation: SingleAttestation, + attDataRootHex: RootHex, + validatorCommitteeIndex: number, + committeeSize: number, + priority?: boolean + ): InsertOutcome { + return this.attestationPool.add( + committeeIndex, + attestation, + attDataRootHex, + validatorCommitteeIndex, + committeeSize, + priority + ); + } + + getAttestationAggregate(slot: Slot, dataRootHex: RootHex, committeeIndex: CommitteeIndex): Attestation | null { + return this.attestationPool.getAggregate(slot, dataRootHex, committeeIndex); + } + + addAggregatedAttestation( + attestation: Attestation, + dataRootHex: RootHex, + attestingIndicesCount: number, + committee: Uint32Array + ): InsertOutcome { + return this.aggregatedAttestationPool.add(attestation, dataRootHex, attestingIndicesCount, committee); + } + + getPoolAggregatedAttestations(bySlot?: Slot): Attestation[] { + return this.aggregatedAttestationPool.getAll(bySlot); + } + + addSyncCommitteeMessage( + subnet: SubnetID, + signature: altair.SyncCommitteeMessage, + indexInSubcommittee: number, + priority?: boolean + ): InsertOutcome { + return this.syncCommitteeMessagePool.add(subnet, signature, indexInSubcommittee, priority); + } + + getSyncCommitteeContribution( + subnet: SubcommitteeIndex, + slot: Slot, + prevBlockRoot: Root + ): altair.SyncCommitteeContribution | null { + return this.syncCommitteeMessagePool.getContribution(subnet, slot, prevBlockRoot); + } + + addSyncContributionAndProof( + contributionAndProof: altair.ContributionAndProof, + syncCommitteeParticipants: number, + priority?: boolean + ): InsertOutcome { + return this.syncContributionAndProofPool.add(contributionAndProof, syncCommitteeParticipants, priority); + } + + addPayloadAttestation( + message: gloas.PayloadAttestationMessage, + payloadAttDataRootHex: RootHex, + validatorCommitteeIndices: number[] + ): InsertOutcome { + return this.payloadAttestationPool.add(message, payloadAttDataRootHex, validatorCommitteeIndices); + } + + getPoolPayloadAttestations(slot?: Slot): gloas.PayloadAttestation[] { + return this.payloadAttestationPool.getAll(slot); + } + + addExecutionPayloadBid(bid: gloas.SignedExecutionPayloadBid): InsertOutcome { + return this.executionPayloadBidPool.add(bid); + } + + // --- Proposer cache + finalized balances (engine-internal) --- + + /** Fee recipient registered for a proposer, or undefined if none (block-production default is + * resolved in `produceBlockBase`; import uses this for its FCU fee recipient). */ + getProposerFeeRecipient(proposerIndex: ValidatorIndex): string | undefined { + return this.beaconProposerCache.get(proposerIndex); + } + + /** Register proposer preparation data. Returns true if new validators were discovered. */ + updateProposerPreparation(epoch: Epoch, proposers: ProposerPreparationData[]): boolean { + const previousValidatorCount = this.beaconProposerCache.getValidatorIndices().length; + for (const proposer of proposers) { + this.beaconProposerCache.add(epoch, proposer); + } + return this.beaconProposerCache.getValidatorIndices().length > previousValidatorCount; + } + + /** Validator indices with proposer preparation registered (attached validators). */ + getProposerCacheValidatorIndices(): ValidatorIndex[] { + return this.beaconProposerCache.getValidatorIndices(); + } + + /** Cached finalized effective-balance increments for a checkpoint, or undefined if not cached. */ + getCheckpointEffectiveBalances(checkpoint: CheckpointWithHex): EffectiveBalanceIncrements | undefined { + return this.checkpointBalancesCache.get(checkpoint); + } + + /** Prune the (internal) op pool + seen-block-proposers on finalization — driven by the facade's finalized event. */ + async pruneOnFinalized(): Promise { + this.seenBlockProposers.prune(computeStartSlotAtEpoch(this.forkChoice.getFinalizedCheckpoint().epoch)); + + // TODO: Improve using regen here + const {blockRoot, stateRoot, slot} = this.forkChoice.getHead(); + const headState = this.regen.getStateSync(stateRoot); + const res = await this.getSerializedBlockByRoot(fromHex(blockRoot)); + if (res == null) { + throw Error(`Head block for ${slot} is not available in cache or database`); + } + if (headState) { + const headBlock = this.config.getForkTypes(res.slot).SignedBeaconBlock.deserialize(res.bytes); + this.opPool.pruneAll(headBlock, headState); + } else { + this.logger.verbose("Head state is null"); + } + } + + /** Liveness: whether the validator was seen via imported blocks or gossip/API in the epoch. */ + validatorSeenAtEpoch(index: ValidatorIndex, epoch: Epoch): boolean { + return ( + // Dedicated liveness cache, registers attesters seen through imported blocks. + this.seenBlockAttesters.isKnown(epoch, index) || + // seenAttesters = single signer of unaggregated attestations + this.seenAttesters.isKnown(epoch, index) || + // seenAggregators = single aggregator index, not participants of the aggregate + this.seenAggregators.isKnown(epoch, index) || + // seenPayloadAttesters = single signer of payload attestation message + this.seenPayloadAttesters.isKnown(epoch, index) || + // seenBlockProposers = single block proposer + this.seenBlockProposers.seenAtEpoch(epoch, index) + ); + } + + /** Whether a block proposer was already seen for this slot (sync anti-unbundling check). */ + isBlockProposerSeen(slot: Slot, proposerIndex: ValidatorIndex): boolean { + return this.seenBlockProposers.isKnown(slot, proposerIndex); } async verifyBlocks( @@ -1839,7 +2178,6 @@ export class BeaconEngine implements IBeaconEngine { // 3. Import attestations to fork choice const FORK_CHOICE_ATT_EPOCH_LIMIT = 1; - const attestationsResult: {blockEpoch: number; attestingIndices: number[]}[] = []; if ( opts.importAttestations === AttestationImportOpt.Force || @@ -1879,7 +2217,8 @@ export class BeaconEngine implements IBeaconEngine { ); } - attestationsResult.push({blockEpoch, attestingIndices: Array.from(indexedAttestation.attestingIndices)}); + // Register attesters seen in this block for the liveness cache (engine-owned). + this.seenBlockAttesters.addIndices(blockEpoch, Array.from(indexedAttestation.attestingIndices)); const correctHead = ssz.Root.equals(rootCache.getBlockRootAtSlot(attestation.data.slot), beaconBlockRoot); const missedSlotVote = @@ -2148,7 +2487,6 @@ export class BeaconEngine implements IBeaconEngine { oldHeadBlockRoot, newHeadBlockRoot: newHead.blockRoot, newHead, - attestations: attestationsResult, blockMeta: { slot: blockSlot, blockRootHex, diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 630a455c944b..85765c8a4bf9 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -9,13 +9,16 @@ import { ProtoBlock, } from "@lodestar/fork-choice"; import {ForkName, ForkPostBellatrix} from "@lodestar/params"; -import {DataAvailabilityStatus, PubkeyCache} from "@lodestar/state-transition"; +import {DataAvailabilityStatus, EffectiveBalanceIncrements, PubkeyCache} from "@lodestar/state-transition"; import { + Attestation, + AttesterSlashing, BLSPubkey, BLSSignature, BeaconBlock, BlindedBeaconBlock, Bytes32, + CommitteeIndex, Epoch, Gwei, Root, @@ -23,26 +26,31 @@ import { SSEPayloadAttributes, SignedAggregateAndProof, SignedBeaconBlock, + SingleAttestation, Slot, + SubcommitteeIndex, SubnetID, ValidatorIndex, altair, + capella, deneb, fulu, gloas, + phase0, } from "@lodestar/types"; import {Logger} from "@lodestar/utils"; import {IBeaconEngineDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; -import {BufferPool} from "../../util/bufferPool.js"; import {IClock} from "../../util/clock.js"; import {BlockRootSlot} from "../../util/sszBytes.js"; +import {ProposerPreparationData} from "../beaconProposerCache.js"; import {IBlockInput} from "../blocks/blockInput/index.js"; import {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; import {ImportBlockOpts} from "../blocks/types.js"; import {ChainEventEmitter, ReorgEventData} from "../emitter.js"; import {CommonBlockBody} from "../interface.js"; import {LightClientServer} from "../lightClient/index.js"; +import {InsertOutcome} from "../opPools/types.js"; import {BlockProcessOpts} from "../options.js"; import { BlockAttributes, @@ -67,7 +75,6 @@ export type BeaconEngineModules = { metrics: Metrics | null; clock: IClock; pubkeyCache: PubkeyCache; - bufferPool: BufferPool; cpStateDatastore: CPStateDatastore; // TODO - beacon engine: emitter is facade infra; forkChoice/regen should not depend on it inside the engine. emitter: ChainEventEmitter; @@ -104,7 +111,6 @@ export type ImportBlockResult = { currFinalizedEpoch: number; oldHeadBlockRoot: string; newHeadBlockRoot: string; - attestations: {blockEpoch: number; attestingIndices: number[]}[]; blockMeta: { slot: number; blockRootHex: string; @@ -123,6 +129,10 @@ export type ProduceBlockBaseResult = { parentBlock: ProtoBlock; proposerIndex: ValidatorIndex; proposerPubKey: BLSPubkey; + /** Proposer fee recipient resolved from the engine cache (cached-or-default); API-requested overrides it */ + defaultFeeRecipient: string; + /** Whether the proposer had a registered fee recipient (vs the configured default) — for logging */ + feeRecipientCached: boolean; safeBlockHash: RootHex; finalizedBlockHash: RootHex; /** EL payload-attribute timestamp (computeTimeAtSlot) */ @@ -399,6 +409,85 @@ export interface IBeaconEngine { preferencesBytes: Uint8Array, signedProposerPreferences: gloas.SignedProposerPreferences ): Promise>; + + // Op pool: gossip validate (+ insert on Accept) / API validate (throws, + insert) / REST reads. The + // pools live inside the engine (not exposed as cache objects); the facade reaches them only here. + validateGossipAttesterSlashing( + attesterSlashing: AttesterSlashing, + fork: ForkName + ): Promise>; + validateApiAttesterSlashing(attesterSlashing: AttesterSlashing, fork: ForkName): Promise; + validateGossipProposerSlashing(proposerSlashing: phase0.ProposerSlashing): Promise>; + validateApiProposerSlashing(proposerSlashing: phase0.ProposerSlashing): Promise; + validateGossipVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise>; + validateApiVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise; + validateGossipBlsToExecutionChange( + blsToExecutionChange: capella.SignedBLSToExecutionChange + ): Promise>; + validateApiBlsToExecutionChange( + blsToExecutionChange: capella.SignedBLSToExecutionChange, + preCapella: boolean + ): Promise; + getPoolProposerPreferences(slot?: Slot): gloas.SignedProposerPreferences[]; + getPoolAttesterSlashings(): AttesterSlashing[]; + getPoolProposerSlashings(): phase0.ProposerSlashing[]; + getPoolVoluntaryExits(): phase0.SignedVoluntaryExit[]; + getPoolBlsToExecutionChanges(): capella.SignedBLSToExecutionChange[]; + + // Op pools (engine-internal): narrow add/read bridges for gossip handlers + REST/validator API. + addAttestationToPool( + committeeIndex: CommitteeIndex, + attestation: SingleAttestation, + attDataRootHex: RootHex, + validatorCommitteeIndex: number, + committeeSize: number, + priority?: boolean + ): InsertOutcome; + getAttestationAggregate(slot: Slot, dataRootHex: RootHex, committeeIndex: CommitteeIndex): Attestation | null; + addAggregatedAttestation( + attestation: Attestation, + dataRootHex: RootHex, + attestingIndicesCount: number, + committee: Uint32Array + ): InsertOutcome; + getPoolAggregatedAttestations(bySlot?: Slot): Attestation[]; + addSyncCommitteeMessage( + subnet: SubnetID, + signature: altair.SyncCommitteeMessage, + indexInSubcommittee: number, + priority?: boolean + ): InsertOutcome; + getSyncCommitteeContribution( + subnet: SubcommitteeIndex, + slot: Slot, + prevBlockRoot: Root + ): altair.SyncCommitteeContribution | null; + addSyncContributionAndProof( + contributionAndProof: altair.ContributionAndProof, + syncCommitteeParticipants: number, + priority?: boolean + ): InsertOutcome; + addPayloadAttestation( + message: gloas.PayloadAttestationMessage, + payloadAttDataRootHex: RootHex, + validatorCommitteeIndices: number[] + ): InsertOutcome; + getPoolPayloadAttestations(slot?: Slot): gloas.PayloadAttestation[]; + addExecutionPayloadBid(bid: gloas.SignedExecutionPayloadBid): InsertOutcome; + + // Proposer cache + finalized balances (engine-internal). + getProposerFeeRecipient(proposerIndex: ValidatorIndex): string | undefined; + updateProposerPreparation(epoch: Epoch, proposers: ProposerPreparationData[]): boolean; + getProposerCacheValidatorIndices(): ValidatorIndex[]; + getCheckpointEffectiveBalances(checkpoint: CheckpointWithHex): EffectiveBalanceIncrements | undefined; + + /** Prune the (internal) op pool + seen-block-proposers on finalization — driven by the facade's finalized event. */ + pruneOnFinalized(): Promise; + /** Liveness: whether the validator was seen via imported blocks or gossip/API in the epoch. */ + validatorSeenAtEpoch(index: ValidatorIndex, epoch: Epoch): boolean; + /** Whether a block proposer was already seen for this slot (sync anti-unbundling check). */ + isBlockProposerSeen(slot: Slot, proposerIndex: ValidatorIndex): boolean; + verifyBlocks( _blockBytes: Uint8Array[], parentBlock: ProtoBlock, diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 587d38e07566..38927f5fe19e 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -58,12 +58,7 @@ export async function importBlock( opts ); - // Register attesters seen in this block - for (const {blockEpoch, attestingIndices} of r.attestations) { - this.seenBlockAttesters.addIndices(blockEpoch, attestingIndices); - } - - // Emit head event (engine already pruned pools and set head state) + // Emit head event (engine already pruned pools, registered seen-attesters, and set head state) if (r.head !== null) { try { this.emitter.emit(routes.events.EventType.head, r.head); @@ -92,7 +87,7 @@ export async function importBlock( const proposerIndex = r.proposerIndexNextSlot; if (proposerIndex !== null) { // TODO - beacon engine: move this there - const feeRecipient = this.beaconProposerCache.get(proposerIndex); + const feeRecipient = this.beaconEngine.getProposerFeeRecipient(proposerIndex); if (feeRecipient && r.blockSummary !== null) { const result = this.forkChoice.shouldOverrideForkChoiceUpdate( r.blockSummary, diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 957ea634aed6..b7fee2e342a2 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -54,7 +54,6 @@ import {BuilderStatus} from "../execution/builder/http.js"; import {IExecutionBuilder, IExecutionEngine} from "../execution/index.js"; import {Metrics} from "../metrics/index.js"; import {computeNodeIdFromPrivateKey} from "../network/subnets/interface.js"; -import {BufferPool} from "../util/bufferPool.js"; import {Clock, ClockEvent, IClock} from "../util/clock.js"; import {CustodyConfig, getValidatorsCustodyRequirement} from "../util/dataColumns.js"; import {callInNextEventLoop} from "../util/eventLoop.js"; @@ -64,9 +63,7 @@ import {JobItemQueue} from "../util/queue/itemQueue.js"; import {SerializedCache} from "../util/serializedCache.js"; import {BlockRootSlot} from "../util/sszBytes.js"; import {ArchiveStore} from "./archiveStore/archiveStore.js"; -import {CheckpointBalancesCache} from "./balancesCache.js"; import {BeaconEngine} from "./beaconEngine/index.js"; -import {BeaconProposerCache} from "./beaconProposerCache.js"; import {IBlockInput, isBlockInputBlobs, isBlockInputColumns} from "./blocks/blockInput/index.js"; import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js"; import {PayloadEnvelopeProcessor} from "./blocks/payloadEnvelopeProcessor.js"; @@ -80,37 +77,13 @@ import {ForkchoiceCaller} from "./forkChoice/index.js"; import {GetBlobsTracker} from "./GetBlobsTracker.js"; import {CommonBlockBody, FindHeadFnName, IBeaconChain, ProposerPreparationData, StateGetOpts} from "./interface.js"; import {LightClientServer} from "./lightClient/index.js"; -import { - AggregatedAttestationPool, - AttestationPool, - ExecutionPayloadBidPool, - OpPool, - PayloadAttestationPool, - ProposerPreferencesPool, - SyncCommitteeMessagePool, - SyncContributionAndProofPool, -} from "./opPools/index.js"; import {IChainOptions} from "./options.js"; import {PrepareNextSlotScheduler} from "./prepareNextSlot.js"; import {AssembledBlockType, BlockType, ProduceResult} from "./produceBlock/index.js"; import {BlockAttributes, PreparedBlockScalars, produceBlockBody} from "./produceBlock/produceBlockBody.js"; import {QueuedStateRegenerator, RegenCaller} from "./regen/index.js"; import {ReprocessController} from "./reprocess.js"; -import { - PayloadEnvelopeInput, - SeenAggregators, - SeenAttesters, - SeenBlockProposers, - SeenContributionAndProof, - SeenExecutionPayloadBids, - SeenPayloadAttesters, - SeenPayloadEnvelopeInput, - SeenProposerPreferences, - SeenSyncCommitteeMessages, -} from "./seenCache/index.js"; -import {SeenAggregatedAttestations} from "./seenCache/seenAggregateAndProof.js"; -import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js"; -import {SeenBlockAttesters} from "./seenCache/seenBlockAttesters.js"; +import {PayloadEnvelopeInput, SeenPayloadEnvelopeInput} from "./seenCache/index.js"; import {SeenBlockInput} from "./seenCache/seenGossipBlockInput.js"; import {DbCPStateDatastore, checkpointToDatastoreKey} from "./stateCache/datastore/db.js"; import {FileCPStateDatastore} from "./stateCache/datastore/file.js"; @@ -144,6 +117,7 @@ const DEFAULT_MAX_PENDING_UNFINALIZED_PAYLOAD_ENVELOPE_WRITES = 16; export class BeaconChain implements IBeaconChain { readonly genesisTime: UintNum64; readonly genesisValidatorsRoot: Root; + readonly beaconEngine: BeaconEngine; readonly executionEngine: IExecutionEngine; readonly executionBuilder?: IExecutionBuilder; // Expose config for convenience in modularized functions @@ -152,11 +126,9 @@ export class BeaconChain implements IBeaconChain { readonly logger: Logger; readonly metrics: Metrics | null; readonly validatorMonitor: ValidatorMonitor | null; - readonly bufferPool: BufferPool; readonly anchorStateLatestBlockSlot: Slot; - readonly beaconEngine: BeaconEngine; // TODO - beacon engine: remove this? get bls(): IBlsVerifier { return this.beaconEngine.bls; @@ -182,86 +154,16 @@ export class BeaconChain implements IBeaconChain { readonly unfinalizedBlockWrites: JobItemQueue<[IBlockInput], void>; readonly unfinalizedPayloadEnvelopeWrites: JobItemQueue<[PayloadEnvelopeInput], void>; - // Ops pool - get attestationPool(): AttestationPool { - return this.beaconEngine.attestationPool; - } - get aggregatedAttestationPool(): AggregatedAttestationPool { - return this.beaconEngine.aggregatedAttestationPool; - } - get syncCommitteeMessagePool(): SyncCommitteeMessagePool { - return this.beaconEngine.syncCommitteeMessagePool; - } - get syncContributionAndProofPool(): SyncContributionAndProofPool { - return this.beaconEngine.syncContributionAndProofPool; - } - // TODO - beacon engine: remove this - get executionPayloadBidPool(): ExecutionPayloadBidPool { - return this.beaconEngine.executionPayloadBidPool; - } - get payloadAttestationPool(): PayloadAttestationPool { - return this.beaconEngine.payloadAttestationPool; - } - // TODO - beacon engine: remove this - get proposerPreferencesPool(): ProposerPreferencesPool { - return this.beaconEngine.proposerPreferencesPool; - } - get opPool(): OpPool { - return this.beaconEngine.opPool; - } + // Op pools are engine-internal (pruned + read/written via the engine); no facade getters. - // Gossip seen cache - get seenAttesters(): SeenAttesters { - return this.beaconEngine.seenAttesters; - } - get seenAggregators(): SeenAggregators { - return this.beaconEngine.seenAggregators; - } - get seenPayloadAttesters(): SeenPayloadAttesters { - return this.beaconEngine.seenPayloadAttesters; - } - // TODO - beacon engine: remove this - get seenAggregatedAttestations(): SeenAggregatedAttestations { - return this.beaconEngine.seenAggregatedAttestations; - } - // TODO - beacon engine: remove this - get seenExecutionPayloadBids(): SeenExecutionPayloadBids { - return this.beaconEngine.seenExecutionPayloadBids; - } - // TODO - beacon engine: remove this - get seenProposerPreferences(): SeenProposerPreferences { - return this.beaconEngine.seenProposerPreferences; - } - // TODO - beacon engine: remove this - get seenBlockProposers(): SeenBlockProposers { - return this.beaconEngine.seenBlockProposers; - } - get seenSyncCommitteeMessages(): SeenSyncCommitteeMessages { - return this.beaconEngine.seenSyncCommitteeMessages; - } - get seenContributionAndProof(): SeenContributionAndProof { - return this.beaconEngine.seenContributionAndProof; - } - get seenAttestationDatas(): SeenAttestationDatas { - return this.beaconEngine.seenAttestationDatas; - } + // Gossip seen caches are engine-internal (pruned + read via the engine); no facade getters. readonly seenBlockInputCache: SeenBlockInput; readonly seenPayloadEnvelopeInputCache: SeenPayloadEnvelopeInput; - // Seen cache for liveness checks - readonly seenBlockAttesters = new SeenBlockAttesters(); // Global state caches + // TODO - beacon engine: should this be moved to BeaconEngine? readonly pubkeyCache: PubkeyCache; - get beaconProposerCache(): BeaconProposerCache { - return this.beaconEngine.beaconProposerCache; - } - - // TODO - beacon engine: remove this - get checkpointBalancesCache(): CheckpointBalancesCache { - return this.beaconEngine.checkpointBalancesCache; - } - /** * Cache produced results (ExecutionPayload, DA Data) from the local execution so that we can send * and get signed/published blinded versions which beacon node can @@ -354,7 +256,6 @@ export class BeaconChain implements IBeaconChain { if (!clock) clock = new Clock({config, genesisTime: this.genesisTime, signal}); - this.bufferPool = new BufferPool(anchorState.serializedSize(), metrics); const fileDataStore = opts.nHistoricalStatesFileDataStore ?? true; // checkpointState is an engine repo; build the datastore from the bootstrap `db` (union) param. this.cpStateDatastore = fileDataStore ? new FileCPStateDatastore(dataDir) : new DbCPStateDatastore(db); @@ -391,7 +292,6 @@ export class BeaconChain implements IBeaconChain { metrics, clock, pubkeyCache, - bufferPool: this.bufferPool, cpStateDatastore: this.cpStateDatastore, emitter, signal, @@ -548,23 +448,8 @@ export class BeaconChain implements IBeaconChain { validatorSeenAtEpoch(index: ValidatorIndex, epoch: Epoch): boolean { // Caller must check that epoch is not older that current epoch - 1 // else the caches for that epoch may already be pruned. - - // TODO - beacon engine: consider move all of these caches to BeaconEngine - return ( - // Dedicated cache for liveness checks, registers attesters seen through blocks. - // Note: this check should be cheaper + overlap with counting participants of aggregates from gossip. - this.seenBlockAttesters.isKnown(epoch, index) || - // - // Re-use gossip caches. Populated on validation of gossip + API messages - // seenAttesters = single signer of unaggregated attestations - this.seenAttesters.isKnown(epoch, index) || - // seenAggregators = single aggregator index, not participants of the aggregate - this.seenAggregators.isKnown(epoch, index) || - // seenPayloadAttesters = single signer of payload attestation message - this.seenPayloadAttesters.isKnown(epoch, index) || - // seenBlockProposers = single block proposer - this.seenBlockProposers.seenAtEpoch(epoch, index) - ); + // All liveness caches (seenBlockAttesters + gossip caches) are engine-internal. + return this.beaconEngine.validatorSeenAtEpoch(index, epoch); } /** Populate in-memory caches with persisted data. Call at least once on startup */ @@ -1079,6 +964,8 @@ export class BeaconChain implements IBeaconChain { // produceBlockBody no longer needs a BeaconState here. proposerIndex, proposerPubKey, + defaultFeeRecipient, + feeRecipientCached, safeBlockHash, finalizedBlockHash, timestamp, @@ -1117,6 +1004,8 @@ export class BeaconChain implements IBeaconChain { payloadAttestations, withdrawals, proposerPubKey, + defaultFeeRecipient, + feeRecipientCached, commonBlockBodyPromise, builderBid, } @@ -1369,16 +1258,7 @@ export class BeaconChain implements IBeaconChain { } private onScrapeMetrics(metrics: Metrics): void { - // aggregatedAttestationPool tracks metrics on its own - metrics.opPool.attestationPool.size.set(this.attestationPool.getAttestationCount()); - metrics.opPool.attesterSlashingPoolSize.set(this.opPool.attesterSlashingsSize); - metrics.opPool.proposerSlashingPoolSize.set(this.opPool.proposerSlashingsSize); - metrics.opPool.voluntaryExitPoolSize.set(this.opPool.voluntaryExitsSize); - metrics.opPool.syncCommitteeMessagePoolSize.set(this.syncCommitteeMessagePool.size); - metrics.opPool.payloadAttestationPool.size.set(this.payloadAttestationPool.size); - metrics.opPool.executionPayloadBidPool.size.set(this.executionPayloadBidPool.size); - // syncContributionAndProofPool tracks metrics on its own - metrics.opPool.blsToExecutionChangePoolSize.set(this.opPool.blsToExecutionChangeSize); + // All op-pool sizes are reported by the engine (the pools are engine-internal). metrics.chain.blacklistedBlocks.set(this.blacklistedBlocks.size); const headState = this.getHeadState(); @@ -1400,16 +1280,7 @@ export class BeaconChain implements IBeaconChain { this.beaconEngine.forkChoice.updateTime(slot); this.metrics?.clockSlot.set(slot); - this.attestationPool.prune(slot); - this.aggregatedAttestationPool.prune(slot); - this.syncCommitteeMessagePool.prune(slot); - this.seenSyncCommitteeMessages.prune(slot); - this.payloadAttestationPool.prune(slot); - this.executionPayloadBidPool.prune(slot); - this.seenExecutionPayloadBids.prune(slot); - this.seenProposerPreferences.prune(slot); - this.proposerPreferencesPool.prune(slot); - this.seenAttestationDatas.onSlot(slot); + // Engine-owned pools + seen-caches are pruned by the engine's own clock listener. this.reprocessController.onSlot(slot); // Prune old cached block production artifacts, those are only useful on their slot @@ -1429,18 +1300,8 @@ export class BeaconChain implements IBeaconChain { private onClockEpoch(epoch: Epoch): void { this.metrics?.clockEpoch.set(epoch); - - this.seenAttesters.prune(epoch); - this.seenAggregators.prune(epoch); - this.seenPayloadAttesters.prune(epoch); - this.seenAggregatedAttestations.prune(epoch); - this.seenBlockAttesters.prune(epoch); - this.beaconProposerCache.prune(epoch); - } - - protected onNewHead(head: ProtoBlock): void { - this.syncContributionAndProofPool.prune(head.slot); - this.seenContributionAndProof.prune(head.slot); + // Engine-owned caches (seen-caches, seenBlockAttesters, beaconProposerCache) are pruned by the + // engine's own clock listener; nothing epoch-scoped remains facade-side. } private onForkChoiceJustified(this: BeaconChain, cp: CheckpointWithHex): void { @@ -1456,40 +1317,20 @@ export class BeaconChain implements IBeaconChain { private async onForkChoiceFinalized(this: BeaconChain, cp: CheckpointWithHex): Promise { this.logger.verbose("Fork choice finalized", {epoch: cp.epoch, root: cp.rootHex}); - const finalizedSlot = computeStartSlotAtEpoch(cp.epoch); - this.seenBlockProposers.prune(finalizedSlot); // Update validator custody to account for effective balance changes await this.updateValidatorsCustodyRequirement(cp); - // TODO: Improve using regen here - const {blockRoot, stateRoot, slot} = this.forkChoice.getHead(); - const headState = this.regen.getStateSync(stateRoot); - const blockResult = await this.getBlockByRoot(blockRoot); - if (blockResult == null) { - throw Error(`Head block for ${slot} is not available in cache or database`); - } - - if (headState) { - this.opPool.pruneAll(blockResult.block, headState); - } - - if (headState === null) { - this.logger.verbose("Head state is null"); - } + // Engine prunes its own op pool + seen-block-proposers on finalization. + await this.beaconEngine.pruneOnFinalized(); } async updateBeaconProposerData(epoch: Epoch, proposers: ProposerPreparationData[]): Promise { - const previousValidatorCount = this.beaconProposerCache.getValidatorIndices().length; - - for (const proposer of proposers) { - this.beaconProposerCache.add(epoch, proposer); - } - - const newValidatorCount = this.beaconProposerCache.getValidatorIndices().length; + // Engine owns the proposer cache; it returns whether new validators were discovered. + const discoveredNewValidators = this.beaconEngine.updateProposerPreparation(epoch, proposers); // Only update validator custody if we discovered new validators - if (newValidatorCount > previousValidatorCount) { + if (discoveredNewValidators) { const finalizedCheckpoint = this.forkChoice.getFinalizedCheckpoint(); await this.updateValidatorsCustodyRequirement(finalizedCheckpoint); } @@ -1504,11 +1345,11 @@ export class BeaconChain implements IBeaconChain { } // Validators attached to the node - const validatorIndices = this.beaconProposerCache.getValidatorIndices(); + const validatorIndices = this.beaconEngine.getProposerCacheValidatorIndices(); // Update custody requirement based on finalized state let effectiveBalances: number[]; - const effectiveBalanceIncrements = this.checkpointBalancesCache.get(finalizedCheckpoint); + const effectiveBalanceIncrements = this.beaconEngine.getCheckpointEffectiveBalances(finalizedCheckpoint); if (effectiveBalanceIncrements) { effectiveBalances = validatorIndices.map( (index) => (effectiveBalanceIncrements[index] ?? 0) * EFFECTIVE_BALANCE_INCREMENT diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 46cd483200c3..1f813e4125a5 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -25,15 +25,13 @@ import { import {Logger} from "@lodestar/utils"; import {IExecutionBuilder, IExecutionEngine} from "../execution/index.js"; import {Metrics} from "../metrics/metrics.js"; -import {BufferPool} from "../util/bufferPool.js"; import {IClock} from "../util/clock.js"; import {CustodyConfig} from "../util/dataColumns.js"; import {SerializedCache} from "../util/serializedCache.js"; import {BlockRootSlot} from "../util/sszBytes.js"; import {IArchiveStore} from "./archiveStore/interface.js"; -import {CheckpointBalancesCache} from "./balancesCache.js"; import {IBeaconEngine} from "./beaconEngine/index.js"; -import {BeaconProposerCache, ProposerPreparationData} from "./beaconProposerCache.js"; +import {ProposerPreparationData} from "./beaconProposerCache.js"; import {IBlockInput} from "./blocks/blockInput/index.js"; import {ImportBlockOpts, ImportPayloadOpts} from "./blocks/types.js"; import {IBlsVerifier} from "./bls/index.js"; @@ -42,33 +40,10 @@ import {ChainEventEmitter} from "./emitter.js"; import {ForkchoiceCaller} from "./forkChoice/index.js"; import {GetBlobsTracker} from "./GetBlobsTracker.js"; import {LightClientServer} from "./lightClient/index.js"; -import {AggregatedAttestationPool} from "./opPools/aggregatedAttestationPool.js"; -import { - AttestationPool, - ExecutionPayloadBidPool, - OpPool, - PayloadAttestationPool, - ProposerPreferencesPool, - SyncCommitteeMessagePool, - SyncContributionAndProofPool, -} from "./opPools/index.js"; import {IChainOptions} from "./options.js"; import {AssembledBlockType, BlockAttributes, BlockType, ProduceResult} from "./produceBlock/produceBlockBody.js"; import {IStateRegenerator, RegenCaller} from "./regen/index.js"; import {ReprocessController} from "./reprocess.js"; -import { - SeenAggregators, - SeenAttesters, - SeenBlockProposers, - SeenContributionAndProof, - SeenExecutionPayloadBids, - SeenPayloadAttesters, - SeenProposerPreferences, - SeenSyncCommitteeMessages, -} from "./seenCache/index.js"; -import {SeenAggregatedAttestations} from "./seenCache/seenAggregateAndProof.js"; -import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js"; -import {SeenBlockAttesters} from "./seenCache/seenBlockAttesters.js"; import {SeenBlockInput} from "./seenCache/seenGossipBlockInput.js"; import {PayloadEnvelopeInput, SeenPayloadEnvelopeInput} from "./seenCache/seenPayloadEnvelopeInput.js"; import {ValidatorMonitor} from "./validatorMonitor.js"; @@ -103,7 +78,6 @@ export interface IBeaconChain { readonly logger: Logger; readonly metrics: Metrics | null; readonly validatorMonitor: ValidatorMonitor | null; - readonly bufferPool: BufferPool | null; /** The initial slot that the chain is started with */ readonly anchorStateLatestBlockSlot: Slot; @@ -119,34 +93,12 @@ export interface IBeaconChain { readonly pubkeyCache: PubkeyCache; readonly archiveStore: IArchiveStore; - // Ops pool - readonly attestationPool: AttestationPool; - readonly aggregatedAttestationPool: AggregatedAttestationPool; - readonly syncCommitteeMessagePool: SyncCommitteeMessagePool; - readonly syncContributionAndProofPool: SyncContributionAndProofPool; - readonly executionPayloadBidPool: ExecutionPayloadBidPool; - readonly payloadAttestationPool: PayloadAttestationPool; - readonly proposerPreferencesPool: ProposerPreferencesPool; - readonly opPool: OpPool; - - // Gossip seen cache - readonly seenAttesters: SeenAttesters; - readonly seenAggregators: SeenAggregators; - readonly seenPayloadAttesters: SeenPayloadAttesters; - readonly seenAggregatedAttestations: SeenAggregatedAttestations; - readonly seenExecutionPayloadBids: SeenExecutionPayloadBids; - readonly seenProposerPreferences: SeenProposerPreferences; - readonly seenBlockProposers: SeenBlockProposers; - readonly seenSyncCommitteeMessages: SeenSyncCommitteeMessages; - readonly seenContributionAndProof: SeenContributionAndProof; - readonly seenAttestationDatas: SeenAttestationDatas; + // Op pools are engine-internal (add/read via the engine); not on the facade. + // Gossip seen caches are engine-internal (pruned + read via the engine); not on the facade. readonly seenBlockInputCache: SeenBlockInput; readonly seenPayloadEnvelopeInputCache: SeenPayloadEnvelopeInput; - // Seen cache for liveness checks - readonly seenBlockAttesters: SeenBlockAttesters; - readonly beaconProposerCache: BeaconProposerCache; - readonly checkpointBalancesCache: CheckpointBalancesCache; + // beaconProposerCache + checkpointBalancesCache are engine-internal (read/written via the engine). readonly blockProductionCache: Map; diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 6641342294a6..fc5e47759ebe 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -90,6 +90,10 @@ export type BlockAttributes = { export type PreparedBlockScalars = { proposerIndex: ValidatorIndex; proposerPubKey: BLSPubkey; + // Proposer fee recipient resolved by produceBlockBase (cached-or-default); the API-requested one, if + // supplied, overrides it. `feeRecipientCached` distinguishes registered vs default for logging. + defaultFeeRecipient: string; + feeRecipientCached: boolean; safeBlockHash: RootHex; finalizedBlockHash: RootHex; timestamp: number; @@ -184,6 +188,8 @@ export async function produceBlockBody( parentBlock, proposerIndex, proposerPubKey, + defaultFeeRecipient, + feeRecipientCached, commonBlockBodyPromise, builderBid, // Precomputed by produceBlockBase (gloas/V4); undefined on the V3 path → computed via `??` below @@ -246,7 +252,7 @@ export async function produceBlockBody( // TODO GLOAS: post-Gloas, proposer feeRecipient is also carried (signed) in // ProposerPreferencesPool. Consider using this unified cache instead // see https://github.com/ChainSafe/lodestar/issues/9379 - const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex); + const feeRecipient = requestedFeeRecipient ?? defaultFeeRecipient; const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer(); @@ -361,12 +367,8 @@ export async function produceBlockBody( shouldOverrideBuilder, }); } else if (isForkPostBellatrix(fork)) { - const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex); - const feeRecipientType = requestedFeeRecipient - ? "requested" - : this.beaconProposerCache.get(proposerIndex) - ? "cached" - : "default"; + const feeRecipient = requestedFeeRecipient ?? defaultFeeRecipient; + const feeRecipientType = requestedFeeRecipient ? "requested" : feeRecipientCached ? "cached" : "default"; Object.assign(logMeta, {feeRecipientType, feeRecipient}); diff --git a/packages/beacon-node/src/chain/validation/attesterSlashing.ts b/packages/beacon-node/src/chain/validation/attesterSlashing.ts index d07e67b2fdc6..772caf90821d 100644 --- a/packages/beacon-node/src/chain/validation/attesterSlashing.ts +++ b/packages/beacon-node/src/chain/validation/attesterSlashing.ts @@ -5,26 +5,26 @@ import { isSlashableValidator, } from "@lodestar/state-transition"; import {AttesterSlashing} from "@lodestar/types"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {AttesterSlashingError, AttesterSlashingErrorCode, GossipAction} from "../errors/index.js"; -import {IBeaconChain} from "../index.js"; export async function validateApiAttesterSlashing( - chain: IBeaconChain, + engine: BeaconEngine, attesterSlashing: AttesterSlashing ): Promise { const prioritizeBls = true; - return validateAttesterSlashing(chain, attesterSlashing, prioritizeBls); + return validateAttesterSlashing(engine, attesterSlashing, prioritizeBls); } export async function validateGossipAttesterSlashing( - chain: IBeaconChain, + engine: BeaconEngine, attesterSlashing: AttesterSlashing ): Promise { - return validateAttesterSlashing(chain, attesterSlashing); + return validateAttesterSlashing(engine, attesterSlashing); } export async function validateAttesterSlashing( - chain: IBeaconChain, + engine: BeaconEngine, attesterSlashing: AttesterSlashing, prioritizeBls = false ): Promise { @@ -33,20 +33,20 @@ export async function validateAttesterSlashing( // attester_slashed_indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices // ), verify if any(attester_slashed_indices.difference(prior_seen_attester_slashed_indices))). const intersectingIndices = getAttesterSlashableIndices(attesterSlashing); - if (chain.opPool.hasSeenAttesterSlashing(intersectingIndices)) { + if (engine.opPool.hasSeenAttesterSlashing(intersectingIndices)) { throw new AttesterSlashingError(GossipAction.IGNORE, { code: AttesterSlashingErrorCode.ALREADY_EXISTS, }); } - const state = chain.getHeadState(); + const state = engine.getHeadState(); // [REJECT] All of the conditions within process_attester_slashing pass validation. try { // verifySignature = false, verified in batch below assertValidAttesterSlashing( - chain.config, - chain.pubkeyCache, + engine.config, + engine.pubkeyCache, state.slot, state.validatorCount, attesterSlashing, @@ -67,8 +67,8 @@ export async function validateAttesterSlashing( }); } - const signatureSets = getAttesterSlashingSignatureSets(chain.config, state.slot, attesterSlashing); - if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { + const signatureSets = getAttesterSlashingSignatureSets(engine.config, state.slot, attesterSlashing); + if (!(await engine.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { throw new AttesterSlashingError(GossipAction.REJECT, { code: AttesterSlashingErrorCode.INVALID, error: Error("Invalid signature"), diff --git a/packages/beacon-node/src/chain/validation/blsToExecutionChange.ts b/packages/beacon-node/src/chain/validation/blsToExecutionChange.ts index c76052339ae5..f8ae3f085301 100644 --- a/packages/beacon-node/src/chain/validation/blsToExecutionChange.ts +++ b/packages/beacon-node/src/chain/validation/blsToExecutionChange.ts @@ -1,33 +1,33 @@ import {getBlsToExecutionChangeSignatureSet, isValidBlsToExecutionChange} from "@lodestar/state-transition"; import {capella} from "@lodestar/types"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {BlsToExecutionChangeError, BlsToExecutionChangeErrorCode, GossipAction} from "../errors/index.js"; -import {IBeaconChain} from "../index.js"; export async function validateApiBlsToExecutionChange( - chain: IBeaconChain, + engine: BeaconEngine, blsToExecutionChange: capella.SignedBLSToExecutionChange ): Promise { const ignoreExists = true; const prioritizeBls = true; - return validateBlsToExecutionChange(chain, blsToExecutionChange, {ignoreExists, prioritizeBls}); + return validateBlsToExecutionChange(engine, blsToExecutionChange, {ignoreExists, prioritizeBls}); } export async function validateGossipBlsToExecutionChange( - chain: IBeaconChain, + engine: BeaconEngine, blsToExecutionChange: capella.SignedBLSToExecutionChange ): Promise { - return validateBlsToExecutionChange(chain, blsToExecutionChange); + return validateBlsToExecutionChange(engine, blsToExecutionChange); } async function validateBlsToExecutionChange( - chain: IBeaconChain, + engine: BeaconEngine, blsToExecutionChange: capella.SignedBLSToExecutionChange, opts: {ignoreExists?: boolean; prioritizeBls?: boolean} = {ignoreExists: false, prioritizeBls: false} ): Promise { const {ignoreExists, prioritizeBls} = opts; // [IGNORE] The blsToExecutionChange is the first valid blsToExecutionChange received for the validator with index // signedBLSToExecutionChange.message.validatorIndex. - if (!ignoreExists && chain.opPool.hasSeenBlsToExecutionChange(blsToExecutionChange.message.validatorIndex)) { + if (!ignoreExists && engine.opPool.hasSeenBlsToExecutionChange(blsToExecutionChange.message.validatorIndex)) { throw new BlsToExecutionChangeError(GossipAction.IGNORE, { code: BlsToExecutionChangeErrorCode.ALREADY_EXISTS, }); @@ -36,8 +36,8 @@ async function validateBlsToExecutionChange( // validate bls to executionChange // NOTE: No need to advance head state since the signature's fork is handled with `broadcastedOnFork`, // and chanes relevant to `isValidBlsToExecutionChange()` happen only on processBlock(), not processEpoch() - const state = chain.getHeadState(); - const {config} = chain; + const state = engine.getHeadState(); + const {config} = engine; const addressChange = blsToExecutionChange.message; if (addressChange.validatorIndex >= state.validatorCount) { throw new BlsToExecutionChangeError(GossipAction.REJECT, { @@ -55,7 +55,7 @@ async function validateBlsToExecutionChange( } const signatureSet = getBlsToExecutionChangeSignatureSet(config, blsToExecutionChange); - if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { + if (!(await engine.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { throw new BlsToExecutionChangeError(GossipAction.REJECT, { code: BlsToExecutionChangeErrorCode.INVALID_SIGNATURE, }); diff --git a/packages/beacon-node/src/chain/validation/proposerSlashing.ts b/packages/beacon-node/src/chain/validation/proposerSlashing.ts index f91886138fe4..0a2459753f29 100644 --- a/packages/beacon-node/src/chain/validation/proposerSlashing.ts +++ b/packages/beacon-node/src/chain/validation/proposerSlashing.ts @@ -1,43 +1,43 @@ import {assertValidProposerSlashing, getProposerSlashingSignatureSets} from "@lodestar/state-transition"; import {phase0} from "@lodestar/types"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {GossipAction, ProposerSlashingError, ProposerSlashingErrorCode} from "../errors/index.js"; -import {IBeaconChain} from "../index.js"; export async function validateApiProposerSlashing( - chain: IBeaconChain, + engine: BeaconEngine, proposerSlashing: phase0.ProposerSlashing ): Promise { const prioritizeBls = true; - return validateProposerSlashing(chain, proposerSlashing, prioritizeBls); + return validateProposerSlashing(engine, proposerSlashing, prioritizeBls); } export async function validateGossipProposerSlashing( - chain: IBeaconChain, + engine: BeaconEngine, proposerSlashing: phase0.ProposerSlashing ): Promise { - return validateProposerSlashing(chain, proposerSlashing); + return validateProposerSlashing(engine, proposerSlashing); } async function validateProposerSlashing( - chain: IBeaconChain, + engine: BeaconEngine, proposerSlashing: phase0.ProposerSlashing, prioritizeBls = false ): Promise { // [IGNORE] The proposer slashing is the first valid proposer slashing received for the proposer with index // proposer_slashing.signed_header_1.message.proposer_index. - if (chain.opPool.hasSeenProposerSlashing(proposerSlashing.signedHeader1.message.proposerIndex)) { + if (engine.opPool.hasSeenProposerSlashing(proposerSlashing.signedHeader1.message.proposerIndex)) { throw new ProposerSlashingError(GossipAction.IGNORE, { code: ProposerSlashingErrorCode.ALREADY_EXISTS, }); } - const state = chain.getHeadState(); + const state = engine.getHeadState(); // [REJECT] All of the conditions within process_proposer_slashing pass validation. try { const proposer = state.getValidator(proposerSlashing.signedHeader1.message.proposerIndex); // verifySignature = false, verified in batch below - assertValidProposerSlashing(chain.config, chain.pubkeyCache, state.slot, proposerSlashing, proposer, false); + assertValidProposerSlashing(engine.config, engine.pubkeyCache, state.slot, proposerSlashing, proposer, false); } catch (e) { throw new ProposerSlashingError(GossipAction.REJECT, { code: ProposerSlashingErrorCode.INVALID, @@ -45,8 +45,8 @@ async function validateProposerSlashing( }); } - const signatureSets = getProposerSlashingSignatureSets(chain.config, state.slot, proposerSlashing); - if (!(await chain.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { + const signatureSets = getProposerSlashingSignatureSets(engine.config, state.slot, proposerSlashing); + if (!(await engine.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { throw new ProposerSlashingError(GossipAction.REJECT, { code: ProposerSlashingErrorCode.INVALID, error: Error("Invalid signature"), diff --git a/packages/beacon-node/src/chain/validation/voluntaryExit.ts b/packages/beacon-node/src/chain/validation/voluntaryExit.ts index 5f7a5538b962..9c8e596b9c59 100644 --- a/packages/beacon-node/src/chain/validation/voluntaryExit.ts +++ b/packages/beacon-node/src/chain/validation/voluntaryExit.ts @@ -1,37 +1,37 @@ import {VoluntaryExitValidity, getVoluntaryExitSignatureSet} from "@lodestar/state-transition"; import {phase0} from "@lodestar/types"; +import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import { GossipAction, VoluntaryExitError, VoluntaryExitErrorCode, voluntaryExitValidityToErrorCode, } from "../errors/index.js"; -import {IBeaconChain} from "../index.js"; import {RegenCaller} from "../regen/index.js"; export async function validateApiVoluntaryExit( - chain: IBeaconChain, + engine: BeaconEngine, voluntaryExit: phase0.SignedVoluntaryExit ): Promise { const prioritizeBls = true; - return validateVoluntaryExit(chain, voluntaryExit, prioritizeBls); + return validateVoluntaryExit(engine, voluntaryExit, prioritizeBls); } export async function validateGossipVoluntaryExit( - chain: IBeaconChain, + engine: BeaconEngine, voluntaryExit: phase0.SignedVoluntaryExit ): Promise { - return validateVoluntaryExit(chain, voluntaryExit); + return validateVoluntaryExit(engine, voluntaryExit); } async function validateVoluntaryExit( - chain: IBeaconChain, + engine: BeaconEngine, voluntaryExit: phase0.SignedVoluntaryExit, prioritizeBls = false ): Promise { // [IGNORE] The voluntary exit is the first valid voluntary exit received for the validator with index // signed_voluntary_exit.message.validator_index. - if (chain.opPool.hasSeenVoluntaryExit(voluntaryExit.message.validatorIndex)) { + if (engine.opPool.hasSeenVoluntaryExit(voluntaryExit.message.validatorIndex)) { throw new VoluntaryExitError(GossipAction.IGNORE, { code: VoluntaryExitErrorCode.ALREADY_EXISTS, }); @@ -44,7 +44,7 @@ async function validateVoluntaryExit( // The voluntaryExit.epoch must be in the past but the validator's status may change in recent epochs. // We dial the head state to the current epoch to get the current status of the validator. This is // relevant on periods of many skipped slots. - const state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.validateGossipVoluntaryExit); + const state = await engine.getHeadStateAtCurrentEpoch(RegenCaller.validateGossipVoluntaryExit); // [REJECT] All of the conditions within process_voluntary_exit pass validation. // verifySignature = false, verified in batch below @@ -55,8 +55,8 @@ async function validateVoluntaryExit( }); } - const signatureSet = getVoluntaryExitSignatureSet(chain.config, state, voluntaryExit); - if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { + const signatureSet = getVoluntaryExitSignatureSet(engine.config, state, voluntaryExit); + if (!(await engine.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { throw new VoluntaryExitError(GossipAction.REJECT, { code: VoluntaryExitErrorCode.INVALID_SIGNATURE, }); diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 7ab0adfc4590..5981a27e4782 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -58,14 +58,7 @@ import { PayloadAttestationErrorCode, } from "../../chain/errors/index.js"; import {IBeaconChain} from "../../chain/interface.js"; -import { - GossipAttestation, - toElectraSingleAttestation, - validateGossipAttesterSlashing, - validateGossipBlsToExecutionChange, - validateGossipProposerSlashing, - validateGossipVoluntaryExit, -} from "../../chain/validation/index.js"; +import {GossipAttestation, toElectraSingleAttestation} from "../../chain/validation/index.js"; import {validateLightClientFinalityUpdate} from "../../chain/validation/lightClientFinalityUpdate.js"; import {validateLightClientOptimisticUpdate} from "../../chain/validation/lightClientOptimisticUpdate.js"; import {OpSource} from "../../chain/validatorMonitor.js"; @@ -885,7 +878,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand ); const aggregatedAttestation = signedAggregateAndProof.message.aggregate; - const insertOutcome = chain.aggregatedAttestationPool.add( + const insertOutcome = chain.beaconEngine.addAggregatedAttestation( aggregatedAttestation, attDataRootHex, indexedAttestation.attestingIndices.length, @@ -916,19 +909,10 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const {serializedData} = gossipData; const {fork} = topic.boundary; const attesterSlashing = sszDeserialize(topic, serializedData); - const res = await runGossipValidation(() => validateGossipAttesterSlashing(chain, attesterSlashing)); + // Engine validates + inserts into its (internal) opPool + updates fork choice; facade only emits. + const res = await chain.beaconEngine.validateGossipAttesterSlashing(attesterSlashing, fork); if (res.status !== GossipValidationStatus.Accept) return res; - // Handler - - try { - chain.opPool.insertAttesterSlashing(fork, attesterSlashing); - // TODO - beacon engine - chain.beaconEngine.forkChoice.onAttesterSlashing(attesterSlashing); - } catch (e) { - logger.error("Error adding attesterSlashing to pool", {}, e as Error); - } - chain.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing); return accept(undefined); }, @@ -939,17 +923,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const proposerSlashing = sszDeserialize(topic, serializedData); - const res = await runGossipValidation(() => validateGossipProposerSlashing(chain, proposerSlashing)); + const res = await chain.beaconEngine.validateGossipProposerSlashing(proposerSlashing); if (res.status !== GossipValidationStatus.Accept) return res; - // Handler - - try { - chain.opPool.insertProposerSlashing(proposerSlashing); - } catch (e) { - logger.error("Error adding attesterSlashing to pool", {}, e as Error); - } - chain.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing); return accept(undefined); }, @@ -957,17 +933,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand [GossipType.voluntary_exit]: async ({gossipData, topic}: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const voluntaryExit = sszDeserialize(topic, serializedData); - const res = await runGossipValidation(() => validateGossipVoluntaryExit(chain, voluntaryExit)); + const res = await chain.beaconEngine.validateGossipVoluntaryExit(voluntaryExit); if (res.status !== GossipValidationStatus.Accept) return res; - // Handler - - try { - chain.opPool.insertVoluntaryExit(voluntaryExit); - } catch (e) { - logger.error("Error adding voluntaryExit to pool", {}, e as Error); - } - chain.emitter.emit(routes.events.EventType.voluntaryExit, voluntaryExit); return accept(undefined); }, @@ -996,7 +964,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand syncCommitteeParticipantIndices ); try { - const insertOutcome = chain.syncContributionAndProofPool.add( + const insertOutcome = chain.beaconEngine.addSyncContributionAndProof( contributionAndProof.message, syncCommitteeParticipantIndices.length ); @@ -1025,7 +993,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand // Handler — add for ALL positions this validator holds in the subcommittee try { for (const indexInSubcommittee of indicesInSubcommittee) { - const insertOutcome = chain.syncCommitteeMessagePool.add(subnet, syncCommittee, indexInSubcommittee); + const insertOutcome = chain.beaconEngine.addSyncCommitteeMessage(subnet, syncCommittee, indexInSubcommittee); metrics?.opPool.syncCommitteeMessagePoolInsertOutcome.inc({insertOutcome}); } } catch (e) { @@ -1063,16 +1031,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand }: GossipHandlerParamGeneric) => { const {serializedData} = gossipData; const blsToExecutionChange = sszDeserialize(topic, serializedData); - const res = await runGossipValidation(() => validateGossipBlsToExecutionChange(chain, blsToExecutionChange)); + const res = await chain.beaconEngine.validateGossipBlsToExecutionChange(blsToExecutionChange); if (res.status !== GossipValidationStatus.Accept) return res; - // Handler - try { - chain.opPool.insertBlsToExecutionChange(blsToExecutionChange); - } catch (e) { - logger.error("Error adding blsToExecutionChange to pool", {}, e as Error); - } - chain.emitter.emit(routes.events.EventType.blsToExecutionChange, blsToExecutionChange); return accept(undefined); }, @@ -1204,7 +1165,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const {attDataRootHex, validatorCommitteeIndices} = res.value; try { - const insertOutcome = chain.payloadAttestationPool.add( + const insertOutcome = chain.beaconEngine.addPayloadAttestation( payloadAttestationMessage, attDataRootHex, validatorCommitteeIndices @@ -1234,7 +1195,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand // Handle valid payload bid by storing in a bid pool try { - const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid); + const insertOutcome = chain.beaconEngine.addExecutionPayloadBid(executionPayloadBid); metrics?.opPool.executionPayloadBidPool.gossipInsertOutcome.inc({insertOutcome}); } catch (e) { logger.error("Error adding to executionPayloadBid pool", {}, e as Error); @@ -1257,7 +1218,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand const res = await chain.beaconEngine.validateGossipProposerPreferences(serializedData, signedProposerPreferences); if (res.status !== GossipValidationStatus.Accept) return res; - chain.proposerPreferencesPool.add(signedProposerPreferences); + // Engine inserts into its (internal) proposerPreferencesPool on Accept; facade only emits. chain.emitter.emit(routes.events.EventType.proposerPreferences, { version: ForkName.gloas, data: signedProposerPreferences, @@ -1320,7 +1281,7 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp // Node may be subscribe to extra subnets (long-lived random subnets). For those, validate the messages // but don't add to attestation pool, to save CPU and RAM if (aggregatorTracker.shouldAggregate(subnet, indexedAttestation.data.slot)) { - const insertOutcome = chain.attestationPool.add( + const insertOutcome = chain.beaconEngine.addAttestationToPool( committeeIndex, attestation, attDataRootHex, diff --git a/packages/beacon-node/src/sync/unknownBlock.ts b/packages/beacon-node/src/sync/unknownBlock.ts index e3fc551a5053..f87ce6e657f9 100644 --- a/packages/beacon-node/src/sync/unknownBlock.ts +++ b/packages/beacon-node/src/sync/unknownBlock.ts @@ -774,7 +774,7 @@ export class BlockInputSync { const proposerBoostWindowMs = this.config.getAttestationDueMs(fork); if ( this.chain.clock.msFromSlot(blockSlot) < proposerBoostWindowMs && - this.chain.seenBlockProposers.isKnown(blockSlot, proposerIndex) + this.chain.beaconEngine.isBlockProposerSeen(blockSlot, proposerIndex) ) { // proposer is known by a gossip block already, wait a bit to make sure this block is not // eligible for proposer boost to prevent unbundling attack diff --git a/packages/beacon-node/test/e2e/api/lodestar/lodestar.test.ts b/packages/beacon-node/test/e2e/api/lodestar/lodestar.test.ts index 774ba8e1c569..2f625cc2cada 100644 --- a/packages/beacon-node/test/e2e/api/lodestar/lodestar.test.ts +++ b/packages/beacon-node/test/e2e/api/lodestar/lodestar.test.ts @@ -5,6 +5,7 @@ import {chainConfig as chainConfigDef} from "@lodestar/config/default"; import {LogLevel, TestLoggerOpts, testLogger} from "@lodestar/logger/test-utils"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {phase0} from "@lodestar/types"; +import {BeaconEngine} from "../../../../src/chain/beaconEngine/beaconEngine.js"; import {BeaconNode} from "../../../../src/index.js"; import {ClockEvent} from "../../../../src/util/clock.js"; import {waitForEvent} from "../../../utils/events/resolver.js"; @@ -60,15 +61,17 @@ describe("api / impl / validator", () => { logger: loggerNodeA, }); + // seen-caches are engine-internal; reach them via the concrete engine for this liveness setup + const engine = bn.chain.beaconEngine as BeaconEngine; // live indices at epoch of consideration, epoch 0 - bn.chain.seenBlockProposers.add(0, 1); - bn.chain.seenBlockAttesters.add(0, 2); - bn.chain.seenAttesters.add(0, 3); - bn.chain.seenAggregators.add(0, 4); + engine.seenBlockProposers.add(0, 1); + engine.seenBlockAttesters.add(0, 2); + engine.seenAttesters.add(0, 3); + engine.seenAggregators.add(0, 4); // live indices at other epochs, epoch 10 - bn.chain.seenBlockProposers.add(10, 1000); - bn.chain.seenAttesters.add(10, 2000); - bn.chain.seenAggregators.add(10, 3000); + engine.seenBlockProposers.add(10, 1000); + engine.seenAttesters.add(10, 2000); + engine.seenAggregators.add(10, 3000); const client = getClient({baseUrl: `http://127.0.0.1:${restPort}`}, {config}); @@ -105,7 +108,7 @@ describe("api / impl / validator", () => { await waitForEvent(bn.chain.clock, ClockEvent.epoch, timeout); // wait for epoch 1 await waitForEvent(bn.chain.clock, ClockEvent.epoch, timeout); // wait for epoch 2 - bn.chain.seenBlockProposers.add(bn.chain.clock.currentEpoch, 1); + (bn.chain.beaconEngine as BeaconEngine).seenBlockProposers.add(bn.chain.clock.currentEpoch, 1); const client = getClient({baseUrl: `http://127.0.0.1:${restPort}`}, {config}); diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index 4f529252b5d5..3994092e4a33 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -11,6 +11,17 @@ import {ChainEventEmitter} from "../../src/chain/emitter.js"; import {LightClientServer} from "../../src/chain/lightClient/index.js"; import {AggregatedAttestationPool, OpPool, SyncContributionAndProofPool} from "../../src/chain/opPools/index.js"; import {QueuedStateRegenerator} from "../../src/chain/regen/index.js"; +import { + SeenAggregators, + SeenAttesters, + SeenBlockProposers, + SeenContributionAndProof, + SeenExecutionPayloadBids, + SeenPayloadAttesters, + SeenSyncCommitteeMessages, +} from "../../src/chain/seenCache/index.js"; +import {SeenAggregatedAttestations} from "../../src/chain/seenCache/seenAggregateAndProof.js"; +import {SeenAttestationDatas} from "../../src/chain/seenCache/seenAttestationData.js"; import {SeenBlockInput} from "../../src/chain/seenCache/seenGossipBlockInput.js"; import {ShufflingCache} from "../../src/chain/shufflingCache.js"; import {ExecutionBuilderHttp} from "../../src/execution/builder/http.js"; @@ -28,6 +39,16 @@ export type MockedBeaconChain = Mocked & { aggregatedAttestationPool: Mocked; syncContributionAndProofPool: Mocked; beaconProposerCache: Mocked; + // Engine-internal seen-caches surfaced on the flat mock (the mock doubles as the engine). + seenAttesters: SeenAttesters; + seenAggregators: SeenAggregators; + seenPayloadAttesters: SeenPayloadAttesters; + seenAggregatedAttestations: SeenAggregatedAttestations; + seenExecutionPayloadBids: SeenExecutionPayloadBids; + seenBlockProposers: SeenBlockProposers; + seenSyncCommitteeMessages: SeenSyncCommitteeMessages; + seenContributionAndProof: SeenContributionAndProof; + seenAttestationDatas: SeenAttestationDatas; seenBlockInputCache: Mocked; shufflingCache: Mocked; regen: Mocked; @@ -147,6 +168,17 @@ vi.mock("../../src/chain/chain.js", async (importActual) => { opPool: new OpPool(config as BeaconConfig), aggregatedAttestationPool: new AggregatedAttestationPool(config as BeaconConfig), syncContributionAndProofPool: new SyncContributionAndProofPool(config, clock), + // Engine-internal gossip seen-caches; the flat mock doubles as the engine, so validators read + // these via `engine.seenX` and tests reach them via `chain.beaconEngine.seenX`. + seenAttesters: new SeenAttesters(), + seenAggregators: new SeenAggregators(), + seenPayloadAttesters: new SeenPayloadAttesters(), + seenAggregatedAttestations: new SeenAggregatedAttestations(null), + seenExecutionPayloadBids: new SeenExecutionPayloadBids(), + seenBlockProposers: new SeenBlockProposers(), + seenSyncCommitteeMessages: new SeenSyncCommitteeMessages(), + seenContributionAndProof: new SeenContributionAndProof(null), + seenAttestationDatas: new SeenAttestationDatas(null), payloadAttestationPool: { add: vi.fn(), getAll: vi.fn(), @@ -236,6 +268,22 @@ export function getMockedBeaconChain(opts?: Partial): "resolvePayloadAttributesInput", "getPayloadAttributesForSSE", "getProposerTargetGasLimit", + // Proposer cache + finalized balances bridges (forward to the mock's collaborators). + "getProposerFeeRecipient", + "updateProposerPreparation", + "getProposerCacheValidatorIndices", + "getCheckpointEffectiveBalances", + // Op-pool add/read bridges (forward to the mock's pools). + "addAttestationToPool", + "getAttestationAggregate", + "addAggregatedAttestation", + "getPoolAggregatedAttestations", + "addSyncCommitteeMessage", + "getSyncCommitteeContribution", + "addSyncContributionAndProof", + "addPayloadAttestation", + "getPoolPayloadAttestations", + "addExecutionPayloadBid", ] as const) { (chain as unknown as Record)[name] = ( BeaconEngine.prototype as unknown as Record diff --git a/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts index c73b124958db..b9266d3e26a8 100644 --- a/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts +++ b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts @@ -108,6 +108,8 @@ describe("produceBlockBody", () => { parentBlock: head, proposerIndex, proposerPubKey, + defaultFeeRecipient: "0x0000000000000000000000000000000000000000", + feeRecipientCached: false, commonBlockBodyPromise, safeBlockHash: parentHashHex, finalizedBlockHash: parentHashHex, diff --git a/packages/beacon-node/test/spec/utils/gossipValidation.ts b/packages/beacon-node/test/spec/utils/gossipValidation.ts index 1a9fbaa47c34..a18220a45c49 100644 --- a/packages/beacon-node/test/spec/utils/gossipValidation.ts +++ b/packages/beacon-node/test/spec/utils/gossipValidation.ts @@ -36,10 +36,6 @@ import {BeaconChain, ChainEvent} from "../../../src/chain/index.js"; import {defaultChainOptions} from "../../../src/chain/options.js"; import {validateGossipAggregateAndProof} from "../../../src/chain/validation/aggregateAndProof.js"; import {GossipAttestation, validateGossipAttestationsSameAttData} from "../../../src/chain/validation/attestation.js"; -import {validateGossipAttesterSlashing} from "../../../src/chain/validation/attesterSlashing.js"; -import {validateGossipBlsToExecutionChange} from "../../../src/chain/validation/blsToExecutionChange.js"; -import {validateGossipProposerSlashing} from "../../../src/chain/validation/proposerSlashing.js"; -import {validateGossipVoluntaryExit} from "../../../src/chain/validation/voluntaryExit.js"; import {ZERO_HASH_HEX} from "../../../src/constants/constants.js"; import {ExecutionEngineMockBackend} from "../../../src/execution/engine/mock.js"; import {getExecutionEngineFromBackend} from "../../../src/execution/index.js"; @@ -621,8 +617,8 @@ async function validateMessageForTopic( throw new GossipActionError(GossipAction.REJECT, {code: "SPEC_FINALIZED_NOT_ANCESTOR"}); } + // validateGossipBlock registers the proposer in the (engine-internal) seenBlockProposers on Accept. assertAccepted(await chain.beaconEngine.validateGossipBlock(bytes, signedBlock, fork)); - chain.seenBlockProposers.add(signedBlock.message.slot, signedBlock.message.proposerIndex); break; } @@ -684,26 +680,21 @@ async function validateMessageForTopic( case GossipType.proposer_slashing: { const slashing = rejectOnInvalidSerializedBytes(() => sszTypesFor(fork).ProposerSlashing.deserialize(bytes)); - await validateGossipProposerSlashing(chain, slashing); - // Mirror gossip handler: insert into opPool so duplicate detection works - chain.opPool.insertProposerSlashing(slashing); + // Engine validates + inserts into its (internal) opPool (mirrors the gossip handler). + assertAccepted(await chain.beaconEngine.validateGossipProposerSlashing(slashing)); break; } case GossipType.attester_slashing: { const slashing = rejectOnInvalidSerializedBytes(() => sszTypesFor(fork).AttesterSlashing.deserialize(bytes)); - await validateGossipAttesterSlashing(chain, slashing); - // Mirror gossip handler: insert into opPool + fork choice - chain.opPool.insertAttesterSlashing(fork, slashing); - chain.beaconEngine.forkChoice.onAttesterSlashing(slashing); + // Engine validates + inserts into opPool + fork choice (mirrors the gossip handler). + assertAccepted(await chain.beaconEngine.validateGossipAttesterSlashing(slashing, fork)); break; } case GossipType.voluntary_exit: { const exit = rejectOnInvalidSerializedBytes(() => sszTypesFor(fork).SignedVoluntaryExit.deserialize(bytes)); - await validateGossipVoluntaryExit(chain, exit); - // Mirror gossip handler: insert into opPool so duplicate detection works - chain.opPool.insertVoluntaryExit(exit); + assertAccepted(await chain.beaconEngine.validateGossipVoluntaryExit(exit)); break; } @@ -738,9 +729,8 @@ async function validateMessageForTopic( if (chain.clock.currentEpoch < chain.config.CAPELLA_FORK_EPOCH) { throw new GossipActionError(GossipAction.IGNORE, {code: "SPEC_PRE_CAPELLA"}); } - await validateGossipBlsToExecutionChange(chain, blsToExecutionChange); - // Mirror gossip handler: insert into opPool so duplicate detection works - chain.opPool.insertBlsToExecutionChange(blsToExecutionChange); + // Engine validates + inserts into its (internal) opPool (mirrors the gossip handler). + assertAccepted(await chain.beaconEngine.validateGossipBlsToExecutionChange(blsToExecutionChange)); break; } diff --git a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts index 981ac2796b94..796b5912382c 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts @@ -283,6 +283,9 @@ describe("api/validator - produceBlockV3", () => { // produceBlockBody now reads all inputs from the prepared scalars (no BeaconState). These mirror the // values produceBlockBase would derive for this bellatrix state. const preparedScalars = { + // produceBlockBase resolves the default fee recipient; the API-requested one (call 1) overrides it. + defaultFeeRecipient: "0x fee recipient address", + feeRecipientCached: false, safeBlockHash: ZERO_HASH_HEX, finalizedBlockHash: ZERO_HASH_HEX, timestamp: computeTimeAtSlot(modules.config, state.slot, state.genesisTime), @@ -320,9 +323,7 @@ describe("api/validator - produceBlockV3", () => { } ); - // use fee recipient set in beaconProposerCacheStub if none passed - modules.chain["beaconProposerCache"].getOrDefault.mockReturnValue("0x fee recipient address"); - + // no fee recipient passed → falls back to the prepared defaultFeeRecipient (resolved by produceBlockBase) await produceBlockBody.call(modules.chain as unknown as BeaconChain, BlockType.Full, { randaoReveal, graffiti: toGraffitiBytes(graffiti), diff --git a/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts b/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts index 4dbbcdd98cb0..769bd626bb94 100644 --- a/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts @@ -33,7 +33,7 @@ describe("GossipMessageValidator", () => { opPool.hasSeenAttesterSlashing.mockReturnValue(true); await expectRejectedWithLodestarError( - validateGossipAttesterSlashing(chainStub, attesterSlashing), + validateGossipAttesterSlashing(chainStub.beaconEngine, attesterSlashing), AttesterSlashingErrorCode.ALREADY_EXISTS ); }); @@ -42,7 +42,7 @@ describe("GossipMessageValidator", () => { const attesterSlashing = ssz.phase0.AttesterSlashing.defaultValue(); await expectRejectedWithLodestarError( - validateGossipAttesterSlashing(chainStub, attesterSlashing), + validateGossipAttesterSlashing(chainStub.beaconEngine, attesterSlashing), AttesterSlashingErrorCode.INVALID ); }); @@ -62,7 +62,7 @@ describe("GossipMessageValidator", () => { }, }; - await validateGossipAttesterSlashing(chainStub, attesterSlashing); + await validateGossipAttesterSlashing(chainStub.beaconEngine, attesterSlashing); }); }); }); 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 a2084cb70561..b5251217bab6 100644 --- a/packages/beacon-node/test/unit/chain/validation/block.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/block.test.ts @@ -58,7 +58,7 @@ describe("gossip block validation", () => { rootHex: "", }); - // Reset seen cache + // Reset seen cache (engine-internal; the flat mock doubles as the engine) ( chain as { seenBlockProposers: SeenBlockProposers; diff --git a/packages/beacon-node/test/unit/chain/validation/blsToExecutionChange.test.ts b/packages/beacon-node/test/unit/chain/validation/blsToExecutionChange.test.ts index 277c8176aab1..34fb87d3f80d 100644 --- a/packages/beacon-node/test/unit/chain/validation/blsToExecutionChange.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/blsToExecutionChange.test.ts @@ -104,13 +104,13 @@ describe("validate bls to execution change", () => { opPool.hasSeenBlsToExecutionChange.mockReturnValue(true); await expectRejectedWithLodestarError( - validateGossipBlsToExecutionChange(chainStub, signedBlsToExecChangeInvalid), + validateGossipBlsToExecutionChange(chainStub.beaconEngine, signedBlsToExecChangeInvalid), BlsToExecutionChangeErrorCode.ALREADY_EXISTS ); }); it("should return valid blsToExecutionChange ", async () => { - await validateGossipBlsToExecutionChange(chainStub, signedBlsToExecChange); + await validateGossipBlsToExecutionChange(chainStub.beaconEngine, signedBlsToExecChange); }); it("should return invalid bls to execution Change - invalid validatorIndex", async () => { @@ -124,7 +124,7 @@ describe("validate bls to execution change", () => { }; await expectRejectedWithLodestarError( - validateGossipBlsToExecutionChange(chainStub, signedBlsToExecChangeInvalid), + validateGossipBlsToExecutionChange(chainStub.beaconEngine, signedBlsToExecChangeInvalid), BlsToExecutionChangeErrorCode.INVALID ); }); @@ -139,7 +139,7 @@ describe("validate bls to execution change", () => { }; await expectRejectedWithLodestarError( - validateGossipBlsToExecutionChange(chainStub, signedBlsToExecChangeInvalid), + validateGossipBlsToExecutionChange(chainStub.beaconEngine, signedBlsToExecChangeInvalid), BlsToExecutionChangeErrorCode.INVALID ); }); @@ -155,7 +155,7 @@ describe("validate bls to execution change", () => { }; await expectRejectedWithLodestarError( - validateGossipBlsToExecutionChange(chainStub, signedBlsToExecChangeInvalid), + validateGossipBlsToExecutionChange(chainStub.beaconEngine, signedBlsToExecChangeInvalid), BlsToExecutionChangeErrorCode.INVALID ); }); @@ -171,7 +171,7 @@ describe("validate bls to execution change", () => { }; await expectRejectedWithLodestarError( - validateGossipBlsToExecutionChange(chainStub, signedBlsToExecChangeInvalid), + validateGossipBlsToExecutionChange(chainStub.beaconEngine, signedBlsToExecChangeInvalid), BlsToExecutionChangeErrorCode.INVALID ); }); diff --git a/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts b/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts index 76c1886f143a..ed048b7761ff 100644 --- a/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts @@ -32,7 +32,7 @@ describe("validate proposer slashing", () => { opPool.hasSeenProposerSlashing.mockReturnValue(true); await expectRejectedWithLodestarError( - validateGossipProposerSlashing(chainStub, proposerSlashing), + validateGossipProposerSlashing(chainStub.beaconEngine, proposerSlashing), ProposerSlashingErrorCode.ALREADY_EXISTS ); }); @@ -44,7 +44,7 @@ describe("validate proposer slashing", () => { proposerSlashing.signedHeader2.message.slot = BigInt(0); await expectRejectedWithLodestarError( - validateGossipProposerSlashing(chainStub, proposerSlashing), + validateGossipProposerSlashing(chainStub.beaconEngine, proposerSlashing), ProposerSlashingErrorCode.INVALID ); }); @@ -60,6 +60,6 @@ describe("validate proposer slashing", () => { signedHeader2: signedHeader2, }; - await validateGossipProposerSlashing(chainStub, proposerSlashing); + await validateGossipProposerSlashing(chainStub.beaconEngine, proposerSlashing); }); }); diff --git a/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts b/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts index 5f42d14aa97e..4d7728f04a70 100644 --- a/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts @@ -39,11 +39,8 @@ describe("Sync Committee Signature validation", () => { beforeEach(() => { chain = getMockedBeaconChain({config}); - ( - chain as { - seenSyncCommitteeMessages: SeenSyncCommitteeMessages; - } - ).seenSyncCommitteeMessages = new SeenSyncCommitteeMessages(); + (chain as {seenSyncCommitteeMessages: SeenSyncCommitteeMessages}).seenSyncCommitteeMessages = + new SeenSyncCommitteeMessages(); clockStub = chain.clock; forkchoiceStub = chain.forkChoice; vi.spyOn(clockStub, "isCurrentSlotGivenGossipDisparity").mockReturnValue(true); diff --git a/packages/beacon-node/test/unit/chain/validation/voluntaryExit.test.ts b/packages/beacon-node/test/unit/chain/validation/voluntaryExit.test.ts index 55436077cb35..ede60d531f55 100644 --- a/packages/beacon-node/test/unit/chain/validation/voluntaryExit.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/voluntaryExit.test.ts @@ -88,7 +88,7 @@ describe("validate voluntary exit", () => { opPool.hasSeenVoluntaryExit.mockReturnValue(true); await expectRejectedWithLodestarError( - validateGossipVoluntaryExit(chainStub, signedVoluntaryExitInvalidSig), + validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExitInvalidSig), VoluntaryExitErrorCode.ALREADY_EXISTS ); }); @@ -104,7 +104,7 @@ describe("validate voluntary exit", () => { }; await expectRejectedWithLodestarError( - validateGossipVoluntaryExit(chainStub, signedVoluntaryExitInvalid), + validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExitInvalid), VoluntaryExitErrorCode.EARLY_EPOCH ); }); @@ -120,7 +120,7 @@ describe("validate voluntary exit", () => { vi.spyOn(chainStub, "getHeadStateAtCurrentEpoch").mockResolvedValue(state); await expectRejectedWithLodestarError( - validateGossipVoluntaryExit(chainStub, signedVoluntaryExit), + validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExit), VoluntaryExitErrorCode.INACTIVE ); }); @@ -138,7 +138,7 @@ describe("validate voluntary exit", () => { vi.spyOn(chainStub, "getHeadStateAtCurrentEpoch").mockResolvedValue(state); await expectRejectedWithLodestarError( - validateGossipVoluntaryExit(chainStub, signedVoluntaryExit), + validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExit), VoluntaryExitErrorCode.ALREADY_EXITED ); }); @@ -154,7 +154,7 @@ describe("validate voluntary exit", () => { vi.spyOn(chainStub, "getHeadStateAtCurrentEpoch").mockResolvedValue(state); await expectRejectedWithLodestarError( - validateGossipVoluntaryExit(chainStub, signedVoluntaryExit), + validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExit), VoluntaryExitErrorCode.SHORT_TIME_ACTIVE ); }); @@ -170,12 +170,12 @@ describe("validate voluntary exit", () => { chainStub.bls.verifySignatureSets.mockResolvedValue(false); await expectRejectedWithLodestarError( - validateGossipVoluntaryExit(chainStub, signedVoluntaryExitInvalidSig), + validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExitInvalidSig), VoluntaryExitErrorCode.INVALID_SIGNATURE ); }); it("should return valid Voluntary Exit", async () => { - await validateGossipVoluntaryExit(chainStub, signedVoluntaryExit); + await validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExit); }); }); diff --git a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts index 37f3de4360e0..213ad6dc04b9 100644 --- a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts +++ b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts @@ -453,7 +453,9 @@ describe("sync by UnknownBlockSync", {timeout: 20_000}, () => { if (blockRootHex === blockRootHexC) blockCResolver(); if (blockRootHex === blockRootHexA) blockAResolver(); }, - seenBlockProposers: seenBlockProposers as SeenBlockProposers, + beaconEngine: { + isBlockProposerSeen: (blockSlot: number, index: number) => seenBlockProposers.isKnown(blockSlot, index), + } as unknown as IBeaconChain["beaconEngine"], seenBlockInputCache: { getByBlock: ({ block, @@ -680,16 +682,16 @@ describe("UnknownBlockSync", () => { prune: vi.fn(), } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], seenBlockInputCache: {prune: vi.fn()} as unknown as SeenBlockInput, - seenBlockProposers: {isKnown: vi.fn().mockReturnValue(false)} as unknown as SeenBlockProposers, forkChoice: { hasPayloadHexUnsafe: vi.fn().mockReturnValue(false), hasBlockHex: vi.fn().mockReturnValue(false), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), } as unknown as IForkChoice, - // Envelope validation is now a BeaconEngine method; route it to the mocked module fn so the - // existing `validateGossipExecutionPayloadEnvelope` assertions still observe the calls. + // Envelope validation + seen-block-proposers are now BeaconEngine methods; route them to the + // mocked module fn / a stub so the existing assertions still observe the calls. beaconEngine: { validateGossipExecutionPayloadEnvelope, + isBlockProposerSeen: vi.fn().mockReturnValue(false), } as unknown as IBeaconChain["beaconEngine"], ...chainOverrides, } as IBeaconChain; @@ -1605,9 +1607,9 @@ describe("UnknownBlockSync", () => { prune: vi.fn(), } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], seenBlockInputCache: {prune: vi.fn()} as unknown as SeenBlockInput, - seenBlockProposers: { - isKnown: vi.fn().mockReturnValue(false), - } as unknown as SeenBlockProposers, + beaconEngine: { + isBlockProposerSeen: vi.fn().mockReturnValue(false), + } as unknown as IBeaconChain["beaconEngine"], }; service = new BlockInputSync(gloasConfig, network, chainForTest as IBeaconChain, logger, null, defaultSyncOptions); diff --git a/packages/beacon-node/test/utils/validationData/aggregateAndProof.ts b/packages/beacon-node/test/utils/validationData/aggregateAndProof.ts index be91ae47c35c..708d7f477642 100644 --- a/packages/beacon-node/test/utils/validationData/aggregateAndProof.ts +++ b/packages/beacon-node/test/utils/validationData/aggregateAndProof.ts @@ -2,10 +2,9 @@ import {DOMAIN_AGGREGATE_AND_PROOF, DOMAIN_SELECTION_PROOF} from "@lodestar/para import {computeSigningRoot} from "@lodestar/state-transition"; import {getSecretKeyFromIndexCached} from "@lodestar/state-transition/test-utils"; import {phase0, ssz} from "@lodestar/types"; -import {IBeaconChain} from "../../../src/chain/index.js"; import {SeenAggregators} from "../../../src/chain/seenCache/index.js"; import {signCached} from "../cache.js"; -import {AttestationValidDataOpts, getAttestationValidData} from "./attestation.js"; +import {AttestationValidDataOpts, ValidationTestChain, getAttestationValidData} from "./attestation.js"; export type AggregateAndProofValidDataOpts = AttestationValidDataOpts; @@ -13,7 +12,7 @@ export type AggregateAndProofValidDataOpts = AttestationValidDataOpts; * Generate a valid gossip SignedAggregateAndProof object. Common logic for unit and perf tests */ export function getAggregateAndProofValidData(opts: AggregateAndProofValidDataOpts): { - chain: IBeaconChain; + chain: ValidationTestChain; signedAggregateAndProof: phase0.SignedAggregateAndProof; validatorIndex: number; } { @@ -24,8 +23,8 @@ export function getAggregateAndProofValidData(opts: AggregateAndProofValidDataOp const sk = getSecretKeyFromIndexCached(validatorIndex); - // Get around the 'readonly' Typescript restriction - (chain as {seenAggregators: IBeaconChain["seenAggregators"]}).seenAggregators = new SeenAggregators(); + // Reset the (engine-internal) seenAggregators for this fixture + chain.seenAggregators = new SeenAggregators(); const aggregatorIndex = validatorIndex; const proofDomain = state.config.getDomain(state.slot, DOMAIN_SELECTION_PROOF, attSlot); diff --git a/packages/beacon-node/test/utils/validationData/attestation.ts b/packages/beacon-node/test/utils/validationData/attestation.ts index 8c161622d5eb..d737f6377858 100644 --- a/packages/beacon-node/test/utils/validationData/attestation.ts +++ b/packages/beacon-node/test/utils/validationData/attestation.ts @@ -18,7 +18,7 @@ import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier} from "../../../src/ch import {IBeaconChain} from "../../../src/chain/index.js"; import {defaultChainOptions} from "../../../src/chain/options.js"; import {IStateRegenerator} from "../../../src/chain/regen/index.js"; -import {SeenAttesters} from "../../../src/chain/seenCache/index.js"; +import {SeenAggregators, SeenAttesters} from "../../../src/chain/seenCache/index.js"; import {SeenAggregatedAttestations} from "../../../src/chain/seenCache/seenAggregateAndProof.js"; import {SeenAttestationDatas} from "../../../src/chain/seenCache/seenAttestationData.js"; import {ShufflingCache} from "../../../src/chain/shufflingCache.js"; @@ -40,8 +40,19 @@ export type AttestationValidDataOpts = { /** * Generate a valid gossip Attestation object. Common logic for unit and perf tests */ +/** + * `IBeaconChain` widened with the engine-internal seen-caches these fixtures surface (the flat mock + * doubles as the engine). Lets tests reach `chain.seenX` even though they are off `IBeaconChain`. + */ +export type ValidationTestChain = IBeaconChain & { + seenAttesters: SeenAttesters; + seenAggregators: SeenAggregators; + seenAggregatedAttestations: SeenAggregatedAttestations; + seenAttestationDatas: SeenAttestationDatas; +}; + export function getAttestationValidData(opts: AttestationValidDataOpts): { - chain: IBeaconChain; + chain: ValidationTestChain; attestation: phase0.Attestation; subnet: SubnetID; validatorIndex: number; @@ -167,6 +178,7 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { forkChoice, regen, seenAttesters: new SeenAttesters(), + seenAggregators: new SeenAggregators(), seenAggregatedAttestations: new SeenAggregatedAttestations(null), seenAttestationDatas: new SeenAttestationDatas(null, 0, 0), bls: blsVerifyAllMainThread @@ -179,7 +191,7 @@ export function getAttestationValidData(opts: AttestationValidDataOpts): { pubkeyCache: state.epochCtx.pubkeyCache, shufflingCache, opts: defaultChainOptions, - } as Partial as IBeaconChain; + } as Partial as ValidationTestChain; // The gossip validators are BeaconEngine methods reading engine collaborators; in this flat fixture // the facade doubles as the engine, so `chain.beaconEngine` exposes the same collaborators. (chain as {beaconEngine: unknown}).beaconEngine = chain; From d9b63e3cea9e82a9fcfbe2ca378965b02372c133 Mon Sep 17 00:00:00 2001 From: twoeths Date: Tue, 7 Jul 2026 13:39:35 +0700 Subject: [PATCH 17/24] fix: avoid calling forkchoice in BeaconChain.importBlock() --- .../src/chain/beaconEngine/beaconEngine.ts | 119 ++++++++++++++++++ .../src/chain/beaconEngine/interface.ts | 14 +++ .../src/chain/blocks/importBlock.ts | 93 ++------------ .../beacon-node/src/chain/validation/block.ts | 1 + .../src/network/processor/gossipHandlers.ts | 2 + 5 files changed, 148 insertions(+), 81 deletions(-) diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 09f9a88ded31..cd81171c1c70 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -11,6 +11,7 @@ import { ForkChoiceErrorCode, ForkChoiceStateGetter, IForkChoice, + NotReorgedReason, PayloadExecutionStatus, PayloadStatus, ProtoBlock, @@ -43,6 +44,7 @@ import { computeEpochAtSlot, computeStartSlotAtEpoch, computeTimeAtSlot, + isStartSlotOfEpoch, isStatePostAltair, isStatePostBellatrix, isStatePostCapella, @@ -200,6 +202,7 @@ import { } from "./gossipValidationResult.js"; import { BeaconEngineModules, + FcuUpdate, FinalizedProtoSummary, IBeaconEngine, ImportBlockResult, @@ -2475,6 +2478,21 @@ export class BeaconEngine implements IBeaconEngine { } } + // 6. Compute the FCU-override decision + notifyForkchoiceUpdate args engine-side; the facade fires + // the EL call (executionEngine is facade-owned) or skips based on `fcuUpdate`. + const fcuUpdate = this.computeImportFcuUpdate({ + isExecutionState, + blockSlot, + currentSlot, + blockRootHex, + blockSummary, + proposerIndexNextSlot, + newHeadBlockRoot: newHead.blockRoot, + oldHeadBlockRoot, + currFinalizedEpoch, + prevFinalizedEpoch, + }); + return { headChanged, head: headResult, @@ -2486,6 +2504,7 @@ export class BeaconEngine implements IBeaconEngine { currFinalizedEpoch, oldHeadBlockRoot, newHeadBlockRoot: newHead.blockRoot, + fcuUpdate, newHead, blockMeta: { slot: blockSlot, @@ -2497,6 +2516,106 @@ export class BeaconEngine implements IBeaconEngine { }; } + /** + * Decide whether to fire a forkchoice-update on the EL after importing a block, and if so return the + * notifyForkchoiceUpdate args. All fork-choice + proposer-cache reads are engine-internal; the facade + * only fires the EL call (or skips) from the returned data. Also emits the `notOverrideFcuReason` + * metric + weak/strong-block logs (engine owns metrics/logger). `disableImportExecutionFcU` stays a + * facade-side gate applied to the returned value. + * + * Returns `null` when there is nothing to fire: non-execution state, weak block (`shouldOverrideFcu`), + * no head/finalized change, or the head execution block hash is ZERO. + */ + private computeImportFcuUpdate(args: { + isExecutionState: boolean; + blockSlot: Slot; + currentSlot: Slot; + blockRootHex: RootHex; + blockSummary: ProtoBlock | null; + proposerIndexNextSlot: number | null; + newHeadBlockRoot: string; + oldHeadBlockRoot: string; + currFinalizedEpoch: number; + prevFinalizedEpoch: number; + }): FcuUpdate | null { + const { + isExecutionState, + blockSlot, + currentSlot, + blockRootHex, + blockSummary, + proposerIndexNextSlot, + newHeadBlockRoot, + oldHeadBlockRoot, + currFinalizedEpoch, + prevFinalizedEpoch, + } = args; + + let shouldOverrideFcu = false; + + if (isExecutionState && blockSlot >= currentSlot) { + let notOverrideFcuReason = NotReorgedReason.Unknown; + const proposalSlot = blockSlot + 1; + try { + if (proposerIndexNextSlot !== null) { + const feeRecipient = this.getProposerFeeRecipient(proposerIndexNextSlot); + if (feeRecipient && blockSummary !== null) { + const result = this.forkChoice.shouldOverrideForkChoiceUpdate( + blockSummary, + this.clock.secFromSlot(currentSlot), + currentSlot + ); + shouldOverrideFcu = result.shouldOverrideFcu; + if (!result.shouldOverrideFcu) { + notOverrideFcuReason = result.reason; + } + } else { + notOverrideFcuReason = NotReorgedReason.NotProposerOfNextSlot; + } + } else { + if (isStartSlotOfEpoch(proposalSlot)) { + notOverrideFcuReason = NotReorgedReason.NotShufflingStable; + } + } + } catch (e) { + if (isStartSlotOfEpoch(proposalSlot)) { + notOverrideFcuReason = NotReorgedReason.NotShufflingStable; + } else { + this.logger.warn("Unable to get beacon proposer. Do not override fcu.", {proposalSlot}, e as Error); + } + } + + if (shouldOverrideFcu) { + this.logger.verbose("Weak block detected. Skip fcu call in importBlock", { + blockRoot: blockRootHex, + slot: blockSlot, + }); + } else { + this.metrics?.importBlock.notOverrideFcuReason.inc({reason: notOverrideFcuReason}); + this.logger.verbose("Strong block detected. Not override fcu call", { + blockRoot: blockRootHex, + slot: blockSlot, + reason: notOverrideFcuReason, + }); + } + } + + if ((newHeadBlockRoot !== oldHeadBlockRoot || currFinalizedEpoch !== prevFinalizedEpoch) && !shouldOverrideFcu) { + const head = this.forkChoice.getHead(); + const headBlockHash = head.executionPayloadBlockHash ?? ZERO_HASH_HEX; + if (headBlockHash !== ZERO_HASH_HEX) { + return { + fork: this.config.getForkName(head.slot), + headBlockHash, + safeBlockHash: getSafeExecutionBlockHash(this.forkChoice), + finalizedBlockHash: this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX, + }; + } + } + + return null; + } + private addAttestationPreElectra( _state: IBeaconStateView, target: phase0.Checkpoint, diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 85765c8a4bf9..ac2be7b21b30 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -86,6 +86,18 @@ export type BeaconEngineModules = { lightClientServer?: LightClientServer; }; +/** + * FCU side-effect for the facade to fire after `importBlock` / `importExecutionPayload`. The engine + * computes the override decision + all hashes internally (reads its own fork choice + proposer cache); + * the facade only fires `executionEngine.notifyForkchoiceUpdate` (EL is facade-owned). + */ +export type FcuUpdate = { + fork: ForkName; + headBlockHash: RootHex; + safeBlockHash: RootHex; + finalizedBlockHash: RootHex; +}; + export type ImportBlockResult = { headChanged: boolean; head: { @@ -111,6 +123,8 @@ export type ImportBlockResult = { currFinalizedEpoch: number; oldHeadBlockRoot: string; newHeadBlockRoot: string; + /** Post-import FCU decision computed engine-side; facade fires `notifyForkchoiceUpdate` from it (or skips). */ + fcuUpdate: FcuUpdate | null; blockMeta: { slot: number; blockRootHex: string; diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 38927f5fe19e..e28038a12a30 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -1,14 +1,8 @@ import {routes} from "@lodestar/api"; -import { - BlockExecutionStatus, - NotReorgedReason, - PayloadExecutionStatus, - getSafeExecutionBlockHash, -} from "@lodestar/fork-choice"; -import {DataAvailabilityStatus, isStartSlotOfEpoch} from "@lodestar/state-transition"; +import {BlockExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice"; +import {DataAvailabilityStatus} from "@lodestar/state-transition"; import {capella} from "@lodestar/types"; import {fromHex, isErrorAborted} from "@lodestar/utils"; -import {ZERO_HASH_HEX} from "../../constants/index.js"; import {callInNextEventLoop} from "../../util/eventLoop.js"; import {isOptimisticBlock} from "../../util/forkChoice.js"; import {isQueueErrorAborted} from "../../util/queue/index.js"; @@ -77,80 +71,17 @@ export async function importBlock( // The engine no longer emits these (FFI-honest); they are derived facade-side from the head ProtoBlock. this.updateHeadAndEmitCheckpointEvents(r.newHead); - // 6. Queue notifyForkchoiceUpdate to engine api - let shouldOverrideFcu = false; - - if (r.isExecutionState && r.blockMeta.slot >= this.forkChoice.getTime()) { - let notOverrideFcuReason = NotReorgedReason.Unknown; - const proposalSlot = r.blockMeta.slot + 1; - try { - const proposerIndex = r.proposerIndexNextSlot; - if (proposerIndex !== null) { - // TODO - beacon engine: move this there - const feeRecipient = this.beaconEngine.getProposerFeeRecipient(proposerIndex); - if (feeRecipient && r.blockSummary !== null) { - const result = this.forkChoice.shouldOverrideForkChoiceUpdate( - r.blockSummary, - this.clock.secFromSlot(this.forkChoice.getTime()), - this.forkChoice.getTime() - ); - shouldOverrideFcu = result.shouldOverrideFcu; - if (!result.shouldOverrideFcu) { - notOverrideFcuReason = result.reason; - } - } else { - notOverrideFcuReason = NotReorgedReason.NotProposerOfNextSlot; - } - } else { - if (isStartSlotOfEpoch(proposalSlot)) { - notOverrideFcuReason = NotReorgedReason.NotShufflingStable; - } + // 6. Fire notifyForkchoiceUpdate on the EL. The engine computed the override decision + all hashes + // internally (fork choice + proposer cache are engine-owned) and returned them as `r.fcuUpdate`; + // the facade only fires the EL call (executionEngine is facade-owned) or skips. `disableImportExecutionFcU` + // stays a facade-side gate. + if (!this.opts.disableImportExecutionFcU && r.fcuUpdate !== null) { + const {fork, headBlockHash, safeBlockHash, finalizedBlockHash} = r.fcuUpdate; + this.executionEngine.notifyForkchoiceUpdate(fork, headBlockHash, safeBlockHash, finalizedBlockHash).catch((e) => { + if (!isErrorAborted(e) && !isQueueErrorAborted(e)) { + this.logger.error("Error pushing notifyForkchoiceUpdate()", {headBlockHash, finalizedBlockHash}, e); } - } catch (e) { - if (isStartSlotOfEpoch(proposalSlot)) { - notOverrideFcuReason = NotReorgedReason.NotShufflingStable; - } else { - this.logger.warn("Unable to get beacon proposer. Do not override fcu.", {proposalSlot}, e as Error); - } - } - - if (shouldOverrideFcu) { - this.logger.verbose("Weak block detected. Skip fcu call in importBlock", { - blockRoot: r.blockMeta.blockRootHex, - slot: r.blockMeta.slot, - }); - } else { - this.metrics?.importBlock.notOverrideFcuReason.inc({reason: notOverrideFcuReason}); - this.logger.verbose("Strong block detected. Not override fcu call", { - blockRoot: r.blockMeta.blockRootHex, - slot: r.blockMeta.slot, - reason: notOverrideFcuReason, - }); - } - } - - if ( - !this.opts.disableImportExecutionFcU && - (r.newHeadBlockRoot !== r.oldHeadBlockRoot || r.currFinalizedEpoch !== r.prevFinalizedEpoch) && - !shouldOverrideFcu - ) { - const headBlockHash = this.forkChoice.getHead().executionPayloadBlockHash ?? ZERO_HASH_HEX; - const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); - const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; - if (headBlockHash !== ZERO_HASH_HEX) { - this.executionEngine - .notifyForkchoiceUpdate( - this.config.getForkName(this.forkChoice.getHead().slot), - headBlockHash, - safeBlockHash, - finalizedBlockHash - ) - .catch((e) => { - if (!isErrorAborted(e) && !isQueueErrorAborted(e)) { - this.logger.error("Error pushing notifyForkchoiceUpdate()", {headBlockHash, finalizedBlockHash}, e); - } - }); - } + }); } const blockRootHex = r.blockMeta.blockRootHex; diff --git a/packages/beacon-node/src/chain/validation/block.ts b/packages/beacon-node/src/chain/validation/block.ts index 658414dbb109..3e42c9f909b8 100644 --- a/packages/beacon-node/src/chain/validation/block.ts +++ b/packages/beacon-node/src/chain/validation/block.ts @@ -20,6 +20,7 @@ export type GossipBlockValidationResult = { }; export async function validateGossipBlock( + // TODO - beacon engine: use "this" engine: BeaconEngine, signedBlock: SignedBeaconBlock, fork: ForkName diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 5981a27e4782..87ebd4efe043 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -888,6 +888,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand if (!options.dontSendGossipAttestationsToForkchoice) { try { + // TODO beacon-engine: move to validateGossipAggregateAndProof chain.beaconEngine.forkChoice.onAttestation(indexedAttestation, attDataRootHex); } catch (e) { logger.debug( @@ -1164,6 +1165,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand if (res.status !== GossipValidationStatus.Accept) return res; const {attDataRootHex, validatorCommitteeIndices} = res.value; + // TODO beacon engine: move to BeaconEngine, check other gossip handlers too try { const insertOutcome = chain.beaconEngine.addPayloadAttestation( payloadAttestationMessage, From 2ae61ae172ee3c5cb2ae37067d72390c699c601f Mon Sep 17 00:00:00 2001 From: twoeths Date: Tue, 7 Jul 2026 14:11:36 +0700 Subject: [PATCH 18/24] feat: move importExecutionPayload to BeconEngine --- .../src/chain/beaconEngine/beaconEngine.ts | 115 +++++++++++ .../src/chain/beaconEngine/interface.ts | 20 ++ .../chain/blocks/importExecutionPayload.ts | 193 +++++------------- .../src/chain/blocks/payloadError.ts | 46 +++++ .../src/metrics/metrics/lodestar.ts | 2 +- .../src/network/processor/gossipHandlers.ts | 2 +- packages/beacon-node/src/sync/unknownBlock.ts | 2 +- .../test/unit/sync/unknownBlock.test.ts | 2 +- 8 files changed, 234 insertions(+), 148 deletions(-) create mode 100644 packages/beacon-node/src/chain/blocks/payloadError.ts diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index cd81171c1c70..de614e11cc61 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -104,10 +104,15 @@ import {BeaconProposerCache, ProposerPreparationData} from "../beaconProposerCac import {isBlockInputBlobs, isBlockInputColumns} from "../blocks/blockInput/blockInput.js"; import {IBlockInput} from "../blocks/blockInput/index.js"; import {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; +import {PayloadError, PayloadErrorCode} from "../blocks/payloadError.js"; import {AttestationImportOpt, ImportBlockOpts} from "../blocks/types.js"; import {getCheckpointFromState} from "../blocks/utils/checkpoint.js"; import {verifyBlocksSignatures} from "../blocks/verifyBlocksSignatures.js"; import {verifyBlocksStateTransitionOnly} from "../blocks/verifyBlocksStateTransitionOnly.js"; +import { + verifyExecutionPayloadEnvelope as verifyExecutionPayloadEnvelopeFields, + verifyExecutionPayloadEnvelopeSignature, +} from "../blocks/verifyExecutionPayloadEnvelope.js"; import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "../bls/index.js"; import {ChainEvent, ChainEventEmitter, ReorgEventData} from "../emitter.js"; import {BlockError, BlockErrorCode} from "../errors/index.js"; @@ -2127,6 +2132,116 @@ export class BeaconEngine implements IBeaconEngine { } } + /** + * Verify a gloas execution payload envelope before importing it: resolve the block's state (regen), + * check the envelope fields against it, and verify the BLS signature (skipped when gossip/API already + * did — `opts.validSignature`). Consensus-only; the EL `notifyNewPayload` runs facade-side in parallel + * with this (mirrors the block pipeline's state/sig ∥ EL split). Throws `PayloadError` on failure. + * + * `_envelopeBytes` is the SSZ bytes of `signedEnvelope` — the JS engine works off the POJO and ignores + * it; the native engine deserializes from it (bytes-first FFI, `(bytes, pojo)` seam, like `verifyBlocks`). + */ + async verifyExecutionPayloadEnvelope( + _envelopeBytes: Uint8Array, + signedEnvelope: gloas.SignedExecutionPayloadEnvelope, + proposerIndex: ValidatorIndex, + opts: {validSignature: boolean} + ): Promise { + const envelope = signedEnvelope.message; + const blockRootHex = toRootHex(envelope.beaconBlockRoot); + + // Get the ProtoBlock for the block whose payload this envelope carries. + const protoBlock = this.forkChoice.getBlockHexDefaultStatus(blockRootHex); + if (!protoBlock) { + throw new PayloadError({code: PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE, blockRootHex}); + } + + // Regenerate state for envelope verification. + const blockState = await this.regen + .getBlockSlotState(protoBlock, protoBlock.slot, {dontTransferCache: true}, RegenCaller.importExecutionPayload) + .catch(() => + // only happen at the 1st batch of skipped slot checkpoint sync + this.regen.getClosestHeadState(protoBlock) + ); + + if (blockState == null) { + throw new PayloadError({code: PayloadErrorCode.MISS_BLOCK_STATE, blockRootHex: protoBlock.blockRoot}); + } + if (!isStatePostGloas(blockState)) { + throw new PayloadError({ + code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR, + message: `Expected gloas+ state for payload import, got fork=${blockState.forkName}`, + }); + } + + // Verify envelope fields against state. When validSignature is true, gossip/API already verified + // both the signature and the executionRequestsRoot, so we skip those checks here. + try { + verifyExecutionPayloadEnvelopeFields(this.config, blockState, envelope, { + verifyExecutionRequestsRoot: !opts.validSignature, + }); + } catch (e) { + throw new PayloadError( + {code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR, message: (e as Error).message}, + `Envelope verification error: ${(e as Error).message}` + ); + } + + if (opts.validSignature !== true) { + const signatureValid = await verifyExecutionPayloadEnvelopeSignature( + this.config, + blockState, + this.pubkeyCache, + signedEnvelope, + proposerIndex, + this.bls + ); + if (!signatureValid) { + throw new PayloadError({code: PayloadErrorCode.INVALID_SIGNATURE}); + } + } + } + + /** + * Import a verified execution payload envelope into fork choice (transitions the block's PENDING + * variant to FULL) and compute the post-import FCU decision. The facade fires the EL calls + * (`notifyNewPayload` before this, `notifyForkchoiceUpdate` from the returned `fcuUpdate`) and emits + * events. `execStatus` is the EL result mapped facade-side; `blockRoot` is bytes-first (native FFI). + */ + importExecutionPayload( + blockRoot: Root, + blockHashHex: RootHex, + payloadBlockNumber: number, + payloadGasLimit: number, + execStatus: PayloadExecutionStatus, + dataAvailabilityStatus: DataAvailabilityStatus + ): {fcuUpdate: FcuUpdate | null; executionOptimistic: boolean} { + const blockRootHex = toRootHex(blockRoot); + + this.forkChoice.onExecutionPayload( + blockRootHex, + blockHashHex, + payloadBlockNumber, + payloadGasLimit, + execStatus, + dataAvailabilityStatus + ); + + // Fire the FCU only when this payload's block is the canonical head. + const head = this.forkChoice.getHead(); + let fcuUpdate: FcuUpdate | null = null; + if (blockRootHex === head.blockRoot) { + fcuUpdate = { + fork: this.config.getForkName(head.slot), + headBlockHash: blockHashHex, + safeBlockHash: getSafeExecutionBlockHash(this.forkChoice), + finalizedBlockHash: this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX, + }; + } + + return {fcuUpdate, executionOptimistic: execStatus === ExecutionStatus.Syncing}; + } + private async _importBlock( v: VerifiedBlockBundle, executionStatus: BlockExecutionStatus | PayloadExecutionStatus, diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index ac2be7b21b30..3ef964b39d74 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -519,6 +519,26 @@ export interface IBeaconEngine { ): Promise; discardVerifiedBlocks(blockRootHexes: RootHex[]): void; + // Execution payload envelope (gloas) import — consensus body of the facade `importExecutionPayload`. + // `verifyExecutionPayloadEnvelope` (regen state + fields + BLS sig, throws `PayloadError`) runs + // facade-parallel to the EL `notifyNewPayload`; `importExecutionPayload` does the fork-choice write + + // FCU decision. The facade fires the EL calls + emits events. Bytes-first (native FFI): `envelopeBytes` + // is the SSZ envelope (JS engine ignores it, uses the pojo); `blockRoot` is the SSZ root as raw bytes. + verifyExecutionPayloadEnvelope( + envelopeBytes: Uint8Array, + signedEnvelope: gloas.SignedExecutionPayloadEnvelope, + proposerIndex: ValidatorIndex, + opts: {validSignature: boolean} + ): Promise; + importExecutionPayload( + blockRoot: Root, + blockHashHex: RootHex, + payloadBlockNumber: number, + payloadGasLimit: number, + execStatus: PayloadExecutionStatus, + dataAvailabilityStatus: DataAvailabilityStatus + ): {fcuUpdate: FcuUpdate | null; executionOptimistic: boolean}; + // --- DB ownership (blocks + states) --- /** diff --git a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts index f1a592ef8579..057df2b5f12e 100644 --- a/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts +++ b/packages/beacon-node/src/chain/blocks/importExecutionPayload.ts @@ -1,67 +1,18 @@ import {routes} from "@lodestar/api"; -import {ExecutionStatus, PayloadExecutionStatus, getSafeExecutionBlockHash} from "@lodestar/fork-choice"; -import {DataAvailabilityStatus, isStatePostGloas} from "@lodestar/state-transition"; -import {isErrorAborted} from "@lodestar/utils"; -import {ZERO_HASH_HEX} from "../../constants/index.js"; +import {ExecutionStatus, PayloadExecutionStatus} from "@lodestar/fork-choice"; +import {DataAvailabilityStatus} from "@lodestar/state-transition"; +import {ssz} from "@lodestar/types"; +import {fromHex, isErrorAborted} from "@lodestar/utils"; import {ExecutionPayloadStatus} from "../../execution/index.js"; import {isQueueErrorAborted} from "../../util/queue/index.js"; import {BeaconChain} from "../chain.js"; -import {RegenCaller} from "../regen/interface.js"; import {PayloadEnvelopeInput} from "../seenCache/seenPayloadEnvelopeInput.js"; +import {PayloadError, PayloadErrorCode} from "./payloadError.js"; import {ImportPayloadOpts} from "./types.js"; -import { - verifyExecutionPayloadEnvelope, - verifyExecutionPayloadEnvelopeSignature, -} from "./verifyExecutionPayloadEnvelope.js"; import {verifyPayloadsDataAvailability} from "./verifyPayloadsDataAvailability.js"; const EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS = 64; -export enum PayloadErrorCode { - EXECUTION_ENGINE_INVALID = "PAYLOAD_ERROR_EXECUTION_ENGINE_INVALID", - EXECUTION_ENGINE_ERROR = "PAYLOAD_ERROR_EXECUTION_ENGINE_ERROR", - BLOCK_NOT_IN_FORK_CHOICE = "PAYLOAD_ERROR_BLOCK_NOT_IN_FORK_CHOICE", - MISS_BLOCK_STATE = "PAYLOAD_ERROR_MISS_BLOCK_STATE", - ENVELOPE_VERIFICATION_ERROR = "PAYLOAD_ERROR_ENVELOPE_VERIFICATION_ERROR", - INVALID_SIGNATURE = "PAYLOAD_ERROR_INVALID_SIGNATURE", -} - -export type PayloadErrorType = - | { - code: PayloadErrorCode.EXECUTION_ENGINE_INVALID; - execStatus: ExecutionPayloadStatus; - errorMessage: string; - } - | { - code: PayloadErrorCode.EXECUTION_ENGINE_ERROR; - execStatus: ExecutionPayloadStatus; - errorMessage: string; - } - | { - code: PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE; - blockRootHex: string; - } - | { - code: PayloadErrorCode.MISS_BLOCK_STATE; - blockRootHex: string; - } - | { - code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR; - message: string; - } - | { - code: PayloadErrorCode.INVALID_SIGNATURE; - }; - -export class PayloadError extends Error { - type: PayloadErrorType; - - constructor(type: PayloadErrorType, message?: string) { - super(message ?? type.code); - this.type = type; - } -} - function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExecutionStatus { switch (status) { case ExecutionPayloadStatus.VALID: @@ -83,16 +34,18 @@ function toForkChoiceExecutionStatus(status: ExecutionPayloadStatus): PayloadExe * The DA wait must have run upstream (range sync awaits DA in `verifyBlocksInEpoch` for the * whole segment; gossip / API path uses the `processExecutionPayload` wrapper below). * + * The consensus body (fork-choice reads/writes, regen state, BLS signature) lives in the engine + * (`verifyExecutionPayloadEnvelope` + `importExecutionPayload`). The facade keeps the EL calls + * (facade-owned execution engine), the hot-DB write queue, metrics, and event emission. + * * Steps: * 1. Emit `execution_payload_available` event for payload attestation - * 2. Get the ProtoBlock from fork choice - * 3. Regenerate state for envelope verification - * 4. Verify envelope (fields against state, signature, and EL in parallel where possible) - * 5. Persist verified payload envelope to hot DB (waits for write-queue space for backpressure) - * 6. Update fork choice (transitions the block's PENDING variant to FULL) - * 7. Queue notifyForkchoiceUpdate to engine api - * 8. Record metrics for payload envelope and column sources - * 9. Emit `execution_payload` event + * 2. Verify the envelope (engine: fork choice + regen state + fields + BLS) in parallel with the EL + * 3. Handle the EL response + * 4. Persist verified payload envelope to hot DB (waits for write-queue space for backpressure) + * 5. Import to fork choice + FCU decision (engine); fire notifyForkchoiceUpdate (facade) + * 6. Record metrics for payload envelope and column sources + * 7. Emit `execution_payload` event */ export async function importExecutionPayload( this: BeaconChain, @@ -118,55 +71,15 @@ export async function importExecutionPayload( }); } - // 2. Get ProtoBlock for parent root lookup - const protoBlock = this.forkChoice.getBlockHexDefaultStatus(blockRootHex); - if (!protoBlock) { - throw new PayloadError({ - code: PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE, - blockRootHex, - }); - } + // Per-envelope SSZ bytes for the engine's bytes-first contract (unused by the JS engine). Gossip + // populates serializedCache; fall back to serializing on a cache miss so all call sites pass real bytes. + const envelopeBytes = + this.serializedCache.get(signedEnvelope) ?? ssz.gloas.SignedExecutionPayloadEnvelope.serialize(signedEnvelope); - // 3. Regenerate state for envelope verification - const blockState = await this.regen - .getBlockSlotState(protoBlock, protoBlock.slot, {dontTransferCache: true}, RegenCaller.importExecutionPayload) - .catch(() => - // only happen at the 1st batch of skipped slot checkpoint sync - this.regen.getClosestHeadState(protoBlock) - ); - - if (blockState == null) { - throw new PayloadError({ - code: PayloadErrorCode.MISS_BLOCK_STATE, - blockRootHex: protoBlock.blockRoot, - }); - } - if (!isStatePostGloas(blockState)) { - throw new PayloadError({ - code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR, - message: `Expected gloas+ state for payload import, got fork=${blockState.forkName}`, - }); - } - - // 4. Verify envelope fields against state first to fail fast before the EL + BLS work. - // When validSignature is true, gossip/API has already verified both the signature and the - // executionRequestsRoot, so we skip those checks here. - try { - verifyExecutionPayloadEnvelope(this.config, blockState, envelope, { - verifyExecutionRequestsRoot: !opts.validSignature, - }); - } catch (e) { - throw new PayloadError( - { - code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR, - message: (e as Error).message, - }, - `Envelope verification error: ${(e as Error).message}` - ); - } - - // 4a. Run EL and signature verification in parallel - const [execResult, signatureValid] = await Promise.all([ + // 2. Verify the envelope (engine: fork choice lookup + regen state + fields + BLS signature) in + // parallel with the EL `notifyNewPayload` (facade-owned). Mirrors the block pipeline's state/sig ∥ EL + // split — the EL result is needed to map the fork-choice execution status below. + const [execResult] = await Promise.all([ this.executionEngine.notifyNewPayload( fork, envelope.payload, @@ -174,25 +87,12 @@ export async function importExecutionPayload( envelope.parentBeaconBlockRoot, envelope.executionRequests ), - - opts.validSignature === true - ? Promise.resolve(true) - : verifyExecutionPayloadEnvelopeSignature( - this.config, - blockState, - this.pubkeyCache, - signedEnvelope, - payloadInput.proposerIndex, - this.bls - ), + this.beaconEngine.verifyExecutionPayloadEnvelope(envelopeBytes, signedEnvelope, payloadInput.proposerIndex, { + validSignature: opts.validSignature === true, + }), ]); - // 4b. Check signature verification result - if (!signatureValid) { - throw new PayloadError({code: PayloadErrorCode.INVALID_SIGNATURE}); - } - - // 4c. Handle EL response + // 3. Handle EL response (the EL is facade-owned; the engine only consumes the mapped status below). switch (execResult.status) { case ExecutionPayloadStatus.VALID: break; @@ -218,7 +118,7 @@ export async function importExecutionPayload( }); } - // 5. Persist payload envelope to hot DB. Wait for write-queue space here to apply backpressure + // 4. Persist payload envelope to hot DB. Wait for write-queue space here to apply backpressure // on the import pipeline during sync, then perform the write asynchronously to avoid blocking. await this.unfinalizedPayloadEnvelopeWrites.waitForSpace(); this.unfinalizedPayloadEnvelopeWrites.push(payloadInput).catch((e) => { @@ -231,10 +131,12 @@ export async function importExecutionPayload( } }); - // 6. Update fork choice, transitions the block's PENDING variant to FULL + // 5. Import to fork choice (engine transitions the block's PENDING variant to FULL) and get the FCU + // decision. The facade fires notifyForkchoiceUpdate on the EL from the returned data; the engine + // never touches the EL. `disableImportExecutionFcU` stays a facade-side gate. const execStatus = toForkChoiceExecutionStatus(execResult.status); - this.beaconEngine.forkChoice.onExecutionPayload( - blockRootHex, + const {fcuUpdate, executionOptimistic} = this.beaconEngine.importExecutionPayload( + fromHex(blockRootHex), blockHashHex, envelope.payload.blockNumber, envelope.payload.gasLimit, @@ -242,19 +144,22 @@ export async function importExecutionPayload( dataAvailabilityStatus ); - // 7. Queue notifyForkchoiceUpdate to engine api - const head = this.forkChoice.getHead(); - if (!this.opts.disableImportExecutionFcU && blockRootHex === head.blockRoot) { - const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice); - const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX; - this.executionEngine.notifyForkchoiceUpdate(fork, blockHashHex, safeBlockHash, finalizedBlockHash).catch((e) => { - if (!isErrorAborted(e) && !isQueueErrorAborted(e)) { - this.logger.error("Error pushing notifyForkchoiceUpdate()", {blockHashHex, finalizedBlockHash}, e); - } - }); + if (!this.opts.disableImportExecutionFcU && fcuUpdate !== null) { + this.executionEngine + .notifyForkchoiceUpdate( + fcuUpdate.fork, + fcuUpdate.headBlockHash, + fcuUpdate.safeBlockHash, + fcuUpdate.finalizedBlockHash + ) + .catch((e) => { + if (!isErrorAborted(e) && !isQueueErrorAborted(e)) { + this.logger.error("Error pushing notifyForkchoiceUpdate()", {blockHashHex}, e); + } + }); } - // 8. Record metrics for payload envelope and column sources + // 6. Record metrics for payload envelope and column sources const delaySec = this.clock.secFromSlot(slot); this.metrics?.importPayload.elapsedTimeTillImported.observe( {source: payloadInput.getPayloadEnvelopeSource().source}, @@ -264,14 +169,14 @@ export async function importExecutionPayload( this.metrics?.importPayload.columnsBySource.inc({source}); } - // 9. Emit event after payload is fully verified and imported to fork choice, only for recent enough payloads + // 7. Emit event after payload is fully verified and imported to fork choice, only for recent enough payloads if (this.clock.currentSlot - slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) { this.emitter.emit(routes.events.EventType.executionPayload, { slot, builderIndex: envelope.builderIndex, blockHash: blockHashHex, blockRoot: blockRootHex, - executionOptimistic: execStatus === ExecutionStatus.Syncing, + executionOptimistic, }); } diff --git a/packages/beacon-node/src/chain/blocks/payloadError.ts b/packages/beacon-node/src/chain/blocks/payloadError.ts new file mode 100644 index 000000000000..27c601fad3fa --- /dev/null +++ b/packages/beacon-node/src/chain/blocks/payloadError.ts @@ -0,0 +1,46 @@ +import {ExecutionPayloadStatus} from "../../execution/index.js"; + +export enum PayloadErrorCode { + EXECUTION_ENGINE_INVALID = "PAYLOAD_ERROR_EXECUTION_ENGINE_INVALID", + EXECUTION_ENGINE_ERROR = "PAYLOAD_ERROR_EXECUTION_ENGINE_ERROR", + BLOCK_NOT_IN_FORK_CHOICE = "PAYLOAD_ERROR_BLOCK_NOT_IN_FORK_CHOICE", + MISS_BLOCK_STATE = "PAYLOAD_ERROR_MISS_BLOCK_STATE", + ENVELOPE_VERIFICATION_ERROR = "PAYLOAD_ERROR_ENVELOPE_VERIFICATION_ERROR", + INVALID_SIGNATURE = "PAYLOAD_ERROR_INVALID_SIGNATURE", +} + +export type PayloadErrorType = + | { + code: PayloadErrorCode.EXECUTION_ENGINE_INVALID; + execStatus: ExecutionPayloadStatus; + errorMessage: string; + } + | { + code: PayloadErrorCode.EXECUTION_ENGINE_ERROR; + execStatus: ExecutionPayloadStatus; + errorMessage: string; + } + | { + code: PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE; + blockRootHex: string; + } + | { + code: PayloadErrorCode.MISS_BLOCK_STATE; + blockRootHex: string; + } + | { + code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR; + message: string; + } + | { + code: PayloadErrorCode.INVALID_SIGNATURE; + }; + +export class PayloadError extends Error { + type: PayloadErrorType; + + constructor(type: PayloadErrorType, message?: string) { + super(message ?? type.code); + this.type = type; + } +} diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index 300b17524c3b..283ac71d8364 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -3,8 +3,8 @@ import {NotReorgedReason} from "@lodestar/fork-choice"; import {ArchiveStoreTask} from "../../chain/archiveStore/interface.js"; import {FrequencyStateArchiveStep} from "../../chain/archiveStore/strategies/frequencyStateArchiveStrategy.js"; import {BlockInputSource} from "../../chain/blocks/blockInput/index.js"; -import {PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js"; import {PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js"; +import {PayloadErrorCode} from "../../chain/blocks/payloadError.js"; import {JobQueueItemType} from "../../chain/bls/index.js"; import {AttestationErrorCode, BlockErrorCode} from "../../chain/errors/index.js"; import { diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 87ebd4efe043..fdd6bbbd5756 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -44,8 +44,8 @@ import { IBlockInput, isBlockInputColumns, } from "../../chain/blocks/blockInput/index.js"; -import {PayloadError, PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js"; import {PayloadEnvelopeInput, PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js"; +import {PayloadError, PayloadErrorCode} from "../../chain/blocks/payloadError.js"; import {BlobSidecarValidation} from "../../chain/blocks/types.js"; import {ChainEvent} from "../../chain/emitter.js"; import { diff --git a/packages/beacon-node/src/sync/unknownBlock.ts b/packages/beacon-node/src/sync/unknownBlock.ts index f87ce6e657f9..28da54381950 100644 --- a/packages/beacon-node/src/sync/unknownBlock.ts +++ b/packages/beacon-node/src/sync/unknownBlock.ts @@ -8,8 +8,8 @@ import {Logger, fromHex, prettyPrintIndices, pruneSetToMax, sleep, toRootHex} fr import {GossipValidationStatus} from "../chain/beaconEngine/gossipValidationResult.js"; import {isBlockInputBlobs, isBlockInputColumns} from "../chain/blocks/blockInput/blockInput.js"; import {BlockInputSource, IBlockInput} from "../chain/blocks/blockInput/types.js"; -import {PayloadError, PayloadErrorCode} from "../chain/blocks/importExecutionPayload.js"; import {PayloadEnvelopeInput, PayloadEnvelopeInputSource} from "../chain/blocks/payloadEnvelopeInput/index.js"; +import {PayloadError, PayloadErrorCode} from "../chain/blocks/payloadError.js"; import {BlockError, BlockErrorCode} from "../chain/errors/index.js"; import {ChainEvent, ChainEventData, IBeaconChain} from "../chain/index.js"; import {validateGloasBlockDataColumnSidecars} from "../chain/validation/dataColumnSidecar.js"; diff --git a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts index 213ad6dc04b9..6b4fe73981e0 100644 --- a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts +++ b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts @@ -12,9 +12,9 @@ import {notNullish, sleep, toRootHex} from "@lodestar/utils"; import {accept} from "../../../src/chain/beaconEngine/gossipValidationResult.js"; import {BlockInputNoData} from "../../../src/chain/blocks/blockInput/blockInput.js"; import {BlockInputSource, DAType, IBlockInput} from "../../../src/chain/blocks/blockInput/types.js"; -import {PayloadError, PayloadErrorCode} from "../../../src/chain/blocks/importExecutionPayload.js"; import {PayloadEnvelopeInput} from "../../../src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js"; import {PayloadEnvelopeInputSource} from "../../../src/chain/blocks/payloadEnvelopeInput/types.js"; +import {PayloadError, PayloadErrorCode} from "../../../src/chain/blocks/payloadError.js"; import {BlockError, BlockErrorCode} from "../../../src/chain/errors/blockError.js"; import {ChainEvent, ChainEventEmitter, IBeaconChain} from "../../../src/chain/index.js"; import {SeenBlockProposers} from "../../../src/chain/seenCache/seenBlockProposers.js"; From 17e3af9317cfadb3dba52197a1158ae4e71c8141 Mon Sep 17 00:00:00 2001 From: twoeths Date: Tue, 7 Jul 2026 16:00:06 +0700 Subject: [PATCH 19/24] fix: remove BeaconChain.get_head() methods --- packages/beacon-node/src/chain/chain.ts | 59 ++----------------- packages/beacon-node/src/chain/interface.ts | 11 +--- .../test/mocks/mockedBeaconChain.ts | 6 ++ 3 files changed, 11 insertions(+), 65 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index b7fee2e342a2..95a3512f88ad 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -2,7 +2,7 @@ import path from "node:path"; import {PrivateKey} from "@libp2p/interface"; import {Type} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; -import {CheckpointWithHex, IForkChoiceRead, ProtoBlock, UpdateHeadOpt} from "@lodestar/fork-choice"; +import {CheckpointWithHex, IForkChoiceRead, ProtoBlock} from "@lodestar/fork-choice"; import {LoggerNode} from "@lodestar/logger/node"; import { EFFECTIVE_BALANCE_INCREMENT, @@ -73,9 +73,8 @@ import {persistPayloadEnvelopeInput} from "./blocks/writePayloadEnvelopeInputToD import {IBlsVerifier} from "./bls/index.js"; import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js"; import {ChainEvent, ChainEventEmitter} from "./emitter.js"; -import {ForkchoiceCaller} from "./forkChoice/index.js"; import {GetBlobsTracker} from "./GetBlobsTracker.js"; -import {CommonBlockBody, FindHeadFnName, IBeaconChain, ProposerPreparationData, StateGetOpts} from "./interface.js"; +import {CommonBlockBody, IBeaconChain, ProposerPreparationData, StateGetOpts} from "./interface.js"; import {LightClientServer} from "./lightClient/index.js"; import {IChainOptions} from "./options.js"; import {PrepareNextSlotScheduler} from "./prepareNextSlot.js"; @@ -1098,8 +1097,8 @@ export class BeaconChain implements IBeaconChain { /** * Single choke point for refreshing the cached head: pass the new canonical head here from ANY flow - * that updates it (importBlock, recomputeForkChoiceHead, getProposerHead, predictProposerHead). It - * emits fork-choice justified/finalized transitions facade-side — the engine no longer emits these + * that updates it (currently importBlock; head recompute / proposer-head selection are engine-internal). + * It emits fork-choice justified/finalized transitions facade-side — the engine no longer emits these * (a native engine can't emit into the JS emitter); they are derived from the head ProtoBlock's * justified and finalized checkpoints. Head-derived: a checkpoint that advances only via an * epoch-boundary tick is emitted on the next head update. @@ -1122,56 +1121,6 @@ export class BeaconChain implements IBeaconChain { } } - recomputeForkChoiceHead(caller: ForkchoiceCaller): ProtoBlock { - this.metrics?.forkChoice.requests.inc(); - const timer = this.metrics?.forkChoice.findHead.startTimer({caller}); - - try { - // GetCanonicalHead runs updateHead(); the returned head IS the new canonical head — cache it - // (and emit any justified/finalized transition it carries) via the single choke point. - const head = this.beaconEngine.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head; - this.updateHeadAndEmitCheckpointEvents(head); - return head; - } catch (e) { - this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead}); - throw e; - } finally { - timer?.(); - } - } - - predictProposerHead(slot: Slot): ProtoBlock { - this.metrics?.forkChoice.requests.inc(); - const timer = this.metrics?.forkChoice.findHead.startTimer({caller: FindHeadFnName.predictProposerHead}); - const secFromSlot = this.clock.secFromSlot(slot); - - try { - const predictedHead = this.beaconEngine.forkChoice.updateAndGetHead({ - mode: UpdateHeadOpt.GetPredictedProposerHead, - secFromSlot, - slot, - }).head; - // The returned value is a proposer-boost-reorg prediction, NOT the canonical head — sync the - // cache to the canonical head (getHead()) through the choke point (may emit justified/finalized). - this.updateHeadAndEmitCheckpointEvents(this.beaconEngine.forkChoice.getHead()); - return predictedHead; - } catch (e) { - this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetPredictedProposerHead}); - throw e; - } finally { - timer?.(); - } - } - - getProposerHead(slot: Slot): ProtoBlock { - // engine.getProposerHead runs updateHead() (GetProposerHead) then returns the proposer head, which - // may be the parent for a reorg. Sync the cache to the canonical head (not the returned value) - // through the choke point so any justified/finalized transition is emitted. - const proposerHead = this.beaconEngine.getProposerHead(slot); - this.updateHeadAndEmitCheckpointEvents(this.beaconEngine.forkChoice.getHead()); - return proposerHead; - } - /** * Returns Promise that resolves either on block found or once 1 slot passes. * Used to handle unknown block root for both unaggregated and aggregated attestations. diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 1f813e4125a5..e553afe6fd5b 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -1,6 +1,6 @@ import {Type} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; -import {CheckpointWithHex, IForkChoiceRead, ProtoBlock} from "@lodestar/fork-choice"; +import {CheckpointWithHex, IForkChoiceRead} from "@lodestar/fork-choice"; import {IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; import { BeaconBlock, @@ -37,7 +37,6 @@ import {ImportBlockOpts, ImportPayloadOpts} from "./blocks/types.js"; import {IBlsVerifier} from "./bls/index.js"; import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js"; import {ChainEventEmitter} from "./emitter.js"; -import {ForkchoiceCaller} from "./forkChoice/index.js"; import {GetBlobsTracker} from "./GetBlobsTracker.js"; import {LightClientServer} from "./lightClient/index.js"; import {IChainOptions} from "./options.js"; @@ -227,14 +226,6 @@ export interface IBeaconChain { getStatus(): Status; - recomputeForkChoiceHead(caller: ForkchoiceCaller): ProtoBlock; - - /** When proposerBoostReorg is enabled, this is called at slot n-1 to predict the head block to build on if we are proposing at slot n */ - predictProposerHead(slot: Slot): ProtoBlock; - - /** When proposerBoostReorg is enabled and we are proposing a block, this is called to determine which head block to build on */ - getProposerHead(slot: Slot): ProtoBlock; - waitForBlock(slot: Slot, root: RootHex): Promise; updateBeaconProposerData(epoch: Epoch, proposers: ProposerPreparationData[]): Promise; diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index 3994092e4a33..02463b493e41 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -33,6 +33,12 @@ import {getMockedLogger} from "./loggerMock.js"; export type MockedBeaconChain = Mocked & { logger: Mocked; forkChoice: MockedForkChoice; + // Engine head-selection methods surfaced on the flat mock (the mock doubles as the engine, so the + // real `prepareForNextSlot` / `produceBlockBase` bound onto it call `this.recomputeForkChoiceHead` + // etc.). No longer on the facade — stubbed here so tests can control head selection. + recomputeForkChoiceHead: Mock<() => ProtoBlock>; + predictProposerHead: Mock<() => ProtoBlock>; + getProposerHead: Mock<() => ProtoBlock>; executionEngine: Mocked; executionBuilder: Mocked; opPool: Mocked; From 5efce01f72ab73f4eddaa65816c9056b36c3d09d Mon Sep 17 00:00:00 2001 From: twoeths Date: Tue, 7 Jul 2026 16:03:01 +0700 Subject: [PATCH 20/24] fix: more BeaconEngine forkchoice methods --- .../src/api/impl/beacon/pool/index.ts | 2 +- .../src/chain/beaconEngine/beaconEngine.ts | 31 +++++++++++++++++++ .../src/chain/beaconEngine/interface.ts | 17 ++++++++++ .../beacon-node/src/chain/blocks/index.ts | 2 +- packages/beacon-node/src/chain/chain.ts | 7 +++-- .../src/network/processor/gossipHandlers.ts | 6 ++-- .../test/mocks/mockedBeaconChain.ts | 6 ++++ packages/fork-choice/src/index.ts | 2 +- 8 files changed, 64 insertions(+), 9 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index 9ff310c34b35..f07060bb1206 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -312,7 +312,7 @@ export function getBeaconPoolApi({ metrics?.opPool.payloadAttestationPool.apiInsertOutcome.inc({insertOutcome}); // TODO - beacon engine: refactor to beaconEngine.notifyPtcMessages() - chain.beaconEngine.forkChoice.notifyPtcMessages( + chain.beaconEngine.notifyPtcMessages( toRootHex(payloadAttestationMessage.data.beaconBlockRoot), payloadAttestationMessage.data.slot, validatorCommitteeIndices, diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index de614e11cc61..8a710636b093 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -11,6 +11,7 @@ import { ForkChoiceErrorCode, ForkChoiceStateGetter, IForkChoice, + LVHExecResponse, NotReorgedReason, PayloadExecutionStatus, PayloadStatus, @@ -2792,6 +2793,36 @@ export class BeaconEngine implements IBeaconEngine { } } + // --- Fork-choice writes (routed here so the facade never writes fork choice directly) --- + + updateTime(currentSlot: Slot): void { + this.forkChoice.updateTime(currentSlot); + } + + getIrrecoverableError(): Error | undefined { + return this.forkChoice.irrecoverableError; + } + + validateLatestHash(execResponse: LVHExecResponse): void { + this.forkChoice.validateLatestHash(execResponse); + } + + // TODO - beacon-engine: transitional wrappers — their gossip/api PTC + attestation consumers move into + // the engine (BLK-2), at which point these fork-choice writes become engine-internal and these go away. + notifyPtcMessages( + blockRoot: RootHex, + slot: Slot, + ptcIndices: number[], + payloadPresent: boolean, + blobDataAvailable: boolean + ): void { + this.forkChoice.notifyPtcMessages(blockRoot, slot, ptcIndices, payloadPresent, blobDataAvailable); + } + + onAttestation(attestation: IndexedAttestation, attDataRoot: string, forceImport?: boolean): void { + this.forkChoice.onAttestation(attestation, attDataRoot, forceImport); + } + // --- DB ownership (blocks + states) --- async migrateFinalized(finalized: CheckpointWithHex): Promise { diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 3ef964b39d74..1b212524ddeb 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -4,6 +4,7 @@ import { BlockExecutionStatus, CheckpointWithHex, IForkChoice, + LVHExecResponse, PayloadExecutionStatus, PayloadStatus, ProtoBlock, @@ -21,6 +22,7 @@ import { CommitteeIndex, Epoch, Gwei, + IndexedAttestation, Root, RootHex, SSEPayloadAttributes, @@ -519,6 +521,21 @@ export interface IBeaconEngine { ): Promise; discardVerifiedBlocks(blockRootHexes: RootHex[]): void; + // Fork-choice writes routed through the engine (so the facade never writes fork choice directly). + updateTime(currentSlot: Slot): void; + getIrrecoverableError(): Error | undefined; + validateLatestHash(execResponse: LVHExecResponse): void; + // TODO - beacon-engine: transitional — the gossip/api PTC + attestation consumers move into the engine + // (BLK-2), at which point these fork-choice writes become engine-internal and these wrappers go away. + notifyPtcMessages( + blockRoot: RootHex, + slot: Slot, + ptcIndices: number[], + payloadPresent: boolean, + blobDataAvailable: boolean + ): void; + onAttestation(attestation: IndexedAttestation, attDataRoot: string, forceImport?: boolean): void; + // Execution payload envelope (gloas) import — consensus body of the facade `importExecutionPayload`. // `verifyExecutionPayloadEnvelope` (regen state + fields + BLS sig, throws `PayloadError`) runs // facade-parallel to the EL `notifyNewPayload`; `importExecutionPayload` does the fork-choice write + diff --git a/packages/beacon-node/src/chain/blocks/index.ts b/packages/beacon-node/src/chain/blocks/index.ts index d85c4993c4f0..9a7eec52a8fe 100644 --- a/packages/beacon-node/src/chain/blocks/index.ts +++ b/packages/beacon-node/src/chain/blocks/index.ts @@ -109,7 +109,7 @@ export async function processBlocks( // and we need to further propagate if (segmentExecStatus.execAborted !== null) { if (segmentExecStatus.invalidSegmentLVH !== undefined) { - this.beaconEngine.forkChoice.validateLatestHash(segmentExecStatus.invalidSegmentLVH); + this.beaconEngine.validateLatestHash(segmentExecStatus.invalidSegmentLVH); } throw segmentExecStatus.execAborted.execError; } diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 95a3512f88ad..64d94f4b626f 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -1222,11 +1222,12 @@ export class BeaconChain implements IBeaconChain { this.logger.verbose("Clock slot", {slot}); // CRITICAL UPDATE - if (this.forkChoice.irrecoverableError) { - this.processShutdownCallback(this.forkChoice.irrecoverableError); + const irrecoverableError = this.beaconEngine.getIrrecoverableError(); + if (irrecoverableError) { + this.processShutdownCallback(irrecoverableError); } - this.beaconEngine.forkChoice.updateTime(slot); + this.beaconEngine.updateTime(slot); this.metrics?.clockSlot.set(slot); // Engine-owned pools + seen-caches are pruned by the engine's own clock listener. diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index fdd6bbbd5756..d5bc14e4609d 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -889,7 +889,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand if (!options.dontSendGossipAttestationsToForkchoice) { try { // TODO beacon-engine: move to validateGossipAggregateAndProof - chain.beaconEngine.forkChoice.onAttestation(indexedAttestation, attDataRootHex); + chain.beaconEngine.onAttestation(indexedAttestation, attDataRootHex); } catch (e) { logger.debug( "Error adding gossip aggregated attestation to forkchoice", @@ -1176,7 +1176,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand } catch (e) { logger.error("Error adding to payloadAttestation pool", {}, e as Error); } - chain.beaconEngine.forkChoice.notifyPtcMessages( + chain.beaconEngine.notifyPtcMessages( toRootHex(payloadAttestationMessage.data.beaconBlockRoot), payloadAttestationMessage.data.slot, validatorCommitteeIndices, @@ -1298,7 +1298,7 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp if (!options.dontSendGossipAttestationsToForkchoice) { try { - chain.beaconEngine.forkChoice.onAttestation(indexedAttestation, attDataRootHex); + chain.beaconEngine.onAttestation(indexedAttestation, attDataRootHex); } catch (e) { logger.debug("Error adding gossip unaggregated attestation to forkchoice", {subnet}, e as Error); } diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index 02463b493e41..e8fc60acf689 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -290,6 +290,12 @@ export function getMockedBeaconChain(opts?: Partial): "addPayloadAttestation", "getPoolPayloadAttestations", "addExecutionPayloadBid", + // Fork-choice write wrappers (forward to the mock's forkChoice). + "updateTime", + "getIrrecoverableError", + "validateLatestHash", + "notifyPtcMessages", + "onAttestation", ] as const) { (chain as unknown as Record)[name] = ( BeaconEngine.prototype as unknown as Record diff --git a/packages/fork-choice/src/index.ts b/packages/fork-choice/src/index.ts index 561414b23c84..2cdcbd7ae2d6 100644 --- a/packages/fork-choice/src/index.ts +++ b/packages/fork-choice/src/index.ts @@ -45,5 +45,5 @@ export type { ProtoBlock, ProtoNode, } from "./protoArray/interface.js"; -export {ExecutionStatus, PayloadStatus, isGloasBlock} from "./protoArray/interface.js"; +export {ExecutionStatus, type LVHExecResponse, PayloadStatus, isGloasBlock} from "./protoArray/interface.js"; export {ProtoArray} from "./protoArray/protoArray.js"; From f3c5eb07983ac36b7a81ec60232b4d7983f821ff Mon Sep 17 00:00:00 2001 From: twoeths Date: Tue, 7 Jul 2026 17:45:48 +0700 Subject: [PATCH 21/24] fix: gossip validation functions as methods --- .../src/chain/beaconEngine/beaconEngine.ts | 58 +++++----- packages/beacon-node/src/chain/chain.ts | 5 + .../src/chain/validation/aggregateAndProof.ts | 58 +++++----- .../src/chain/validation/attestation.ts | 103 ++++++++---------- .../src/chain/validation/attesterSlashing.ts | 22 ++-- .../src/chain/validation/blobSidecar.ts | 24 ++-- .../beacon-node/src/chain/validation/block.ts | 39 ++++--- .../chain/validation/blsToExecutionChange.ts | 18 +-- .../src/chain/validation/dataColumnSidecar.ts | 28 ++--- .../chain/validation/executionPayloadBid.ts | 34 +++--- .../validation/executionPayloadEnvelope.ts | 42 ++++--- .../validation/payloadAttestationMessage.ts | 28 ++--- .../chain/validation/proposerPreferences.ts | 20 ++-- .../src/chain/validation/proposerSlashing.ts | 20 ++-- .../src/chain/validation/syncCommittee.ts | 36 +++--- .../syncCommitteeContributionAndProof.ts | 20 ++-- .../src/chain/validation/voluntaryExit.ts | 18 +-- .../validation/aggregateAndProof.test.ts | 4 +- .../perf/chain/validation/attestation.test.ts | 6 +- .../test/spec/utils/gossipValidation.ts | 6 +- .../validation/aggregateAndProof.test.ts | 6 +- ...hufflingForAttestationVerification.test.ts | 8 +- .../attestation/validateAttestation.test.ts | 6 +- ...idateGossipAttestationsSameAttData.test.ts | 7 +- .../chain/validation/attesterSlashing.test.ts | 6 +- .../test/unit/chain/validation/block.test.ts | 24 ++-- .../validation/blsToExecutionChange.test.ts | 12 +- .../chain/validation/proposerSlashing.test.ts | 6 +- .../chain/validation/syncCommittee.test.ts | 20 ++-- .../chain/validation/voluntaryExit.test.ts | 14 +-- .../beacon-node/test/unit/util/kzg.test.ts | 2 +- 31 files changed, 355 insertions(+), 345 deletions(-) diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 8a710636b093..658e98c12b2a 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -1616,7 +1616,7 @@ export class BeaconEngine implements IBeaconEngine { signedBlock: SignedBeaconBlock, fork: ForkName ): Promise> { - return runGossipValidation(() => validateGossipBlock(this, signedBlock, fork)); + return runGossipValidation(() => validateGossipBlock.call(this, signedBlock, fork)); } validateGossipSyncCommittee( @@ -1624,14 +1624,14 @@ export class BeaconEngine implements IBeaconEngine { syncCommittee: altair.SyncCommitteeMessage, subnet: SubnetID ): Promise> { - return runGossipValidation(() => validateGossipSyncCommittee(this, syncCommittee, subnet)); + return runGossipValidation(() => validateGossipSyncCommittee.call(this, syncCommittee, subnet)); } validateApiSyncCommittee( _syncCommitteeBytes: Uint8Array, syncCommittee: altair.SyncCommitteeMessage ): Promise> { - return runGossipValidation(() => validateApiSyncCommittee(this, syncCommittee)); + return runGossipValidation(() => validateApiSyncCommittee.call(this, syncCommittee)); } validateSyncCommitteeGossipContributionAndProof( @@ -1640,7 +1640,11 @@ export class BeaconEngine implements IBeaconEngine { skipValidationKnownParticipants = false ): Promise> { return runGossipValidation(() => - validateSyncCommitteeGossipContributionAndProof(this, signedContributionAndProof, skipValidationKnownParticipants) + validateSyncCommitteeGossipContributionAndProof.call( + this, + signedContributionAndProof, + skipValidationKnownParticipants + ) ); } @@ -1650,7 +1654,7 @@ export class BeaconEngine implements IBeaconEngine { blobSidecar: deneb.BlobSidecar, subnet: SubnetID ): Promise> { - return runGossipValidation(() => validateGossipBlobSidecar(this, fork, blobSidecar, subnet)); + return runGossipValidation(() => validateGossipBlobSidecar.call(this, fork, blobSidecar, subnet)); } validateGossipFuluDataColumnSidecar( @@ -1659,7 +1663,7 @@ export class BeaconEngine implements IBeaconEngine { gossipSubnet: SubnetID ): Promise> { return runGossipValidation(() => - validateGossipFuluDataColumnSidecar(this, dataColumnSidecar, gossipSubnet, this.metrics) + validateGossipFuluDataColumnSidecar.call(this, dataColumnSidecar, gossipSubnet, this.metrics) ); } @@ -1670,7 +1674,7 @@ export class BeaconEngine implements IBeaconEngine { gossipSubnet: SubnetID ): Promise> { return runGossipValidation(() => - validateGossipGloasDataColumnSidecar(this, payloadInput, dataColumnSidecar, gossipSubnet, this.metrics) + validateGossipGloasDataColumnSidecar.call(this, payloadInput, dataColumnSidecar, gossipSubnet, this.metrics) ); } @@ -1678,14 +1682,14 @@ export class BeaconEngine implements IBeaconEngine { _payloadAttestationBytes: Uint8Array, payloadAttestationMessage: gloas.PayloadAttestationMessage ): Promise> { - return runGossipValidation(() => validateGossipPayloadAttestationMessage(this, payloadAttestationMessage)); + return runGossipValidation(() => validateGossipPayloadAttestationMessage.call(this, payloadAttestationMessage)); } validateApiPayloadAttestationMessage( _payloadAttestationBytes: Uint8Array, payloadAttestationMessage: gloas.PayloadAttestationMessage ): Promise> { - return runGossipValidation(() => validateApiPayloadAttestationMessage(this, payloadAttestationMessage)); + return runGossipValidation(() => validateApiPayloadAttestationMessage.call(this, payloadAttestationMessage)); } // The batch attestation validator already returns `Result[]` (caught internally, never throws out); @@ -1695,7 +1699,7 @@ export class BeaconEngine implements IBeaconEngine { fork: ForkName, attestations: GossipAttestation[] ): Promise<{results: GossipValidationResult[]; batchableBls: boolean}> { - const {results, batchableBls} = await validateGossipAttestationsSameAttData(fork, this, attestations); + const {results, batchableBls} = await validateGossipAttestationsSameAttData.call(this, fork, attestations); return {results: results.map(fromResult), batchableBls}; } @@ -1703,7 +1707,7 @@ export class BeaconEngine implements IBeaconEngine { fork: ForkName, attestationOrBytes: ApiAttestation ): Promise> { - return runGossipValidation(() => validateApiAttestation(fork, this, attestationOrBytes)); + return runGossipValidation(() => validateApiAttestation.call(this, fork, attestationOrBytes)); } validateGossipAggregateAndProof( @@ -1712,7 +1716,7 @@ export class BeaconEngine implements IBeaconEngine { signedAggregateAndProof: SignedAggregateAndProof ): Promise> { return runGossipValidation(() => - validateGossipAggregateAndProof(fork, this, signedAggregateAndProof, aggregateBytes) + validateGossipAggregateAndProof.call(this, fork, signedAggregateAndProof, aggregateBytes) ); } @@ -1721,7 +1725,7 @@ export class BeaconEngine implements IBeaconEngine { fork: ForkName, signedAggregateAndProof: SignedAggregateAndProof ): Promise> { - return runGossipValidation(() => validateApiAggregateAndProof(fork, this, signedAggregateAndProof)); + return runGossipValidation(() => validateApiAggregateAndProof.call(this, fork, signedAggregateAndProof)); } validateGossipExecutionPayloadEnvelope( @@ -1733,7 +1737,7 @@ export class BeaconEngine implements IBeaconEngine { bidExecutionRequestsRoot: Root ): Promise> { return runGossipValidation(() => - validateGossipExecutionPayloadEnvelope( + validateGossipExecutionPayloadEnvelope.call( this, executionPayloadEnvelope, proposerIndex, @@ -1753,7 +1757,7 @@ export class BeaconEngine implements IBeaconEngine { bidExecutionRequestsRoot: Root ): Promise> { return runGossipValidation(() => - validateApiExecutionPayloadEnvelope( + validateApiExecutionPayloadEnvelope.call( this, executionPayloadEnvelope, proposerIndex, @@ -1768,21 +1772,23 @@ export class BeaconEngine implements IBeaconEngine { _bidBytes: Uint8Array, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid ): Promise> { - return runGossipValidation(() => validateGossipExecutionPayloadBid(this, signedExecutionPayloadBid)); + return runGossipValidation(() => validateGossipExecutionPayloadBid.call(this, signedExecutionPayloadBid)); } validateApiExecutionPayloadBid( _bidBytes: Uint8Array, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid ): Promise> { - return runGossipValidation(() => validateApiExecutionPayloadBid(this, signedExecutionPayloadBid)); + return runGossipValidation(() => validateApiExecutionPayloadBid.call(this, signedExecutionPayloadBid)); } async validateGossipProposerPreferences( _preferencesBytes: Uint8Array, signedProposerPreferences: gloas.SignedProposerPreferences ): Promise> { - const res = await runGossipValidation(() => validateGossipProposerPreferences(this, signedProposerPreferences)); + const res = await runGossipValidation(() => + validateGossipProposerPreferences.call(this, signedProposerPreferences) + ); // Insert on Accept — the pool is engine-internal, so the facade (gossip handler / API) no longer adds. if (res.status === GossipValidationStatus.Accept) { this.proposerPreferencesPool.add(signedProposerPreferences); @@ -1798,7 +1804,7 @@ export class BeaconEngine implements IBeaconEngine { attesterSlashing: AttesterSlashing, fork: ForkName ): Promise> { - const res = await runGossipValidation(() => validateGossipAttesterSlashing(this, attesterSlashing)); + const res = await runGossipValidation(() => validateGossipAttesterSlashing.call(this, attesterSlashing)); if (res.status === GossipValidationStatus.Accept) { // Contain insert failures — a pool error must not flip the gossip verdict (matches prior handler). try { @@ -1812,14 +1818,14 @@ export class BeaconEngine implements IBeaconEngine { } async validateApiAttesterSlashing(attesterSlashing: AttesterSlashing, fork: ForkName): Promise { - await validateApiAttesterSlashing(this, attesterSlashing); + await validateApiAttesterSlashing.call(this, attesterSlashing); this.opPool.insertAttesterSlashing(fork, attesterSlashing); } async validateGossipProposerSlashing( proposerSlashing: phase0.ProposerSlashing ): Promise> { - const res = await runGossipValidation(() => validateGossipProposerSlashing(this, proposerSlashing)); + const res = await runGossipValidation(() => validateGossipProposerSlashing.call(this, proposerSlashing)); if (res.status === GossipValidationStatus.Accept) { try { this.opPool.insertProposerSlashing(proposerSlashing); @@ -1831,12 +1837,12 @@ export class BeaconEngine implements IBeaconEngine { } async validateApiProposerSlashing(proposerSlashing: phase0.ProposerSlashing): Promise { - await validateApiProposerSlashing(this, proposerSlashing); + await validateApiProposerSlashing.call(this, proposerSlashing); this.opPool.insertProposerSlashing(proposerSlashing); } async validateGossipVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise> { - const res = await runGossipValidation(() => validateGossipVoluntaryExit(this, voluntaryExit)); + const res = await runGossipValidation(() => validateGossipVoluntaryExit.call(this, voluntaryExit)); if (res.status === GossipValidationStatus.Accept) { try { this.opPool.insertVoluntaryExit(voluntaryExit); @@ -1848,14 +1854,14 @@ export class BeaconEngine implements IBeaconEngine { } async validateApiVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise { - await validateApiVoluntaryExit(this, voluntaryExit); + await validateApiVoluntaryExit.call(this, voluntaryExit); this.opPool.insertVoluntaryExit(voluntaryExit); } async validateGossipBlsToExecutionChange( blsToExecutionChange: capella.SignedBLSToExecutionChange ): Promise> { - const res = await runGossipValidation(() => validateGossipBlsToExecutionChange(this, blsToExecutionChange)); + const res = await runGossipValidation(() => validateGossipBlsToExecutionChange.call(this, blsToExecutionChange)); if (res.status === GossipValidationStatus.Accept) { try { this.opPool.insertBlsToExecutionChange(blsToExecutionChange); @@ -1870,7 +1876,7 @@ export class BeaconEngine implements IBeaconEngine { blsToExecutionChange: capella.SignedBLSToExecutionChange, preCapella: boolean ): Promise { - await validateApiBlsToExecutionChange(this, blsToExecutionChange); + await validateApiBlsToExecutionChange.call(this, blsToExecutionChange); this.opPool.insertBlsToExecutionChange(blsToExecutionChange, preCapella); } diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 64d94f4b626f..aa09fef19605 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -116,6 +116,11 @@ const DEFAULT_MAX_PENDING_UNFINALIZED_PAYLOAD_ENVELOPE_WRITES = 16; export class BeaconChain implements IBeaconChain { readonly genesisTime: UintNum64; readonly genesisValidatorsRoot: Root; + // TODO - beacon engine: narrow this to `IBeaconEngine` so the facade can't reach engine-owned + // consensus state. Blocked on the facade's remaining concrete-only reads — `bls` (kept shared; add to + // IBeaconEngine as a carve-out), `regen` and `lightClientServer` — which must route through engine + // methods first (Part B / BLK-1 + BLK-3). Narrowing now would force `as BeaconEngine` casts here + in + // ~46 validation unit-test call sites. readonly beaconEngine: BeaconEngine; readonly executionEngine: IExecutionEngine; readonly executionBuilder?: IExecutionBuilder; diff --git a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts index 1512e1e9f193..a393dba43f1d 100644 --- a/packages/beacon-node/src/chain/validation/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/aggregateAndProof.ts @@ -26,30 +26,30 @@ export type AggregateAndProofValidationResult = { }; export async function validateApiAggregateAndProof( + this: BeaconEngine, fork: ForkName, - engine: BeaconEngine, signedAggregateAndProof: SignedAggregateAndProof ): Promise { const skipValidationKnownAttesters = true; const prioritizeBls = true; - return validateAggregateAndProof(fork, engine, signedAggregateAndProof, null, { + return validateAggregateAndProof.call(this, fork, signedAggregateAndProof, null, { skipValidationKnownAttesters, prioritizeBls, }); } export async function validateGossipAggregateAndProof( + this: BeaconEngine, fork: ForkName, - engine: BeaconEngine, signedAggregateAndProof: SignedAggregateAndProof, serializedData: Uint8Array ): Promise { - return validateAggregateAndProof(fork, engine, signedAggregateAndProof, serializedData); + return validateAggregateAndProof.call(this, fork, signedAggregateAndProof, serializedData); } async function validateAggregateAndProof( + this: BeaconEngine, fork: ForkName, - engine: BeaconEngine, signedAggregateAndProof: SignedAggregateAndProof, serializedData: Uint8Array | null = null, opts: {skipValidationKnownAttesters: boolean; prioritizeBls: boolean} = { @@ -81,7 +81,7 @@ async function validateAggregateAndProof( }); } // [REJECT] `aggregate.data.index == 0` if `block.slot == aggregate.data.slot`. - const block = engine.forkChoice.getBlockDefaultStatus(attData.beaconBlockRoot); + const block = this.forkChoice.getBlockDefaultStatus(attData.beaconBlockRoot); // If block is unknown, we don't handle it here. It will throw error later on at `verifyHeadBlockAndTargetRoot()` if (block !== null && block.slot === attData.slot && attData.index !== 0) { @@ -96,12 +96,12 @@ async function validateAggregateAndProof( // the corresponding execution payload for `block` has been seen (a client MAY queue // attestations for processing once the payload is retrieved and SHOULD request the // payload envelope via `ExecutionPayloadEnvelopesByRoot`). - // TODO - beacon engine: unstable uses engine.seenPayloadEnvelope() which also checks the facade-owned + // TODO - beacon engine: unstable uses this.seenPayloadEnvelope() which also checks the facade-owned // seenPayloadEnvelopeInputCache; the engine has only the forkChoice branch. if ( block !== null && attData.index === 1 && - !engine.forkChoice.hasPayloadHexUnsafe(toRootHex(attData.beaconBlockRoot)) + !this.forkChoice.hasPayloadHexUnsafe(toRootHex(attData.beaconBlockRoot)) ) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.EXECUTION_PAYLOAD_NOT_SEEN, @@ -129,17 +129,15 @@ async function validateAggregateAndProof( } const seenAttDataKey = serializedData ? getSeenAttDataKeyFromSignedAggregateAndProof(fork, serializedData) : null; - const cachedAttData = seenAttDataKey - ? engine.seenAttestationDatas.get(attSlot, committeeIndex, seenAttDataKey) - : null; + const cachedAttData = seenAttDataKey ? this.seenAttestationDatas.get(attSlot, committeeIndex, seenAttDataKey) : null; const attEpoch = computeEpochAtSlot(attSlot); const attTarget = attData.target; const targetEpoch = attTarget.epoch; - engine.metrics?.gossipAttestation.attestationSlotToClockSlot.observe( + this.metrics?.gossipAttestation.attestationSlotToClockSlot.observe( {caller: RegenCaller.validateGossipAggregateAndProof}, - engine.clock.currentSlot - attSlot + this.clock.currentSlot - attSlot ); if (!cachedAttData) { @@ -159,13 +157,13 @@ async function validateAggregateAndProof( // [IGNORE] the epoch of `aggregate.data.slot` is either the current or previous epoch // (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) // -- i.e. `compute_epoch_at_slot(aggregate.data.slot) in (get_previous_epoch(state), get_current_epoch(state))` - verifyPropagationSlotRange(fork, engine, attSlot); + verifyPropagationSlotRange.call(this, fork, attSlot); } // [IGNORE] The aggregate is the first valid aggregate received for the aggregator with // index aggregate_and_proof.aggregator_index for the epoch aggregate.data.target.epoch. const aggregatorIndex = aggregateAndProof.aggregatorIndex; - if (engine.seenAggregators.isKnown(targetEpoch, aggregatorIndex)) { + if (this.seenAggregators.isKnown(targetEpoch, aggregatorIndex)) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.AGGREGATOR_ALREADY_KNOWN, targetEpoch, @@ -180,7 +178,7 @@ async function validateAggregateAndProof( : toRootHex(ssz.phase0.AttestationData.hashTreeRoot(attData)); if ( !skipValidationKnownAttesters && - engine.seenAggregatedAttestations.isKnown(targetEpoch, committeeIndex, attDataRootHex, aggregationBits) + this.seenAggregatedAttestations.isKnown(targetEpoch, committeeIndex, attDataRootHex, aggregationBits) ) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.ATTESTERS_ALREADY_KNOWN, @@ -196,22 +194,22 @@ async function validateAggregateAndProof( // [REJECT] The aggregate attestation's target block is an ancestor of the block named in the LMD vote // -- i.e. `get_checkpoint_block(store, aggregate.data.beacon_block_root, aggregate.data.target.epoch) == aggregate.data.target.root` - const attHeadBlock = verifyHeadBlockAndTargetRoot( - engine, + const attHeadBlock = verifyHeadBlockAndTargetRoot.call( + this, attData.beaconBlockRoot, attTarget.root, attSlot, attEpoch, RegenCaller.validateGossipAggregateAndProof, - engine.opts.maxSkipSlots + this.opts.maxSkipSlots ); // [IGNORE] The current finalized_checkpoint is an ancestor of the block defined by aggregate.data.beacon_block_root // -- i.e. get_ancestor(store, aggregate.data.beacon_block_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == store.finalized_checkpoint.root - // > Altready check in `engine.forkChoice.hasBlock(attestation.data.beaconBlockRoot)` + // > Altready check in `this.forkChoice.hasBlock(attestation.data.beaconBlockRoot)` - const shuffling = await getShufflingForAttestationVerification( - engine, + const shuffling = await getShufflingForAttestationVerification.call( + this, attEpoch, attHeadBlock, RegenCaller.validateGossipAttestation @@ -260,27 +258,27 @@ async function validateAggregateAndProof( // by the validator with index aggregate_and_proof.aggregator_index. // [REJECT] The aggregator signature, signed_aggregate_and_proof.signature, is valid. // [REJECT] The signature of aggregate is valid. - const signingRoot = cachedAttData ? cachedAttData.signingRoot : getAttestationDataSigningRoot(engine.config, attData); + const signingRoot = cachedAttData ? cachedAttData.signingRoot : getAttestationDataSigningRoot(this.config, attData); const indexedAttestationSignatureSet = createAggregateSignatureSetFromComponents( indexedAttestation.attestingIndices, signingRoot, indexedAttestation.signature ); const signatureSets = [ - getSelectionProofSignatureSet(engine.config, attSlot, aggregatorIndex, signedAggregateAndProof), - getAggregateAndProofSignatureSet(engine.config, attEpoch, aggregatorIndex, signedAggregateAndProof), + getSelectionProofSignatureSet(this.config, attSlot, aggregatorIndex, signedAggregateAndProof), + getAggregateAndProofSignatureSet(this.config, attEpoch, aggregatorIndex, signedAggregateAndProof), indexedAttestationSignatureSet, ]; // no need to write to SeenAttestationDatas - if (!(await engine.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { + if (!(await this.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { throw new AttestationError(GossipAction.REJECT, {code: AttestationErrorCode.INVALID_SIGNATURE}); } // It's important to double check that the attestation still hasn't been observed, since // there can be a race-condition if we receive two attestations at the same time and // process them in different threads. - if (engine.seenAggregators.isKnown(targetEpoch, aggregatorIndex)) { + if (this.seenAggregators.isKnown(targetEpoch, aggregatorIndex)) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.AGGREGATOR_ALREADY_KNOWN, targetEpoch, @@ -291,7 +289,7 @@ async function validateAggregateAndProof( // Same race-condition check as above for seen aggregators if ( !skipValidationKnownAttesters && - engine.seenAggregatedAttestations.isKnown(targetEpoch, committeeIndex, attDataRootHex, aggregationBits) + this.seenAggregatedAttestations.isKnown(targetEpoch, committeeIndex, attDataRootHex, aggregationBits) ) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.ATTESTERS_ALREADY_KNOWN, @@ -300,8 +298,8 @@ async function validateAggregateAndProof( }); } - engine.seenAggregators.add(targetEpoch, aggregatorIndex); - engine.seenAggregatedAttestations.add( + this.seenAggregators.add(targetEpoch, aggregatorIndex); + this.seenAggregatedAttestations.add( targetEpoch, committeeIndex, attDataRootHex, diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index 6a43e2bac05d..5feb54e57639 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -101,8 +101,8 @@ export type Step0Result = AttestationValidationResult & { * - do not prioritize bls signature set */ export async function validateGossipAttestationsSameAttData( + this: BeaconEngine, fork: ForkName, - engine: BeaconEngine, attestationOrBytesArr: GossipAttestation[], // for unit test, consumers do not need to pass this step0ValidationFn = validateAttestationNoSignatureCheck @@ -118,7 +118,7 @@ export async function validateGossipAttestationsSameAttData( const step0ResultOrErrors: Result[] = []; for (const attestationOrBytes of attestationOrBytesArr) { const {subnet} = attestationOrBytes; - const resultOrError = await wrapError(step0ValidationFn(fork, engine, attestationOrBytes, subnet)); + const resultOrError = await wrapError(step0ValidationFn.call(this, fork, attestationOrBytes, subnet)); step0ResultOrErrors.push(resultOrError); } @@ -139,12 +139,12 @@ export async function validateGossipAttestationsSameAttData( } let signatureValids: boolean[]; - const batchableBls = signatureSets.length >= engine.opts.minSameMessageSignatureSetsToBatch; + const batchableBls = signatureSets.length >= this.opts.minSameMessageSignatureSetsToBatch; if (batchableBls) { // all signature sets should have same signing root since we filtered in network processor - signatureValids = await engine.bls.verifySignatureSetsSameMessage( + signatureValids = await this.bls.verifySignatureSetsSameMessage( signatureSets.map((set) => { - const publicKey = engine.pubkeyCache.getOrThrow(set.index); + const publicKey = this.pubkeyCache.getOrThrow(set.index); return {publicKey, signature: set.signature}; }), signatureSets[0].signingRoot @@ -152,7 +152,7 @@ export async function validateGossipAttestationsSameAttData( } else { // don't want to block the main thread if there are too few signatures signatureValids = await Promise.all( - signatureSets.map((set) => engine.bls.verifySignatureSets([set], {batchable: true})) + signatureSets.map((set) => this.bls.verifySignatureSets([set], {batchable: true})) ); } @@ -172,7 +172,7 @@ export async function validateGossipAttestationsSameAttData( // It's important to double check that the attestation still hasn't been observed, since // there can be a race-condition if we receive two attestations at the same time and // process them in different threads. - if (engine.seenAttesters.isKnown(targetEpoch, validatorIndex)) { + if (this.seenAttesters.isKnown(targetEpoch, validatorIndex)) { step0ResultOrErrors[oldIndex] = { err: new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN, @@ -183,7 +183,7 @@ export async function validateGossipAttestationsSameAttData( } // valid - engine.seenAttesters.add(targetEpoch, validatorIndex); + this.seenAttesters.add(targetEpoch, validatorIndex); } else { step0ResultOrErrors[oldIndex] = { err: new AttestationError(GossipAction.REJECT, { @@ -206,21 +206,21 @@ export async function validateGossipAttestationsSameAttData( * - prioritize bls signature set */ export async function validateApiAttestation( + this: BeaconEngine, fork: ForkName, - engine: BeaconEngine, attestationOrBytes: ApiAttestation ): Promise { const prioritizeBls = true; const subnet = null; try { - const step0Result = await validateAttestationNoSignatureCheck(fork, engine, attestationOrBytes, subnet); + const step0Result = await validateAttestationNoSignatureCheck.call(this, fork, attestationOrBytes, subnet); const {attestation, signatureSet, validatorIndex} = step0Result; - const isValid = await engine.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}); + const isValid = await this.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}); if (isValid) { const targetEpoch = attestation.data.target.epoch; - engine.seenAttesters.add(targetEpoch, validatorIndex); + this.seenAttesters.add(targetEpoch, validatorIndex); return step0Result; } @@ -242,8 +242,8 @@ export async function validateApiAttestation( * This is to avoid deserializing similar attestation multiple times which could help the gc */ async function validateAttestationNoSignatureCheck( + this: BeaconEngine, fork: ForkName, - engine: BeaconEngine, attestationOrBytes: AttestationOrBytes, /** Optional, to allow verifying attestations through API with unknown subnet */ subnet: SubnetID | null @@ -270,7 +270,7 @@ async function validateAttestationNoSignatureCheck( ? (getCommitteeIndexFromAttestationOrBytes(fork, attestationOrBytes) ?? 0) : PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX; const cachedAttData = - attDataKey !== null ? engine.seenAttestationDatas.get(attSlot, committeeIndexForLookup, attDataKey) : null; + attDataKey !== null ? this.seenAttestationDatas.get(attSlot, committeeIndexForLookup, attDataKey) : null; if (cachedAttData === null) { const attestation = sszDeserializeSingleAttestation(fork, attestationOrBytes.serializedData); // only deserialize on the first AttestationData that's not cached @@ -307,7 +307,7 @@ async function validateAttestationNoSignatureCheck( } // [REJECT] `attestation.data.index == 0` if `block.slot == attestation.data.slot`. - const block = engine.forkChoice.getBlockDefaultStatus(attData.beaconBlockRoot); + const block = this.forkChoice.getBlockDefaultStatus(attData.beaconBlockRoot); // block being null will be handled by `verifyHeadBlockAndTargetRoot` if (block !== null && block.slot === attSlot && attData.index !== 0) { @@ -322,12 +322,12 @@ async function validateAttestationNoSignatureCheck( // the corresponding execution payload for `block` has been seen (a client MAY queue // attestations for processing once the payload is retrieved and SHOULD request the // payload envelope via `ExecutionPayloadEnvelopesByRoot`). - // TODO - beacon engine: unstable uses engine.seenPayloadEnvelope() which also checks the facade-owned + // TODO - beacon engine: unstable uses this.seenPayloadEnvelope() which also checks the facade-owned // seenPayloadEnvelopeInputCache; the engine has only the forkChoice branch. if ( block !== null && attData.index === 1 && - !engine.forkChoice.hasPayloadHexUnsafe(toRootHex(attData.beaconBlockRoot)) + !this.forkChoice.hasPayloadHexUnsafe(toRootHex(attData.beaconBlockRoot)) ) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.EXECUTION_PAYLOAD_NOT_SEEN, @@ -349,9 +349,9 @@ async function validateAttestationNoSignatureCheck( committeeIndex = attestationOrCache.cache.committeeIndex; } - engine.metrics?.gossipAttestation.attestationSlotToClockSlot.observe( + this.metrics?.gossipAttestation.attestationSlotToClockSlot.observe( {caller: RegenCaller.validateGossipAttestation}, - engine.clock.currentSlot - attSlot + this.clock.currentSlot - attSlot ); if (!attestationOrCache.cache) { @@ -373,7 +373,7 @@ async function validateAttestationNoSignatureCheck( // [IGNORE] the epoch of `attestation.data.slot` is either the current or previous epoch // (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) // -- i.e. `compute_epoch_at_slot(attestation.data.slot) in (get_previous_epoch(state), get_current_epoch(state))` - verifyPropagationSlotRange(fork, engine, attestationOrCache.attestation.data.slot); + verifyPropagationSlotRange.call(this, fork, attestationOrCache.attestation.data.slot); } let aggregationBits: BitArray | null = null; @@ -416,14 +416,14 @@ async function validateAttestationNoSignatureCheck( // [IGNORE] The block being voted for (attestation.data.beacon_block_root) has been seen (via both gossip // and non-gossip sources) (a client MAY queue attestations for processing once block is retrieved). - const attHeadBlock = verifyHeadBlockAndTargetRoot( - engine, + const attHeadBlock = verifyHeadBlockAndTargetRoot.call( + this, attestationOrCache.attestation.data.beaconBlockRoot, attestationOrCache.attestation.data.target.root, attSlot, attEpoch, RegenCaller.validateGossipAttestation, - engine.opts.maxSkipSlots + this.opts.maxSkipSlots ); // [REJECT] The block being voted for (attestation.data.beacon_block_root) passes validation. @@ -437,8 +437,8 @@ async function validateAttestationNoSignatureCheck( // --i.e. get_ancestor(store, attestation.data.beacon_block_root, compute_start_slot_at_epoch(attestation.data.target.epoch)) == attestation.data.target.root // > Altready check in `verifyHeadBlockAndTargetRoot()` - const shuffling = await getShufflingForAttestationVerification( - engine, + const shuffling = await getShufflingForAttestationVerification.call( + this, attEpoch, attHeadBlock, RegenCaller.validateGossipAttestation @@ -447,7 +447,7 @@ async function validateAttestationNoSignatureCheck( // [REJECT] The committee index is within the expected range // -- i.e. data.index < get_committee_count_per_slot(state, data.target.epoch) committeeValidatorIndices = getCommitteeValidatorIndices(shuffling, attSlot, committeeIndex); - getSigningRoot = () => getAttestationDataSigningRoot(engine.config, attData); + getSigningRoot = () => getAttestationDataSigningRoot(this.config, attData); expectedSubnet = computeSubnetForSlot(shuffling, attSlot, committeeIndex); } @@ -510,7 +510,7 @@ async function validateAttestationNoSignatureCheck( // [IGNORE] There has been no other valid attestation seen on an attestation subnet that has an // identical attestation.data.target.epoch and participating validator index. - if (engine.seenAttesters.isKnown(targetEpoch, validatorIndex)) { + if (this.seenAttesters.isKnown(targetEpoch, validatorIndex)) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.ATTESTATION_ALREADY_KNOWN, targetEpoch, @@ -551,7 +551,7 @@ async function validateAttestationNoSignatureCheck( const committeeIndexKey = isForkPostElectra(fork) ? committeeIndex : PRE_ELECTRA_SINGLE_ATTESTATION_COMMITTEE_INDEX; - engine.seenAttestationDatas.add(attSlot, committeeIndexKey, attDataKey, { + this.seenAttestationDatas.add(attSlot, committeeIndexKey, attDataKey, { committeeValidatorIndices, committeeIndex, signingRoot: signatureSet.signingRoot, @@ -607,11 +607,9 @@ async function validateAttestationNoSignatureCheck( * Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. * Note: We do not queue future attestations for later processing */ -export function verifyPropagationSlotRange(fork: ForkName, engine: BeaconEngine, attestationSlot: Slot): void { +export function verifyPropagationSlotRange(this: BeaconEngine, fork: ForkName, attestationSlot: Slot): void { // slot with future tolerance of MAXIMUM_GOSSIP_CLOCK_DISPARITY - const latestPermissibleSlot = engine.clock.slotWithFutureTolerance( - engine.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000 - ); + const latestPermissibleSlot = this.clock.slotWithFutureTolerance(this.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000); if (attestationSlot > latestPermissibleSlot) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.FUTURE_SLOT, @@ -625,14 +623,14 @@ export function verifyPropagationSlotRange(fork: ForkName, engine: BeaconEngine, // // see: https://github.com/ethereum/consensus-specs/pull/3360 if (ForkSeq[fork] < ForkSeq.deneb) { - const currentSlot = engine.clock.currentSlot; - const withinPastDisparity = currentSlot > 0 && engine.clock.isCurrentSlotGivenGossipDisparity(currentSlot - 1); + const currentSlot = this.clock.currentSlot; + const withinPastDisparity = currentSlot > 0 && this.clock.isCurrentSlotGivenGossipDisparity(currentSlot - 1); const earliestPermissibleSlot = Math.max( // Pre-Deneb propagation is time-bounded: an attestation remains valid at the exact old // boundary `compute_time_at_slot(slot + range + 1) + MAXIMUM_GOSSIP_CLOCK_DISPARITY`. // Model that boundary by extending the lower slot bound by one additional slot only while // the clock still considers the previous slot current given gossip disparity. - currentSlot - engine.config.ATTESTATION_PROPAGATION_SLOT_RANGE - (withinPastDisparity ? 1 : 0), + currentSlot - this.config.ATTESTATION_PROPAGATION_SLOT_RANGE - (withinPastDisparity ? 1 : 0), 0 ); @@ -658,7 +656,7 @@ export function verifyPropagationSlotRange(fork: ForkName, engine: BeaconEngine, // lower bound for previous epoch is same as epoch of earliestPermissibleSlot const currentEpochWithPastTolerance = computeEpochAtSlot( - engine.clock.slotWithPastTolerance(engine.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000) + this.clock.slotWithPastTolerance(this.config.MAXIMUM_GOSSIP_CLOCK_DISPARITY / 1000) ); const earliestPermissiblePreviousEpoch = Math.max(currentEpochWithPastTolerance - 1, 0); @@ -678,7 +676,7 @@ export function verifyPropagationSlotRange(fork: ForkName, engine: BeaconEngine, * 2. attestation's target block is an ancestor of the block named in the LMD vote */ export function verifyHeadBlockAndTargetRoot( - engine: BeaconEngine, + this: BeaconEngine, beaconBlockRoot: Root, targetRoot: Root, attestationSlot: Slot, @@ -686,13 +684,13 @@ export function verifyHeadBlockAndTargetRoot( caller: RegenCaller, maxSkipSlots?: number ): ProtoBlock { - const headBlock = verifyHeadBlockIsKnown(engine, beaconBlockRoot); + const headBlock = verifyHeadBlockIsKnown.call(this, beaconBlockRoot); // Lighthouse rejects the attestation, however Lodestar only ignores considering it's not against the spec // it's more about a DOS protection to us // With verifyPropagationSlotRange() and maxSkipSlots = 32, it's unlikely we have to regenerate states in queue // to validate beacon_attestation and aggregate_and_proof const slotDistance = attestationSlot - headBlock.slot; - engine.metrics?.gossipAttestation.headSlotToAttestationSlot.observe({caller}, slotDistance); + this.metrics?.gossipAttestation.headSlotToAttestationSlot.observe({caller}, slotDistance); if (maxSkipSlots !== undefined && slotDistance > maxSkipSlots) { throw new AttestationError(GossipAction.IGNORE, { @@ -718,34 +716,29 @@ export function verifyHeadBlockAndTargetRoot( * see https://github.com/ChainSafe/lodestar/blob/v1.11.3/packages/beacon-node/src/chain/validation/attestation.ts#L566 */ export async function getShufflingForAttestationVerification( - engine: BeaconEngine, + this: BeaconEngine, attEpoch: Epoch, attHeadBlock: ProtoBlock, regenCaller: RegenCaller ): Promise { const blockEpoch = computeEpochAtSlot(attHeadBlock.slot); - const shufflingDependentRoot = getShufflingDependentRoot(engine.forkChoice, attEpoch, blockEpoch, attHeadBlock); + const shufflingDependentRoot = getShufflingDependentRoot(this.forkChoice, attEpoch, blockEpoch, attHeadBlock); - const shuffling = await engine.shufflingCache.get(attEpoch, shufflingDependentRoot); + const shuffling = await this.shufflingCache.get(attEpoch, shufflingDependentRoot); if (shuffling) { // most of the time, we should get the shuffling from cache - engine.metrics?.gossipAttestation.shufflingCacheHit.inc({caller: regenCaller}); + this.metrics?.gossipAttestation.shufflingCacheHit.inc({caller: regenCaller}); return shuffling; } - engine.metrics?.gossipAttestation.shufflingCacheMiss.inc({caller: regenCaller}); + this.metrics?.gossipAttestation.shufflingCacheMiss.inc({caller: regenCaller}); try { // for the 1st time of the same epoch and dependent root, it awaits for the regen state // from the 2nd time, it should use the same cached promise and it should reach the above code - engine.metrics?.gossipAttestation.shufflingCacheRegenHit.inc({caller: regenCaller}); - return await engine.regenStateForAttestationVerification( - attEpoch, - shufflingDependentRoot, - attHeadBlock, - regenCaller - ); + this.metrics?.gossipAttestation.shufflingCacheRegenHit.inc({caller: regenCaller}); + return await this.regenStateForAttestationVerification(attEpoch, shufflingDependentRoot, attHeadBlock, regenCaller); } catch (e) { - engine.metrics?.gossipAttestation.shufflingCacheRegenMiss.inc({caller: regenCaller}); + this.metrics?.gossipAttestation.shufflingCacheRegenMiss.inc({caller: regenCaller}); throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.MISSING_STATE_TO_VERIFY_ATTESTATION, error: e as Error, @@ -767,7 +760,7 @@ export function getAttestationDataSigningRoot(config: BeaconConfig, data: phase0 } /** - * Checks if the `attestation.data.beaconBlockRoot` is known to this engine. + * Checks if the `attestation.data.beaconBlockRoot` is known to this this. * * The block root may not be known for two reasons: * @@ -778,10 +771,10 @@ export function getAttestationDataSigningRoot(config: BeaconConfig, data: phase0 * it's still fine to ignore here because there's no need for us to handle attestations that are * already finalized. */ -function verifyHeadBlockIsKnown(engine: BeaconEngine, beaconBlockRoot: Root): ProtoBlock { +function verifyHeadBlockIsKnown(this: BeaconEngine, beaconBlockRoot: Root): ProtoBlock { // TODO (LH): Enforce a maximum skip distance for unaggregated attestations. - const headBlock = engine.forkChoice.getBlockDefaultStatus(beaconBlockRoot); + const headBlock = this.forkChoice.getBlockDefaultStatus(beaconBlockRoot); if (headBlock === null) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.UNKNOWN_OR_PREFINALIZED_BEACON_BLOCK_ROOT, diff --git a/packages/beacon-node/src/chain/validation/attesterSlashing.ts b/packages/beacon-node/src/chain/validation/attesterSlashing.ts index 772caf90821d..fa395fdedb7a 100644 --- a/packages/beacon-node/src/chain/validation/attesterSlashing.ts +++ b/packages/beacon-node/src/chain/validation/attesterSlashing.ts @@ -9,22 +9,22 @@ import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {AttesterSlashingError, AttesterSlashingErrorCode, GossipAction} from "../errors/index.js"; export async function validateApiAttesterSlashing( - engine: BeaconEngine, + this: BeaconEngine, attesterSlashing: AttesterSlashing ): Promise { const prioritizeBls = true; - return validateAttesterSlashing(engine, attesterSlashing, prioritizeBls); + return validateAttesterSlashing.call(this, attesterSlashing, prioritizeBls); } export async function validateGossipAttesterSlashing( - engine: BeaconEngine, + this: BeaconEngine, attesterSlashing: AttesterSlashing ): Promise { - return validateAttesterSlashing(engine, attesterSlashing); + return validateAttesterSlashing.call(this, attesterSlashing); } export async function validateAttesterSlashing( - engine: BeaconEngine, + this: BeaconEngine, attesterSlashing: AttesterSlashing, prioritizeBls = false ): Promise { @@ -33,20 +33,20 @@ export async function validateAttesterSlashing( // attester_slashed_indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices // ), verify if any(attester_slashed_indices.difference(prior_seen_attester_slashed_indices))). const intersectingIndices = getAttesterSlashableIndices(attesterSlashing); - if (engine.opPool.hasSeenAttesterSlashing(intersectingIndices)) { + if (this.opPool.hasSeenAttesterSlashing(intersectingIndices)) { throw new AttesterSlashingError(GossipAction.IGNORE, { code: AttesterSlashingErrorCode.ALREADY_EXISTS, }); } - const state = engine.getHeadState(); + const state = this.getHeadState(); // [REJECT] All of the conditions within process_attester_slashing pass validation. try { // verifySignature = false, verified in batch below assertValidAttesterSlashing( - engine.config, - engine.pubkeyCache, + this.config, + this.pubkeyCache, state.slot, state.validatorCount, attesterSlashing, @@ -67,8 +67,8 @@ export async function validateAttesterSlashing( }); } - const signatureSets = getAttesterSlashingSignatureSets(engine.config, state.slot, attesterSlashing); - if (!(await engine.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { + const signatureSets = getAttesterSlashingSignatureSets(this.config, state.slot, attesterSlashing); + if (!(await this.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { throw new AttesterSlashingError(GossipAction.REJECT, { code: AttesterSlashingErrorCode.INVALID, error: Error("Invalid signature"), diff --git a/packages/beacon-node/src/chain/validation/blobSidecar.ts b/packages/beacon-node/src/chain/validation/blobSidecar.ts index 264c260e3f94..932956d64cad 100644 --- a/packages/beacon-node/src/chain/validation/blobSidecar.ts +++ b/packages/beacon-node/src/chain/validation/blobSidecar.ts @@ -21,7 +21,7 @@ import {IBeaconChain} from "../interface.js"; import {RegenCaller} from "../regen/index.js"; export async function validateGossipBlobSidecar( - engine: BeaconEngine, + this: BeaconEngine, fork: ForkName, blobSidecar: deneb.BlobSidecar, subnet: SubnetID @@ -29,7 +29,7 @@ export async function validateGossipBlobSidecar( const blobSlot = blobSidecar.signedBlockHeader.message.slot; // [REJECT] The sidecar's index is consistent with `MAX_BLOBS_PER_BLOCK` -- i.e. `blob_sidecar.index < MAX_BLOBS_PER_BLOCK`. - const maxBlobsPerBlock = engine.config.getMaxBlobsPerBlock(computeEpochAtSlot(blobSlot)); + const maxBlobsPerBlock = this.config.getMaxBlobsPerBlock(computeEpochAtSlot(blobSlot)); if (blobSidecar.index >= maxBlobsPerBlock) { throw new BlobSidecarGossipError(GossipAction.REJECT, { code: BlobSidecarErrorCode.INDEX_TOO_LARGE, @@ -39,7 +39,7 @@ export async function validateGossipBlobSidecar( } // [REJECT] The sidecar is for the correct subnet -- i.e. `compute_subnet_for_blob_sidecar(sidecar.index) == subnet_id`. - if (computeSubnetForBlobSidecar(fork, engine.config, blobSidecar.index) !== subnet) { + if (computeSubnetForBlobSidecar(fork, this.config, blobSidecar.index) !== subnet) { throw new BlobSidecarGossipError(GossipAction.REJECT, { code: BlobSidecarErrorCode.INVALID_INDEX, blobIdx: blobSidecar.index, @@ -50,7 +50,7 @@ export async function validateGossipBlobSidecar( // [IGNORE] The sidecar is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- // i.e. validate that sidecar.slot <= current_slot (a client MAY queue future blocks for processing at // the appropriate slot). - const currentSlotWithGossipDisparity = engine.clock.currentSlotWithGossipDisparity; + const currentSlotWithGossipDisparity = this.clock.currentSlotWithGossipDisparity; if (currentSlotWithGossipDisparity < blobSlot) { throw new BlobSidecarGossipError(GossipAction.IGNORE, { code: BlobSidecarErrorCode.FUTURE_SLOT, @@ -61,7 +61,7 @@ export async function validateGossipBlobSidecar( // [IGNORE] The sidecar is from a slot greater than the latest finalized slot -- i.e. validate that // sidecar.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) - const finalizedCheckpoint = engine.forkChoice.getFinalizedCheckpoint(); + const finalizedCheckpoint = this.forkChoice.getFinalizedCheckpoint(); const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch); if (blobSlot <= finalizedSlot) { throw new BlobSidecarGossipError(GossipAction.IGNORE, { @@ -79,7 +79,7 @@ export async function validateGossipBlobSidecar( // already know this block. const blockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blobSidecar.signedBlockHeader.message); const blockHex = toRootHex(blockRoot); - if (engine.forkChoice.getBlockHexDefaultStatus(blockHex) !== null) { + if (this.forkChoice.getBlockHexDefaultStatus(blockHex) !== null) { throw new BlobSidecarGossipError(GossipAction.IGNORE, {code: BlobSidecarErrorCode.ALREADY_KNOWN, root: blockHex}); } @@ -90,7 +90,7 @@ export async function validateGossipBlobSidecar( // gossip and non-gossip sources) (a client MAY queue blocks for processing once the parent block is // retrieved). const parentRoot = toRootHex(blobSidecar.signedBlockHeader.message.parentRoot); - const parentBlock = engine.forkChoice.getBlockHexDefaultStatus(parentRoot); + const parentBlock = this.forkChoice.getBlockHexDefaultStatus(parentRoot); if (parentBlock === null) { // If fork choice does *not* consider the parent to be a descendant of the finalized block, // then there are two more cases: @@ -124,7 +124,7 @@ export async function validateGossipBlobSidecar( // this is something we should change this in the future to make the code airtight to the spec. // [IGNORE] The block's parent (defined by block.parent_root) has been seen (via both gossip and non-gossip sources) (a client MAY queue blocks for processing once the parent block is retrieved). // [REJECT] The block's parent (defined by block.parent_root) passes validation. - const blockState = await engine.regen + const blockState = await this.regen .getBlockSlotState(parentBlock, blobSlot, {dontTransferCache: true}, RegenCaller.validateGossipBlock) .catch(() => { throw new BlobSidecarGossipError(GossipAction.IGNORE, { @@ -137,14 +137,14 @@ export async function validateGossipBlobSidecar( // [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey. const signature = blobSidecar.signedBlockHeader.signature; - if (!engine.seenBlockInputCache.isVerifiedProposerSignature(blobSlot, blockHex, signature)) { + if (!this.seenBlockInputCache.isVerifiedProposerSignature(blobSlot, blockHex, signature)) { const signatureSet = getBlockHeaderProposerSignatureSetByParentStateSlot( - engine.config, + this.config, blockState.slot, blobSidecar.signedBlockHeader ); // Don't batch so verification is not delayed - if (!(await engine.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { + if (!(await this.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { throw new BlobSidecarGossipError(GossipAction.REJECT, { code: BlobSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID, blockRoot: blockHex, @@ -153,7 +153,7 @@ export async function validateGossipBlobSidecar( }); } - engine.seenBlockInputCache.markVerifiedProposerSignature(blobSlot, blockHex, signature); + this.seenBlockInputCache.markVerifiedProposerSignature(blobSlot, blockHex, signature); } // verify if the blob inclusion proof is correct diff --git a/packages/beacon-node/src/chain/validation/block.ts b/packages/beacon-node/src/chain/validation/block.ts index 3e42c9f909b8..18dc2c35b1e0 100644 --- a/packages/beacon-node/src/chain/validation/block.ts +++ b/packages/beacon-node/src/chain/validation/block.ts @@ -20,12 +20,11 @@ export type GossipBlockValidationResult = { }; export async function validateGossipBlock( - // TODO - beacon engine: use "this" - engine: BeaconEngine, + this: BeaconEngine, signedBlock: SignedBeaconBlock, fork: ForkName ): Promise { - const config = engine.config; + const config = this.config; const block = signedBlock.message; const blockSlot = block.slot; const blockEpoch = computeEpochAtSlot(blockSlot); @@ -33,7 +32,7 @@ export async function validateGossipBlock( // [IGNORE] The block is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- i.e.validate // that signed_beacon_block.message.slot <= current_slot (a client MAY queue future blocks for processing at the // appropriate slot). - const currentSlotWithGossipDisparity = engine.clock.currentSlotWithGossipDisparity; + const currentSlotWithGossipDisparity = this.clock.currentSlotWithGossipDisparity; if (currentSlotWithGossipDisparity < blockSlot) { throw new BlockGossipError(GossipAction.IGNORE, { code: BlockErrorCode.FUTURE_SLOT, @@ -44,7 +43,7 @@ export async function validateGossipBlock( // [IGNORE] The block is from a slot greater than the latest finalized slot -- i.e. validate that // signed_beacon_block.message.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) - const finalizedCheckpoint = engine.forkChoice.getFinalizedCheckpoint(); + const finalizedCheckpoint = this.forkChoice.getFinalizedCheckpoint(); const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch); if (blockSlot <= finalizedSlot) { throw new BlockGossipError(GossipAction.IGNORE, { @@ -61,7 +60,7 @@ export async function validateGossipBlock( // check, we will load the parent and state from disk only to find out later that we // already know this block. const blockRoot = toRootHex(config.getForkTypes(blockSlot).BeaconBlock.hashTreeRoot(block)); - if (engine.forkChoice.getBlockHexDefaultStatus(blockRoot) !== null) { + if (this.forkChoice.getBlockHexDefaultStatus(blockRoot) !== null) { throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRoot}); } @@ -70,14 +69,14 @@ export async function validateGossipBlock( // [IGNORE] The block is the first block with valid signature received for the proposer for the slot, signed_beacon_block.message.slot. const proposerIndex = block.proposerIndex; - if (engine.seenBlockProposers.isKnown(blockSlot, proposerIndex)) { + if (this.seenBlockProposers.isKnown(blockSlot, proposerIndex)) { throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex}); } // [REJECT] The current finalized_checkpoint is an ancestor of block -- i.e. // get_ancestor(store, block.parent_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == store.finalized_checkpoint.root const parentRoot = toRootHex(block.parentRoot); - const parentBlock = engine.forkChoice.getBlockHexDefaultStatus(parentRoot); + const parentBlock = this.forkChoice.getBlockHexDefaultStatus(parentRoot); if (parentBlock === null) { // If fork choice does *not* consider the parent to be a descendant of the finalized block, // then there are two more cases: @@ -105,7 +104,7 @@ export async function validateGossipBlock( // (via gossip or non-gossip sources) (a client MAY queue blocks for processing once the parent payload is retrieved). if (isGloasBeaconBlock(block)) { const parentBlockHashHex = toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash); - if (engine.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHashHex) === null) { + if (this.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHashHex) === null) { throw new BlockGossipError(GossipAction.IGNORE, { code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN, parentRoot, @@ -173,14 +172,14 @@ export async function validateGossipBlock( // this is something we should change this in the future to make the code airtight to the spec. // [IGNORE] The block's parent (defined by block.parent_root) has been seen (via both gossip and non-gossip sources) (a client MAY queue blocks for processing once the parent block is retrieved). // [REJECT] The block's parent (defined by block.parent_root) passes validation. - const blockState = await engine.regen + const blockState = await this.regen .getPreState(block, {dontTransferCache: true}, RegenCaller.validateGossipBlock) .catch(() => { throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.PARENT_UNKNOWN, parentRoot}); }); // in forky condition, make sure to populate ShufflingCache with regened state - engine.shufflingCache.processState(blockState); + this.shufflingCache.processState(blockState); // [REJECT] The block's execution payload timestamp is correct with respect to the slot // -- i.e. execution_payload.timestamp == compute_timestamp_at_slot(state, block.slot). @@ -188,8 +187,8 @@ export async function validateGossipBlock( if (!isExecutionBlockBodyType(block.body)) throw Error("Not execution block body type"); const executionPayload = block.body.executionPayload; if (isStatePostBellatrix(blockState) && blockState.isExecutionStateType && blockState.isExecutionEnabled(block)) { - const expectedTimestamp = computeTimeAtSlot(config, blockSlot, engine.clock.genesisTime); - if (executionPayload.timestamp !== computeTimeAtSlot(config, blockSlot, engine.clock.genesisTime)) { + const expectedTimestamp = computeTimeAtSlot(config, blockSlot, this.clock.genesisTime); + if (executionPayload.timestamp !== computeTimeAtSlot(config, blockSlot, this.clock.genesisTime)) { throw new BlockGossipError(GossipAction.REJECT, { code: BlockErrorCode.INCORRECT_TIMESTAMP, timestamp: executionPayload.timestamp, @@ -205,17 +204,17 @@ export async function validateGossipBlock( } // [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey. - if (!engine.seenBlockInputCache.isVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature)) { - const signatureSet = getBlockProposerSignatureSet(engine.config, signedBlock); + if (!this.seenBlockInputCache.isVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature)) { + const signatureSet = getBlockProposerSignatureSet(this.config, signedBlock); // Don't batch so verification is not delayed - if (!(await engine.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { + if (!(await this.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { throw new BlockGossipError(GossipAction.REJECT, { code: BlockErrorCode.PROPOSAL_SIGNATURE_INVALID, blockSlot, }); } - engine.seenBlockInputCache.markVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature); + this.seenBlockInputCache.markVerifiedProposerSignature(blockSlot, blockRoot, signedBlock.signature); } // [REJECT] The block is proposed by the expected proposer_index for the block's slot in the context of the current @@ -227,20 +226,20 @@ export async function validateGossipBlock( } // Check again in case there two blocks are processed concurrently - if (engine.seenBlockProposers.isKnown(blockSlot, proposerIndex)) { + if (this.seenBlockProposers.isKnown(blockSlot, proposerIndex)) { throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.REPEAT_PROPOSAL, proposerIndex}); } // Simple implementation of a pending block queue. Keeping the block here recycles the queue logic, and keeps the // gossip validation promise without any extra infrastructure. // Do the sleep at the end, since regen and signature validation can already take longer than `msToBlockSlot`. - const msToBlockSlot = computeTimeAtSlot(config, blockSlot, engine.clock.genesisTime) * 1000 - Date.now(); + const msToBlockSlot = computeTimeAtSlot(config, blockSlot, this.clock.genesisTime) * 1000 - Date.now(); if (msToBlockSlot <= config.MAXIMUM_GOSSIP_CLOCK_DISPARITY && msToBlockSlot > 0) { // If block is between 0 and 500 ms early, hold it in a promise. Equivalent to a pending queue. await sleep(msToBlockSlot); } - engine.seenBlockProposers.add(blockSlot, proposerIndex); + this.seenBlockProposers.add(blockSlot, proposerIndex); return {skippedSlots}; } diff --git a/packages/beacon-node/src/chain/validation/blsToExecutionChange.ts b/packages/beacon-node/src/chain/validation/blsToExecutionChange.ts index f8ae3f085301..1cb88a853bab 100644 --- a/packages/beacon-node/src/chain/validation/blsToExecutionChange.ts +++ b/packages/beacon-node/src/chain/validation/blsToExecutionChange.ts @@ -4,30 +4,30 @@ import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {BlsToExecutionChangeError, BlsToExecutionChangeErrorCode, GossipAction} from "../errors/index.js"; export async function validateApiBlsToExecutionChange( - engine: BeaconEngine, + this: BeaconEngine, blsToExecutionChange: capella.SignedBLSToExecutionChange ): Promise { const ignoreExists = true; const prioritizeBls = true; - return validateBlsToExecutionChange(engine, blsToExecutionChange, {ignoreExists, prioritizeBls}); + return validateBlsToExecutionChange.call(this, blsToExecutionChange, {ignoreExists, prioritizeBls}); } export async function validateGossipBlsToExecutionChange( - engine: BeaconEngine, + this: BeaconEngine, blsToExecutionChange: capella.SignedBLSToExecutionChange ): Promise { - return validateBlsToExecutionChange(engine, blsToExecutionChange); + return validateBlsToExecutionChange.call(this, blsToExecutionChange); } async function validateBlsToExecutionChange( - engine: BeaconEngine, + this: BeaconEngine, blsToExecutionChange: capella.SignedBLSToExecutionChange, opts: {ignoreExists?: boolean; prioritizeBls?: boolean} = {ignoreExists: false, prioritizeBls: false} ): Promise { const {ignoreExists, prioritizeBls} = opts; // [IGNORE] The blsToExecutionChange is the first valid blsToExecutionChange received for the validator with index // signedBLSToExecutionChange.message.validatorIndex. - if (!ignoreExists && engine.opPool.hasSeenBlsToExecutionChange(blsToExecutionChange.message.validatorIndex)) { + if (!ignoreExists && this.opPool.hasSeenBlsToExecutionChange(blsToExecutionChange.message.validatorIndex)) { throw new BlsToExecutionChangeError(GossipAction.IGNORE, { code: BlsToExecutionChangeErrorCode.ALREADY_EXISTS, }); @@ -36,8 +36,8 @@ async function validateBlsToExecutionChange( // validate bls to executionChange // NOTE: No need to advance head state since the signature's fork is handled with `broadcastedOnFork`, // and chanes relevant to `isValidBlsToExecutionChange()` happen only on processBlock(), not processEpoch() - const state = engine.getHeadState(); - const {config} = engine; + const state = this.getHeadState(); + const {config} = this; const addressChange = blsToExecutionChange.message; if (addressChange.validatorIndex >= state.validatorCount) { throw new BlsToExecutionChangeError(GossipAction.REJECT, { @@ -55,7 +55,7 @@ async function validateBlsToExecutionChange( } const signatureSet = getBlsToExecutionChangeSignatureSet(config, blsToExecutionChange); - if (!(await engine.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { + if (!(await this.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { throw new BlsToExecutionChangeError(GossipAction.REJECT, { code: BlsToExecutionChangeErrorCode.INVALID_SIGNATURE, }); diff --git a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts index 5e04db4afcd3..cf5d4000b55f 100644 --- a/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts +++ b/packages/beacon-node/src/chain/validation/dataColumnSidecar.ts @@ -30,7 +30,7 @@ import {RegenCaller} from "../regen/interface.js"; // SPEC FUNCTION // https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.4/specs/fulu/p2p-interface.md#data_column_sidecar_subnet_id export async function validateGossipFuluDataColumnSidecar( - engine: BeaconEngine, + this: BeaconEngine, dataColumnSidecar: fulu.DataColumnSidecar, gossipSubnet: SubnetID, metrics: Metrics | null @@ -39,10 +39,10 @@ export async function validateGossipFuluDataColumnSidecar( const blockRootHex = toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader)); // 1) [REJECT] The sidecar is valid as verified by verify_data_column_sidecar - verifyFuluDataColumnSidecar(engine.config, dataColumnSidecar); + verifyFuluDataColumnSidecar(this.config, dataColumnSidecar); // 2) [REJECT] The sidecar is for the correct subnet -- i.e. compute_subnet_for_data_column_sidecar(sidecar.index) == subnet_id - if (computeSubnetForDataColumnSidecar(engine.config, dataColumnSidecar) !== gossipSubnet) { + if (computeSubnetForDataColumnSidecar(this.config, dataColumnSidecar) !== gossipSubnet) { throw new DataColumnSidecarGossipError(GossipAction.REJECT, { code: DataColumnSidecarErrorCode.INVALID_SUBNET, columnIndex: dataColumnSidecar.index, @@ -53,7 +53,7 @@ export async function validateGossipFuluDataColumnSidecar( // 3) [IGNORE] The sidecar is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) // -- i.e. validate that sidecar.slot <= current_slot (a client MAY queue future blocks // for processing at the appropriate slot). - const currentSlotWithGossipDisparity = engine.clock.currentSlotWithGossipDisparity; + const currentSlotWithGossipDisparity = this.clock.currentSlotWithGossipDisparity; if (currentSlotWithGossipDisparity < blockHeader.slot) { throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { code: DataColumnSidecarErrorCode.FUTURE_SLOT, @@ -64,7 +64,7 @@ export async function validateGossipFuluDataColumnSidecar( // 4) [IGNORE] The sidecar is from a slot greater than the latest finalized slot -- i.e. validate that // sidecar.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch) - const finalizedCheckpoint = engine.forkChoice.getFinalizedCheckpoint(); + const finalizedCheckpoint = this.forkChoice.getFinalizedCheckpoint(); const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch); if (blockHeader.slot <= finalizedSlot) { throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { @@ -77,7 +77,7 @@ export async function validateGossipFuluDataColumnSidecar( // 6) [IGNORE] The sidecar's block's parent (defined by block_header.parent_root) has been seen (via gossip // or non-gossip sources) const parentRoot = toRootHex(blockHeader.parentRoot); - const parentBlock = engine.forkChoice.getBlockHexDefaultStatus(parentRoot); + const parentBlock = this.forkChoice.getBlockHexDefaultStatus(parentRoot); if (parentBlock === null) { // If fork choice does *not* consider the parent to be a descendant of the finalized block, // then there are two more cases: @@ -109,7 +109,7 @@ export async function validateGossipFuluDataColumnSidecar( // As a result, we throw an IGNORE (whereas the spec says we should REJECT for this scenario). // this is something we should change this in the future to make the code airtight to the spec. // 7) [REJECT] The sidecar's block's parent passes validation. - const blockState = await engine.regen + const blockState = await this.regen .getBlockSlotState(parentBlock, blockHeader.slot, {dontTransferCache: true}, RegenCaller.validateGossipDataColumn) .catch(() => { throw new DataColumnSidecarGossipError(GossipAction.IGNORE, { @@ -137,15 +137,15 @@ export async function validateGossipFuluDataColumnSidecar( // 5) [REJECT] The proposer signature of sidecar.signed_block_header, is valid with respect to the block_header.proposer_index pubkey. const signature = dataColumnSidecar.signedBlockHeader.signature; - if (!engine.seenBlockInputCache.isVerifiedProposerSignature(blockHeader.slot, blockRootHex, signature)) { + if (!this.seenBlockInputCache.isVerifiedProposerSignature(blockHeader.slot, blockRootHex, signature)) { const signatureSet = getBlockHeaderProposerSignatureSetByParentStateSlot( - engine.config, + this.config, blockState.slot, dataColumnSidecar.signedBlockHeader ); if ( - !(await engine.bls.verifySignatureSets([signatureSet], { + !(await this.bls.verifySignatureSets([signatureSet], { // verify on main thread so that we only need to verify block proposer signature once per block verifyOnMainThread: true, })) @@ -158,7 +158,7 @@ export async function validateGossipFuluDataColumnSidecar( }); } - engine.seenBlockInputCache.markVerifiedProposerSignature(blockHeader.slot, blockRootHex, signature); + this.seenBlockInputCache.markVerifiedProposerSignature(blockHeader.slot, blockRootHex, signature); } // 9) [REJECT] The current finalized_checkpoint is an ancestor of the sidecar's block @@ -209,14 +209,14 @@ export async function validateGossipFuluDataColumnSidecar( // SPEC FUNCTION // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.3/specs/gloas/p2p-interface.md#data_column_sidecar_subnet_id export async function validateGossipGloasDataColumnSidecar( - engine: BeaconEngine, + this: BeaconEngine, payloadInput: PayloadEnvelopeInput, dataColumnSidecar: gloas.DataColumnSidecar, gossipSubnet: SubnetID, metrics: Metrics | null ): Promise { const blockRootHex = toRootHex(dataColumnSidecar.beaconBlockRoot); - const block = engine.forkChoice.getBlockHexDefaultStatus(blockRootHex); + const block = this.forkChoice.getBlockHexDefaultStatus(blockRootHex); // [IGNORE] A valid block for the sidecar's `slot` has been seen. if (block === null) { @@ -242,7 +242,7 @@ export async function validateGossipGloasDataColumnSidecar( verifyGloasDataColumnSidecar(dataColumnSidecar, kzgCommitments); // [REJECT] The sidecar must be on the correct subnet - if (computeSubnetForDataColumnSidecar(engine.config, dataColumnSidecar) !== gossipSubnet) { + if (computeSubnetForDataColumnSidecar(this.config, dataColumnSidecar) !== gossipSubnet) { throw new DataColumnSidecarGossipError(GossipAction.REJECT, { code: DataColumnSidecarErrorCode.INVALID_SUBNET, columnIndex: dataColumnSidecar.index, diff --git a/packages/beacon-node/src/chain/validation/executionPayloadBid.ts b/packages/beacon-node/src/chain/validation/executionPayloadBid.ts index dd184ed228d8..670331f5b993 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadBid.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadBid.ts @@ -16,21 +16,21 @@ import {ExecutionPayloadBidError, ExecutionPayloadBidErrorCode, GossipAction} fr import {RegenCaller} from "../regen/index.js"; export async function validateApiExecutionPayloadBid( - engine: BeaconEngine, + this: BeaconEngine, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid ): Promise<{proposerIndex: ValidatorIndex}> { - return validateExecutionPayloadBid(engine, signedExecutionPayloadBid); + return validateExecutionPayloadBid.call(this, signedExecutionPayloadBid); } export async function validateGossipExecutionPayloadBid( - engine: BeaconEngine, + this: BeaconEngine, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid ): Promise<{proposerIndex: ValidatorIndex}> { - return validateExecutionPayloadBid(engine, signedExecutionPayloadBid); + return validateExecutionPayloadBid.call(this, signedExecutionPayloadBid); } async function validateExecutionPayloadBid( - engine: BeaconEngine, + this: BeaconEngine, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid ): Promise<{proposerIndex: ValidatorIndex}> { const bid = signedExecutionPayloadBid.message; @@ -38,7 +38,7 @@ async function validateExecutionPayloadBid( const parentBlockHashHex = toRootHex(bid.parentBlockHash); // [IGNORE] `bid.slot` is the current slot or the next slot. - const currentSlot = engine.clock.currentSlot; + const currentSlot = this.clock.currentSlot; if (bid.slot !== currentSlot && bid.slot !== currentSlot + 1) { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.INVALID_SLOT, @@ -50,7 +50,7 @@ async function validateExecutionPayloadBid( // [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice. // Moved earlier than the spec ordering so we can derive the proposer dependent root for the // proposer-preferences lookup below from a known fork-choice block. - const parentBlock = engine.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex); + const parentBlock = this.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex); if (parentBlock === null) { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT, @@ -81,7 +81,7 @@ async function validateExecutionPayloadBid( // letting a raw `ForkChoiceError` escape the `GossipActionError` contract. const dependentRootHex = (() => { try { - return getShufflingDependentRoot(engine.forkChoice, bidEpoch, computeEpochAtSlot(parentBlock.slot), parentBlock); + return getShufflingDependentRoot(this.forkChoice, bidEpoch, computeEpochAtSlot(parentBlock.slot), parentBlock); } catch { return null; } @@ -98,7 +98,7 @@ async function validateExecutionPayloadBid( }); } - const proposerPreferences = engine.proposerPreferencesPool.get(bid.slot, dependentRootHex); + const proposerPreferences = this.proposerPreferencesPool.get(bid.slot, dependentRootHex); if (proposerPreferences === null) { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES, @@ -109,7 +109,7 @@ async function validateExecutionPayloadBid( } // Use the bid's parent branch state for builder checks - const state = await engine.regen + const state = await this.regen .getBlockSlotState(parentBlock, bid.slot, {dontTransferCache: true}, RegenCaller.validateGossipExecutionPayloadBid) .catch(() => { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { @@ -176,7 +176,7 @@ async function validateExecutionPayloadBid( // payload's hash) and EMPTY parents (EMPTY/PENDING variants carry the inherited parent // payload's hash, since the new block doesn't have its own payload). Variant carries the // executed payload's gas_limit, which we use as `parent_gas_limit` below. - const parentPayloadVariant = engine.forkChoice.getBlockHexAndBlockHash(parentBlockRootHex, parentBlockHashHex); + const parentPayloadVariant = this.forkChoice.getBlockHexAndBlockHash(parentBlockRootHex, parentBlockHashHex); if (parentPayloadVariant === null || parentPayloadVariant.executionPayloadBlockHash === null) { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH, @@ -204,7 +204,7 @@ async function validateExecutionPayloadBid( // consensus layer -- i.e. validate that // `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`. const blobKzgCommitmentsLen = bid.blobKzgCommitments.length; - const maxBlobsPerBlock = engine.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot)); + const maxBlobsPerBlock = this.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot)); if (blobKzgCommitmentsLen > maxBlobsPerBlock) { throw new ExecutionPayloadBidError(GossipAction.REJECT, { code: ExecutionPayloadBidErrorCode.TOO_MANY_KZG_COMMITMENTS, @@ -214,7 +214,7 @@ async function validateExecutionPayloadBid( } // [IGNORE] this is the first signed bid seen with a valid signature from the given builder for this slot. - if (engine.seenExecutionPayloadBids.isKnown(bid.slot, bid.builderIndex)) { + if (this.seenExecutionPayloadBids.isKnown(bid.slot, bid.builderIndex)) { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.BID_ALREADY_KNOWN, builderIndex: bid.builderIndex, @@ -226,7 +226,7 @@ async function validateExecutionPayloadBid( // [IGNORE] this bid is the highest value bid seen for the tuple // `(bid.slot, bid.parent_block_hash, bid.parent_block_root)`. - const bestBid = engine.executionPayloadBidPool.getBestBid(bid.slot, parentBlockHashHex, parentBlockRootHex); + const bestBid = this.executionPayloadBidPool.getBestBid(bid.slot, parentBlockHashHex, parentBlockRootHex); if (bestBid !== null && bestBid.message.value >= bid.value) { throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.BID_TOO_LOW, @@ -259,11 +259,11 @@ async function validateExecutionPayloadBid( // [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`. const signatureSet = createSingleSignatureSetFromComponents( PublicKey.fromBytes(builder.pubkey), - getExecutionPayloadBidSigningRoot(engine.config, state.slot, bid), + getExecutionPayloadBidSigningRoot(this.config, state.slot, bid), signedExecutionPayloadBid.signature ); - if (!(await engine.bls.verifySignatureSets([signatureSet]))) { + if (!(await this.bls.verifySignatureSets([signatureSet]))) { throw new ExecutionPayloadBidError(GossipAction.REJECT, { code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE, builderIndex: bid.builderIndex, @@ -272,7 +272,7 @@ async function validateExecutionPayloadBid( } // Valid - engine.seenExecutionPayloadBids.add(bid.slot, bid.builderIndex); + this.seenExecutionPayloadBids.add(bid.slot, bid.builderIndex); return {proposerIndex: proposerPreferences.message.validatorIndex}; } diff --git a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts index cbacdb47b344..baf07ebe86a7 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts @@ -13,15 +13,15 @@ import {RegenCaller} from "../regen/index.js"; // The `bid*` / `proposerIndex` values are read facade-side from the `PayloadEnvelopeInput` (owned by // BeaconChain) and passed in as scalars — the engine no longer touches the DA seen cache. export async function validateApiExecutionPayloadEnvelope( - engine: BeaconEngine, + this: BeaconEngine, executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope, proposerIndex: ValidatorIndex, bidBuilderIndex: ValidatorIndex, bidBlockHashHex: RootHex, bidExecutionRequestsRoot: Root ): Promise { - return validateExecutionPayloadEnvelope( - engine, + return validateExecutionPayloadEnvelope.call( + this, executionPayloadEnvelope, proposerIndex, bidBuilderIndex, @@ -31,15 +31,15 @@ export async function validateApiExecutionPayloadEnvelope( } export async function validateGossipExecutionPayloadEnvelope( - engine: BeaconEngine, + this: BeaconEngine, executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope, proposerIndex: ValidatorIndex, bidBuilderIndex: ValidatorIndex, bidBlockHashHex: RootHex, bidExecutionRequestsRoot: Root ): Promise { - return validateExecutionPayloadEnvelope( - engine, + return validateExecutionPayloadEnvelope.call( + this, executionPayloadEnvelope, proposerIndex, bidBuilderIndex, @@ -49,7 +49,7 @@ export async function validateGossipExecutionPayloadEnvelope( } async function validateExecutionPayloadEnvelope( - engine: BeaconEngine, + this: BeaconEngine, executionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope, proposerIndex: ValidatorIndex, bidBuilderIndex: ValidatorIndex, @@ -63,7 +63,7 @@ async function validateExecutionPayloadEnvelope( // [IGNORE] The envelope's block root `envelope.beacon_block_root` has been seen (via // gossip or non-gossip sources) (a client MAY queue payload for processing once // the block is retrieved). - const block = engine.forkChoice.getBlockDefaultStatus(envelope.beaconBlockRoot); + const block = this.forkChoice.getBlockDefaultStatus(envelope.beaconBlockRoot); if (block === null) { throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { code: ExecutionPayloadEnvelopeErrorCode.BLOCK_ROOT_UNKNOWN, @@ -73,9 +73,9 @@ async function validateExecutionPayloadEnvelope( // [IGNORE] The node has not seen another valid // `SignedExecutionPayloadEnvelope` for this block root from this builder. - const envelopeBlock = engine.forkChoice.getBlockHex(blockRootHex, PayloadStatus.FULL); + const envelopeBlock = this.forkChoice.getBlockHex(blockRootHex, PayloadStatus.FULL); - // const payloadInput = engine.seenPayloadEnvelopeInputCache.get(blockRootHex); + // const payloadInput = this.seenPayloadEnvelopeInputCache.get(blockRootHex); // if (envelopeBlock || payloadInput?.hasPayloadEnvelope()) { // TODO - beacon engine: unstable also check seenPayloadEnvelopeInputCache but we should not do it // see also https://github.com/ethereum/consensus-specs/pull/5355 @@ -88,7 +88,7 @@ async function validateExecutionPayloadEnvelope( } // [IGNORE] The envelope is from a slot greater than or equal to the latest finalized slot -- i.e. validate that `payload.slotNumber >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)` - const finalizedCheckpoint = engine.forkChoice.getFinalizedCheckpoint(); + const finalizedCheckpoint = this.forkChoice.getFinalizedCheckpoint(); const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch); if (payload.slotNumber < finalizedSlot) { throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { @@ -140,15 +140,13 @@ async function validateExecutionPayloadEnvelope( } // Get the block state to verify the builder's signature. - const blockState = await engine.regen - .getState(block.stateRoot, RegenCaller.validateGossipPayloadEnvelope) - .catch(() => { - throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { - code: ExecutionPayloadEnvelopeErrorCode.UNKNOWN_BLOCK_STATE, - blockRoot: blockRootHex, - slot: payload.slotNumber, - }); + const blockState = await this.regen.getState(block.stateRoot, RegenCaller.validateGossipPayloadEnvelope).catch(() => { + throw new ExecutionPayloadEnvelopeError(GossipAction.IGNORE, { + code: ExecutionPayloadEnvelopeErrorCode.UNKNOWN_BLOCK_STATE, + blockRoot: blockRootHex, + slot: payload.slotNumber, }); + }); if (!isStatePostGloas(blockState)) { throw new Error(`Expected gloas+ state for execution payload envelope validation, got fork=${blockState.forkName}`); } @@ -156,14 +154,14 @@ async function validateExecutionPayloadEnvelope( // [REJECT] `signed_execution_payload_envelope.signature` is valid as verified // by `verify_execution_payload_envelope_signature`. const signatureSet = getExecutionPayloadEnvelopeSignatureSet( - engine.config, - engine.pubkeyCache, + this.config, + this.pubkeyCache, blockState, executionPayloadEnvelope, proposerIndex ); - if (!(await engine.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { + if (!(await this.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) { throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE, }); diff --git a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts index 8ddae054aca3..6e3f19afdc80 100644 --- a/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts +++ b/packages/beacon-node/src/chain/validation/payloadAttestationMessage.ts @@ -16,22 +16,22 @@ export type PayloadAttestationValidationResult = { }; export async function validateApiPayloadAttestationMessage( - engine: BeaconEngine, + this: BeaconEngine, payloadAttestationMessage: gloas.PayloadAttestationMessage ): Promise { const prioritizeBls = true; - return validatePayloadAttestationMessage(engine, payloadAttestationMessage, prioritizeBls); + return validatePayloadAttestationMessage.call(this, payloadAttestationMessage, prioritizeBls); } export async function validateGossipPayloadAttestationMessage( - engine: BeaconEngine, + this: BeaconEngine, payloadAttestationMessage: gloas.PayloadAttestationMessage ): Promise { - return validatePayloadAttestationMessage(engine, payloadAttestationMessage); + return validatePayloadAttestationMessage.call(this, payloadAttestationMessage); } async function validatePayloadAttestationMessage( - engine: BeaconEngine, + this: BeaconEngine, payloadAttestationMessage: gloas.PayloadAttestationMessage, prioritizeBls = false ): Promise { @@ -39,10 +39,10 @@ async function validatePayloadAttestationMessage( const epoch = computeEpochAtSlot(data.slot); // [IGNORE] The message's slot is for the current slot (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance), i.e. `data.slot == current_slot`. - if (!engine.clock.isCurrentSlotGivenGossipDisparity(data.slot)) { + if (!this.clock.isCurrentSlotGivenGossipDisparity(data.slot)) { throw new PayloadAttestationError(GossipAction.IGNORE, { code: PayloadAttestationErrorCode.NOT_CURRENT_SLOT, - currentSlot: engine.clock.currentSlot, + currentSlot: this.clock.currentSlot, slot: data.slot, }); } @@ -50,7 +50,7 @@ async function validatePayloadAttestationMessage( // [IGNORE] The `payload_attestation_message` is the first valid message received // from the validator with index `payload_attestation_message.validator_index`. // A single validator can participate PTC at most once per epoch - if (engine.seenPayloadAttesters.isKnown(epoch, validatorIndex)) { + if (this.seenPayloadAttesters.isKnown(epoch, validatorIndex)) { throw new PayloadAttestationError(GossipAction.IGNORE, { code: PayloadAttestationErrorCode.PAYLOAD_ATTESTATION_ALREADY_KNOWN, validatorIndex, @@ -62,7 +62,7 @@ async function validatePayloadAttestationMessage( // [IGNORE] The message's block `data.beacon_block_root` has been seen (via // gossip or non-gossip sources) (a client MAY queue attestation for processing // once the block is retrieved. Note a client might want to request payload after). - const block = engine.forkChoice.getBlockDefaultStatus(data.beaconBlockRoot); + const block = this.forkChoice.getBlockDefaultStatus(data.beaconBlockRoot); if (!block) { throw new PayloadAttestationError(GossipAction.IGNORE, { code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT, @@ -86,7 +86,7 @@ async function validatePayloadAttestationMessage( // it is possible that the block didn't pass the validation // Use the referenced block's branch state for the PTC committee check - const state = await engine.regen + const state = await this.regen .getBlockSlotState(block, data.slot, {dontTransferCache: true}, RegenCaller.validateGossipPayloadAttestationMessage) .catch(() => { throw new PayloadAttestationError(GossipAction.IGNORE, { @@ -114,7 +114,7 @@ async function validatePayloadAttestationMessage( } // [REJECT] `payload_attestation_message.signature` is valid with respect to the validator's public key. - const validatorPubkey = engine.pubkeyCache.get(validatorIndex); + const validatorPubkey = this.pubkeyCache.get(validatorIndex); if (!validatorPubkey) { throw new PayloadAttestationError(GossipAction.REJECT, { code: PayloadAttestationErrorCode.INVALID_ATTESTER, @@ -124,18 +124,18 @@ async function validatePayloadAttestationMessage( const signatureSet = createSingleSignatureSetFromComponents( validatorPubkey, - getPayloadAttestationDataSigningRoot(engine.config, data), + getPayloadAttestationDataSigningRoot(this.config, data), payloadAttestationMessage.signature ); - if (!(await engine.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { + if (!(await this.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { throw new PayloadAttestationError(GossipAction.REJECT, { code: PayloadAttestationErrorCode.INVALID_SIGNATURE, }); } // Valid - engine.seenPayloadAttesters.add(epoch, validatorIndex); + this.seenPayloadAttesters.add(epoch, validatorIndex); return { attDataRootHex: toRootHex(ssz.gloas.PayloadAttestationData.hashTreeRoot(data)), diff --git a/packages/beacon-node/src/chain/validation/proposerPreferences.ts b/packages/beacon-node/src/chain/validation/proposerPreferences.ts index 9ca61e7516ec..3c92fa3bc820 100644 --- a/packages/beacon-node/src/chain/validation/proposerPreferences.ts +++ b/packages/beacon-node/src/chain/validation/proposerPreferences.ts @@ -14,7 +14,7 @@ import {GossipAction, ProposerPreferencesError, ProposerPreferencesErrorCode} fr * https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#proposer_preferences */ export async function validateGossipProposerPreferences( - engine: BeaconEngine, + this: BeaconEngine, signedProposerPreferences: gloas.SignedProposerPreferences ): Promise { const preferences = signedProposerPreferences.message; @@ -23,7 +23,7 @@ export async function validateGossipProposerPreferences( const proposalEpoch = computeEpochAtSlot(proposalSlot); // [IGNORE] `preferences.proposal_slot` is in the current or next epoch. - const currentEpoch = engine.clock.currentEpoch; + const currentEpoch = this.clock.currentEpoch; if (proposalEpoch < currentEpoch || proposalEpoch > currentEpoch + 1) { throw new ProposerPreferencesError(GossipAction.IGNORE, { code: ProposerPreferencesErrorCode.INVALID_EPOCH, @@ -33,7 +33,7 @@ export async function validateGossipProposerPreferences( } // [IGNORE] `preferences.proposal_slot` has not already passed. - const currentSlot = engine.clock.currentSlot; + const currentSlot = this.clock.currentSlot; if (proposalSlot <= currentSlot) { throw new ProposerPreferencesError(GossipAction.IGNORE, { code: ProposerPreferencesErrorCode.PROPOSAL_SLOT_PASSED, @@ -47,7 +47,7 @@ export async function validateGossipProposerPreferences( // the previous-root checkpoint state (populated by `processSlotsToNearestCheckpoint` for // any imported branch crossing into `proposalEpoch - 1`). The head-state path also handles // narrow timing windows where the checkpoint state isn't yet populated. - const headState = engine.getHeadState(); + const headState = this.getHeadState(); let proposers: ValidatorIndex[] | null = null; if (headState.epoch === proposalEpoch && headState.currentDecisionRoot === dependentRootHex) { proposers = headState.currentProposers; @@ -55,7 +55,7 @@ export async function validateGossipProposerPreferences( proposers = headState.nextProposers; } else { // Sync lookup only to not trigger disk reload from gossip input. - const checkpointState = engine.regen.getCheckpointStateSync({epoch: proposalEpoch - 1, rootHex: dependentRootHex}); + const checkpointState = this.regen.getCheckpointStateSync({epoch: proposalEpoch - 1, rootHex: dependentRootHex}); if (checkpointState !== null) { // State is at `proposalEpoch - 1`, so proposers for `proposalSlot` (next epoch from // the state's perspective) live in `nextProposers`. @@ -81,7 +81,7 @@ export async function validateGossipProposerPreferences( } // [IGNORE] First valid message for (dependent_root, proposal_slot, validator_index). - if (engine.seenProposerPreferences.isKnown(dependentRootHex, proposalSlot, validatorIndex)) { + if (this.seenProposerPreferences.isKnown(dependentRootHex, proposalSlot, validatorIndex)) { throw new ProposerPreferencesError(GossipAction.IGNORE, { code: ProposerPreferencesErrorCode.ALREADY_KNOWN, proposalSlot, @@ -92,12 +92,12 @@ export async function validateGossipProposerPreferences( // [REJECT] `signed_proposer_preferences.signature` is valid with respect to the validator's public key. const signatureSet = createSingleSignatureSetFromComponents( - engine.pubkeyCache.getOrThrow(validatorIndex), - getProposerPreferencesSigningRoot(engine.config, preferences), + this.pubkeyCache.getOrThrow(validatorIndex), + getProposerPreferencesSigningRoot(this.config, preferences), signedProposerPreferences.signature ); - if (!(await engine.bls.verifySignatureSets([signatureSet], {batchable: true}))) { + if (!(await this.bls.verifySignatureSets([signatureSet], {batchable: true}))) { throw new ProposerPreferencesError(GossipAction.REJECT, { code: ProposerPreferencesErrorCode.INVALID_SIGNATURE, proposalSlot, @@ -106,5 +106,5 @@ export async function validateGossipProposerPreferences( } // Valid - engine.seenProposerPreferences.add(dependentRootHex, proposalSlot, validatorIndex); + this.seenProposerPreferences.add(dependentRootHex, proposalSlot, validatorIndex); } diff --git a/packages/beacon-node/src/chain/validation/proposerSlashing.ts b/packages/beacon-node/src/chain/validation/proposerSlashing.ts index 0a2459753f29..0bf9277876bd 100644 --- a/packages/beacon-node/src/chain/validation/proposerSlashing.ts +++ b/packages/beacon-node/src/chain/validation/proposerSlashing.ts @@ -4,40 +4,40 @@ import type {BeaconEngine} from "../beaconEngine/beaconEngine.js"; import {GossipAction, ProposerSlashingError, ProposerSlashingErrorCode} from "../errors/index.js"; export async function validateApiProposerSlashing( - engine: BeaconEngine, + this: BeaconEngine, proposerSlashing: phase0.ProposerSlashing ): Promise { const prioritizeBls = true; - return validateProposerSlashing(engine, proposerSlashing, prioritizeBls); + return validateProposerSlashing.call(this, proposerSlashing, prioritizeBls); } export async function validateGossipProposerSlashing( - engine: BeaconEngine, + this: BeaconEngine, proposerSlashing: phase0.ProposerSlashing ): Promise { - return validateProposerSlashing(engine, proposerSlashing); + return validateProposerSlashing.call(this, proposerSlashing); } async function validateProposerSlashing( - engine: BeaconEngine, + this: BeaconEngine, proposerSlashing: phase0.ProposerSlashing, prioritizeBls = false ): Promise { // [IGNORE] The proposer slashing is the first valid proposer slashing received for the proposer with index // proposer_slashing.signed_header_1.message.proposer_index. - if (engine.opPool.hasSeenProposerSlashing(proposerSlashing.signedHeader1.message.proposerIndex)) { + if (this.opPool.hasSeenProposerSlashing(proposerSlashing.signedHeader1.message.proposerIndex)) { throw new ProposerSlashingError(GossipAction.IGNORE, { code: ProposerSlashingErrorCode.ALREADY_EXISTS, }); } - const state = engine.getHeadState(); + const state = this.getHeadState(); // [REJECT] All of the conditions within process_proposer_slashing pass validation. try { const proposer = state.getValidator(proposerSlashing.signedHeader1.message.proposerIndex); // verifySignature = false, verified in batch below - assertValidProposerSlashing(engine.config, engine.pubkeyCache, state.slot, proposerSlashing, proposer, false); + assertValidProposerSlashing(this.config, this.pubkeyCache, state.slot, proposerSlashing, proposer, false); } catch (e) { throw new ProposerSlashingError(GossipAction.REJECT, { code: ProposerSlashingErrorCode.INVALID, @@ -45,8 +45,8 @@ async function validateProposerSlashing( }); } - const signatureSets = getProposerSlashingSignatureSets(engine.config, state.slot, proposerSlashing); - if (!(await engine.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { + const signatureSets = getProposerSlashingSignatureSets(this.config, state.slot, proposerSlashing); + if (!(await this.bls.verifySignatureSets(signatureSets, {batchable: true, priority: prioritizeBls}))) { throw new ProposerSlashingError(GossipAction.REJECT, { code: ProposerSlashingErrorCode.INVALID, error: Error("Invalid signature"), diff --git a/packages/beacon-node/src/chain/validation/syncCommittee.ts b/packages/beacon-node/src/chain/validation/syncCommittee.ts index ece83579c103..13a076ca4565 100644 --- a/packages/beacon-node/src/chain/validation/syncCommittee.ts +++ b/packages/beacon-node/src/chain/validation/syncCommittee.ts @@ -12,15 +12,15 @@ type IndexInSubcommittee = number; * Spec v1.1.0-alpha.8 */ export async function validateGossipSyncCommittee( - engine: BeaconEngine, + this: BeaconEngine, syncCommittee: altair.SyncCommitteeMessage, subnet: SubnetID ): Promise<{indicesInSubcommittee: IndexInSubcommittee[]}> { const {slot, validatorIndex, beaconBlockRoot} = syncCommittee; const messageRoot = toRootHex(beaconBlockRoot); - const headState = engine.getHeadState(); - const indicesInSubcommittee = validateGossipSyncCommitteeExceptSig(engine, headState, subnet, syncCommittee); + const headState = this.getHeadState(); + const indicesInSubcommittee = validateGossipSyncCommitteeExceptSig.call(this, headState, subnet, syncCommittee); // [IGNORE] The signature's slot is for the current slot, i.e. sync_committee_signature.slot == current_slot. // > Checked in validateGossipSyncCommitteeExceptSig() @@ -32,16 +32,16 @@ export async function validateGossipSyncCommittee( // [IGNORE] There has been no other valid sync committee signature for the declared slot for the validator referenced // by sync_committee_signature.validator_index. - const prevRoot = engine.seenSyncCommitteeMessages.get(slot, subnet, validatorIndex); + const prevRoot = this.seenSyncCommitteeMessages.get(slot, subnet, validatorIndex); if (prevRoot) { let shouldIgnore = false; if (prevRoot === messageRoot) { shouldIgnore = true; } else { - const headRoot = engine.forkChoice.getHeadRoot(); - engine.metrics?.gossipSyncCommittee.equivocationCount.inc(); + const headRoot = this.forkChoice.getHeadRoot(); + this.metrics?.gossipSyncCommittee.equivocationCount.inc(); if (messageRoot === headRoot) { - engine.metrics?.gossipSyncCommittee.equivocationToHeadCount.inc(); + this.metrics?.gossipSyncCommittee.equivocationToHeadCount.inc(); } else { shouldIgnore = true; } @@ -63,35 +63,35 @@ export async function validateGossipSyncCommittee( // > Checked in validateGossipSyncCommitteeExceptSig() // [REJECT] The signature is valid for the message beacon_block_root for the validator referenced by validator_index. - await validateSyncCommitteeSigOnly(engine, headState, syncCommittee); + await validateSyncCommitteeSigOnly.call(this, headState, syncCommittee); // Register this valid item as seen - engine.seenSyncCommitteeMessages.add(slot, subnet, validatorIndex, messageRoot); + this.seenSyncCommitteeMessages.add(slot, subnet, validatorIndex, messageRoot); return {indicesInSubcommittee}; } export async function validateApiSyncCommittee( - engine: BeaconEngine, + this: BeaconEngine, syncCommittee: altair.SyncCommitteeMessage ): Promise { // Resolve head state internally — no IBeaconStateView crosses the engine seam. - const headState = engine.getHeadState(); + const headState = this.getHeadState(); const prioritizeBls = true; - return validateSyncCommitteeSigOnly(engine, headState, syncCommittee, prioritizeBls); + return validateSyncCommitteeSigOnly.call(this, headState, syncCommittee, prioritizeBls); } /** * Abstracted so it can be re-used in API validation. */ async function validateSyncCommitteeSigOnly( - engine: BeaconEngine, + this: BeaconEngine, headState: IBeaconStateView, syncCommittee: altair.SyncCommitteeMessage, prioritizeBls = false ): Promise { - const signatureSet = getSyncCommitteeSignatureSet(engine.config, headState, syncCommittee); - if (!(await engine.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { + const signatureSet = getSyncCommitteeSignatureSet(this.config, headState, syncCommittee); + if (!(await this.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { throw new SyncCommitteeError(GossipAction.REJECT, { code: SyncCommitteeErrorCode.INVALID_SIGNATURE, }); @@ -102,7 +102,7 @@ async function validateSyncCommitteeSigOnly( * Spec v1.1.0-alpha.8 */ export function validateGossipSyncCommitteeExceptSig( - engine: BeaconEngine, + this: BeaconEngine, headState: IBeaconStateView, subnet: SubnetID, data: Pick @@ -110,10 +110,10 @@ export function validateGossipSyncCommitteeExceptSig( const {slot, validatorIndex} = data; // [IGNORE] The signature's slot is for the current slot, i.e. sync_committee_signature.slot == current_slot. // (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) - if (!engine.clock.isCurrentSlotGivenGossipDisparity(slot)) { + if (!this.clock.isCurrentSlotGivenGossipDisparity(slot)) { throw new SyncCommitteeError(GossipAction.IGNORE, { code: SyncCommitteeErrorCode.NOT_CURRENT_SLOT, - currentSlot: engine.clock.currentSlot, + currentSlot: this.clock.currentSlot, slot, }); } diff --git a/packages/beacon-node/src/chain/validation/syncCommitteeContributionAndProof.ts b/packages/beacon-node/src/chain/validation/syncCommitteeContributionAndProof.ts index 52850609a703..25f666c79316 100644 --- a/packages/beacon-node/src/chain/validation/syncCommitteeContributionAndProof.ts +++ b/packages/beacon-node/src/chain/validation/syncCommitteeContributionAndProof.ts @@ -14,7 +14,7 @@ import {validateGossipSyncCommitteeExceptSig} from "./syncCommittee.js"; * Spec v1.1.0-beta.2 */ export async function validateSyncCommitteeGossipContributionAndProof( - engine: BeaconEngine, + this: BeaconEngine, signedContributionAndProof: altair.SignedContributionAndProof, skipValidationKnownParticipants = false ): Promise<{syncCommitteeParticipantIndices: ValidatorIndex[]}> { @@ -22,8 +22,8 @@ export async function validateSyncCommitteeGossipContributionAndProof( const {contribution, aggregatorIndex} = contributionAndProof; const {subcommitteeIndex, slot} = contribution; - const headState = engine.getHeadState(); - validateGossipSyncCommitteeExceptSig(engine, headState, subcommitteeIndex, { + const headState = this.getHeadState(); + validateGossipSyncCommitteeExceptSig.call(this, headState, subcommitteeIndex, { slot, validatorIndex: contributionAndProof.aggregatorIndex, }); @@ -38,7 +38,7 @@ export async function validateSyncCommitteeGossipContributionAndProof( // _[IGNORE]_ A valid sync committee contribution with equal `slot`, `beacon_block_root` and `subcommittee_index` whose // `aggregation_bits` is non-strict superset has _not_ already been seen. - if (!skipValidationKnownParticipants && engine.seenContributionAndProof.participantsKnown(contribution)) { + if (!skipValidationKnownParticipants && this.seenContributionAndProof.participantsKnown(contribution)) { throw new SyncCommitteeError(GossipAction.IGNORE, { code: SyncCommitteeErrorCode.SYNC_COMMITTEE_PARTICIPANTS_ALREADY_KNOWN, }); @@ -46,7 +46,7 @@ export async function validateSyncCommitteeGossipContributionAndProof( // [IGNORE] The sync committee contribution is the first valid contribution received for the aggregator with index // contribution_and_proof.aggregator_index for the slot contribution.slot and subcommittee index contribution.subcommittee_index. - if (engine.seenContributionAndProof.isAggregatorKnown(slot, subcommitteeIndex, aggregatorIndex)) { + if (this.seenContributionAndProof.isAggregatorKnown(slot, subcommitteeIndex, aggregatorIndex)) { throw new SyncCommitteeError(GossipAction.IGNORE, { code: SyncCommitteeErrorCode.SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN, }); @@ -76,24 +76,24 @@ export async function validateSyncCommitteeGossipContributionAndProof( const signatureSets = [ // [REJECT] The contribution_and_proof.selection_proof is a valid signature of the SyncAggregatorSelectionData // derived from the contribution by the validator with index contribution_and_proof.aggregator_index. - getSyncCommitteeSelectionProofSignatureSet(engine.config, headState, contributionAndProof), + getSyncCommitteeSelectionProofSignatureSet(this.config, headState, contributionAndProof), // [REJECT] The aggregator signature, signed_contribution_and_proof.signature, is valid. - getContributionAndProofSignatureSet(engine.config, headState, signedContributionAndProof), + getContributionAndProofSignatureSet(this.config, headState, signedContributionAndProof), // [REJECT] The aggregate signature is valid for the message beacon_block_root and aggregate pubkey derived from // the participation info in aggregation_bits for the subcommittee specified by the contribution.subcommittee_index. - getSyncCommitteeContributionSignatureSet(engine.config, headState, contribution, syncCommitteeParticipantIndices), + getSyncCommitteeContributionSignatureSet(this.config, headState, contribution, syncCommitteeParticipantIndices), ]; - if (!(await engine.bls.verifySignatureSets(signatureSets, {batchable: true}))) { + if (!(await this.bls.verifySignatureSets(signatureSets, {batchable: true}))) { throw new SyncCommitteeError(GossipAction.REJECT, { code: SyncCommitteeErrorCode.INVALID_SIGNATURE, }); } // no need to add to seenSyncCommittteeContributionCache here, gossip handler will do that - engine.seenContributionAndProof.add(contributionAndProof, syncCommitteeParticipantIndices.length); + this.seenContributionAndProof.add(contributionAndProof, syncCommitteeParticipantIndices.length); return {syncCommitteeParticipantIndices}; } diff --git a/packages/beacon-node/src/chain/validation/voluntaryExit.ts b/packages/beacon-node/src/chain/validation/voluntaryExit.ts index 9c8e596b9c59..214c8cba5abf 100644 --- a/packages/beacon-node/src/chain/validation/voluntaryExit.ts +++ b/packages/beacon-node/src/chain/validation/voluntaryExit.ts @@ -10,28 +10,28 @@ import { import {RegenCaller} from "../regen/index.js"; export async function validateApiVoluntaryExit( - engine: BeaconEngine, + this: BeaconEngine, voluntaryExit: phase0.SignedVoluntaryExit ): Promise { const prioritizeBls = true; - return validateVoluntaryExit(engine, voluntaryExit, prioritizeBls); + return validateVoluntaryExit.call(this, voluntaryExit, prioritizeBls); } export async function validateGossipVoluntaryExit( - engine: BeaconEngine, + this: BeaconEngine, voluntaryExit: phase0.SignedVoluntaryExit ): Promise { - return validateVoluntaryExit(engine, voluntaryExit); + return validateVoluntaryExit.call(this, voluntaryExit); } async function validateVoluntaryExit( - engine: BeaconEngine, + this: BeaconEngine, voluntaryExit: phase0.SignedVoluntaryExit, prioritizeBls = false ): Promise { // [IGNORE] The voluntary exit is the first valid voluntary exit received for the validator with index // signed_voluntary_exit.message.validator_index. - if (engine.opPool.hasSeenVoluntaryExit(voluntaryExit.message.validatorIndex)) { + if (this.opPool.hasSeenVoluntaryExit(voluntaryExit.message.validatorIndex)) { throw new VoluntaryExitError(GossipAction.IGNORE, { code: VoluntaryExitErrorCode.ALREADY_EXISTS, }); @@ -44,7 +44,7 @@ async function validateVoluntaryExit( // The voluntaryExit.epoch must be in the past but the validator's status may change in recent epochs. // We dial the head state to the current epoch to get the current status of the validator. This is // relevant on periods of many skipped slots. - const state = await engine.getHeadStateAtCurrentEpoch(RegenCaller.validateGossipVoluntaryExit); + const state = await this.getHeadStateAtCurrentEpoch(RegenCaller.validateGossipVoluntaryExit); // [REJECT] All of the conditions within process_voluntary_exit pass validation. // verifySignature = false, verified in batch below @@ -55,8 +55,8 @@ async function validateVoluntaryExit( }); } - const signatureSet = getVoluntaryExitSignatureSet(engine.config, state, voluntaryExit); - if (!(await engine.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { + const signatureSet = getVoluntaryExitSignatureSet(this.config, state, voluntaryExit); + if (!(await this.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { throw new VoluntaryExitError(GossipAction.REJECT, { code: VoluntaryExitErrorCode.INVALID_SIGNATURE, }); diff --git a/packages/beacon-node/test/perf/chain/validation/aggregateAndProof.test.ts b/packages/beacon-node/test/perf/chain/validation/aggregateAndProof.test.ts index daf454a59976..8186a201110e 100644 --- a/packages/beacon-node/test/perf/chain/validation/aggregateAndProof.test.ts +++ b/packages/beacon-node/test/perf/chain/validation/aggregateAndProof.test.ts @@ -27,7 +27,7 @@ describe("validate gossip signedAggregateAndProof", () => { }, fn: async () => { const fork = chain.config.getForkName(stateSlot); - await validateApiAggregateAndProof(fork, chain.beaconEngine as BeaconEngine, agg); + await validateApiAggregateAndProof.call(chain.beaconEngine as BeaconEngine, fork, agg); }, }); @@ -39,7 +39,7 @@ describe("validate gossip signedAggregateAndProof", () => { }, fn: async () => { const fork = chain.config.getForkName(stateSlot); - await validateGossipAggregateAndProof(fork, chain.beaconEngine as BeaconEngine, agg, serializedData); + await validateGossipAggregateAndProof.call(chain.beaconEngine as BeaconEngine, fork, agg, serializedData); }, }); } diff --git a/packages/beacon-node/test/perf/chain/validation/attestation.test.ts b/packages/beacon-node/test/perf/chain/validation/attestation.test.ts index 9b2ddefaa4db..77395c50e50d 100644 --- a/packages/beacon-node/test/perf/chain/validation/attestation.test.ts +++ b/packages/beacon-node/test/perf/chain/validation/attestation.test.ts @@ -54,7 +54,11 @@ describe("validate gossip attestation", () => { id: `batch validate gossip attestation - vc ${vc} - chunk ${chunkSize}`, beforeEach: () => chain.seenAttesters["validatorIndexesByEpoch"].clear(), fn: async () => { - await validateGossipAttestationsSameAttData(fork, chain.beaconEngine as BeaconEngine, attestationOrBytesArr); + await validateGossipAttestationsSameAttData.call( + chain.beaconEngine as BeaconEngine, + fork, + attestationOrBytesArr + ); }, runsFactor: chunkSize, }); diff --git a/packages/beacon-node/test/spec/utils/gossipValidation.ts b/packages/beacon-node/test/spec/utils/gossipValidation.ts index a18220a45c49..f75289df6873 100644 --- a/packages/beacon-node/test/spec/utils/gossipValidation.ts +++ b/packages/beacon-node/test/spec/utils/gossipValidation.ts @@ -639,7 +639,7 @@ async function validateMessageForTopic( throw new GossipActionError(GossipAction.IGNORE, {code: "SPEC_FINALIZED_NOT_ANCESTOR"}); } - await validateGossipAggregateAndProof(fork, chain.beaconEngine, aggregate, bytes); + await validateGossipAggregateAndProof.call(chain.beaconEngine, fork, aggregate, bytes); break; } @@ -672,7 +672,9 @@ async function validateMessageForTopic( subnet: Number(message.subnet_id ?? 0), }; - const batchResult = await validateGossipAttestationsSameAttData(fork, chain.beaconEngine, [gossipAttestation]); + const batchResult = await validateGossipAttestationsSameAttData.call(chain.beaconEngine, fork, [ + gossipAttestation, + ]); const first = batchResult.results[0]; if (first?.err) throw first.err; break; diff --git a/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts b/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts index 9dc77cf16898..060e5240a135 100644 --- a/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts @@ -41,7 +41,7 @@ describe("chain / validation / aggregateAndProof", () => { const {chain, signedAggregateAndProof} = getValidData({}); const fork = chain.config.getForkName(stateSlot); - await validateApiAggregateAndProof(fork, chain.beaconEngine as BeaconEngine, signedAggregateAndProof); + await validateApiAggregateAndProof.call(chain.beaconEngine as BeaconEngine, fork, signedAggregateAndProof); }); it("BAD_TARGET_EPOCH", async () => { @@ -187,9 +187,9 @@ describe("chain / validation / aggregateAndProof", () => { const fork = chain.config.getForkName(stateSlot); const serializedData = ssz.phase0.SignedAggregateAndProof.serialize(signedAggregateAndProof); await expectRejectedWithLodestarError( - validateGossipAggregateAndProof( - fork, + validateGossipAggregateAndProof.call( chain.beaconEngine as BeaconEngine, + fork, signedAggregateAndProof, serializedData ), diff --git a/packages/beacon-node/test/unit/chain/validation/attestation/getShufflingForAttestationVerification.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation/getShufflingForAttestationVerification.test.ts index 6cb921123855..2a401b38597f 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation/getShufflingForAttestationVerification.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation/getShufflingForAttestationVerification.test.ts @@ -50,7 +50,7 @@ describe("getShufflingForAttestationVerification", () => { } return Promise.resolve(null); }); - const resultShuffling = await getShufflingForAttestationVerification( + const resultShuffling = await getShufflingForAttestationVerification.call( chain.beaconEngine, attEpoch, attHeadBlock as ProtoBlock, @@ -80,7 +80,7 @@ describe("getShufflingForAttestationVerification", () => { } return Promise.resolve(null); }); - const resultShuffling = await getShufflingForAttestationVerification( + const resultShuffling = await getShufflingForAttestationVerification.call( chain.beaconEngine, attEpoch, attHeadBlock as ProtoBlock, @@ -112,7 +112,7 @@ describe("getShufflingForAttestationVerification", () => { Promise.resolve(expectedShuffling) ); - const resultShuffling = await getShufflingForAttestationVerification( + const resultShuffling = await getShufflingForAttestationVerification.call( chain.beaconEngine, attEpoch, attHeadBlock as ProtoBlock, @@ -131,7 +131,7 @@ describe("getShufflingForAttestationVerification", () => { blockRoot, } as Partial; try { - await getShufflingForAttestationVerification( + await getShufflingForAttestationVerification.call( chain.beaconEngine, attEpoch, attHeadBlock as ProtoBlock, diff --git a/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts index eb90b53b138a..14f460235970 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation/validateAttestation.test.ts @@ -48,7 +48,7 @@ describe("validateAttestation", () => { const {chain, attestation} = getValidData(); const fork = chain.config.getForkName(stateSlot); - await validateApiAttestation(fork, chain.beaconEngine as BeaconEngine, {attestation, serializedData: null}); + await validateApiAttestation.call(chain.beaconEngine as BeaconEngine, fork, {attestation, serializedData: null}); }); it("INVALID_SERIALIZED_BYTES_ERROR_CODE", async () => { @@ -304,7 +304,7 @@ describe("validateAttestation", () => { ): Promise { const fork = chain.config.getForkName(stateSlot); await expectRejectedWithLodestarError( - validateApiAttestation(fork, chain.beaconEngine as BeaconEngine, attestationOrBytes), + validateApiAttestation.call(chain.beaconEngine as BeaconEngine, fork, attestationOrBytes), errorCode ); } @@ -315,7 +315,7 @@ describe("validateAttestation", () => { errorCode: string ): Promise { const fork = chain.config.getForkName(stateSlot); - const {results} = await validateGossipAttestationsSameAttData(fork, chain.beaconEngine as BeaconEngine, [ + const {results} = await validateGossipAttestationsSameAttData.call(chain.beaconEngine as BeaconEngine, fork, [ attestationOrBytes, ]); expect(results.length).toEqual(1); diff --git a/packages/beacon-node/test/unit/chain/validation/attestation/validateGossipAttestationsSameAttData.test.ts b/packages/beacon-node/test/unit/chain/validation/attestation/validateGossipAttestationsSameAttData.test.ts index 5556d4b6a157..12972bb16056 100644 --- a/packages/beacon-node/test/unit/chain/validation/attestation/validateGossipAttestationsSameAttData.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attestation/validateGossipAttestationsSameAttData.test.ts @@ -124,7 +124,12 @@ describe("validateGossipAttestationsSameAttData", () => { callIndex++; return result; }; - await validateGossipAttestationsSameAttData(ForkName.phase0, engine, new Array(5).fill({}), phase0ValidationFn); + await validateGossipAttestationsSameAttData.call( + engine, + ForkName.phase0, + new Array(5).fill({}), + phase0ValidationFn + ); for (let validatorIndex = 0; validatorIndex < phase0Result.length; validatorIndex++) { if (seenAttesters.includes(validatorIndex)) { expect(engine.seenAttesters.isKnown(0, validatorIndex)).toBe(true); diff --git a/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts b/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts index 769bd626bb94..c677bbed52d6 100644 --- a/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts @@ -33,7 +33,7 @@ describe("GossipMessageValidator", () => { opPool.hasSeenAttesterSlashing.mockReturnValue(true); await expectRejectedWithLodestarError( - validateGossipAttesterSlashing(chainStub.beaconEngine, attesterSlashing), + validateGossipAttesterSlashing.call(chainStub.beaconEngine, attesterSlashing), AttesterSlashingErrorCode.ALREADY_EXISTS ); }); @@ -42,7 +42,7 @@ describe("GossipMessageValidator", () => { const attesterSlashing = ssz.phase0.AttesterSlashing.defaultValue(); await expectRejectedWithLodestarError( - validateGossipAttesterSlashing(chainStub.beaconEngine, attesterSlashing), + validateGossipAttesterSlashing.call(chainStub.beaconEngine, attesterSlashing), AttesterSlashingErrorCode.INVALID ); }); @@ -62,7 +62,7 @@ describe("GossipMessageValidator", () => { }, }; - await validateGossipAttesterSlashing(chainStub.beaconEngine, attesterSlashing); + await validateGossipAttesterSlashing.call(chainStub.beaconEngine, attesterSlashing); }); }); }); 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 b5251217bab6..a54909fb1923 100644 --- a/packages/beacon-node/test/unit/chain/validation/block.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/block.test.ts @@ -73,7 +73,7 @@ describe("gossip block validation", () => { const signedBlock = {signature, message: {...block, slot: clockSlot + 1}}; await expectRejectedWithLodestarError( - validateGossipBlock(chain.beaconEngine, signedBlock, ForkName.phase0), + validateGossipBlock.call(chain.beaconEngine, signedBlock, ForkName.phase0), BlockErrorCode.FUTURE_SLOT ); }); @@ -87,7 +87,7 @@ describe("gossip block validation", () => { }); await expectRejectedWithLodestarError( - validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), + validateGossipBlock.call(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT ); }); @@ -97,7 +97,7 @@ describe("gossip block validation", () => { forkChoice.getBlockHexDefaultStatus.mockReturnValue({} as ProtoBlock); await expectRejectedWithLodestarError( - validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), + validateGossipBlock.call(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.ALREADY_KNOWN ); }); @@ -107,7 +107,7 @@ describe("gossip block validation", () => { chain.seenBlockProposers.add(job.message.slot, job.message.proposerIndex); await expectRejectedWithLodestarError( - validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), + validateGossipBlock.call(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.REPEAT_PROPOSAL ); }); @@ -119,7 +119,7 @@ describe("gossip block validation", () => { forkChoice.getBlockHexDefaultStatus.mockReturnValueOnce(null); await expectRejectedWithLodestarError( - validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), + validateGossipBlock.call(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.PARENT_UNKNOWN ); }); @@ -131,7 +131,7 @@ describe("gossip block validation", () => { forkChoice.getBlockHexDefaultStatus.mockReturnValueOnce({slot: clockSlot + 1} as ProtoBlock); await expectRejectedWithLodestarError( - validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), + validateGossipBlock.call(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.NOT_LATER_THAN_PARENT ); }); @@ -145,7 +145,7 @@ describe("gossip block validation", () => { regen.getPreState.mockRejectedValue(undefined); await expectRejectedWithLodestarError( - validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), + validateGossipBlock.call(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.PARENT_UNKNOWN ); }); @@ -161,7 +161,7 @@ describe("gossip block validation", () => { verifySignature.mockResolvedValue(false); await expectRejectedWithLodestarError( - validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), + validateGossipBlock.call(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.PROPOSAL_SIGNATURE_INVALID ); }); @@ -180,7 +180,7 @@ describe("gossip block validation", () => { vi.spyOn(state.cachedState.epochCtx, "getBeaconProposer").mockReturnValue(proposerIndex + 1); await expectRejectedWithLodestarError( - validateGossipBlock(chain.beaconEngine, job, ForkName.phase0), + validateGossipBlock.call(chain.beaconEngine, job, ForkName.phase0), BlockErrorCode.INCORRECT_PROPOSER ); }); @@ -198,7 +198,7 @@ describe("gossip block validation", () => { // Force proposer shuffling cache to return correct value vi.spyOn(state.cachedState.epochCtx, "getBeaconProposer").mockReturnValue(proposerIndex); - await validateGossipBlock(chain.beaconEngine, job, ForkName.phase0); + await validateGossipBlock.call(chain.beaconEngine, job, ForkName.phase0); }); it("deneb - TOO_MANY_KZG_COMMITMENTS", async () => { @@ -223,7 +223,7 @@ describe("gossip block validation", () => { (job as SignedBeaconBlock).message.body.blobKzgCommitments.push(new Uint8Array([0])); await expectRejectedWithLodestarError( - validateGossipBlock(chain.beaconEngine, job, ForkName.deneb), + validateGossipBlock.call(chain.beaconEngine, job, ForkName.deneb), BlockErrorCode.TOO_MANY_KZG_COMMITMENTS ); }); @@ -248,6 +248,6 @@ describe("gossip block validation", () => { vi.spyOn(state.cachedState.epochCtx, "getBeaconProposer").mockReturnValue(proposerIndex); // Keep number of kzg commitments as is so it stays within the limit - await validateGossipBlock(chain.beaconEngine, job, ForkName.deneb); + await validateGossipBlock.call(chain.beaconEngine, job, ForkName.deneb); }); }); diff --git a/packages/beacon-node/test/unit/chain/validation/blsToExecutionChange.test.ts b/packages/beacon-node/test/unit/chain/validation/blsToExecutionChange.test.ts index 34fb87d3f80d..c61f79d2ad49 100644 --- a/packages/beacon-node/test/unit/chain/validation/blsToExecutionChange.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/blsToExecutionChange.test.ts @@ -104,13 +104,13 @@ describe("validate bls to execution change", () => { opPool.hasSeenBlsToExecutionChange.mockReturnValue(true); await expectRejectedWithLodestarError( - validateGossipBlsToExecutionChange(chainStub.beaconEngine, signedBlsToExecChangeInvalid), + validateGossipBlsToExecutionChange.call(chainStub.beaconEngine, signedBlsToExecChangeInvalid), BlsToExecutionChangeErrorCode.ALREADY_EXISTS ); }); it("should return valid blsToExecutionChange ", async () => { - await validateGossipBlsToExecutionChange(chainStub.beaconEngine, signedBlsToExecChange); + await validateGossipBlsToExecutionChange.call(chainStub.beaconEngine, signedBlsToExecChange); }); it("should return invalid bls to execution Change - invalid validatorIndex", async () => { @@ -124,7 +124,7 @@ describe("validate bls to execution change", () => { }; await expectRejectedWithLodestarError( - validateGossipBlsToExecutionChange(chainStub.beaconEngine, signedBlsToExecChangeInvalid), + validateGossipBlsToExecutionChange.call(chainStub.beaconEngine, signedBlsToExecChangeInvalid), BlsToExecutionChangeErrorCode.INVALID ); }); @@ -139,7 +139,7 @@ describe("validate bls to execution change", () => { }; await expectRejectedWithLodestarError( - validateGossipBlsToExecutionChange(chainStub.beaconEngine, signedBlsToExecChangeInvalid), + validateGossipBlsToExecutionChange.call(chainStub.beaconEngine, signedBlsToExecChangeInvalid), BlsToExecutionChangeErrorCode.INVALID ); }); @@ -155,7 +155,7 @@ describe("validate bls to execution change", () => { }; await expectRejectedWithLodestarError( - validateGossipBlsToExecutionChange(chainStub.beaconEngine, signedBlsToExecChangeInvalid), + validateGossipBlsToExecutionChange.call(chainStub.beaconEngine, signedBlsToExecChangeInvalid), BlsToExecutionChangeErrorCode.INVALID ); }); @@ -171,7 +171,7 @@ describe("validate bls to execution change", () => { }; await expectRejectedWithLodestarError( - validateGossipBlsToExecutionChange(chainStub.beaconEngine, signedBlsToExecChangeInvalid), + validateGossipBlsToExecutionChange.call(chainStub.beaconEngine, signedBlsToExecChangeInvalid), BlsToExecutionChangeErrorCode.INVALID ); }); diff --git a/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts b/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts index ed048b7761ff..7fa3b1c99e6d 100644 --- a/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts @@ -32,7 +32,7 @@ describe("validate proposer slashing", () => { opPool.hasSeenProposerSlashing.mockReturnValue(true); await expectRejectedWithLodestarError( - validateGossipProposerSlashing(chainStub.beaconEngine, proposerSlashing), + validateGossipProposerSlashing.call(chainStub.beaconEngine, proposerSlashing), ProposerSlashingErrorCode.ALREADY_EXISTS ); }); @@ -44,7 +44,7 @@ describe("validate proposer slashing", () => { proposerSlashing.signedHeader2.message.slot = BigInt(0); await expectRejectedWithLodestarError( - validateGossipProposerSlashing(chainStub.beaconEngine, proposerSlashing), + validateGossipProposerSlashing.call(chainStub.beaconEngine, proposerSlashing), ProposerSlashingErrorCode.INVALID ); }); @@ -60,6 +60,6 @@ describe("validate proposer slashing", () => { signedHeader2: signedHeader2, }; - await validateGossipProposerSlashing(chainStub.beaconEngine, proposerSlashing); + await validateGossipProposerSlashing.call(chainStub.beaconEngine, proposerSlashing); }); }); diff --git a/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts b/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts index 4d7728f04a70..ab90bb46160b 100644 --- a/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/syncCommittee.test.ts @@ -56,7 +56,7 @@ describe("Sync Committee Signature validation", () => { const syncCommittee = getSyncCommitteeSignature(1, 0); await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, 0), + validateGossipSyncCommittee.call(chain.beaconEngine, syncCommittee, 0), SyncCommitteeErrorCode.NOT_CURRENT_SLOT ); }); @@ -67,7 +67,7 @@ describe("Sync Committee Signature validation", () => { chain.getHeadState.mockReturnValue(headState); chain.seenSyncCommitteeMessages.get = () => toHexString(syncCommittee.beaconBlockRoot); await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, 0), + validateGossipSyncCommittee.call(chain.beaconEngine, syncCommittee, 0), SyncCommitteeErrorCode.SYNC_COMMITTEE_MESSAGE_KNOWN ); }); @@ -80,7 +80,7 @@ describe("Sync Committee Signature validation", () => { chain.seenSyncCommitteeMessages.get = () => prevRoot; forkchoiceStub.getHeadRoot.mockReturnValue(prevRoot); await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, 0), + validateGossipSyncCommittee.call(chain.beaconEngine, syncCommittee, 0), SyncCommitteeErrorCode.SYNC_COMMITTEE_MESSAGE_KNOWN ); }); @@ -91,7 +91,7 @@ describe("Sync Committee Signature validation", () => { chain.getHeadState.mockReturnValue(headState); await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, 0), + validateGossipSyncCommittee.call(chain.beaconEngine, syncCommittee, 0), SyncCommitteeErrorCode.VALIDATOR_NOT_IN_SYNC_COMMITTEE ); }); @@ -105,7 +105,7 @@ describe("Sync Committee Signature validation", () => { const headState = new BeaconStateView(generateCachedAltairState({slot: currentSlot}, altairForkEpoch)); chain.getHeadState.mockReturnValue(headState); await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, 0), + validateGossipSyncCommittee.call(chain.beaconEngine, syncCommittee, 0), SyncCommitteeErrorCode.INVALID_SUBCOMMITTEE_INDEX ); }); @@ -117,7 +117,7 @@ describe("Sync Committee Signature validation", () => { chain.getHeadState.mockReturnValue(headState); chain.bls.verifySignatureSets.mockReturnValue(false); await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, 0), + validateGossipSyncCommittee.call(chain.beaconEngine, syncCommittee, 0), SyncCommitteeErrorCode.INVALID_SIGNATURE ); }); @@ -131,14 +131,14 @@ describe("Sync Committee Signature validation", () => { chain.getHeadState.mockReturnValue(headState); // "should be null" expect(chain.seenSyncCommitteeMessages.get(slot, subnet, validatorIndex)).toBeNull(); - await validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, subnet); + await validateGossipSyncCommittee.call(chain.beaconEngine, syncCommittee, subnet); expect(chain.seenSyncCommitteeMessages.get(slot, subnet, validatorIndex)).toBe( toHexString(syncCommittee.beaconBlockRoot) ); // receive same message again await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, subnet), + validateGossipSyncCommittee.call(chain.beaconEngine, syncCommittee, subnet), SyncCommitteeErrorCode.SYNC_COMMITTEE_MESSAGE_KNOWN ); }); @@ -156,7 +156,7 @@ describe("Sync Committee Signature validation", () => { expect(chain.seenSyncCommitteeMessages.get(slot, subnet, validatorIndex)).toBe(prevRoot); // but forkchoice head is message root forkchoiceStub.getHeadRoot.mockReturnValue(toHexString(syncCommittee.beaconBlockRoot)); - await validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, subnet); + await validateGossipSyncCommittee.call(chain.beaconEngine, syncCommittee, subnet); // should accept the message and overwrite prevRoot expect(chain.seenSyncCommitteeMessages.get(slot, subnet, validatorIndex)).toBe( toHexString(syncCommittee.beaconBlockRoot) @@ -164,7 +164,7 @@ describe("Sync Committee Signature validation", () => { // receive same message again await expectRejectedWithLodestarError( - validateGossipSyncCommittee(chain.beaconEngine, syncCommittee, subnet), + validateGossipSyncCommittee.call(chain.beaconEngine, syncCommittee, subnet), SyncCommitteeErrorCode.SYNC_COMMITTEE_MESSAGE_KNOWN ); }); diff --git a/packages/beacon-node/test/unit/chain/validation/voluntaryExit.test.ts b/packages/beacon-node/test/unit/chain/validation/voluntaryExit.test.ts index ede60d531f55..2503a8d28c23 100644 --- a/packages/beacon-node/test/unit/chain/validation/voluntaryExit.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/voluntaryExit.test.ts @@ -88,7 +88,7 @@ describe("validate voluntary exit", () => { opPool.hasSeenVoluntaryExit.mockReturnValue(true); await expectRejectedWithLodestarError( - validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExitInvalidSig), + validateGossipVoluntaryExit.call(chainStub.beaconEngine, signedVoluntaryExitInvalidSig), VoluntaryExitErrorCode.ALREADY_EXISTS ); }); @@ -104,7 +104,7 @@ describe("validate voluntary exit", () => { }; await expectRejectedWithLodestarError( - validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExitInvalid), + validateGossipVoluntaryExit.call(chainStub.beaconEngine, signedVoluntaryExitInvalid), VoluntaryExitErrorCode.EARLY_EPOCH ); }); @@ -120,7 +120,7 @@ describe("validate voluntary exit", () => { vi.spyOn(chainStub, "getHeadStateAtCurrentEpoch").mockResolvedValue(state); await expectRejectedWithLodestarError( - validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExit), + validateGossipVoluntaryExit.call(chainStub.beaconEngine, signedVoluntaryExit), VoluntaryExitErrorCode.INACTIVE ); }); @@ -138,7 +138,7 @@ describe("validate voluntary exit", () => { vi.spyOn(chainStub, "getHeadStateAtCurrentEpoch").mockResolvedValue(state); await expectRejectedWithLodestarError( - validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExit), + validateGossipVoluntaryExit.call(chainStub.beaconEngine, signedVoluntaryExit), VoluntaryExitErrorCode.ALREADY_EXITED ); }); @@ -154,7 +154,7 @@ describe("validate voluntary exit", () => { vi.spyOn(chainStub, "getHeadStateAtCurrentEpoch").mockResolvedValue(state); await expectRejectedWithLodestarError( - validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExit), + validateGossipVoluntaryExit.call(chainStub.beaconEngine, signedVoluntaryExit), VoluntaryExitErrorCode.SHORT_TIME_ACTIVE ); }); @@ -170,12 +170,12 @@ describe("validate voluntary exit", () => { chainStub.bls.verifySignatureSets.mockResolvedValue(false); await expectRejectedWithLodestarError( - validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExitInvalidSig), + validateGossipVoluntaryExit.call(chainStub.beaconEngine, signedVoluntaryExitInvalidSig), VoluntaryExitErrorCode.INVALID_SIGNATURE ); }); it("should return valid Voluntary Exit", async () => { - await validateGossipVoluntaryExit(chainStub.beaconEngine, signedVoluntaryExit); + await validateGossipVoluntaryExit.call(chainStub.beaconEngine, signedVoluntaryExit); }); }); diff --git a/packages/beacon-node/test/unit/util/kzg.test.ts b/packages/beacon-node/test/unit/util/kzg.test.ts index b8b7dfecafcf..40e98e572d63 100644 --- a/packages/beacon-node/test/unit/util/kzg.test.ts +++ b/packages/beacon-node/test/unit/util/kzg.test.ts @@ -66,7 +66,7 @@ describe("KZG", () => { for (const blobSidecar of blobSidecars) { try { - await validateGossipBlobSidecar(chain.beaconEngine, fork, blobSidecar, blobSidecar.index); + await validateGossipBlobSidecar.call(chain.beaconEngine, fork, blobSidecar, blobSidecar.index); } catch (_e) { // We expect some error from here // console.log(error); From fec127f8db906001fa9f89e23d46511a1f87c6eb Mon Sep 17 00:00:00 2001 From: twoeths Date: Wed, 8 Jul 2026 16:39:01 +0700 Subject: [PATCH 22/24] feat: move gossip handlers to BeaconEngine --- .../src/api/impl/beacon/blocks/index.ts | 7 - .../src/api/impl/beacon/pool/index.ts | 75 +---- .../src/api/impl/validator/index.ts | 23 +- .../src/chain/beaconEngine/beaconEngine.ts | 317 +++++++++++++----- .../src/chain/beaconEngine/interface.ts | 56 +--- .../src/chain/beaconEngine/options.ts | 2 + .../src/network/processor/gossipHandlers.ts | 133 +------- .../src/network/processor/index.ts | 8 +- .../test/mocks/mockedBeaconChain.ts | 7 - .../src/options/beaconNodeOptions/chain.ts | 9 + .../src/options/beaconNodeOptions/network.ts | 9 - .../unit/options/beaconNodeOptions.test.ts | 4 +- 12 files changed, 292 insertions(+), 358 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 9b7f2db50267..252d32fc17d2 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -867,13 +867,6 @@ export function getBeaconBlockApi({ throw bidValidation.error ?? new ApiError(400, `Invalid execution payload bid: ${bidValidation.code}`); } - try { - const insertOutcome = chain.beaconEngine.addExecutionPayloadBid(signedExecutionPayloadBid); - metrics?.opPool.executionPayloadBidPool.apiInsertOutcome.inc({insertOutcome}); - } catch (e) { - chain.logger.error("Error adding to executionPayloadBid pool", {}, e as Error); - } - const sentPeers = await network.publishSignedExecutionPayloadBid(signedExecutionPayloadBid); chain.emitter.emit(routes.events.EventType.executionPayloadBid, { diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index f07060bb1206..1ba3b9c7d0f0 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -1,15 +1,7 @@ import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; -import { - ForkName, - ForkPostElectra, - ForkPreElectra, - SYNC_COMMITTEE_SUBNET_SIZE, - isForkPostElectra, - isForkPostGloas, -} from "@lodestar/params"; -import {isStatePostAltair} from "@lodestar/state-transition"; -import {Epoch, SingleAttestation, isElectraAttestation, ssz, sszTypesFor} from "@lodestar/types"; +import {ForkName, ForkPostElectra, ForkPreElectra, isForkPostElectra, isForkPostGloas} from "@lodestar/params"; +import {SingleAttestation, isElectraAttestation, ssz, sszTypesFor} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; import {GossipValidationStatus} from "../../../../chain/beaconEngine/gossipValidationResult.js"; import { @@ -302,23 +294,6 @@ export function getBeaconPoolApi({ } return; } - const {attDataRootHex, validatorCommitteeIndices} = res.value; - - const insertOutcome = chain.beaconEngine.addPayloadAttestation( - payloadAttestationMessage, - attDataRootHex, - validatorCommitteeIndices - ); - metrics?.opPool.payloadAttestationPool.apiInsertOutcome.inc({insertOutcome}); - - // TODO - beacon engine: refactor to beaconEngine.notifyPtcMessages() - chain.beaconEngine.notifyPtcMessages( - toRootHex(payloadAttestationMessage.data.beaconBlockRoot), - payloadAttestationMessage.data.slot, - validatorCommitteeIndices, - payloadAttestationMessage.data.payloadPresent, - payloadAttestationMessage.data.blobDataAvailable - ); await network.publishPayloadAttestationMessage(payloadAttestationMessage); } catch (e) { @@ -344,18 +319,6 @@ export function getBeaconPoolApi({ * https://github.com/ethereum/beacon-APIs/pull/135 */ async submitPoolSyncCommitteeSignatures({signatures}, context) { - // Fetch states for all slots of the `signatures` - const slots = new Set(); - for (const signature of signatures) { - slots.add(signature.slot); - } - - // TODO: Fetch states at signature slots - const state = chain.getHeadState(); - if (!isStatePostAltair(state)) { - throw new ApiError(400, "Sync committee pool is not supported before Altair"); - } - // SSZ request: slice each message out of the (fixed-size element) list body. JSON request // (no sszBytes): serialize once. Never re-serialize on the SSZ path. const signatureBytes = context?.sszBytes @@ -367,14 +330,9 @@ export function getBeaconPoolApi({ await Promise.all( signatures.map(async (signature, i) => { try { - const synCommittee = state.getIndexedSyncCommittee(signature.slot); - const indexesInCommittee = synCommittee.validatorIndexMap.get(signature.validatorIndex); - if (indexesInCommittee === undefined || indexesInCommittee.length === 0) { - return; // Not a sync committee member - } - - // Verify signature only, all other data is very likely to be correct, since the `signature` object is created by this node. - // Worst case if `signature` is not valid, gossip peers will drop it and slightly downscore us. + // Engine verifies the signature (allowing late API messages), resolves committee membership, + // inserts into its (internal) pool, and returns the subnets to broadcast to (empty if this + // validator isn't in the sync committee). const res = await chain.beaconEngine.validateApiSyncCommittee(signatureBytes[i], signature); if (res.status !== GossipValidationStatus.Accept) { failures.push({index: i, message: res.error?.message ?? res.code}); @@ -389,27 +347,10 @@ export function getBeaconPoolApi({ return; } - // The same validator can appear multiple times in the sync committee. It can appear multiple times per - // subnet even. First compute on which subnet the signature must be broadcasted to. - const subnets: number[] = []; - // same to api attestation, we allow api SyncCommittee to be added to pool even when it's late - // see https://github.com/ChainSafe/lodestar/issues/7548 - const priority = true; - - for (const indexInCommittee of indexesInCommittee) { - // Sync committee subnet members are just sequential in the order they appear in SyncCommitteeIndexes array - const subnet = Math.floor(indexInCommittee / SYNC_COMMITTEE_SUBNET_SIZE); - const indexInSubcommittee = indexInCommittee % SYNC_COMMITTEE_SUBNET_SIZE; - chain.beaconEngine.addSyncCommitteeMessage(subnet, signature, indexInSubcommittee, priority); - - // Cheap de-duplication code to avoid using a Set. indexesInCommittee is always sorted - if (subnets.length === 0 || subnets.at(-1) !== subnet) { - subnets.push(subnet); - } - } - // TODO: Broadcast at once to all topics - await Promise.all(subnets.map(async (subnet) => network.publishSyncCommitteeSignature(signature, subnet))); + await Promise.all( + res.value.subnets.map(async (subnet) => network.publishSyncCommitteeSignature(signature, subnet)) + ); } catch (e) { // TODO: gossipsub should allow publishing same message to different topics // https://github.com/ChainSafe/js-libp2p-gossipsub/issues/272 diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 45f4e7a7d95d..80218d10b618 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1301,15 +1301,7 @@ export function getValidatorApi( } return; } - const {indexedAttestation, committeeValidatorIndices, attDataRootHex} = res.value; - - const insertOutcome = chain.beaconEngine.addAggregatedAttestation( - signedAggregateAndProof.message.aggregate, - attDataRootHex, - indexedAttestation.attestingIndices.length, - committeeValidatorIndices - ); - metrics?.opPool.aggregatedAttestationPool.apiInsertOutcome.inc({insertOutcome}); + const {indexedAttestation} = res.value; const sentPeers = await network.publishBeaconAggregateAndProof(signedAggregateAndProof); chain.validatorMonitor?.onPoolSubmitAggregatedAttestation(seenTimestampSec, indexedAttestation, sentPeers); @@ -1351,10 +1343,10 @@ export function getValidatorApi( }; try { // TODO: Validate in batch - const res = await chain.beaconEngine.validateSyncCommitteeGossipContributionAndProof( + // Engine validates (skipping the known-participants check) + inserts into its (internal) pool on Accept. + const res = await chain.beaconEngine.validateApiSyncCommitteeContributionAndProof( contributionBytes[i], - contributionAndProof, - true // skip known participants check + contributionAndProof ); if (res.status !== GossipValidationStatus.Accept) { if (res.code === SyncCommitteeErrorCode.SYNC_COMMITTEE_AGGREGATOR_ALREADY_KNOWN) { @@ -1368,13 +1360,6 @@ export function getValidatorApi( } return; } - const {syncCommitteeParticipantIndices} = res.value; - const insertOutcome = chain.beaconEngine.addSyncContributionAndProof( - contributionAndProof.message, - syncCommitteeParticipantIndices.length, - true - ); - metrics?.opPool.syncContributionAndProofPool.apiInsertOutcome.inc({insertOutcome}); await network.publishContributionAndProof(contributionAndProof); } catch (e) { failures.push({index: i, message: (e as Error).message}); diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 658e98c12b2a..341fa6b21379 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -29,6 +29,7 @@ import { MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT, + SYNC_COMMITTEE_SUBNET_SIZE, isForkPostBellatrix, isForkPostGloas, } from "@lodestar/params"; @@ -159,11 +160,7 @@ import {ShufflingCache} from "../shufflingCache.js"; import {FIFOBlockStateCache} from "../stateCache/fifoBlockStateCache.js"; import {PersistentCheckpointStateCache, toCheckpointHex} from "../stateCache/persistentCheckpointsCache.js"; import {BlockStateCache, CheckpointHex, CheckpointStateCache} from "../stateCache/types.js"; -import { - AggregateAndProofValidationResult, - validateApiAggregateAndProof, - validateGossipAggregateAndProof, -} from "../validation/aggregateAndProof.js"; +import {validateApiAggregateAndProof, validateGossipAggregateAndProof} from "../validation/aggregateAndProof.js"; import { ApiAttestation, AttestationValidationResult, @@ -1623,15 +1620,52 @@ export class BeaconEngine implements IBeaconEngine { _syncCommitteeBytes: Uint8Array, syncCommittee: altair.SyncCommitteeMessage, subnet: SubnetID - ): Promise> { - return runGossipValidation(() => validateGossipSyncCommittee.call(this, syncCommittee, subnet)); + ): Promise> { + return runGossipValidation(async () => { + const result = await validateGossipSyncCommittee.call(this, syncCommittee, subnet); + // Insert for ALL positions this validator holds in the subcommittee (moved from the gossip handler). + // The subcommittee indices are consumed here internally; the facade only needs the verdict. + try { + for (const indexInSubcommittee of result.indicesInSubcommittee) { + const insertOutcome = this.syncCommitteeMessagePool.add(subnet, syncCommittee, indexInSubcommittee); + this.metrics?.opPool.syncCommitteeMessagePoolInsertOutcome.inc({insertOutcome}); + } + } catch (e) { + this.logger.debug("Error adding to syncCommittee pool", {subnet}, e as Error); + } + }); } + // Returns the subnets to broadcast to (the facade publishes). Resolves committee membership + pool insert + // internally so the facade never touches state or the pool for API sync-committee submission. validateApiSyncCommittee( _syncCommitteeBytes: Uint8Array, syncCommittee: altair.SyncCommitteeMessage - ): Promise> { - return runGossipValidation(() => validateApiSyncCommittee.call(this, syncCommittee)); + ): Promise> { + return runGossipValidation(async () => { + const state = this.getHeadState(); + // The node's own validators — skip silently if this validator isn't in the sync committee. + const indexesInCommittee = isStatePostAltair(state) + ? state.getIndexedSyncCommittee(syncCommittee.slot).validatorIndexMap.get(syncCommittee.validatorIndex) + : undefined; + if (indexesInCommittee === undefined || indexesInCommittee.length === 0) { + return {subnets: []}; + } + // Verify signature only, all other data is very likely correct since this node produced the signature. + await validateApiSyncCommittee.call(this, syncCommittee); + // The same validator can appear multiple times in the committee (and per subnet). Insert each position + // (priority: allow late API messages into the pool) and collect the subnets for the facade to publish. + const subnets: number[] = []; + for (const indexInCommittee of indexesInCommittee) { + const subnet = Math.floor(indexInCommittee / SYNC_COMMITTEE_SUBNET_SIZE); + const indexInSubcommittee = indexInCommittee % SYNC_COMMITTEE_SUBNET_SIZE; + this.syncCommitteeMessagePool.add(subnet, syncCommittee, indexInSubcommittee, true); + if (subnets.length === 0 || subnets.at(-1) !== subnet) { + subnets.push(subnet); + } + } + return {subnets}; + }); } validateSyncCommitteeGossipContributionAndProof( @@ -1639,13 +1673,57 @@ export class BeaconEngine implements IBeaconEngine { signedContributionAndProof: altair.SignedContributionAndProof, skipValidationKnownParticipants = false ): Promise> { - return runGossipValidation(() => - validateSyncCommitteeGossipContributionAndProof.call( + return runGossipValidation(async () => { + const result = await validateSyncCommitteeGossipContributionAndProof.call( this, signedContributionAndProof, skipValidationKnownParticipants - ) - ); + ); + // Insert into the pool on Accept (moved from the gossip handler). + this.insertSyncContributionOnAccept( + signedContributionAndProof, + result.syncCommitteeParticipantIndices.length, + "gossip" + ); + return result; + }); + } + + validateApiSyncCommitteeContributionAndProof( + _contributionBytes: Uint8Array, + signedContributionAndProof: altair.SignedContributionAndProof + ): Promise> { + return runGossipValidation(async () => { + // API path skips the known-participants check (the signature object is produced by this node). + const result = await validateSyncCommitteeGossipContributionAndProof.call(this, signedContributionAndProof, true); + // Insert into the pool on Accept (moved from the API handler); the facade only needs the verdict. + this.insertSyncContributionOnAccept( + signedContributionAndProof, + result.syncCommitteeParticipantIndices.length, + "api" + ); + }); + } + + /** Shared post-validation pool insert for a valid sync contribution (gossip swallows errors; API does not). */ + private insertSyncContributionOnAccept( + signed: altair.SignedContributionAndProof, + syncCommitteeParticipants: number, + source: "gossip" | "api" + ): void { + const metric = this.metrics?.opPool.syncContributionAndProofPool; + if (source === "gossip") { + try { + const insertOutcome = this.syncContributionAndProofPool.add(signed.message, syncCommitteeParticipants); + metric?.gossipInsertOutcome.inc({insertOutcome}); + } catch (e) { + this.logger.error("Error adding to contributionAndProof pool", {}, e as Error); + } + } else { + // API allows late messages into the pool (priority) and surfaces a pool error as a submit failure. + const insertOutcome = this.syncContributionAndProofPool.add(signed.message, syncCommitteeParticipants, true); + metric?.apiInsertOutcome.inc({insertOutcome}); + } } validateGossipBlobSidecar( @@ -1681,15 +1759,53 @@ export class BeaconEngine implements IBeaconEngine { validateGossipPayloadAttestationMessage( _payloadAttestationBytes: Uint8Array, payloadAttestationMessage: gloas.PayloadAttestationMessage - ): Promise> { - return runGossipValidation(() => validateGossipPayloadAttestationMessage.call(this, payloadAttestationMessage)); + ): Promise> { + return runGossipValidation(async () => { + const result = await validateGossipPayloadAttestationMessage.call(this, payloadAttestationMessage); + // Insert into the pool + notify fork choice on Accept (moved from the gossip handler). The rich result + // is consumed here internally; the facade only needs the Accept/Reject verdict, so nothing is returned. + this.insertPayloadAttestationOnAccept(payloadAttestationMessage, result, "gossip"); + }); } validateApiPayloadAttestationMessage( _payloadAttestationBytes: Uint8Array, payloadAttestationMessage: gloas.PayloadAttestationMessage - ): Promise> { - return runGossipValidation(() => validateApiPayloadAttestationMessage.call(this, payloadAttestationMessage)); + ): Promise> { + return runGossipValidation(async () => { + const result = await validateApiPayloadAttestationMessage.call(this, payloadAttestationMessage); + // Insert into the pool + notify fork choice on Accept (moved from the API handler); nothing returned. + this.insertPayloadAttestationOnAccept(payloadAttestationMessage, result, "api"); + }); + } + + /** Shared post-validation side-effects for a valid payload attestation (pool insert + PTC fork-choice notify). */ + private insertPayloadAttestationOnAccept( + message: gloas.PayloadAttestationMessage, + result: PayloadAttestationValidationResult, + source: "gossip" | "api" + ): void { + const {attDataRootHex, validatorCommitteeIndices} = result; + if (source === "gossip") { + // Gossip swallows pool-insert errors so a pool failure can't flip the verdict (matches prior handler). + try { + const insertOutcome = this.payloadAttestationPool.add(message, attDataRootHex, validatorCommitteeIndices); + this.metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({insertOutcome}); + } catch (e) { + this.logger.error("Error adding to payloadAttestation pool", {}, e as Error); + } + } else { + // API path does not swallow (matches prior handler — a pool error surfaces as a submit failure). + const insertOutcome = this.payloadAttestationPool.add(message, attDataRootHex, validatorCommitteeIndices); + this.metrics?.opPool.payloadAttestationPool.apiInsertOutcome.inc({insertOutcome}); + } + this.forkChoice.notifyPtcMessages( + toRootHex(message.data.beaconBlockRoot), + message.data.slot, + validatorCommitteeIndices, + message.data.payloadPresent, + message.data.blobDataAvailable + ); } // The batch attestation validator already returns `Result[]` (caught internally, never throws out); @@ -1697,10 +1813,48 @@ export class BeaconEngine implements IBeaconEngine { // `GossipAttestation.serializedData`, so there is no separate leading bytes parameter here. async validateGossipAttestationsSameAttData( fork: ForkName, - attestations: GossipAttestation[] + attestations: GossipAttestation[], + // Per-attestation `aggregatorTracker.shouldAggregate` decision (facade-computed, plain data): insert into + // the pool only for subnets this node aggregates for. Aligned with `attestations`. + shouldAddToPool: boolean[] ): Promise<{results: GossipValidationResult[]; batchableBls: boolean}> { const {results, batchableBls} = await validateGossipAttestationsSameAttData.call(this, fork, attestations); - return {results: results.map(fromResult), batchableBls}; + const mapped = results.map(fromResult); + // Post-validation side-effects on Accept (moved from the gossip handler): pool insert (gated by the + // facade's aggregatorTracker decision), then the fork-choice write. Contain errors — a pool/fork-choice + // error must not flip the gossip verdict. + for (const [i, res] of mapped.entries()) { + if (res.status !== GossipValidationStatus.Accept) { + continue; + } + const value = res.value; + if (shouldAddToPool[i]) { + try { + const insertOutcome = this.attestationPool.add( + value.committeeIndex, + value.attestation, + value.attDataRootHex, + value.validatorCommitteeIndex, + value.committeeSize + ); + this.metrics?.opPool.attestationPool.gossipInsertOutcome.inc({insertOutcome}); + } catch (e) { + this.logger.error("Error adding unaggregated attestation to pool", {subnet: value.subnet}, e as Error); + } + } + if (!this.opts.dontSendGossipAttestationsToForkchoice) { + try { + this.forkChoice.onAttestation(value.indexedAttestation, value.attDataRootHex); + } catch (e) { + this.logger.debug( + "Error adding gossip unaggregated attestation to forkchoice", + {subnet: value.subnet}, + e as Error + ); + } + } + } + return {results: mapped, batchableBls}; } validateApiAttestation( @@ -1714,18 +1868,52 @@ export class BeaconEngine implements IBeaconEngine { aggregateBytes: Uint8Array, fork: ForkName, signedAggregateAndProof: SignedAggregateAndProof - ): Promise> { - return runGossipValidation(() => - validateGossipAggregateAndProof.call(this, fork, signedAggregateAndProof, aggregateBytes) - ); + ): Promise> { + return runGossipValidation(async () => { + const result = await validateGossipAggregateAndProof.call(this, fork, signedAggregateAndProof, aggregateBytes); + // Insert into the aggregated-attestation pool on Accept (moved from the gossip handler). + const insertOutcome = this.aggregatedAttestationPool.add( + signedAggregateAndProof.message.aggregate, + result.attDataRootHex, + result.indexedAttestation.attestingIndices.length, + result.committeeValidatorIndices + ); + this.metrics?.opPool.aggregatedAttestationPool.gossipInsertOutcome.inc({insertOutcome}); + // Fork-choice write on Accept (moved from the gossip handler). Contain errors — a fork-choice error + // must not flip the gossip verdict. + if (!this.opts.dontSendGossipAttestationsToForkchoice) { + try { + this.forkChoice.onAttestation(result.indexedAttestation, result.attDataRootHex); + } catch (e) { + this.logger.debug( + "Error adding gossip aggregated attestation to forkchoice", + {slot: result.indexedAttestation.data.slot}, + e as Error + ); + } + } + // The facade only needs `indexedAttestation` (validator monitor); pool/fork-choice fields consumed above. + return {indexedAttestation: result.indexedAttestation}; + }); } validateApiAggregateAndProof( _aggregateBytes: Uint8Array, fork: ForkName, signedAggregateAndProof: SignedAggregateAndProof - ): Promise> { - return runGossipValidation(() => validateApiAggregateAndProof.call(this, fork, signedAggregateAndProof)); + ): Promise> { + return runGossipValidation(async () => { + const result = await validateApiAggregateAndProof.call(this, fork, signedAggregateAndProof); + // Insert into the aggregated-attestation pool on Accept (moved from the API handler). + const insertOutcome = this.aggregatedAttestationPool.add( + signedAggregateAndProof.message.aggregate, + result.attDataRootHex, + result.indexedAttestation.attestingIndices.length, + result.committeeValidatorIndices + ); + this.metrics?.opPool.aggregatedAttestationPool.apiInsertOutcome.inc({insertOutcome}); + return {indexedAttestation: result.indexedAttestation}; + }); } validateGossipExecutionPayloadEnvelope( @@ -1772,14 +1960,33 @@ export class BeaconEngine implements IBeaconEngine { _bidBytes: Uint8Array, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid ): Promise> { - return runGossipValidation(() => validateGossipExecutionPayloadBid.call(this, signedExecutionPayloadBid)); + return runGossipValidation(async () => { + const result = await validateGossipExecutionPayloadBid.call(this, signedExecutionPayloadBid); + // Insert into the bid pool on Accept (moved from the gossip handler). + try { + const insertOutcome = this.executionPayloadBidPool.add(signedExecutionPayloadBid); + this.metrics?.opPool.executionPayloadBidPool.gossipInsertOutcome.inc({insertOutcome}); + } catch (e) { + this.logger.error("Error adding to executionPayloadBid pool", {}, e as Error); + } + return result; + }); } validateApiExecutionPayloadBid( _bidBytes: Uint8Array, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid - ): Promise> { - return runGossipValidation(() => validateApiExecutionPayloadBid.call(this, signedExecutionPayloadBid)); + ): Promise> { + return runGossipValidation(async () => { + await validateApiExecutionPayloadBid.call(this, signedExecutionPayloadBid); + // Insert into the bid pool on Accept (moved from the API handler); the facade only needs the verdict. + try { + const insertOutcome = this.executionPayloadBidPool.add(signedExecutionPayloadBid); + this.metrics?.opPool.executionPayloadBidPool.apiInsertOutcome.inc({insertOutcome}); + } catch (e) { + this.logger.error("Error adding to executionPayloadBid pool", {}, e as Error); + } + }); } async validateGossipProposerPreferences( @@ -1925,28 +2132,10 @@ export class BeaconEngine implements IBeaconEngine { return this.attestationPool.getAggregate(slot, dataRootHex, committeeIndex); } - addAggregatedAttestation( - attestation: Attestation, - dataRootHex: RootHex, - attestingIndicesCount: number, - committee: Uint32Array - ): InsertOutcome { - return this.aggregatedAttestationPool.add(attestation, dataRootHex, attestingIndicesCount, committee); - } - getPoolAggregatedAttestations(bySlot?: Slot): Attestation[] { return this.aggregatedAttestationPool.getAll(bySlot); } - addSyncCommitteeMessage( - subnet: SubnetID, - signature: altair.SyncCommitteeMessage, - indexInSubcommittee: number, - priority?: boolean - ): InsertOutcome { - return this.syncCommitteeMessagePool.add(subnet, signature, indexInSubcommittee, priority); - } - getSyncCommitteeContribution( subnet: SubcommitteeIndex, slot: Slot, @@ -1955,30 +2144,10 @@ export class BeaconEngine implements IBeaconEngine { return this.syncCommitteeMessagePool.getContribution(subnet, slot, prevBlockRoot); } - addSyncContributionAndProof( - contributionAndProof: altair.ContributionAndProof, - syncCommitteeParticipants: number, - priority?: boolean - ): InsertOutcome { - return this.syncContributionAndProofPool.add(contributionAndProof, syncCommitteeParticipants, priority); - } - - addPayloadAttestation( - message: gloas.PayloadAttestationMessage, - payloadAttDataRootHex: RootHex, - validatorCommitteeIndices: number[] - ): InsertOutcome { - return this.payloadAttestationPool.add(message, payloadAttDataRootHex, validatorCommitteeIndices); - } - getPoolPayloadAttestations(slot?: Slot): gloas.PayloadAttestation[] { return this.payloadAttestationPool.getAll(slot); } - addExecutionPayloadBid(bid: gloas.SignedExecutionPayloadBid): InsertOutcome { - return this.executionPayloadBidPool.add(bid); - } - // --- Proposer cache + finalized balances (engine-internal) --- /** Fee recipient registered for a proposer, or undefined if none (block-production default is @@ -2813,22 +2982,6 @@ export class BeaconEngine implements IBeaconEngine { this.forkChoice.validateLatestHash(execResponse); } - // TODO - beacon-engine: transitional wrappers — their gossip/api PTC + attestation consumers move into - // the engine (BLK-2), at which point these fork-choice writes become engine-internal and these go away. - notifyPtcMessages( - blockRoot: RootHex, - slot: Slot, - ptcIndices: number[], - payloadPresent: boolean, - blobDataAvailable: boolean - ): void { - this.forkChoice.notifyPtcMessages(blockRoot, slot, ptcIndices, payloadPresent, blobDataAvailable); - } - - onAttestation(attestation: IndexedAttestation, attDataRoot: string, forceImport?: boolean): void { - this.forkChoice.onAttestation(attestation, attDataRoot, forceImport); - } - // --- DB ownership (blocks + states) --- async migrateFinalized(finalized: CheckpointWithHex): Promise { diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 1b212524ddeb..490a5c76d279 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -62,10 +62,8 @@ import { import {SeenBlockInput} from "../seenCache/seenGossipBlockInput.js"; import {ShufflingCache} from "../shufflingCache.js"; import {CPStateDatastore} from "../stateCache/datastore/types.js"; -import {AggregateAndProofValidationResult} from "../validation/aggregateAndProof.js"; import {ApiAttestation, AttestationValidationResult, GossipAttestation} from "../validation/attestation.js"; import {GossipBlockValidationResult} from "../validation/block.js"; -import {PayloadAttestationValidationResult} from "../validation/payloadAttestationMessage.js"; import {ValidatorMonitor} from "../validatorMonitor.js"; import {GossipValidationResult} from "./gossipValidationResult.js"; import {IBeaconEngineOptions} from "./options.js"; @@ -342,16 +340,20 @@ export interface IBeaconEngine { syncCommitteeBytes: Uint8Array, syncCommittee: altair.SyncCommitteeMessage, subnet: SubnetID - ): Promise>; + ): Promise>; validateApiSyncCommittee( syncCommitteeBytes: Uint8Array, syncCommittee: altair.SyncCommitteeMessage - ): Promise>; + ): Promise>; validateSyncCommitteeGossipContributionAndProof( contributionBytes: Uint8Array, signedContributionAndProof: altair.SignedContributionAndProof, skipValidationKnownParticipants?: boolean ): Promise>; + validateApiSyncCommitteeContributionAndProof( + contributionBytes: Uint8Array, + signedContributionAndProof: altair.SignedContributionAndProof + ): Promise>; validateGossipBlobSidecar( blobBytes: Uint8Array, fork: ForkName, @@ -372,14 +374,15 @@ export interface IBeaconEngine { validateGossipPayloadAttestationMessage( payloadAttestationBytes: Uint8Array, payloadAttestationMessage: gloas.PayloadAttestationMessage - ): Promise>; + ): Promise>; validateApiPayloadAttestationMessage( payloadAttestationBytes: Uint8Array, payloadAttestationMessage: gloas.PayloadAttestationMessage - ): Promise>; + ): Promise>; validateGossipAttestationsSameAttData( fork: ForkName, - attestations: GossipAttestation[] + attestations: GossipAttestation[], + shouldAddToPool: boolean[] ): Promise<{results: GossipValidationResult[]; batchableBls: boolean}>; validateApiAttestation( fork: ForkName, @@ -389,12 +392,12 @@ export interface IBeaconEngine { aggregateBytes: Uint8Array, fork: ForkName, signedAggregateAndProof: SignedAggregateAndProof - ): Promise>; + ): Promise>; validateApiAggregateAndProof( aggregateBytes: Uint8Array, fork: ForkName, signedAggregateAndProof: SignedAggregateAndProof - ): Promise>; + ): Promise>; // The bid scalars + proposerIndex are looked up facade-side from the `PayloadEnvelopeInput` (the engine // no longer touches the DA seen cache) and passed in. validateGossipExecutionPayloadEnvelope( @@ -420,7 +423,7 @@ export interface IBeaconEngine { validateApiExecutionPayloadBid( bidBytes: Uint8Array, signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid - ): Promise>; + ): Promise>; validateGossipProposerPreferences( preferencesBytes: Uint8Array, signedProposerPreferences: gloas.SignedProposerPreferences @@ -460,36 +463,13 @@ export interface IBeaconEngine { priority?: boolean ): InsertOutcome; getAttestationAggregate(slot: Slot, dataRootHex: RootHex, committeeIndex: CommitteeIndex): Attestation | null; - addAggregatedAttestation( - attestation: Attestation, - dataRootHex: RootHex, - attestingIndicesCount: number, - committee: Uint32Array - ): InsertOutcome; getPoolAggregatedAttestations(bySlot?: Slot): Attestation[]; - addSyncCommitteeMessage( - subnet: SubnetID, - signature: altair.SyncCommitteeMessage, - indexInSubcommittee: number, - priority?: boolean - ): InsertOutcome; getSyncCommitteeContribution( subnet: SubcommitteeIndex, slot: Slot, prevBlockRoot: Root ): altair.SyncCommitteeContribution | null; - addSyncContributionAndProof( - contributionAndProof: altair.ContributionAndProof, - syncCommitteeParticipants: number, - priority?: boolean - ): InsertOutcome; - addPayloadAttestation( - message: gloas.PayloadAttestationMessage, - payloadAttDataRootHex: RootHex, - validatorCommitteeIndices: number[] - ): InsertOutcome; getPoolPayloadAttestations(slot?: Slot): gloas.PayloadAttestation[]; - addExecutionPayloadBid(bid: gloas.SignedExecutionPayloadBid): InsertOutcome; // Proposer cache + finalized balances (engine-internal). getProposerFeeRecipient(proposerIndex: ValidatorIndex): string | undefined; @@ -525,16 +505,6 @@ export interface IBeaconEngine { updateTime(currentSlot: Slot): void; getIrrecoverableError(): Error | undefined; validateLatestHash(execResponse: LVHExecResponse): void; - // TODO - beacon-engine: transitional — the gossip/api PTC + attestation consumers move into the engine - // (BLK-2), at which point these fork-choice writes become engine-internal and these wrappers go away. - notifyPtcMessages( - blockRoot: RootHex, - slot: Slot, - ptcIndices: number[], - payloadPresent: boolean, - blobDataAvailable: boolean - ): void; - onAttestation(attestation: IndexedAttestation, attDataRoot: string, forceImport?: boolean): void; // Execution payload envelope (gloas) import — consensus body of the facade `importExecutionPayload`. // `verifyExecutionPayloadEnvelope` (regen state + fields + BLS sig, throws `PayloadError`) runs diff --git a/packages/beacon-node/src/chain/beaconEngine/options.ts b/packages/beacon-node/src/chain/beaconEngine/options.ts index df8147fabad3..264bb8981831 100644 --- a/packages/beacon-node/src/chain/beaconEngine/options.ts +++ b/packages/beacon-node/src/chain/beaconEngine/options.ts @@ -36,6 +36,8 @@ export type IBeaconEngineOptions = ShufflingCacheOpts & maxSkipSlots?: number; /** Min number of same-message signature sets to batch in gossip attestation validation */ minSameMessageSignatureSetsToBatch: number; + /** By default the engine passes gossip-validated attestations to fork choice; set to skip that write */ + dontSendGossipAttestationsToForkchoice?: boolean; /** Default fee recipient used by the engine-owned beaconProposerCache */ suggestedFeeRecipient: string; /** Emit an SSE payloadAttributes event every slot (not only when proposing) */ diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index d5bc14e4609d..10f091c9a5f6 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -79,14 +79,6 @@ import {INetwork} from "../interface.js"; import {PeerAction} from "../peers/index.js"; import {AggregatorTracker} from "./aggregatorTracker.js"; -/** - * Gossip handler options as part of network options - */ -export type GossipHandlerOpts = { - /** By default pass gossip attestations to forkchoice */ - dontSendGossipAttestationsToForkchoice?: boolean; -}; - export type ValidatorFnsModules = { chain: IBeaconChain; config: BeaconConfig; @@ -114,15 +106,15 @@ const BLOCK_AVAILABILITY_CUTOFF_MS = 3_000; * the handler function scope is hard to achieve without very hacky strategies * - Ethereum Consensus gossipsub protocol strictly defined a single topic for message */ -export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipHandlerOpts): GossipHandlers { - return {...getSequentialHandlers(modules, options), ...getBatchHandlers(modules, options)}; +export function getGossipHandlers(modules: ValidatorFnsModules): GossipHandlers { + return {...getSequentialHandlers(modules), ...getBatchHandlers(modules)}; } /** * Default handlers validate gossip messages one by one. * We only have a choice to do batch validation for beacon_attestation topic. */ -function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHandlerOpts): SequentialGossipHandlers { +function getSequentialHandlers(modules: ValidatorFnsModules): SequentialGossipHandlers { const {chain, config, metrics, logger, core} = modules; async function validateBeaconBlock( @@ -870,34 +862,13 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand } // Handler - const {indexedAttestation, committeeValidatorIndices, attDataRootHex} = res.value; + // TODO beacon-engine: make the result type thinner + const {indexedAttestation} = res.value; chain.validatorMonitor?.registerGossipAggregatedAttestation( seenTimestampSec, signedAggregateAndProof, indexedAttestation ); - const aggregatedAttestation = signedAggregateAndProof.message.aggregate; - - const insertOutcome = chain.beaconEngine.addAggregatedAttestation( - aggregatedAttestation, - attDataRootHex, - indexedAttestation.attestingIndices.length, - committeeValidatorIndices - ); - metrics?.opPool.aggregatedAttestationPool.gossipInsertOutcome.inc({insertOutcome}); - - if (!options.dontSendGossipAttestationsToForkchoice) { - try { - // TODO beacon-engine: move to validateGossipAggregateAndProof - chain.beaconEngine.onAttestation(indexedAttestation, attDataRootHex); - } catch (e) { - logger.debug( - "Error adding gossip aggregated attestation to forkchoice", - {slot: aggregatedAttestation.data.slot}, - e as Error - ); - } - } chain.emitter.emit(routes.events.EventType.attestation, signedAggregateAndProof.message.aggregate); return accept(undefined); @@ -964,15 +935,6 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand contributionAndProof.message, syncCommitteeParticipantIndices ); - try { - const insertOutcome = chain.beaconEngine.addSyncContributionAndProof( - contributionAndProof.message, - syncCommitteeParticipantIndices.length - ); - metrics?.opPool.syncContributionAndProofPool.gossipInsertOutcome.inc({insertOutcome}); - } catch (e) { - logger.error("Error adding to contributionAndProof pool", {}, e as Error); - } chain.emitter.emit(routes.events.EventType.contributionAndProof, contributionAndProof); return accept(undefined); @@ -989,17 +951,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand } return res; } - const {indicesInSubcommittee} = res.value; - - // Handler — add for ALL positions this validator holds in the subcommittee - try { - for (const indexInSubcommittee of indicesInSubcommittee) { - const insertOutcome = chain.beaconEngine.addSyncCommitteeMessage(subnet, syncCommittee, indexInSubcommittee); - metrics?.opPool.syncCommitteeMessagePoolInsertOutcome.inc({insertOutcome}); - } - } catch (e) { - logger.debug("Error adding to syncCommittee pool", {subnet}, e as Error); - } + // Engine validated + inserted the message into its (internal) pool for every subcommittee position. return accept(undefined); }, @@ -1163,26 +1115,6 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand payloadAttestationMessage ); if (res.status !== GossipValidationStatus.Accept) return res; - const {attDataRootHex, validatorCommitteeIndices} = res.value; - - // TODO beacon engine: move to BeaconEngine, check other gossip handlers too - try { - const insertOutcome = chain.beaconEngine.addPayloadAttestation( - payloadAttestationMessage, - attDataRootHex, - validatorCommitteeIndices - ); - metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({insertOutcome}); - } catch (e) { - logger.error("Error adding to payloadAttestation pool", {}, e as Error); - } - chain.beaconEngine.notifyPtcMessages( - toRootHex(payloadAttestationMessage.data.beaconBlockRoot), - payloadAttestationMessage.data.slot, - validatorCommitteeIndices, - payloadAttestationMessage.data.payloadPresent, - payloadAttestationMessage.data.blobDataAvailable - ); return accept(undefined); }, [GossipType.execution_payload_bid]: async ({ @@ -1195,14 +1127,6 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand if (res.status !== GossipValidationStatus.Accept) return res; const {proposerIndex} = res.value; - // Handle valid payload bid by storing in a bid pool - try { - const insertOutcome = chain.beaconEngine.addExecutionPayloadBid(executionPayloadBid); - metrics?.opPool.executionPayloadBidPool.gossipInsertOutcome.inc({insertOutcome}); - } catch (e) { - logger.error("Error adding to executionPayloadBid pool", {}, e as Error); - } - chain.validatorMonitor?.registerExecutionPayloadBid(OpSource.gossip, proposerIndex, executionPayloadBid.message); chain.emitter.emit(routes.events.EventType.executionPayloadBid, { @@ -1233,8 +1157,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand /** * For now, only beacon_attestation topic is batched. */ -function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOpts): BatchGossipHandlers { - const {chain, metrics, logger, aggregatorTracker} = modules; +function getBatchHandlers(modules: ValidatorFnsModules): BatchGossipHandlers { + const {chain, metrics, aggregatorTracker} = modules; return { [GossipType.beacon_attestation]: async ( gossipHandlerParams: GossipHandlerParamGeneric[] @@ -1253,9 +1177,14 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp attDataBase64: param.gossipData.indexed, subnet: param.topic.subnet, })) as GossipAttestation[]; + // Node may be subscribed to extra subnets (long-lived random subnets). For those, validate the + // messages but don't add to the attestation pool, to save CPU and RAM. This aggregatorTracker + // decision is facade/network state — computed here and passed to the engine (which inserts on Accept). + const shouldAddToPool = validationParams.map((p) => aggregatorTracker.shouldAggregate(p.subnet, p.attSlot)); const {results: validationResults, batchableBls} = await chain.beaconEngine.validateGossipAttestationsSameAttData( fork, - validationParams + validationParams, + shouldAddToPool ); for (const [i, validationResult] of validationResults.entries()) { if (validationResult.status !== GossipValidationStatus.Accept) { @@ -1264,45 +1193,13 @@ function getBatchHandlers(modules: ValidatorFnsModules, options: GossipHandlerOp } // Handler - const { - indexedAttestation, - attDataRootHex, - attestation, - committeeIndex, - validatorCommitteeIndex, - committeeSize, - } = validationResult.value; + const {indexedAttestation, attestation} = validationResult.value; chain.validatorMonitor?.registerGossipUnaggregatedAttestation( gossipHandlerParams[i].seenTimestampSec, indexedAttestation ); - const {subnet} = validationResult.value; results.push(accept(undefined)); - try { - // Node may be subscribe to extra subnets (long-lived random subnets). For those, validate the messages - // but don't add to attestation pool, to save CPU and RAM - if (aggregatorTracker.shouldAggregate(subnet, indexedAttestation.data.slot)) { - const insertOutcome = chain.beaconEngine.addAttestationToPool( - committeeIndex, - attestation, - attDataRootHex, - validatorCommitteeIndex, - committeeSize - ); - metrics?.opPool.attestationPool.gossipInsertOutcome.inc({insertOutcome}); - } - } catch (e) { - logger.error("Error adding unaggregated attestation to pool", {subnet}, e as Error); - } - - if (!options.dontSendGossipAttestationsToForkchoice) { - try { - chain.beaconEngine.onAttestation(indexedAttestation, attDataRootHex); - } catch (e) { - logger.debug("Error adding gossip unaggregated attestation to forkchoice", {subnet}, e as Error); - } - } if (isForkPostElectra(fork)) { chain.emitter.emit( diff --git a/packages/beacon-node/src/network/processor/index.ts b/packages/beacon-node/src/network/processor/index.ts index 81940b07d5f1..f04cc51434f5 100644 --- a/packages/beacon-node/src/network/processor/index.ts +++ b/packages/beacon-node/src/network/processor/index.ts @@ -31,7 +31,7 @@ import { GossipValidatorFn, } from "../gossip/interface.js"; import {createExtractBlockSlotRootFns} from "./extractSlotRootFns.js"; -import {GossipHandlerOpts, ValidatorFnsModules, getGossipHandlers} from "./gossipHandlers.js"; +import {ValidatorFnsModules, getGossipHandlers} from "./gossipHandlers.js"; import {createGossipQueues} from "./gossipQueues/index.js"; import {ValidatorFnModules, getGossipValidatorBatchFn, getGossipValidatorFn} from "./gossipValidatorFn.js"; import {PendingGossipsubMessage} from "./types.js"; @@ -49,7 +49,7 @@ export type NetworkProcessorModules = ValidatorFnsModules & gossipHandlers?: GossipHandlers; }; -export type NetworkProcessorOpts = GossipHandlerOpts & { +export type NetworkProcessorOpts = { maxGossipTopicConcurrency?: number; }; @@ -206,9 +206,9 @@ export class NetworkProcessor { this.events = events; this.gossipQueues = createGossipQueues(); this.gossipTopicConcurrency = mapValues(this.gossipQueues, () => 0); - this.gossipValidatorFn = getGossipValidatorFn(modules.gossipHandlers ?? getGossipHandlers(modules, opts), modules); + this.gossipValidatorFn = getGossipValidatorFn(modules.gossipHandlers ?? getGossipHandlers(modules), modules); this.gossipValidatorBatchFn = getGossipValidatorBatchFn( - modules.gossipHandlers ?? getGossipHandlers(modules, opts), + modules.gossipHandlers ?? getGossipHandlers(modules), modules ); diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index e8fc60acf689..83f10d7010aa 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -282,20 +282,13 @@ export function getMockedBeaconChain(opts?: Partial): // Op-pool add/read bridges (forward to the mock's pools). "addAttestationToPool", "getAttestationAggregate", - "addAggregatedAttestation", "getPoolAggregatedAttestations", - "addSyncCommitteeMessage", "getSyncCommitteeContribution", - "addSyncContributionAndProof", - "addPayloadAttestation", "getPoolPayloadAttestations", - "addExecutionPayloadBid", // Fork-choice write wrappers (forward to the mock's forkChoice). "updateTime", "getIrrecoverableError", "validateLatestHash", - "notifyPtcMessages", - "onAttestation", ] as const) { (chain as unknown as Record)[name] = ( BeaconEngine.prototype as unknown as Record diff --git a/packages/cli/src/options/beaconNodeOptions/chain.ts b/packages/cli/src/options/beaconNodeOptions/chain.ts index a1e65467351b..5f7afee60cd7 100644 --- a/packages/cli/src/options/beaconNodeOptions/chain.ts +++ b/packages/cli/src/options/beaconNodeOptions/chain.ts @@ -27,6 +27,7 @@ export type ChainArgs = { emitPayloadAttributes?: boolean; broadcastValidationStrictness?: string; "chain.minSameMessageSignatureSetsToBatch"?: number; + "chain.dontSendGossipAttestationsToForkchoice"?: boolean; "chain.maxShufflingCacheEpochs"?: number; "chain.archiveStateEpochFrequency": number; "chain.archiveDataEpochs"?: number; @@ -68,6 +69,7 @@ export function parseArgs(args: ChainArgs): IBeaconNodeOptions["chain"] { broadcastValidationStrictness: args.broadcastValidationStrictness, minSameMessageSignatureSetsToBatch: args["chain.minSameMessageSignatureSetsToBatch"] ?? defaultOptions.chain.minSameMessageSignatureSetsToBatch, + dontSendGossipAttestationsToForkchoice: args["chain.dontSendGossipAttestationsToForkchoice"], maxShufflingCacheEpochs: args["chain.maxShufflingCacheEpochs"] ?? defaultOptions.chain.maxShufflingCacheEpochs, archiveStateEpochFrequency: args["chain.archiveStateEpochFrequency"], archiveDataEpochs: args["chain.archiveDataEpochs"], @@ -229,6 +231,13 @@ Will double processing times. Use only for debugging purposes.", group: "chain", }, + "chain.dontSendGossipAttestationsToForkchoice": { + hidden: true, + type: "boolean", + description: "Pass gossip attestations to forkchoice or not", + group: "chain", + }, + "chain.assertCorrectProgressiveBalances": { hidden: true, description: "Enable asserting the progressive balances", diff --git a/packages/cli/src/options/beaconNodeOptions/network.ts b/packages/cli/src/options/beaconNodeOptions/network.ts index f211f2d752f3..d4e7048dd5d6 100644 --- a/packages/cli/src/options/beaconNodeOptions/network.ts +++ b/packages/cli/src/options/beaconNodeOptions/network.ts @@ -31,7 +31,6 @@ export type NetworkArgs = { "network.maxPeers"?: number; "network.connectToDiscv5Bootnodes"?: boolean; "network.discv5FirstQueryDelayMs"?: number; - "network.dontSendGossipAttestationsToForkchoice"?: boolean; "network.allowPublishToZeroPeers"?: boolean; "network.gossipsubD"?: number; "network.gossipsubDLow"?: number; @@ -187,7 +186,6 @@ export function parseArgs(args: NetworkArgs): IBeaconNodeOptions["network"] { tcp, connectToDiscv5Bootnodes: args["network.connectToDiscv5Bootnodes"], discv5FirstQueryDelayMs: args["network.discv5FirstQueryDelayMs"], - dontSendGossipAttestationsToForkchoice: args["network.dontSendGossipAttestationsToForkchoice"], allowPublishToZeroPeers: args["network.allowPublishToZeroPeers"], gossipsubD: args["network.gossipsubD"], gossipsubDLow: args["network.gossipsubDLow"], @@ -405,13 +403,6 @@ export const options: CliCommandOptions = { deprecated: true, }, - "network.dontSendGossipAttestationsToForkchoice": { - hidden: true, - type: "boolean", - description: "Pass gossip attestations to forkchoice or not", - group: "network", - }, - "network.allowPublishToZeroPeers": { hidden: true, type: "boolean", diff --git a/packages/cli/test/unit/options/beaconNodeOptions.test.ts b/packages/cli/test/unit/options/beaconNodeOptions.test.ts index 02e596a876d3..52f806ca65fc 100644 --- a/packages/cli/test/unit/options/beaconNodeOptions.test.ts +++ b/packages/cli/test/unit/options/beaconNodeOptions.test.ts @@ -34,6 +34,7 @@ describe("options / beaconNodeOptions", () => { "chain.maxSkipSlots": 100, "chain.archiveStateEpochFrequency": 1024, "chain.minSameMessageSignatureSetsToBatch": 32, + "chain.dontSendGossipAttestationsToForkchoice": true, "chain.maxShufflingCacheEpochs": 100, "chain.archiveDataEpochs": 10000, "chain.nHistoricalStatesFileDataStore": true, @@ -84,7 +85,6 @@ describe("options / beaconNodeOptions", () => { "network.blockCountTotalLimit": 1000, "network.blockCountPeerLimit": 500, "network.rateTrackerTimeoutMs": 60000, - "network.dontSendGossipAttestationsToForkchoice": true, "network.allowPublishToZeroPeers": true, "network.gossipsubD": 4, "network.gossipsubDLow": 2, @@ -135,6 +135,7 @@ describe("options / beaconNodeOptions", () => { archiveStateEpochFrequency: 1024, emitPayloadAttributes: false, minSameMessageSignatureSetsToBatch: 32, + dontSendGossipAttestationsToForkchoice: true, maxShufflingCacheEpochs: 100, archiveDataEpochs: 10000, archiveMode: ArchiveMode.Frequency, @@ -187,7 +188,6 @@ describe("options / beaconNodeOptions", () => { disablePeerScoring: true, connectToDiscv5Bootnodes: true, discv5FirstQueryDelayMs: 1000, - dontSendGossipAttestationsToForkchoice: true, allowPublishToZeroPeers: true, gossipsubD: 4, gossipsubDLow: 2, From e1af4244a43a7bf2c321c81ac545390a0f537940 Mon Sep 17 00:00:00 2001 From: twoeths Date: Thu, 9 Jul 2026 17:27:22 +0700 Subject: [PATCH 23/24] fix: move forkchoice to BeaconEngine --- .../src/api/impl/beacon/blocks/index.ts | 20 ++-- .../src/api/impl/beacon/blocks/utils.ts | 12 +- .../src/api/impl/beacon/state/utils.ts | 15 +-- .../beacon-node/src/api/impl/debug/index.ts | 31 +++--- .../src/api/impl/lodestar/index.ts | 12 +- .../src/api/impl/validator/index.ts | 14 +-- .../src/chain/archiveStore/archiveStore.ts | 2 +- .../archiveStore/utils/updateBackfillRange.ts | 2 +- .../src/chain/beaconEngine/beaconEngine.ts | 105 ++++++++++++++++++ .../src/chain/beaconEngine/interface.ts | 44 ++++++++ .../blocks/verifyBlocksExecutionPayloads.ts | 2 - .../chain/blocks/verifyBlocksSanityChecks.ts | 13 ++- packages/beacon-node/src/chain/chain.ts | 56 +++++----- packages/beacon-node/src/chain/interface.ts | 3 +- .../seenCache/seenPayloadEnvelopeInput.ts | 23 ++-- .../src/network/processor/gossipHandlers.ts | 6 +- .../src/network/processor/index.ts | 23 ++-- .../reqresp/handlers/blobSidecarsByRange.ts | 6 +- .../reqresp/handlers/blobSidecarsByRoot.ts | 4 +- .../handlers/dataColumnSidecarsByRange.ts | 6 +- .../handlers/dataColumnSidecarsByRoot.ts | 2 +- .../executionPayloadEnvelopesByRoot.ts | 2 +- packages/beacon-node/src/node/notifier.ts | 2 +- packages/beacon-node/src/sync/range/range.ts | 4 +- packages/beacon-node/src/sync/sync.ts | 8 +- packages/beacon-node/src/sync/unknownBlock.ts | 26 ++--- .../src/sync/utils/remoteSyncType.ts | 18 +-- .../test/e2e/chain/proposerBoostReorg.test.ts | 2 +- .../stateCache/nHistoricalStates.test.ts | 6 +- .../test/e2e/sync/checkpointSync.test.ts | 27 +++-- .../test/e2e/sync/finalizedSync.test.ts | 13 ++- .../test/mocks/mockedBeaconChain.ts | 32 ++++++ .../produceBlock/produceBlockBody.test.ts | 2 +- .../spec/presets/fast_confirmation.test.ts | 16 +-- .../test/spec/presets/fork_choice.test.ts | 26 +++-- .../test/spec/utils/gossipValidation.ts | 8 +- .../blocks/verifyBlocksSanityChecks.test.ts | 21 ++-- .../seenPayloadEnvelopeInput.test.ts | 21 ++-- .../test/unit/sync/unknownBlock.test.ts | 72 ++++++------ .../unit/sync/utils/remoteSyncType.test.ts | 14 +-- 40 files changed, 463 insertions(+), 258 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 252d32fc17d2..ca0b461ae869 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -241,7 +241,7 @@ export function getBeaconBlockApi({ case routes.beacon.BroadcastValidation.consensus: { // check if this beacon node produced the block else run validations if (!blockLocallyProduced) { - const parentBlock = chain.forkChoice.getBlockDefaultStatus(signedBlock.message.parentRoot); + const parentBlock = chain.beaconEngine.getBlockDefaultStatus(signedBlock.message.parentRoot); if (parentBlock === null) { chain.emitter.emit(ChainEvent.blockUnknownParent, { blockInput: blockForImport, @@ -496,12 +496,12 @@ export function getBeaconBlockApi({ const finalizedBlock = config.getForkTypes(blockSlot).SignedBeaconBlock.deserialize(finalizedBlockBytes); result.push(toBeaconHeaderResponse(config, finalizedBlock, true)); } - const nonFinalizedBlocks = chain.forkChoice.getBlockSummariesByParentRoot(parentRoot); + const nonFinalizedBlocks = chain.beaconEngine.getBlockSummariesByParentRoot(parentRoot); await Promise.all( nonFinalizedBlocks.map(async (summary) => { const blockResult = await chain.getBlockByRoot(summary.blockRoot); if (blockResult) { - const canonical = chain.forkChoice.getCanonicalBlockAtSlot(blockResult.block.message.slot); + const canonical = chain.beaconEngine.getCanonicalProtoBlockAtSlot(blockResult.block.message.slot); if (canonical) { result.push( toBeaconHeaderResponse(config, blockResult.block, canonical.blockRoot === summary.blockRoot) @@ -525,7 +525,7 @@ export function getBeaconBlockApi({ }; } - const headSlot = chain.forkChoice.getHead().slot; + const headSlot = chain.beaconEngine.getHead().slot; if (!parentRoot && slot === undefined) { slot = headSlot; } @@ -552,7 +552,7 @@ export function getBeaconBlockApi({ // fork blocks // TODO: What is this logic? await Promise.all( - chain.forkChoice.getBlockSummariesAtSlot(slot).map(async (summary) => { + chain.beaconEngine.getBlockSummariesAtSlot(slot).map(async (summary) => { if (isOptimisticBlock(summary)) { executionOptimistic = true; } @@ -624,7 +624,7 @@ export function getBeaconBlockApi({ // Fast path: From head state already available in memory get historical blockRoot const slot = typeof blockId === "string" ? parseInt(blockId) : blockId; if (!Number.isNaN(slot)) { - const head = chain.forkChoice.getHead(); + const head = chain.beaconEngine.getHead(); if (slot === head.slot) { return { @@ -639,12 +639,12 @@ export function getBeaconBlockApi({ data: {root: state.getBlockRootAtSlot(slot)}, meta: { executionOptimistic: isOptimisticBlock(head), - finalized: computeEpochAtSlot(slot) <= chain.forkChoice.getFinalizedCheckpoint().epoch, + finalized: computeEpochAtSlot(slot) <= chain.beaconEngine.getFinalizedCheckpoint().epoch, }, }; } } else if (blockId === "head") { - const head = chain.forkChoice.getHead(); + const head = chain.beaconEngine.getHead(); return { data: {root: fromHex(head.blockRoot)}, meta: {executionOptimistic: isOptimisticBlock(head), finalized: false}, @@ -675,7 +675,7 @@ export function getBeaconBlockApi({ } // TODO GLOAS: review checks, do we want to implement `broadcast_validation`? - let block = chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.EMPTY); + let block = chain.beaconEngine.getBlockHex(blockRootHex, PayloadStatus.EMPTY); if (block === null) { // Only wait if the envelope is for the current slot if (chain.clock.isCurrentSlotGivenGossipDisparity(slot)) { @@ -684,7 +684,7 @@ export function getBeaconBlockApi({ slot, }); await chain.waitForBlock(slot, blockRootHex); - block = chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.EMPTY); + block = chain.beaconEngine.getBlockHex(blockRootHex, PayloadStatus.EMPTY); } if (block === null) { throw new ApiError(404, `Block not found for beacon block root ${blockRootHex}`); diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts b/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts index 31525230e57a..185058d6b1e3 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/utils.ts @@ -1,8 +1,8 @@ import {routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; -import {IForkChoiceRead} from "@lodestar/fork-choice"; import {blockToHeader} from "@lodestar/state-transition"; import {RootHex, SignedBeaconBlock, Slot} from "@lodestar/types"; +import {IBeaconEngine} from "../../../../chain/beaconEngine/index.js"; import {IBeaconChain} from "../../../../chain/interface.js"; import {GENESIS_SLOT} from "../../../../constants/index.js"; import {rootHexRegex} from "../../../../execution/engine/utils.js"; @@ -23,10 +23,10 @@ export function toBeaconHeaderResponse( }; } -export function resolveBlockId(forkChoice: IForkChoiceRead, blockId: routes.beacon.BlockId): RootHex | Slot { +export function resolveBlockId(beaconEngine: IBeaconEngine, blockId: routes.beacon.BlockId): RootHex | Slot { blockId = String(blockId).toLowerCase(); if (blockId === "head") { - return forkChoice.getHead().blockRoot; + return beaconEngine.getHead().blockRoot; } if (blockId === "genesis") { @@ -34,11 +34,11 @@ export function resolveBlockId(forkChoice: IForkChoiceRead, blockId: routes.beac } if (blockId === "finalized") { - return forkChoice.getFinalizedBlock().blockRoot; + return beaconEngine.getFinalizedBlock().blockRoot; } if (blockId === "justified") { - return forkChoice.getJustifiedBlock().blockRoot; + return beaconEngine.getJustifiedBlock().blockRoot; } if (blockId.startsWith("0x")) { @@ -60,7 +60,7 @@ export async function getBlockResponse( chain: IBeaconChain, blockId: routes.beacon.BlockId ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean}> { - const rootOrSlot = resolveBlockId(chain.forkChoice, blockId); + const rootOrSlot = resolveBlockId(chain.beaconEngine, blockId); const res = typeof rootOrSlot === "string" diff --git a/packages/beacon-node/src/api/impl/beacon/state/utils.ts b/packages/beacon-node/src/api/impl/beacon/state/utils.ts index 5e65aabfd2a0..0d0dfe68cba9 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/utils.ts @@ -1,18 +1,19 @@ import {routes} from "@lodestar/api"; -import {CheckpointWithHex, IForkChoiceRead} from "@lodestar/fork-choice"; +import {CheckpointWithHex} from "@lodestar/fork-choice"; import {GENESIS_SLOT} from "@lodestar/params"; import {IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; import {BLSPubkey, Epoch, RootHex, Slot, ValidatorIndex, getValidatorStatus, phase0} from "@lodestar/types"; import {fromHex} from "@lodestar/utils"; +import {IBeaconEngine} from "../../../../chain/beaconEngine/index.js"; import {IBeaconChain} from "../../../../chain/index.js"; import {ApiError, ValidationError} from "../../errors.js"; export function resolveStateId( - forkChoice: IForkChoiceRead, + beaconEngine: IBeaconEngine, stateId: routes.beacon.StateId ): RootHex | Slot | CheckpointWithHex { if (stateId === "head") { - return forkChoice.getHead().stateRoot; + return beaconEngine.getHead().stateRoot; } if (stateId === "genesis") { @@ -20,11 +21,11 @@ export function resolveStateId( } if (stateId === "finalized") { - return forkChoice.getFinalizedCheckpoint(); + return beaconEngine.getFinalizedCheckpoint(); } if (stateId === "justified") { - return forkChoice.getJustifiedCheckpoint(); + return beaconEngine.getJustifiedCheckpoint(); } if (typeof stateId === "string" && stateId.startsWith("0x")) { @@ -45,7 +46,7 @@ export async function getStateResponseWithRegen( chain: IBeaconChain, inStateId: routes.beacon.StateId ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean}> { - const stateId = resolveStateId(chain.forkChoice, inStateId); + const stateId = resolveStateId(chain.beaconEngine, inStateId); const res = typeof stateId === "string" @@ -53,7 +54,7 @@ export async function getStateResponseWithRegen( : typeof stateId === "number" ? stateId > chain.clock.currentSlot ? null // Don't try to serve future slots - : stateId >= chain.forkChoice.getFinalizedBlock().slot + : stateId >= chain.beaconEngine.getFinalizedBlock().slot ? await chain.getStateBySlot(stateId, {allowRegen: true}) : await chain.getHistoricalStateBySlot(stateId) : await chain.getStateOrBytesByCheckpoint(stateId); diff --git a/packages/beacon-node/src/api/impl/debug/index.ts b/packages/beacon-node/src/api/impl/debug/index.ts index fb30bb41d270..1dcfe6997749 100644 --- a/packages/beacon-node/src/api/impl/debug/index.ts +++ b/packages/beacon-node/src/api/impl/debug/index.ts @@ -42,7 +42,7 @@ export function getDebugApi({ }: Pick): ApplicationMethods { return { async getDebugChainHeadsV2() { - const heads = chain.forkChoice.getHeads(); + const heads = chain.beaconEngine.getHeads(); return { data: heads.map((block) => ({ slot: block.slot, @@ -55,9 +55,9 @@ export function getDebugApi({ async getDebugForkChoice() { return { data: { - justifiedCheckpoint: chain.forkChoice.getJustifiedCheckpoint(), - finalizedCheckpoint: chain.forkChoice.getFinalizedCheckpoint(), - forkChoiceNodes: chain.forkChoice.getAllNodes().map((node) => ({ + justifiedCheckpoint: chain.beaconEngine.getJustifiedCheckpoint(), + finalizedCheckpoint: chain.beaconEngine.getFinalizedCheckpoint(), + forkChoiceNodes: chain.beaconEngine.getAllNodes().map((node) => ({ slot: node.slot, blockRoot: node.blockRoot, parentRoot: node.parentRoot, @@ -72,15 +72,16 @@ export function getDebugApi({ }, async getDebugForkChoiceV2() { - const {forkChoice} = chain; + const {beaconEngine} = chain; return { data: { - justifiedCheckpoint: forkChoice.getJustifiedCheckpoint(), - finalizedCheckpoint: forkChoice.getFinalizedCheckpoint(), - forkChoiceNodes: forkChoice.getAllNodes().map((node) => { + justifiedCheckpoint: beaconEngine.getJustifiedCheckpoint(), + finalizedCheckpoint: beaconEngine.getFinalizedCheckpoint(), + forkChoiceNodes: beaconEngine.getAllNodes().map((node) => { // Payload-specific fields apply only to a revealed Gloas payload = the FULL variant of a // Gloas block - const ptc = node.payloadStatus === PayloadStatus.FULL ? forkChoice.getPTCVoteCounts(node.blockRoot) : null; + const ptc = + node.payloadStatus === PayloadStatus.FULL ? beaconEngine.getPTCVoteCounts(node.blockRoot) : null; return { payloadStatus: toPayloadStatusName(node.payloadStatus), slot: node.slot, @@ -108,18 +109,18 @@ export function getDebugApi({ }; }), extraData: { - unrealizedJustifiedCheckpoint: forkChoice.getUnrealizedJustifiedCheckpoint(), - unrealizedFinalizedCheckpoint: forkChoice.getUnrealizedFinalizedCheckpoint(), - proposerBoostRoot: forkChoice.getProposerBoostRoot(), - previousProposerBoostRoot: forkChoice.getPreviousProposerBoostRoot(), - headRoot: forkChoice.getHeadRoot(), + unrealizedJustifiedCheckpoint: beaconEngine.getUnrealizedJustifiedCheckpoint(), + unrealizedFinalizedCheckpoint: beaconEngine.getUnrealizedFinalizedCheckpoint(), + proposerBoostRoot: beaconEngine.getProposerBoostRoot(), + previousProposerBoostRoot: beaconEngine.getPreviousProposerBoostRoot(), + headRoot: beaconEngine.getHeadRoot(), }, }, }; }, async getProtoArrayNodes() { - const nodes = chain.forkChoice.getAllNodes().map((node) => ({ + const nodes = chain.beaconEngine.getAllNodes().map((node) => ({ // if node has executionPayloadNumber, it will overwrite the below default executionPayloadNumber: 0, ...node, diff --git a/packages/beacon-node/src/api/impl/lodestar/index.ts b/packages/beacon-node/src/api/impl/lodestar/index.ts index a283c0ca5ab5..7596d077c379 100644 --- a/packages/beacon-node/src/api/impl/lodestar/index.ts +++ b/packages/beacon-node/src/api/impl/lodestar/index.ts @@ -281,12 +281,12 @@ export function getLodestarApi({ }, async getFastConfirmationInfo() { - const confirmedRoot = chain.forkChoice.getConfirmedRoot(); - const confirmedBlock = chain.forkChoice.getConfirmedBlock(); - const justifiedCheckpoint = chain.forkChoice.getJustifiedCheckpoint(); - const finalizedCheckpoint = chain.forkChoice.getFinalizedCheckpoint(); - const headRoot = chain.forkChoice.getHeadRoot(); - const head = chain.forkChoice.getHead(); + const confirmedRoot = chain.beaconEngine.getConfirmedRoot(); + const confirmedBlock = chain.beaconEngine.getConfirmedBlock(); + const justifiedCheckpoint = chain.beaconEngine.getJustifiedCheckpoint(); + const finalizedCheckpoint = chain.beaconEngine.getFinalizedCheckpoint(); + const headRoot = chain.beaconEngine.getHeadRoot(); + const head = chain.beaconEngine.getHead(); return { data: { diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 80218d10b618..a74fdd930234 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -266,7 +266,7 @@ export function getValidatorApi( case SyncState.SyncingFinalized: case SyncState.SyncingHead: { const currentSlot = chain.clock.currentSlot; - const headSlot = chain.forkChoice.getHead().slot; + const headSlot = chain.beaconEngine.getHead().slot; if (currentSlot - headSlot > SYNC_TOLERANCE_EPOCHS * SLOTS_PER_EPOCH) { throw new NodeIsSyncing(`headSlot ${headSlot} currentSlot ${currentSlot}`); } @@ -303,7 +303,7 @@ export function getValidatorApi( */ function notOnOptimisticBlockRoot(beaconBlockRoot: Root): void { - const protoBeaconBlock = chain.forkChoice.getBlockDefaultStatus(beaconBlockRoot); + const protoBeaconBlock = chain.beaconEngine.getBlockDefaultStatus(beaconBlockRoot); if (!protoBeaconBlock) { throw new ApiError(404, `Block not in forkChoice, beaconBlockRoot=${toRootHex(beaconBlockRoot)}`); } @@ -315,7 +315,7 @@ export function getValidatorApi( } function notOnOutOfRangeData(beaconBlockRoot: Root): void { - const protoBeaconBlock = chain.forkChoice.getBlockDefaultStatus(beaconBlockRoot); + const protoBeaconBlock = chain.beaconEngine.getBlockDefaultStatus(beaconBlockRoot); if (!protoBeaconBlock) { throw new ApiError(404, `Block not in forkChoice, beaconBlockRoot=${toRootHex(beaconBlockRoot)}`); } @@ -931,7 +931,7 @@ export function getValidatorApi( const headState = chain.getHeadState(); const headSlot = headState.slot; const attEpoch = computeEpochAtSlot(slot); - const headBlockRootHex = chain.forkChoice.getHead().blockRoot; + const headBlockRootHex = chain.beaconEngine.getHead().blockRoot; const headBlockRoot = fromHex(headBlockRootHex); const fork = config.getForkName(slot); @@ -945,7 +945,7 @@ export function getValidatorApi( let index: CommitteeIndex; if (isForkPostGloas(fork)) { - const canonicalBlock = chain.forkChoice.getCanonicalBlockByRoot(beaconBlockRoot); + const canonicalBlock = chain.beaconEngine.getCanonicalBlockByRoot(beaconBlockRoot); if (!canonicalBlock) { // This should never happen throw Error(`Block not found in fork choice for slot=${slot}, root=${toRootHex(beaconBlockRoot)}`); @@ -1006,7 +1006,7 @@ export function getValidatorApi( notWhileSyncing(); await waitForSlot(slot); - const block = chain.forkChoice.getCanonicalBlockAtSlot(slot); + const block = chain.beaconEngine.getCanonicalProtoBlockAtSlot(slot); if (!block) { // No block is seen at slot. Return 404 so vc can skip casting payload attestation. throw new ApiError(404, `No canonical block found at slot=${slot}`); @@ -1061,7 +1061,7 @@ export function getValidatorApi( // when a validator is configured with multiple beacon node urls, this beaconBlockRoot may come from another beacon node // and it hasn't been in our forkchoice since we haven't seen / processing that block // see https://github.com/ChainSafe/lodestar/issues/5063 - if (!chain.forkChoice.hasBlock(beaconBlockRoot)) { + if (!chain.beaconEngine.hasBlock(beaconBlockRoot)) { const rootHex = toRootHex(beaconBlockRoot); network.searchUnknownBlock({slot, root: rootHex}, BlockInputSource.api); // if result of this call is false, i.e. block hasn't seen after 1 slot then the below notOnOptimisticBlockRoot call will throw error diff --git a/packages/beacon-node/src/chain/archiveStore/archiveStore.ts b/packages/beacon-node/src/chain/archiveStore/archiveStore.ts index 0c97832ec80c..7dc5829d2ef6 100644 --- a/packages/beacon-node/src/chain/archiveStore/archiveStore.ts +++ b/packages/beacon-node/src/chain/archiveStore/archiveStore.ts @@ -114,7 +114,7 @@ export class ArchiveStore { async getHistoricalStateBySlot( slot: number ): Promise<{state: Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { - const finalizedBlock = this.chain.forkChoice.getFinalizedBlock(); + const finalizedBlock = this.chain.beaconEngine.getFinalizedBlock(); if (slot >= finalizedBlock.slot) { return null; diff --git a/packages/beacon-node/src/chain/archiveStore/utils/updateBackfillRange.ts b/packages/beacon-node/src/chain/archiveStore/utils/updateBackfillRange.ts index c17fd87941f4..80dd69022a4d 100644 --- a/packages/beacon-node/src/chain/archiveStore/utils/updateBackfillRange.ts +++ b/packages/beacon-node/src/chain/archiveStore/utils/updateBackfillRange.ts @@ -20,7 +20,7 @@ export async function updateBackfillRange( try { // Mark the sequence in backfill db from finalized block's slot till anchor slot as // filled. - const finalizedBlockFC = chain.forkChoice.getBlockHexDefaultStatus(finalized.rootHex); + const finalizedBlockFC = chain.beaconEngine.getBlockHexDefaultStatus(finalized.rootHex); if (finalizedBlockFC && finalizedBlockFC.slot > chain.anchorStateLatestBlockSlot) { await db.backfilledRanges.put(finalizedBlockFC.slot, chain.anchorStateLatestBlockSlot); diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 341fa6b21379..7f689f8a8d50 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -16,6 +16,7 @@ import { PayloadExecutionStatus, PayloadStatus, ProtoBlock, + ProtoNode, UpdateHeadOpt, getSafeExecutionBlockHash, } from "@lodestar/fork-choice"; @@ -532,6 +533,110 @@ export class BeaconEngine implements IBeaconEngine { } } + // --- TODO - beacon engine: temp forkChoice read pass-throughs (curate when regen removed — BLK-3). --- + getHead(): ProtoBlock { + return this.forkChoice.getHead(); + } + getHeadRoot(): RootHex { + return this.forkChoice.getHeadRoot(); + } + getHeads(): ProtoBlock[] { + return this.forkChoice.getHeads(); + } + getFinalizedCheckpoint(): CheckpointWithHex { + return this.forkChoice.getFinalizedCheckpoint(); + } + getJustifiedCheckpoint(): CheckpointWithHex { + return this.forkChoice.getJustifiedCheckpoint(); + } + getUnrealizedJustifiedCheckpoint(): CheckpointWithHex { + return this.forkChoice.getUnrealizedJustifiedCheckpoint(); + } + getUnrealizedFinalizedCheckpoint(): CheckpointWithHex { + return this.forkChoice.getUnrealizedFinalizedCheckpoint(); + } + getProposerBoostRoot(): RootHex { + return this.forkChoice.getProposerBoostRoot(); + } + getPreviousProposerBoostRoot(): RootHex { + return this.forkChoice.getPreviousProposerBoostRoot(); + } + getPTCVoteCounts( + blockRootHex: RootHex + ): {attesterCount: number; payloadPresentCount: number; dataAvailableCount: number} | null { + return this.forkChoice.getPTCVoteCounts(blockRootHex); + } + getFinalizedBlock(): ProtoBlock { + return this.forkChoice.getFinalizedBlock(); + } + getJustifiedBlock(): ProtoBlock { + return this.forkChoice.getJustifiedBlock(); + } + getConfirmedRoot(): RootHex { + return this.forkChoice.getConfirmedRoot(); + } + getConfirmedBlock(): ProtoBlock | null { + return this.forkChoice.getConfirmedBlock(); + } + getBlockDefaultStatus(blockRoot: Root): ProtoBlock | null { + return this.forkChoice.getBlockDefaultStatus(blockRoot); + } + getBlockHexDefaultStatus(blockRoot: RootHex): ProtoBlock | null { + return this.forkChoice.getBlockHexDefaultStatus(blockRoot); + } + getBlockHex(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoBlock | null { + return this.forkChoice.getBlockHex(blockRoot, payloadStatus); + } + getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null { + return this.forkChoice.getBlockHexAndBlockHash(blockRoot, blockHash); + } + getCanonicalProtoBlockAtSlot(slot: Slot): ProtoBlock | null { + return this.forkChoice.getCanonicalBlockAtSlot(slot); + } + getCanonicalBlockByRoot(blockRoot: Root): ProtoBlock | null { + return this.forkChoice.getCanonicalBlockByRoot(blockRoot); + } + getCanonicalBlockClosestLteSlot(slot: Slot): ProtoBlock | null { + return this.forkChoice.getCanonicalBlockClosestLteSlot(slot); + } + getBlockSummariesAtSlot(slot: Slot): ProtoBlock[] { + return this.forkChoice.getBlockSummariesAtSlot(slot); + } + getBlockSummariesByParentRoot(parentRoot: RootHex): ProtoBlock[] { + return this.forkChoice.getBlockSummariesByParentRoot(parentRoot); + } + getAllAncestorBlocks(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoBlock[] { + return this.forkChoice.getAllAncestorBlocks(blockRoot, payloadStatus); + } + getAllNodes(): ProtoNode[] { + return this.forkChoice.getAllNodes(); + } + getSlotsPresent(windowStart: number): number { + return this.forkChoice.getSlotsPresent(windowStart); + } + // TODO - beacon engine: hot path — cached in NativeBeaconEngine. + hasBlock(blockRoot: Root): boolean { + return this.forkChoice.hasBlock(blockRoot); + } + // TODO - beacon engine: hot path — cached in NativeBeaconEngine. + hasBlockHex(blockRoot: RootHex): boolean { + return this.forkChoice.hasBlockHex(blockRoot); + } + // TODO - beacon engine: hot path — cached in NativeBeaconEngine. + hasBlockHexUnsafe(blockRoot: RootHex): boolean { + return this.forkChoice.hasBlockHexUnsafe(blockRoot); + } + // TODO - beacon engine: hot path — cached in NativeBeaconEngine. + hasPayloadHexUnsafe(blockRoot: RootHex): boolean { + return this.forkChoice.hasPayloadHexUnsafe(blockRoot); + } + /** Projected thin ref (no ProtoBlock crosses) for DA-cache prune, anchored at (blockRoot, payloadStatus). */ + getAllAncestorBlockRootSlots(blockRoot: RootHex, payloadStatus: PayloadStatus): BlockRootSlot[] { + return this.forkChoice + .getAllAncestorBlocks(blockRoot, payloadStatus) + .map((block) => ({slot: block.slot, root: fromHex(block.blockRoot)})); + } + /** DB read of an execution payload envelope. Callers that hold the DA cache check it first. */ async getExecutionPayloadEnvelope( blockSlot: Slot, diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index 490a5c76d279..ec4a238f5ffe 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -8,6 +8,7 @@ import { PayloadExecutionStatus, PayloadStatus, ProtoBlock, + ProtoNode, } from "@lodestar/fork-choice"; import {ForkName, ForkPostBellatrix} from "@lodestar/params"; import {DataAvailabilityStatus, EffectiveBalanceIncrements, PubkeyCache} from "@lodestar/state-transition"; @@ -254,6 +255,49 @@ export interface IBeaconEngine { // builder-bid lookup, forkChoice exec hashes, base-state scalars) so they are not recomputed per path. // More of the production flow migrates here in later steps. getProposerHead(slot: Slot): ProtoBlock; + + // --- TODO - beacon engine: temp forkChoice read pass-throughs (curate when regen removed — BLK-3). --- + // Thin forwards to `forkChoice`; return ProtoBlock/ProtoNode/CheckpointWithHex directly (not bytes-honest + // yet). Head is engine-owned; head/checkpoint reads route here (see BLK-1 plan §1). + getHead(): ProtoBlock; + getHeadRoot(): RootHex; + getHeads(): ProtoBlock[]; + getFinalizedCheckpoint(): CheckpointWithHex; + getJustifiedCheckpoint(): CheckpointWithHex; + getUnrealizedJustifiedCheckpoint(): CheckpointWithHex; + getUnrealizedFinalizedCheckpoint(): CheckpointWithHex; + getProposerBoostRoot(): RootHex; + getPreviousProposerBoostRoot(): RootHex; + getPTCVoteCounts(blockRootHex: RootHex): { + attesterCount: number; + payloadPresentCount: number; + dataAvailableCount: number; + } | null; + getFinalizedBlock(): ProtoBlock; + getJustifiedBlock(): ProtoBlock; + getConfirmedRoot(): RootHex; + getConfirmedBlock(): ProtoBlock | null; + getBlockDefaultStatus(blockRoot: Root): ProtoBlock | null; + getBlockHexDefaultStatus(blockRoot: RootHex): ProtoBlock | null; + getBlockHex(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoBlock | null; + getBlockHexAndBlockHash(blockRoot: RootHex, blockHash: RootHex): ProtoBlock | null; + // Distinct name from the facade's DB-backed `getCanonicalBlockAtSlot` (async, returns a full block). + getCanonicalProtoBlockAtSlot(slot: Slot): ProtoBlock | null; + getCanonicalBlockByRoot(blockRoot: Root): ProtoBlock | null; + getCanonicalBlockClosestLteSlot(slot: Slot): ProtoBlock | null; + getBlockSummariesAtSlot(slot: Slot): ProtoBlock[]; + getBlockSummariesByParentRoot(parentRoot: RootHex): ProtoBlock[]; + getAllAncestorBlocks(blockRoot: RootHex, payloadStatus: PayloadStatus): ProtoBlock[]; + getAllNodes(): ProtoNode[]; + getSlotsPresent(windowStart: number): number; + // Hot path (per gossip message / sidecar) — NativeBeaconEngine caches these; JS engine forwards to forkChoice. + hasBlock(blockRoot: Root): boolean; + hasBlockHex(blockRoot: RootHex): boolean; + hasBlockHexUnsafe(blockRoot: RootHex): boolean; + hasPayloadHexUnsafe(blockRoot: RootHex): boolean; + // Projected thin ref (no ProtoBlock crosses) — DA-cache prune anchored at (blockRoot, payloadStatus). + getAllAncestorBlockRootSlots(blockRoot: RootHex, payloadStatus: PayloadStatus): BlockRootSlot[]; + // Execution payload envelope DB (engine-owned). Roots cross as raw bytes (FFI-honest). getExecutionPayloadEnvelope( blockSlot: Slot, diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts index 0992574c7b69..c59cab2ae159 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts @@ -2,7 +2,6 @@ import {ChainForkConfig} from "@lodestar/config"; import { BlockExecutionStatus, ExecutionStatus, - IForkChoiceRead, LVHInvalidResponse, LVHValidResponse, ProtoBlock, @@ -25,7 +24,6 @@ export type VerifyBlockExecutionPayloadModules = { clock: IClock; logger: Logger; metrics: Metrics | null; - forkChoice: IForkChoiceRead; config: ChainForkConfig; }; diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksSanityChecks.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksSanityChecks.ts index ad00ab8e88bd..5a429d21ba05 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksSanityChecks.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksSanityChecks.ts @@ -1,9 +1,10 @@ import {ChainForkConfig} from "@lodestar/config"; -import {IForkChoiceRead, ProtoBlock} from "@lodestar/fork-choice"; +import {ProtoBlock} from "@lodestar/fork-choice"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {RootHex, Slot, isGloasBeaconBlock} from "@lodestar/types"; import {toRootHex} from "@lodestar/utils"; import {IClock} from "../../util/clock.js"; +import {IBeaconEngine} from "../beaconEngine/index.js"; import {BlockError, BlockErrorCode} from "../errors/index.js"; import {IChainOptions} from "../options.js"; import {IBlockInput} from "./blockInput/types.js"; @@ -24,7 +25,7 @@ import {ImportBlockOpts} from "./types.js"; */ export function verifyBlocksSanityChecks( chain: { - forkChoice: IForkChoiceRead; + beaconEngine: IBeaconEngine; clock: IClock; config: ChainForkConfig; opts: IChainOptions; @@ -76,7 +77,7 @@ export function verifyBlocksSanityChecks( // Not finalized slot // IGNORE if `partiallyVerifiedBlock.ignoreIfFinalized` - const finalizedSlot = computeStartSlotAtEpoch(chain.forkChoice.getFinalizedCheckpoint().epoch); + const finalizedSlot = computeStartSlotAtEpoch(chain.beaconEngine.getFinalizedCheckpoint().epoch); if (blockSlot <= finalizedSlot) { if (opts.ignoreIfFinalized) { continue; @@ -92,7 +93,7 @@ export function verifyBlocksSanityChecks( } else { // When importing a block segment, only the first NON-IGNORED block must be known to the fork-choice. const parentRoot = toRootHex(block.message.parentRoot); - const parentBlockDefaultStatus = chain.forkChoice.getBlockHexDefaultStatus(parentRoot); + const parentBlockDefaultStatus = chain.beaconEngine.getBlockHexDefaultStatus(parentRoot); if (!parentBlockDefaultStatus) { throw new BlockError(block, {code: BlockErrorCode.PARENT_UNKNOWN, parentRoot}); } @@ -100,7 +101,7 @@ export function verifyBlocksSanityChecks( parentBlock = parentBlockDefaultStatus; if (isGloasBeaconBlock(block.message)) { const parentBlockHash = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash); - const parentBlockWithPayload = chain.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHash); + const parentBlockWithPayload = chain.beaconEngine.getBlockHexAndBlockHash(parentRoot, parentBlockHash); if (!parentBlockWithPayload) { // Checkpoint sync: parent's FULL variant may not be in fork-choice yet because the // anchor block is initialized with PENDING+EMPTY only. The parent's payload arrives @@ -130,7 +131,7 @@ export function verifyBlocksSanityChecks( // Not already known // IGNORE if `partiallyVerifiedBlock.ignoreIfKnown` - if (chain.forkChoice.hasBlockHex(blockHash)) { + if (chain.beaconEngine.hasBlockHex(blockHash)) { if (opts.ignoreIfKnown) { continue; } diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index aa09fef19605..0e4def2c314d 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -2,7 +2,7 @@ import path from "node:path"; import {PrivateKey} from "@libp2p/interface"; import {Type} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; -import {CheckpointWithHex, IForkChoiceRead, ProtoBlock} from "@lodestar/fork-choice"; +import {CheckpointWithHex, ProtoBlock} from "@lodestar/fork-choice"; import {LoggerNode} from "@lodestar/logger/node"; import { EFFECTIVE_BALANCE_INCREMENT, @@ -137,14 +137,11 @@ export class BeaconChain implements IBeaconChain { get bls(): IBlsVerifier { return this.beaconEngine.bls; } - // Read facet only — writes go through `beaconEngine.forkChoice`. TODO - beacon engine: remove this - // getter in Phase 6 when the facade holds no state. - get forkChoice(): IForkChoiceRead { - return this.beaconEngine.forkChoice; - } // Cached head ProtoBlock, updated from importBlock's result. Lets the facade serve getStatus and // detect finalized/justified transitions (it carries finalized*/justified* checkpoints) without the // engine emitting into the JS emitter (FFI-honest). Not the source of truth for all head reads yet. + // TODO - beacon engine: last fork-choice-derived field on the facade — move into the engine (the head + // cache belongs in NativeBeaconEngine); the checkpoint-event emission that reads it moves too. private headProtoBlock: ProtoBlock; readonly clock: IClock; readonly emitter: ChainEventEmitter; @@ -307,7 +304,7 @@ export class BeaconChain implements IBeaconChain { anchorState ); // Seed the facade head cache from the engine's initial head (one-time read); refreshed on each import. - this.headProtoBlock = this.beaconEngine.forkChoice.getHead(); + this.headProtoBlock = this.beaconEngine.getHead(); this.blacklistedBlocks = new Map((opts.blacklistedBlocks ?? []).map((hex) => [hex, null])); @@ -330,8 +327,7 @@ export class BeaconChain implements IBeaconChain { this.seenPayloadEnvelopeInputCache = new SeenPayloadEnvelopeInput({ config, clock, - // TODO - beacon engine: cannot have forkchoice here - forkChoice: this.beaconEngine.forkChoice, + beaconEngine: this.beaconEngine, chainEvents: emitter, signal, serializedCache: this.serializedCache, @@ -434,11 +430,11 @@ export class BeaconChain implements IBeaconChain { } seenBlock(blockRoot: RootHex): boolean { - return this.seenBlockInputCache.hasBlock(blockRoot) || this.forkChoice.hasBlockHexUnsafe(blockRoot); + return this.seenBlockInputCache.hasBlock(blockRoot) || this.beaconEngine.hasBlockHexUnsafe(blockRoot); } seenPayloadEnvelope(blockRoot: RootHex): boolean { - return this.seenPayloadEnvelopeInputCache.hasPayload(blockRoot) || this.forkChoice.hasPayloadHexUnsafe(blockRoot); + return this.seenPayloadEnvelopeInputCache.hasPayload(blockRoot) || this.beaconEngine.hasPayloadHexUnsafe(blockRoot); } regenCanAcceptWork(): boolean { @@ -471,7 +467,7 @@ export class BeaconChain implements IBeaconChain { // TODO - beacon engine: remove this getHeadState(): IBeaconStateView { // head state should always exist - const head = this.forkChoice.getHead(); + const head = this.beaconEngine.getHead(); const headState = this.regen.getClosestHeadState(head); if (!headState) { throw Error(`headState does not exist for head root=${head.blockRoot} slot=${head.slot}`); @@ -492,7 +488,7 @@ export class BeaconChain implements IBeaconChain { return headState; } // only use regen queue if necessary, it'll cache in checkpointStateCache if regen gets through epoch transition - const head = this.forkChoice.getHead(); + const head = this.beaconEngine.getHead(); const startSlot = computeStartSlotAtEpoch(epoch); return this.regen.getBlockSlotState(head, startSlot, {dontTransferCache: true}, regenCaller); } @@ -501,7 +497,7 @@ export class BeaconChain implements IBeaconChain { slot: Slot, opts?: StateGetOpts ): Promise<{state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean} | null> { - const finalizedBlock = this.forkChoice.getFinalizedBlock(); + const finalizedBlock = this.beaconEngine.getFinalizedBlock(); if (slot < finalizedBlock.slot) { // request for finalized state not supported in this API @@ -511,7 +507,7 @@ export class BeaconChain implements IBeaconChain { if (opts?.allowRegen) { // Find closest canonical block to slot, then trigger regen - const block = this.forkChoice.getCanonicalBlockClosestLteSlot(slot) ?? finalizedBlock; + const block = this.beaconEngine.getCanonicalBlockClosestLteSlot(slot) ?? finalizedBlock; const state = await this.regen.getBlockSlotState(block, slot, {dontTransferCache: true}, RegenCaller.restApi); return { state, @@ -522,7 +518,7 @@ export class BeaconChain implements IBeaconChain { // Just check if state is already in the cache. If it's not dialed to the correct slot, // do not bother in advancing the state. restApiCanTriggerRegen == false means do no work - const block = this.forkChoice.getCanonicalBlockAtSlot(slot); + const block = this.beaconEngine.getCanonicalProtoBlockAtSlot(slot); if (!block) { return null; } @@ -553,10 +549,10 @@ export class BeaconChain implements IBeaconChain { ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { if (opts?.allowRegen) { const state = await this.regen.getState(stateRoot, RegenCaller.restApi); - const block = this.forkChoice.getBlockDefaultStatus( + const block = this.beaconEngine.getBlockDefaultStatus( ssz.phase0.BeaconBlockHeader.hashTreeRoot(state.latestBlockHeader) ); - const finalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; + const finalizedEpoch = this.beaconEngine.getFinalizedCheckpoint().epoch; return { state, executionOptimistic: block != null && isOptimisticBlock(block), @@ -571,10 +567,10 @@ export class BeaconChain implements IBeaconChain { // TODO: This is very inneficient for debug requests of serialized content, since it deserializes to serialize again const cachedStateCtx = this.regen.getStateSync(stateRoot); if (cachedStateCtx) { - const block = this.forkChoice.getBlockDefaultStatus( + const block = this.beaconEngine.getBlockDefaultStatus( ssz.phase0.BeaconBlockHeader.hashTreeRoot(cachedStateCtx.latestBlockHeader) ); - const finalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; + const finalizedEpoch = this.beaconEngine.getFinalizedCheckpoint().epoch; return { state: cachedStateCtx, executionOptimistic: block != null && isOptimisticBlock(block), @@ -610,10 +606,10 @@ export class BeaconChain implements IBeaconChain { const checkpointHex = {epoch: checkpoint.epoch, rootHex: checkpoint.rootHex}; const cachedStateCtx = this.regen.getCheckpointStateSync(checkpointHex); if (cachedStateCtx) { - const block = this.forkChoice.getBlockDefaultStatus( + const block = this.beaconEngine.getBlockDefaultStatus( ssz.phase0.BeaconBlockHeader.hashTreeRoot(cachedStateCtx.latestBlockHeader) ); - const finalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; + const finalizedEpoch = this.beaconEngine.getFinalizedCheckpoint().epoch; return { state: cachedStateCtx, executionOptimistic: block != null && isOptimisticBlock(block), @@ -630,8 +626,8 @@ export class BeaconChain implements IBeaconChain { const checkpointHex = {epoch: checkpoint.epoch, rootHex: checkpoint.rootHex}; const cachedStateCtx = await this.regen.getCheckpointStateOrBytes(checkpointHex); if (cachedStateCtx) { - const block = this.forkChoice.getBlockDefaultStatus(checkpoint.root); - const finalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch; + const block = this.beaconEngine.getBlockDefaultStatus(checkpoint.root); + const finalizedEpoch = this.beaconEngine.getFinalizedCheckpoint().epoch; return { state: cachedStateCtx, executionOptimistic: block != null && isOptimisticBlock(block), @@ -645,10 +641,10 @@ export class BeaconChain implements IBeaconChain { async getCanonicalBlockAtSlot( slot: Slot ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { - const finalizedBlock = this.forkChoice.getFinalizedBlock(); + const finalizedBlock = this.beaconEngine.getFinalizedBlock(); if (slot > finalizedBlock.slot) { // Unfinalized slot, attempt to find in fork-choice - const block = this.forkChoice.getCanonicalBlockAtSlot(slot); + const block = this.beaconEngine.getCanonicalProtoBlockAtSlot(slot); if (block) { // Block found in fork-choice. // It may be in the block input cache, awaiting full DA reconstruction, check there first @@ -683,7 +679,7 @@ export class BeaconChain implements IBeaconChain { async getBlockByRoot( root: string ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { - const protoBlock = this.forkChoice.getBlockHexDefaultStatus(root); + const protoBlock = this.beaconEngine.getBlockHexDefaultStatus(root); if (protoBlock) { // Block found in fork-choice. // It may be in the block input cache, awaiting full DA reconstruction, check there first @@ -709,7 +705,7 @@ export class BeaconChain implements IBeaconChain { root: Uint8Array ): Promise<{block: Uint8Array; executionOptimistic: boolean; finalized: boolean; slot: Slot} | null> { // TODO - beacon engine: make a separate call, need to be lightweight - const protoBlock = this.forkChoice.getBlockHexDefaultStatus(toRootHex(root)); + const protoBlock = this.beaconEngine.getBlockHexDefaultStatus(toRootHex(root)); if (protoBlock) { // Block found in fork-choice. // It may be in the block input cache, awaiting full DA reconstruction, check there first @@ -1286,7 +1282,7 @@ export class BeaconChain implements IBeaconChain { // Only update validator custody if we discovered new validators if (discoveredNewValidators) { - const finalizedCheckpoint = this.forkChoice.getFinalizedCheckpoint(); + const finalizedCheckpoint = this.beaconEngine.getFinalizedCheckpoint(); await this.updateValidatorsCustodyRequirement(finalizedCheckpoint); } } @@ -1351,7 +1347,7 @@ export class BeaconChain implements IBeaconChain { const executionBuilder = this.executionBuilder; if (executionBuilder) { const {faultInspectionWindow, allowedFaults} = executionBuilder; - const slotsPresent = this.forkChoice.getSlotsPresent(clockSlot - faultInspectionWindow); + const slotsPresent = this.beaconEngine.getSlotsPresent(clockSlot - faultInspectionWindow); const previousStatus = executionBuilder.status; const shouldEnable = slotsPresent >= Math.min(faultInspectionWindow - allowedFaults, clockSlot); diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index e553afe6fd5b..96b74d789769 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -1,6 +1,6 @@ import {Type} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; -import {CheckpointWithHex, IForkChoiceRead} from "@lodestar/fork-choice"; +import {CheckpointWithHex} from "@lodestar/fork-choice"; import {IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; import { BeaconBlock, @@ -83,7 +83,6 @@ export interface IBeaconChain { readonly beaconEngine: IBeaconEngine; readonly bls: IBlsVerifier; - readonly forkChoice: IForkChoiceRead; readonly clock: IClock; readonly emitter: ChainEventEmitter; readonly regen: IStateRegenerator; diff --git a/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts b/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts index 496a76503799..5d134b5e1f75 100644 --- a/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts +++ b/packages/beacon-node/src/chain/seenCache/seenPayloadEnvelopeInput.ts @@ -1,11 +1,12 @@ import {ChainForkConfig} from "@lodestar/config"; -import {CheckpointWithHex, IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; +import {CheckpointWithHex, ProtoBlock} from "@lodestar/fork-choice"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {RootHex} from "@lodestar/types"; -import {Logger} from "@lodestar/utils"; +import {Logger, toRootHex} from "@lodestar/utils"; import {Metrics} from "../../metrics/metrics.js"; import {IClock} from "../../util/clock.js"; import {SerializedCache} from "../../util/serializedCache.js"; +import {IBeaconEngine} from "../beaconEngine/index.js"; import {isDaOutOfRange} from "../blocks/blockInput/index.js"; import {CreateFromBidProps, CreateFromBlockProps, PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; import {ChainEvent, ChainEventEmitter} from "../emitter.js"; @@ -16,7 +17,7 @@ export {PayloadEnvelopeInput} from "../blocks/payloadEnvelopeInput/index.js"; export type SeenPayloadEnvelopeInputModules = { config: ChainForkConfig; clock: IClock; - forkChoice: IForkChoice; + beaconEngine: IBeaconEngine; chainEvents: ChainEventEmitter; signal: AbortSignal; serializedCache: SerializedCache; @@ -36,7 +37,7 @@ export type SeenPayloadEnvelopeInputModules = { export class SeenPayloadEnvelopeInput { private readonly config: ChainForkConfig; private readonly clock: IClock; - private readonly forkChoice: IForkChoice; + private readonly beaconEngine: IBeaconEngine; private readonly chainEvents: ChainEventEmitter; private readonly signal: AbortSignal; private readonly serializedCache: SerializedCache; @@ -47,7 +48,7 @@ export class SeenPayloadEnvelopeInput { constructor({ config, clock, - forkChoice, + beaconEngine, chainEvents, signal, serializedCache, @@ -56,7 +57,7 @@ export class SeenPayloadEnvelopeInput { }: SeenPayloadEnvelopeInputModules) { this.config = config; this.clock = clock; - this.forkChoice = forkChoice; + this.beaconEngine = beaconEngine; this.chainEvents = chainEvents; this.signal = signal; this.serializedCache = serializedCache; @@ -153,14 +154,18 @@ export class SeenPayloadEnvelopeInput { } pruneBelowParent(parentBlock: ProtoBlock): void { - for (const block of this.forkChoice.getAllAncestorBlocks(parentBlock.blockRoot, parentBlock.payloadStatus)) { + for (const block of this.beaconEngine.getAllAncestorBlockRootSlots( + parentBlock.blockRoot, + parentBlock.payloadStatus + )) { if (block.slot < parentBlock.slot) { - const input = this.payloadInputs.get(block.blockRoot); + const blockRootHex = toRootHex(block.root); + const input = this.payloadInputs.get(blockRootHex); if (input) { this.evictPayloadInput(input); this.logger?.verbose("SeenPayloadEnvelopeInput.pruneBelowParent deleted", { slot: block.slot, - root: block.blockRoot, + root: blockRootHex, }); } } diff --git a/packages/beacon-node/src/network/processor/gossipHandlers.ts b/packages/beacon-node/src/network/processor/gossipHandlers.ts index 10f091c9a5f6..363cb32c4500 100644 --- a/packages/beacon-node/src/network/processor/gossipHandlers.ts +++ b/packages/beacon-node/src/network/processor/gossipHandlers.ts @@ -304,7 +304,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules): SequentialGossipHa const blockRootHex = toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(dataColumnBlockHeader)); // check to see if block has already been processed and BlockInput has been deleted (column received via reqresp or other means) - if (chain.forkChoice.hasBlockHex(blockRootHex)) { + // TODO - beacon engine: hot path (per column sidecar) — cached in NativeBeaconEngine. See BLK-1 plan §2b. + if (chain.beaconEngine.hasBlockHex(blockRootHex)) { metrics?.peerDas.dataColumnSidecarProcessingSkip.inc(); logger.debug("Already processed block for column sidecar, skipping processing", { slot, @@ -406,7 +407,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules): SequentialGossipHa const blockRootHex = toRootHex(dataColumnSidecar.beaconBlockRoot); // check to see if payload has already been processed and PayloadEnvelopeInput has been deleted (column received via reqresp or other means) - if (chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.FULL) !== null) { + // TODO - beacon engine: hot path (per column sidecar) — cached in NativeBeaconEngine. See BLK-1 plan §2b. + if (chain.beaconEngine.getBlockHex(blockRootHex, PayloadStatus.FULL) !== null) { metrics?.peerDas.dataColumnSidecarProcessingSkip.inc(); logger.debug("Already processed payload for column sidecar, skipping processing", { slot, diff --git a/packages/beacon-node/src/network/processor/index.ts b/packages/beacon-node/src/network/processor/index.ts index f04cc51434f5..2bdefb0c3410 100644 --- a/packages/beacon-node/src/network/processor/index.ts +++ b/packages/beacon-node/src/network/processor/index.ts @@ -342,8 +342,11 @@ export class NetworkProcessor { // this determines whether this message needs to wait for a Block or Envelope // a message should only wait for what it voted for, hence we don't want to put it on both queues let preprocessResult: PreprocessResult = {action: PreprocessAction.PushToQueue}; + // TODO - beacon engine: hot path (per gossip message) — the beaconEngine membership reads below + // (hasBlockHexUnsafe / hasPayloadHexUnsafe / getBlockHexAndBlockHash / getBlockHexDefaultStatus) are + // cached in NativeBeaconEngine so they don't cross FFI. See BLK-1 plan §2b. // no need to check if root is a descendant of the current finalized block, it will be checked once we validate the message if needed - if (root && !this.chain.forkChoice.hasBlockHexUnsafe(root)) { + if (root && !this.chain.beaconEngine.hasBlockHexUnsafe(root)) { // starting from GLOAS, unknown root from data_column_sidecar also falls into this case this.searchUnknownBlock({slot, root}, BlockInputSource.network_processor, message.propagationSource.toString()); // for beacon_attestation and beacon_aggregate_and_proof messages, this is only temporary. @@ -362,8 +365,8 @@ export class NetworkProcessor { if (ForkSeq[fork] >= ForkSeq.gloas) { // GLOAS: also check parent envelope, same logic as execution_payload_bid const parentBlockHash = getParentBlockHashFromGloasSignedBeaconBlockSerialized(message.msg.data); - if (parentBlockHash && !this.chain.forkChoice.getBlockHexAndBlockHash(parentRoot, parentBlockHash)) { - const protoBlock = this.chain.forkChoice.getBlockHexDefaultStatus(parentRoot); + if (parentBlockHash && !this.chain.beaconEngine.getBlockHexAndBlockHash(parentRoot, parentBlockHash)) { + const protoBlock = this.chain.beaconEngine.getBlockHexDefaultStatus(parentRoot); if (protoBlock === null) { this.searchUnknownBlock( {slot, root: parentRoot}, @@ -382,7 +385,7 @@ export class NetworkProcessor { ); } } - } else if (!this.chain.forkChoice.hasBlockHexUnsafe(parentRoot)) { + } else if (!this.chain.beaconEngine.hasBlockHexUnsafe(parentRoot)) { this.searchUnknownBlock( {slot, root: parentRoot}, BlockInputSource.network_processor, @@ -405,7 +408,7 @@ export class NetworkProcessor { topicType === GossipType.beacon_attestation ? getDataIndexFromSingleAttestationSerialized(fork, message.msg.data) : getDataIndexFromSignedAggregateAndProofSerialized(message.msg.data); - if (attIndex === 1 && !this.chain.forkChoice.hasPayloadHexUnsafe(root)) { + if (attIndex === 1 && !this.chain.beaconEngine.hasPayloadHexUnsafe(root)) { // attestation votes that the payload is available but it is not yet known this.searchUnknownEnvelope( {slot, root}, @@ -419,7 +422,7 @@ export class NetworkProcessor { case GossipType.payload_attestation_message: { if (root == null) break; const payloadPresent = getPayloadPresentFromPayloadAttestationMessageSerialized(message.msg.data); - if (payloadPresent && !this.chain.forkChoice.hasPayloadHexUnsafe(root)) { + if (payloadPresent && !this.chain.beaconEngine.hasPayloadHexUnsafe(root)) { // payload attestation votes that the payload is available but it is not yet known this.searchUnknownEnvelope( {slot, root}, @@ -433,7 +436,7 @@ export class NetworkProcessor { } case GossipType.data_column_sidecar: { if (root == null) break; - if (!this.chain.forkChoice.hasPayloadHexUnsafe(root)) { + if (!this.chain.beaconEngine.hasPayloadHexUnsafe(root)) { this.searchUnknownEnvelope( {slot, root}, BlockInputSource.network_processor, @@ -448,7 +451,7 @@ export class NetworkProcessor { // extractBlockSlotRootFn does not return a root for this topic. // Extract beacon_block_root directly const blockRoot = getBeaconBlockRootFromExecutionPayloadEnvelopeSerialized(message.msg.data); - if (blockRoot && !this.chain.forkChoice.hasBlockHexUnsafe(blockRoot)) { + if (blockRoot && !this.chain.beaconEngine.hasBlockHexUnsafe(blockRoot)) { this.searchUnknownBlock( {slot, root: blockRoot}, BlockInputSource.network_processor, @@ -467,9 +470,9 @@ export class NetworkProcessor { if ( parentBlockRoot && parentBlockHash && - !this.chain.forkChoice.getBlockHexAndBlockHash(parentBlockRoot, parentBlockHash) + !this.chain.beaconEngine.getBlockHexAndBlockHash(parentBlockRoot, parentBlockHash) ) { - const protoBlock = this.chain.forkChoice.getBlockHexDefaultStatus(parentBlockRoot); + const protoBlock = this.chain.beaconEngine.getBlockHexDefaultStatus(parentBlockRoot); if (protoBlock === null) { this.searchUnknownBlock( {slot, root: parentBlockRoot}, diff --git a/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRange.ts index ba914d027591..64f809924249 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRange.ts @@ -19,7 +19,7 @@ export async function* onBlobSidecarsByRange( const finalized = db.blobSidecarsArchive; const unfinalized = db.blobSidecars; - const finalizedSlot = chain.forkChoice.getFinalizedBlock().slot; + const finalizedSlot = chain.beaconEngine.getFinalizedBlock().slot; // Blobs are migrated to blobSidecarsArchive at finalization (including the finalized block // itself), so the archive loop serves up to AND INCLUDING finalizedSlot and the headChain // loop starts above it to avoid duplicate yields. See archiveBlocks.ts for the migration logic. @@ -38,10 +38,10 @@ export async function* onBlobSidecarsByRange( // Non-finalized range of blobs if (endSlot > archiveMaxSlot) { - const headBlock = chain.forkChoice.getHead(); + const headBlock = chain.beaconEngine.getHead(); const headRoot = headBlock.blockRoot; // TODO DENEB: forkChoice should mantain an array of canonical blocks, and change only on reorg - const headChain = chain.forkChoice.getAllAncestorBlocks(headRoot, headBlock.payloadStatus); + const headChain = chain.beaconEngine.getAllAncestorBlocks(headRoot, headBlock.payloadStatus); // `getAllAncestorBlocks` includes both the head and the previous-finalized boundary. // Iterate head chain with ascending block numbers diff --git a/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRoot.ts index d40a8b04839f..05dc816807c4 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/blobSidecarsByRoot.ts @@ -10,7 +10,7 @@ export async function* onBlobSidecarsByRoot( requestBody: BlobSidecarsByRootRequest, chain: IBeaconChain ): AsyncIterable { - const finalizedSlot = chain.forkChoice.getFinalizedBlock().slot; + const finalizedSlot = chain.beaconEngine.getFinalizedBlock().slot; // Spec: [max(current_epoch - MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS, DENEB_FORK_EPOCH), current_epoch] const currentEpoch = chain.clock.currentEpoch; @@ -27,7 +27,7 @@ export async function* onBlobSidecarsByRoot( for (const blobIdentifier of requestBody) { const {blockRoot, index} = blobIdentifier; const blockRootHex = toRootHex(blockRoot); - const block = chain.forkChoice.getBlockHexDefaultStatus(blockRootHex); + const block = chain.beaconEngine.getBlockHexDefaultStatus(blockRootHex); // NOTE: Only support non-finalized blocks. // SPEC: Clients MUST support requesting blocks and sidecars since the latest finalized epoch. diff --git a/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts index 8d8496314db6..8e977c76b878 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts @@ -51,7 +51,7 @@ export async function* onDataColumnSidecarsByRange( } const finalized = db.dataColumnSidecarArchive; - const finalizedSlot = chain.forkChoice.getFinalizedBlock().slot; + const finalizedSlot = chain.beaconEngine.getFinalizedBlock().slot; // Columns of the last finalized block live in different DBs depending on fork: // - Pre-gloas (fulu): migrated to dataColumnSidecarArchive in the same finalization run. // - Post-gloas: stay in the hot db (db.dataColumnSidecar) until the next finalization run, @@ -99,12 +99,12 @@ export async function* onDataColumnSidecarsByRange( // Non-finalized range of columns if (endSlot > archiveMaxSlot) { - const headBlock = chain.forkChoice.getHead(); + const headBlock = chain.beaconEngine.getHead(); const headRoot = headBlock.blockRoot; // getAllAncestorBlocks includes the last finalized block as its final element. // Skip anything the archive loop above already served via the block.slot > archiveMaxSlot // filter below (pre-gloas this skips finalizedSlot, post-gloas it keeps it). - const headChain = chain.forkChoice.getAllAncestorBlocks(headRoot, headBlock.payloadStatus); + const headChain = chain.beaconEngine.getAllAncestorBlocks(headRoot, headBlock.payloadStatus); // Iterate head chain with ascending block numbers for (let i = headChain.length - 1; i >= 0; i--) { diff --git a/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts index 5e4105932747..0ac8757d468b 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts @@ -32,7 +32,7 @@ export async function* onDataColumnSidecarsByRoot( } const blockRootHex = toRootHex(blockRoot); - const block = chain.forkChoice.getBlockHexDefaultStatus(blockRootHex); + const block = chain.beaconEngine.getBlockHexDefaultStatus(blockRootHex); // If the block is not in fork choice, it may be finalized. Attempt to find its slot in block archive const slot = block ? block.slot : await chain.getFinalizedBlockSlotByRoot(blockRoot); diff --git a/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts b/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts index 7b0221546712..02f58fa281fc 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts @@ -17,7 +17,7 @@ export async function* onExecutionPayloadEnvelopesByRoot( for (const root of requestBody) { const rootHex = toRootHex(root); - const block = chain.forkChoice.getBlockHexDefaultStatus(rootHex); + const block = chain.beaconEngine.getBlockHexDefaultStatus(rootHex); // If the block is not in fork choice, it may be finalized. Attempt to find its slot in block archive const slot = block ? block.slot : await chain.getFinalizedBlockSlotByRoot(root); diff --git a/packages/beacon-node/src/node/notifier.ts b/packages/beacon-node/src/node/notifier.ts index 37a59a9c28d4..4f36d4d5d7cf 100644 --- a/packages/beacon-node/src/node/notifier.ts +++ b/packages/beacon-node/src/node/notifier.ts @@ -65,7 +65,7 @@ export async function runNodeNotifier(modules: NodeNotifierModules): Promise { - this.update(this.chain.forkChoice.getFinalizedCheckpoint().epoch); + this.update(this.chain.beaconEngine.getFinalizedCheckpoint().epoch); this.emit(RangeSyncEvent.completedChain); if (err === null && target !== null) { @@ -369,7 +369,7 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) { syncChain.peers === 0 || // Outdated: our chain has progressed beyond this sync chain syncChain.target.slot < localFinalizedSlot || - this.chain.forkChoice.hasBlock(syncChain.target.root) + this.chain.beaconEngine.hasBlock(syncChain.target.root) ) { syncChain.remove(); this.chains.delete(id); diff --git a/packages/beacon-node/src/sync/sync.ts b/packages/beacon-node/src/sync/sync.ts index d877cbdfaa00..0d77b6dd7480 100644 --- a/packages/beacon-node/src/sync/sync.ts +++ b/packages/beacon-node/src/sync/sync.ts @@ -102,7 +102,7 @@ export class BeaconSync implements IBeaconSync { }; } - const head = this.chain.forkChoice.getHead(); + const head = this.chain.beaconEngine.getHead(); switch (this.state) { case SyncState.SyncingFinalized: @@ -139,7 +139,7 @@ export class BeaconSync implements IBeaconSync { get state(): SyncState { const currentSlot = this.chain.clock.currentSlot; - const headSlot = this.chain.forkChoice.getHead().slot; + const headSlot = this.chain.beaconEngine.getHead().slot; if ( // Consider node synced IF @@ -187,7 +187,7 @@ export class BeaconSync implements IBeaconSync { */ private addPeer = (data: NetworkEventData[NetworkEvent.peerConnected]): void => { const localStatus = this.chain.getStatus(); - const syncType = getPeerSyncType(localStatus, data.status, this.chain.forkChoice, this.slotImportTolerance); + const syncType = getPeerSyncType(localStatus, data.status, this.chain.beaconEngine, this.slotImportTolerance); this.logger.verbose("Peer sync type classified", { peer: data.peer, syncType, @@ -249,7 +249,7 @@ export class BeaconSync implements IBeaconSync { // If we stopped being synced and fallen significantly behind, stop gossip else if (state !== SyncState.Synced) { - const syncDiff = this.chain.clock.currentSlot - this.chain.forkChoice.getHead().slot; + const syncDiff = this.chain.clock.currentSlot - this.chain.beaconEngine.getHead().slot; if (syncDiff > this.slotImportTolerance * 2) { if (this.network.isSubscribedToGossipCoreTopics()) { this.logger.warn(`Node sync has fallen behind by ${syncDiff} slots`); diff --git a/packages/beacon-node/src/sync/unknownBlock.ts b/packages/beacon-node/src/sync/unknownBlock.ts index 28da54381950..e7d18811bd3b 100644 --- a/packages/beacon-node/src/sync/unknownBlock.ts +++ b/packages/beacon-node/src/sync/unknownBlock.ts @@ -439,7 +439,7 @@ export class BlockInputSync { | {kind: "block" | "parentBlock" | "parentPayload"; rootHex: RootHex} | {kind: "invalidParentPayload"; parentRootHex: RootHex; parentBlockHashHex: RootHex} { const parentRootHex = blockInput.parentRootHex; - if (!this.chain.forkChoice.hasBlockHex(parentRootHex)) { + if (!this.chain.beaconEngine.hasBlockHex(parentRootHex)) { return {kind: "parentBlock", rootHex: parentRootHex}; } @@ -453,11 +453,11 @@ export class BlockInputSync { const block = blockInput.getBlock() as gloas.SignedBeaconBlock; const parentBlockHashHex = toRootHex(block.message.body.signedExecutionPayloadBid.message.parentBlockHash); - if (this.chain.forkChoice.getBlockHexAndBlockHash(parentRootHex, parentBlockHashHex) !== null) { + if (this.chain.beaconEngine.getBlockHexAndBlockHash(parentRootHex, parentBlockHashHex) !== null) { return {kind: "ready"}; } - if (this.chain.forkChoice.hasPayloadHexUnsafe(parentRootHex)) { + if (this.chain.beaconEngine.hasPayloadHexUnsafe(parentRootHex)) { return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex}; } @@ -686,11 +686,11 @@ export class BlockInputSync { const pending = res.result; this.pendingBlocks.set(pending.blockInput.blockRootHex, pending); const blockSlot = pending.blockInput.slot; - const finalizedSlot = this.chain.forkChoice.getFinalizedBlock().slot; + const finalizedSlot = this.chain.beaconEngine.getFinalizedBlock().slot; const delaySec = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.chain.genesisTime); this.metrics?.blockInputSync.elapsedTimeTillReceived.observe(delaySec); - const parentInForkChoice = this.chain.forkChoice.hasBlockHex(pending.blockInput.parentRootHex); + const parentInForkChoice = this.chain.beaconEngine.hasBlockHex(pending.blockInput.parentRootHex); const logCtx2 = { ...logCtx, slot: blockSlot, @@ -867,13 +867,13 @@ export class BlockInputSync { */ private async reconcilePayloadEnvelope(pendingPayload: PendingPayloadEnvelope): Promise { const rootHex = getPayloadSyncCacheItemRootHex(pendingPayload); - if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) { + if (this.chain.beaconEngine.hasPayloadHexUnsafe(rootHex)) { this.pendingPayloads.delete(rootHex); return; } const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(rootHex); - if (!this.chain.forkChoice.hasBlockHex(rootHex)) { + if (!this.chain.beaconEngine.hasBlockHex(rootHex)) { // Block not in fork choice yet. payloadInput may be seeded from the block body during download, so a // non-null payloadInput does not imply the block is imported; defer regardless and pull the block first. // onBlockImported re-triggers the search to resume this envelope. @@ -947,7 +947,7 @@ export class BlockInputSync { } const rootHex = getPayloadSyncCacheItemRootHex(payload); - if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) { + if (this.chain.beaconEngine.hasPayloadHexUnsafe(rootHex)) { this.pendingPayloads.delete(rootHex); return; } @@ -1003,13 +1003,13 @@ export class BlockInputSync { return; } - if (this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)) { + if (this.chain.beaconEngine.hasPayloadHexUnsafe(rootHex)) { this.logger.debug("Payload already imported while processing unknown payload", logCtx); this.pendingPayloads.delete(rootHex); return; } - if (!this.chain.forkChoice.hasBlockHex(rootHex)) { + if (!this.chain.beaconEngine.hasBlockHex(rootHex)) { this.logger.debug("Payload input is ready before its block is in fork choice", logCtx); const added = this.addByRootHex(rootHex); pendingPayload.status = PendingPayloadInputStatus.downloaded; @@ -1114,7 +1114,7 @@ export class BlockInputSync { } payloadInput ??= this.chain.seenPayloadEnvelopeInputCache.get(rootHex); - if (!this.chain.forkChoice.hasBlockHex(rootHex)) { + if (!this.chain.beaconEngine.hasBlockHex(rootHex)) { // Block not in fork choice yet. Validating now would throw BLOCK_ROOT_UNKNOWN, so keep the downloaded // envelope and wait for the block body; reconcilePayloadEnvelope validates once the block lands. // payloadInput may be seeded from the block body during download, so a non-null payloadInput does not @@ -1404,9 +1404,9 @@ export class BlockInputSync { if (cacheItem.status === PendingBlockInputStatus.downloaded) { // download was successful, no need to go with another peer, return - const result = this.chain.forkChoice.hasBlockHex(cacheItem.blockInput.blockRootHex) + const result = this.chain.beaconEngine.hasBlockHex(cacheItem.blockInput.blockRootHex) ? FetchResult.SuccessLate - : this.chain.forkChoice.hasBlockHex(cacheItem.blockInput.parentRootHex) + : this.chain.beaconEngine.hasBlockHex(cacheItem.blockInput.parentRootHex) ? FetchResult.SuccessResolved : FetchResult.SuccessMissingParent; this.metrics?.blockInputSync.fetchTimeSec.observe({result}, Date.now() / 1000 - fetchStartSec); diff --git a/packages/beacon-node/src/sync/utils/remoteSyncType.ts b/packages/beacon-node/src/sync/utils/remoteSyncType.ts index e3fb0801090b..6fbc5c3231c4 100644 --- a/packages/beacon-node/src/sync/utils/remoteSyncType.ts +++ b/packages/beacon-node/src/sync/utils/remoteSyncType.ts @@ -1,6 +1,6 @@ -import {IForkChoiceRead} from "@lodestar/fork-choice"; import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {Slot, Status} from "@lodestar/types"; +import {IBeaconEngine} from "../../chain/beaconEngine/index.js"; import {IBeaconChain} from "../../chain/interface.js"; import {ChainTarget} from "../range/utils/index.js"; @@ -24,7 +24,7 @@ function withinRangeOf(value: number, target: number, range: number): boolean { export function getPeerSyncType( local: Status, remote: Status, - forkChoice: IForkChoiceRead, + beaconEngine: IBeaconEngine, slotImportTolerance: number ): PeerSyncType { // Aux vars: Inclusive boundaries of the range to consider a peer's head synced to ours. @@ -56,7 +56,7 @@ export function getPeerSyncType( (local.finalizedEpoch + 1 === remote.finalizedEpoch && withinRangeOf(remote.headSlot, local.headSlot, slotImportTolerance)) || // Peer's head is known => SYNCED - forkChoice.hasBlock(remote.headRoot) + beaconEngine.hasBlock(remote.headRoot) ) { return PeerSyncType.FullySynced; } @@ -71,7 +71,7 @@ export function getPeerSyncType( return PeerSyncType.Behind; } - if (remote.headSlot > nearRangeEnd && !forkChoice.hasBlock(remote.headRoot)) { + if (remote.headSlot > nearRangeEnd && !beaconEngine.hasBlock(remote.headRoot)) { // This peer has a head ahead enough of ours and we have no knowledge of their best block. return PeerSyncType.Advanced; } @@ -94,8 +94,8 @@ export const rangeSyncTypes = Object.keys(RangeSyncType) as RangeSyncType[]; * - The remotes finalized epoch is greater than our current finalized epoch and we have * not seen the finalized hash before */ -export function getRangeSyncType(local: Status, remote: Status, forkChoice: IForkChoiceRead): RangeSyncType { - if (remote.finalizedEpoch > local.finalizedEpoch && !forkChoice.hasBlock(remote.finalizedRoot)) { +export function getRangeSyncType(local: Status, remote: Status, beaconEngine: IBeaconEngine): RangeSyncType { + if (remote.finalizedEpoch > local.finalizedEpoch && !beaconEngine.hasBlock(remote.finalizedRoot)) { return RangeSyncType.Finalized; } return RangeSyncType.Head; @@ -106,17 +106,17 @@ export function getRangeSyncTarget( remote: Status, chain: IBeaconChain ): {syncType: RangeSyncType; startEpoch: Slot; target: ChainTarget} { - const forkChoice = chain.forkChoice; + const beaconEngine = chain.beaconEngine; // finalized sync - if (remote.finalizedEpoch > local.finalizedEpoch && !forkChoice.hasBlock(remote.finalizedRoot)) { + if (remote.finalizedEpoch > local.finalizedEpoch && !beaconEngine.hasBlock(remote.finalizedRoot)) { return { // If RangeSyncType.Finalized, the range of blocks fetchable from startEpoch and target must allow to switch // to RangeSyncType.Head // // finalizedRoot is a block with slot <= computeStartSlotAtEpoch(finalizedEpoch). // If finalizedEpoch does not start with a skipped slot, the SyncChain with this target MUST process the - // first block of the next epoch in order to trigger the condition above `forkChoice.hasBlock(remote.finalizedRoot)` + // first block of the next epoch in order to trigger the condition above `beaconEngine.hasBlock(remote.finalizedRoot)` // and do a Head sync. // // When doing a finalized sync, we'll process blocks up to the finalized checkpoint, which does not allow to diff --git a/packages/beacon-node/test/e2e/chain/proposerBoostReorg.test.ts b/packages/beacon-node/test/e2e/chain/proposerBoostReorg.test.ts index 460fcc13314f..84a3da9cf4f1 100644 --- a/packages/beacon-node/test/e2e/chain/proposerBoostReorg.test.ts +++ b/packages/beacon-node/test/e2e/chain/proposerBoostReorg.test.ts @@ -89,7 +89,7 @@ describe("proposer boost reorg", () => { logger, }); - (bn.chain.forkChoice as TimelinessForkChoice).lateSlot = reorgSlot; + (bn.chain.beaconEngine.forkChoice as TimelinessForkChoice).lateSlot = reorgSlot; afterEachCallbacks.push(async () => bn.close()); const {validators} = await getAndInitDevValidators({ node: bn, diff --git a/packages/beacon-node/test/e2e/chain/stateCache/nHistoricalStates.test.ts b/packages/beacon-node/test/e2e/chain/stateCache/nHistoricalStates.test.ts index 40182ecb5d9a..11806c99601f 100644 --- a/packages/beacon-node/test/e2e/chain/stateCache/nHistoricalStates.test.ts +++ b/packages/beacon-node/test/e2e/chain/stateCache/nHistoricalStates.test.ts @@ -373,13 +373,13 @@ describe("regen/reload states with n-historical states configuration", () => { ); expect(checkpoints[0]).toEqual(checkpoints[1]); expect(checkpoints[0].epoch).toEqual(3); - const head = reorgedBn.chain.forkChoice.getHead(); + const head = reorgedBn.chain.beaconEngine.forkChoice.getHead(); loggerNodeA.info("Node A emitted checkpoint event, head slot: " + head.slot); // setup reorg data for both bns for (const bn of [reorgedBn, followupBn]) { - (bn.chain.forkChoice as ReorgedForkChoice).reorgedSlot = reorgedSlot; - (bn.chain.forkChoice as ReorgedForkChoice).reorgDistance = reorgDistance; + (bn.chain.beaconEngine.forkChoice as ReorgedForkChoice).reorgedSlot = reorgedSlot; + (bn.chain.beaconEngine.forkChoice as ReorgedForkChoice).reorgDistance = reorgDistance; } // both nodes see the reorg event diff --git a/packages/beacon-node/test/e2e/sync/checkpointSync.test.ts b/packages/beacon-node/test/e2e/sync/checkpointSync.test.ts index ccc5aa92cce8..d0cc1e26483d 100644 --- a/packages/beacon-node/test/e2e/sync/checkpointSync.test.ts +++ b/packages/beacon-node/test/e2e/sync/checkpointSync.test.ts @@ -63,9 +63,12 @@ describe("sync / checkpoint sync optimistic flow for gloas", () => { ): Array<{blockRoot: string; slot: Slot; variant: PayloadStatus}> { // For ancestors below head: check all 3 variants (PENDING, EMPTY, FULL). // For the head: range sync may have delivered the block but not its envelope yet. - const bn2Head = bn2.chain.forkChoice.getHead(); + const bn2Head = bn2.chain.beaconEngine.forkChoice.getHead(); const gloasFirstSlot = GLOAS_FORK_EPOCH * SLOTS_PER_EPOCH; - const bn2Ancestors = bn2.chain.forkChoice.getAllAncestorBlocks(bn2Head.blockRoot, bn2Head.payloadStatus); + const bn2Ancestors = bn2.chain.beaconEngine.forkChoice.getAllAncestorBlocks( + bn2Head.blockRoot, + bn2Head.payloadStatus + ); const variantsToCheck: Array<{blockRoot: string; slot: Slot; variant: PayloadStatus}> = []; for (const ancestor of bn2Ancestors) { @@ -81,7 +84,7 @@ describe("sync / checkpoint sync optimistic flow for gloas", () => { ); for (const {blockRoot, slot, variant} of variantsToCheck) { - const node = bn2.chain.forkChoice.getBlockHex(blockRoot, variant); + const node = bn2.chain.beaconEngine.forkChoice.getBlockHex(blockRoot, variant); expect(node?.executionStatus).toBeWithMessage( ExecutionStatus.Syncing, `expected gloas variant ${variant} of slot=${slot} root=${blockRoot} to be Syncing pre-validation` @@ -102,8 +105,8 @@ describe("sync / checkpoint sync optimistic flow for gloas", () => { variantsToCheck: Array<{blockRoot: string; slot: Slot; variant: PayloadStatus}>, loggerNodeB: ReturnType ): void { - const bn2Head = bn2.chain.forkChoice.getHead(); - const nodeAHeadFull = bn.chain.forkChoice.getBlockHex(bn2Head.blockRoot, PayloadStatus.FULL); + const bn2Head = bn2.chain.beaconEngine.forkChoice.getHead(); + const nodeAHeadFull = bn.chain.beaconEngine.forkChoice.getBlockHex(bn2Head.blockRoot, PayloadStatus.FULL); if (nodeAHeadFull === null || nodeAHeadFull.executionPayloadBlockHash === null) { throw Error(`No FULL variant found on Node A for synced head root=${bn2Head.blockRoot}`); } @@ -123,7 +126,7 @@ describe("sync / checkpoint sync optimistic flow for gloas", () => { {blockRoot: bn2Head.blockRoot, slot: bn2Head.slot, variant: PayloadStatus.FULL}, ]; for (const {blockRoot, slot, variant} of allVariants) { - const node = bn2.chain.forkChoice.getBlockHex(blockRoot, variant); + const node = bn2.chain.beaconEngine.forkChoice.getBlockHex(blockRoot, variant); expect(node?.executionStatus).toBeWithMessage( ExecutionStatus.Valid, `expected gloas variant ${variant} of slot=${slot} root=${blockRoot} to be Valid post-validation` @@ -171,8 +174,8 @@ describe("sync / checkpoint sync optimistic flow for gloas", () => { // from slot 24 onward fetches the dangling parent payload envelope of block 23. const REORGED_SLOT = (GLOAS_FORK_EPOCH + 1) * SLOTS_PER_EPOCH; // 24 const REORG_DISTANCE = 2; // block at slot 25 has parent at slot 23 - (bn.chain.forkChoice as ReorgedForkChoice).reorgedSlot = REORGED_SLOT; - (bn.chain.forkChoice as ReorgedForkChoice).reorgDistance = REORG_DISTANCE; + (bn.chain.beaconEngine.forkChoice as ReorgedForkChoice).reorgedSlot = REORGED_SLOT; + (bn.chain.beaconEngine.forkChoice as ReorgedForkChoice).reorgDistance = REORG_DISTANCE; const {validators} = await getAndInitDevValidators({ node: bn, @@ -206,7 +209,7 @@ describe("sync / checkpoint sync optimistic flow for gloas", () => { // Confirm the finalized checkpoint is the skipped-slot one: root must resolve to the // block at slot 23 (last block before the reorged slot 24), not slot 24. - const finalizedBlock = bn.chain.forkChoice.getFinalizedBlock(); + const finalizedBlock = bn.chain.beaconEngine.forkChoice.getFinalizedBlock(); expect( finalizedBlock.slot, "Skipped checkpoint expected: finalized block should be at slot 23, not slot 24" @@ -251,7 +254,7 @@ describe("sync / checkpoint sync optimistic flow for gloas", () => { }); afterEachCallbacks.push(() => bn2.close()); - const headSummary = bn.chain.forkChoice.getHead(); + const headSummary = bn.chain.beaconEngine.forkChoice.getHead(); // 30s is plenty: Node B has to range-sync only ~17 blocks (slot 24 → ~slot 40) over // localhost loopback. A successful sync completes in seconds; this timeout exists to // bound the failure case so we surface a meaningful expect.fail message quickly. @@ -343,7 +346,7 @@ describe("sync / checkpoint sync optimistic flow for gloas", () => { loggerNodeA.info("Node A reached the epoch-3 finalized checkpoint"); // Confirm the finalized checkpoint resolves to the block AT the boundary slot (regular case). - const finalizedBlock = bn.chain.forkChoice.getFinalizedBlock(); + const finalizedBlock = bn.chain.beaconEngine.forkChoice.getFinalizedBlock(); expect(finalizedBlock.slot, "Regular checkpoint expected: finalized block should be at the boundary slot").toEqual( EPOCH_3_BOUNDARY_SLOT ); @@ -376,7 +379,7 @@ describe("sync / checkpoint sync optimistic flow for gloas", () => { }); afterEachCallbacks.push(() => bn2.close()); - const headSummary = bn.chain.forkChoice.getHead(); + const headSummary = bn.chain.beaconEngine.forkChoice.getHead(); const waitForSynced = waitForEvent( bn2.chain.emitter, routes.events.EventType.head, diff --git a/packages/beacon-node/test/e2e/sync/finalizedSync.test.ts b/packages/beacon-node/test/e2e/sync/finalizedSync.test.ts index 526fb469990c..0c3335ae0a69 100644 --- a/packages/beacon-node/test/e2e/sync/finalizedSync.test.ts +++ b/packages/beacon-node/test/e2e/sync/finalizedSync.test.ts @@ -126,7 +126,7 @@ describe("sync / finalized sync for gloas", () => { afterEachCallbacks.push(() => bn2.close()); afterEachCallbacks.push(() => bn2.close()); - const headSummary = bn.chain.forkChoice.getHead(); + const headSummary = bn.chain.beaconEngine.forkChoice.getHead(); const head = await bn.db.block.get(fromHexString(headSummary.blockRoot)); if (!head) throw Error("First beacon node has no head block"); const waitForSynced = waitForEvent( @@ -149,17 +149,20 @@ describe("sync / finalized sync for gloas", () => { // Walk Node B's fork-choice from head back through its ancestors. Every gloas block in the // canonical chain below the head MUST have its FULL payload variant in fork-choice // Also assert that some PTC votes have been included - const bn2Head = bn2.chain.forkChoice.getHead(); - const bn2Ancestors = bn2.chain.forkChoice.getAllAncestorBlocks(bn2Head.blockRoot, bn2Head.payloadStatus); + const bn2Head = bn2.chain.beaconEngine.forkChoice.getHead(); + const bn2Ancestors = bn2.chain.beaconEngine.forkChoice.getAllAncestorBlocks( + bn2Head.blockRoot, + bn2Head.payloadStatus + ); const gloasFirstSlot = GLOAS_FORK_EPOCH * SLOTS_PER_EPOCH; for (const block of bn2Ancestors) { if (block.slot >= gloasFirstSlot && block.slot < bn2Head.slot) { - expect(bn2.chain.forkChoice.hasPayloadHexUnsafe(block.blockRoot)).toBeWithMessage( + expect(bn2.chain.beaconEngine.forkChoice.hasPayloadHexUnsafe(block.blockRoot)).toBeWithMessage( true, `Node B missing FULL payload variant for gloas block slot=${block.slot} root=${block.blockRoot}` ); if (block.slot > gloasFirstSlot) { - const ptcVotes = bn2.chain.forkChoice.getPTCVotes(block.blockRoot) ?? []; + const ptcVotes = bn2.chain.beaconEngine.forkChoice.getPTCVotes(block.blockRoot) ?? []; expect(ptcVotes.some(Boolean)).toBeWithMessage( true, diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index 83f10d7010aa..71e0008b9b9d 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -289,6 +289,38 @@ export function getMockedBeaconChain(opts?: Partial): "updateTime", "getIrrecoverableError", "validateLatestHash", + // Temp forkChoice read pass-throughs (forward to the mock's forkChoice) — BLK-1. + "getHead", + "getHeadRoot", + "getHeads", + "getFinalizedCheckpoint", + "getJustifiedCheckpoint", + "getUnrealizedJustifiedCheckpoint", + "getUnrealizedFinalizedCheckpoint", + "getProposerBoostRoot", + "getPreviousProposerBoostRoot", + "getPTCVoteCounts", + "getFinalizedBlock", + "getJustifiedBlock", + "getConfirmedRoot", + "getConfirmedBlock", + "getBlockDefaultStatus", + "getBlockHexDefaultStatus", + "getBlockHex", + "getBlockHexAndBlockHash", + "getCanonicalProtoBlockAtSlot", + "getCanonicalBlockByRoot", + "getCanonicalBlockClosestLteSlot", + "getBlockSummariesAtSlot", + "getBlockSummariesByParentRoot", + "getAllAncestorBlocks", + "getAllNodes", + "getSlotsPresent", + "hasBlock", + "hasBlockHex", + "hasBlockHexUnsafe", + "hasPayloadHexUnsafe", + "getAllAncestorBlockRootSlots", ] as const) { (chain as unknown as Record)[name] = ( BeaconEngine.prototype as unknown as Record diff --git a/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts index b9266d3e26a8..fe69e5c9306a 100644 --- a/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts +++ b/packages/beacon-node/test/perf/chain/produceBlock/produceBlockBody.test.ts @@ -81,7 +81,7 @@ describe("produceBlockBody", () => { maxMs: Infinity, timeoutBench: 60 * 1000, beforeEach: async () => { - const head = chain.forkChoice.getHead(); + const head = chain.beaconEngine.forkChoice.getHead(); const proposerIndex = state.epochCtx.getBeaconProposer(state.slot); const proposerPubKey = state.epochCtx.pubkeyCache.getOrThrow(proposerIndex).toBytes(); 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 717f4a634b1a..0f21b7edbfb0 100644 --- a/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts +++ b/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts @@ -378,11 +378,11 @@ const fastConfirmationTest = logger.debug(`Step ${i}/${stepsLen} check`); // Forkchoice head is computed lazily only on request - const head = (chain.forkChoice as ForkChoice).updateHead(); - const proposerBootRoot = (chain.forkChoice as ForkChoice).getProposerBoostRoot(); - const justifiedCheckpoint = chain.forkChoice.getJustifiedCheckpoint(); - const finalizedCheckpoint = chain.forkChoice.getFinalizedCheckpoint(); - const confirmedRoot = chain.forkChoice.getConfirmedRoot(); + const head = (chain.beaconEngine.forkChoice as ForkChoice).updateHead(); + const proposerBootRoot = (chain.beaconEngine.forkChoice as ForkChoice).getProposerBoostRoot(); + const justifiedCheckpoint = chain.beaconEngine.forkChoice.getJustifiedCheckpoint(); + const finalizedCheckpoint = chain.beaconEngine.forkChoice.getFinalizedCheckpoint(); + const confirmedRoot = chain.beaconEngine.forkChoice.getConfirmedRoot(); if (step.checks.head) { expect({slot: head.slot, root: head.blockRoot}).toEqualWithMessage( @@ -402,7 +402,7 @@ const fastConfirmationTest = // Compare in slots because proposer boost steps doesn't always come on // slot boundary. if (step.checks.time && step.checks.time > 0) - expect(chain.forkChoice.getTime()).toEqualWithMessage( + expect(chain.beaconEngine.forkChoice.getTime()).toEqualWithMessage( Math.floor(bnToNum(step.checks.time) / (config.SLOT_DURATION_MS / 1000)), `Invalid forkchoice time at step ${i}` ); @@ -423,7 +423,7 @@ const fastConfirmationTest = if (step.checks.get_proposer_head) { const currentSlot = Math.floor(tickTime / (config.SLOT_DURATION_MS / 1000)); - const {proposerHead, notReorgedReason} = (chain.forkChoice as ForkChoice).getProposerHead( + const {proposerHead, notReorgedReason} = (chain.beaconEngine.forkChoice as ForkChoice).getProposerHead( head, tickTime % (config.SLOT_DURATION_MS / 1000), currentSlot @@ -460,7 +460,7 @@ const fastConfirmationTest = if (step.checks.should_override_forkchoice_update) { const currentSlot = Math.floor(tickTime / (config.SLOT_DURATION_MS / 1000)); - const result = chain.forkChoice.shouldOverrideForkChoiceUpdate( + const result = chain.beaconEngine.forkChoice.shouldOverrideForkChoiceUpdate( head, tickTime % (config.SLOT_DURATION_MS / 1000), currentSlot diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index d4515df94305..0ab6ffe72887 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -226,7 +226,7 @@ const forkChoiceTest = throw Error(`No payload attestation message ${step.payload_attestation_message}`); try { const blockRoot = toRootHex(payloadAttestationMessage.data.beaconBlockRoot); - const protoBlock = chain.forkChoice.getBlockHexDefaultStatus(blockRoot); + const protoBlock = chain.beaconEngine.forkChoice.getBlockHexDefaultStatus(blockRoot); if (!protoBlock) { throw Error(`Block not found for root ${blockRoot}`); } @@ -487,7 +487,7 @@ const forkChoiceTest = const gasLimit = envelope.message.payload.gasLimit; // Verify envelope against the state - const protoBlock = chain.forkChoice.getBlockHexDefaultStatus(beaconBlockRoot); + const protoBlock = chain.beaconEngine.forkChoice.getBlockHexDefaultStatus(beaconBlockRoot); if (!protoBlock) throw Error(`Block not found for root ${beaconBlockRoot}`); const blockState = await chain.regen.getBlockSlotState( protoBlock, @@ -515,7 +515,7 @@ const forkChoiceTest = validationError: null, }); - (chain.forkChoice as ForkChoice).onExecutionPayload( + (chain.beaconEngine.forkChoice as ForkChoice).onExecutionPayload( beaconBlockRoot, blockHash, blockNumber, @@ -548,8 +548,8 @@ const forkChoiceTest = logger.debug(`Step ${i}/${stepsLen} check`); // Forkchoice head is computed lazily only on request - const head = (chain.forkChoice as ForkChoice).updateHead(); - const proposerBootRoot = (chain.forkChoice as ForkChoice).getProposerBoostRoot(); + const head = (chain.beaconEngine.forkChoice as ForkChoice).updateHead(); + const proposerBootRoot = (chain.beaconEngine.forkChoice as ForkChoice).getProposerBoostRoot(); if (step.checks.head !== undefined) { expect({slot: head.slot, root: head.blockRoot}).toEqualWithMessage( @@ -567,25 +567,25 @@ const forkChoiceTest = // Compare in slots because proposer boost steps doesn't always come on // slot boundary. if (step.checks.time !== undefined && step.checks.time > 0) - expect(chain.forkChoice.getTime()).toEqualWithMessage( + expect(chain.beaconEngine.forkChoice.getTime()).toEqualWithMessage( Math.floor(bnToNum(step.checks.time) / (config.SLOT_DURATION_MS / 1000)), `Invalid forkchoice time at step ${i}` ); if (step.checks.justified_checkpoint) { - expect(toSpecTestCheckpoint(chain.forkChoice.getJustifiedCheckpoint())).toEqualWithMessage( + expect(toSpecTestCheckpoint(chain.beaconEngine.forkChoice.getJustifiedCheckpoint())).toEqualWithMessage( step.checks.justified_checkpoint, `Invalid justified checkpoint at step ${i}` ); } if (step.checks.finalized_checkpoint) { - expect(toSpecTestCheckpoint(chain.forkChoice.getFinalizedCheckpoint())).toEqualWithMessage( + expect(toSpecTestCheckpoint(chain.beaconEngine.forkChoice.getFinalizedCheckpoint())).toEqualWithMessage( step.checks.finalized_checkpoint, `Invalid finalized checkpoint at step ${i}` ); } if (step.checks.get_proposer_head) { const currentSlot = Math.floor(tickTime / (config.SLOT_DURATION_MS / 1000)); - const {proposerHead, notReorgedReason} = (chain.forkChoice as ForkChoice).getProposerHead( + const {proposerHead, notReorgedReason} = (chain.beaconEngine.forkChoice as ForkChoice).getProposerHead( head, tickTime % (config.SLOT_DURATION_MS / 1000), currentSlot @@ -608,7 +608,7 @@ const forkChoiceTest = } if (step.checks.should_override_forkchoice_update) { const currentSlot = Math.floor(tickTime / (config.SLOT_DURATION_MS / 1000)); - const result = chain.forkChoice.shouldOverrideForkChoiceUpdate( + const result = chain.beaconEngine.forkChoice.shouldOverrideForkChoiceUpdate( head, tickTime % (config.SLOT_DURATION_MS / 1000), currentSlot @@ -623,7 +623,9 @@ const forkChoiceTest = } if (step.checks.payload_timeliness_vote) { expect( - chain.forkChoice.getPayloadTimelinessVotes(step.checks.payload_timeliness_vote.block_root) + chain.beaconEngine.forkChoice.getPayloadTimelinessVotes( + step.checks.payload_timeliness_vote.block_root + ) ).toEqualWithMessage( step.checks.payload_timeliness_vote.votes, `Invalid payload timeliness votes at step ${i}` @@ -631,7 +633,7 @@ const forkChoiceTest = } if (step.checks.payload_data_availability_vote) { expect( - chain.forkChoice.getPayloadDataAvailabilityVotes( + chain.beaconEngine.forkChoice.getPayloadDataAvailabilityVotes( step.checks.payload_data_availability_vote.block_root ) ).toEqualWithMessage( diff --git a/packages/beacon-node/test/spec/utils/gossipValidation.ts b/packages/beacon-node/test/spec/utils/gossipValidation.ts index f75289df6873..ed58e9f89001 100644 --- a/packages/beacon-node/test/spec/utils/gossipValidation.ts +++ b/packages/beacon-node/test/spec/utils/gossipValidation.ts @@ -254,7 +254,7 @@ function setFinalizedCheckpoint(chain: BeaconChain, checkpoint: FinalizedCheckpo rootHex: checkpoint.rootHex, }; - const forkChoice = chain.forkChoice as unknown as { + const forkChoice = chain.beaconEngine.forkChoice as unknown as { fcStore: { finalizedCheckpoint: typeof checkpointWithHex; unrealizedFinalizedCheckpoint: typeof checkpointWithHex; @@ -304,11 +304,11 @@ function computePostState( } function invalidateImportedBlock(chain: BeaconChain, blockRootHex: RootHex, parentRootHex: RootHex): void { - const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentRootHex); + const parentBlock = chain.beaconEngine.forkChoice.getBlockHexDefaultStatus(parentRootHex); if (!parentBlock?.executionPayloadBlockHash) { throw new Error(`Cannot invalidate ${blockRootHex}: parent ${parentRootHex} has no latest valid execution hash`); } - const block = chain.forkChoice.getBlockHexDefaultStatus(blockRootHex); + const block = chain.beaconEngine.forkChoice.getBlockHexDefaultStatus(blockRootHex); if (!block?.executionPayloadBlockHash) { throw new Error(`Cannot invalidate ${blockRootHex}: block has no execution payload hash`); } @@ -328,7 +328,7 @@ function isDescendantAtFinalizedCheckpoint( ): boolean { try { const finalizedSlot = computeStartSlotAtEpoch(checkpoint.epoch); - return chain.forkChoice.getAncestor(blockRootHex, finalizedSlot).blockRoot === checkpoint.rootHex; + return chain.beaconEngine.forkChoice.getAncestor(blockRootHex, finalizedSlot).blockRoot === checkpoint.rootHex; } catch { return false; } 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 74e2b3e71d07..aeafd408088f 100644 --- a/packages/beacon-node/test/unit/chain/blocks/verifyBlocksSanityChecks.test.ts +++ b/packages/beacon-node/test/unit/chain/blocks/verifyBlocksSanityChecks.test.ts @@ -1,10 +1,11 @@ import {beforeEach, describe, expect, it} from "vitest"; import {createChainForkConfig} from "@lodestar/config"; import {config} from "@lodestar/config/default"; -import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; +import {ProtoBlock} from "@lodestar/fork-choice"; import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {SignedBeaconBlock, Slot, ssz} from "@lodestar/types"; import {toHex, toRootHex} from "@lodestar/utils"; +import {IBeaconEngine} from "../../../../src/chain/beaconEngine/index.js"; import {BlockInputPreData} from "../../../../src/chain/blocks/blockInput/blockInput.js"; import {BlockInputSource} from "../../../../src/chain/blocks/blockInput/index.js"; import {verifyBlocksSanityChecks as verifyBlocksImportSanityChecks} from "../../../../src/chain/blocks/verifyBlocksSanityChecks.js"; @@ -16,6 +17,7 @@ import {expectThrowsLodestarError} from "../../../utils/errors.js"; describe("chain / blocks / verifyBlocksSanityChecks", () => { let forkChoice: MockedBeaconChain["forkChoice"]; + let beaconEngine: MockedBeaconChain["beaconEngine"]; let clock: ClockStopped; let modules: Parameters[0]; let block: SignedBeaconBlock; @@ -25,14 +27,17 @@ describe("chain / blocks / verifyBlocksSanityChecks", () => { block = ssz.phase0.SignedBeaconBlock.defaultValue(); block.message.slot = currentSlot; - forkChoice = getMockedBeaconChain().forkChoice; + // The flat mock doubles as the engine; its read methods forward to the mocked forkChoice, so stub via forkChoice. + const chain = getMockedBeaconChain(); + forkChoice = chain.forkChoice; + beaconEngine = chain.beaconEngine; forkChoice.getFinalizedCheckpoint.mockReturnValue({ epoch: 0, root: Buffer.alloc(32), rootHex: "", }); clock = new ClockStopped(currentSlot); - modules = {config, forkChoice, clock, opts: {} as IChainOptions, blacklistedBlocks: new Map()}; + modules = {config, beaconEngine, clock, opts: {} as IChainOptions, blacklistedBlocks: new Map()}; // On first call, parentRoot is known forkChoice.getBlockHexDefaultStatus.mockReturnValue({} as ProtoBlock); }); @@ -105,7 +110,7 @@ describe("chain / blocks / verifyBlocksSanityChecks", () => { // allBlocks[0] = Genesis, not submitted // allBlocks[1] = OK // allBlocks[2] = OK - modules.forkChoice = getForkChoice([blocks[0]]); + modules.beaconEngine = getBeaconEngine([blocks[0]]); clock.setSlot(3); const {relevantBlocks, parentSlots} = verifyBlocksSanityChecks(modules, blocksToProcess, null, { @@ -125,7 +130,7 @@ describe("chain / blocks / verifyBlocksSanityChecks", () => { // allBlocks[1] = ALREADY_KNOWN // allBlocks[2] = OK // allBlocks[3] = OK - modules.forkChoice = getForkChoice([blocks[0], blocks[1]]); + modules.beaconEngine = getBeaconEngine([blocks[0], blocks[1]]); clock.setSlot(4); const {relevantBlocks} = verifyBlocksSanityChecks(modules, blocksToProcess, null, { @@ -145,7 +150,7 @@ describe("chain / blocks / verifyBlocksSanityChecks", () => { // allBlocks[1] = WOULD_REVERT_FINALIZED_SLOT + ALREADY_KNOWN // allBlocks[2] = OK // allBlocks[3] = OK - modules.forkChoice = getForkChoice([blocks[0], blocks[1]], finalizedEpoch); + modules.beaconEngine = getBeaconEngine([blocks[0], blocks[1]], finalizedEpoch); clock.setSlot(finalizedSlot + 4); const {relevantBlocks} = verifyBlocksSanityChecks(modules, blocksToProcess, null, { @@ -209,7 +214,7 @@ function getValidChain(count: number, initialSlot = 0): SignedBeaconBlock[] { return blocks; } -function getForkChoice(knownBlocks: SignedBeaconBlock[], finalizedEpoch = 0): IForkChoice { +function getBeaconEngine(knownBlocks: SignedBeaconBlock[], finalizedEpoch = 0): IBeaconEngine { const blocks = new Map(); for (const block of knownBlocks) { const protoBlock = toProtoBlock(block); @@ -226,7 +231,7 @@ function getForkChoice(knownBlocks: SignedBeaconBlock[], finalizedEpoch = 0): IF getFinalizedCheckpoint() { return {epoch: finalizedEpoch, root: Buffer.alloc(32), rootHex: ""}; }, - } as Partial as IForkChoice; + } as Partial as IBeaconEngine; } function toProtoBlock(block: SignedBeaconBlock): ProtoBlock { diff --git a/packages/beacon-node/test/unit/chain/seenCache/seenPayloadEnvelopeInput.test.ts b/packages/beacon-node/test/unit/chain/seenCache/seenPayloadEnvelopeInput.test.ts index 3623a936ace9..98e2910f4957 100644 --- a/packages/beacon-node/test/unit/chain/seenCache/seenPayloadEnvelopeInput.test.ts +++ b/packages/beacon-node/test/unit/chain/seenCache/seenPayloadEnvelopeInput.test.ts @@ -1,9 +1,11 @@ import {beforeEach, describe, expect, it, vi} from "vitest"; -import {ExecutionStatus, IForkChoice, PayloadStatus, ProtoBlock} from "@lodestar/fork-choice"; +import {ExecutionStatus, PayloadStatus, ProtoBlock} from "@lodestar/fork-choice"; import {testLogger} from "@lodestar/logger/test-utils"; import {ForkName} from "@lodestar/params"; import {DataAvailabilityStatus} from "@lodestar/state-transition"; import {RootHex} from "@lodestar/types"; +import {fromHex} from "@lodestar/utils"; +import {IBeaconEngine} from "../../../../src/chain/beaconEngine/index.js"; import {ChainEventEmitter} from "../../../../src/chain/emitter.js"; import {SeenPayloadEnvelopeInput} from "../../../../src/chain/seenCache/seenPayloadEnvelopeInput.js"; import {SerializedCache} from "../../../../src/util/serializedCache.js"; @@ -14,21 +16,21 @@ describe("SeenPayloadEnvelopeInput", () => { let cache: SeenPayloadEnvelopeInput; let abortController: AbortController; let chainEvents: ChainEventEmitter; - let forkChoice: IForkChoice; + let beaconEngine: IBeaconEngine; let serializedCache: SerializedCache; beforeEach(() => { chainEvents = new ChainEventEmitter(); abortController = new AbortController(); - forkChoice = { - getAllAncestorBlocks: vi.fn(), - } as unknown as IForkChoice; + beaconEngine = { + getAllAncestorBlockRootSlots: vi.fn(), + } as unknown as IBeaconEngine; serializedCache = new SerializedCache(); cache = new SeenPayloadEnvelopeInput({ config, clock: getMockedClock(), - forkChoice, + beaconEngine, chainEvents, signal: abortController.signal, serializedCache, @@ -79,7 +81,10 @@ describe("SeenPayloadEnvelopeInput", () => { const newRootHex = addPayloadInput(2); const parentBlock = protoBlock(newRootHex, 2); - vi.mocked(forkChoice.getAllAncestorBlocks).mockReturnValue([parentBlock, protoBlock(oldRootHex, 1)]); + vi.mocked(beaconEngine.getAllAncestorBlockRootSlots).mockReturnValue([ + {slot: 2, root: fromHex(newRootHex)}, + {slot: 1, root: fromHex(oldRootHex)}, + ]); cache.pruneBelowParent(parentBlock); expect(cache.get(oldRootHex)).toBeUndefined(); @@ -90,7 +95,7 @@ describe("SeenPayloadEnvelopeInput", () => { const rootHex = addPayloadInput(1); const parentBlock = protoBlock(rootHex, 1); - vi.mocked(forkChoice.getAllAncestorBlocks).mockReturnValue([parentBlock]); + vi.mocked(beaconEngine.getAllAncestorBlockRootSlots).mockReturnValue([{slot: 1, root: fromHex(rootHex)}]); cache.pruneBelowParent(parentBlock); expect(cache.get(rootHex)).toBeDefined(); diff --git a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts index 6b4fe73981e0..b35a7cb3510a 100644 --- a/packages/beacon-node/test/unit/sync/unknownBlock.test.ts +++ b/packages/beacon-node/test/unit/sync/unknownBlock.test.ts @@ -422,7 +422,6 @@ describe("sync by UnknownBlockSync", {timeout: 20_000}, () => { const chain: Partial = { emitter, clock: new ClockStopped(0), - forkChoice: forkChoice as IForkChoice, genesisTime: 0, custodyConfig: {sampledColumns: [], custodyColumns: []} as unknown as CustodyConfig, processBlock: async (blockInput, opts) => { @@ -454,6 +453,7 @@ describe("sync by UnknownBlockSync", {timeout: 20_000}, () => { if (blockRootHex === blockRootHexA) blockAResolver(); }, beaconEngine: { + ...forkChoice, isBlockProposerSeen: (blockSlot: number, index: number) => seenBlockProposers.isKnown(blockSlot, index), } as unknown as IBeaconChain["beaconEngine"], seenBlockInputCache: { @@ -663,6 +663,9 @@ describe("UnknownBlockSync", () => { const networkEvents = new NetworkEventBus(); const peersById = new Map(peers.map((peer) => [peer.peerId, peer])); + // Merge the beaconEngine override into the base engine methods so per-test overrides (forkChoice + // reads) don't clobber the base envelope-validation / seen-block-proposers stubs. + const {beaconEngine: beaconEngineOverride, ...restOverrides} = chainOverrides ?? {}; const chain = { emitter, clock: new ClockStopped(0), @@ -682,18 +685,17 @@ describe("UnknownBlockSync", () => { prune: vi.fn(), } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], seenBlockInputCache: {prune: vi.fn()} as unknown as SeenBlockInput, - forkChoice: { + ...restOverrides, + // Envelope validation + seen-block-proposers + forkChoice reads are now BeaconEngine methods; route + // them to the mocked module fn / stubs so the existing assertions still observe the calls. + beaconEngine: { hasPayloadHexUnsafe: vi.fn().mockReturnValue(false), hasBlockHex: vi.fn().mockReturnValue(false), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), - } as unknown as IForkChoice, - // Envelope validation + seen-block-proposers are now BeaconEngine methods; route them to the - // mocked module fn / a stub so the existing assertions still observe the calls. - beaconEngine: { validateGossipExecutionPayloadEnvelope, isBlockProposerSeen: vi.fn().mockReturnValue(false), + ...beaconEngineOverride, } as unknown as IBeaconChain["beaconEngine"], - ...chainOverrides, } as IBeaconChain; const network = { @@ -760,11 +762,11 @@ describe("UnknownBlockSync", () => { get: vi.fn().mockImplementation((root: string) => (root === blockRootHex ? payloadInput : undefined)), prune: vi.fn(), } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], - forkChoice: { + beaconEngine: { hasPayloadHexUnsafe: vi.fn().mockReturnValue(false), hasBlockHex: vi.fn().mockImplementation((root: string) => root === blockRootHex), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), - } as unknown as IForkChoice, + } as unknown as IBeaconChain["beaconEngine"], }, custodyConfig: {sampledColumns: [0], sampleGroups: [[0]]} as unknown as CustodyConfig, networkOverrides: { @@ -831,11 +833,11 @@ describe("UnknownBlockSync", () => { get: vi.fn().mockImplementation((root: string) => (root === blockRootHex ? payloadInput : undefined)), prune: vi.fn(), } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], - forkChoice: { + beaconEngine: { hasPayloadHexUnsafe: vi.fn().mockReturnValue(false), hasBlockHex: vi.fn().mockImplementation((root: string) => root === blockRootHex), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), - } as unknown as IForkChoice, + } as unknown as IBeaconChain["beaconEngine"], }, custodyConfig: {sampledColumns: [0, 1], sampleGroups: [[0], [1]]} as unknown as CustodyConfig, networkOverrides: { @@ -917,7 +919,7 @@ describe("UnknownBlockSync", () => { }), prune: vi.fn(), } as unknown as SeenBlockInput, - forkChoice: { + beaconEngine: { hasPayloadHexUnsafe: vi.fn().mockReturnValue(false), hasBlockHex: vi.fn().mockImplementation((root: string) => knownRoots.has(root)), getBlockHexAndBlockHash: vi @@ -929,7 +931,7 @@ describe("UnknownBlockSync", () => { : null ), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), - } as unknown as IForkChoice, + } as unknown as IBeaconChain["beaconEngine"], }, networkOverrides: { sendExecutionPayloadEnvelopesByRoot, @@ -1019,7 +1021,7 @@ describe("UnknownBlockSync", () => { }), prune: vi.fn(), } as unknown as SeenBlockInput, - forkChoice: { + beaconEngine: { hasPayloadHexUnsafe: vi.fn().mockReturnValue(false), hasBlockHex: vi.fn().mockImplementation((root: string) => knownRoots.has(root)), getBlockHexAndBlockHash: vi @@ -1031,7 +1033,7 @@ describe("UnknownBlockSync", () => { : null ), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), - } as unknown as IForkChoice, + } as unknown as IBeaconChain["beaconEngine"], }, networkOverrides: { sendExecutionPayloadEnvelopesByRoot, @@ -1114,7 +1116,7 @@ describe("UnknownBlockSync", () => { }), prune: vi.fn(), } as unknown as SeenBlockInput, - forkChoice: { + beaconEngine: { hasPayloadHexUnsafe: vi.fn().mockReturnValue(false), hasBlockHex: vi.fn().mockImplementation((root: string) => knownRoots.has(root)), getBlockHexAndBlockHash: vi @@ -1126,7 +1128,7 @@ describe("UnknownBlockSync", () => { : null ), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), - } as unknown as IForkChoice, + } as unknown as IBeaconChain["beaconEngine"], }, networkOverrides: { sendExecutionPayloadEnvelopesByRoot, @@ -1189,11 +1191,11 @@ describe("UnknownBlockSync", () => { get: vi.fn().mockImplementation((root: string) => (root === blockRootHex ? payloadInput : undefined)), prune: vi.fn(), } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], - forkChoice: { + beaconEngine: { hasPayloadHexUnsafe: vi.fn().mockReturnValue(false), hasBlockHex: vi.fn().mockImplementation((root: string) => root === blockRootHex), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), - } as unknown as IForkChoice, + } as unknown as IBeaconChain["beaconEngine"], }, networkOverrides: {sendExecutionPayloadEnvelopesByRoot}, peers: [{peerId: peer}], @@ -1282,11 +1284,11 @@ describe("UnknownBlockSync", () => { get: vi.fn().mockReturnValue(payloadInput), prune: vi.fn(), } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], - forkChoice: { + beaconEngine: { hasPayloadHexUnsafe: vi.fn().mockReturnValue(false), hasBlockHex: vi.fn().mockImplementation((root: string) => root === payloadInput.blockRootHex), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), - } as unknown as IForkChoice, + } as unknown as IBeaconChain["beaconEngine"], }, }); @@ -1346,7 +1348,7 @@ describe("UnknownBlockSync", () => { get: vi.fn().mockImplementation((root: string) => (root === parentRootHex ? payloadInput : undefined)), prune: vi.fn(), } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], - forkChoice: { + beaconEngine: { hasPayloadHexUnsafe: vi .fn() .mockImplementation((root: string) => root === parentRootHex && hasParentPayload), @@ -1362,7 +1364,7 @@ describe("UnknownBlockSync", () => { : null ), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), - } as unknown as IForkChoice, + } as unknown as IBeaconChain["beaconEngine"], }, networkOverrides: { sendExecutionPayloadEnvelopesByRoot, @@ -1438,12 +1440,12 @@ describe("UnknownBlockSync", () => { prune: seenPayloadPrune, } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], seenBlockInputCache: {prune: seenBlockInputPrune} as unknown as SeenBlockInput, - forkChoice: { + beaconEngine: { hasPayloadHexUnsafe: vi.fn().mockReturnValue(false), hasBlockHex: vi.fn().mockImplementation((root: string) => root === parentRootHex), getBlockHexAndBlockHash: vi.fn().mockReturnValue(null), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), - } as unknown as IForkChoice, + } as unknown as IBeaconChain["beaconEngine"], }, networkOverrides: {sendExecutionPayloadEnvelopesByRoot}, peers: [{peerId: peer}], @@ -1506,7 +1508,7 @@ describe("UnknownBlockSync", () => { get: vi.fn().mockImplementation((root: string) => (root === parentRootHex ? payloadInput : undefined)), prune: seenPayloadPrune, } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], - forkChoice: { + beaconEngine: { hasPayloadHexUnsafe: vi.fn().mockReturnValue(false), hasBlockHex: vi.fn().mockImplementation((root: string) => root === parentRootHex), getBlockHexDefaultStatus: vi @@ -1514,7 +1516,7 @@ describe("UnknownBlockSync", () => { .mockImplementation((root: string) => (root === parentRootHex ? ({slot: 1} as ProtoBlock) : null)), getBlockHexAndBlockHash: vi.fn().mockReturnValue(null), getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), - } as unknown as IForkChoice, + } as unknown as IBeaconChain["beaconEngine"], }, networkOverrides: {sendExecutionPayloadEnvelopesByRoot}, peers: [{peerId: peer}], @@ -1591,7 +1593,13 @@ describe("UnknownBlockSync", () => { genesisTime: 0, metrics: null, processBlock, - forkChoice: { + seenPayloadEnvelopeInputCache: { + add: vi.fn(), + get: vi.fn(), + prune: vi.fn(), + } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], + seenBlockInputCache: {prune: vi.fn()} as unknown as SeenBlockInput, + beaconEngine: { getFinalizedBlock: vi.fn().mockReturnValue({slot: 0} as ProtoBlock), hasBlockHex: vi.fn().mockImplementation((rootHex: string) => rootHex === parentRootHex), getBlockHexAndBlockHash: vi @@ -1600,14 +1608,6 @@ describe("UnknownBlockSync", () => { rootHex === parentRootHex && blockHashHex === parentBlockHashHex ? ({} as ProtoBlock) : null ), hasPayloadHexUnsafe: vi.fn().mockReturnValue(false), - } as unknown as IForkChoice, - seenPayloadEnvelopeInputCache: { - add: vi.fn(), - get: vi.fn(), - prune: vi.fn(), - } as unknown as IBeaconChain["seenPayloadEnvelopeInputCache"], - seenBlockInputCache: {prune: vi.fn()} as unknown as SeenBlockInput, - beaconEngine: { isBlockProposerSeen: vi.fn().mockReturnValue(false), } as unknown as IBeaconChain["beaconEngine"], }; diff --git a/packages/beacon-node/test/unit/sync/utils/remoteSyncType.test.ts b/packages/beacon-node/test/unit/sync/utils/remoteSyncType.test.ts index d8c6749ff62f..a147e254f7ac 100644 --- a/packages/beacon-node/test/unit/sync/utils/remoteSyncType.test.ts +++ b/packages/beacon-node/test/unit/sync/utils/remoteSyncType.test.ts @@ -1,7 +1,7 @@ import {describe, expect, it} from "vitest"; import {toHexString} from "@chainsafe/ssz"; -import {IForkChoice} from "@lodestar/fork-choice"; import {Root, phase0} from "@lodestar/types"; +import {IBeaconEngine} from "../../../../src/chain/beaconEngine/index.js"; import {ZERO_HASH} from "../../../../src/constants/index.js"; import { PeerSyncType, @@ -72,8 +72,8 @@ describe("network / peers / remoteSyncType", () => { it(id, () => { const local = {...status, ...localPartial}; const remote = {...status, ...remotePartial}; - const forkChoice = getMockForkChoice(blocks || []); - expect(getPeerSyncType(local, remote, forkChoice, slotImportTolerance)).toBe(syncType); + const beaconEngine = getMockBeaconEngine(blocks || []); + expect(getPeerSyncType(local, remote, beaconEngine, slotImportTolerance)).toBe(syncType); }); } }); @@ -117,16 +117,16 @@ describe("network / peers / remoteSyncType", () => { it(id, () => { const local = {...status, ...localPartial}; const remote = {...status, ...remotePartial}; - const forkChoice = getMockForkChoice(blocks || []); - expect(getRangeSyncType(local, remote, forkChoice)).toBe(syncType); + const beaconEngine = getMockBeaconEngine(blocks || []); + expect(getRangeSyncType(local, remote, beaconEngine)).toBe(syncType); }); } }); }); -function getMockForkChoice(blocks: Root[]): IForkChoice { +function getMockBeaconEngine(blocks: Root[]): IBeaconEngine { const blockSet = new Set(blocks.map((blockRoot) => toHexString(blockRoot))); return { hasBlock: (blockRoot: Root) => blockSet.has(toHexString(blockRoot)), - } as IForkChoice; + } as IBeaconEngine; } From 14f38eda8297e2915d53177d2245247e73bb93da Mon Sep 17 00:00:00 2001 From: twoeths Date: Sun, 12 Jul 2026 13:29:04 +0700 Subject: [PATCH 24/24] feat: move regen and getState*() api to BeaconEngine --- .../src/api/impl/beacon/blocks/index.ts | 19 +- .../src/api/impl/beacon/rewards/index.ts | 9 +- .../src/api/impl/beacon/state/index.ts | 408 ++------- .../src/api/impl/beacon/state/utils.ts | 127 +-- .../beacon-node/src/api/impl/debug/index.ts | 21 +- .../src/api/impl/lodestar/index.ts | 69 +- .../beacon-node/src/api/impl/proof/index.ts | 15 +- .../src/api/impl/validator/index.ts | 58 +- .../src/chain/archiveStore/archiveStore.ts | 44 - .../src/chain/archiveStore/interface.ts | 14 - .../src/chain/beaconEngine/beaconEngine.ts | 833 +++++++++++++++++- .../src/chain/beaconEngine/interface.ts | 137 ++- .../src/chain/beaconEngine/options.ts | 4 + .../beacon-node/src/chain/blocks/index.ts | 1 + packages/beacon-node/src/chain/chain.ts | 272 +----- packages/beacon-node/src/chain/interface.ts | 41 - packages/beacon-node/src/network/network.ts | 2 +- packages/beacon-node/src/node/nodejs.ts | 3 +- packages/beacon-node/src/node/notifier.ts | 26 +- packages/beacon-node/src/sync/range/range.ts | 7 +- .../e2e/api/impl/lightclient/endpoint.test.ts | 3 +- .../e2e/doppelganger/doppelganger.test.ts | 5 +- .../test/e2e/sync/checkpointSync.test.ts | 15 +- .../test/mocks/mockedBeaconChain.ts | 6 +- .../test/sim/electra-interop.test.ts | 5 +- .../spec/presets/fast_confirmation.test.ts | 2 +- .../test/spec/presets/fork_choice.test.ts | 6 +- .../test/spec/utils/gossipValidation.ts | 2 +- .../unit/api/impl/beacon/state/utils.test.ts | 2 +- .../chain/validation/attesterSlashing.test.ts | 2 +- .../validation/blsToExecutionChange.test.ts | 2 +- .../chain/validation/proposerSlashing.test.ts | 2 +- .../chain/validation/voluntaryExit.test.ts | 8 +- .../beacon-node/test/utils/node/simTest.ts | 8 +- 34 files changed, 1255 insertions(+), 923 deletions(-) diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index ca0b461ae869..6979c4246d3d 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -634,14 +634,17 @@ export function getBeaconBlockApi({ } if (slot < head.slot && head.slot <= slot + SLOTS_PER_HISTORICAL_ROOT) { - const state = chain.getHeadState(); - return { - data: {root: state.getBlockRootAtSlot(slot)}, - meta: { - executionOptimistic: isOptimisticBlock(head), - finalized: computeEpochAtSlot(slot) <= chain.beaconEngine.getFinalizedCheckpoint().epoch, - }, - }; + // Canonical block root ≤ slot (matches the former state.getBlockRootAtSlot; handles skipped slots). + const canonical = chain.beaconEngine.getCanonicalBlockClosestLteSlot(slot); + if (canonical) { + return { + data: {root: fromHex(canonical.blockRoot)}, + meta: { + executionOptimistic: isOptimisticBlock(head), + finalized: computeEpochAtSlot(slot) <= chain.beaconEngine.getFinalizedCheckpoint().epoch, + }, + }; + } } } else if (blockId === "head") { const head = chain.beaconEngine.getHead(); diff --git a/packages/beacon-node/src/api/impl/beacon/rewards/index.ts b/packages/beacon-node/src/api/impl/beacon/rewards/index.ts index 11b98f1f1f1b..1baed0d8fe4c 100644 --- a/packages/beacon-node/src/api/impl/beacon/rewards/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/rewards/index.ts @@ -10,20 +10,23 @@ export function getBeaconRewardsApi({ return { async getBlockRewards({blockId}) { const {block, executionOptimistic, finalized} = await getBlockResponse(chain, blockId); - const data = await chain.getBlockRewards(block.message); + const data = await chain.beaconEngine.getBlockRewards(block.message); return {data, meta: {executionOptimistic, finalized}}; }, async getAttestationsRewards({epoch, validatorIds}) { assertUniqueItems(validatorIds, "Duplicate validator IDs provided"); - const {rewards, executionOptimistic, finalized} = await chain.getAttestationsRewards(epoch, validatorIds); + const {rewards, executionOptimistic, finalized} = await chain.beaconEngine.getAttestationsRewards( + epoch, + validatorIds + ); return {data: rewards, meta: {executionOptimistic, finalized}}; }, async getSyncCommitteeRewards({blockId, validatorIds}) { assertUniqueItems(validatorIds, "Duplicate validator IDs provided"); const {block, executionOptimistic, finalized} = await getBlockResponse(chain, blockId); - const data = await chain.getSyncCommitteeRewards(block.message, validatorIds); + const data = await chain.beaconEngine.getSyncCommitteeRewards(block.message, validatorIds); return {data, meta: {executionOptimistic, finalized}}; }, }; diff --git a/packages/beacon-node/src/api/impl/beacon/state/index.ts b/packages/beacon-node/src/api/impl/beacon/state/index.ts index 2bd60136f711..ceb5b09e26f5 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/index.ts @@ -1,143 +1,62 @@ import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; -import {EPOCHS_PER_HISTORICAL_VECTOR, SLOTS_PER_EPOCH, SYNC_COMMITTEE_SUBNET_SIZE} from "@lodestar/params"; -import { - IBeaconStateView, - computeEpochAtSlot, - computeStartSlotAtEpoch, - getCurrentEpoch, - isStatePostAltair, - isStatePostElectra, - isStatePostFulu, -} from "@lodestar/state-transition"; -import {ValidatorIndex, getValidatorStatus, ssz} from "@lodestar/types"; -import {ApiError} from "../../errors.js"; import {ApiModules} from "../../types.js"; import {assertUniqueItems} from "../../utils.js"; -import { - filterStateValidatorsByStatus, - getStateResponseWithRegen, - getStateValidatorIndex, - toValidatorResponse, -} from "./utils.js"; +import {resolveStateId, unwrapStateResult, unwrapStateResultWithFork} from "./utils.js"; export function getBeaconStateApi({ chain, - config, }: Pick): ApplicationMethods { - async function getState( - stateId: routes.beacon.StateId - ): Promise<{state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean}> { - // TODO - beacon engine: this api will be dropped for BeaconEngine - const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, stateId); - - return { - state: state instanceof Uint8Array ? chain.getHeadState().loadOtherState(state) : state, - executionOptimistic, - finalized, - }; - } - return { async getStateRoot({stateId}) { - const {state, executionOptimistic, finalized} = await getState(stateId); - return { - data: {root: state.hashTreeRoot()}, - meta: {executionOptimistic, finalized}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized} = unwrapStateResult( + await chain.beaconEngine.getStateRoot(id), + stateId + ); + return {data, meta: {executionOptimistic, finalized}}; }, async getStateFork({stateId}) { - const {state, executionOptimistic, finalized} = await getState(stateId); - return { - data: state.fork, - meta: {executionOptimistic, finalized}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized} = unwrapStateResult( + await chain.beaconEngine.getStateFork(id), + stateId + ); + return {data, meta: {executionOptimistic, finalized}}; }, async getStateRandao({stateId, epoch}) { - const {state, executionOptimistic, finalized} = await getState(stateId); - const stateEpoch = computeEpochAtSlot(state.slot); - const usedEpoch = epoch ?? stateEpoch; - - if (!(stateEpoch < usedEpoch + EPOCHS_PER_HISTORICAL_VECTOR && usedEpoch <= stateEpoch)) { - throw new ApiError(400, "Requested epoch is out of range"); - } - - const randao = state.getRandaoMix(usedEpoch); - - return { - data: {randao}, - meta: {executionOptimistic, finalized}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized} = unwrapStateResult( + await chain.beaconEngine.getStateRandao(id, epoch), + stateId + ); + return {data, meta: {executionOptimistic, finalized}}; }, async getStateFinalityCheckpoints({stateId}) { - const {state, executionOptimistic, finalized} = await getState(stateId); - return { - data: { - currentJustified: state.currentJustifiedCheckpoint, - previousJustified: state.previousJustifiedCheckpoint, - finalized: state.finalizedCheckpoint, - }, - meta: {executionOptimistic, finalized}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized} = unwrapStateResult( + await chain.beaconEngine.getStateFinalityCheckpoints(id), + stateId + ); + return {data, meta: {executionOptimistic, finalized}}; }, async getStateValidators({stateId, validatorIds = [], statuses = []}) { - const {state, executionOptimistic, finalized} = await getState(stateId); - const currentEpoch = getCurrentEpoch(state); - const {pubkeyCache} = chain; - - const validatorResponses: routes.beacon.ValidatorResponse[] = []; if (validatorIds.length) { assertUniqueItems(validatorIds, "Duplicate validator IDs provided"); - - for (const id of validatorIds) { - const resp = getStateValidatorIndex(id, state, pubkeyCache); - if (resp.valid) { - const validatorIndex = resp.validatorIndex; - const validator = state.getValidator(validatorIndex); - if (statuses.length && !statuses.includes(getValidatorStatus(validator, currentEpoch))) { - continue; - } - const validatorResponse = toValidatorResponse( - validatorIndex, - validator, - state.getBalance(validatorIndex), - currentEpoch - ); - validatorResponses.push(validatorResponse); - } - } - return { - data: validatorResponses, - meta: {executionOptimistic, finalized}, - }; } - if (statuses.length) { assertUniqueItems(statuses, "Duplicate statuses provided"); - - const validatorsByStatus = filterStateValidatorsByStatus(statuses, state, pubkeyCache, currentEpoch); - return { - data: validatorsByStatus, - meta: {executionOptimistic, finalized}, - }; } - - // TODO: This loops over the entire state, it's a DOS vector - const validatorsArr = state.getAllValidators(); - const balancesArr = state.getAllBalances(); - const resp: routes.beacon.ValidatorResponse[] = []; - for (let i = 0; i < validatorsArr.length; i++) { - resp.push(toValidatorResponse(i, validatorsArr[i], balancesArr[i], currentEpoch)); - } - - return { - data: resp, - meta: {executionOptimistic, finalized}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized} = unwrapStateResult( + await chain.beaconEngine.getStateValidators(id, validatorIds, statuses), + stateId + ); + return {data, meta: {executionOptimistic, finalized}}; }, async postStateValidators(args, context) { @@ -145,92 +64,36 @@ export function getBeaconStateApi({ }, async postStateValidatorIdentities({stateId, validatorIds = []}) { - const {state, executionOptimistic, finalized} = await getState(stateId); - const {pubkeyCache} = chain; - - let validatorIdentities: routes.beacon.ValidatorIdentities; - if (validatorIds.length) { assertUniqueItems(validatorIds, "Duplicate validator IDs provided"); - - validatorIdentities = []; - for (const id of validatorIds) { - const resp = getStateValidatorIndex(id, state, pubkeyCache); - if (resp.valid) { - const index = resp.validatorIndex; - const {pubkey, activationEpoch} = state.getValidator(index); - validatorIdentities.push({index, pubkey, activationEpoch}); - } - } - } else { - const validatorsArr = state.getAllValidators(); - validatorIdentities = new Array(validatorsArr.length) as routes.beacon.ValidatorIdentities; - for (let i = 0; i < validatorsArr.length; i++) { - const {pubkey, activationEpoch} = validatorsArr[i]; - validatorIdentities[i] = {index: i, pubkey, activationEpoch}; - } } - - return { - data: validatorIdentities, - meta: {executionOptimistic, finalized}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized} = unwrapStateResult( + await chain.beaconEngine.getStateValidatorIdentities(id, validatorIds), + stateId + ); + return {data, meta: {executionOptimistic, finalized}}; }, async getStateValidator({stateId, validatorId}) { - const {state, executionOptimistic, finalized} = await getState(stateId); - const {pubkeyCache} = chain; - - const resp = getStateValidatorIndex(validatorId, state, pubkeyCache); - if (!resp.valid) { - throw new ApiError(resp.code, resp.reason); - } - - const validatorIndex = resp.validatorIndex; - return { - data: toValidatorResponse( - validatorIndex, - state.getValidator(validatorIndex), - state.getBalance(validatorIndex), - getCurrentEpoch(state) - ), - meta: {executionOptimistic, finalized}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized} = unwrapStateResult( + await chain.beaconEngine.getStateValidator(id, validatorId), + stateId + ); + return {data, meta: {executionOptimistic, finalized}}; }, async getStateValidatorBalances({stateId, validatorIds = []}) { - const {state, executionOptimistic, finalized} = await getState(stateId); - if (validatorIds.length) { assertUniqueItems(validatorIds, "Duplicate validator IDs provided"); - - const balances: routes.beacon.ValidatorBalance[] = []; - for (const id of validatorIds) { - const resp = getStateValidatorIndex(id, state, chain.pubkeyCache); - - if (resp.valid) { - balances.push({ - index: resp.validatorIndex, - balance: state.getBalance(resp.validatorIndex), - }); - } - } - return { - data: balances, - meta: {executionOptimistic, finalized}, - }; - } - - // TODO: This loops over the entire state, it's a DOS vector - const balancesArr = state.getAllBalances(); - const resp: routes.beacon.ValidatorBalance[] = []; - for (let i = 0; i < balancesArr.length; i++) { - resp.push({index: i, balance: balancesArr[i]}); } - return { - data: resp, - meta: {executionOptimistic, finalized}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized} = unwrapStateResult( + await chain.beaconEngine.getStateValidatorBalances(id, validatorIds), + stateId + ); + return {data, meta: {executionOptimistic, finalized}}; }, async postStateValidatorBalances(args, context) { @@ -238,158 +101,57 @@ export function getBeaconStateApi({ }, async getEpochCommittees({stateId, ...filters}) { - const {state, executionOptimistic, finalized} = await getState(stateId); - - const stateEpoch = computeEpochAtSlot(state.slot); - const epoch = filters.epoch ?? stateEpoch; - const startSlot = computeStartSlotAtEpoch(epoch); - const endSlot = startSlot + SLOTS_PER_EPOCH - 1; - - if (Math.abs(epoch - stateEpoch) > 1) { - throw new ApiError(400, `Epoch ${epoch} must be within one epoch of state epoch ${stateEpoch}`); - } - - if (filters.slot !== undefined && (filters.slot < startSlot || filters.slot > endSlot)) { - throw new ApiError(400, `Slot ${filters.slot} is not in epoch ${epoch}`); - } - - const decisionRoot = state.getShufflingDecisionRoot(epoch); - // TODO - engine: should not access shufflingCache, directly. It belongs to BeaconEngine - const shuffling = await chain.beaconEngine.shufflingCache.get(epoch, decisionRoot); - if (!shuffling) { - throw new ApiError( - 500, - `No shuffling found to calculate committees for epoch: ${epoch} and decisionRoot: ${decisionRoot}` - ); - } - const committees = shuffling.committees; - const committeesFlat = committees.flatMap((slotCommittees, slotInEpoch) => { - const slot = startSlot + slotInEpoch; - if (filters.slot !== undefined && filters.slot !== slot) { - return []; - } - return slotCommittees.flatMap((committee, committeeIndex) => { - if (filters.index !== undefined && filters.index !== committeeIndex) { - return []; - } - return [ - { - index: committeeIndex, - slot, - validators: Array.from(committee), - }, - ]; - }); - }); - - return { - data: committeesFlat, - meta: {executionOptimistic, finalized}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized} = unwrapStateResult( + await chain.beaconEngine.getEpochCommittees(id, filters), + stateId + ); + return {data, meta: {executionOptimistic, finalized}}; }, - /** - * Retrieves the sync committees for the given state. - * @param epoch Fetch sync committees for the given epoch. If not present then the sync committees for the epoch of the state will be obtained. - */ async getEpochSyncCommittees({stateId, epoch}) { - // TODO: Should pick a state with the provided epoch too - const {state, executionOptimistic, finalized} = await getState(stateId); - - // TODO: If possible compute the syncCommittees in advance of the fork and expose them here. - // So the validators can prepare and potentially attest the first block. Not critical tho, it's very unlikely - const stateEpoch = computeEpochAtSlot(state.slot); - if (stateEpoch < config.ALTAIR_FORK_EPOCH) { - throw new ApiError(400, "Requested state before ALTAIR_FORK_EPOCH"); - } - if (!isStatePostAltair(state)) { - throw new Error("Expected Altair state for sync committee lookup"); - } - - const syncCommitteeCache = state.getIndexedSyncCommitteeAtEpoch(epoch ?? stateEpoch); - const validatorIndices = new Array(...syncCommitteeCache.validatorIndices); - - // Subcommittee assignments of the current sync committee - const validatorAggregates: ValidatorIndex[][] = []; - for (let i = 0; i < validatorIndices.length; i += SYNC_COMMITTEE_SUBNET_SIZE) { - validatorAggregates.push(validatorIndices.slice(i, i + SYNC_COMMITTEE_SUBNET_SIZE)); - } - - return { - data: { - validators: validatorIndices, - validatorAggregates, - }, - meta: {executionOptimistic, finalized}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized} = unwrapStateResult( + await chain.beaconEngine.getEpochSyncCommittees(id, epoch), + stateId + ); + return {data, meta: {executionOptimistic, finalized}}; }, async getPendingDeposits({stateId}, context) { - const {state, executionOptimistic, finalized} = await getState(stateId); - const fork = state.forkName; - - if (!isStatePostElectra(state)) { - throw new ApiError(400, `Cannot retrieve pending deposits for pre-electra state fork=${fork}`); - } - - const pendingDeposits = state.pendingDeposits; - - return { - data: context?.returnBytes ? ssz.electra.PendingDeposits.serialize(pendingDeposits) : pendingDeposits, - meta: {executionOptimistic, finalized, version: fork}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized, fork} = unwrapStateResultWithFork( + await chain.beaconEngine.getStatePendingDeposits(id, Boolean(context?.returnBytes)), + stateId + ); + return {data, meta: {executionOptimistic, finalized, version: fork}}; }, async getPendingPartialWithdrawals({stateId}, context) { - const {state, executionOptimistic, finalized} = await getState(stateId); - const fork = state.forkName; - - if (!isStatePostElectra(state)) { - throw new ApiError(400, `Cannot retrieve pending partial withdrawals for pre-electra state fork=${fork}`); - } - - const pendingPartialWithdrawals = state.pendingPartialWithdrawals; - - return { - data: context?.returnBytes - ? ssz.electra.PendingPartialWithdrawals.serialize(pendingPartialWithdrawals) - : pendingPartialWithdrawals, - meta: {executionOptimistic, finalized, version: fork}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized, fork} = unwrapStateResultWithFork( + await chain.beaconEngine.getStatePendingPartialWithdrawals(id, Boolean(context?.returnBytes)), + stateId + ); + return {data, meta: {executionOptimistic, finalized, version: fork}}; }, async getPendingConsolidations({stateId}, context) { - const {state, executionOptimistic, finalized} = await getState(stateId); - const fork = state.forkName; - - if (!isStatePostElectra(state)) { - throw new ApiError(400, `Cannot retrieve pending consolidations for pre-electra state fork=${fork}`); - } - - const pendingConsolidations = state.pendingConsolidations; - - return { - data: context?.returnBytes - ? ssz.electra.PendingConsolidations.serialize(pendingConsolidations) - : pendingConsolidations, - meta: {executionOptimistic, finalized, version: fork}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized, fork} = unwrapStateResultWithFork( + await chain.beaconEngine.getStatePendingConsolidations(id, Boolean(context?.returnBytes)), + stateId + ); + return {data, meta: {executionOptimistic, finalized, version: fork}}; }, async getProposerLookahead({stateId}, context) { - const {state, executionOptimistic, finalized} = await getState(stateId); - const fork = state.forkName; - - if (!isStatePostFulu(state)) { - throw new ApiError(400, `Cannot retrieve proposer lookahead for pre-fulu state fork=${fork}`); - } - - const proposerLookahead = state.proposerLookahead; - - return { - data: context?.returnBytes ? ssz.fulu.ProposerLookahead.serialize(proposerLookahead) : proposerLookahead, - meta: {executionOptimistic, finalized, version: fork}, - }; + const id = resolveStateId(chain.beaconEngine, stateId); + const {data, executionOptimistic, finalized, fork} = unwrapStateResultWithFork( + await chain.beaconEngine.getStateProposerLookahead(id, Boolean(context?.returnBytes)), + stateId + ); + return {data, meta: {executionOptimistic, finalized, version: fork}}; }, }; } diff --git a/packages/beacon-node/src/api/impl/beacon/state/utils.ts b/packages/beacon-node/src/api/impl/beacon/state/utils.ts index 0d0dfe68cba9..a1460143bafa 100644 --- a/packages/beacon-node/src/api/impl/beacon/state/utils.ts +++ b/packages/beacon-node/src/api/impl/beacon/state/utils.ts @@ -1,11 +1,8 @@ import {routes} from "@lodestar/api"; import {CheckpointWithHex} from "@lodestar/fork-choice"; -import {GENESIS_SLOT} from "@lodestar/params"; -import {IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; -import {BLSPubkey, Epoch, RootHex, Slot, ValidatorIndex, getValidatorStatus, phase0} from "@lodestar/types"; -import {fromHex} from "@lodestar/utils"; -import {IBeaconEngine} from "../../../../chain/beaconEngine/index.js"; -import {IBeaconChain} from "../../../../chain/index.js"; +import {ForkName, GENESIS_SLOT} from "@lodestar/params"; +import {RootHex, Slot} from "@lodestar/types"; +import {ApiStateResult, ApiStateResultWithFork, IBeaconEngine} from "../../../../chain/beaconEngine/index.js"; import {ApiError, ValidationError} from "../../errors.js"; export function resolveStateId( @@ -41,105 +38,33 @@ export function resolveStateId( return blockSlot; } -// TODO - beacon engine: move it over there. Do not support returning the whole IBeaconStateView. -export async function getStateResponseWithRegen( - chain: IBeaconChain, - inStateId: routes.beacon.StateId -): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean}> { - const stateId = resolveStateId(chain.beaconEngine, inStateId); - - const res = - typeof stateId === "string" - ? await chain.getStateByStateRoot(stateId, {allowRegen: true}) - : typeof stateId === "number" - ? stateId > chain.clock.currentSlot - ? null // Don't try to serve future slots - : stateId >= chain.beaconEngine.getFinalizedBlock().slot - ? await chain.getStateBySlot(stateId, {allowRegen: true}) - : await chain.getHistoricalStateBySlot(stateId) - : await chain.getStateOrBytesByCheckpoint(stateId); - - if (!res) { - throw new ApiError(404, `State not found for id '${inStateId}'`); +/** + * Unwrap an engine {@link ApiStateResult}: `null` → 404, `invalid` → the mapped `ApiError`, otherwise the + * DTO + meta. Keeps HTTP-status mapping in the API layer (the engine stays HTTP-free). + */ +export function unwrapStateResult( + res: ApiStateResult, + stateId: routes.beacon.StateId +): {data: T; executionOptimistic: boolean; finalized: boolean; fork?: ForkName} { + if (res === null) { + throw new ApiError(404, `State not found for id '${stateId}'`); } - - return res; -} - -export function toValidatorResponse( - index: ValidatorIndex, - validator: phase0.Validator, - balance: number, - currentEpoch: Epoch -): routes.beacon.ValidatorResponse { - return { - index, - status: getValidatorStatus(validator, currentEpoch), - balance, - validator, - }; -} - -export function filterStateValidatorsByStatus( - statuses: string[], - state: IBeaconStateView, - pubkeyCache: PubkeyCache, - currentEpoch: Epoch -): routes.beacon.ValidatorResponse[] { - const responses: routes.beacon.ValidatorResponse[] = []; - const validators = state.getValidatorsByStatus(new Set(statuses), currentEpoch); - for (const validator of validators) { - const resp = getStateValidatorIndex(validator.pubkey, state, pubkeyCache); - if (resp.valid) { - responses.push( - toValidatorResponse(resp.validatorIndex, validator, state.getBalance(resp.validatorIndex), currentEpoch) - ); - } + if ("invalid" in res) { + throw new ApiError(res.invalid.code, res.invalid.message); } - return responses; + return res; } -type StateValidatorIndexResponse = - | {valid: true; validatorIndex: ValidatorIndex} - | {valid: false; code: number; reason: string}; - -export function getStateValidatorIndex( - id: routes.beacon.ValidatorId | BLSPubkey, - state: IBeaconStateView, - pubkeyCache: PubkeyCache -): StateValidatorIndexResponse { - if (typeof id === "string") { - // mutate `id` and fallthrough to below - if (id.startsWith("0x")) { - try { - id = fromHex(id); - } catch (_e) { - return {valid: false, code: 400, reason: "Invalid pubkey hex encoding"}; - } - } else { - id = Number(id); - } - } - - if (typeof id === "number") { - const validatorIndex = id; - // validator is invalid or added later than given stateId - if (!Number.isSafeInteger(validatorIndex)) { - return {valid: false, code: 400, reason: "Invalid validator index"}; - } - if (validatorIndex >= state.validatorCount) { - return {valid: false, code: 404, reason: "Validator index from future state"}; - } - return {valid: true, validatorIndex}; - } - - // typeof id === Uint8Array - const validatorIndex = pubkeyCache.getIndex(id); - if (validatorIndex === null) { - return {valid: false, code: 404, reason: "Validator pubkey not found in state"}; +/** {@link unwrapStateResult} for reads whose success branch always carries `fork` (used as `version` meta). */ +export function unwrapStateResultWithFork( + res: ApiStateResultWithFork, + stateId: routes.beacon.StateId +): {data: T; executionOptimistic: boolean; finalized: boolean; fork: ForkName} { + if (res === null) { + throw new ApiError(404, `State not found for id '${stateId}'`); } - if (validatorIndex >= state.validatorCount) { - return {valid: false, code: 404, reason: "Validator pubkey from future state"}; + if ("invalid" in res) { + throw new ApiError(res.invalid.code, res.invalid.message); } - return {valid: true, validatorIndex}; + return res; } diff --git a/packages/beacon-node/src/api/impl/debug/index.ts b/packages/beacon-node/src/api/impl/debug/index.ts index 1dcfe6997749..8ee4848d4318 100644 --- a/packages/beacon-node/src/api/impl/debug/index.ts +++ b/packages/beacon-node/src/api/impl/debug/index.ts @@ -9,7 +9,8 @@ import {getBlobKzgCommitments} from "../../../util/dataColumns.js"; import {isOptimisticBlock} from "../../../util/forkChoice.js"; import {getStateSlotFromBytes} from "../../../util/multifork.js"; import {getBlockResponse} from "../beacon/blocks/utils.js"; -import {getStateResponseWithRegen} from "../beacon/state/utils.js"; +import {resolveStateId} from "../beacon/state/utils.js"; +import {ApiError} from "../errors.js"; import {ApiModules} from "../types.js"; import {assertUniqueItems} from "../utils.js"; @@ -133,15 +134,17 @@ export function getDebugApi({ }, async getStateV2({stateId}, context) { - const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, stateId); - let slot: number, data: Uint8Array | BeaconState; - if (state instanceof Uint8Array) { - slot = getStateSlotFromBytes(state); - data = state; - } else { - slot = state.slot; - data = context?.returnBytes ? state.serialize() : state.toValue(); + const id = resolveStateId(chain.beaconEngine, stateId); + const res = await chain.beaconEngine.getSerializedState(id); + if (!res) { + throw new ApiError(404, `State not found for id '${stateId}'`); } + const {state: stateBytes, executionOptimistic, finalized} = res; + const slot = getStateSlotFromBytes(stateBytes); + // Whole-state debug endpoint: bytes are the payload; JSON path materializes the full state value. + const data: Uint8Array | BeaconState = context?.returnBytes + ? stateBytes + : config.getForkTypes(slot).BeaconState.deserialize(stateBytes); return { data, meta: { diff --git a/packages/beacon-node/src/api/impl/lodestar/index.ts b/packages/beacon-node/src/api/impl/lodestar/index.ts index 7596d077c379..853ad9b51973 100644 --- a/packages/beacon-node/src/api/impl/lodestar/index.ts +++ b/packages/beacon-node/src/api/impl/lodestar/index.ts @@ -2,23 +2,18 @@ import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; import {ChainForkConfig} from "@lodestar/config"; import {Repository} from "@lodestar/db"; -import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params"; -import { - computeEpochAtSlot, - computeStartSlotAtEpoch, - getIndexedAttestation, - isStatePostCapella, -} from "@lodestar/state-transition"; -import {Attestation, Epoch, IndexedAttestation, ssz} from "@lodestar/types"; +import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; +import {Attestation, Epoch} from "@lodestar/types"; import {Checkpoint} from "@lodestar/types/phase0"; import {fromHex, toHex, toRootHex} from "@lodestar/utils"; import {BeaconChain} from "../../../chain/index.js"; -import {QueuedStateRegenerator, RegenRequest} from "../../../chain/regen/index.js"; +import {RegenRequest} from "../../../chain/regen/index.js"; import {IBeaconDb} from "../../../db/interface.js"; import {GossipType} from "../../../network/index.js"; import {getStateSlotFromBytes} from "../../../util/multifork.js"; import {ProfileThread, profileThread, writeHeapSnapshot} from "../../../util/profile.js"; -import {getStateResponseWithRegen} from "../beacon/state/utils.js"; +import {resolveStateId} from "../beacon/state/utils.js"; import {ApiError} from "../errors.js"; import {ApiModules} from "../types.js"; import {getAttesterSlashingsFromIndexedAttestations} from "./attesterSlashing.js"; @@ -90,8 +85,7 @@ export function getLodestarApi({ }, async getLatestWeakSubjectivityCheckpointEpoch() { - const state = chain.getHeadState(); - return {data: state.getLatestWeakSubjectivityCheckpointEpoch()}; + return {data: chain.beaconEngine.getLatestWeakSubjectivityCheckpointEpoch()}; }, async getSyncChainsDebugState() { @@ -106,9 +100,9 @@ export function getLodestarApi({ async getRegenQueueItems() { return { - data: (chain.regen as QueuedStateRegenerator).jobQueue.getItems().map((item) => ({ - key: item.args[0].key, - args: regenRequestToJson(config, item.args[0]), + data: chain.beaconEngine.dumpRegenQueueItems().map((item) => ({ + key: item.key, + args: regenRequestToJson(config, item.args), addedTimeMs: item.addedTimeMs, })), }; @@ -129,7 +123,7 @@ export function getLodestarApi({ }, async getStateCacheItems() { - return {data: chain.regen.dumpCacheSummary()}; + return {data: chain.beaconEngine.dumpCacheSummary()}; }, async getGossipPeerScoreStats() { @@ -148,7 +142,7 @@ export function getLodestarApi({ }, async dropStateCache() { - chain.regen.dropCache(); + chain.beaconEngine.dropCache(); }, async connectPeer({peerId, multiaddrs}) { @@ -217,28 +211,19 @@ export function getLodestarApi({ }, async getHistoricalSummaries({stateId}) { - const {state, executionOptimistic, finalized} = await getStateResponseWithRegen(chain, stateId); - - const stateView = state instanceof Uint8Array ? chain.getHeadState().loadOtherState(state) : state; - - const fork = config.getForkName(stateView.slot); - if (ForkSeq[fork] < ForkSeq.capella) { - throw new Error("Historical summaries are not supported before Capella"); - } - if (!isStatePostCapella(stateView)) { - throw new Error("Expected Capella state for historical summaries"); + const id = resolveStateId(chain.beaconEngine, stateId); + const res = await chain.beaconEngine.getHistoricalSummaries(id); + if (!res) { + throw new ApiError(404, `State not found for id '${stateId}'`); } - const {gindex} = ssz[fork].BeaconState.getPathInfo(["historicalSummaries"]); - const proof = stateView.getSingleProof(gindex); - return { data: { - slot: stateView.slot, - historicalSummaries: stateView.historicalSummaries, - proof: proof, + slot: res.slot, + historicalSummaries: res.historicalSummaries, + proof: res.proof, }, - meta: {executionOptimistic, finalized, version: fork}, + meta: {executionOptimistic: res.executionOptimistic, finalized: res.finalized, version: res.fork}, }; }, @@ -326,19 +311,15 @@ export function getLodestarApi({ } } - const indexedAttestations: IndexedAttestation[] = []; // Assume all blocks are from the same fork const forkSeq = config.getForkSeq(signedBlocks[0].message.slot); - for (const [epoch, attestationsPerEpoch] of attestations) { - const slot = computeStartSlotAtEpoch(epoch); - const {state} = await getStateResponseWithRegen(chain, slot); - const stateView = state instanceof Uint8Array ? chain.getHeadState().loadOtherState(state) : state; - const shuffling = stateView.getShufflingAtEpoch(epoch); - for (const attestation of attestationsPerEpoch) { - indexedAttestations.push(getIndexedAttestation(shuffling, forkSeq, attestation)); - } - } + const requests = Array.from(attestations, ([epoch, attestationsPerEpoch]) => ({ + slot: computeStartSlotAtEpoch(epoch), + epoch, + attestations: attestationsPerEpoch, + })); + const indexedAttestations = await chain.beaconEngine.getIndexedAttestationsForSlashing(requests, forkSeq); const result = getAttesterSlashingsFromIndexedAttestations(forkSeq, indexedAttestations); diff --git a/packages/beacon-node/src/api/impl/proof/index.ts b/packages/beacon-node/src/api/impl/proof/index.ts index 913114b2f19e..7d33d040233b 100644 --- a/packages/beacon-node/src/api/impl/proof/index.ts +++ b/packages/beacon-node/src/api/impl/proof/index.ts @@ -3,7 +3,8 @@ import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; import {ApiOptions} from "../../options.js"; import {getBlockResponse} from "../beacon/blocks/utils.js"; -import {getStateResponseWithRegen} from "../beacon/state/utils.js"; +import {resolveStateId} from "../beacon/state/utils.js"; +import {ApiError} from "../errors.js"; import {ApiModules} from "../types.js"; export function getProofApi( @@ -21,13 +22,15 @@ export function getProofApi( throw new Error("Requested proof is too large."); } - const res = await getStateResponseWithRegen(chain, stateId); - - const state = res.state instanceof Uint8Array ? chain.getHeadState().loadOtherState(res.state) : res.state; + const id = resolveStateId(chain.beaconEngine, stateId); + const res = await chain.beaconEngine.getStateProof(id, descriptor); + if (!res) { + throw new ApiError(404, `State not found for id '${stateId}'`); + } return { - data: state.createMultiProof(descriptor), - meta: {version: config.getForkName(state.slot)}, + data: res.proof, + meta: {version: res.fork}, }; }, async getBlockProof({blockId, descriptor}) { diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index a74fdd930234..8da880b02181 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -34,9 +34,9 @@ import { ProducedBlockSource, Root, Slot, + ValidatorIndex, Wei, bellatrix, - getValidatorStatus, gloas, ssz, sszTypesFor, @@ -62,7 +62,6 @@ import { ProduceFullDeneb, ProduceFullGloas, } from "../../../chain/produceBlock/index.js"; -import {RegenCaller} from "../../../chain/regen/index.js"; import {BuilderStatus, NoBidReceived} from "../../../execution/builder/http.js"; import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js"; import {CommitteeSubscription} from "../../../network/subnets/index.js"; @@ -928,20 +927,28 @@ export function getValidatorApi( // This needs a state in the same epoch as `slot` such that state.currentJustifiedCheckpoint is correct. // Note: This may trigger an epoch transition if there skipped slots at the beginning of the epoch. - const headState = chain.getHeadState(); - const headSlot = headState.slot; + const head = chain.beaconEngine.getHead(); + const headSlot = head.slot; const attEpoch = computeEpochAtSlot(slot); - const headBlockRootHex = chain.beaconEngine.getHead().blockRoot; - const headBlockRoot = fromHex(headBlockRootHex); + const headBlockRoot = fromHex(head.blockRoot); const fork = config.getForkName(slot); + // Canonical block root ≤ slot (matches the former state.getBlockRootAtSlot; handles skipped slots). + const blockRootAtSlot = (atSlot: Slot): Root => { + const canonical = chain.beaconEngine.getCanonicalBlockClosestLteSlot(atSlot); + if (!canonical) { + throw Error(`No canonical block found for slot=${atSlot}`); + } + return fromHex(canonical.blockRoot); + }; + const beaconBlockRoot = slot >= headSlot ? // When attesting to the head slot or later, always use the head of the chain. headBlockRoot : // Permit attesting to slots *prior* to the current head. This is desirable when // the VC and BN are out-of-sync due to time issues or overloading. - headState.getBlockRootAtSlot(slot); + blockRootAtSlot(slot); let index: CommitteeIndex; if (isForkPostGloas(fork)) { @@ -972,7 +979,7 @@ export function getValidatorApi( targetSlot >= headSlot ? // If the state is earlier than the target slot then the target *must* be the head block root. headBlockRoot - : headState.getBlockRootAtSlot(targetSlot); + : blockRootAtSlot(targetSlot); // Check the execution status as validator shouldn't vote on an optimistic head // Check on target is sufficient as a valid target would imply a valid source @@ -980,18 +987,15 @@ export function getValidatorApi( notOnOutOfRangeData(targetRoot); // To get the correct source we must get a state in the same epoch as the attestation's epoch. - // An epoch transition may change state.currentJustifiedCheckpoint - const attEpochState = await chain.getHeadStateAtEpoch(attEpoch, RegenCaller.produceAttestationData); - - // TODO confirm if the below is correct assertion - // notOnOutOfRangeData(attEpochState.currentJustifiedCheckpoint.root); + // An epoch transition may change state.currentJustifiedCheckpoint (resolved inside the engine). + const source = await chain.beaconEngine.getAttestationSourceCheckpoint(attEpoch); return { data: { slot, index, beaconBlockRoot, - source: attEpochState.currentJustifiedCheckpoint, + source, target: {epoch: attEpoch, root: targetRoot}, }, }; @@ -1477,24 +1481,18 @@ export function getValidatorApi( // should only send active or pending validator to builder // Spec: https://ethereum.github.io/builder-specs/#/Builder/registerValidator - const headState = chain.getHeadState(); const currentEpoch = chain.clock.currentEpoch; - const filteredRegistrations = registrations.filter((registration) => { - const {pubkey} = registration.message; - const validatorIndex = chain.pubkeyCache.getIndex(pubkey); - if (validatorIndex === null) return false; - - const validator = headState.getValidator(validatorIndex); - const status = getValidatorStatus(validator, currentEpoch); - return ( - status === "active_exiting" || - status === "active_ongoing" || - status === "active_slashed" || - status === "pending_initialized" || - status === "pending_queued" - ); - }); + // Resolve pubkey→index facade-side (shared pubkeyCache); the engine returns which are active/pending. + const withIndex: {registration: (typeof registrations)[number]; index: ValidatorIndex}[] = []; + for (const registration of registrations) { + const index = chain.pubkeyCache.getIndex(registration.message.pubkey); + if (index !== null) { + withIndex.push({registration, index}); + } + } + const activeOrPending = chain.beaconEngine.getActiveOrPendingValidators(withIndex.map((x) => x.index)); + const filteredRegistrations = withIndex.filter((x) => activeOrPending.has(x.index)).map((x) => x.registration); await chain.executionBuilder.registerValidator(currentEpoch, filteredRegistrations); diff --git a/packages/beacon-node/src/chain/archiveStore/archiveStore.ts b/packages/beacon-node/src/chain/archiveStore/archiveStore.ts index 7dc5829d2ef6..b3b5da38c561 100644 --- a/packages/beacon-node/src/chain/archiveStore/archiveStore.ts +++ b/packages/beacon-node/src/chain/archiveStore/archiveStore.ts @@ -4,12 +4,10 @@ import {Checkpoint} from "@lodestar/types/phase0"; import {callFnWhenAwait} from "@lodestar/utils"; import {IBeaconChainDb} from "../../db/index.js"; import {Metrics} from "../../metrics/metrics.js"; -import {isOptimisticBlock} from "../../util/forkChoice.js"; import {JobItemQueue, isQueueErrorAborted} from "../../util/queue/index.js"; import {ChainEvent} from "../emitter.js"; import {IBeaconChain} from "../interface.js"; import {PROCESS_FINALIZED_CHECKPOINT_QUEUE_LENGTH} from "./constants.js"; -import {HistoricalStateRegen} from "./historicalState/historicalStateRegen.js"; import {ArchiveStoreOpts, ArchiveStoreTask} from "./interface.js"; import {migrateFinalizedDA} from "./utils/archiveBlocks.js"; import {updateBackfillRange} from "./utils/updateBackfillRange.js"; @@ -38,8 +36,6 @@ export class ArchiveStore { private readonly opts: ArchiveStoreInitOpts; private readonly signal: AbortSignal; - private historicalStateRegen?: HistoricalStateRegen; - constructor(modules: ArchiveStoreModules, opts: ArchiveStoreInitOpts, signal: AbortSignal) { this.chain = modules.chain; this.db = modules.db; @@ -87,46 +83,6 @@ export class ArchiveStore { this.signal ); } - - if (this.opts.serveHistoricalState) { - this.historicalStateRegen = await HistoricalStateRegen.init({ - opts: { - genesisTime: this.chain.clock.genesisTime, - dbLocation: this.opts.dbName, - nativeStateView: this.opts.nativeStateView ?? false, - }, - config: this.chain.config, - metrics: this.metrics, - logger: this.logger, - signal: this.signal, - }); - } - } - - async close(): Promise { - await this.historicalStateRegen?.close(); - } - - async scrapeMetrics(): Promise { - return this.historicalStateRegen?.scrapeMetrics() ?? ""; - } - - async getHistoricalStateBySlot( - slot: number - ): Promise<{state: Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { - const finalizedBlock = this.chain.beaconEngine.getFinalizedBlock(); - - if (slot >= finalizedBlock.slot) { - return null; - } - - // request for finalized state using historical state regen - const stateSerialized = await this.historicalStateRegen?.getHistoricalState(slot); - if (!stateSerialized) { - return null; - } - - return {state: stateSerialized, executionOptimistic: isOptimisticBlock(finalizedBlock), finalized: true}; } /** diff --git a/packages/beacon-node/src/chain/archiveStore/interface.ts b/packages/beacon-node/src/chain/archiveStore/interface.ts index af12fd4d752f..4b39b8953049 100644 --- a/packages/beacon-node/src/chain/archiveStore/interface.ts +++ b/packages/beacon-node/src/chain/archiveStore/interface.ts @@ -65,20 +65,6 @@ export interface IArchiveStore { * Initialize archive store and load any worker required */ init(): Promise; - /** - * Cleanup and close any worker - */ - close(): Promise; - /** - * Scrape metrics from the archive store - */ - scrapeMetrics(): Promise; - /** - * Get historical state by slot - */ - getHistoricalStateBySlot( - slot: number - ): Promise<{state: Uint8Array; executionOptimistic: boolean; finalized: boolean} | null>; /** * Archive latest finalized state */ diff --git a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts index 7f689f8a8d50..68c0de768f60 100644 --- a/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts +++ b/packages/beacon-node/src/chain/beaconEngine/beaconEngine.ts @@ -1,3 +1,4 @@ +import {CompactMultiProof} from "@chainsafe/persistent-merkle-tree"; import {BitArray} from "@chainsafe/ssz"; import {routes} from "@lodestar/api"; import {BeaconConfig} from "@lodestar/config"; @@ -20,7 +21,9 @@ import { UpdateHeadOpt, getSafeExecutionBlockHash, } from "@lodestar/fork-choice"; +import type {LoggerNode} from "@lodestar/logger/node"; import { + EPOCHS_PER_HISTORICAL_VECTOR, ForkName, ForkPostAltair, ForkPostBellatrix, @@ -40,23 +43,31 @@ import { EpochShuffling, IBeaconStateView, IBeaconStateViewBellatrix, + IBeaconStateViewGloas, PubkeyCache, RootCache, StateHashTreeRootSource, calculateCommitteeAssignments, + computeEndSlotAtEpoch, computeEpochAtSlot, computeStartSlotAtEpoch, computeTimeAtSlot, + getCurrentEpoch, + getEffectiveBalancesFromStateBytes, + getIndexedAttestation, isStartSlotOfEpoch, isStatePostAltair, isStatePostBellatrix, isStatePostCapella, + isStatePostElectra, + isStatePostFulu, isStatePostGloas, proposerShufflingDecisionRoot, } from "@lodestar/state-transition"; import { Attestation, AttesterSlashing, + BLSPubkey, BLSSignature, BeaconBlock, BlindedBeaconBlock, @@ -80,13 +91,15 @@ import { deneb, electra, fulu, + getValidatorStatus, gloas, isGloasBeaconBlock, phase0, + rewards, ssz, } from "@lodestar/types"; import {Logger, byteArrayEquals, fromHex, sleep, toRootHex} from "@lodestar/utils"; -import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js"; +import {GENESIS_EPOCH, ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js"; import {IBeaconEngineDb} from "../../db/index.js"; import {Metrics} from "../../metrics/index.js"; import {BufferPool} from "../../util/bufferPool.js"; @@ -95,6 +108,7 @@ import {getShufflingDependentRoot} from "../../util/dependentRoot.js"; import {callInNextEventLoop} from "../../util/eventLoop.js"; import {isOptimisticBlock} from "../../util/forkChoice.js"; import {BlockRootSlot, getSlotFromSignedBeaconBlockSerialized} from "../../util/sszBytes.js"; +import {HistoricalStateRegen} from "../archiveStore/historicalState/historicalStateRegen.js"; import {ArchiveMode, ArchiveStoreTask, StateArchiveStrategy} from "../archiveStore/interface.js"; import {FrequencyStateArchiveStrategy} from "../archiveStore/strategies/frequencyStateArchiveStrategy.js"; import { @@ -120,7 +134,7 @@ import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from ". import {ChainEvent, ChainEventEmitter, ReorgEventData} from "../emitter.js"; import {BlockError, BlockErrorCode} from "../errors/index.js"; import {ForkchoiceCaller, initializeForkChoice} from "../forkChoice/index.js"; -import {CommonBlockBody, FindHeadFnName} from "../interface.js"; +import {CommonBlockBody, FindHeadFnName, StateGetOpts} from "../interface.js"; import {LightClientServer} from "../lightClient/index.js"; import { AggregatedAttestationPool, @@ -142,7 +156,7 @@ import { PayloadAttributesWithdrawals, preparePayloadAttributes, } from "../produceBlock/produceBlockBody.js"; -import {QueuedStateRegenerator, RegenCaller} from "../regen/index.js"; +import {QueuedStateRegenerator, RegenCaller, RegenRequest} from "../regen/index.js"; import { SeenAggregators, SeenAttesters, @@ -205,6 +219,8 @@ import { runGossipValidation, } from "./gossipValidationResult.js"; import { + ApiStateResult, + ApiStateResultWithFork, BeaconEngineModules, FcuUpdate, FinalizedProtoSummary, @@ -287,6 +303,10 @@ export class BeaconEngine implements IBeaconEngine { // Engine owns state archival (states DB). Finalized-state archive runs inside migrateFinalized; the // facade delegates temp-state (onCheckpoint) and shutdown persistence to engine methods. private readonly stateArchiveStrategy: StateArchiveStrategy; + // Historical (below-finalized) state serving via a worker thread; built lazily on --serveHistoricalState. + private historicalStateRegen?: HistoricalStateRegen; + private readonly dbName: string; + private readonly signal: AbortSignal; lightClientServer: LightClientServer | undefined; private readonly verifiedBlocks = new Map(); private readonly emitter: ChainEventEmitter; @@ -313,6 +333,8 @@ export class BeaconEngine implements IBeaconEngine { this.opts = opts; this.logger = logger; this.db = db; + this.dbName = modules.dbName; + this.signal = signal; this.metrics = metrics; this.clock = clock; this.pubkeyCache = pubkeyCache; @@ -3167,6 +3189,768 @@ export class BeaconEngine implements IBeaconEngine { await this.opPool.toPersisted(this.db); } + // === BLK-3: state-read engine methods (regen-backed; no IBeaconStateView crosses the seam) === + + /** Dump regen cache summary (lodestar debug endpoint). */ + dumpCacheSummary(): routes.lodestar.StateCacheItem[] { + return this.regen.dumpCacheSummary(); + } + + /** Drop regen caches (lodestar debug endpoint). */ + dropCache(): void { + this.regen.dropCache(); + } + + /** Snapshot of the regen job queue (lodestar debug endpoint). */ + dumpRegenQueueItems(): {key: RegenRequest["key"]; args: RegenRequest; addedTimeMs: number}[] { + return this.regen.jobQueue.getItems().map((item) => ({ + key: item.args[0].key, + args: item.args[0], + addedTimeMs: item.addedTimeMs, + })); + } + + /** Active validator count of the head state (network init). */ + getActiveValidatorCount(): number { + return this.getHeadState().activeValidatorCount; + } + + /** Head-state electra queue counts (facade metrics); null pre-electra. */ + getHeadPendingCounts(): { + pendingDeposits: number; + pendingPartialWithdrawals: number; + pendingConsolidations: number; + } | null { + const state = this.getHeadState(); + if (!isStatePostElectra(state)) { + return null; + } + return { + pendingDeposits: state.pendingDepositsCount, + pendingPartialWithdrawals: state.pendingPartialWithdrawalsCount, + pendingConsolidations: state.pendingConsolidationsCount, + }; + } + + /** Validator-monitor end-of-epoch hook against the head state (facade clock listener drives the timing). */ + validatorMonitorOnceEveryEndOfEpoch(): void { + this.validatorMonitor?.onceEveryEndOfEpoch(this.getHeadState()); + } + + /** Whether regen can accept more work (backpressure). */ + regenCanAcceptWork(): boolean { + return this.regen.canAcceptWork(); + } + + /** Initialize regen caches from disk (startup). */ + async initRegen(): Promise { + await this.regen.init(); + } + + /** Head-state execution flags for the notifier (post-bellatrix merge-transition display). */ + getHeadExecutionStateInfo(): {isExecutionStateType: boolean; isMergeTransitionComplete: boolean} { + const state = this.getHeadState(); + if (!isStatePostBellatrix(state) || !state.isExecutionStateType) { + return {isExecutionStateType: false, isMergeTransitionComplete: false}; + } + return {isExecutionStateType: true, isMergeTransitionComplete: state.isMergeTransitionComplete}; + } + + /** Build the historical-state worker if enabled (--serveHistoricalState). Idempotent; call on startup. */ + async loadHistoricalStateRegen(): Promise { + if (this.opts.serveHistoricalState && !this.historicalStateRegen) { + this.historicalStateRegen = await HistoricalStateRegen.init({ + opts: { + genesisTime: this.clock.genesisTime, + dbLocation: this.dbName, + nativeStateView: this.opts.nativeStateView ?? false, + }, + config: this.config, + metrics: this.metrics, + logger: this.logger as LoggerNode, + signal: this.signal, + }); + } + } + + /** Terminate the historical-state worker (graceful shutdown). */ + async closeHistoricalStateRegen(): Promise { + await this.historicalStateRegen?.close(); + } + + /** Prometheus metrics from the historical-state worker. */ + async scrapeHistoricalStateMetrics(): Promise { + return (await this.historicalStateRegen?.scrapeMetrics()) ?? ""; + } + + /** Serialized historical (below-finalized) state by slot; null if unavailable / not enabled. */ + async getHistoricalStateBySlot( + slot: Slot + ): Promise<{state: Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { + const finalizedBlock = this.forkChoice.getFinalizedBlock(); + if (slot >= finalizedBlock.slot) { + return null; + } + const stateSerialized = await this.historicalStateRegen?.getHistoricalState(slot); + if (!stateSerialized) { + return null; + } + return {state: stateSerialized, executionOptimistic: isOptimisticBlock(finalizedBlock), finalized: true}; + } + + // --- API state resolution (engine-internal; state never leaves the engine) --- + + /** Resolve an API stateId (already reduced to root/slot/checkpoint) to a state view or bytes. */ + private async resolveStateForApi( + id: RootHex | Slot | CheckpointWithHex + ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { + if (typeof id === "string") { + return this.getStateByStateRoot(id, {allowRegen: true}); + } + if (typeof id === "number") { + if (id > this.clock.currentSlot) { + return null; // Don't try to serve future slots + } + return id >= this.getFinalizedBlock().slot + ? this.getStateBySlot(id, {allowRegen: true}) + : this.getHistoricalStateBySlot(id); + } + return this.getStateOrBytesByCheckpoint(id); + } + + /** Resolve to a state *view* (deserializing archive/checkpoint bytes engine-side onto the head state). */ + private async resolveStateViewForApi( + id: RootHex | Slot | CheckpointWithHex + ): Promise<{state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean} | null> { + const res = await this.resolveStateForApi(id); + if (!res) { + return null; + } + const state = res.state instanceof Uint8Array ? this.getHeadState().loadOtherState(res.state) : res.state; + return {state, executionOptimistic: res.executionOptimistic, finalized: res.finalized}; + } + + /** Serialized state for the debug `getStateV2` endpoint (the only whole-state-bytes crossing). */ + async getSerializedState( + id: RootHex | Slot | CheckpointWithHex + ): Promise<{state: Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { + const res = await this.resolveStateForApi(id); + if (!res) { + return null; + } + const state = res.state instanceof Uint8Array ? res.state : res.state.serialize(); + return {state, executionOptimistic: res.executionOptimistic, finalized: res.finalized}; + } + + /** Effective balances of `validatorIndices` from the finalized checkpoint state (custody group count). */ + async getFinalizedEffectiveBalances( + finalizedCheckpoint: CheckpointWithHex, + validatorIndices: ValidatorIndex[] + ): Promise { + const stateOrBytes = (await this.getStateOrBytesByCheckpoint(finalizedCheckpoint))?.state; + if (!stateOrBytes) { + return null; + } + if (stateOrBytes instanceof Uint8Array) { + return getEffectiveBalancesFromStateBytes(this.config, stateOrBytes, validatorIndices); + } + return validatorIndices.map((index) => stateOrBytes.getValidator(index).effectiveBalance ?? 0); + } + + /** Multi-proof of the resolved state for the given SSZ `descriptor`; null if the state is unavailable. */ + async getStateProof( + id: RootHex | Slot | CheckpointWithHex, + descriptor: Uint8Array + ): Promise<{proof: CompactMultiProof; fork: ForkName} | null> { + const res = await this.resolveStateViewForApi(id); + if (!res) { + return null; + } + return {proof: res.state.createMultiProof(descriptor), fork: this.config.getForkName(res.state.slot)}; + } + + /** Historical summaries + single-proof of the resolved state; null if unavailable (post-capella only). */ + async getHistoricalSummaries(id: RootHex | Slot | CheckpointWithHex): Promise<{ + slot: Slot; + historicalSummaries: capella.HistoricalSummaries; + proof: Uint8Array[]; + fork: ForkName; + executionOptimistic: boolean; + finalized: boolean; + } | null> { + const res = await this.resolveStateViewForApi(id); + if (!res) { + return null; + } + const {state, executionOptimistic, finalized} = res; + const fork = this.config.getForkName(state.slot); + if (ForkSeq[fork] < ForkSeq.capella) { + throw new Error("Historical summaries are not supported before Capella"); + } + if (!isStatePostCapella(state)) { + throw new Error("Expected Capella state for historical summaries"); + } + const {gindex} = ssz[fork].BeaconState.getPathInfo(["historicalSummaries"]); + return { + slot: state.slot, + historicalSummaries: state.historicalSummaries, + proof: state.getSingleProof(gindex), + fork, + executionOptimistic, + finalized, + }; + } + + /** Resolve per-epoch shuffling and build indexed attestations (attester-slashing debug helper). */ + async getIndexedAttestationsForSlashing( + requests: {slot: Slot; epoch: Epoch; attestations: Attestation[]}[], + forkSeq: ForkSeq + ): Promise { + const indexed: IndexedAttestation[] = []; + for (const {slot, epoch, attestations} of requests) { + const res = await this.resolveStateViewForApi(slot); + if (!res) { + throw Error(`State not available for slot ${slot}`); + } + const shuffling = res.state.getShufflingAtEpoch(epoch); + for (const attestation of attestations) { + indexed.push(getIndexedAttestation(shuffling, forkSeq, attestation)); + } + } + return indexed; + } + + // --- beacon/state API family (each resolves state internally, returns a targeted DTO) --- + + private toValidatorResponse( + index: ValidatorIndex, + validator: phase0.Validator, + balance: number, + currentEpoch: Epoch + ): routes.beacon.ValidatorResponse { + return {index, status: getValidatorStatus(validator, currentEpoch), balance, validator}; + } + + private resolveStateValidatorIndex( + id: routes.beacon.ValidatorId | BLSPubkey, + state: IBeaconStateView + ): StateValidatorIndexResponse { + return getStateValidatorIndex(id, state, this.pubkeyCache); + } + + private filterStateValidatorsByStatus( + statuses: string[], + state: IBeaconStateView, + currentEpoch: Epoch + ): routes.beacon.ValidatorResponse[] { + const responses: routes.beacon.ValidatorResponse[] = []; + const validators = state.getValidatorsByStatus(new Set(statuses), currentEpoch); + for (const validator of validators) { + const resp = this.resolveStateValidatorIndex(validator.pubkey, state); + if (resp.valid) { + responses.push( + this.toValidatorResponse(resp.validatorIndex, validator, state.getBalance(resp.validatorIndex), currentEpoch) + ); + } + } + return responses; + } + + async getStateRoot(id: RootHex | Slot | CheckpointWithHex): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + return { + data: {root: res.state.hashTreeRoot()}, + executionOptimistic: res.executionOptimistic, + finalized: res.finalized, + }; + } + + async getStateFork(id: RootHex | Slot | CheckpointWithHex): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + return {data: res.state.fork, executionOptimistic: res.executionOptimistic, finalized: res.finalized}; + } + + async getStateRandao( + id: RootHex | Slot | CheckpointWithHex, + epoch?: Epoch + ): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + const {state, executionOptimistic, finalized} = res; + const stateEpoch = computeEpochAtSlot(state.slot); + const usedEpoch = epoch ?? stateEpoch; + if (!(stateEpoch < usedEpoch + EPOCHS_PER_HISTORICAL_VECTOR && usedEpoch <= stateEpoch)) { + return {invalid: {code: 400, message: "Requested epoch is out of range"}}; + } + return {data: {randao: state.getRandaoMix(usedEpoch)}, executionOptimistic, finalized}; + } + + async getStateFinalityCheckpoints(id: RootHex | Slot | CheckpointWithHex): Promise< + ApiStateResult<{ + currentJustified: phase0.Checkpoint; + previousJustified: phase0.Checkpoint; + finalized: phase0.Checkpoint; + }> + > { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + const {state, executionOptimistic, finalized} = res; + return { + data: { + currentJustified: state.currentJustifiedCheckpoint, + previousJustified: state.previousJustifiedCheckpoint, + finalized: state.finalizedCheckpoint, + }, + executionOptimistic, + finalized, + }; + } + + async getStateValidators( + id: RootHex | Slot | CheckpointWithHex, + validatorIds: routes.beacon.ValidatorId[], + statuses: string[] + ): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + const {state, executionOptimistic, finalized} = res; + const currentEpoch = getCurrentEpoch(state); + + let data: routes.beacon.ValidatorResponse[]; + if (validatorIds.length) { + data = []; + for (const vid of validatorIds) { + const resp = this.resolveStateValidatorIndex(vid, state); + if (resp.valid) { + const validator = state.getValidator(resp.validatorIndex); + if (statuses.length && !statuses.includes(getValidatorStatus(validator, currentEpoch))) { + continue; + } + data.push( + this.toValidatorResponse( + resp.validatorIndex, + validator, + state.getBalance(resp.validatorIndex), + currentEpoch + ) + ); + } + } + } else if (statuses.length) { + data = this.filterStateValidatorsByStatus(statuses, state, currentEpoch); + } else { + // TODO: This loops over the entire state, it's a DOS vector + const validatorsArr = state.getAllValidators(); + const balancesArr = state.getAllBalances(); + data = []; + for (let i = 0; i < validatorsArr.length; i++) { + data.push(this.toValidatorResponse(i, validatorsArr[i], balancesArr[i], currentEpoch)); + } + } + return {data, executionOptimistic, finalized}; + } + + async getStateValidatorIdentities( + id: RootHex | Slot | CheckpointWithHex, + validatorIds: routes.beacon.ValidatorId[] + ): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + const {state, executionOptimistic, finalized} = res; + + let data: routes.beacon.ValidatorIdentities; + if (validatorIds.length) { + data = []; + for (const vid of validatorIds) { + const resp = this.resolveStateValidatorIndex(vid, state); + if (resp.valid) { + const {pubkey, activationEpoch} = state.getValidator(resp.validatorIndex); + data.push({index: resp.validatorIndex, pubkey, activationEpoch}); + } + } + } else { + const validatorsArr = state.getAllValidators(); + data = new Array(validatorsArr.length) as routes.beacon.ValidatorIdentities; + for (let i = 0; i < validatorsArr.length; i++) { + const {pubkey, activationEpoch} = validatorsArr[i]; + data[i] = {index: i, pubkey, activationEpoch}; + } + } + return {data, executionOptimistic, finalized}; + } + + async getStateValidator( + id: RootHex | Slot | CheckpointWithHex, + validatorId: routes.beacon.ValidatorId + ): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + const {state, executionOptimistic, finalized} = res; + const resp = this.resolveStateValidatorIndex(validatorId, state); + if (!resp.valid) { + return {invalid: {code: resp.code, message: resp.reason}}; + } + return { + data: this.toValidatorResponse( + resp.validatorIndex, + state.getValidator(resp.validatorIndex), + state.getBalance(resp.validatorIndex), + getCurrentEpoch(state) + ), + executionOptimistic, + finalized, + }; + } + + async getStateValidatorBalances( + id: RootHex | Slot | CheckpointWithHex, + validatorIds: routes.beacon.ValidatorId[] + ): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + const {state, executionOptimistic, finalized} = res; + + let data: routes.beacon.ValidatorBalance[]; + if (validatorIds.length) { + data = []; + for (const vid of validatorIds) { + const resp = this.resolveStateValidatorIndex(vid, state); + if (resp.valid) { + data.push({index: resp.validatorIndex, balance: state.getBalance(resp.validatorIndex)}); + } + } + } else { + // TODO: This loops over the entire state, it's a DOS vector + const balancesArr = state.getAllBalances(); + data = []; + for (let i = 0; i < balancesArr.length; i++) { + data.push({index: i, balance: balancesArr[i]}); + } + } + return {data, executionOptimistic, finalized}; + } + + async getEpochCommittees( + id: RootHex | Slot | CheckpointWithHex, + filters: {epoch?: Epoch; index?: CommitteeIndex; slot?: Slot} + ): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + const {state, executionOptimistic, finalized} = res; + + const stateEpoch = computeEpochAtSlot(state.slot); + const epoch = filters.epoch ?? stateEpoch; + const startSlot = computeStartSlotAtEpoch(epoch); + const endSlot = startSlot + SLOTS_PER_EPOCH - 1; + + if (Math.abs(epoch - stateEpoch) > 1) { + return {invalid: {code: 400, message: `Epoch ${epoch} must be within one epoch of state epoch ${stateEpoch}`}}; + } + if (filters.slot !== undefined && (filters.slot < startSlot || filters.slot > endSlot)) { + return {invalid: {code: 400, message: `Slot ${filters.slot} is not in epoch ${epoch}`}}; + } + + const decisionRoot = state.getShufflingDecisionRoot(epoch); + const shuffling = await this.shufflingCache.get(epoch, decisionRoot); + if (!shuffling) { + return { + invalid: { + code: 500, + message: `No shuffling found to calculate committees for epoch: ${epoch} and decisionRoot: ${decisionRoot}`, + }, + }; + } + const data = shuffling.committees.flatMap((slotCommittees, slotInEpoch) => { + const slot = startSlot + slotInEpoch; + if (filters.slot !== undefined && filters.slot !== slot) { + return []; + } + return slotCommittees.flatMap((committee, committeeIndex) => { + if (filters.index !== undefined && filters.index !== committeeIndex) { + return []; + } + return [{index: committeeIndex, slot, validators: Array.from(committee)}]; + }); + }); + return {data, executionOptimistic, finalized}; + } + + async getEpochSyncCommittees( + id: RootHex | Slot | CheckpointWithHex, + epoch?: Epoch + ): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + const {state, executionOptimistic, finalized} = res; + + const stateEpoch = computeEpochAtSlot(state.slot); + if (stateEpoch < this.config.ALTAIR_FORK_EPOCH) { + return {invalid: {code: 400, message: "Requested state before ALTAIR_FORK_EPOCH"}}; + } + if (!isStatePostAltair(state)) { + throw new Error("Expected Altair state for sync committee lookup"); + } + + const syncCommitteeCache = state.getIndexedSyncCommitteeAtEpoch(epoch ?? stateEpoch); + const validatorIndices = new Array(...syncCommitteeCache.validatorIndices); + const validatorAggregates: ValidatorIndex[][] = []; + for (let i = 0; i < validatorIndices.length; i += SYNC_COMMITTEE_SUBNET_SIZE) { + validatorAggregates.push(validatorIndices.slice(i, i + SYNC_COMMITTEE_SUBNET_SIZE)); + } + return {data: {validators: validatorIndices, validatorAggregates}, executionOptimistic, finalized}; + } + + async getStatePendingDeposits( + id: RootHex | Slot | CheckpointWithHex, + returnBytes: boolean + ): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + const {state, executionOptimistic, finalized} = res; + const fork = state.forkName; + if (!isStatePostElectra(state)) { + return {invalid: {code: 400, message: `Cannot retrieve pending deposits for pre-electra state fork=${fork}`}}; + } + const data = returnBytes ? ssz.electra.PendingDeposits.serialize(state.pendingDeposits) : state.pendingDeposits; + return {data, fork, executionOptimistic, finalized}; + } + + async getStatePendingPartialWithdrawals( + id: RootHex | Slot | CheckpointWithHex, + returnBytes: boolean + ): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + const {state, executionOptimistic, finalized} = res; + const fork = state.forkName; + if (!isStatePostElectra(state)) { + return { + invalid: {code: 400, message: `Cannot retrieve pending partial withdrawals for pre-electra state fork=${fork}`}, + }; + } + const data = returnBytes + ? ssz.electra.PendingPartialWithdrawals.serialize(state.pendingPartialWithdrawals) + : state.pendingPartialWithdrawals; + return {data, fork, executionOptimistic, finalized}; + } + + async getStatePendingConsolidations( + id: RootHex | Slot | CheckpointWithHex, + returnBytes: boolean + ): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + const {state, executionOptimistic, finalized} = res; + const fork = state.forkName; + if (!isStatePostElectra(state)) { + return { + invalid: {code: 400, message: `Cannot retrieve pending consolidations for pre-electra state fork=${fork}`}, + }; + } + const data = returnBytes + ? ssz.electra.PendingConsolidations.serialize(state.pendingConsolidations) + : state.pendingConsolidations; + return {data, fork, executionOptimistic, finalized}; + } + + async getStateProposerLookahead( + id: RootHex | Slot | CheckpointWithHex, + returnBytes: boolean + ): Promise> { + const res = await this.resolveStateViewForApi(id); + if (!res) return null; + const {state, executionOptimistic, finalized} = res; + const fork = state.forkName; + if (!isStatePostFulu(state)) { + return {invalid: {code: 400, message: `Cannot retrieve proposer lookahead for pre-fulu state fork=${fork}`}}; + } + const data = returnBytes ? ssz.fulu.ProposerLookahead.serialize(state.proposerLookahead) : state.proposerLookahead; + return {data, fork, executionOptimistic, finalized}; + } + + private async getStateBySlot( + slot: Slot, + opts?: StateGetOpts + ): Promise<{state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean} | null> { + const finalizedBlock = this.getFinalizedBlock(); + + if (slot < finalizedBlock.slot) { + // request for finalized state not supported here; caller falls back to getHistoricalStateBySlot + return null; + } + + if (opts?.allowRegen) { + const block = this.getCanonicalBlockClosestLteSlot(slot) ?? finalizedBlock; + const state = await this.regen.getBlockSlotState(block, slot, {dontTransferCache: true}, RegenCaller.restApi); + return { + state, + executionOptimistic: isOptimisticBlock(block), + finalized: slot === finalizedBlock.slot && finalizedBlock.slot !== GENESIS_SLOT, + }; + } + + const block = this.getCanonicalProtoBlockAtSlot(slot); + if (!block) { + return null; + } + + const state = this.regen.getStateSync(block.stateRoot); + return ( + state && { + state, + executionOptimistic: isOptimisticBlock(block), + finalized: slot === finalizedBlock.slot && finalizedBlock.slot !== GENESIS_SLOT, + } + ); + } + + private async getStateByStateRoot( + stateRoot: RootHex, + opts?: StateGetOpts + ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { + if (opts?.allowRegen) { + const state = await this.regen.getState(stateRoot, RegenCaller.restApi); + const block = this.getBlockDefaultStatus(ssz.phase0.BeaconBlockHeader.hashTreeRoot(state.latestBlockHeader)); + const finalizedEpoch = this.getFinalizedCheckpoint().epoch; + return { + state, + executionOptimistic: block != null && isOptimisticBlock(block), + finalized: state.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH, + }; + } + + const cachedStateCtx = this.regen.getStateSync(stateRoot); + if (cachedStateCtx) { + const block = this.getBlockDefaultStatus( + ssz.phase0.BeaconBlockHeader.hashTreeRoot(cachedStateCtx.latestBlockHeader) + ); + const finalizedEpoch = this.getFinalizedCheckpoint().epoch; + return { + state: cachedStateCtx, + executionOptimistic: block != null && isOptimisticBlock(block), + finalized: cachedStateCtx.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH, + }; + } + + const data = await this.getSerializedStateByRoot(fromHex(stateRoot)); + return data && {state: data, executionOptimistic: false, finalized: true}; + } + + private async getStateOrBytesByCheckpoint( + checkpoint: CheckpointWithHex + ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { + const checkpointHex = {epoch: checkpoint.epoch, rootHex: checkpoint.rootHex}; + const cachedStateCtx = await this.regen.getCheckpointStateOrBytes(checkpointHex); + if (cachedStateCtx) { + const block = this.getBlockDefaultStatus(checkpoint.root); + const finalizedEpoch = this.getFinalizedCheckpoint().epoch; + return { + state: cachedStateCtx, + executionOptimistic: block != null && isOptimisticBlock(block), + finalized: checkpoint.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH, + }; + } + + return null; + } + + /** Latest weak-subjectivity checkpoint epoch from the head state (lodestar debug). */ + getLatestWeakSubjectivityCheckpointEpoch(): Epoch { + return this.getHeadState().getLatestWeakSubjectivityCheckpointEpoch(); + } + + /** Head-state `latestExecutionPayloadBid` (gloas empty-block detection in range sync); undefined pre-gloas. */ + getHeadLatestExecutionPayloadBid(): gloas.ExecutionPayloadBid | undefined { + const state = this.getHeadState(); + return isStatePostGloas(state) ? (state as IBeaconStateViewGloas).latestExecutionPayloadBid : undefined; + } + + /** Subset of `indices` that are active-or-pending at the current epoch (builder registration filter). */ + getActiveOrPendingValidators(indices: ValidatorIndex[]): Set { + const state = this.getHeadState(); + const currentEpoch = this.clock.currentEpoch; + const kept = new Set(); + for (const index of indices) { + const status = getValidatorStatus(state.getValidator(index), currentEpoch); + if ( + status === "active_exiting" || + status === "active_ongoing" || + status === "active_slashed" || + status === "pending_initialized" || + status === "pending_queued" + ) { + kept.add(index); + } + } + return kept; + } + + /** + * Attestation `source` = the state's *realized* `currentJustifiedCheckpoint` in `attEpoch` (may regen + * forward past head). Not fork-choice's justified checkpoint — the two diverge at an epoch boundary and + * using fork-choice's would yield an invalid source. + */ + async getAttestationSourceCheckpoint(attEpoch: Epoch): Promise { + const state = await this.getHeadStateAtEpoch(attEpoch, RegenCaller.produceAttestationData); + return state.currentJustifiedCheckpoint; + } + + async getBlockRewards(block: BeaconBlock | BlindedBeaconBlock): Promise { + let preState = this.regen.getPreStateSync(block); + + if (preState === null) { + throw Error(`Pre-state is unavailable given block's parent root ${toRootHex(block.parentRoot)}`); + } + + preState = preState.processSlots(block.slot); // Dial preState's slot to block.slot + + const proposerRewards = this.regen.getStateSync(toRootHex(block.stateRoot))?.proposerRewards ?? undefined; + + return preState.computeBlockRewards(block, proposerRewards); + } + + async getAttestationsRewards( + epoch: Epoch, + validatorIds?: (ValidatorIndex | string)[] + ): Promise<{rewards: rewards.AttestationsRewards; executionOptimistic: boolean; finalized: boolean}> { + // We use end slot of (epoch + 1) to ensure we have seen all attestations. On-time or late. + const slot = computeEndSlotAtEpoch(epoch + 1); + // No regen if state not in cache (mirrors former getStateBySlot({allowRegen: false})). + const finalizedBlock = this.forkChoice.getFinalizedBlock(); + const block = slot < finalizedBlock.slot ? null : this.getCanonicalProtoBlockAtSlot(slot); + const cachedState = block && this.regen.getStateSync(block.stateRoot); + + if (!block || !cachedState) { + throw Error(`State is unavailable for slot ${slot}`); + } + + const executionOptimistic = isOptimisticBlock(block); + const finalized = slot === finalizedBlock.slot && finalizedBlock.slot !== GENESIS_SLOT; + const rewards = await cachedState.computeAttestationsRewards(validatorIds); + + return {rewards, executionOptimistic, finalized}; + } + + async getSyncCommitteeRewards( + block: BeaconBlock | BlindedBeaconBlock, + validatorIds?: (ValidatorIndex | string)[] + ): Promise { + let preState = this.regen.getPreStateSync(block); + + if (preState === null) { + throw Error(`Pre-state is unavailable given block's parent root ${toRootHex(block.parentRoot)}`); + } + + preState = preState.processSlots(block.slot); // Dial preState's slot to block.slot + if (!isStatePostAltair(preState)) { + throw new Error("Sync committee rewards are not supported before Altair"); + } + + return preState.computeSyncCommitteeRewards(block, validatorIds ?? []); + } + // TODO - beacon engine: scalar state reads (getBeaconProposer, getValidator, getBalance, // getRandaoMix, getBlockRootAtSlot, getStateRootAtSlot, getShufflingDecisionRoot). Deferred — // signature shape (state vs stateRoot) to be decided alongside Phase 4 bytes-first. @@ -3265,3 +4049,46 @@ export class BeaconEngine implements IBeaconEngine { function toFinalizedProtoSummary(block: ProtoBlock): FinalizedProtoSummary { return {slot: block.slot, blockRoot: block.blockRoot, payloadStatus: block.payloadStatus}; } + +export type StateValidatorIndexResponse = + | {valid: true; validatorIndex: ValidatorIndex} + | {valid: false; code: number; reason: string}; + +/** Resolve a validator id (index | index-string | pubkey hex | pubkey bytes) to its index in `state`. */ +export function getStateValidatorIndex( + id: routes.beacon.ValidatorId | BLSPubkey, + state: IBeaconStateView, + pubkeyCache: PubkeyCache +): StateValidatorIndexResponse { + if (typeof id === "string") { + if (id.startsWith("0x")) { + try { + id = fromHex(id); + } catch (_e) { + return {valid: false, code: 400, reason: "Invalid pubkey hex encoding"}; + } + } else { + id = Number(id); + } + } + + if (typeof id === "number") { + const validatorIndex = id; + if (!Number.isSafeInteger(validatorIndex)) { + return {valid: false, code: 400, reason: "Invalid validator index"}; + } + if (validatorIndex >= state.validatorCount) { + return {valid: false, code: 404, reason: "Validator index from future state"}; + } + return {valid: true, validatorIndex}; + } + + const validatorIndex = pubkeyCache.getIndex(id); + if (validatorIndex === null) { + return {valid: false, code: 404, reason: "Validator pubkey not found in state"}; + } + if (validatorIndex >= state.validatorCount) { + return {valid: false, code: 404, reason: "Validator pubkey from future state"}; + } + return {valid: true, validatorIndex}; +} diff --git a/packages/beacon-node/src/chain/beaconEngine/interface.ts b/packages/beacon-node/src/chain/beaconEngine/interface.ts index ec4a238f5ffe..3aeda4d56eba 100644 --- a/packages/beacon-node/src/chain/beaconEngine/interface.ts +++ b/packages/beacon-node/src/chain/beaconEngine/interface.ts @@ -1,3 +1,4 @@ +import {CompactMultiProof} from "@chainsafe/persistent-merkle-tree"; import {routes} from "@lodestar/api"; import {BeaconConfig} from "@lodestar/config"; import { @@ -10,7 +11,7 @@ import { ProtoBlock, ProtoNode, } from "@lodestar/fork-choice"; -import {ForkName, ForkPostBellatrix} from "@lodestar/params"; +import {ForkName, ForkPostBellatrix, ForkSeq} from "@lodestar/params"; import {DataAvailabilityStatus, EffectiveBalanceIncrements, PubkeyCache} from "@lodestar/state-transition"; import { Attestation, @@ -37,9 +38,11 @@ import { altair, capella, deneb, + electra, fulu, gloas, phase0, + rewards, } from "@lodestar/types"; import {Logger} from "@lodestar/utils"; import {IBeaconEngineDb} from "../../db/index.js"; @@ -60,6 +63,7 @@ import { PayloadAttributesInput, PayloadAttributesWithdrawals, } from "../produceBlock/produceBlockBody.js"; +import {RegenRequest} from "../regen/index.js"; import {SeenBlockInput} from "../seenCache/seenGossipBlockInput.js"; import {ShufflingCache} from "../shufflingCache.js"; import {CPStateDatastore} from "../stateCache/datastore/types.js"; @@ -73,6 +77,8 @@ export type BeaconEngineModules = { opts: IBeaconEngineOptions; config: BeaconConfig; logger: Logger; + /** Engine-owned DB directory (historical-state worker location). */ + dbName: string; metrics: Metrics | null; clock: IClock; pubkeyCache: PubkeyCache; @@ -240,6 +246,22 @@ export type MigrateFinalizedResult = { * collaborators and flows migrates here across later phases. This interface is the contract shared * with the native engine (lodestar-z) — both the JS and native engines implement the same signatures. */ +/** + * Result of an engine API state read. `null` = state not found (API → 404); `invalid` = a state-derived + * validation failure the API maps to `ApiError(code, message)` (keeps the engine HTTP-free); otherwise the + * targeted `data` + meta. `fork` is included where the response meta needs `version`. + */ +export type ApiStateResult = + | {data: T; executionOptimistic: boolean; finalized: boolean; fork?: ForkName} + | {invalid: {code: number; message: string}} + | null; + +/** Like {@link ApiStateResult} but the success branch always carries `fork` (endpoints with `version` meta). */ +export type ApiStateResultWithFork = + | {data: T; executionOptimistic: boolean; finalized: boolean; fork: ForkName} + | {invalid: {code: number; message: string}} + | null; + export interface IBeaconEngine { readonly config: BeaconConfig; // Full fork choice (read + write). The engine owns it; writes are routed here while the flows that @@ -372,6 +394,119 @@ export interface IBeaconEngine { currentEpoch: Epoch ): Promise<{data: routes.validator.PtcDuty[]; dependentRoot: Root; head: ProtoBlock}>; + // State reads (regen-backed; no IBeaconStateView crosses — the engine resolves state internally and + // returns scalars / DTOs). See BLK-3. + dumpCacheSummary(): routes.lodestar.StateCacheItem[]; + dropCache(): void; + dumpRegenQueueItems(): {key: RegenRequest["key"]; args: RegenRequest; addedTimeMs: number}[]; + getActiveValidatorCount(): number; + getHeadPendingCounts(): { + pendingDeposits: number; + pendingPartialWithdrawals: number; + pendingConsolidations: number; + } | null; + validatorMonitorOnceEveryEndOfEpoch(): void; + regenCanAcceptWork(): boolean; + initRegen(): Promise; + getHeadExecutionStateInfo(): {isExecutionStateType: boolean; isMergeTransitionComplete: boolean}; + getLatestWeakSubjectivityCheckpointEpoch(): Epoch; + // Historical (below-finalized) state serving — engine-owned worker (--serveHistoricalState). + loadHistoricalStateRegen(): Promise; + closeHistoricalStateRegen(): Promise; + scrapeHistoricalStateMetrics(): Promise; + getHistoricalStateBySlot( + slot: Slot + ): Promise<{state: Uint8Array; executionOptimistic: boolean; finalized: boolean} | null>; + // API state queries — the engine resolves the state internally (no IBeaconStateView crosses). `id` is + // the resolved stateId (root / slot / checkpoint); the API keeps `resolveStateId` (400 on bad input). + getSerializedState( + id: RootHex | Slot | CheckpointWithHex + ): Promise<{state: Uint8Array; executionOptimistic: boolean; finalized: boolean} | null>; + getFinalizedEffectiveBalances( + finalizedCheckpoint: CheckpointWithHex, + validatorIndices: ValidatorIndex[] + ): Promise; + getStateProof( + id: RootHex | Slot | CheckpointWithHex, + descriptor: Uint8Array + ): Promise<{proof: CompactMultiProof; fork: ForkName} | null>; + getHistoricalSummaries(id: RootHex | Slot | CheckpointWithHex): Promise<{ + slot: Slot; + historicalSummaries: capella.HistoricalSummaries; + proof: Uint8Array[]; + fork: ForkName; + executionOptimistic: boolean; + finalized: boolean; + } | null>; + getIndexedAttestationsForSlashing( + requests: {slot: Slot; epoch: Epoch; attestations: Attestation[]}[], + forkSeq: ForkSeq + ): Promise; + // beacon/state family — each returns the endpoint DTO (or invalid/null); no IBeaconStateView crosses. + getStateRoot(id: RootHex | Slot | CheckpointWithHex): Promise>; + getStateFork(id: RootHex | Slot | CheckpointWithHex): Promise>; + getStateRandao(id: RootHex | Slot | CheckpointWithHex, epoch?: Epoch): Promise>; + getStateFinalityCheckpoints(id: RootHex | Slot | CheckpointWithHex): Promise< + ApiStateResult<{ + currentJustified: phase0.Checkpoint; + previousJustified: phase0.Checkpoint; + finalized: phase0.Checkpoint; + }> + >; + getStateValidators( + id: RootHex | Slot | CheckpointWithHex, + validatorIds: routes.beacon.ValidatorId[], + statuses: string[] + ): Promise>; + getStateValidatorIdentities( + id: RootHex | Slot | CheckpointWithHex, + validatorIds: routes.beacon.ValidatorId[] + ): Promise>; + getStateValidator( + id: RootHex | Slot | CheckpointWithHex, + validatorId: routes.beacon.ValidatorId + ): Promise>; + getStateValidatorBalances( + id: RootHex | Slot | CheckpointWithHex, + validatorIds: routes.beacon.ValidatorId[] + ): Promise>; + getEpochCommittees( + id: RootHex | Slot | CheckpointWithHex, + filters: {epoch?: Epoch; index?: CommitteeIndex; slot?: Slot} + ): Promise>; + getEpochSyncCommittees( + id: RootHex | Slot | CheckpointWithHex, + epoch?: Epoch + ): Promise>; + getStatePendingDeposits( + id: RootHex | Slot | CheckpointWithHex, + returnBytes: boolean + ): Promise>; + getStatePendingPartialWithdrawals( + id: RootHex | Slot | CheckpointWithHex, + returnBytes: boolean + ): Promise>; + getStatePendingConsolidations( + id: RootHex | Slot | CheckpointWithHex, + returnBytes: boolean + ): Promise>; + getStateProposerLookahead( + id: RootHex | Slot | CheckpointWithHex, + returnBytes: boolean + ): Promise>; + getHeadLatestExecutionPayloadBid(): gloas.ExecutionPayloadBid | undefined; + getActiveOrPendingValidators(indices: ValidatorIndex[]): Set; + getAttestationSourceCheckpoint(attEpoch: Epoch): Promise; + getBlockRewards(block: BeaconBlock | BlindedBeaconBlock): Promise; + getAttestationsRewards( + epoch: Epoch, + validatorIds?: (ValidatorIndex | string)[] + ): Promise<{rewards: rewards.AttestationsRewards; executionOptimistic: boolean; finalized: boolean}>; + getSyncCommitteeRewards( + block: BeaconBlock | BlindedBeaconBlock, + validatorIds?: (ValidatorIndex | string)[] + ): Promise; + // Gossip + API validation flows. The first parameter is the message's SSZ bytes (unused by the JS // engine, required by the native engine's bytes-first contract), followed by the deserialized object. Each // returns a `GossipValidationResult` (no throw) so the native engine can return outcomes across FFI. diff --git a/packages/beacon-node/src/chain/beaconEngine/options.ts b/packages/beacon-node/src/chain/beaconEngine/options.ts index 264bb8981831..263047f77f5a 100644 --- a/packages/beacon-node/src/chain/beaconEngine/options.ts +++ b/packages/beacon-node/src/chain/beaconEngine/options.ts @@ -32,6 +32,10 @@ export type IBeaconEngineOptions = ShufflingCacheOpts & StatesArchiveOpts & BlsMultiThreadWorkerPoolOptions & { blsVerifyAllMainThread?: boolean; + /** Serve historical (below-finalized) states via the engine-owned worker thread */ + serveHistoricalState?: boolean; + /** Use the native state view in the historical-state worker */ + nativeStateView?: boolean; /** Gossip block/blob/data-column validation observes (no longer gates) skipped slots */ maxSkipSlots?: number; /** Min number of same-message signature sets to batch in gossip attestation validation */ diff --git a/packages/beacon-node/src/chain/blocks/index.ts b/packages/beacon-node/src/chain/blocks/index.ts index 9a7eec52a8fe..0ec3fa5c36d5 100644 --- a/packages/beacon-node/src/chain/blocks/index.ts +++ b/packages/beacon-node/src/chain/blocks/index.ts @@ -184,6 +184,7 @@ export async function processBlocks( const {preState, postState} = err.type; const preRoot = preState.hashTreeRoot(); const postRoot = postState.hashTreeRoot(); + // TODO beacon-engine: should belong to BeaconEngine this.persistInvalidStateRoot(preState, postState, signedBlock).catch((e) => { this.logger.error( "Error persisting invalid state root objects", diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 0e4def2c314d..63958ec1c8fc 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -8,21 +8,10 @@ import { EFFECTIVE_BALANCE_INCREMENT, type ForkPostFulu, type ForkPostGloas, - GENESIS_SLOT, SLOTS_PER_EPOCH, isForkPostGloas, } from "@lodestar/params"; -import { - IBeaconStateView, - PubkeyCache, - computeEndSlotAtEpoch, - computeEpochAtSlot, - computeStartSlotAtEpoch, - getEffectiveBalancesFromStateBytes, - isStatePostAltair, - isStatePostElectra, - isStatePostGloas, -} from "@lodestar/state-transition"; +import {IBeaconStateView, PubkeyCache, isStatePostGloas} from "@lodestar/state-transition"; import { BeaconBlock, BlindedBeaconBlock, @@ -41,7 +30,6 @@ import { gloas, isBlindedBeaconBlock, phase0, - rewards, ssz, sszTypesFor, } from "@lodestar/types"; @@ -74,13 +62,12 @@ import {IBlsVerifier} from "./bls/index.js"; import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js"; import {ChainEvent, ChainEventEmitter} from "./emitter.js"; import {GetBlobsTracker} from "./GetBlobsTracker.js"; -import {CommonBlockBody, IBeaconChain, ProposerPreparationData, StateGetOpts} from "./interface.js"; +import {CommonBlockBody, IBeaconChain, ProposerPreparationData} from "./interface.js"; import {LightClientServer} from "./lightClient/index.js"; import {IChainOptions} from "./options.js"; import {PrepareNextSlotScheduler} from "./prepareNextSlot.js"; import {AssembledBlockType, BlockType, ProduceResult} from "./produceBlock/index.js"; import {BlockAttributes, PreparedBlockScalars, produceBlockBody} from "./produceBlock/produceBlockBody.js"; -import {QueuedStateRegenerator, RegenCaller} from "./regen/index.js"; import {ReprocessController} from "./reprocess.js"; import {PayloadEnvelopeInput, SeenPayloadEnvelopeInput} from "./seenCache/index.js"; import {SeenBlockInput} from "./seenCache/seenGossipBlockInput.js"; @@ -145,10 +132,6 @@ export class BeaconChain implements IBeaconChain { private headProtoBlock: ProtoBlock; readonly clock: IClock; readonly emitter: ChainEventEmitter; - // TODO - beacon engine: remove this - get regen(): QueuedStateRegenerator { - return this.beaconEngine.regen; - } readonly lightClientServer?: LightClientServer; readonly reprocessController: ReprocessController; readonly archiveStore: ArchiveStore; @@ -290,6 +273,7 @@ export class BeaconChain implements IBeaconChain { opts, config, logger, + dbName, metrics, clock, pubkeyCache, @@ -417,7 +401,7 @@ export class BeaconChain implements IBeaconChain { } async close(): Promise { - await this.archiveStore.close(); + await this.beaconEngine.closeHistoricalStateRegen(); await this.bls.close(); // Since we don't persist unfinalized fork-choice, @@ -438,7 +422,7 @@ export class BeaconChain implements IBeaconChain { } regenCanAcceptWork(): boolean { - return this.regen.canAcceptWork(); + return this.beaconEngine.regenCanAcceptWork(); } blsThreadPoolCanAcceptWork(): boolean { @@ -454,8 +438,9 @@ export class BeaconChain implements IBeaconChain { /** Populate in-memory caches with persisted data. Call at least once on startup */ async loadFromDisk(): Promise { - await this.regen.init(); + await this.beaconEngine.initRegen(); await this.beaconEngine.loadOpPoolFromDisk(); + await this.beaconEngine.loadHistoricalStateRegen(); } /** Persist in-memory data to the DB. Call at least once before stopping the process */ @@ -464,125 +449,6 @@ export class BeaconChain implements IBeaconChain { await this.beaconEngine.persistOpPoolToDisk(); } - // TODO - beacon engine: remove this - getHeadState(): IBeaconStateView { - // head state should always exist - const head = this.beaconEngine.getHead(); - const headState = this.regen.getClosestHeadState(head); - if (!headState) { - throw Error(`headState does not exist for head root=${head.blockRoot} slot=${head.slot}`); - } - return headState; - } - - async getHeadStateAtCurrentEpoch(regenCaller: RegenCaller): Promise { - return this.getHeadStateAtEpoch(this.clock.currentEpoch, regenCaller); - } - - async getHeadStateAtEpoch(epoch: Epoch, regenCaller: RegenCaller): Promise { - // using getHeadState() means we'll use checkpointStateCache if it's available - const headState = this.getHeadState(); - // head state is in the same epoch, or we pulled up head state already from past epoch - if (epoch <= computeEpochAtSlot(headState.slot)) { - // should go to this most of the time - return headState; - } - // only use regen queue if necessary, it'll cache in checkpointStateCache if regen gets through epoch transition - const head = this.beaconEngine.getHead(); - const startSlot = computeStartSlotAtEpoch(epoch); - return this.regen.getBlockSlotState(head, startSlot, {dontTransferCache: true}, regenCaller); - } - - async getStateBySlot( - slot: Slot, - opts?: StateGetOpts - ): Promise<{state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean} | null> { - const finalizedBlock = this.beaconEngine.getFinalizedBlock(); - - if (slot < finalizedBlock.slot) { - // request for finalized state not supported in this API - // fall back to caller to look in db or getHistoricalStateBySlot - return null; - } - - if (opts?.allowRegen) { - // Find closest canonical block to slot, then trigger regen - const block = this.beaconEngine.getCanonicalBlockClosestLteSlot(slot) ?? finalizedBlock; - const state = await this.regen.getBlockSlotState(block, slot, {dontTransferCache: true}, RegenCaller.restApi); - return { - state, - executionOptimistic: isOptimisticBlock(block), - finalized: slot === finalizedBlock.slot && finalizedBlock.slot !== GENESIS_SLOT, - }; - } - - // Just check if state is already in the cache. If it's not dialed to the correct slot, - // do not bother in advancing the state. restApiCanTriggerRegen == false means do no work - const block = this.beaconEngine.getCanonicalProtoBlockAtSlot(slot); - if (!block) { - return null; - } - - const state = this.regen.getStateSync(block.stateRoot); - return ( - state && { - state, - executionOptimistic: isOptimisticBlock(block), - finalized: slot === finalizedBlock.slot && finalizedBlock.slot !== GENESIS_SLOT, - } - ); - } - - async getHistoricalStateBySlot( - slot: number - ): Promise<{state: Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { - if (!this.opts.serveHistoricalState) { - throw Error("Historical state regen is not enabled, set --serveHistoricalState to fetch this data"); - } - - return this.archiveStore.getHistoricalStateBySlot(slot); - } - - async getStateByStateRoot( - stateRoot: RootHex, - opts?: StateGetOpts - ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { - if (opts?.allowRegen) { - const state = await this.regen.getState(stateRoot, RegenCaller.restApi); - const block = this.beaconEngine.getBlockDefaultStatus( - ssz.phase0.BeaconBlockHeader.hashTreeRoot(state.latestBlockHeader) - ); - const finalizedEpoch = this.beaconEngine.getFinalizedCheckpoint().epoch; - return { - state, - executionOptimistic: block != null && isOptimisticBlock(block), - finalized: state.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH, - }; - } - - // TODO: This can only fulfill requests for a very narrow set of roots. - // - very recent states that happen to be in the cache - // - 1 every 100s of states that are persisted in the archive state - - // TODO: This is very inneficient for debug requests of serialized content, since it deserializes to serialize again - const cachedStateCtx = this.regen.getStateSync(stateRoot); - if (cachedStateCtx) { - const block = this.beaconEngine.getBlockDefaultStatus( - ssz.phase0.BeaconBlockHeader.hashTreeRoot(cachedStateCtx.latestBlockHeader) - ); - const finalizedEpoch = this.beaconEngine.getFinalizedCheckpoint().epoch; - return { - state: cachedStateCtx, - executionOptimistic: block != null && isOptimisticBlock(block), - finalized: cachedStateCtx.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH, - }; - } - - // this is mostly useful for a node with `--chain.archiveStateEpochFrequency 1` - const data = await this.beaconEngine.getSerializedStateByRoot(fromHex(stateRoot)); - return data && {state: data, executionOptimistic: false, finalized: true}; - } - async getPersistedCheckpointState(checkpoint?: phase0.Checkpoint): Promise { if (!this.cpStateDatastore) { throw new Error("n-historical-state flag is not enabled"); @@ -599,45 +465,6 @@ export class BeaconChain implements IBeaconChain { return this.cpStateDatastore.read(persistedKey); } - getStateByCheckpoint( - checkpoint: CheckpointWithHex - ): {state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean} | null { - // finalized or justified checkpoint states maynot be available with PersistentCheckpointStateCache, use getCheckpointStateOrBytes() api to get Uint8Array - const checkpointHex = {epoch: checkpoint.epoch, rootHex: checkpoint.rootHex}; - const cachedStateCtx = this.regen.getCheckpointStateSync(checkpointHex); - if (cachedStateCtx) { - const block = this.beaconEngine.getBlockDefaultStatus( - ssz.phase0.BeaconBlockHeader.hashTreeRoot(cachedStateCtx.latestBlockHeader) - ); - const finalizedEpoch = this.beaconEngine.getFinalizedCheckpoint().epoch; - return { - state: cachedStateCtx, - executionOptimistic: block != null && isOptimisticBlock(block), - finalized: cachedStateCtx.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH, - }; - } - - return null; - } - - async getStateOrBytesByCheckpoint( - checkpoint: CheckpointWithHex - ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null> { - const checkpointHex = {epoch: checkpoint.epoch, rootHex: checkpoint.rootHex}; - const cachedStateCtx = await this.regen.getCheckpointStateOrBytes(checkpointHex); - if (cachedStateCtx) { - const block = this.beaconEngine.getBlockDefaultStatus(checkpoint.root); - const finalizedEpoch = this.beaconEngine.getFinalizedCheckpoint().epoch; - return { - state: cachedStateCtx, - executionOptimistic: block != null && isOptimisticBlock(block), - finalized: checkpoint.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH, - }; - } - - return null; - } - async getCanonicalBlockAtSlot( slot: Slot ): Promise<{block: SignedBeaconBlock; executionOptimistic: boolean; finalized: boolean} | null> { @@ -1211,11 +1038,12 @@ export class BeaconChain implements IBeaconChain { // All op-pool sizes are reported by the engine (the pools are engine-internal). metrics.chain.blacklistedBlocks.set(this.blacklistedBlocks.size); - const headState = this.getHeadState(); - if (isStatePostElectra(headState)) { - metrics.pendingDeposits.set(headState.pendingDepositsCount); - metrics.pendingPartialWithdrawals.set(headState.pendingPartialWithdrawalsCount); - metrics.pendingConsolidations.set(headState.pendingConsolidationsCount); + // TODO - beacon engine: move metrics to BeaconEngine? + const pendingCounts = this.beaconEngine.getHeadPendingCounts(); + if (pendingCounts) { + metrics.pendingDeposits.set(pendingCounts.pendingDeposits); + metrics.pendingPartialWithdrawals.set(pendingCounts.pendingPartialWithdrawals); + metrics.pendingConsolidations.set(pendingCounts.pendingConsolidations); } } @@ -1242,7 +1070,7 @@ export class BeaconChain implements IBeaconChain { if (metrics && (slot + 1) % SLOTS_PER_EPOCH === 0) { // On the last slot of the epoch sleep(this.config.SLOT_DURATION_MS / 2) - .then(() => this.validatorMonitor?.onceEveryEndOfEpoch(this.getHeadState())) + .then(() => this.beaconEngine.validatorMonitorOnceEveryEndOfEpoch()) .catch((e) => { if (!isErrorAborted(e)) this.logger.error("Error on validator monitor onceEveryEndOfEpoch", {slot}, e); }); @@ -1259,6 +1087,7 @@ export class BeaconChain implements IBeaconChain { this.logger.verbose("Fork choice justified", {epoch: cp.epoch, root: cp.rootHex}); } + // TODO beacon-engine: move to BeaconEngine private onCheckpoint(this: BeaconChain, _checkpoint: phase0.Checkpoint, state: IBeaconStateView): void { // Defer to not block other checkpoint event handlers, which can cause lightclient update delays callInNextEventLoop(() => { @@ -1312,8 +1141,11 @@ export class BeaconChain implements IBeaconChain { finalizedRoot: finalizedCheckpoint.rootHex, }); - const stateOrBytes = (await this.getStateOrBytesByCheckpoint(finalizedCheckpoint))?.state; - if (!stateOrBytes) { + const effectiveBalancesFromState = await this.beaconEngine.getFinalizedEffectiveBalances( + finalizedCheckpoint, + validatorIndices + ); + if (!effectiveBalancesFromState) { // If even the state is not available, we cannot update the custody group count this.logger.debug("No finalized state or bytes available to update target custody group count", { finalizedEpoch: finalizedCheckpoint.epoch, @@ -1322,11 +1154,7 @@ export class BeaconChain implements IBeaconChain { return; } - if (stateOrBytes instanceof Uint8Array) { - effectiveBalances = getEffectiveBalancesFromStateBytes(this.config, stateOrBytes, validatorIndices); - } else { - effectiveBalances = validatorIndices.map((index) => stateOrBytes.getValidator(index).effectiveBalance ?? 0); - } + effectiveBalances = effectiveBalancesFromState; } const targetCustodyGroupCount = getValidatorsCustodyRequirement(this.config, effectiveBalances); @@ -1367,64 +1195,6 @@ export class BeaconChain implements IBeaconChain { } } } - - async getBlockRewards(block: BeaconBlock | BlindedBeaconBlock): Promise { - let preState = this.regen.getPreStateSync(block); - - if (preState === null) { - throw Error(`Pre-state is unavailable given block's parent root ${toRootHex(block.parentRoot)}`); - } - - preState = preState.processSlots(block.slot); // Dial preState's slot to block.slot - - const proposerRewards = this.regen.getStateSync(toRootHex(block.stateRoot))?.proposerRewards ?? undefined; - - return preState.computeBlockRewards(block, proposerRewards); - } - - async getAttestationsRewards( - epoch: Epoch, - validatorIds?: (ValidatorIndex | string)[] - ): Promise<{rewards: rewards.AttestationsRewards; executionOptimistic: boolean; finalized: boolean}> { - // We use end slot of (epoch + 1) to ensure we have seen all attestations. On-time or late. Any late attestation beyond this slot is not considered - const slot = computeEndSlotAtEpoch(epoch + 1); - const stateResult = await this.getStateBySlot(slot, {allowRegen: false}); // No regen if state not in cache - - if (stateResult === null) { - throw Error(`State is unavailable for slot ${slot}`); - } - - const {executionOptimistic, finalized} = stateResult; - const stateRoot = toRootHex(stateResult.state.hashTreeRoot()); - - const cachedState = this.regen.getStateSync(stateRoot); - - if (cachedState === null) { - throw Error(`State is not in cache for slot ${slot}`); - } - - const rewards = await cachedState.computeAttestationsRewards(validatorIds); - - return {rewards, executionOptimistic, finalized}; - } - - async getSyncCommitteeRewards( - block: BeaconBlock | BlindedBeaconBlock, - validatorIds?: (ValidatorIndex | string)[] - ): Promise { - let preState = this.regen.getPreStateSync(block); - - if (preState === null) { - throw Error(`Pre-state is unavailable given block's parent root ${toRootHex(block.parentRoot)}`); - } - - preState = preState.processSlots(block.slot); // Dial preState's slot to block.slot - if (!isStatePostAltair(preState)) { - throw new Error("Sync committee rewards are not supported before Altair"); - } - - return preState.computeSyncCommitteeRewards(block, validatorIds ?? []); - } } /** Build a CheckpointWithHex from a ProtoBlock's `{epoch, rootHex}` checkpoint fields (no re-hashing). */ diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 96b74d789769..8c4baea482ac 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -1,6 +1,5 @@ import {Type} from "@chainsafe/ssz"; import {BeaconConfig} from "@lodestar/config"; -import {CheckpointWithHex} from "@lodestar/fork-choice"; import {IBeaconStateView, PubkeyCache} from "@lodestar/state-transition"; import { BeaconBlock, @@ -20,7 +19,6 @@ import { deneb, gloas, phase0, - rewards, } from "@lodestar/types"; import {Logger} from "@lodestar/utils"; import {IExecutionBuilder, IExecutionEngine} from "../execution/index.js"; @@ -41,7 +39,6 @@ import {GetBlobsTracker} from "./GetBlobsTracker.js"; import {LightClientServer} from "./lightClient/index.js"; import {IChainOptions} from "./options.js"; import {AssembledBlockType, BlockAttributes, BlockType, ProduceResult} from "./produceBlock/produceBlockBody.js"; -import {IStateRegenerator, RegenCaller} from "./regen/index.js"; import {ReprocessController} from "./reprocess.js"; import {SeenBlockInput} from "./seenCache/seenGossipBlockInput.js"; import {PayloadEnvelopeInput, SeenPayloadEnvelopeInput} from "./seenCache/seenPayloadEnvelopeInput.js"; @@ -85,7 +82,6 @@ export interface IBeaconChain { readonly bls: IBlsVerifier; readonly clock: IClock; readonly emitter: ChainEventEmitter; - readonly regen: IStateRegenerator; readonly lightClientServer?: LightClientServer; readonly reprocessController: ReprocessController; readonly pubkeyCache: PubkeyCache; @@ -124,35 +120,8 @@ export interface IBeaconChain { validatorSeenAtEpoch(index: ValidatorIndex, epoch: Epoch): boolean; - // TODO - beacon engine: remove getHeadState* - getHeadState(): IBeaconStateView; - getHeadStateAtCurrentEpoch(regenCaller: RegenCaller): Promise; - getHeadStateAtEpoch(epoch: Epoch, regenCaller: RegenCaller): Promise; - - getHistoricalStateBySlot( - slot: Slot - ): Promise<{state: Uint8Array; executionOptimistic: boolean; finalized: boolean} | null>; - - /** Returns a local state canonical at `slot` */ - getStateBySlot( - slot: Slot, - opts?: StateGetOpts - ): Promise<{state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean} | null>; - /** Returns a local state by state root */ - getStateByStateRoot( - stateRoot: RootHex, - opts?: StateGetOpts - ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null>; /** Return serialized bytes of a persisted checkpoint state */ getPersistedCheckpointState(checkpoint?: phase0.Checkpoint): Promise; - /** Returns a cached state by checkpoint */ - getStateByCheckpoint( - checkpoint: CheckpointWithHex - ): {state: IBeaconStateView; executionOptimistic: boolean; finalized: boolean} | null; - /** Return state bytes by checkpoint */ - getStateOrBytesByCheckpoint( - checkpoint: CheckpointWithHex - ): Promise<{state: IBeaconStateView | Uint8Array; executionOptimistic: boolean; finalized: boolean} | null>; /** * Since we can have multiple parallel chains, @@ -241,16 +210,6 @@ export interface IBeaconChain { regenCanAcceptWork(): boolean; blsThreadPoolCanAcceptWork(): boolean; - - getBlockRewards(blockRef: BeaconBlock | BlindedBeaconBlock): Promise; - getAttestationsRewards( - epoch: Epoch, - validatorIds?: (ValidatorIndex | string)[] - ): Promise<{rewards: rewards.AttestationsRewards; executionOptimistic: boolean; finalized: boolean}>; - getSyncCommitteeRewards( - blockRef: BeaconBlock | BlindedBeaconBlock, - validatorIds?: (ValidatorIndex | string)[] - ): Promise; } export type SSZObjectType = diff --git a/packages/beacon-node/src/network/network.ts b/packages/beacon-node/src/network/network.ts index db2def4ce921..587a3a0e4c6e 100644 --- a/packages/beacon-node/src/network/network.ts +++ b/packages/beacon-node/src/network/network.ts @@ -165,7 +165,7 @@ export class Network implements INetwork { const events = new NetworkEventBus(); const aggregatorTracker = new AggregatorTracker(); - const activeValidatorCount = chain.getHeadState().activeValidatorCount; + const activeValidatorCount = chain.beaconEngine.getActiveValidatorCount(); const initialStatus = chain.getStatus(); const initialCustodyGroupCount = chain.custodyConfig.targetCustodyGroupCount; diff --git a/packages/beacon-node/src/node/nodejs.ts b/packages/beacon-node/src/node/nodejs.ts index 29189ce3bcce..4523f9d0ed8b 100644 --- a/packages/beacon-node/src/node/nodejs.ts +++ b/packages/beacon-node/src/node/nodejs.ts @@ -316,7 +316,8 @@ export class BeaconNode { const metricsServer = opts.metrics.enabled ? await getHttpMetricsServer(opts.metrics, { register: (metrics as Metrics).register, - getOtherMetrics: async () => Promise.all([network.scrapeMetrics(), chain.archiveStore.scrapeMetrics()]), + getOtherMetrics: async () => + Promise.all([network.scrapeMetrics(), chain.beaconEngine.scrapeHistoricalStateMetrics()]), logger: logger.child({module: LoggerModule.metrics}), }) : null; diff --git a/packages/beacon-node/src/node/notifier.ts b/packages/beacon-node/src/node/notifier.ts index 4f36d4d5d7cf..1b7a4384e9fc 100644 --- a/packages/beacon-node/src/node/notifier.ts +++ b/packages/beacon-node/src/node/notifier.ts @@ -1,12 +1,7 @@ import {BeaconConfig} from "@lodestar/config"; import {ExecutionStatus, ProtoBlock} from "@lodestar/fork-choice"; import {EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH} from "@lodestar/params"; -import { - IBeaconStateView, - computeEpochAtSlot, - computeStartSlotAtEpoch, - isStatePostBellatrix, -} from "@lodestar/state-transition"; +import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {Epoch} from "@lodestar/types"; import {ErrorAborted, Logger, prettyBytes, prettyBytesShort, sleep} from "@lodestar/utils"; import {IBeaconChain} from "../chain/index.js"; @@ -66,9 +61,9 @@ export async function runNodeNotifier(modules: NodeNotifierModules): Promise 0 ? (skippedSlots > 1000 ? `${headInfo.slot} ` : `(slot -${skippedSlots}) `) : ""; const headRow = `head: ${headDiffInfo}${prettyBytes(headInfo.blockRoot)}`; - const executionInfo = getHeadExecutionInfo(config, clockEpoch, headState, headInfo); + const executionInfo = getHeadExecutionInfo( + config, + clockEpoch, + chain.beaconEngine.getHeadExecutionStateInfo(), + headInfo + ); const finalizedCheckpointRow = `finalized: ${prettyBytes(finalizedRoot)}:${finalizedEpoch}`; let nodeState: string[]; @@ -160,7 +160,7 @@ function timeToNextHalfSlot(config: BeaconConfig, chain: IBeaconChain, isFirstTi function getHeadExecutionInfo( config: BeaconConfig, clockEpoch: Epoch, - headState: IBeaconStateView, + execState: {isExecutionStateType: boolean; isMergeTransitionComplete: boolean}, headInfo: ProtoBlock ): string[] { if (clockEpoch < config.BELLATRIX_FORK_EPOCH) { @@ -170,8 +170,8 @@ function getHeadExecutionInfo( const executionStatusStr = headInfo.executionStatus.toLowerCase(); // Add execution status to notifier only if head is on/post bellatrix - if (isStatePostBellatrix(headState) && headState.isExecutionStateType) { - if (headState.isMergeTransitionComplete) { + if (execState.isExecutionStateType) { + if (execState.isMergeTransitionComplete) { const executionPayloadHashInfo = headInfo.executionStatus !== ExecutionStatus.PreMerge ? headInfo.executionPayloadBlockHash : "empty"; const executionPayloadNumberInfo = diff --git a/packages/beacon-node/src/sync/range/range.ts b/packages/beacon-node/src/sync/range/range.ts index f8868d52c4da..440baabee50d 100644 --- a/packages/beacon-node/src/sync/range/range.ts +++ b/packages/beacon-node/src/sync/range/range.ts @@ -1,7 +1,7 @@ import {EventEmitter} from "node:events"; import {StrictEventEmitter} from "strict-event-emitter-types"; import {BeaconConfig} from "@lodestar/config"; -import {IBeaconStateViewGloas, computeStartSlotAtEpoch, isStatePostGloas} from "@lodestar/state-transition"; +import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {Epoch, Status, fulu} from "@lodestar/types"; import {Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils"; import {IBlockInput} from "../../chain/blocks/blockInput/types.js"; @@ -315,10 +315,7 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) { // The first batch of a new sync chain may need to detect whether the parent block was an // gloas "empty" block (no envelope produced). It does so by comparing the first // downloaded block's `bid.parentBlockHash` against the head state's `latestExecutionPayloadBid.blockHash`. - const headState = this.chain.getHeadState(); - const latestBid = isStatePostGloas(headState) - ? (headState as IBeaconStateViewGloas).latestExecutionPayloadBid - : undefined; + const latestBid = this.chain.beaconEngine.getHeadLatestExecutionPayloadBid(); syncChain = new SyncChain( startEpoch, diff --git a/packages/beacon-node/test/e2e/api/impl/lightclient/endpoint.test.ts b/packages/beacon-node/test/e2e/api/impl/lightclient/endpoint.test.ts index 303a5506f0a6..74e054930e40 100644 --- a/packages/beacon-node/test/e2e/api/impl/lightclient/endpoint.test.ts +++ b/packages/beacon-node/test/e2e/api/impl/lightclient/endpoint.test.ts @@ -7,6 +7,7 @@ import {isStatePostAltair} from "@lodestar/state-transition"; import {phase0, ssz} from "@lodestar/types"; import {sleep} from "@lodestar/utils"; import {Validator} from "@lodestar/validator"; +import {BeaconEngine} from "../../../../../src/chain/beaconEngine/index.js"; import {BeaconNode} from "../../../../../src/node/nodejs.js"; import {waitForEvent} from "../../../../utils/events/resolver.js"; import {getDevBeaconNode} from "../../../../utils/node/beacon.js"; @@ -131,7 +132,7 @@ describe.skipIf(process.env.CI)("lightclient api", () => { // Get the actual sync committee root from the head state // The sync committee is computed using a weighted random shuffle, not simple alternation // Since the test starts at Electra, headState is always post-Altair and has currentSyncCommittee - const headState = bn.chain.getHeadState(); + const headState = (bn.chain.beaconEngine as BeaconEngine).getHeadState(); if (!isStatePostAltair(headState)) { throw new Error("Expected Altair state in lightclient test"); } diff --git a/packages/beacon-node/test/e2e/doppelganger/doppelganger.test.ts b/packages/beacon-node/test/e2e/doppelganger/doppelganger.test.ts index 2ddc38507d91..4e5a6ad4fa29 100644 --- a/packages/beacon-node/test/e2e/doppelganger/doppelganger.test.ts +++ b/packages/beacon-node/test/e2e/doppelganger/doppelganger.test.ts @@ -6,6 +6,7 @@ import {LogLevel, TestLoggerOpts, testLogger} from "@lodestar/logger/test-utils" import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {BLSPubkey, Epoch, Slot, phase0, ssz} from "@lodestar/types"; import {Validator} from "@lodestar/validator"; +import {BeaconEngine} from "../../../src/chain/beaconEngine/index.js"; import {BeaconNode} from "../../../src/node/index.js"; import {ClockEvent} from "../../../src/util/clock.js"; import {waitForEvent} from "../../utils/events/resolver.js"; @@ -124,7 +125,7 @@ describe.skip("doppelganger / doppelganger test", () => { }); const {beaconNode: bn2, validators} = await createBNAndVC({ - genesisTime: bn.chain.getHeadState().genesisTime, + genesisTime: (bn.chain.beaconEngine as BeaconEngine).getHeadState().genesisTime, }); await connect(bn2.network, bn.network); @@ -203,7 +204,7 @@ describe.skip("doppelganger / doppelganger test", () => { }); const {beaconNode: bn2, validators} = await createBNAndVC({ - genesisTime: bn.chain.getHeadState().genesisTime, + genesisTime: (bn.chain.beaconEngine as BeaconEngine).getHeadState().genesisTime, doppelgangerProtection: false, }); diff --git a/packages/beacon-node/test/e2e/sync/checkpointSync.test.ts b/packages/beacon-node/test/e2e/sync/checkpointSync.test.ts index d0cc1e26483d..01b126a0c38d 100644 --- a/packages/beacon-node/test/e2e/sync/checkpointSync.test.ts +++ b/packages/beacon-node/test/e2e/sync/checkpointSync.test.ts @@ -7,6 +7,7 @@ import {LogLevel, TestLoggerOpts, testLogger} from "@lodestar/logger/test-utils" import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {BeaconStateView, DataAvailabilityStatus} from "@lodestar/state-transition"; import {Slot} from "@lodestar/types"; +import {BeaconEngine} from "../../../src/chain/beaconEngine/index.js"; import {ChainEvent} from "../../../src/chain/index.js"; import {ReorgedForkChoice} from "../../mocks/fork-choice/reorg.js"; import {waitForEvent} from "../../utils/events/resolver.js"; @@ -220,11 +221,14 @@ describe("sync / checkpoint sync optimistic flow for gloas", () => { rootHex: finalizedCp.rootHex, }); - const finalizedStateRes = bn.chain.getStateByCheckpoint(finalizedCp); + const finalizedStateRes = (bn.chain.beaconEngine as BeaconEngine).regen.getCheckpointStateSync({ + epoch: finalizedCp.epoch, + rootHex: finalizedCp.rootHex, + }); if (!finalizedStateRes) { throw Error("Node A finalized checkpoint state not available"); } - const anchorState = (finalizedStateRes.state as BeaconStateView).cachedState; + const anchorState = (finalizedStateRes as BeaconStateView).cachedState; expect(anchorState.slot, "Anchor state should be at the epoch-3 boundary slot").toEqual(REORGED_SLOT); expect( anchorState.latestBlockHeader.slot, @@ -351,11 +355,14 @@ describe("sync / checkpoint sync optimistic flow for gloas", () => { EPOCH_3_BOUNDARY_SLOT ); - const finalizedStateRes = bn.chain.getStateByCheckpoint(finalizedCp); + const finalizedStateRes = (bn.chain.beaconEngine as BeaconEngine).regen.getCheckpointStateSync({ + epoch: finalizedCp.epoch, + rootHex: finalizedCp.rootHex, + }); if (!finalizedStateRes) { throw Error("Node A finalized checkpoint state not available"); } - const anchorState = (finalizedStateRes.state as BeaconStateView).cachedState; + const anchorState = (finalizedStateRes as BeaconStateView).cachedState; expect(anchorState.slot, "Anchor state should be at the epoch-3 boundary slot").toEqual(EPOCH_3_BOUNDARY_SLOT); expect( anchorState.latestBlockHeader.slot, diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index 71e0008b9b9d..9d949f55bd4b 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -2,7 +2,7 @@ import {Mock, Mocked, vi} from "vitest"; import {BeaconConfig, ChainForkConfig} from "@lodestar/config"; import {config as defaultConfig} from "@lodestar/config/default"; import {EpochDifference, ForkChoice, ProtoBlock} from "@lodestar/fork-choice"; -import {createPubkeyCache} from "@lodestar/state-transition"; +import {IBeaconStateView, createPubkeyCache} from "@lodestar/state-transition"; import {Logger} from "@lodestar/utils"; import {BeaconEngine} from "../../src/chain/beaconEngine/beaconEngine.js"; import {BeaconProposerCache} from "../../src/chain/beaconProposerCache.js"; @@ -58,6 +58,10 @@ export type MockedBeaconChain = Mocked & { seenBlockInputCache: Mocked; shufflingCache: Mocked; regen: Mocked; + // Engine state-read methods surfaced on the flat mock (the mock doubles as the engine). No longer on + // the facade after BLK-3 — stubbed here so validation tests can control the head state. + getHeadState: Mock<() => IBeaconStateView>; + getHeadStateAtCurrentEpoch: Mock<() => Promise>; bls: { verifySignatureSets: Mock<() => boolean>; verifySignatureSetsSameMessage: Mock<() => boolean>; diff --git a/packages/beacon-node/test/sim/electra-interop.test.ts b/packages/beacon-node/test/sim/electra-interop.test.ts index 797bad456c84..9defe14a31bf 100644 --- a/packages/beacon-node/test/sim/electra-interop.test.ts +++ b/packages/beacon-node/test/sim/electra-interop.test.ts @@ -10,6 +10,7 @@ import {Epoch, Slot, electra} from "@lodestar/types"; import {LogLevel, sleep} from "@lodestar/utils"; import {ValidatorProposerConfig} from "@lodestar/validator"; import {BeaconRestApiServerOpts} from "../../src/api/index.js"; +import {BeaconEngine} from "../../src/chain/beaconEngine/index.js"; import {defaultExecutionEngineHttpOpts} from "../../src/execution/engine/http.js"; import {ExecutionPayloadStatus, PayloadAttributes} from "../../src/execution/engine/interface.js"; import {bytesToData, dataToBytes} from "../../src/execution/engine/utils.js"; @@ -359,7 +360,7 @@ describe("executionEngine / ExecutionEngineHttp", () => { await waitForSlot(bn, 5); // Expect new validator to be in unfinalized cache, in state.validators and not in finalized cache - let headState = bn.chain.getHeadState() as BeaconStateView; + let headState = (bn.chain.beaconEngine as BeaconEngine).getHeadState() as BeaconStateView; let epochCtx = headState.cachedState.epochCtx; if (headState.validatorCount !== 33 || headState.getAllBalances().length !== 33) { throw Error("New validator is not reflected in the beacon state at slot 5"); @@ -385,7 +386,7 @@ describe("executionEngine / ExecutionEngineHttp", () => { await sleep(500); // Check if new validator is in finalized cache - headState = bn.chain.getHeadState() as BeaconStateView; + headState = (bn.chain.beaconEngine as BeaconEngine).getHeadState() as BeaconStateView; epochCtx = headState.cachedState.epochCtx; if (headState.validatorCount !== 33 || headState.getAllBalances().length !== 33) { 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 0f21b7edbfb0..8700629fac11 100644 --- a/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts +++ b/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts @@ -170,7 +170,7 @@ const fastConfirmationTest = logger.debug(`Step ${i}/${stepsLen} attestation`, {root: step.attestation, valid: Boolean(step.valid)}); const attestation = testcase.attestations.get(step.attestation); if (!attestation) throw Error(`No attestation ${step.attestation}`); - const headState = chain.getHeadState(); + const headState = chain.beaconEngine.getHeadState(); const attDataRootHex = toHexString(sszTypesFor(fork).AttestationData.hashTreeRoot(attestation.data)); const attEpoch = computeEpochAtSlot(attestation.data.slot); const decisionRoot = headState.getShufflingDecisionRoot(attEpoch); diff --git a/packages/beacon-node/test/spec/presets/fork_choice.test.ts b/packages/beacon-node/test/spec/presets/fork_choice.test.ts index 0ab6ffe72887..30b41ef3d045 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.test.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.test.ts @@ -187,7 +187,7 @@ const forkChoiceTest = logger.debug(`Step ${i}/${stepsLen} attestation`, {root: step.attestation, valid: isValid}); const attestation = testcase.attestations.get(step.attestation); if (!attestation) throw Error(`No attestation ${step.attestation}`); - const headState = chain.getHeadState() as BeaconStateView; + const headState = chain.beaconEngine.getHeadState() as BeaconStateView; const attDataRootHex = toHexString(sszTypesFor(fork).AttestationData.hashTreeRoot(attestation.data)); const indexedAttestation = headState.cachedState.epochCtx.getIndexedAttestation( ForkSeq[fork], @@ -232,7 +232,7 @@ const forkChoiceTest = } if (protoBlock.slot === payloadAttestationMessage.data.slot) { - const blockState = await chain.regen.getBlockSlotState( + const blockState = await chain.beaconEngine.regen.getBlockSlotState( protoBlock, payloadAttestationMessage.data.slot, {dontTransferCache: true}, @@ -489,7 +489,7 @@ const forkChoiceTest = // Verify envelope against the state const protoBlock = chain.beaconEngine.forkChoice.getBlockHexDefaultStatus(beaconBlockRoot); if (!protoBlock) throw Error(`Block not found for root ${beaconBlockRoot}`); - const blockState = await chain.regen.getBlockSlotState( + const blockState = await chain.beaconEngine.regen.getBlockSlotState( protoBlock, protoBlock.slot, {dontTransferCache: true}, diff --git a/packages/beacon-node/test/spec/utils/gossipValidation.ts b/packages/beacon-node/test/spec/utils/gossipValidation.ts index ed58e9f89001..46c83c874a16 100644 --- a/packages/beacon-node/test/spec/utils/gossipValidation.ts +++ b/packages/beacon-node/test/spec/utils/gossipValidation.ts @@ -482,7 +482,7 @@ export async function runGossipValidationTest( } const postState = computePostState(parentState, signedBlock, fork); - const expectedProposerIndex: number | null = chain.getHeadState().getBeaconProposerOrNull(slot); + const expectedProposerIndex: number | null = chain.beaconEngine.getHeadState().getBeaconProposerOrNull(slot); if (blockEntry.failed) { // payload_status === "VALID" (filtered above) diff --git a/packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts b/packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts index 0eb499465142..99b7a10ec51e 100644 --- a/packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts +++ b/packages/beacon-node/test/unit/api/impl/beacon/state/utils.test.ts @@ -1,7 +1,7 @@ import {describe, expect, it} from "vitest"; import {toHexString} from "@chainsafe/ssz"; import {BeaconStateView} from "@lodestar/state-transition"; -import {getStateValidatorIndex} from "../../../../../../src/api/impl/beacon/state/utils.js"; +import {getStateValidatorIndex} from "../../../../../../src/chain/beaconEngine/index.js"; import {generateCachedAltairState} from "../../../../../utils/state.js"; describe("beacon state api utils", () => { diff --git a/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts b/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts index c677bbed52d6..62354e95042a 100644 --- a/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/attesterSlashing.test.ts @@ -19,7 +19,7 @@ describe("GossipMessageValidator", () => { opPool = chainStub.opPool; const state = new BeaconStateView(generateCachedState()); - vi.spyOn(chainStub, "getHeadState").mockReturnValue(state); + chainStub.getHeadState.mockReturnValue(state); vi.spyOn(opPool, "hasSeenAttesterSlashing"); }); diff --git a/packages/beacon-node/test/unit/chain/validation/blsToExecutionChange.test.ts b/packages/beacon-node/test/unit/chain/validation/blsToExecutionChange.test.ts index c61f79d2ad49..9dc105be7a7d 100644 --- a/packages/beacon-node/test/unit/chain/validation/blsToExecutionChange.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/blsToExecutionChange.test.ts @@ -85,7 +85,7 @@ describe("validate bls to execution change", () => { beforeEach(() => { chainStub = getMockedBeaconChain({config}); opPool = chainStub.opPool; - vi.spyOn(chainStub, "getHeadState").mockReturnValue(state); + chainStub.getHeadState.mockReturnValue(state); vi.spyOn(chainStub, "getHeadStateAtCurrentEpoch"); vi.spyOn(opPool, "hasSeenBlsToExecutionChange"); }); diff --git a/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts b/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts index 7fa3b1c99e6d..836509e1dd4b 100644 --- a/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/proposerSlashing.test.ts @@ -19,7 +19,7 @@ describe("validate proposer slashing", () => { opPool = chainStub.opPool; const state = new BeaconStateView(generateCachedState()); - vi.spyOn(chainStub, "getHeadState").mockReturnValue(state); + chainStub.getHeadState.mockReturnValue(state); vi.spyOn(opPool, "hasSeenProposerSlashing"); }); diff --git a/packages/beacon-node/test/unit/chain/validation/voluntaryExit.test.ts b/packages/beacon-node/test/unit/chain/validation/voluntaryExit.test.ts index 2503a8d28c23..a9036bce220a 100644 --- a/packages/beacon-node/test/unit/chain/validation/voluntaryExit.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/voluntaryExit.test.ts @@ -68,7 +68,7 @@ describe("validate voluntary exit", () => { state = new TestBeaconStateView(createCachedBeaconStateTest(originalState.clone(), config)); chainStub = getMockedBeaconChain({config}); opPool = chainStub.opPool; - vi.spyOn(chainStub, "getHeadStateAtCurrentEpoch").mockResolvedValue(state); + chainStub.getHeadStateAtCurrentEpoch.mockResolvedValue(state); vi.spyOn(opPool, "hasSeenBlsToExecutionChange"); vi.spyOn(opPool, "hasSeenVoluntaryExit"); chainStub.bls.verifySignatureSets.mockResolvedValue(true); @@ -117,7 +117,7 @@ describe("validate voluntary exit", () => { state.setValidator(0, inactiveValidator); - vi.spyOn(chainStub, "getHeadStateAtCurrentEpoch").mockResolvedValue(state); + chainStub.getHeadStateAtCurrentEpoch.mockResolvedValue(state); await expectRejectedWithLodestarError( validateGossipVoluntaryExit.call(chainStub.beaconEngine, signedVoluntaryExit), @@ -135,7 +135,7 @@ describe("validate voluntary exit", () => { state.setValidator(0, exitedValidator); - vi.spyOn(chainStub, "getHeadStateAtCurrentEpoch").mockResolvedValue(state); + chainStub.getHeadStateAtCurrentEpoch.mockResolvedValue(state); await expectRejectedWithLodestarError( validateGossipVoluntaryExit.call(chainStub.beaconEngine, signedVoluntaryExit), @@ -151,7 +151,7 @@ describe("validate voluntary exit", () => { state.setValidator(0, recentlyActivated); - vi.spyOn(chainStub, "getHeadStateAtCurrentEpoch").mockResolvedValue(state); + chainStub.getHeadStateAtCurrentEpoch.mockResolvedValue(state); await expectRejectedWithLodestarError( validateGossipVoluntaryExit.call(chainStub.beaconEngine, signedVoluntaryExit), diff --git a/packages/beacon-node/test/utils/node/simTest.ts b/packages/beacon-node/test/utils/node/simTest.ts index 1c230c1e3fc8..add87ef9fcb4 100644 --- a/packages/beacon-node/test/utils/node/simTest.ts +++ b/packages/beacon-node/test/utils/node/simTest.ts @@ -11,6 +11,7 @@ import { import {BeaconBlock, Epoch, Slot} from "@lodestar/types"; import {Checkpoint} from "@lodestar/types/phase0"; import {Logger, mapValues, toRootHex} from "@lodestar/utils"; +import {BeaconEngine} from "../../../src/chain/beaconEngine/index.js"; import {ChainEvent, HeadEventData} from "../../../src/chain/index.js"; import {RegenCaller} from "../../../src/chain/regen/index.js"; import {BeaconNode} from "../../../src/index.js"; @@ -68,7 +69,7 @@ export function simTestInfoTracker(bn: BeaconNode, logger: Logger): () => void { if (checkpoint.epoch <= lastSeenEpoch) return; lastSeenEpoch = checkpoint.epoch; - const checkpointState = bn.chain.regen.getCheckpointStateSync({ + const checkpointState = (bn.chain.beaconEngine as BeaconEngine).regen.getCheckpointStateSync({ epoch: checkpoint.epoch, rootHex: toRootHex(checkpoint.root), }); @@ -77,7 +78,10 @@ export function simTestInfoTracker(bn: BeaconNode, logger: Logger): () => void { } const lastSlot = computeStartSlotAtEpoch(checkpoint.epoch) - 1; const lastStateRoot = checkpointState.getStateRootAtSlot(lastSlot); - const lastState = await bn.chain.regen.getState(toRootHex(lastStateRoot), RegenCaller.onForkChoiceFinalized); + const lastState = await (bn.chain.beaconEngine as BeaconEngine).regen.getState( + toRootHex(lastStateRoot), + RegenCaller.onForkChoiceFinalized + ); logParticipation(lastState); }