@lodestar/state-transition 1.23.0-dev.bb40ef7eb7 → 1.23.0-dev.d0ba6bc3cc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,28 @@
1
1
  import { PublicKey } from "@chainsafe/blst";
2
2
  import * as immutable from "immutable";
3
- import { BLSSignature, CommitteeIndex, Epoch, Slot, ValidatorIndex, phase0, SyncPeriod, Attestation, IndexedAttestation } from "@lodestar/types";
3
+ import { BLSSignature, CommitteeIndex, Epoch, Slot, ValidatorIndex, phase0, RootHex, SyncPeriod, Attestation, IndexedAttestation } from "@lodestar/types";
4
4
  import { BeaconConfig, ChainConfig } from "@lodestar/config";
5
5
  import { ForkSeq } from "@lodestar/params";
6
6
  import { LodestarError } from "@lodestar/utils";
7
- import { EpochShuffling } from "../util/epochShuffling.js";
7
+ import { EpochShuffling, IShufflingCache } from "../util/epochShuffling.js";
8
+ import { AttesterDuty } from "../util/calculateCommitteeAssignments.js";
8
9
  import { EpochCacheMetrics } from "../metrics.js";
9
10
  import { EffectiveBalanceIncrements } from "./effectiveBalanceIncrements.js";
11
+ import { BeaconStateAllForks } from "./types.js";
10
12
  import { Index2PubkeyCache, PubkeyIndexMap, UnfinalizedPubkeyIndexMap, PubkeyHex } from "./pubkeyCache.js";
11
- import { BeaconStateAllForks, ShufflingGetter } from "./types.js";
12
13
  import { SyncCommitteeCache } from "./syncCommitteeCache.js";
14
+ import { CachedBeaconStateAllForks } from "./stateCache.js";
13
15
  /** `= PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT)` */
14
16
  export declare const PROPOSER_WEIGHT_FACTOR: number;
15
17
  export type EpochCacheImmutableData = {
16
18
  config: BeaconConfig;
17
19
  pubkey2index: PubkeyIndexMap;
18
20
  index2pubkey: Index2PubkeyCache;
21
+ shufflingCache?: IShufflingCache;
19
22
  };
20
23
  export type EpochCacheOpts = {
21
24
  skipSyncCommitteeCache?: boolean;
22
25
  skipSyncPubkeys?: boolean;
23
- shufflingGetter?: ShufflingGetter;
24
26
  };
25
27
  /** Defers computing proposers by persisting only the seed, and dropping it once indexes are computed */
26
28
  type ProposersDeferred = {
@@ -79,6 +81,11 @@ export declare class EpochCache {
79
81
  * Unique pubkey registry shared in the same fork. There should only exist one for the fork.
80
82
  */
81
83
  unfinalizedPubkey2index: UnfinalizedPubkeyIndexMap;
84
+ /**
85
+ * ShufflingCache is passed in from `beacon-node` so should be available at runtime but may not be
86
+ * present during testing.
87
+ */
88
+ shufflingCache?: IShufflingCache;
82
89
  /**
83
90
  * Indexes of the block proposers for the current epoch.
84
91
  *
@@ -93,6 +100,12 @@ export declare class EpochCache {
93
100
  * should be in the epoch context.
94
101
  */
95
102
  proposersNextEpoch: ProposersDeferred;
103
+ /**
104
+ * Epoch decision roots to look up correct shuffling from the Shuffling Cache
105
+ */
106
+ previousDecisionRoot: RootHex;
107
+ currentDecisionRoot: RootHex;
108
+ nextDecisionRoot: RootHex;
96
109
  /**
97
110
  * Shuffling of validator indexes. Immutable through the epoch, then it's replaced entirely.
98
111
  * Note: Per spec definition, shuffling will always be defined. They are never called before loadState()
@@ -103,7 +116,12 @@ export declare class EpochCache {
103
116
  /** Same as previousShuffling */
104
117
  currentShuffling: EpochShuffling;
105
118
  /** Same as previousShuffling */
106
- nextShuffling: EpochShuffling;
119
+ nextShuffling: EpochShuffling | null;
120
+ /**
121
+ * Cache nextActiveIndices so that in afterProcessEpoch the next shuffling can be build synchronously
122
+ * in case it is not built or the ShufflingCache is not available
123
+ */
124
+ nextActiveIndices: Uint32Array;
107
125
  /**
108
126
  * Effective balances, for altair processAttestations()
109
127
  */
@@ -169,7 +187,6 @@ export declare class EpochCache {
169
187
  currentSyncCommitteeIndexed: SyncCommitteeCache;
170
188
  /** TODO: Indexed SyncCommitteeCache */
171
189
  nextSyncCommitteeIndexed: SyncCommitteeCache;
172
- epoch: Epoch;
173
190
  syncPeriod: SyncPeriod;
174
191
  /**
175
192
  * state.validators.length of every state at epoch boundary
@@ -180,17 +197,24 @@ export declare class EpochCache {
180
197
  * then the list will be (in terms of epoch) [103, 104, 105]
181
198
  */
182
199
  historicalValidatorLengths: immutable.List<number>;
200
+ epoch: Epoch;
201
+ get nextEpoch(): Epoch;
183
202
  constructor(data: {
184
203
  config: BeaconConfig;
185
204
  pubkey2index: PubkeyIndexMap;
186
205
  index2pubkey: Index2PubkeyCache;
187
206
  unfinalizedPubkey2index: UnfinalizedPubkeyIndexMap;
207
+ shufflingCache?: IShufflingCache;
188
208
  proposers: number[];
189
209
  proposersPrevEpoch: number[] | null;
190
210
  proposersNextEpoch: ProposersDeferred;
211
+ previousDecisionRoot: RootHex;
212
+ currentDecisionRoot: RootHex;
213
+ nextDecisionRoot: RootHex;
191
214
  previousShuffling: EpochShuffling;
192
215
  currentShuffling: EpochShuffling;
193
- nextShuffling: EpochShuffling;
216
+ nextShuffling: EpochShuffling | null;
217
+ nextActiveIndices: Uint32Array;
194
218
  effectiveBalanceIncrements: EffectiveBalanceIncrements;
195
219
  totalSlashingsByIncrement: number;
196
220
  syncParticipantReward: number;
@@ -215,19 +239,23 @@ export declare class EpochCache {
215
239
  *
216
240
  * SLOW CODE - 🐢
217
241
  */
218
- static createFromState(state: BeaconStateAllForks, { config, pubkey2index, index2pubkey }: EpochCacheImmutableData, opts?: EpochCacheOpts): EpochCache;
242
+ static createFromState(state: BeaconStateAllForks, { config, pubkey2index, index2pubkey, shufflingCache }: EpochCacheImmutableData, opts?: EpochCacheOpts): EpochCache;
219
243
  /**
220
244
  * Copies a given EpochCache while avoiding copying its immutable parts.
221
245
  */
222
246
  clone(): EpochCache;
223
247
  /**
224
248
  * Called to re-use information, such as the shuffling of the next epoch, after transitioning into a
225
- * new epoch.
249
+ * new epoch. Also handles pre-computation of values that may change during the upcoming epoch and
250
+ * that get used in the following epoch transition. Often those pre-computations are not used by the
251
+ * chain but are courtesy values that are served via the API for epoch look ahead of duties.
252
+ *
253
+ * Steps for afterProcessEpoch
254
+ * 1) update previous/current/next values of cached items
226
255
  */
227
- afterProcessEpoch(state: BeaconStateAllForks, epochTransitionCache: {
228
- indicesEligibleForActivationQueue: ValidatorIndex[];
229
- nextEpochShufflingActiveValidatorIndices: ValidatorIndex[];
230
- nextEpochShufflingActiveIndicesLength: number;
256
+ afterProcessEpoch(state: CachedBeaconStateAllForks, epochTransitionCache: {
257
+ nextShufflingDecisionRoot: RootHex;
258
+ nextShufflingActiveIndices: Uint32Array;
231
259
  nextEpochTotalActiveBalanceByIncrement: number;
232
260
  }): void;
233
261
  beforeEpochTransition(): void;
@@ -330,6 +358,7 @@ export declare class EpochCache {
330
358
  getShufflingAtSlot(slot: Slot): EpochShuffling;
331
359
  getShufflingAtSlotOrNull(slot: Slot): EpochShuffling | null;
332
360
  getShufflingAtEpoch(epoch: Epoch): EpochShuffling;
361
+ getShufflingDecisionRoot(epoch: Epoch): RootHex;
333
362
  getShufflingAtEpochOrNull(epoch: Epoch): EpochShuffling | null;
334
363
  /**
335
364
  * Note: The range of slots a validator has to perform duties is off by one.
@@ -351,17 +380,11 @@ export declare class EpochCache {
351
380
  isPostElectra(): boolean;
352
381
  getValidatorCountAtEpoch(targetEpoch: Epoch): number | undefined;
353
382
  }
354
- type AttesterDuty = {
355
- validatorIndex: ValidatorIndex;
356
- committeeIndex: CommitteeIndex;
357
- committeeLength: number;
358
- committeesAtSlot: number;
359
- validatorCommitteeIndex: number;
360
- slot: Slot;
361
- };
362
383
  export declare enum EpochCacheErrorCode {
363
384
  COMMITTEE_INDEX_OUT_OF_RANGE = "EPOCH_CONTEXT_ERROR_COMMITTEE_INDEX_OUT_OF_RANGE",
364
385
  COMMITTEE_EPOCH_OUT_OF_RANGE = "EPOCH_CONTEXT_ERROR_COMMITTEE_EPOCH_OUT_OF_RANGE",
386
+ DECISION_ROOT_EPOCH_OUT_OF_RANGE = "EPOCH_CONTEXT_ERROR_DECISION_ROOT_EPOCH_OUT_OF_RANGE",
387
+ NEXT_SHUFFLING_NOT_AVAILABLE = "EPOCH_CONTEXT_ERROR_NEXT_SHUFFLING_NOT_AVAILABLE",
365
388
  NO_SYNC_COMMITTEE = "EPOCH_CONTEXT_ERROR_NO_SYNC_COMMITTEE",
366
389
  PROPOSER_EPOCH_MISMATCH = "EPOCH_CONTEXT_ERROR_PROPOSER_EPOCH_MISMATCH"
367
390
  }
@@ -373,6 +396,14 @@ type EpochCacheErrorType = {
373
396
  code: EpochCacheErrorCode.COMMITTEE_EPOCH_OUT_OF_RANGE;
374
397
  requestedEpoch: Epoch;
375
398
  currentEpoch: Epoch;
399
+ } | {
400
+ code: EpochCacheErrorCode.DECISION_ROOT_EPOCH_OUT_OF_RANGE;
401
+ requestedEpoch: Epoch;
402
+ currentEpoch: Epoch;
403
+ } | {
404
+ code: EpochCacheErrorCode.NEXT_SHUFFLING_NOT_AVAILABLE;
405
+ epoch: Epoch;
406
+ decisionRoot: RootHex;
376
407
  } | {
377
408
  code: EpochCacheErrorCode.NO_SYNC_COMMITTEE;
378
409
  epoch: Epoch;
@@ -4,10 +4,11 @@ import { createBeaconConfig } from "@lodestar/config";
4
4
  import { ATTESTATION_SUBNET_COUNT, DOMAIN_BEACON_PROPOSER, EFFECTIVE_BALANCE_INCREMENT, FAR_FUTURE_EPOCH, ForkSeq, GENESIS_EPOCH, PROPOSER_WEIGHT, SLOTS_PER_EPOCH, WEIGHT_DENOMINATOR, } from "@lodestar/params";
5
5
  import { LodestarError, fromHex } from "@lodestar/utils";
6
6
  import { computeActivationExitEpoch, computeEpochAtSlot, computeStartSlotAtEpoch, getChurnLimit, isActiveValidator, isAggregatorFromCommitteeLength, computeSyncPeriodAtEpoch, getSeed, computeProposers, getActivationChurnLimit, } from "../util/index.js";
7
- import { computeEpochShuffling, getShufflingDecisionBlock } from "../util/epochShuffling.js";
7
+ import { computeEpochShuffling, calculateShufflingDecisionRoot, } from "../util/epochShuffling.js";
8
8
  import { computeBaseRewardPerIncrement, computeSyncParticipantReward } from "../util/syncCommittee.js";
9
9
  import { sumTargetUnslashedBalanceIncrements } from "../util/targetUnslashedBalance.js";
10
10
  import { getTotalSlashingsByIncrement } from "../epoch/processSlashings.js";
11
+ import { calculateCommitteeAssignments } from "../util/calculateCommitteeAssignments.js";
11
12
  import { getEffectiveBalanceIncrementsWithLen } from "./effectiveBalanceIncrements.js";
12
13
  import { PubkeyIndexMap, syncPubkeys, toMemoryEfficientHexStr, newUnfinalizedPubkeyIndexMap, } from "./pubkeyCache.js";
13
14
  import { computeSyncCommitteeCache, getSyncCommitteeCache, SyncCommitteeCacheEmpty, } from "./syncCommitteeCache.js";
@@ -36,17 +37,25 @@ export const PROPOSER_WEIGHT_FACTOR = PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PR
36
37
  * - syncPeriod
37
38
  **/
38
39
  export class EpochCache {
40
+ get nextEpoch() {
41
+ return this.epoch + 1;
42
+ }
39
43
  constructor(data) {
40
44
  this.config = data.config;
41
45
  this.pubkey2index = data.pubkey2index;
42
46
  this.index2pubkey = data.index2pubkey;
43
47
  this.unfinalizedPubkey2index = data.unfinalizedPubkey2index;
48
+ this.shufflingCache = data.shufflingCache;
44
49
  this.proposers = data.proposers;
45
50
  this.proposersPrevEpoch = data.proposersPrevEpoch;
46
51
  this.proposersNextEpoch = data.proposersNextEpoch;
52
+ this.previousDecisionRoot = data.previousDecisionRoot;
53
+ this.currentDecisionRoot = data.currentDecisionRoot;
54
+ this.nextDecisionRoot = data.nextDecisionRoot;
47
55
  this.previousShuffling = data.previousShuffling;
48
56
  this.currentShuffling = data.currentShuffling;
49
57
  this.nextShuffling = data.nextShuffling;
58
+ this.nextActiveIndices = data.nextActiveIndices;
50
59
  this.effectiveBalanceIncrements = data.effectiveBalanceIncrements;
51
60
  this.totalSlashingsByIncrement = data.totalSlashingsByIncrement;
52
61
  this.syncParticipantReward = data.syncParticipantReward;
@@ -71,7 +80,7 @@ export class EpochCache {
71
80
  *
72
81
  * SLOW CODE - 🐢
73
82
  */
74
- static createFromState(state, { config, pubkey2index, index2pubkey }, opts) {
83
+ static createFromState(state, { config, pubkey2index, index2pubkey, shufflingCache }, opts) {
75
84
  const currentEpoch = computeEpochAtSlot(state.slot);
76
85
  const isGenesis = currentEpoch === GENESIS_EPOCH;
77
86
  const previousEpoch = isGenesis ? GENESIS_EPOCH : currentEpoch - 1;
@@ -88,17 +97,17 @@ export class EpochCache {
88
97
  }
89
98
  const effectiveBalanceIncrements = getEffectiveBalanceIncrementsWithLen(validatorCount);
90
99
  const totalSlashingsByIncrement = getTotalSlashingsByIncrement(state);
91
- const previousActiveIndices = [];
92
- const currentActiveIndices = [];
93
- const nextActiveIndices = [];
100
+ const previousActiveIndicesAsNumberArray = [];
101
+ const currentActiveIndicesAsNumberArray = [];
102
+ const nextActiveIndicesAsNumberArray = [];
94
103
  // BeaconChain could provide a shuffling cache to avoid re-computing shuffling every epoch
95
104
  // in that case, we don't need to compute shufflings again
96
- const previousShufflingDecisionBlock = getShufflingDecisionBlock(state, previousEpoch);
97
- const cachedPreviousShuffling = opts?.shufflingGetter?.(previousEpoch, previousShufflingDecisionBlock);
98
- const currentShufflingDecisionBlock = getShufflingDecisionBlock(state, currentEpoch);
99
- const cachedCurrentShuffling = opts?.shufflingGetter?.(currentEpoch, currentShufflingDecisionBlock);
100
- const nextShufflingDecisionBlock = getShufflingDecisionBlock(state, nextEpoch);
101
- const cachedNextShuffling = opts?.shufflingGetter?.(nextEpoch, nextShufflingDecisionBlock);
105
+ const previousDecisionRoot = calculateShufflingDecisionRoot(config, state, previousEpoch);
106
+ const cachedPreviousShuffling = shufflingCache?.getSync(previousEpoch, previousDecisionRoot);
107
+ const currentDecisionRoot = calculateShufflingDecisionRoot(config, state, currentEpoch);
108
+ const cachedCurrentShuffling = shufflingCache?.getSync(currentEpoch, currentDecisionRoot);
109
+ const nextDecisionRoot = calculateShufflingDecisionRoot(config, state, nextEpoch);
110
+ const cachedNextShuffling = shufflingCache?.getSync(nextEpoch, nextDecisionRoot);
102
111
  for (let i = 0; i < validatorCount; i++) {
103
112
  const validator = validators[i];
104
113
  // Note: Not usable for fork-choice balances since in-active validators are not zero'ed
@@ -106,17 +115,17 @@ export class EpochCache {
106
115
  // we only need to track active indices for previous, current and next epoch if we have to compute shufflings
107
116
  // skip doing that if we already have cached shufflings
108
117
  if (cachedPreviousShuffling == null && isActiveValidator(validator, previousEpoch)) {
109
- previousActiveIndices.push(i);
118
+ previousActiveIndicesAsNumberArray.push(i);
110
119
  }
111
120
  if (isActiveValidator(validator, currentEpoch)) {
112
121
  if (cachedCurrentShuffling == null) {
113
- currentActiveIndices.push(i);
122
+ currentActiveIndicesAsNumberArray.push(i);
114
123
  }
115
124
  // We track totalActiveBalanceIncrements as ETH to fit total network balance in a JS number (53 bits)
116
125
  totalActiveBalanceIncrements += effectiveBalanceIncrements[i];
117
126
  }
118
127
  if (cachedNextShuffling == null && isActiveValidator(validator, nextEpoch)) {
119
- nextActiveIndices.push(i);
128
+ nextActiveIndicesAsNumberArray.push(i);
120
129
  }
121
130
  const { exitEpoch } = validator;
122
131
  if (exitEpoch !== FAR_FUTURE_EPOCH) {
@@ -137,13 +146,40 @@ export class EpochCache {
137
146
  else if (totalActiveBalanceIncrements >= Number.MAX_SAFE_INTEGER) {
138
147
  throw Error("totalActiveBalanceIncrements >= Number.MAX_SAFE_INTEGER. MAX_EFFECTIVE_BALANCE is too low.");
139
148
  }
140
- const currentShuffling = cachedCurrentShuffling ??
141
- computeEpochShuffling(state, currentActiveIndices, currentActiveIndices.length, currentEpoch);
142
- const previousShuffling = cachedPreviousShuffling ??
143
- (isGenesis
144
- ? currentShuffling
145
- : computeEpochShuffling(state, previousActiveIndices, previousActiveIndices.length, previousEpoch));
146
- const nextShuffling = cachedNextShuffling ?? computeEpochShuffling(state, nextActiveIndices, nextActiveIndices.length, nextEpoch);
149
+ const nextActiveIndices = new Uint32Array(nextActiveIndicesAsNumberArray);
150
+ let previousShuffling;
151
+ let currentShuffling;
152
+ let nextShuffling;
153
+ if (!shufflingCache) {
154
+ // Only for testing. shufflingCache should always be available in prod
155
+ previousShuffling = computeEpochShuffling(state, new Uint32Array(previousActiveIndicesAsNumberArray), previousEpoch);
156
+ currentShuffling = isGenesis
157
+ ? previousShuffling
158
+ : computeEpochShuffling(state, new Uint32Array(currentActiveIndicesAsNumberArray), currentEpoch);
159
+ nextShuffling = computeEpochShuffling(state, nextActiveIndices, nextEpoch);
160
+ }
161
+ else {
162
+ currentShuffling = cachedCurrentShuffling
163
+ ? cachedCurrentShuffling
164
+ : shufflingCache.getSync(currentEpoch, currentDecisionRoot, {
165
+ state,
166
+ activeIndices: new Uint32Array(currentActiveIndicesAsNumberArray),
167
+ });
168
+ previousShuffling = cachedPreviousShuffling
169
+ ? cachedPreviousShuffling
170
+ : isGenesis
171
+ ? currentShuffling
172
+ : shufflingCache.getSync(previousEpoch, previousDecisionRoot, {
173
+ state,
174
+ activeIndices: new Uint32Array(previousActiveIndicesAsNumberArray),
175
+ });
176
+ nextShuffling = cachedNextShuffling
177
+ ? cachedNextShuffling
178
+ : shufflingCache.getSync(nextEpoch, nextDecisionRoot, {
179
+ state,
180
+ activeIndices: nextActiveIndices,
181
+ });
182
+ }
147
183
  const currentProposerSeed = getSeed(state, currentEpoch, DOMAIN_BEACON_PROPOSER);
148
184
  // Allow to create CachedBeaconState for empty states, or no active validators
149
185
  const proposers = currentShuffling.activeIndices.length > 0
@@ -207,13 +243,18 @@ export class EpochCache {
207
243
  index2pubkey,
208
244
  // `createFromFinalizedState()` creates cache with empty unfinalizedPubkey2index. Be cautious to only pass in finalized state
209
245
  unfinalizedPubkey2index: newUnfinalizedPubkeyIndexMap(),
246
+ shufflingCache,
210
247
  proposers,
211
248
  // On first epoch, set to null to prevent unnecessary work since this is only used for metrics
212
249
  proposersPrevEpoch: null,
213
250
  proposersNextEpoch,
251
+ previousDecisionRoot,
252
+ currentDecisionRoot,
253
+ nextDecisionRoot,
214
254
  previousShuffling,
215
255
  currentShuffling,
216
256
  nextShuffling,
257
+ nextActiveIndices,
217
258
  effectiveBalanceIncrements,
218
259
  totalSlashingsByIncrement,
219
260
  syncParticipantReward,
@@ -247,13 +288,18 @@ export class EpochCache {
247
288
  index2pubkey: this.index2pubkey,
248
289
  // No need to clone this reference. On each mutation the `unfinalizedPubkey2index` reference is replaced, @see `addPubkey`
249
290
  unfinalizedPubkey2index: this.unfinalizedPubkey2index,
291
+ shufflingCache: this.shufflingCache,
250
292
  // Immutable data
251
293
  proposers: this.proposers,
252
294
  proposersPrevEpoch: this.proposersPrevEpoch,
253
295
  proposersNextEpoch: this.proposersNextEpoch,
296
+ previousDecisionRoot: this.previousDecisionRoot,
297
+ currentDecisionRoot: this.currentDecisionRoot,
298
+ nextDecisionRoot: this.nextDecisionRoot,
254
299
  previousShuffling: this.previousShuffling,
255
300
  currentShuffling: this.currentShuffling,
256
301
  nextShuffling: this.nextShuffling,
302
+ nextActiveIndices: this.nextActiveIndices,
257
303
  // Uint8Array, requires cloning, but it is cloned only when necessary before an epoch transition
258
304
  // See EpochCache.beforeEpochTransition()
259
305
  effectiveBalanceIncrements: this.effectiveBalanceIncrements,
@@ -278,23 +324,79 @@ export class EpochCache {
278
324
  }
279
325
  /**
280
326
  * Called to re-use information, such as the shuffling of the next epoch, after transitioning into a
281
- * new epoch.
327
+ * new epoch. Also handles pre-computation of values that may change during the upcoming epoch and
328
+ * that get used in the following epoch transition. Often those pre-computations are not used by the
329
+ * chain but are courtesy values that are served via the API for epoch look ahead of duties.
330
+ *
331
+ * Steps for afterProcessEpoch
332
+ * 1) update previous/current/next values of cached items
282
333
  */
283
334
  afterProcessEpoch(state, epochTransitionCache) {
335
+ // Because the slot was incremented before entering this function the "next epoch" is actually the "current epoch"
336
+ // in this context but that is not actually true because the state transition happens in the last 4 seconds of the
337
+ // epoch. For the context of this function "upcoming epoch" is used to denote the epoch that will begin after this
338
+ // function returns. The epoch that is "next" once the state transition is complete is referred to as the
339
+ // epochAfterUpcoming for the same reason to help minimize confusion.
340
+ const upcomingEpoch = this.nextEpoch;
341
+ const epochAfterUpcoming = upcomingEpoch + 1;
342
+ // move current to previous
284
343
  this.previousShuffling = this.currentShuffling;
285
- this.currentShuffling = this.nextShuffling;
286
- const currEpoch = this.currentShuffling.epoch;
287
- const nextEpoch = currEpoch + 1;
288
- this.nextShuffling = computeEpochShuffling(state, epochTransitionCache.nextEpochShufflingActiveValidatorIndices, epochTransitionCache.nextEpochShufflingActiveIndicesLength, nextEpoch);
289
- // Roll current proposers into previous proposers for metrics
344
+ this.previousDecisionRoot = this.currentDecisionRoot;
290
345
  this.proposersPrevEpoch = this.proposers;
291
- const currentProposerSeed = getSeed(state, this.currentShuffling.epoch, DOMAIN_BEACON_PROPOSER);
292
- this.proposers = computeProposers(this.config.getForkSeqAtEpoch(currEpoch), currentProposerSeed, this.currentShuffling, this.effectiveBalanceIncrements);
346
+ // move next to current or calculate upcoming
347
+ this.currentDecisionRoot = this.nextDecisionRoot;
348
+ if (this.nextShuffling) {
349
+ // was already pulled from the ShufflingCache to the EpochCache (should be in most cases)
350
+ this.currentShuffling = this.nextShuffling;
351
+ }
352
+ else {
353
+ this.shufflingCache?.metrics?.shufflingCache.nextShufflingNotOnEpochCache.inc();
354
+ this.currentShuffling =
355
+ this.shufflingCache?.getSync(upcomingEpoch, this.currentDecisionRoot, {
356
+ state,
357
+ // have to use the "nextActiveIndices" that were saved in the last transition here to calculate
358
+ // the upcoming shuffling if it is not already built (similar condition to the below computation)
359
+ activeIndices: this.nextActiveIndices,
360
+ }) ??
361
+ // allow for this case during testing where the ShufflingCache is not present, may affect perf testing
362
+ // so should be taken into account when structuring tests. Should not affect unit or other tests though
363
+ computeEpochShuffling(state, this.nextActiveIndices, upcomingEpoch);
364
+ }
365
+ const upcomingProposerSeed = getSeed(state, upcomingEpoch, DOMAIN_BEACON_PROPOSER);
366
+ // next epoch was moved to current epoch so use current here
367
+ this.proposers = computeProposers(this.config.getForkSeqAtEpoch(upcomingEpoch), upcomingProposerSeed, this.currentShuffling, this.effectiveBalanceIncrements);
368
+ // handle next values
369
+ this.nextDecisionRoot = epochTransitionCache.nextShufflingDecisionRoot;
370
+ this.nextActiveIndices = epochTransitionCache.nextShufflingActiveIndices;
371
+ if (this.shufflingCache) {
372
+ this.nextShuffling = null;
373
+ // This promise will resolve immediately after the synchronous code of the state-transition runs. Until
374
+ // the build is done on a worker thread it will be calculated immediately after the epoch transition
375
+ // completes. Once the work is done concurrently it should be ready by time this get runs so the promise
376
+ // will resolve directly on the next spin of the event loop because the epoch transition and shuffling take
377
+ // about the same time to calculate so theoretically its ready now. Do not await here though in case it
378
+ // is not ready yet as the transition must not be asynchronous.
379
+ this.shufflingCache
380
+ .get(epochAfterUpcoming, this.nextDecisionRoot)
381
+ .then((shuffling) => {
382
+ if (!shuffling) {
383
+ throw new Error("EpochShuffling not returned from get in afterProcessEpoch");
384
+ }
385
+ this.nextShuffling = shuffling;
386
+ })
387
+ .catch((err) => {
388
+ this.shufflingCache?.logger?.error("EPOCH_CONTEXT_SHUFFLING_BUILD_ERROR", { epoch: epochAfterUpcoming, decisionRoot: epochTransitionCache.nextShufflingDecisionRoot }, err);
389
+ });
390
+ }
391
+ else {
392
+ // Only for testing. shufflingCache should always be available in prod
393
+ this.nextShuffling = computeEpochShuffling(state, this.nextActiveIndices, epochAfterUpcoming);
394
+ }
293
395
  // Only pre-compute the seed since it's very cheap. Do the expensive computeProposers() call only on demand.
294
- this.proposersNextEpoch = { computed: false, seed: getSeed(state, this.nextShuffling.epoch, DOMAIN_BEACON_PROPOSER) };
396
+ this.proposersNextEpoch = { computed: false, seed: getSeed(state, epochAfterUpcoming, DOMAIN_BEACON_PROPOSER) };
295
397
  // TODO: DEDUPLICATE from createEpochCache
296
398
  //
297
- // Precompute churnLimit for efficient initiateValidatorExit() during block proposing MUST be recompute everytime the
399
+ // Precompute churnLimit for efficient initiateValidatorExit() during block proposing MUST be recompute every time the
298
400
  // active validator indices set changes in size. Validators change active status only when:
299
401
  // - validator.activation_epoch is set. Only changes in process_registry_updates() if validator can be activated. If
300
402
  // the value changes it will be set to `epoch + 1 + MAX_SEED_LOOKAHEAD`.
@@ -311,13 +413,13 @@ export class EpochCache {
311
413
  this.churnLimit = getChurnLimit(this.config, this.currentShuffling.activeIndices.length);
312
414
  this.activationChurnLimit = getActivationChurnLimit(this.config, this.config.getForkSeq(state.slot), this.currentShuffling.activeIndices.length);
313
415
  // Maybe advance exitQueueEpoch at the end of the epoch if there haven't been any exists for a while
314
- const exitQueueEpoch = computeActivationExitEpoch(currEpoch);
416
+ const exitQueueEpoch = computeActivationExitEpoch(upcomingEpoch);
315
417
  if (exitQueueEpoch > this.exitQueueEpoch) {
316
418
  this.exitQueueEpoch = exitQueueEpoch;
317
419
  this.exitQueueChurn = 0;
318
420
  }
319
421
  this.totalActiveBalanceIncrements = epochTransitionCache.nextEpochTotalActiveBalanceByIncrement;
320
- if (currEpoch >= this.config.ALTAIR_FORK_EPOCH) {
422
+ if (upcomingEpoch >= this.config.ALTAIR_FORK_EPOCH) {
321
423
  this.syncParticipantReward = computeSyncParticipantReward(this.totalActiveBalanceIncrements);
322
424
  this.syncProposerReward = Math.floor(this.syncParticipantReward * PROPOSER_WEIGHT_FACTOR);
323
425
  this.baseRewardPerIncrement = computeBaseRewardPerIncrement(this.totalActiveBalanceIncrements);
@@ -336,7 +438,7 @@ export class EpochCache {
336
438
  // Only keep validatorLength for epochs after finalized cpState.epoch
337
439
  // eg. [100(epoch 1), 102(epoch 2)].push(104(epoch 3)), this.epoch = 3, finalized cp epoch = 1
338
440
  // We keep the last (3 - 1) items = [102, 104]
339
- if (currEpoch >= this.config.ELECTRA_FORK_EPOCH) {
441
+ if (upcomingEpoch >= this.config.ELECTRA_FORK_EPOCH) {
340
442
  this.historicalValidatorLengths = this.historicalValidatorLengths.push(state.validators.length);
341
443
  // If number of validatorLengths we want to keep exceeds the current list size, it implies
342
444
  // finalized checkpoint hasn't advanced, and no need to slice
@@ -456,7 +558,7 @@ export class EpochCache {
456
558
  */
457
559
  getBeaconProposersNextEpoch() {
458
560
  if (!this.proposersNextEpoch.computed) {
459
- const indexes = computeProposers(this.config.getForkSeqAtEpoch(this.epoch + 1), this.proposersNextEpoch.seed, this.nextShuffling, this.effectiveBalanceIncrements);
561
+ const indexes = computeProposers(this.config.getForkSeqAtEpoch(this.epoch + 1), this.proposersNextEpoch.seed, this.getShufflingAtEpoch(this.nextEpoch), this.effectiveBalanceIncrements);
460
562
  this.proposersNextEpoch = { computed: true, indexes };
461
563
  }
462
564
  return this.proposersNextEpoch.indexes;
@@ -497,28 +599,8 @@ export class EpochCache {
497
599
  }
498
600
  }
499
601
  getCommitteeAssignments(epoch, requestedValidatorIndices) {
500
- const requestedValidatorIndicesSet = new Set(requestedValidatorIndices);
501
- const duties = new Map();
502
- const epochCommittees = this.getShufflingAtEpoch(epoch).committees;
503
- for (let epochSlot = 0; epochSlot < SLOTS_PER_EPOCH; epochSlot++) {
504
- const slotCommittees = epochCommittees[epochSlot];
505
- for (let i = 0, committeesAtSlot = slotCommittees.length; i < committeesAtSlot; i++) {
506
- for (let j = 0, committeeLength = slotCommittees[i].length; j < committeeLength; j++) {
507
- const validatorIndex = slotCommittees[i][j];
508
- if (requestedValidatorIndicesSet.has(validatorIndex)) {
509
- duties.set(validatorIndex, {
510
- validatorIndex,
511
- committeeLength,
512
- committeesAtSlot,
513
- validatorCommitteeIndex: j,
514
- committeeIndex: i,
515
- slot: epoch * SLOTS_PER_EPOCH + epochSlot,
516
- });
517
- }
518
- }
519
- }
520
- }
521
- return duties;
602
+ const shuffling = this.getShufflingAtEpoch(epoch);
603
+ return calculateCommitteeAssignments(shuffling, requestedValidatorIndices);
522
604
  }
523
605
  /**
524
606
  * Return the committee assignment in the ``epoch`` for ``validator_index``.
@@ -628,6 +710,13 @@ export class EpochCache {
628
710
  getShufflingAtEpoch(epoch) {
629
711
  const shuffling = this.getShufflingAtEpochOrNull(epoch);
630
712
  if (shuffling === null) {
713
+ if (epoch === this.nextEpoch) {
714
+ throw new EpochCacheError({
715
+ code: EpochCacheErrorCode.NEXT_SHUFFLING_NOT_AVAILABLE,
716
+ epoch: epoch,
717
+ decisionRoot: this.getShufflingDecisionRoot(this.nextEpoch),
718
+ });
719
+ }
631
720
  throw new EpochCacheError({
632
721
  code: EpochCacheErrorCode.COMMITTEE_EPOCH_OUT_OF_RANGE,
633
722
  currentEpoch: this.currentShuffling.epoch,
@@ -636,18 +725,36 @@ export class EpochCache {
636
725
  }
637
726
  return shuffling;
638
727
  }
639
- getShufflingAtEpochOrNull(epoch) {
640
- if (epoch === this.previousShuffling.epoch) {
641
- return this.previousShuffling;
642
- }
643
- else if (epoch === this.currentShuffling.epoch) {
644
- return this.currentShuffling;
645
- }
646
- else if (epoch === this.nextShuffling.epoch) {
647
- return this.nextShuffling;
728
+ getShufflingDecisionRoot(epoch) {
729
+ switch (epoch) {
730
+ case this.epoch - 1:
731
+ return this.previousDecisionRoot;
732
+ case this.epoch:
733
+ return this.currentDecisionRoot;
734
+ case this.nextEpoch:
735
+ return this.nextDecisionRoot;
736
+ default:
737
+ throw new EpochCacheError({
738
+ code: EpochCacheErrorCode.DECISION_ROOT_EPOCH_OUT_OF_RANGE,
739
+ currentEpoch: this.epoch,
740
+ requestedEpoch: epoch,
741
+ });
648
742
  }
649
- else {
650
- return null;
743
+ }
744
+ getShufflingAtEpochOrNull(epoch) {
745
+ switch (epoch) {
746
+ case this.epoch - 1:
747
+ return this.previousShuffling;
748
+ case this.epoch:
749
+ return this.currentShuffling;
750
+ case this.nextEpoch:
751
+ if (!this.nextShuffling) {
752
+ this.nextShuffling =
753
+ this.shufflingCache?.getSync(this.nextEpoch, this.getShufflingDecisionRoot(this.nextEpoch)) ?? null;
754
+ }
755
+ return this.nextShuffling;
756
+ default:
757
+ return null;
651
758
  }
652
759
  }
653
760
  /**
@@ -735,6 +842,8 @@ export var EpochCacheErrorCode;
735
842
  (function (EpochCacheErrorCode) {
736
843
  EpochCacheErrorCode["COMMITTEE_INDEX_OUT_OF_RANGE"] = "EPOCH_CONTEXT_ERROR_COMMITTEE_INDEX_OUT_OF_RANGE";
737
844
  EpochCacheErrorCode["COMMITTEE_EPOCH_OUT_OF_RANGE"] = "EPOCH_CONTEXT_ERROR_COMMITTEE_EPOCH_OUT_OF_RANGE";
845
+ EpochCacheErrorCode["DECISION_ROOT_EPOCH_OUT_OF_RANGE"] = "EPOCH_CONTEXT_ERROR_DECISION_ROOT_EPOCH_OUT_OF_RANGE";
846
+ EpochCacheErrorCode["NEXT_SHUFFLING_NOT_AVAILABLE"] = "EPOCH_CONTEXT_ERROR_NEXT_SHUFFLING_NOT_AVAILABLE";
738
847
  EpochCacheErrorCode["NO_SYNC_COMMITTEE"] = "EPOCH_CONTEXT_ERROR_NO_SYNC_COMMITTEE";
739
848
  EpochCacheErrorCode["PROPOSER_EPOCH_MISMATCH"] = "EPOCH_CONTEXT_ERROR_PROPOSER_EPOCH_MISMATCH";
740
849
  })(EpochCacheErrorCode || (EpochCacheErrorCode = {}));