From fb26c172a416d688258010c630e5302c6d40ecba Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 11 Dec 2025 03:56:55 +0530 Subject: [PATCH 01/29] fix : remove build() method --- .../beacon-node/src/chain/shufflingCache.ts | 31 +------------------ .../src/cache/epochTransitionCache.ts | 4 --- .../src/util/epochShuffling.ts | 5 --- 3 files changed, 1 insertion(+), 39 deletions(-) diff --git a/packages/beacon-node/src/chain/shufflingCache.ts b/packages/beacon-node/src/chain/shufflingCache.ts index bdedddacf6db..b7847e9e4437 100644 --- a/packages/beacon-node/src/chain/shufflingCache.ts +++ b/packages/beacon-node/src/chain/shufflingCache.ts @@ -1,11 +1,4 @@ -import { - BeaconStateAllForks, - EpochShuffling, - IShufflingCache, - ShufflingBuildProps, - computeEpochShuffling, - computeEpochShufflingAsync, -} from "@lodestar/state-transition"; +import {EpochShuffling, IShufflingCache, ShufflingBuildProps, computeEpochShuffling} from "@lodestar/state-transition"; import {Epoch, RootHex} from "@lodestar/types"; import {LodestarError, Logger, MapDef, pruneSetToMax} from "@lodestar/utils"; import {Metrics} from "../metrics/metrics.js"; @@ -170,28 +163,6 @@ export class ShufflingCache implements IShufflingCache { return shuffling as T extends ShufflingBuildProps ? EpochShuffling : EpochShuffling | null; } - /** - * Queue asynchronous build for an EpochShuffling, triggered from state-transition - */ - build(epoch: number, decisionRoot: string, state: BeaconStateAllForks, activeIndices: Uint32Array): void { - this.insertPromise(epoch, decisionRoot); - /** - * TODO: (@matthewkeil) This will get replaced by a proper build queue and a worker to do calculations - * on a NICE thread - */ - const timer = this.metrics?.shufflingCache.shufflingCalculationTime.startTimer({source: "build"}); - computeEpochShufflingAsync(state, activeIndices, epoch) - .then((shuffling) => { - this.set(shuffling, decisionRoot); - }) - .catch((err) => - this.logger?.error(`error building shuffling for epoch ${epoch} at decisionRoot ${decisionRoot}`, {}, err) - ) - .finally(() => { - timer?.(); - }); - } - /** * Add an EpochShuffling to the ShufflingCache. If a promise for the shuffling is present it will * resolve the promise with the built shuffling diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index a3ec69956162..96fee9825e46 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -380,7 +380,6 @@ export function beforeProcessEpoch( }); // Trigger async build of shuffling for epoch after next (nextShuffling post epoch transition) - const epochAfterNext = state.epochCtx.nextEpoch + 1; // cannot call calculateShufflingDecisionRoot here because spec prevent getting current slot // as a decision block. we are part way through the transition though and this was added in // process slot beforeProcessEpoch happens so it available and valid @@ -397,9 +396,6 @@ export function beforeProcessEpoch( } const asyncShufflingCalculation = opts?.asyncShufflingCalculation ?? false; - if (asyncShufflingCalculation) { - state.epochCtx.shufflingCache?.build(epochAfterNext, nextShufflingDecisionRoot, state, nextShufflingActiveIndices); - } if (totalActiveStakeByIncrement < 1) { totalActiveStakeByIncrement = 1; diff --git a/packages/state-transition/src/util/epochShuffling.ts b/packages/state-transition/src/util/epochShuffling.ts index 6d0acbd32455..cb65628de1c2 100644 --- a/packages/state-transition/src/util/epochShuffling.ts +++ b/packages/state-transition/src/util/epochShuffling.ts @@ -50,11 +50,6 @@ export interface IShufflingCache { * not been queued nor a shuffling was calculated. */ get(epoch: Epoch, decisionRoot: RootHex): Promise; - - /** - * Queue asynchronous build for an EpochShuffling - */ - build(epoch: Epoch, decisionRoot: RootHex, state: BeaconStateAllForks, activeIndices: Uint32Array): void; } /** From 4353bb022cb15830b9ae37968faed3126200d57f Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 11 Dec 2025 04:00:37 +0530 Subject: [PATCH 02/29] fix : add shuffling to cache --- packages/beacon-node/src/chain/shufflingCache.ts | 2 +- packages/beacon-node/test/unit/chain/shufflingCache.test.ts | 4 ++-- packages/state-transition/src/util/epochShuffling.ts | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/beacon-node/src/chain/shufflingCache.ts b/packages/beacon-node/src/chain/shufflingCache.ts index b7847e9e4437..a93220c38c93 100644 --- a/packages/beacon-node/src/chain/shufflingCache.ts +++ b/packages/beacon-node/src/chain/shufflingCache.ts @@ -167,7 +167,7 @@ export class ShufflingCache implements IShufflingCache { * Add an EpochShuffling to the ShufflingCache. If a promise for the shuffling is present it will * resolve the promise with the built shuffling */ - private set(shuffling: EpochShuffling, decisionRoot: string): void { + set(shuffling: EpochShuffling, decisionRoot: string): void { const shufflingAtEpoch = this.itemsByDecisionRootByEpoch.getOrDefault(shuffling.epoch); // if a pending shuffling promise exists, resolve it const cacheItem = shufflingAtEpoch.get(decisionRoot); diff --git a/packages/beacon-node/test/unit/chain/shufflingCache.test.ts b/packages/beacon-node/test/unit/chain/shufflingCache.test.ts index b8a9602ea6ce..f9380c67781c 100644 --- a/packages/beacon-node/test/unit/chain/shufflingCache.test.ts +++ b/packages/beacon-node/test/unit/chain/shufflingCache.test.ts @@ -29,7 +29,7 @@ describe("ShufflingCache", () => { shufflingCache.insertPromise(currentEpoch, "0x00"); expect(await shufflingCache.get(currentEpoch, currentDecisionRoot)).toEqual(state.epochCtx.currentShuffling); // insert shuffling at other epochs does prune the cache - shufflingCache["set"](state.epochCtx.previousShuffling, state.epochCtx.previousDecisionRoot); + shufflingCache.set(state.epochCtx.previousShuffling, state.epochCtx.previousDecisionRoot); // the current shuffling is not available anymore expect(await shufflingCache.get(currentEpoch, currentDecisionRoot)).toBeNull(); }); @@ -40,7 +40,7 @@ describe("ShufflingCache", () => { shufflingCache.insertPromise(previousEpoch, previousDecisionRoot); const shufflingRequest0 = shufflingCache.get(previousEpoch, previousDecisionRoot); const shufflingRequest1 = shufflingCache.get(previousEpoch, previousDecisionRoot); - shufflingCache["set"](state.epochCtx.previousShuffling, previousDecisionRoot); + shufflingCache.set(state.epochCtx.previousShuffling, previousDecisionRoot); expect(await shufflingRequest0).toEqual(state.epochCtx.previousShuffling); expect(await shufflingRequest1).toEqual(state.epochCtx.previousShuffling); }); diff --git a/packages/state-transition/src/util/epochShuffling.ts b/packages/state-transition/src/util/epochShuffling.ts index cb65628de1c2..a533a0a9e404 100644 --- a/packages/state-transition/src/util/epochShuffling.ts +++ b/packages/state-transition/src/util/epochShuffling.ts @@ -50,6 +50,11 @@ export interface IShufflingCache { * not been queued nor a shuffling was calculated. */ get(epoch: Epoch, decisionRoot: RootHex): Promise; + + /** + * Add an EpochShuffling to the ShufflingCache. + */ + set(shuffling: EpochShuffling, decisionRoot: RootHex): void; } /** From 27b506f1747d2b0f432c7dccadc652c5caab8979 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 11 Dec 2025 04:03:40 +0530 Subject: [PATCH 03/29] fix : reverting async shuffling refactor --- .../beacon-node/src/chain/prepareNextSlot.ts | 7 +--- .../beacon-node/src/chain/regen/interface.ts | 4 --- .../state-transition/src/cache/epochCache.ts | 33 +++---------------- .../src/cache/epochTransitionCache.ts | 13 -------- 4 files changed, 5 insertions(+), 52 deletions(-) diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index 43fbb0226a48..58745ea6c925 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -118,12 +118,7 @@ export class PrepareNextSlotScheduler { // 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 - // - // Shuffling calculation will be done asynchronously when passing asyncShufflingCalculation=true. Shuffling will be queued in - // beforeProcessEpoch and should theoretically be ready immediately after the synchronous epoch transition finished and the - // event loop is free. In long periods of non-finality too many forks will cause the shufflingCache to throw an error for - // too many queued shufflings so only run async during normal epoch transition. See issue ChainSafe/lodestar#7244 - {dontTransferCache: !isEpochTransition, asyncShufflingCalculation: true}, + {dontTransferCache: !isEpochTransition}, RegenCaller.precomputeEpoch ); diff --git a/packages/beacon-node/src/chain/regen/interface.ts b/packages/beacon-node/src/chain/regen/interface.ts index dcb604747f4e..c027565a81a1 100644 --- a/packages/beacon-node/src/chain/regen/interface.ts +++ b/packages/beacon-node/src/chain/regen/interface.ts @@ -31,10 +31,6 @@ export enum RegenFnName { export type StateRegenerationOpts = { dontTransferCache: boolean; - /** - * Do not queue shuffling calculation async. Forces sync JIT calculation in afterProcessEpoch if not passed as `true` - */ - asyncShufflingCalculation?: boolean; }; export interface IStateRegenerator extends IStateRegeneratorInternal { diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 63b23e05204d..2768e7849c85 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -678,35 +678,10 @@ export class EpochCache { this.nextDecisionRoot = epochTransitionCache.nextShufflingDecisionRoot; this.nextActiveIndices = epochTransitionCache.nextShufflingActiveIndices; if (this.shufflingCache) { - if (!epochTransitionCache.asyncShufflingCalculation) { - this.nextShuffling = this.shufflingCache.getSync(epochAfterUpcoming, this.nextDecisionRoot, { - state, - activeIndices: this.nextActiveIndices, - }); - } else { - this.nextShuffling = null; - // This promise will resolve immediately after the synchronous code of the state-transition runs. Until - // the build is done on a worker thread it will be calculated immediately after the epoch transition - // completes. Once the work is done concurrently it should be ready by time this get runs so the promise - // will resolve directly on the next spin of the event loop because the epoch transition and shuffling take - // about the same time to calculate so theoretically its ready now. Do not await here though in case it - // is not ready yet as the transition must not be asynchronous. - this.shufflingCache - .get(epochAfterUpcoming, this.nextDecisionRoot) - .then((shuffling) => { - if (!shuffling) { - throw new Error("EpochShuffling not returned from get in afterProcessEpoch"); - } - this.nextShuffling = shuffling; - }) - .catch((err) => { - this.shufflingCache?.logger?.error( - "EPOCH_CONTEXT_SHUFFLING_BUILD_ERROR", - {epoch: epochAfterUpcoming, decisionRoot: epochTransitionCache.nextShufflingDecisionRoot}, - err - ); - }); - } + this.nextShuffling = this.shufflingCache.getSync(epochAfterUpcoming, this.nextDecisionRoot, { + state, + activeIndices: this.nextActiveIndices, + }); } else { // Only for testing. shufflingCache should always be available in prod this.nextShuffling = computeEpochShuffling(state, this.nextActiveIndices, epochAfterUpcoming); diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index 96fee9825e46..e376720c332f 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -32,10 +32,6 @@ export type EpochTransitionCacheOpts = { * Assert progressive balances the same to EpochTransitionCache */ assertCorrectProgressiveBalances?: boolean; - /** - * Do not queue shuffling calculation async. Forces sync JIT calculation in afterProcessEpoch - */ - asyncShufflingCalculation?: boolean; }; /** @@ -179,12 +175,6 @@ export interface EpochTransitionCache { */ nextEpochTotalActiveBalanceByIncrement: number; - /** - * Compute the shuffling sync or async. Defaults to synchronous. Need to pass `true` with the - * `EpochTransitionCacheOpts` - */ - asyncShufflingCalculation: boolean; - /** * Track by validator index if it's active in the prev epoch. * Used in metrics @@ -395,8 +385,6 @@ export function beforeProcessEpoch( nextShufflingActiveIndices[i] = nextEpochShufflingActiveValidatorIndices[i]; } - const asyncShufflingCalculation = opts?.asyncShufflingCalculation ?? false; - if (totalActiveStakeByIncrement < 1) { totalActiveStakeByIncrement = 1; } else if (totalActiveStakeByIncrement >= Number.MAX_SAFE_INTEGER) { @@ -522,7 +510,6 @@ export function beforeProcessEpoch( indicesToEject, nextShufflingDecisionRoot, nextShufflingActiveIndices, - asyncShufflingCalculation, // to be updated in processEffectiveBalanceUpdates nextEpochTotalActiveBalanceByIncrement: 0, isActivePrevEpoch, From bbdf6925237b9413cb78de90d19b0bd251c8b091 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 11 Dec 2025 14:11:52 +0530 Subject: [PATCH 04/29] chore: update comments --- packages/state-transition/src/cache/epochTransitionCache.ts | 2 +- packages/state-transition/src/util/epochShuffling.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index e376720c332f..e694eb957936 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -369,7 +369,7 @@ export function beforeProcessEpoch( } }); - // Trigger async build of shuffling for epoch after next (nextShuffling post epoch transition) + // Prepare shuffling data for epoch after next (nextShuffling post epoch transition) // cannot call calculateShufflingDecisionRoot here because spec prevent getting current slot // as a decision block. we are part way through the transition though and this was added in // process slot beforeProcessEpoch happens so it available and valid diff --git a/packages/state-transition/src/util/epochShuffling.ts b/packages/state-transition/src/util/epochShuffling.ts index a533a0a9e404..8f57e0274a28 100644 --- a/packages/state-transition/src/util/epochShuffling.ts +++ b/packages/state-transition/src/util/epochShuffling.ts @@ -52,7 +52,8 @@ export interface IShufflingCache { get(epoch: Epoch, decisionRoot: RootHex): Promise; /** - * Add an EpochShuffling to the ShufflingCache. + * Add an EpochShuffling to the ShufflingCache. If a promise for the shuffling + * is present it will resolve the promise with the built shuffling. */ set(shuffling: EpochShuffling, decisionRoot: RootHex): void; } From 97a13726df24296e7d2f4952d60d5c3fa62698b3 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Tue, 6 Jan 2026 20:17:30 +0530 Subject: [PATCH 05/29] Remove ShufflingCache dependency from state-transition package --- .../src/chain/blocks/importBlock.ts | 7 ++ packages/beacon-node/src/chain/chain.ts | 8 +- .../beacon-node/src/chain/shufflingCache.ts | 37 ++---- .../state-transition/src/cache/epochCache.ts | 116 ++++-------------- .../state-transition/src/cache/stateCache.ts | 3 +- .../src/epoch/processProposerLookahead.ts | 8 +- .../src/util/epochShuffling.ts | 44 +------ 7 files changed, 46 insertions(+), 177 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 2d5e8fbf50b7..b7fae9bb3fe4 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -117,6 +117,13 @@ export async function importBlock( // Some block event handlers require state being in state cache so need to do this before emitting EventType.block this.regen.processState(blockRootHex, postState); + const parentEpoch = computeEpochAtSlot(parentBlockSlot); + if (parentEpoch < blockEpoch && postState.epochCtx.nextShuffling !== null) { + // current epoch and previous epoch are likely cached in previous states + this.shufflingCache.set(postState.epochCtx.nextShuffling, postState.epochCtx.nextDecisionRoot); + this.logger.verbose("Processed shuffling for next epoch", {parentEpoch, blockEpoch, slot: blockSlot}); + } + this.metrics?.importBlock.bySource.inc({source: source.source}); this.logger.verbose("Added block to forkchoice and state cache", {slot: blockSlot, root: blockRootHex}); diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 091739b7d3da..ab302fac7e61 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -306,7 +306,7 @@ export class BeaconChain implements IBeaconChain { }); this._earliestAvailableSlot = cachedState.slot; - this.shufflingCache = cachedState.epochCtx.shufflingCache = new ShufflingCache(metrics, logger, this.opts, [ + this.shufflingCache = new ShufflingCache(metrics, logger, this.opts, [ { shuffling: cachedState.epochCtx.previousShuffling, decisionRoot: cachedState.epochCtx.previousDecisionRoot, @@ -1004,8 +1004,10 @@ export class BeaconChain implements IBeaconChain { state = await this.regen.getState(attHeadBlock.stateRoot, regenCaller); } - // should always be the current epoch of the active context so no need to await a result from the ShufflingCache - return state.epochCtx.getShufflingAtEpoch(attEpoch); + // Get shuffling from the regenerated state and populate the cache + const shuffling = state.epochCtx.getShufflingAtEpoch(attEpoch); + this.shufflingCache.set(shuffling, shufflingDependentRoot); + return shuffling; } /** diff --git a/packages/beacon-node/src/chain/shufflingCache.ts b/packages/beacon-node/src/chain/shufflingCache.ts index a93220c38c93..dbe2ff43ce98 100644 --- a/packages/beacon-node/src/chain/shufflingCache.ts +++ b/packages/beacon-node/src/chain/shufflingCache.ts @@ -1,4 +1,4 @@ -import {EpochShuffling, IShufflingCache, ShufflingBuildProps, computeEpochShuffling} from "@lodestar/state-transition"; +import {EpochShuffling} from "@lodestar/state-transition"; import {Epoch, RootHex} from "@lodestar/types"; import {LodestarError, Logger, MapDef, pruneSetToMax} from "@lodestar/utils"; import {Metrics} from "../metrics/metrics.js"; @@ -46,7 +46,7 @@ export type ShufflingCacheOpts = { * - if a shuffling is not available (which does not happen with default chain option of maxSkipSlots = 32), track a promise to make sure we don't compute the same shuffling twice * - skip computing shuffling when loading state bytes from disk */ -export class ShufflingCache implements IShufflingCache { +export class ShufflingCache { /** LRU cache implemented as a map, pruned every time we add an item */ private readonly itemsByDecisionRootByEpoch: MapDef> = new MapDef( () => new Map() @@ -129,38 +129,21 @@ export class ShufflingCache implements IShufflingCache { } /** - * Gets a cached shuffling via the epoch and decision root. If the shuffling is not - * available it will build it synchronously and return the shuffling. - * - * NOTE: If a shuffling is already queued and not calculated it will build and resolve - * the promise but the already queued build will happen at some later time + * Gets a cached shuffling synchronously via the epoch and decision root. + * Returns null if not found in cache. */ - getSync( - epoch: Epoch, - decisionRoot: RootHex, - buildProps?: T - ): T extends ShufflingBuildProps ? EpochShuffling : EpochShuffling | null { + getSync(epoch: Epoch, decisionRoot: RootHex): EpochShuffling | null { const cacheItem = this.itemsByDecisionRootByEpoch.getOrDefault(epoch).get(decisionRoot); if (!cacheItem) { this.metrics?.shufflingCache.miss.inc(); - } else if (isShufflingCacheItem(cacheItem)) { + return null; + } + if (isShufflingCacheItem(cacheItem)) { this.metrics?.shufflingCache.hit.inc(); return cacheItem.shuffling; - } else if (buildProps) { - // TODO: (@matthewkeil) This should possible log a warning?? - this.metrics?.shufflingCache.shufflingPromiseNotResolvedAndThrownAway.inc(); - } else { - this.metrics?.shufflingCache.shufflingPromiseNotResolved.inc(); } - - let shuffling: EpochShuffling | null = null; - if (buildProps) { - const timer = this.metrics?.shufflingCache.shufflingCalculationTime.startTimer({source: "getSync"}); - shuffling = computeEpochShuffling(buildProps.state, buildProps.activeIndices, epoch); - timer?.(); - this.set(shuffling, decisionRoot); - } - return shuffling as T extends ShufflingBuildProps ? EpochShuffling : EpochShuffling | null; + this.metrics?.shufflingCache.shufflingPromiseNotResolved.inc(); + return null; } /** diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 2768e7849c85..5cf4a36008a3 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -30,12 +30,7 @@ import { import {LodestarError} from "@lodestar/utils"; import {getTotalSlashingsByIncrement} from "../epoch/processSlashings.js"; import {AttesterDuty, calculateCommitteeAssignments} from "../util/calculateCommitteeAssignments.js"; -import { - EpochShuffling, - IShufflingCache, - calculateShufflingDecisionRoot, - computeEpochShuffling, -} from "../util/epochShuffling.js"; +import {EpochShuffling, calculateShufflingDecisionRoot, computeEpochShuffling} from "../util/epochShuffling.js"; import { computeActivationExitEpoch, computeEpochAtSlot, @@ -70,7 +65,6 @@ export type EpochCacheImmutableData = { config: BeaconConfig; pubkey2index: PubkeyIndexMap; index2pubkey: Index2PubkeyCache; - shufflingCache?: IShufflingCache; }; export type EpochCacheOpts = { @@ -120,12 +114,6 @@ export class EpochCache { * $VALIDATOR_COUNT x BLST deserialized pubkey (Jacobian coordinates) */ index2pubkey: Index2PubkeyCache; - /** - * ShufflingCache is passed in from `beacon-node` so should be available at runtime but may not be - * present during testing. - */ - shufflingCache?: IShufflingCache; - /** * Indexes of the block proposers for the current epoch. * For pre-fulu, this is computed and cached from the current shuffling. @@ -257,7 +245,6 @@ export class EpochCache { config: BeaconConfig; pubkey2index: PubkeyIndexMap; index2pubkey: Index2PubkeyCache; - shufflingCache?: IShufflingCache; proposers: number[]; proposersPrevEpoch: number[] | null; proposersNextEpoch: ProposersDeferred; @@ -289,7 +276,6 @@ export class EpochCache { this.config = data.config; this.pubkey2index = data.pubkey2index; this.index2pubkey = data.index2pubkey; - this.shufflingCache = data.shufflingCache; this.proposers = data.proposers; this.proposersPrevEpoch = data.proposersPrevEpoch; this.proposersNextEpoch = data.proposersNextEpoch; @@ -327,7 +313,7 @@ export class EpochCache { */ static createFromState( state: BeaconStateAllForks, - {config, pubkey2index, index2pubkey, shufflingCache}: EpochCacheImmutableData, + {config, pubkey2index, index2pubkey}: EpochCacheImmutableData, opts?: EpochCacheOpts ): EpochCache { const currentEpoch = computeEpochAtSlot(state.slot); @@ -354,14 +340,10 @@ export class EpochCache { const currentActiveIndicesAsNumberArray: ValidatorIndex[] = []; const nextActiveIndicesAsNumberArray: ValidatorIndex[] = []; - // BeaconChain could provide a shuffling cache to avoid re-computing shuffling every epoch - // in that case, we don't need to compute shufflings again + // Decision roots for shuffling, beacon-node will use these to populate its ShufflingCache const previousDecisionRoot = calculateShufflingDecisionRoot(config, state, previousEpoch); - const cachedPreviousShuffling = shufflingCache?.getSync(previousEpoch, previousDecisionRoot); const currentDecisionRoot = calculateShufflingDecisionRoot(config, state, currentEpoch); - const cachedCurrentShuffling = shufflingCache?.getSync(currentEpoch, currentDecisionRoot); const nextDecisionRoot = calculateShufflingDecisionRoot(config, state, nextEpoch); - const cachedNextShuffling = shufflingCache?.getSync(nextEpoch, nextDecisionRoot); for (let i = 0; i < validatorCount; i++) { const validator = validators[i]; @@ -369,19 +351,16 @@ export class EpochCache { // Note: Not usable for fork-choice balances since in-active validators are not zero'ed effectiveBalanceIncrements[i] = Math.floor(validator.effectiveBalance / EFFECTIVE_BALANCE_INCREMENT); - // we only need to track active indices for previous, current and next epoch if we have to compute shufflings - // skip doing that if we already have cached shufflings - if (cachedPreviousShuffling == null && isActiveValidator(validator, previousEpoch)) { + // Collect active indices for each epoch to compute shufflings + if (isActiveValidator(validator, previousEpoch)) { previousActiveIndicesAsNumberArray.push(i); } if (isActiveValidator(validator, currentEpoch)) { - if (cachedCurrentShuffling == null) { - currentActiveIndicesAsNumberArray.push(i); - } + currentActiveIndicesAsNumberArray.push(i); // We track totalActiveBalanceIncrements as ETH to fit total network balance in a JS number (53 bits) totalActiveBalanceIncrements += effectiveBalanceIncrements[i]; } - if (cachedNextShuffling == null && isActiveValidator(validator, nextEpoch)) { + if (isActiveValidator(validator, nextEpoch)) { nextActiveIndicesAsNumberArray.push(i); } @@ -405,47 +384,18 @@ export class EpochCache { } const nextActiveIndices = new Uint32Array(nextActiveIndicesAsNumberArray); - let previousShuffling: EpochShuffling; - let currentShuffling: EpochShuffling; - let nextShuffling: EpochShuffling; - - if (!shufflingCache) { - // Only for testing. shufflingCache should always be available in prod - previousShuffling = computeEpochShuffling( - state, - new Uint32Array(previousActiveIndicesAsNumberArray), - previousEpoch - ); - currentShuffling = isGenesis - ? previousShuffling - : computeEpochShuffling(state, new Uint32Array(currentActiveIndicesAsNumberArray), currentEpoch); + const previousShuffling = computeEpochShuffling( + state, + new Uint32Array(previousActiveIndicesAsNumberArray), + previousEpoch + ); + + const currentShuffling = isGenesis + ? previousShuffling + : computeEpochShuffling(state, new Uint32Array(currentActiveIndicesAsNumberArray), currentEpoch); - nextShuffling = computeEpochShuffling(state, nextActiveIndices, nextEpoch); - } else { - currentShuffling = cachedCurrentShuffling - ? cachedCurrentShuffling - : shufflingCache.getSync(currentEpoch, currentDecisionRoot, { - state, - activeIndices: new Uint32Array(currentActiveIndicesAsNumberArray), - }); - - previousShuffling = cachedPreviousShuffling - ? cachedPreviousShuffling - : isGenesis - ? currentShuffling - : shufflingCache.getSync(previousEpoch, previousDecisionRoot, { - state, - activeIndices: new Uint32Array(previousActiveIndicesAsNumberArray), - }); - - nextShuffling = cachedNextShuffling - ? cachedNextShuffling - : shufflingCache.getSync(nextEpoch, nextDecisionRoot, { - state, - activeIndices: nextActiveIndices, - }); - } + const nextShuffling = computeEpochShuffling(state, nextActiveIndices, nextEpoch); const currentProposerSeed = getSeed(state, currentEpoch, DOMAIN_BEACON_PROPOSER); @@ -552,7 +502,6 @@ export class EpochCache { config, pubkey2index, index2pubkey, - shufflingCache, proposers, // On first epoch, set to null to prevent unnecessary work since this is only used for metrics proposersPrevEpoch: null, @@ -596,7 +545,6 @@ export class EpochCache { // Common append-only structures shared with all states, no need to clone pubkey2index: this.pubkey2index, index2pubkey: this.index2pubkey, - shufflingCache: this.shufflingCache, // Immutable data proposers: this.proposers, proposersPrevEpoch: this.proposersPrevEpoch, @@ -658,34 +606,16 @@ export class EpochCache { // move next to current or calculate upcoming this.currentDecisionRoot = this.nextDecisionRoot; if (this.nextShuffling) { - // was already pulled from the ShufflingCache to the EpochCache (should be in most cases) + // was already pulled to the EpochCache this.currentShuffling = this.nextShuffling; } else { - this.shufflingCache?.metrics?.shufflingCache.nextShufflingNotOnEpochCache.inc(); - this.currentShuffling = - this.shufflingCache?.getSync(upcomingEpoch, this.currentDecisionRoot, { - state, - // have to use the "nextActiveIndices" that were saved in the last transition here to calculate - // the upcoming shuffling if it is not already built (similar condition to the below computation) - activeIndices: this.nextActiveIndices, - }) ?? - // allow for this case during testing where the ShufflingCache is not present, may affect perf testing - // so should be taken into account when structuring tests. Should not affect unit or other tests though - computeEpochShuffling(state, this.nextActiveIndices, upcomingEpoch); + this.currentShuffling = computeEpochShuffling(state, this.nextActiveIndices, upcomingEpoch); } - // handle next values + // handle next values - always compute the shuffling for epoch after upcoming this.nextDecisionRoot = epochTransitionCache.nextShufflingDecisionRoot; this.nextActiveIndices = epochTransitionCache.nextShufflingActiveIndices; - if (this.shufflingCache) { - this.nextShuffling = this.shufflingCache.getSync(epochAfterUpcoming, this.nextDecisionRoot, { - state, - activeIndices: this.nextActiveIndices, - }); - } else { - // Only for testing. shufflingCache should always be available in prod - this.nextShuffling = computeEpochShuffling(state, this.nextActiveIndices, epochAfterUpcoming); - } + this.nextShuffling = computeEpochShuffling(state, this.nextActiveIndices, epochAfterUpcoming); // TODO: DEDUPLICATE from createEpochCache // @@ -1078,10 +1008,6 @@ export class EpochCache { case this.epoch: return this.currentShuffling; case this.nextEpoch: - if (!this.nextShuffling) { - this.nextShuffling = - this.shufflingCache?.getSync(this.nextEpoch, this.getShufflingDecisionRoot(this.nextEpoch)) ?? null; - } return this.nextShuffling; default: return null; diff --git a/packages/state-transition/src/cache/stateCache.ts b/packages/state-transition/src/cache/stateCache.ts index 4e1093905d7e..59cefe23af64 100644 --- a/packages/state-transition/src/cache/stateCache.ts +++ b/packages/state-transition/src/cache/stateCache.ts @@ -179,7 +179,7 @@ export function loadCachedBeaconState; - }; -} -export interface IShufflingCache { - metrics: PublicShufflingCacheMetrics | null; - logger: Logger | null; - /** - * Gets a cached shuffling via the epoch and decision root. If the state and - * activeIndices are passed and a shuffling is not available it will be built - * synchronously. If the state is not passed and the shuffling is not available - * nothing will be returned. - * - * NOTE: If a shuffling is already queued and not calculated it will build and resolve - * the promise but the already queued build will happen at some later time - */ - getSync( - epoch: Epoch, - decisionRoot: RootHex, - buildProps?: T - ): T extends ShufflingBuildProps ? EpochShuffling : EpochShuffling | null; - - /** - * Gets a cached shuffling via the epoch and decision root. Returns a promise - * for the shuffling if it hs not calculated yet. Returns null if a build has - * not been queued nor a shuffling was calculated. - */ - get(epoch: Epoch, decisionRoot: RootHex): Promise; - - /** - * Add an EpochShuffling to the ShufflingCache. If a promise for the shuffling - * is present it will resolve the promise with the built shuffling. - */ - set(shuffling: EpochShuffling, decisionRoot: RootHex): void; -} - /** * Readonly interface for EpochShuffling. */ From ec2983f86dc4d071e9559d6762f31d1943459c61 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Tue, 6 Jan 2026 20:25:44 +0530 Subject: [PATCH 06/29] proposer lookahead comments --- packages/state-transition/src/cache/epochCache.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 5cf4a36008a3..be9a9cd45c1e 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -612,7 +612,19 @@ export class EpochCache { this.currentShuffling = computeEpochShuffling(state, this.nextActiveIndices, upcomingEpoch); } - // handle next values - always compute the shuffling for epoch after upcoming + // Compute shuffling for epoch n+2 + // + // Post-Fulu (EIP-7917), the beacon state includes a `proposer_lookahead` field that stores + // proposer indices for MIN_SEED_LOOKAHEAD + 1 epochs ahead (2 epochs with MIN_SEED_LOOKAHEAD=1). + // At each epoch boundary, processProposerLookahead() shifts out the current epoch's proposers + // and appends new proposers for epoch n + MIN_SEED_LOOKAHEAD + 1 (i.e., epoch n+2). + // + // processProposerLookahead() computes its own shuffling for epoch n+2 using computeEpochShuffling() + // with cache.nextShufflingActiveIndices. We also cache the n+2 shuffling here in nextShuffling so that: + // 1. It becomes currentShuffling in the next epoch transition to avoiding recomputation + // 2. beacon-node can populate its ShufflingCache for attestation verification + // + // See: https://eips.ethereum.org/EIPS/eip-7917 this.nextDecisionRoot = epochTransitionCache.nextShufflingDecisionRoot; this.nextActiveIndices = epochTransitionCache.nextShufflingActiveIndices; this.nextShuffling = computeEpochShuffling(state, this.nextActiveIndices, epochAfterUpcoming); From 03cb62989997e62b86eb2932d7537946bed083a7 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Tue, 6 Jan 2026 20:29:20 +0530 Subject: [PATCH 07/29] remove metrics --- .../beacon-node/src/metrics/metrics/lodestar.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index 721b6ffbcbac..d815e1f72694 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -1312,29 +1312,15 @@ export function createLodestarMetrics( name: "lodestar_shuffling_cache_recalculated_shuffling_count", help: "Count of shuffling that were build multiple times", }), - shufflingPromiseNotResolvedAndThrownAway: register.gauge({ - name: "lodestar_shuffling_cache_promise_not_resolved_and_thrown_away_count", - help: "Count of shuffling cache promises that were discarded and the shuffling was built synchronously", - }), shufflingPromiseNotResolved: register.gauge({ name: "lodestar_shuffling_cache_promise_not_resolved_count", help: "Count of shuffling cache promises that were requested before the promise was resolved", }), - nextShufflingNotOnEpochCache: register.gauge({ - name: "lodestar_shuffling_cache_next_shuffling_not_on_epoch_cache", - help: "The next shuffling was not on the epoch cache before the epoch transition", - }), shufflingPromiseResolutionTime: register.histogram({ name: "lodestar_shuffling_cache_promise_resolution_time_seconds", help: "Time from promise insertion until promise resolution when shuffling was ready in seconds", buckets: [0.5, 1, 1.5, 2], }), - shufflingCalculationTime: register.histogram<{source: "build" | "getSync"}>({ - name: "lodestar_shuffling_cache_shuffling_calculation_time_seconds", - help: "Run time of shuffling calculation", - buckets: [0.5, 0.75, 1, 1.25, 1.5], - labelNames: ["source"], - }), }, seenCache: { From c3dbaaa3f33a67767a326d45c7a255993cb37337 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Tue, 13 Jan 2026 17:59:03 +0530 Subject: [PATCH 08/29] add metrics --- packages/beacon-node/src/metrics/metrics/lodestar.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index d815e1f72694..1788a06be3f7 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -1321,6 +1321,11 @@ export function createLodestarMetrics( help: "Time from promise insertion until promise resolution when shuffling was ready in seconds", buckets: [0.5, 1, 1.5, 2], }), + shufflingCalculationTime: register.histogram({ + name: "lodestar_shuffling_cache_shuffling_calculation_time_seconds", + help: "Run time of shuffling calculation", + buckets: [0.5, 0.75, 1, 1.25, 1.5], + }), }, seenCache: { From 9ae55bc7cabf3731789c1a160473d7aba81b1aa8 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Tue, 13 Jan 2026 18:21:40 +0530 Subject: [PATCH 09/29] make set private and remove getSync --- packages/beacon-node/src/chain/chain.ts | 5 +---- .../beacon-node/src/chain/shufflingCache.ts | 20 +------------------ 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index ab302fac7e61..10419d4bc881 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -1004,10 +1004,7 @@ export class BeaconChain implements IBeaconChain { state = await this.regen.getState(attHeadBlock.stateRoot, regenCaller); } - // Get shuffling from the regenerated state and populate the cache - const shuffling = state.epochCtx.getShufflingAtEpoch(attEpoch); - this.shufflingCache.set(shuffling, shufflingDependentRoot); - return shuffling; + return state.epochCtx.getShufflingAtEpoch(attEpoch); } /** diff --git a/packages/beacon-node/src/chain/shufflingCache.ts b/packages/beacon-node/src/chain/shufflingCache.ts index dbe2ff43ce98..f7036b300a2c 100644 --- a/packages/beacon-node/src/chain/shufflingCache.ts +++ b/packages/beacon-node/src/chain/shufflingCache.ts @@ -128,29 +128,11 @@ export class ShufflingCache { return cacheItem.promise; } - /** - * Gets a cached shuffling synchronously via the epoch and decision root. - * Returns null if not found in cache. - */ - getSync(epoch: Epoch, decisionRoot: RootHex): EpochShuffling | null { - const cacheItem = this.itemsByDecisionRootByEpoch.getOrDefault(epoch).get(decisionRoot); - if (!cacheItem) { - this.metrics?.shufflingCache.miss.inc(); - return null; - } - if (isShufflingCacheItem(cacheItem)) { - this.metrics?.shufflingCache.hit.inc(); - return cacheItem.shuffling; - } - this.metrics?.shufflingCache.shufflingPromiseNotResolved.inc(); - return null; - } - /** * Add an EpochShuffling to the ShufflingCache. If a promise for the shuffling is present it will * resolve the promise with the built shuffling */ - set(shuffling: EpochShuffling, decisionRoot: string): void { + private set(shuffling: EpochShuffling, decisionRoot: string): void { const shufflingAtEpoch = this.itemsByDecisionRootByEpoch.getOrDefault(shuffling.epoch); // if a pending shuffling promise exists, resolve it const cacheItem = shufflingAtEpoch.get(decisionRoot); From f6a425bfdbdee4764c152bb30c96e25f6add60a0 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Tue, 13 Jan 2026 18:38:07 +0530 Subject: [PATCH 10/29] add back processState --- .../beacon-node/src/chain/blocks/importBlock.ts | 14 +++++++------- packages/beacon-node/src/chain/shufflingCache.ts | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index b7fae9bb3fe4..74f66cc59193 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -117,13 +117,6 @@ export async function importBlock( // Some block event handlers require state being in state cache so need to do this before emitting EventType.block this.regen.processState(blockRootHex, postState); - const parentEpoch = computeEpochAtSlot(parentBlockSlot); - if (parentEpoch < blockEpoch && postState.epochCtx.nextShuffling !== null) { - // current epoch and previous epoch are likely cached in previous states - this.shufflingCache.set(postState.epochCtx.nextShuffling, postState.epochCtx.nextDecisionRoot); - this.logger.verbose("Processed shuffling for next epoch", {parentEpoch, blockEpoch, slot: blockSlot}); - } - this.metrics?.importBlock.bySource.inc({source: source.source}); this.logger.verbose("Added block to forkchoice and state cache", {slot: blockSlot, root: blockRootHex}); @@ -425,6 +418,13 @@ export async function importBlock( this.logger.verbose("After importBlock caching postState without SSZ cache", {slot: postState.slot}); } + // Cache the next epoch shuffling when crossing an epoch boundary + const parentEpoch = computeEpochAtSlot(parentBlockSlot); + if (parentEpoch < blockEpoch && postState.epochCtx.nextShuffling !== null) { + this.shufflingCache.processState(postState, postState.epochCtx.nextShuffling.epoch); + 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; diff --git a/packages/beacon-node/src/chain/shufflingCache.ts b/packages/beacon-node/src/chain/shufflingCache.ts index f7036b300a2c..2d8dd6bde9c7 100644 --- a/packages/beacon-node/src/chain/shufflingCache.ts +++ b/packages/beacon-node/src/chain/shufflingCache.ts @@ -1,6 +1,6 @@ -import {EpochShuffling} from "@lodestar/state-transition"; +import {CachedBeaconStateAllForks, EpochShuffling, attesterShufflingDecisionRoot} from "@lodestar/state-transition"; import {Epoch, RootHex} from "@lodestar/types"; -import {LodestarError, Logger, MapDef, pruneSetToMax} from "@lodestar/utils"; +import {LodestarError, Logger, MapDef, pruneSetToMax, toRootHex} from "@lodestar/utils"; import {Metrics} from "../metrics/metrics.js"; /** @@ -128,6 +128,18 @@ export class ShufflingCache { return cacheItem.promise; } + /** + * Process a post state to extract and cache the shuffling for the given epoch. + * Extracts the decision root from the state internally. + */ + processState(state: CachedBeaconStateAllForks, epoch: Epoch): void { + const shuffling = state.epochCtx.getShufflingAtEpoch(epoch); + const decisionRoot = attesterShufflingDecisionRoot(state, epoch); + if (decisionRoot !== null) { + this.set(shuffling, toRootHex(decisionRoot)); + } + } + /** * Add an EpochShuffling to the ShufflingCache. If a promise for the shuffling is present it will * resolve the promise with the built shuffling From 58d82a20733c5ba0783e898011295803a7945ea2 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Tue, 13 Jan 2026 19:24:23 +0530 Subject: [PATCH 11/29] deduplicate computeEpochShuffling and make nextShuffling non-optional --- .../state-transition/src/cache/epochCache.ts | 35 ++++++++----------- .../src/cache/epochTransitionCache.ts | 8 +++++ .../src/epoch/processProposerLookahead.ts | 2 ++ 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index be9a9cd45c1e..fade6a053403 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -152,7 +152,7 @@ export class EpochCache { /** Same as previousShuffling */ currentShuffling: EpochShuffling; /** Same as previousShuffling */ - nextShuffling: EpochShuffling | null; + nextShuffling: EpochShuffling; /** * Cache nextActiveIndices so that in afterProcessEpoch the next shuffling can be build synchronously * in case it is not built or the ShufflingCache is not available @@ -253,7 +253,7 @@ export class EpochCache { nextDecisionRoot: RootHex; previousShuffling: EpochShuffling; currentShuffling: EpochShuffling; - nextShuffling: EpochShuffling | null; + nextShuffling: EpochShuffling; nextActiveIndices: Uint32Array; effectiveBalanceIncrements: EffectiveBalanceIncrements; totalSlashingsByIncrement: number; @@ -385,15 +385,15 @@ export class EpochCache { const nextActiveIndices = new Uint32Array(nextActiveIndicesAsNumberArray); - const previousShuffling = computeEpochShuffling( + const currentShuffling = computeEpochShuffling( state, - new Uint32Array(previousActiveIndicesAsNumberArray), - previousEpoch + new Uint32Array(currentActiveIndicesAsNumberArray), + currentEpoch ); - const currentShuffling = isGenesis - ? previousShuffling - : computeEpochShuffling(state, new Uint32Array(currentActiveIndicesAsNumberArray), currentEpoch); + const previousShuffling = isGenesis + ? currentShuffling + : computeEpochShuffling(state, new Uint32Array(previousActiveIndicesAsNumberArray), previousEpoch); const nextShuffling = computeEpochShuffling(state, nextActiveIndices, nextEpoch); @@ -603,14 +603,9 @@ export class EpochCache { this.previousShuffling = this.currentShuffling; this.previousDecisionRoot = this.currentDecisionRoot; - // move next to current or calculate upcoming + // move next to current this.currentDecisionRoot = this.nextDecisionRoot; - if (this.nextShuffling) { - // was already pulled to the EpochCache - this.currentShuffling = this.nextShuffling; - } else { - this.currentShuffling = computeEpochShuffling(state, this.nextActiveIndices, upcomingEpoch); - } + this.currentShuffling = this.nextShuffling; // Compute shuffling for epoch n+2 // @@ -619,15 +614,15 @@ export class EpochCache { // At each epoch boundary, processProposerLookahead() shifts out the current epoch's proposers // and appends new proposers for epoch n + MIN_SEED_LOOKAHEAD + 1 (i.e., epoch n+2). // - // processProposerLookahead() computes its own shuffling for epoch n+2 using computeEpochShuffling() - // with cache.nextShufflingActiveIndices. We also cache the n+2 shuffling here in nextShuffling so that: - // 1. It becomes currentShuffling in the next epoch transition to avoiding recomputation - // 2. beacon-node can populate its ShufflingCache for attestation verification + // processProposerLookahead() already computes the n+2 shuffling and stores it in + // epochTransitionCache.nextShuffling. Reuse it here to avoid duplicate computation. + // Pre-Fulu, we need to compute it here since processProposerLookahead doesn't run. // // See: https://eips.ethereum.org/EIPS/eip-7917 this.nextDecisionRoot = epochTransitionCache.nextShufflingDecisionRoot; this.nextActiveIndices = epochTransitionCache.nextShufflingActiveIndices; - this.nextShuffling = computeEpochShuffling(state, this.nextActiveIndices, epochAfterUpcoming); + this.nextShuffling = + epochTransitionCache.nextShuffling ?? computeEpochShuffling(state, this.nextActiveIndices, epochAfterUpcoming); // TODO: DEDUPLICATE from createEpochCache // diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index e694eb957936..b74f4089c1a9 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -26,6 +26,7 @@ import { FLAG_UNSLASHED, hasMarkers, } from "../util/attesterStatus.js"; +import {EpochShuffling} from "../util/epochShuffling.js"; export type EpochTransitionCacheOpts = { /** @@ -162,6 +163,12 @@ export interface EpochTransitionCache { */ nextShufflingDecisionRoot: RootHex; + /** + * Pre-computed shuffling for epoch N+2, populated by processProposerLookahead (Fulu+). + * Used by afterProcessEpoch to avoid recomputing the same shuffling. + */ + nextShuffling: EpochShuffling | null; + /** * Altair specific, this is total active balances for the next epoch. * This is only used in `afterProcessEpoch` to compute base reward and sync participant reward. @@ -510,6 +517,7 @@ export function beforeProcessEpoch( indicesToEject, nextShufflingDecisionRoot, nextShufflingActiveIndices, + nextShuffling: null, // to be updated in processEffectiveBalanceUpdates nextEpochTotalActiveBalanceByIncrement: 0, isActivePrevEpoch, diff --git a/packages/state-transition/src/epoch/processProposerLookahead.ts b/packages/state-transition/src/epoch/processProposerLookahead.ts index 0fd94a63cf36..4d92afcf45cc 100644 --- a/packages/state-transition/src/epoch/processProposerLookahead.ts +++ b/packages/state-transition/src/epoch/processProposerLookahead.ts @@ -23,6 +23,8 @@ export function processProposerLookahead( const epoch = state.epochCtx.epoch + MIN_SEED_LOOKAHEAD + 1; const shuffling = computeEpochShuffling(state, cache.nextShufflingActiveIndices, epoch); + // Save shuffling to cache so afterProcessEpoch can reuse it instead of recomputing + cache.nextShuffling = shuffling; const lastEpochProposerLookahead = computeProposerIndices(fork, state, shuffling, epoch); From 382a664ceecdd12a9f0221c5feaa129af970865c Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Tue, 13 Jan 2026 19:34:50 +0530 Subject: [PATCH 12/29] avoid expensive cache computation when shuffling cache --- .../state-transition/src/cache/epochCache.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index fade6a053403..2b03466f66e7 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -70,6 +70,10 @@ export type EpochCacheImmutableData = { export type EpochCacheOpts = { skipSyncCommitteeCache?: boolean; skipSyncPubkeys?: boolean; + /** Pre-computed shufflings from beacon-node's ShufflingCache, if available */ + previousShuffling?: EpochShuffling; + currentShuffling?: EpochShuffling; + nextShuffling?: EpochShuffling; }; /** Defers computing proposers by persisting only the seed, and dropping it once indexes are computed */ @@ -385,17 +389,19 @@ export class EpochCache { const nextActiveIndices = new Uint32Array(nextActiveIndicesAsNumberArray); - const currentShuffling = computeEpochShuffling( - state, - new Uint32Array(currentActiveIndicesAsNumberArray), - currentEpoch - ); + // Use shufflings from opts if provided (from beacon-node's ShufflingCache), otherwise compute + const currentShuffling = + opts?.currentShuffling ?? + computeEpochShuffling(state, new Uint32Array(currentActiveIndicesAsNumberArray), currentEpoch); - const previousShuffling = isGenesis - ? currentShuffling - : computeEpochShuffling(state, new Uint32Array(previousActiveIndicesAsNumberArray), previousEpoch); + const previousShuffling = + opts?.previousShuffling ?? + (isGenesis + ? currentShuffling + : computeEpochShuffling(state, new Uint32Array(previousActiveIndicesAsNumberArray), previousEpoch)); - const nextShuffling = computeEpochShuffling(state, nextActiveIndices, nextEpoch); + const nextShuffling = + opts?.nextShuffling ?? computeEpochShuffling(state, nextActiveIndices, nextEpoch); const currentProposerSeed = getSeed(state, currentEpoch, DOMAIN_BEACON_PROPOSER); From a06d62f22f8bc996f0c479150d74ee9a7ed855c6 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Wed, 14 Jan 2026 19:04:46 +0530 Subject: [PATCH 13/29] cache population --- packages/beacon-node/src/chain/chain.ts | 4 ++-- packages/beacon-node/src/chain/shufflingCache.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 10419d4bc881..42ad5590e4ff 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -1003,8 +1003,8 @@ export class BeaconChain implements IBeaconChain { this.metrics?.gossipAttestation.useHeadBlockState.inc({caller: regenCaller}); state = await this.regen.getState(attHeadBlock.stateRoot, regenCaller); } - - return state.epochCtx.getShufflingAtEpoch(attEpoch); + // resolve the promise to unblock other calls of the same epoch and dependent root + return this.shufflingCache.processState(state, attEpoch); } /** diff --git a/packages/beacon-node/src/chain/shufflingCache.ts b/packages/beacon-node/src/chain/shufflingCache.ts index 2d8dd6bde9c7..6f356cde9a8d 100644 --- a/packages/beacon-node/src/chain/shufflingCache.ts +++ b/packages/beacon-node/src/chain/shufflingCache.ts @@ -131,13 +131,15 @@ export class ShufflingCache { /** * Process a post state to extract and cache the shuffling for the given epoch. * Extracts the decision root from the state internally. + * Returns the shuffling for use by callers that need it immediately. */ - processState(state: CachedBeaconStateAllForks, epoch: Epoch): void { + processState(state: CachedBeaconStateAllForks, epoch: Epoch): EpochShuffling { const shuffling = state.epochCtx.getShufflingAtEpoch(epoch); const decisionRoot = attesterShufflingDecisionRoot(state, epoch); if (decisionRoot !== null) { this.set(shuffling, toRootHex(decisionRoot)); } + return shuffling; } /** From 57e6099b69caf66b547c64ad3bd2f2c9066abad2 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Wed, 14 Jan 2026 19:57:19 +0530 Subject: [PATCH 14/29] fix: add back ShufflingGetter --- .../state-transition/src/cache/epochCache.ts | 31 ++++++++++--------- packages/state-transition/src/types.ts | 1 + .../perf/util/loadState/loadState.test.ts | 4 +-- .../test/unit/cachedBeaconState.test.ts | 30 ++++++++++++++++-- 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 2b03466f66e7..3a64098cb065 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -56,7 +56,7 @@ import { computeSyncCommitteeCache, getSyncCommitteeCache, } from "./syncCommitteeCache.js"; -import {BeaconStateAllForks, BeaconStateAltair, BeaconStateGloas} from "./types.js"; +import {BeaconStateAllForks, BeaconStateAltair, BeaconStateGloas, ShufflingGetter} from "./types.js"; /** `= PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT)` */ export const PROPOSER_WEIGHT_FACTOR = PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT); @@ -70,10 +70,7 @@ export type EpochCacheImmutableData = { export type EpochCacheOpts = { skipSyncCommitteeCache?: boolean; skipSyncPubkeys?: boolean; - /** Pre-computed shufflings from beacon-node's ShufflingCache, if available */ - previousShuffling?: EpochShuffling; - currentShuffling?: EpochShuffling; - nextShuffling?: EpochShuffling; + shufflingGetter?: ShufflingGetter; }; /** Defers computing proposers by persisting only the seed, and dropping it once indexes are computed */ @@ -344,10 +341,15 @@ export class EpochCache { const currentActiveIndicesAsNumberArray: ValidatorIndex[] = []; const nextActiveIndicesAsNumberArray: ValidatorIndex[] = []; - // Decision roots for shuffling, beacon-node will use these to populate its ShufflingCache + // BeaconChain could provide a shuffling getter to avoid re-computing shuffling every epoch + // in that case, we don't need to compute shufflings again + const shufflingGetter = opts?.shufflingGetter; const previousDecisionRoot = calculateShufflingDecisionRoot(config, state, previousEpoch); + const cachedPreviousShuffling = shufflingGetter?.(previousEpoch, previousDecisionRoot); const currentDecisionRoot = calculateShufflingDecisionRoot(config, state, currentEpoch); + const cachedCurrentShuffling = shufflingGetter?.(currentEpoch, currentDecisionRoot); const nextDecisionRoot = calculateShufflingDecisionRoot(config, state, nextEpoch); + const cachedNextShuffling = shufflingGetter?.(nextEpoch, nextDecisionRoot); for (let i = 0; i < validatorCount; i++) { const validator = validators[i]; @@ -356,15 +358,17 @@ export class EpochCache { effectiveBalanceIncrements[i] = Math.floor(validator.effectiveBalance / EFFECTIVE_BALANCE_INCREMENT); // Collect active indices for each epoch to compute shufflings - if (isActiveValidator(validator, previousEpoch)) { + if (cachedPreviousShuffling == null && isActiveValidator(validator, previousEpoch)) { previousActiveIndicesAsNumberArray.push(i); } if (isActiveValidator(validator, currentEpoch)) { - currentActiveIndicesAsNumberArray.push(i); + if (cachedCurrentShuffling == null) { + currentActiveIndicesAsNumberArray.push(i); + } // We track totalActiveBalanceIncrements as ETH to fit total network balance in a JS number (53 bits) totalActiveBalanceIncrements += effectiveBalanceIncrements[i]; } - if (isActiveValidator(validator, nextEpoch)) { + if (cachedNextShuffling == null && isActiveValidator(validator, nextEpoch)) { nextActiveIndicesAsNumberArray.push(i); } @@ -389,19 +393,18 @@ export class EpochCache { const nextActiveIndices = new Uint32Array(nextActiveIndicesAsNumberArray); - // Use shufflings from opts if provided (from beacon-node's ShufflingCache), otherwise compute + // Use cached shufflings if available, otherwise compute const currentShuffling = - opts?.currentShuffling ?? + cachedCurrentShuffling ?? computeEpochShuffling(state, new Uint32Array(currentActiveIndicesAsNumberArray), currentEpoch); const previousShuffling = - opts?.previousShuffling ?? + cachedPreviousShuffling ?? (isGenesis ? currentShuffling : computeEpochShuffling(state, new Uint32Array(previousActiveIndicesAsNumberArray), previousEpoch)); - const nextShuffling = - opts?.nextShuffling ?? computeEpochShuffling(state, nextActiveIndices, nextEpoch); + const nextShuffling = cachedNextShuffling ?? computeEpochShuffling(state, nextActiveIndices, nextEpoch); const currentProposerSeed = getSeed(state, currentEpoch, DOMAIN_BEACON_PROPOSER); diff --git a/packages/state-transition/src/types.ts b/packages/state-transition/src/types.ts index 6dcf3e8b06f1..02a84f2b4c08 100644 --- a/packages/state-transition/src/types.ts +++ b/packages/state-transition/src/types.ts @@ -23,4 +23,5 @@ export type { BeaconStateFulu, BeaconStateGloas, BeaconStatePhase0, + ShufflingGetter, } from "./cache/types.js"; diff --git a/packages/state-transition/test/perf/util/loadState/loadState.test.ts b/packages/state-transition/test/perf/util/loadState/loadState.test.ts index b4b2bebc5336..526b02bd30e5 100644 --- a/packages/state-transition/test/perf/util/loadState/loadState.test.ts +++ b/packages/state-transition/test/perf/util/loadState/loadState.test.ts @@ -57,15 +57,15 @@ describe("loadState", () => { pubkey2index.set(pubkey, validatorIndex); index2pubkey[validatorIndex] = PublicKey.fromBytes(pubkey); } + const shufflingGetter = () => seedState.epochCtx.currentShuffling; createCachedBeaconState( migratedState, { config: seedState.config, pubkey2index, index2pubkey, - shufflingCache: seedState.epochCtx.shufflingCache, }, - {skipSyncPubkeys: true, skipSyncCommitteeCache: true} + {skipSyncPubkeys: true, skipSyncCommitteeCache: true, shufflingGetter} ); }, }); diff --git a/packages/state-transition/test/unit/cachedBeaconState.test.ts b/packages/state-transition/test/unit/cachedBeaconState.test.ts index 61375bf772a9..e2387a4e0b72 100644 --- a/packages/state-transition/test/unit/cachedBeaconState.test.ts +++ b/packages/state-transition/test/unit/cachedBeaconState.test.ts @@ -3,9 +3,10 @@ import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map"; import {fromHexString} from "@chainsafe/ssz"; import {createBeaconConfig} from "@lodestar/config"; import {config as defaultConfig} from "@lodestar/config/default"; -import {ssz} from "@lodestar/types"; +import {Epoch, RootHex, ssz} from "@lodestar/types"; import {toHexString} from "@lodestar/utils"; import {createCachedBeaconState, loadCachedBeaconState} from "../../src/cache/stateCache.js"; +import {EpochShuffling, calculateShufflingDecisionRoot} from "../../src/util/epochShuffling.js"; import {modifyStateSameValidator, newStateWithValidators} from "../utils/capella.js"; import {interopPubkeysCached} from "../utils/interop.js"; import {createCachedBeaconStateTest} from "../utils/state.js"; @@ -165,15 +166,38 @@ describe("CachedBeaconState", () => { const newStateBytes = newCachedState.serialize(); expect(newStateBytes).toEqual(stateBytes); expect(newCachedState.hashTreeRoot()).toEqual(state.hashTreeRoot()); + const shufflingGetter = (shufflingEpoch: Epoch, dependentRoot: RootHex): EpochShuffling | null => { + if ( + shufflingEpoch === seedState.epochCtx.epoch - 1 && + dependentRoot === calculateShufflingDecisionRoot(config, seedState, shufflingEpoch) + ) { + return seedState.epochCtx.previousShuffling; + } + + if ( + shufflingEpoch === seedState.epochCtx.epoch && + dependentRoot === calculateShufflingDecisionRoot(config, seedState, shufflingEpoch) + ) { + return seedState.epochCtx.currentShuffling; + } + + if ( + shufflingEpoch === seedState.epochCtx.epoch + 1 && + dependentRoot === calculateShufflingDecisionRoot(config, seedState, shufflingEpoch) + ) { + return seedState.epochCtx.nextShuffling; + } + + return null; + }; const cachedState = createCachedBeaconState( state, { config, pubkey2index: new PubkeyIndexMap(), index2pubkey: [], - shufflingCache: seedState.epochCtx.shufflingCache, }, - {skipSyncCommitteeCache: true} + {skipSyncCommitteeCache: true, shufflingGetter} ); // validatorCountDelta < 0 is unrealistic and shuffling computation results in a different result if (validatorCountDelta >= 0) { From 14a28e94c965b97d4152856a3c82361ef26950ef Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Wed, 14 Jan 2026 20:17:22 +0530 Subject: [PATCH 15/29] remove nextShufflingDecisionRoot and fix type error --- packages/beacon-node/src/chain/chain.ts | 6 +++-- .../state-transition/src/cache/epochCache.ts | 5 +++-- .../src/cache/epochTransitionCache.ts | 22 +++---------------- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index d514f4c8300b..855cbab7b70d 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -20,8 +20,10 @@ import { computeEpochAtSlot, computeStartSlotAtEpoch, computeSyncCommitteeRewards, + createCachedBeaconState, getEffectiveBalanceIncrementsZeroInactive, getEffectiveBalancesFromStateBytes, + isCachedBeaconState, processSlots, } from "@lodestar/state-transition"; import { @@ -296,9 +298,9 @@ export class BeaconChain implements IBeaconChain { // When the BeaconStateCache is created in eth1 genesis builder it may be incorrect. Until we can ensure that // it's safe to re-use _ANY_ BeaconStateCache, this option is disabled by default and only used in tests. const cachedState = - isCachedBeaconState(anchorState) && opts.skipCreateStateCacheIfAvailable + isCachedBeaconState(anchorState as BeaconStateAllForks) && opts.skipCreateStateCacheIfAvailable ? anchorState - : createCachedBeaconState(anchorState, { + : createCachedBeaconState(anchorState as BeaconStateAllForks, { config, pubkey2index: new PubkeyIndexMap(), index2pubkey: [], diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 311c94f770c0..4d77fd02958c 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -10,6 +10,7 @@ import { GENESIS_EPOCH, PROPOSER_WEIGHT, SLOTS_PER_EPOCH, + SLOTS_PER_HISTORICAL_ROOT, WEIGHT_DENOMINATOR, } from "@lodestar/params"; import { @@ -27,7 +28,7 @@ import { gloas, phase0, } from "@lodestar/types"; -import {LodestarError} from "@lodestar/utils"; +import {LodestarError, toRootHex} from "@lodestar/utils"; import {getTotalSlashingsByIncrement} from "../epoch/processSlashings.js"; import {AttesterDuty, calculateCommitteeAssignments} from "../util/calculateCommitteeAssignments.js"; import {EpochShuffling, calculateShufflingDecisionRoot, computeEpochShuffling} from "../util/epochShuffling.js"; @@ -625,7 +626,7 @@ export class EpochCache { // Pre-Fulu, we need to compute it here since processProposerLookahead doesn't run. // // See: https://eips.ethereum.org/EIPS/eip-7917 - this.nextDecisionRoot = epochTransitionCache.nextShufflingDecisionRoot; + this.nextDecisionRoot = toRootHex(state.blockRoots.get(state.slot % SLOTS_PER_HISTORICAL_ROOT)); this.nextActiveIndices = epochTransitionCache.nextShufflingActiveIndices; this.nextShuffling = epochTransitionCache.nextShuffling ?? computeEpochShuffling(state, this.nextActiveIndices, epochAfterUpcoming); diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index b74f4089c1a9..01d7e94ff153 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -1,12 +1,6 @@ -import { - EPOCHS_PER_SLASHINGS_VECTOR, - FAR_FUTURE_EPOCH, - ForkSeq, - MIN_ACTIVATION_BALANCE, - SLOTS_PER_HISTORICAL_ROOT, -} from "@lodestar/params"; -import {Epoch, RootHex, ValidatorIndex} from "@lodestar/types"; -import {intDiv, toRootHex} from "@lodestar/utils"; +import {EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, ForkSeq, MIN_ACTIVATION_BALANCE} from "@lodestar/params"; +import {Epoch, ValidatorIndex} from "@lodestar/types"; +import {intDiv} from "@lodestar/utils"; import {processPendingAttestations} from "../epoch/processPendingAttestations.js"; import { CachedBeaconStateAllForks, @@ -158,11 +152,6 @@ export interface EpochTransitionCache { */ nextShufflingActiveIndices: Uint32Array; - /** - * Shuffling decision root that gets set on the EpochCache in afterProcessEpoch - */ - nextShufflingDecisionRoot: RootHex; - /** * Pre-computed shuffling for epoch N+2, populated by processProposerLookahead (Fulu+). * Used by afterProcessEpoch to avoid recomputing the same shuffling. @@ -377,10 +366,6 @@ export function beforeProcessEpoch( }); // Prepare shuffling data for epoch after next (nextShuffling post epoch transition) - // cannot call calculateShufflingDecisionRoot here because spec prevent getting current slot - // as a decision block. we are part way through the transition though and this was added in - // process slot beforeProcessEpoch happens so it available and valid - const nextShufflingDecisionRoot = toRootHex(state.blockRoots.get(state.slot % SLOTS_PER_HISTORICAL_ROOT)); const nextShufflingActiveIndices = new Uint32Array(nextEpochShufflingActiveIndicesLength); if (nextEpochShufflingActiveIndicesLength > nextEpochShufflingActiveValidatorIndices.length) { throw new Error( @@ -515,7 +500,6 @@ export function beforeProcessEpoch( indicesEligibleForActivationQueue, indicesEligibleForActivation: indicesEligibleForActivation.map(({validatorIndex}) => validatorIndex), indicesToEject, - nextShufflingDecisionRoot, nextShufflingActiveIndices, nextShuffling: null, // to be updated in processEffectiveBalanceUpdates From bc30b36d641313ee35864296698b6d3060d301b0 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 15 Jan 2026 17:24:12 +0530 Subject: [PATCH 16/29] fix merge error --- packages/beacon-node/src/chain/chain.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 855cbab7b70d..4b4e9f7f062e 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -20,10 +20,8 @@ import { computeEpochAtSlot, computeStartSlotAtEpoch, computeSyncCommitteeRewards, - createCachedBeaconState, getEffectiveBalanceIncrementsZeroInactive, getEffectiveBalancesFromStateBytes, - isCachedBeaconState, processSlots, } from "@lodestar/state-transition"; import { @@ -292,20 +290,7 @@ export class BeaconChain implements IBeaconChain { logger, }); - // Restore state caches - // anchorState may already by a CachedBeaconState. If so, don't create the cache again, since deserializing all - // pubkeys takes ~30 seconds for 350k keys (mainnet 2022Q2). - // When the BeaconStateCache is created in eth1 genesis builder it may be incorrect. Until we can ensure that - // it's safe to re-use _ANY_ BeaconStateCache, this option is disabled by default and only used in tests. - const cachedState = - isCachedBeaconState(anchorState as BeaconStateAllForks) && opts.skipCreateStateCacheIfAvailable - ? anchorState - : createCachedBeaconState(anchorState as BeaconStateAllForks, { - config, - pubkey2index: new PubkeyIndexMap(), - index2pubkey: [], - }); - this._earliestAvailableSlot = cachedState.slot; + this._earliestAvailableSlot = anchorState.slot; this.shufflingCache = new ShufflingCache(metrics, logger, this.opts, [ { From 48285554eb23d87aa06d5d55b4afe6fcaf99bb07 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 15 Jan 2026 17:28:27 +0530 Subject: [PATCH 17/29] fix metrics --- packages/beacon-node/src/chain/shufflingCache.ts | 3 ++- packages/beacon-node/src/metrics/metrics/lodestar.ts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/beacon-node/src/chain/shufflingCache.ts b/packages/beacon-node/src/chain/shufflingCache.ts index 6f356cde9a8d..2d92588a0370 100644 --- a/packages/beacon-node/src/chain/shufflingCache.ts +++ b/packages/beacon-node/src/chain/shufflingCache.ts @@ -157,7 +157,8 @@ export class ShufflingCache { (Date.now() - cacheItem.timeInsertedMs) / 1000 ); } else { - this.metrics?.shufflingCache.shufflingBuiltMultipleTimes.inc(); + this.metrics?.shufflingCache.shufflingSetMultipleTimes.inc(); + return; } } // set the shuffling diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index fbae0a98adfe..2f01fe13c024 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -1308,9 +1308,9 @@ export function createLodestarMetrics( name: "lodestar_shuffling_cache_miss_count", help: "Count of shuffling cache miss", }), - shufflingBuiltMultipleTimes: register.gauge({ - name: "lodestar_shuffling_cache_recalculated_shuffling_count", - help: "Count of shuffling that were build multiple times", + shufflingSetMultipleTimes: register.gauge({ + name: "lodestar_shuffling_cache_set_multiple_times_count", + help: "Count of shuffling that were set multiple times", }), shufflingPromiseNotResolved: register.gauge({ name: "lodestar_shuffling_cache_promise_not_resolved_count", From 7f81ffcede6769b1e279ec6531b12c31e03d438f Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 15 Jan 2026 17:42:38 +0530 Subject: [PATCH 18/29] use set in brackets --- packages/beacon-node/test/unit/chain/shufflingCache.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/beacon-node/test/unit/chain/shufflingCache.test.ts b/packages/beacon-node/test/unit/chain/shufflingCache.test.ts index f9380c67781c..b8a9602ea6ce 100644 --- a/packages/beacon-node/test/unit/chain/shufflingCache.test.ts +++ b/packages/beacon-node/test/unit/chain/shufflingCache.test.ts @@ -29,7 +29,7 @@ describe("ShufflingCache", () => { shufflingCache.insertPromise(currentEpoch, "0x00"); expect(await shufflingCache.get(currentEpoch, currentDecisionRoot)).toEqual(state.epochCtx.currentShuffling); // insert shuffling at other epochs does prune the cache - shufflingCache.set(state.epochCtx.previousShuffling, state.epochCtx.previousDecisionRoot); + shufflingCache["set"](state.epochCtx.previousShuffling, state.epochCtx.previousDecisionRoot); // the current shuffling is not available anymore expect(await shufflingCache.get(currentEpoch, currentDecisionRoot)).toBeNull(); }); @@ -40,7 +40,7 @@ describe("ShufflingCache", () => { shufflingCache.insertPromise(previousEpoch, previousDecisionRoot); const shufflingRequest0 = shufflingCache.get(previousEpoch, previousDecisionRoot); const shufflingRequest1 = shufflingCache.get(previousEpoch, previousDecisionRoot); - shufflingCache.set(state.epochCtx.previousShuffling, previousDecisionRoot); + shufflingCache["set"](state.epochCtx.previousShuffling, previousDecisionRoot); expect(await shufflingRequest0).toEqual(state.epochCtx.previousShuffling); expect(await shufflingRequest1).toEqual(state.epochCtx.previousShuffling); }); From 2eb4ee05d5f1bf6efae6556fed4291ea8fb8c55a Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Thu, 15 Jan 2026 20:56:33 +0530 Subject: [PATCH 19/29] cache shufflings on checkpoint event for regen states and fix test --- packages/beacon-node/src/chain/chain.ts | 10 ++++++++++ .../test/unit/cachedBeaconState.test.ts | 13 +++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 4b4e9f7f062e..1a162f11dfbb 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -418,6 +418,7 @@ export class BeaconChain implements IBeaconChain { clock.addListener(ClockEvent.epoch, this.onClockEpoch.bind(this)); emitter.addListener(ChainEvent.forkChoiceFinalized, this.onForkChoiceFinalized.bind(this)); emitter.addListener(ChainEvent.forkChoiceJustified, this.onForkChoiceJustified.bind(this)); + emitter.addListener(ChainEvent.checkpoint, this.onCheckpoint.bind(this)); } async init(): Promise { @@ -1165,6 +1166,15 @@ export class BeaconChain implements IBeaconChain { this.logger.verbose("Fork choice justified", {epoch: cp.epoch, root: cp.rootHex}); } + private onCheckpoint(this: BeaconChain, _checkpoint: phase0.Checkpoint, state: CachedBeaconStateAllForks): void { + const {epochCtx} = state; + this.shufflingCache["set"](epochCtx.previousShuffling, epochCtx.previousDecisionRoot); + this.shufflingCache["set"](epochCtx.currentShuffling, epochCtx.currentDecisionRoot); + if (epochCtx.nextShuffling !== null) { + this.shufflingCache["set"](epochCtx.nextShuffling, epochCtx.nextDecisionRoot); + } + } + private async onForkChoiceFinalized(this: BeaconChain, cp: CheckpointWithHex): Promise { this.logger.verbose("Fork choice finalized", {epoch: cp.epoch, root: cp.rootHex}); this.seenBlockProposers.prune(computeStartSlotAtEpoch(cp.epoch)); diff --git a/packages/state-transition/test/unit/cachedBeaconState.test.ts b/packages/state-transition/test/unit/cachedBeaconState.test.ts index e2387a4e0b72..bbad6831414c 100644 --- a/packages/state-transition/test/unit/cachedBeaconState.test.ts +++ b/packages/state-transition/test/unit/cachedBeaconState.test.ts @@ -160,12 +160,6 @@ describe("CachedBeaconState", () => { // confirm loadState() result const stateBytes = state.serialize(); - const newCachedState = loadCachedBeaconState(seedState, stateBytes, { - skipSyncCommitteeCache: true, - }); - const newStateBytes = newCachedState.serialize(); - expect(newStateBytes).toEqual(stateBytes); - expect(newCachedState.hashTreeRoot()).toEqual(state.hashTreeRoot()); const shufflingGetter = (shufflingEpoch: Epoch, dependentRoot: RootHex): EpochShuffling | null => { if ( shufflingEpoch === seedState.epochCtx.epoch - 1 && @@ -190,6 +184,13 @@ describe("CachedBeaconState", () => { return null; }; + const newCachedState = loadCachedBeaconState(seedState, stateBytes, { + skipSyncCommitteeCache: true, + shufflingGetter, + }); + const newStateBytes = newCachedState.serialize(); + expect(newStateBytes).toEqual(stateBytes); + expect(newCachedState.hashTreeRoot()).toEqual(state.hashTreeRoot()); const cachedState = createCachedBeaconState( state, { From f81861db93cde079d5b2fdbc0927cefde2c23486 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Fri, 16 Jan 2026 14:20:34 +0530 Subject: [PATCH 20/29] fix: cache all shufflings via processState on checkpoint event --- .../src/chain/blocks/importBlock.ts | 6 ++--- packages/beacon-node/src/chain/chain.ts | 10 +++---- .../beacon-node/src/chain/shufflingCache.ts | 27 +++++++++++-------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index 74f66cc59193..d7ddb588ee52 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -418,10 +418,10 @@ export async function importBlock( this.logger.verbose("After importBlock caching postState without SSZ cache", {slot: postState.slot}); } - // Cache the next epoch shuffling when crossing an epoch boundary + // Cache shufflings when crossing an epoch boundary const parentEpoch = computeEpochAtSlot(parentBlockSlot); - if (parentEpoch < blockEpoch && postState.epochCtx.nextShuffling !== null) { - this.shufflingCache.processState(postState, postState.epochCtx.nextShuffling.epoch); + if (parentEpoch < blockEpoch) { + this.shufflingCache.processState(postState); this.logger.verbose("Processed shuffling for next epoch", {parentEpoch, blockEpoch, slot: blockSlot}); } diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 1a162f11dfbb..119307f4f129 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -983,7 +983,8 @@ export class BeaconChain implements IBeaconChain { state = await this.regen.getState(attHeadBlock.stateRoot, regenCaller); } // resolve the promise to unblock other calls of the same epoch and dependent root - return this.shufflingCache.processState(state, attEpoch); + this.shufflingCache.processState(state); + return state.epochCtx.getShufflingAtEpoch(attEpoch); } /** @@ -1167,12 +1168,7 @@ export class BeaconChain implements IBeaconChain { } private onCheckpoint(this: BeaconChain, _checkpoint: phase0.Checkpoint, state: CachedBeaconStateAllForks): void { - const {epochCtx} = state; - this.shufflingCache["set"](epochCtx.previousShuffling, epochCtx.previousDecisionRoot); - this.shufflingCache["set"](epochCtx.currentShuffling, epochCtx.currentDecisionRoot); - if (epochCtx.nextShuffling !== null) { - this.shufflingCache["set"](epochCtx.nextShuffling, epochCtx.nextDecisionRoot); - } + this.shufflingCache.processState(state); } private async onForkChoiceFinalized(this: BeaconChain, cp: CheckpointWithHex): Promise { diff --git a/packages/beacon-node/src/chain/shufflingCache.ts b/packages/beacon-node/src/chain/shufflingCache.ts index 2d92588a0370..a24e03f25ba1 100644 --- a/packages/beacon-node/src/chain/shufflingCache.ts +++ b/packages/beacon-node/src/chain/shufflingCache.ts @@ -1,6 +1,6 @@ -import {CachedBeaconStateAllForks, EpochShuffling, attesterShufflingDecisionRoot} from "@lodestar/state-transition"; +import {CachedBeaconStateAllForks, EpochShuffling} from "@lodestar/state-transition"; import {Epoch, RootHex} from "@lodestar/types"; -import {LodestarError, Logger, MapDef, pruneSetToMax, toRootHex} from "@lodestar/utils"; +import {LodestarError, Logger, MapDef, pruneSetToMax} from "@lodestar/utils"; import {Metrics} from "../metrics/metrics.js"; /** @@ -129,17 +129,22 @@ export class ShufflingCache { } /** - * Process a post state to extract and cache the shuffling for the given epoch. - * Extracts the decision root from the state internally. - * Returns the shuffling for use by callers that need it immediately. + * Process a state to extract and cache all shufflings (previous, current, next). + * Uses the stored decision roots from epochCtx. */ - processState(state: CachedBeaconStateAllForks, epoch: Epoch): EpochShuffling { - const shuffling = state.epochCtx.getShufflingAtEpoch(epoch); - const decisionRoot = attesterShufflingDecisionRoot(state, epoch); - if (decisionRoot !== null) { - this.set(shuffling, toRootHex(decisionRoot)); + processState(state: CachedBeaconStateAllForks): void { + const {epochCtx} = state; + + // Cache previous shuffling + this.set(epochCtx.previousShuffling, epochCtx.previousDecisionRoot); + + // Cache current shuffling + this.set(epochCtx.currentShuffling, epochCtx.currentDecisionRoot); + + // Cache next shuffling if it exists + if (epochCtx.nextShuffling !== null) { + this.set(epochCtx.nextShuffling, epochCtx.nextDecisionRoot); } - return shuffling; } /** From f3aff93283c825cd478b17d1321dcdec22e64cbe Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Fri, 16 Jan 2026 16:58:10 +0530 Subject: [PATCH 21/29] remove null check --- packages/beacon-node/src/chain/shufflingCache.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/beacon-node/src/chain/shufflingCache.ts b/packages/beacon-node/src/chain/shufflingCache.ts index a24e03f25ba1..19ddb7c1b763 100644 --- a/packages/beacon-node/src/chain/shufflingCache.ts +++ b/packages/beacon-node/src/chain/shufflingCache.ts @@ -141,10 +141,8 @@ export class ShufflingCache { // Cache current shuffling this.set(epochCtx.currentShuffling, epochCtx.currentDecisionRoot); - // Cache next shuffling if it exists - if (epochCtx.nextShuffling !== null) { - this.set(epochCtx.nextShuffling, epochCtx.nextDecisionRoot); - } + // Cache next shuffling + this.set(epochCtx.nextShuffling, epochCtx.nextDecisionRoot); } /** From 62bce647883ca230fd6661e873ffb20caa22ddcc Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Fri, 16 Jan 2026 16:59:33 +0530 Subject: [PATCH 22/29] use calculateDecisionRoot --- packages/state-transition/src/cache/epochCache.ts | 7 +++---- packages/state-transition/src/util/epochShuffling.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 4d77fd02958c..100cf9db6d10 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -10,7 +10,6 @@ import { GENESIS_EPOCH, PROPOSER_WEIGHT, SLOTS_PER_EPOCH, - SLOTS_PER_HISTORICAL_ROOT, WEIGHT_DENOMINATOR, } from "@lodestar/params"; import { @@ -28,10 +27,10 @@ import { gloas, phase0, } from "@lodestar/types"; -import {LodestarError, toRootHex} from "@lodestar/utils"; +import {LodestarError} from "@lodestar/utils"; import {getTotalSlashingsByIncrement} from "../epoch/processSlashings.js"; import {AttesterDuty, calculateCommitteeAssignments} from "../util/calculateCommitteeAssignments.js"; -import {EpochShuffling, calculateShufflingDecisionRoot, computeEpochShuffling} from "../util/epochShuffling.js"; +import {EpochShuffling, calculateDecisionRoot, calculateShufflingDecisionRoot, computeEpochShuffling} from "../util/epochShuffling.js"; import { computeActivationExitEpoch, computeEpochAtSlot, @@ -626,7 +625,7 @@ export class EpochCache { // Pre-Fulu, we need to compute it here since processProposerLookahead doesn't run. // // See: https://eips.ethereum.org/EIPS/eip-7917 - this.nextDecisionRoot = toRootHex(state.blockRoots.get(state.slot % SLOTS_PER_HISTORICAL_ROOT)); + this.nextDecisionRoot = calculateDecisionRoot(state, epochAfterUpcoming); this.nextActiveIndices = epochTransitionCache.nextShufflingActiveIndices; this.nextShuffling = epochTransitionCache.nextShuffling ?? computeEpochShuffling(state, this.nextActiveIndices, epochAfterUpcoming); diff --git a/packages/state-transition/src/util/epochShuffling.ts b/packages/state-transition/src/util/epochShuffling.ts index 1bd197d12eed..24880ec83adf 100644 --- a/packages/state-transition/src/util/epochShuffling.ts +++ b/packages/state-transition/src/util/epochShuffling.ts @@ -123,7 +123,7 @@ export async function computeEpochShufflingAsync( }; } -function calculateDecisionRoot(state: BeaconStateAllForks, epoch: Epoch): RootHex { +export function calculateDecisionRoot(state: BeaconStateAllForks, epoch: Epoch): RootHex { const pivotSlot = computeStartSlotAtEpoch(epoch - 1) - 1; return toRootHex(getBlockRootAtSlot(state, pivotSlot)); } From 3dc07b852cd779d88cd6d7fbc913c130968f99a1 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 17 Jan 2026 15:44:09 +0530 Subject: [PATCH 23/29] use setImmediate to defer processing --- packages/beacon-node/src/chain/chain.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 119307f4f129..ef707e96e899 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -1168,7 +1168,9 @@ export class BeaconChain implements IBeaconChain { } private onCheckpoint(this: BeaconChain, _checkpoint: phase0.Checkpoint, state: CachedBeaconStateAllForks): void { - this.shufflingCache.processState(state); + setImmediate(() => { + this.shufflingCache.processState(state); + }); } private async onForkChoiceFinalized(this: BeaconChain, cp: CheckpointWithHex): Promise { From 0681f48dcbf8efe1e8eebe5bf05f36d661b809ec Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Sat, 17 Jan 2026 15:44:17 +0530 Subject: [PATCH 24/29] pnpm lint --- packages/state-transition/src/cache/epochCache.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 100cf9db6d10..5054d9402754 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -30,7 +30,12 @@ import { import {LodestarError} from "@lodestar/utils"; import {getTotalSlashingsByIncrement} from "../epoch/processSlashings.js"; import {AttesterDuty, calculateCommitteeAssignments} from "../util/calculateCommitteeAssignments.js"; -import {EpochShuffling, calculateDecisionRoot, calculateShufflingDecisionRoot, computeEpochShuffling} from "../util/epochShuffling.js"; +import { + EpochShuffling, + calculateDecisionRoot, + calculateShufflingDecisionRoot, + computeEpochShuffling, +} from "../util/epochShuffling.js"; import { computeActivationExitEpoch, computeEpochAtSlot, From eb05b91006a4e3b0afb282c5b6c0323f1c166f3d Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Mon, 19 Jan 2026 12:54:22 +0530 Subject: [PATCH 25/29] use callInNextEventLoop instead of setImmediate --- packages/beacon-node/src/chain/chain.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index ef707e96e899..b7c7779618dc 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -52,6 +52,7 @@ 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"; import {ensureDir, writeIfNotExist} from "../util/file.js"; import {isOptimisticBlock} from "../util/forkChoice.js"; import {SerializedCache} from "../util/serializedCache.js"; @@ -1168,7 +1169,8 @@ export class BeaconChain implements IBeaconChain { } private onCheckpoint(this: BeaconChain, _checkpoint: phase0.Checkpoint, state: CachedBeaconStateAllForks): void { - setImmediate(() => { + // Defer to not block other checkpoint event handlers, which can cause lightclient update delays + callInNextEventLoop(() => { this.shufflingCache.processState(state); }); } From f75cd15626de749372d7876a22d2d9a6c0185f1b Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Tue, 20 Jan 2026 01:27:38 +0800 Subject: [PATCH 26/29] chore: fix parse-path build error --- pnpm-lock.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad083f5431f3..efc652b92011 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3110,10 +3110,6 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@types/parse-path@7.1.0': - resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==} - deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed. - '@types/proper-lockfile@4.1.4': resolution: {integrity: sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==} @@ -9819,10 +9815,6 @@ snapshots: '@types/normalize-package-data@2.4.4': {} - '@types/parse-path@7.1.0': - dependencies: - parse-path: 7.0.0 - '@types/proper-lockfile@4.1.4': dependencies: '@types/retry': 0.12.2 @@ -12904,7 +12896,6 @@ snapshots: parse-url@9.2.0: dependencies: - '@types/parse-path': 7.1.0 parse-path: 7.0.0 parse5@7.1.2: From e24b5f37b3bb0752e74d09205cdcf7daf8af6fe0 Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Tue, 20 Jan 2026 16:03:29 +0530 Subject: [PATCH 27/29] remove shufflingCalculationTime metric --- packages/beacon-node/src/metrics/metrics/lodestar.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index 2f01fe13c024..2def2c231f42 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -1321,11 +1321,6 @@ export function createLodestarMetrics( help: "Time from promise insertion until promise resolution when shuffling was ready in seconds", buckets: [0.5, 1, 1.5, 2], }), - shufflingCalculationTime: register.histogram({ - name: "lodestar_shuffling_cache_shuffling_calculation_time_seconds", - help: "Run time of shuffling calculation", - buckets: [0.5, 0.75, 1, 1.25, 1.5], - }), }, seenCache: { From c2f380102177d6484373738fc8d5f5f061683417 Mon Sep 17 00:00:00 2001 From: matthewkeil Date: Fri, 23 Jan 2026 03:27:22 +0700 Subject: [PATCH 28/29] Revert "chore: fix parse-path build error" This reverts commit f75cd15626de749372d7876a22d2d9a6c0185f1b. --- pnpm-lock.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efc652b92011..ad083f5431f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3110,6 +3110,10 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/parse-path@7.1.0': + resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==} + deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed. + '@types/proper-lockfile@4.1.4': resolution: {integrity: sha512-uo2ABllncSqg9F1D4nugVl9v93RmjxF6LJzQLMLDdPaXCUIDPeOJ21Gbqi43xNKzBi/WQ0Q0dICqufzQbMjipQ==} @@ -9815,6 +9819,10 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/parse-path@7.1.0': + dependencies: + parse-path: 7.0.0 + '@types/proper-lockfile@4.1.4': dependencies: '@types/retry': 0.12.2 @@ -12896,6 +12904,7 @@ snapshots: parse-url@9.2.0: dependencies: + '@types/parse-path': 7.1.0 parse-path: 7.0.0 parse5@7.1.2: From 86b0359251898298b5dede678cb902c53aa14cad Mon Sep 17 00:00:00 2001 From: Rahul Guha <19rahul2003@gmail.com> Date: Fri, 23 Jan 2026 02:11:07 +0530 Subject: [PATCH 29/29] remove the unused metrics --- dashboards/lodestar_beacon_chain.json | 43 ++------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/dashboards/lodestar_beacon_chain.json b/dashboards/lodestar_beacon_chain.json index 04817bfbe768..15da60130f9d 100644 --- a/dashboards/lodestar_beacon_chain.json +++ b/dashboards/lodestar_beacon_chain.json @@ -948,26 +948,13 @@ "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", - "expr": "rate(lodestar_shuffling_cache_recalculated_shuffling_count[32m]) * 384", + "expr": "rate(lodestar_shuffling_cache_set_multiple_times_count[32m]) * 384", "hide": false, "instant": false, - "legendFormat": "built_multiple_times", + "legendFormat": "set_multiple_times", "range": true, "refId": "D" }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "rate(lodestar_shuffling_cache_promise_not_resolved_and_thrown_away_count[32m]) * 384", - "hide": false, - "instant": false, - "legendFormat": "not_resolved_thrown_away", - "range": true, - "refId": "E" - }, { "datasource": { "type": "prometheus", @@ -980,19 +967,6 @@ "legendFormat": "not_resolved", "range": true, "refId": "F" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "rate(lodestar_shuffling_cache_next_shuffling_not_on_epoch_cache[32m]) * 384", - "hide": false, - "instant": false, - "legendFormat": "next_shuffling_not_on_cache", - "range": true, - "refId": "G" } ], "title": "Insert vs Hit vs Miss", @@ -1076,19 +1050,6 @@ "legendFormat": "resolution", "range": true, "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${DS_PROMETHEUS}" - }, - "editorMode": "code", - "expr": "rate(lodestar_shuffling_cache_shuffling_calculation_time_seconds_sum[32m])\n/\nrate(lodestar_shuffling_cache_shuffling_calculation_time_seconds_count[32m])", - "hide": false, - "instant": false, - "legendFormat": "calculation_{{source}}", - "range": true, - "refId": "B" } ], "title": "Timing",