@lodestar/beacon-node 1.41.0 → 1.42.0-dev.5f2fffc2ce

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.
Files changed (99) hide show
  1. package/lib/api/impl/beacon/state/utils.d.ts +2 -2
  2. package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
  3. package/lib/api/impl/beacon/state/utils.js.map +1 -1
  4. package/lib/api/impl/validator/index.d.ts.map +1 -1
  5. package/lib/api/impl/validator/index.js +5 -1
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/chain/archiveStore/archiveStore.d.ts +0 -1
  8. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  9. package/lib/chain/archiveStore/archiveStore.js +0 -9
  10. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  11. package/lib/chain/archiveStore/interface.d.ts +4 -4
  12. package/lib/chain/archiveStore/interface.d.ts.map +1 -1
  13. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.d.ts +4 -4
  14. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.d.ts.map +1 -1
  15. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.js +4 -1
  16. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.js.map +1 -1
  17. package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
  18. package/lib/chain/archiveStore/utils/archiveBlocks.js +38 -0
  19. package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
  20. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  21. package/lib/chain/blocks/importBlock.js +11 -7
  22. package/lib/chain/blocks/importBlock.js.map +1 -1
  23. package/lib/chain/blocks/verifyBlocksSignatures.js +1 -1
  24. package/lib/chain/blocks/verifyBlocksSignatures.js.map +1 -1
  25. package/lib/chain/chain.d.ts +3 -3
  26. package/lib/chain/chain.d.ts.map +1 -1
  27. package/lib/chain/chain.js +16 -7
  28. package/lib/chain/chain.js.map +1 -1
  29. package/lib/chain/interface.d.ts +2 -2
  30. package/lib/chain/interface.d.ts.map +1 -1
  31. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  32. package/lib/chain/prepareNextSlot.js +6 -2
  33. package/lib/chain/prepareNextSlot.js.map +1 -1
  34. package/lib/chain/regen/errors.d.ts +11 -1
  35. package/lib/chain/regen/errors.d.ts.map +1 -1
  36. package/lib/chain/regen/errors.js +2 -0
  37. package/lib/chain/regen/errors.js.map +1 -1
  38. package/lib/chain/regen/interface.d.ts +12 -6
  39. package/lib/chain/regen/interface.d.ts.map +1 -1
  40. package/lib/chain/regen/queued.d.ts +11 -6
  41. package/lib/chain/regen/queued.d.ts.map +1 -1
  42. package/lib/chain/regen/queued.js +40 -8
  43. package/lib/chain/regen/queued.js.map +1 -1
  44. package/lib/chain/regen/regen.d.ts +5 -0
  45. package/lib/chain/regen/regen.d.ts.map +1 -1
  46. package/lib/chain/regen/regen.js +33 -6
  47. package/lib/chain/regen/regen.js.map +1 -1
  48. package/lib/chain/stateCache/datastore/db.d.ts +4 -5
  49. package/lib/chain/stateCache/datastore/db.d.ts.map +1 -1
  50. package/lib/chain/stateCache/datastore/db.js +32 -10
  51. package/lib/chain/stateCache/datastore/db.js.map +1 -1
  52. package/lib/chain/stateCache/datastore/file.d.ts +1 -1
  53. package/lib/chain/stateCache/datastore/file.d.ts.map +1 -1
  54. package/lib/chain/stateCache/datastore/file.js +5 -5
  55. package/lib/chain/stateCache/datastore/file.js.map +1 -1
  56. package/lib/chain/stateCache/datastore/types.d.ts +1 -1
  57. package/lib/chain/stateCache/datastore/types.d.ts.map +1 -1
  58. package/lib/chain/stateCache/fifoBlockStateCache.d.ts +7 -4
  59. package/lib/chain/stateCache/fifoBlockStateCache.d.ts.map +1 -1
  60. package/lib/chain/stateCache/fifoBlockStateCache.js +8 -3
  61. package/lib/chain/stateCache/fifoBlockStateCache.js.map +1 -1
  62. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts +33 -14
  63. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  64. package/lib/chain/stateCache/persistentCheckpointsCache.js +217 -119
  65. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  66. package/lib/chain/stateCache/types.d.ts +15 -8
  67. package/lib/chain/stateCache/types.d.ts.map +1 -1
  68. package/lib/chain/stateCache/types.js.map +1 -1
  69. package/lib/chain/validation/voluntaryExit.d.ts.map +1 -1
  70. package/lib/chain/validation/voluntaryExit.js +2 -2
  71. package/lib/chain/validation/voluntaryExit.js.map +1 -1
  72. package/package.json +15 -15
  73. package/src/api/impl/beacon/state/utils.ts +2 -2
  74. package/src/api/impl/validator/index.ts +7 -3
  75. package/src/chain/archiveStore/archiveStore.ts +0 -10
  76. package/src/chain/archiveStore/interface.ts +4 -4
  77. package/src/chain/archiveStore/strategies/frequencyStateArchiveStrategy.ts +8 -5
  78. package/src/chain/archiveStore/utils/archiveBlocks.ts +59 -1
  79. package/src/chain/blocks/importBlock.ts +11 -6
  80. package/src/chain/blocks/verifyBlocksSignatures.ts +1 -1
  81. package/src/chain/chain.ts +23 -12
  82. package/src/chain/interface.ts +2 -2
  83. package/src/chain/prepareNextSlot.ts +6 -2
  84. package/src/chain/regen/errors.ts +6 -1
  85. package/src/chain/regen/interface.ts +12 -6
  86. package/src/chain/regen/queued.ts +48 -12
  87. package/src/chain/regen/regen.ts +37 -7
  88. package/src/chain/stateCache/datastore/db.ts +33 -10
  89. package/src/chain/stateCache/datastore/file.ts +6 -5
  90. package/src/chain/stateCache/datastore/types.ts +3 -2
  91. package/src/chain/stateCache/fifoBlockStateCache.ts +10 -4
  92. package/src/chain/stateCache/persistentCheckpointsCache.ts +248 -139
  93. package/src/chain/stateCache/types.ts +18 -8
  94. package/src/chain/validation/voluntaryExit.ts +2 -1
  95. package/lib/chain/archiveStore/utils/archivePayloads.d.ts +0 -7
  96. package/lib/chain/archiveStore/utils/archivePayloads.d.ts.map +0 -1
  97. package/lib/chain/archiveStore/utils/archivePayloads.js +0 -10
  98. package/lib/chain/archiveStore/utils/archivePayloads.js.map +0 -1
  99. package/src/chain/archiveStore/utils/archivePayloads.ts +0 -15
@@ -1,5 +1,6 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {BeaconConfig} from "@lodestar/config";
3
+ import {CheckpointWithPayloadStatus} from "@lodestar/fork-choice";
3
4
  import {
4
5
  CachedBeaconStateAllForks,
5
6
  computeStartSlotAtEpoch,
@@ -14,7 +15,7 @@ import {IClock} from "../../util/clock.js";
14
15
  import {serializeState} from "../serializeState.js";
15
16
  import {CPStateDatastore, DatastoreKey} from "./datastore/index.js";
16
17
  import {MapTracker} from "./mapMetrics.js";
17
- import {BlockStateCache, CacheItemType, CheckpointHex, CheckpointStateCache} from "./types.js";
18
+ import {BlockStateCache, CacheItemType, CheckpointHexPayload, CheckpointStateCache} from "./types.js";
18
19
 
19
20
  export type PersistentCheckpointStateCacheOpts = {
20
21
  /** Keep max n state epochs in memory, persist the rest to disk */
@@ -54,6 +55,22 @@ type CacheItem = InMemoryCacheItem | PersistedCacheItem;
54
55
 
55
56
  type LoadedStateBytesData = {persistedKey: DatastoreKey; stateBytes: Uint8Array};
56
57
 
58
+ /** Bitmask for tracking which payload variants exist per root in the epochIndex */
59
+ enum PayloadAvailability {
60
+ NOT_PRESENT = 1,
61
+ PRESENT = 2,
62
+ }
63
+
64
+ const PAYLOAD_AVAILABILITY_ALL = [PayloadAvailability.NOT_PRESENT, PayloadAvailability.PRESENT] as const;
65
+
66
+ function toPayloadAvailability(payloadPresent: boolean): PayloadAvailability {
67
+ return payloadPresent ? PayloadAvailability.PRESENT : PayloadAvailability.NOT_PRESENT;
68
+ }
69
+
70
+ function fromPayloadAvailability(flag: PayloadAvailability): boolean {
71
+ return flag === PayloadAvailability.PRESENT;
72
+ }
73
+
57
74
  /**
58
75
  * Before n-historical states, lodestar keeps all checkpoint states since finalized
59
76
  * Since Sep 2024, lodestar stores 3 most recent checkpoint states in memory and the rest on disk. The finalized state
@@ -106,8 +123,8 @@ const PROCESS_CHECKPOINT_STATES_BPS = 6667;
106
123
  */
107
124
  export class PersistentCheckpointStateCache implements CheckpointStateCache {
108
125
  private readonly cache: MapTracker<CacheKey, CacheItem>;
109
- /** Epoch -> Set<blockRoot> */
110
- private readonly epochIndex = new MapDef<Epoch, Set<RootHex>>(() => new Set<string>());
126
+ /** Epoch -> Map<blockRoot, PayloadAvailability bitmask> */
127
+ private readonly epochIndex = new MapDef<Epoch, Map<RootHex, number>>(() => new Map());
111
128
  private readonly config: BeaconConfig;
112
129
  private readonly metrics: Metrics | null | undefined;
113
130
  private readonly logger: Logger;
@@ -203,13 +220,18 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
203
220
  * - Get block for processing
204
221
  * - Regen head state
205
222
  */
206
- async getOrReload(cp: CheckpointHex): Promise<CachedBeaconStateAllForks | null> {
223
+ async getOrReload(cp: CheckpointHexPayload): Promise<CachedBeaconStateAllForks | null> {
207
224
  const stateOrStateBytesData = await this.getStateOrLoadDb(cp);
208
225
  if (stateOrStateBytesData === null || isCachedBeaconState(stateOrStateBytesData)) {
209
226
  return stateOrStateBytesData ?? null;
210
227
  }
211
228
  const {persistedKey, stateBytes} = stateOrStateBytesData;
212
- const logMeta = {persistedKey: toHex(persistedKey)};
229
+ const logMeta = {
230
+ epoch: cp.epoch,
231
+ rootHex: cp.rootHex,
232
+ payloadPresent: cp.payloadPresent,
233
+ persistedKey: toHex(persistedKey),
234
+ };
213
235
  this.logger.debug("Reload: read state successful", logMeta);
214
236
  this.metrics?.cpStateCache.stateReloadSecFromSlot.observe(
215
237
  this.clock?.secFromSlot(this.clock?.currentSlot ?? 0) ?? 0
@@ -250,7 +272,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
250
272
  // only remove persisted state once we reload successfully
251
273
  const cpKey = toCacheKey(cp);
252
274
  this.cache.set(cpKey, {type: CacheItemType.inMemory, state: newCachedState, persistedKey});
253
- this.epochIndex.getOrDefault(cp.epoch).add(cp.rootHex);
275
+ this.addToEpochIndex(cp.epoch, cp.rootHex, cp.payloadPresent);
254
276
  // don't prune from memory here, call it at the last 1/3 of slot 0 of an epoch
255
277
  return newCachedState;
256
278
  } catch (e) {
@@ -262,7 +284,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
262
284
  /**
263
285
  * Return either state or state bytes loaded from db.
264
286
  */
265
- async getStateOrBytes(cp: CheckpointHex): Promise<CachedBeaconStateAllForks | Uint8Array | null> {
287
+ async getStateOrBytes(cp: CheckpointHexPayload): Promise<CachedBeaconStateAllForks | Uint8Array | null> {
266
288
  const stateOrLoadedState = await this.getStateOrLoadDb(cp);
267
289
  if (stateOrLoadedState === null || isCachedBeaconState(stateOrLoadedState)) {
268
290
  return stateOrLoadedState;
@@ -273,7 +295,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
273
295
  /**
274
296
  * Return either state or state bytes with persisted key loaded from db.
275
297
  */
276
- async getStateOrLoadDb(cp: CheckpointHex): Promise<CachedBeaconStateAllForks | LoadedStateBytesData | null> {
298
+ async getStateOrLoadDb(cp: CheckpointHexPayload): Promise<CachedBeaconStateAllForks | LoadedStateBytesData | null> {
277
299
  const cpKey = toCacheKey(cp);
278
300
  const inMemoryState = this.get(cpKey);
279
301
  if (inMemoryState) {
@@ -304,7 +326,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
304
326
  /**
305
327
  * Similar to get() api without reloading from disk
306
328
  */
307
- get(cpOrKey: CheckpointHex | string): CachedBeaconStateAllForks | null {
329
+ get(cpOrKey: CheckpointHexPayload | CacheKey): CachedBeaconStateAllForks | null {
308
330
  this.metrics?.cpStateCache.lookups.inc();
309
331
  const cpKey = typeof cpOrKey === "string" ? cpOrKey : toCacheKey(cpOrKey);
310
332
  const cacheItem = this.cache.get(cpKey);
@@ -330,9 +352,11 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
330
352
 
331
353
  /**
332
354
  * Add a state of a checkpoint to this cache, prune from memory if necessary.
355
+ * @param payloadPresent - For Gloas: true if this is payload state, false if block state.
356
+ * Always true for pre-Gloas.
333
357
  */
334
- add(cp: phase0.Checkpoint, state: CachedBeaconStateAllForks): void {
335
- const cpHex = toCheckpointHex(cp);
358
+ add(cp: phase0.Checkpoint, state: CachedBeaconStateAllForks, payloadPresent: boolean): void {
359
+ const cpHex = toCheckpointHexPayload(cp, payloadPresent);
336
360
  const key = toCacheKey(cpHex);
337
361
  const cacheItem = this.cache.get(key);
338
362
  this.metrics?.cpStateCache.adds.inc();
@@ -343,27 +367,32 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
343
367
  this.logger.verbose("Added checkpoint state to memory but a persisted key existed", {
344
368
  epoch: cp.epoch,
345
369
  rootHex: cpHex.rootHex,
370
+ payloadPresent,
346
371
  persistedKey: toHex(persistedKey),
347
372
  });
348
373
  } else {
349
374
  this.cache.set(key, {type: CacheItemType.inMemory, state});
350
- this.logger.verbose("Added checkpoint state to memory", {epoch: cp.epoch, rootHex: cpHex.rootHex});
375
+ this.logger.verbose("Added checkpoint state to memory", {
376
+ epoch: cp.epoch,
377
+ rootHex: cpHex.rootHex,
378
+ payloadPresent,
379
+ });
351
380
  }
352
- this.epochIndex.getOrDefault(cp.epoch).add(cpHex.rootHex);
381
+ this.addToEpochIndex(cp.epoch, cpHex.rootHex, cpHex.payloadPresent);
353
382
  this.prunePersistedStates();
354
383
  }
355
384
 
356
385
  /**
357
386
  * Searches in-memory state for the latest cached state with a `root` without reload, starting with `epoch` and descending
358
387
  */
359
- getLatest(rootHex: RootHex, maxEpoch: Epoch): CachedBeaconStateAllForks | null {
388
+ getLatest(rootHex: RootHex, maxEpoch: Epoch, payloadPresent: boolean): CachedBeaconStateAllForks | null {
360
389
  // sort epochs in descending order, only consider epochs lte `epoch`
361
390
  const epochs = Array.from(this.epochIndex.keys())
362
391
  .sort((a, b) => b - a)
363
392
  .filter((e) => e <= maxEpoch);
364
393
  for (const epoch of epochs) {
365
- if (this.epochIndex.get(epoch)?.has(rootHex)) {
366
- const inMemoryClonedState = this.get({rootHex, epoch});
394
+ if (this.hasPayloadVariant(epoch, rootHex, payloadPresent)) {
395
+ const inMemoryClonedState = this.get({rootHex, epoch, payloadPresent});
367
396
  if (inMemoryClonedState) {
368
397
  return inMemoryClonedState;
369
398
  }
@@ -379,20 +408,24 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
379
408
  * - Get block for processing
380
409
  * - Regen head state
381
410
  */
382
- async getOrReloadLatest(rootHex: RootHex, maxEpoch: Epoch): Promise<CachedBeaconStateAllForks | null> {
411
+ async getOrReloadLatest(
412
+ rootHex: RootHex,
413
+ maxEpoch: Epoch,
414
+ payloadPresent: boolean
415
+ ): Promise<CachedBeaconStateAllForks | null> {
383
416
  // sort epochs in descending order, only consider epochs lte `epoch`
384
417
  const epochs = Array.from(this.epochIndex.keys())
385
418
  .sort((a, b) => b - a)
386
419
  .filter((e) => e <= maxEpoch);
387
420
  for (const epoch of epochs) {
388
- if (this.epochIndex.get(epoch)?.has(rootHex)) {
421
+ if (this.hasPayloadVariant(epoch, rootHex, payloadPresent)) {
389
422
  try {
390
- const state = await this.getOrReload({rootHex, epoch});
423
+ const state = await this.getOrReload({rootHex, epoch, payloadPresent});
391
424
  if (state) {
392
425
  return state;
393
426
  }
394
427
  } catch (e) {
395
- this.logger.debug("Error get or reload state", {epoch, rootHex}, e as Error);
428
+ this.logger.debug("Error get or reload state", {epoch, rootHex, payloadPresent}, e as Error);
396
429
  }
397
430
  }
398
431
  }
@@ -400,12 +433,14 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
400
433
  }
401
434
 
402
435
  /**
403
- * Update the precomputed checkpoint and return the number of his for the
436
+ * Update the precomputed checkpoint and return the number of hits for the
404
437
  * previous one (if any).
438
+ * @param payloadPresent - For Gloas: true if head block has FULL payload, false if EMPTY.
439
+ * Always true for pre-Gloas.
405
440
  */
406
- updatePreComputedCheckpoint(rootHex: RootHex, epoch: Epoch): number | null {
441
+ updatePreComputedCheckpoint(rootHex: RootHex, epoch: Epoch, payloadPresent: boolean): number | null {
407
442
  const previousHits = this.preComputedCheckpointHits;
408
- this.preComputedCheckpoint = toCacheKey({rootHex, epoch});
443
+ this.preComputedCheckpoint = toCacheKey({rootHex, epoch, payloadPresent});
409
444
  this.preComputedCheckpointHits = 0;
410
445
  return previousHits;
411
446
  }
@@ -479,6 +514,9 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
479
514
  * - 2 then we'll persist {root: b2, epoch n-2} checkpoint state to disk, there are also 2 checkpoint states in memory at epoch n, same to the above (maxEpochsInMemory=1)
480
515
  *
481
516
  * As of Mar 2024, it takes <=350ms to persist a holesky state on fast server
517
+ *
518
+ * For Gloas: Processes both block state and payload state variants together. The decision of which roots to persist/prune
519
+ * is based on root canonicality (from state's view), not payload presence. Both variants are managed as a unit.
482
520
  */
483
521
  async processState(blockRootHex: RootHex, state: CachedBeaconStateAllForks): Promise<number> {
484
522
  let persistCount = 0;
@@ -549,7 +587,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
549
587
  *
550
588
  * Use seed state from the block cache if cannot find any seed states within this cache.
551
589
  */
552
- findSeedStateToReload(reloadedCp: CheckpointHex): CachedBeaconStateAllForks {
590
+ findSeedStateToReload(reloadedCp: CheckpointHexPayload): CachedBeaconStateAllForks {
553
591
  const maxEpoch = Math.max(...Array.from(this.epochIndex.keys()));
554
592
  const reloadedCpSlot = computeStartSlotAtEpoch(reloadedCp.epoch);
555
593
  let firstState: CachedBeaconStateAllForks | null = null;
@@ -562,32 +600,35 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
562
600
  return firstState;
563
601
  }
564
602
 
565
- for (const rootHex of this.epochIndex.get(epoch) || []) {
566
- const cpKey = toCacheKey({rootHex, epoch});
567
- const cacheItem = this.cache.get(cpKey);
568
- if (cacheItem === undefined) {
569
- // should not happen
570
- continue;
571
- }
572
- if (isInMemoryCacheItem(cacheItem)) {
573
- const {state} = cacheItem;
574
- if (firstState === null) {
575
- firstState = state;
603
+ for (const [rootHex, bitmask] of this.epochIndex.get(epoch) || []) {
604
+ for (const flag of PAYLOAD_AVAILABILITY_ALL) {
605
+ if (!(bitmask & flag)) continue;
606
+ const payloadPresent = fromPayloadAvailability(flag);
607
+ const cpKey = toCacheKey({rootHex, epoch, payloadPresent});
608
+ const cacheItem = this.cache.get(cpKey);
609
+ if (cacheItem === undefined) {
610
+ continue;
576
611
  }
577
- const cpLog = {cpEpoch: epoch, cpRoot: rootHex};
578
-
579
- try {
580
- // amongst states of the same epoch, choose the one with the same view of reloadedCp
581
- if (
582
- reloadedCpSlot < state.slot &&
583
- toRootHex(getBlockRootAtSlot(state, reloadedCpSlot)) === reloadedCp.rootHex
584
- ) {
585
- this.logger.verbose("Reload: use checkpoint state as seed state", {...cpLog, ...logCtx});
586
- return state;
612
+ if (isInMemoryCacheItem(cacheItem)) {
613
+ const {state} = cacheItem;
614
+ if (firstState === null) {
615
+ firstState = state;
616
+ }
617
+ const cpLog = {cpEpoch: epoch, cpRoot: rootHex, payloadPresent};
618
+
619
+ try {
620
+ // amongst states of the same epoch, choose the one with the same view of reloadedCp
621
+ if (
622
+ reloadedCpSlot < state.slot &&
623
+ toRootHex(getBlockRootAtSlot(state, reloadedCpSlot)) === reloadedCp.rootHex
624
+ ) {
625
+ this.logger.verbose("Reload: use checkpoint state as seed state", {...cpLog, ...logCtx});
626
+ return state;
627
+ }
628
+ } catch (e) {
629
+ // getBlockRootAtSlot may throw error
630
+ this.logger.debug("Error finding checkpoint state to reload", {...cpLog, ...logCtx}, e as Error);
587
631
  }
588
- } catch (e) {
589
- // getBlockRootAtSlot may throw error
590
- this.logger.debug("Error finding checkpoint state to reload", {...cpLog, ...logCtx}, e as Error);
591
632
  }
592
633
  }
593
634
  }
@@ -604,6 +645,31 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
604
645
  this.epochIndex.clear();
605
646
  }
606
647
 
648
+ private addToEpochIndex(epoch: Epoch, rootHex: RootHex, payloadPresent: boolean): void {
649
+ const rootMap = this.epochIndex.getOrDefault(epoch);
650
+ rootMap.set(rootHex, (rootMap.get(rootHex) ?? 0) | toPayloadAvailability(payloadPresent));
651
+ }
652
+
653
+ private removeFromEpochIndex(epoch: Epoch, rootHex: RootHex, payloadPresent: boolean): void {
654
+ const rootMap = this.epochIndex.get(epoch);
655
+ if (rootMap === undefined) return;
656
+ const existing = rootMap.get(rootHex);
657
+ if (existing === undefined) return;
658
+ const updated = existing & ~toPayloadAvailability(payloadPresent);
659
+ if (updated === 0) {
660
+ rootMap.delete(rootHex);
661
+ if (rootMap.size === 0) {
662
+ this.epochIndex.delete(epoch);
663
+ }
664
+ } else {
665
+ rootMap.set(rootHex, updated);
666
+ }
667
+ }
668
+
669
+ private hasPayloadVariant(epoch: Epoch, rootHex: RootHex, payloadPresent: boolean): boolean {
670
+ return Boolean((this.epochIndex.get(epoch)?.get(rootHex) ?? 0) & toPayloadAvailability(payloadPresent));
671
+ }
672
+
607
673
  /** ONLY FOR DEBUGGING PURPOSES. For lodestar debug API */
608
674
  dumpSummary(): routes.lodestar.StateCacheItem[] {
609
675
  return Array.from(this.cache.keys()).map((key) => {
@@ -682,7 +748,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
682
748
  const prevEpochRoot = toRootHex(getBlockRootAtSlot(state, epochBoundarySlot - 1));
683
749
 
684
750
  // for each epoch, usually there are 2 rootHexes respective to the 2 checkpoint states: Previous Root Checkpoint State and Current Root Checkpoint State
685
- const cpRootHexes = this.epochIndex.get(epoch) ?? [];
751
+ const cpRootHexMap = this.epochIndex.get(epoch) ?? new Map<RootHex, number>();
686
752
  const persistedRootHexes = new Set<RootHex>();
687
753
 
688
754
  // 1) if there is no CRCS, persist PRCS (block 0 of epoch is skipped). In this case prevEpochRoot === epochBoundaryHex
@@ -691,76 +757,81 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
691
757
  persistedRootHexes.add(epochBoundaryHex);
692
758
 
693
759
  // 3) persist any states with unknown roots to this state
694
- for (const rootHex of cpRootHexes) {
760
+ for (const rootHex of cpRootHexMap.keys()) {
695
761
  if (rootHex !== epochBoundaryHex && rootHex !== prevEpochRoot) {
696
762
  persistedRootHexes.add(rootHex);
697
763
  }
698
764
  }
699
765
 
700
- for (const rootHex of cpRootHexes) {
701
- const cpKey = toCacheKey({epoch: epoch, rootHex});
702
- const cacheItem = this.cache.get(cpKey);
703
-
704
- if (cacheItem !== undefined && isInMemoryCacheItem(cacheItem)) {
705
- let {persistedKey} = cacheItem;
706
- const {state} = cacheItem;
707
- const logMeta = {
708
- stateSlot: state.slot,
709
- rootHex,
710
- epochBoundaryHex,
711
- persistedKey: persistedKey ? toHex(persistedKey) : "",
712
- };
713
-
714
- if (persistedRootHexes.has(rootHex)) {
715
- if (persistedKey) {
716
- // we don't care if the checkpoint state is already persisted
717
- this.logger.verbose("Pruned checkpoint state from memory but no need to persist", logMeta);
718
- } else {
719
- // persist and do not update epochIndex
720
- this.metrics?.cpStateCache.statePersistSecFromSlot.observe(
721
- this.clock?.secFromSlot(this.clock?.currentSlot ?? 0) ?? 0
722
- );
723
- const cpPersist = {epoch: epoch, root: fromHex(rootHex)};
724
- // It's not sustainable to allocate ~240MB for each state every epoch, so we use buffer pool to reuse the memory.
725
- // As monitored on holesky as of Jan 2024:
726
- // - This does not increase heap allocation while gc time is the same
727
- // - It helps stabilize persist time and save ~300ms in average (1.5s vs 1.2s)
728
- // - It also helps the state reload to save ~500ms in average (4.3s vs 3.8s)
729
- // - Also `serializeState.test.ts` perf test shows a lot of differences allocating ~240MB once vs per state serialization
730
- const timer = this.metrics?.stateSerializeDuration.startTimer({
731
- source: AllocSource.PERSISTENT_CHECKPOINTS_CACHE_STATE,
732
- });
733
- persistedKey = await serializeState(
734
- state,
735
- AllocSource.PERSISTENT_CHECKPOINTS_CACHE_STATE,
736
- (stateBytes) => {
737
- timer?.();
738
- return this.datastore.write(cpPersist, stateBytes);
739
- },
740
- this.bufferPool
741
- );
766
+ for (const [rootHex, bitmask] of cpRootHexMap) {
767
+ for (const flag of PAYLOAD_AVAILABILITY_ALL) {
768
+ if (!(bitmask & flag)) continue;
769
+ const payloadPresent = fromPayloadAvailability(flag);
770
+ const cpKey = toCacheKey({epoch: epoch, rootHex, payloadPresent});
771
+ const cacheItem = this.cache.get(cpKey);
742
772
 
743
- persistCount++;
744
- this.logger.verbose("Pruned checkpoint state from memory and persisted to disk", {
745
- ...logMeta,
746
- persistedKey: toHex(persistedKey),
747
- });
748
- }
749
- // overwrite cpKey, this means the state is deleted from memory
750
- this.cache.set(cpKey, {type: CacheItemType.persisted, value: persistedKey});
751
- } else {
752
- if (persistedKey) {
753
- // persisted file will be eventually deleted by the archive task
754
- // this also means the state is deleted from memory
773
+ if (cacheItem !== undefined && isInMemoryCacheItem(cacheItem)) {
774
+ let {persistedKey} = cacheItem;
775
+ const {state} = cacheItem;
776
+ const logMeta = {
777
+ stateSlot: state.slot,
778
+ rootHex,
779
+ payloadPresent,
780
+ epochBoundaryHex,
781
+ persistedKey: persistedKey ? toHex(persistedKey) : "",
782
+ };
783
+
784
+ if (persistedRootHexes.has(rootHex)) {
785
+ if (persistedKey) {
786
+ // we don't care if the checkpoint state is already persisted
787
+ this.logger.verbose("Pruned checkpoint state from memory but no need to persist", logMeta);
788
+ } else {
789
+ // persist and do not update epochIndex
790
+ this.metrics?.cpStateCache.statePersistSecFromSlot.observe(
791
+ this.clock?.secFromSlot(this.clock?.currentSlot ?? 0) ?? 0
792
+ );
793
+ const cpPersist = {epoch: epoch, root: fromHex(rootHex)};
794
+ // It's not sustainable to allocate ~240MB for each state every epoch, so we use buffer pool to reuse the memory.
795
+ // As monitored on holesky as of Jan 2024:
796
+ // - This does not increase heap allocation while gc time is the same
797
+ // - It helps stabilize persist time and save ~300ms in average (1.5s vs 1.2s)
798
+ // - It also helps the state reload to save ~500ms in average (4.3s vs 3.8s)
799
+ // - Also `serializeState.test.ts` perf test shows a lot of differences allocating ~240MB once vs per state serialization
800
+ const timer = this.metrics?.stateSerializeDuration.startTimer({
801
+ source: AllocSource.PERSISTENT_CHECKPOINTS_CACHE_STATE,
802
+ });
803
+ persistedKey = await serializeState(
804
+ state,
805
+ AllocSource.PERSISTENT_CHECKPOINTS_CACHE_STATE,
806
+ (stateBytes) => {
807
+ timer?.();
808
+ return this.datastore.write(cpPersist, stateBytes, payloadPresent);
809
+ },
810
+ this.bufferPool
811
+ );
812
+
813
+ persistCount++;
814
+ this.logger.verbose("Pruned checkpoint state from memory and persisted to disk", {
815
+ ...logMeta,
816
+ persistedKey: toHex(persistedKey),
817
+ });
818
+ }
819
+ // overwrite cpKey, this means the state is deleted from memory
755
820
  this.cache.set(cpKey, {type: CacheItemType.persisted, value: persistedKey});
756
- // do not update epochIndex
757
821
  } else {
758
- // delete the state from memory
759
- this.cache.delete(cpKey);
760
- this.epochIndex.get(epoch)?.delete(rootHex);
822
+ if (persistedKey) {
823
+ // persisted file will be eventually deleted by the archive task
824
+ // this also means the state is deleted from memory
825
+ this.cache.set(cpKey, {type: CacheItemType.persisted, value: persistedKey});
826
+ // do not update epochIndex
827
+ } else {
828
+ // delete the state from memory
829
+ this.cache.delete(cpKey);
830
+ this.removeFromEpochIndex(epoch, rootHex, payloadPresent);
831
+ }
832
+ this.metrics?.cpStateCache.statePruneFromMemoryCount.inc();
833
+ this.logger.verbose("Pruned checkpoint state from memory", logMeta);
761
834
  }
762
- this.metrics?.cpStateCache.statePruneFromMemoryCount.inc();
763
- this.logger.verbose("Pruned checkpoint state from memory", logMeta);
764
835
  }
765
836
  }
766
837
  }
@@ -773,26 +844,40 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
773
844
  */
774
845
  private async deleteAllEpochItems(epoch: Epoch): Promise<void> {
775
846
  let persistCount = 0;
776
- const rootHexes = this.epochIndex.get(epoch) || [];
777
- for (const rootHex of rootHexes) {
778
- const key = toCacheKey({rootHex, epoch});
779
- const cacheItem = this.cache.get(key);
780
-
781
- if (cacheItem) {
782
- const persistedKey = isPersistedCacheItem(cacheItem) ? cacheItem.value : cacheItem.persistedKey;
783
- if (persistedKey) {
784
- await this.datastore.remove(persistedKey);
785
- persistCount++;
786
- this.metrics?.cpStateCache.persistedStateRemoveCount.inc();
847
+ const rootHexMap = this.epochIndex.get(epoch) || new Map<RootHex, number>();
848
+ for (const [rootHex, bitmask] of rootHexMap) {
849
+ for (const flag of PAYLOAD_AVAILABILITY_ALL) {
850
+ if (!(bitmask & flag)) continue;
851
+ const payloadPresent = fromPayloadAvailability(flag);
852
+ const key = toCacheKey({rootHex, epoch, payloadPresent});
853
+ const cacheItem = this.cache.get(key);
854
+
855
+ if (cacheItem) {
856
+ const persistedKey = isPersistedCacheItem(cacheItem) ? cacheItem.value : cacheItem.persistedKey;
857
+ if (persistedKey) {
858
+ await this.datastore.remove(persistedKey);
859
+ persistCount++;
860
+ this.metrics?.cpStateCache.persistedStateRemoveCount.inc();
861
+ }
787
862
  }
863
+ this.cache.delete(key);
864
+ this.logger.verbose("Pruned checkpoint state", {
865
+ epoch,
866
+ rootHex,
867
+ payloadPresent,
868
+ type: cacheItem ? (isPersistedCacheItem(cacheItem) ? "persisted" : "in-memory") : "missing",
869
+ });
788
870
  }
789
- this.cache.delete(key);
790
871
  }
791
872
  this.epochIndex.delete(epoch);
792
- this.logger.verbose("Pruned checkpoint states for epoch", {
873
+ this.logger.verbose("Pruned all checkpoint states for epoch", {
793
874
  epoch,
794
875
  persistCount,
795
- rootHexes: Array.from(rootHexes).join(","),
876
+ items: Array.from(rootHexMap.entries())
877
+ .flatMap(([rootHex, bitmask]) =>
878
+ PAYLOAD_AVAILABILITY_ALL.filter((f) => bitmask & f).map((f) => `${rootHex}:${fromPayloadAvailability(f)}`)
879
+ )
880
+ .join(","),
796
881
  });
797
882
  }
798
883
 
@@ -844,29 +929,57 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
844
929
  }
845
930
  }
846
931
 
847
- export function toCheckpointHex(checkpoint: phase0.Checkpoint): CheckpointHex {
932
+ export function toCheckpointHexPayload(checkpoint: phase0.Checkpoint, payloadPresent: boolean): CheckpointHexPayload {
848
933
  return {
849
934
  epoch: checkpoint.epoch,
850
935
  rootHex: toRootHex(checkpoint.root),
936
+ payloadPresent,
851
937
  };
852
938
  }
853
939
 
854
- export function toCheckpointKey(cp: CheckpointHex): string {
855
- return `${cp.rootHex}:${cp.epoch}`;
856
- }
940
+ /**
941
+ * Convert fork-choice CheckpointWithPayloadStatus to beacon-node CheckpointHexPayload.
942
+ * Maps PayloadStatus enum to boolean payloadPresent.
943
+ * @throws Error if checkpoint has PENDING payload status (ambiguous which variant to use)
944
+ */
945
+ export function fcCheckpointToHexPayload(checkpoint: CheckpointWithPayloadStatus): CheckpointHexPayload {
946
+ const PayloadStatus = {PENDING: 0, EMPTY: 1, FULL: 2} as const;
857
947
 
858
- function toCacheKey(cp: CheckpointHex | phase0.Checkpoint): CacheKey {
859
- if (isCheckpointHex(cp)) {
860
- return `${cp.rootHex}_${cp.epoch}`;
948
+ if (checkpoint.payloadStatus === PayloadStatus.PENDING) {
949
+ throw Error(
950
+ `Cannot convert checkpoint with PENDING payload status at epoch ${checkpoint.epoch} root ${checkpoint.rootHex}`
951
+ );
861
952
  }
862
- return `${toRootHex(cp.root)}_${cp.epoch}`;
953
+
954
+ return {
955
+ epoch: checkpoint.epoch,
956
+ rootHex: checkpoint.rootHex,
957
+ payloadPresent: checkpoint.payloadStatus === PayloadStatus.FULL,
958
+ };
959
+ }
960
+
961
+ export function toCheckpointKey(cp: CheckpointHexPayload): string {
962
+ return `${cp.rootHex}:${cp.epoch}:${cp.payloadPresent}`;
863
963
  }
864
964
 
865
- function fromCacheKey(key: CacheKey): CheckpointHex {
866
- const [rootHex, epoch] = key.split("_");
965
+ /**
966
+ * Convert checkpoint to cache key string.
967
+ * Format: `{rootHex}_{epoch}_{payloadPresent}`
968
+ */
969
+ function toCacheKey(cp: CheckpointHexPayload): CacheKey {
970
+ return `${cp.rootHex}_${cp.epoch}_${cp.payloadPresent}`;
971
+ }
972
+
973
+ function fromCacheKey(key: CacheKey): CheckpointHexPayload {
974
+ const parts = key.split("_");
975
+ const rootHex = parts[0];
976
+ const epoch = Number(parts[1]);
977
+ // For backward compatibility with old format (rootHex_epoch), default to true
978
+ const payloadPresent = parts.length > 2 ? parts[2] === "true" : true;
867
979
  return {
868
980
  rootHex,
869
- epoch: Number(epoch),
981
+ epoch,
982
+ payloadPresent,
870
983
  };
871
984
  }
872
985
 
@@ -883,7 +996,3 @@ function isInMemoryCacheItem(cacheItem: CacheItem): cacheItem is InMemoryCacheIt
883
996
  function isPersistedCacheItem(cacheItem: CacheItem): cacheItem is PersistedCacheItem {
884
997
  return cacheItem.type === CacheItemType.persisted;
885
998
  }
886
-
887
- function isCheckpointHex(cp: CheckpointHex | phase0.Checkpoint): cp is CheckpointHex {
888
- return (cp as CheckpointHex).rootHex !== undefined;
889
- }
@@ -2,7 +2,11 @@ import {routes} from "@lodestar/api";
2
2
  import {CachedBeaconStateAllForks} from "@lodestar/state-transition";
3
3
  import {Epoch, RootHex, phase0} from "@lodestar/types";
4
4
 
5
- export type CheckpointHex = {epoch: Epoch; rootHex: RootHex};
5
+ /**
6
+ * Checkpoint hex representation for state cache keys.
7
+ * Extends CheckpointWithHex (from fork-choice) with payloadPresent.
8
+ */
9
+ export type CheckpointHexPayload = {epoch: Epoch; rootHex: RootHex; payloadPresent: boolean};
6
10
 
7
11
  /**
8
12
  * Lodestar currently keeps two state caches around.
@@ -31,6 +35,8 @@ export interface BlockStateCache {
31
35
  size: number;
32
36
  prune(headStateRootHex: RootHex): void;
33
37
  deleteAllBeforeEpoch(finalizedEpoch: Epoch): void;
38
+ /** Upgrade cache capacity for Gloas fork (2x states for block + payload states) */
39
+ upgradeToGloas(): void;
34
40
  dumpSummary(): routes.lodestar.StateCacheItem[];
35
41
  /** Expose beacon states stored in cache. Use with caution */
36
42
  getStates(): IterableIterator<CachedBeaconStateAllForks>;
@@ -59,13 +65,17 @@ export interface BlockStateCache {
59
65
  */
60
66
  export interface CheckpointStateCache {
61
67
  init?: () => Promise<void>;
62
- getOrReload(cp: CheckpointHex): Promise<CachedBeaconStateAllForks | null>;
63
- getStateOrBytes(cp: CheckpointHex): Promise<CachedBeaconStateAllForks | Uint8Array | null>;
64
- get(cpOrKey: CheckpointHex | string): CachedBeaconStateAllForks | null;
65
- add(cp: phase0.Checkpoint, state: CachedBeaconStateAllForks): void;
66
- getLatest(rootHex: RootHex, maxEpoch: Epoch): CachedBeaconStateAllForks | null;
67
- getOrReloadLatest(rootHex: RootHex, maxEpoch: Epoch): Promise<CachedBeaconStateAllForks | null>;
68
- updatePreComputedCheckpoint(rootHex: RootHex, epoch: Epoch): number | null;
68
+ getOrReload(cp: CheckpointHexPayload): Promise<CachedBeaconStateAllForks | null>;
69
+ getStateOrBytes(cp: CheckpointHexPayload): Promise<CachedBeaconStateAllForks | Uint8Array | null>;
70
+ get(cpOrKey: CheckpointHexPayload | string): CachedBeaconStateAllForks | null;
71
+ add(cp: phase0.Checkpoint, state: CachedBeaconStateAllForks, payloadPresent: boolean): void;
72
+ getLatest(rootHex: RootHex, maxEpoch: Epoch, payloadPresent: boolean): CachedBeaconStateAllForks | null;
73
+ getOrReloadLatest(
74
+ rootHex: RootHex,
75
+ maxEpoch: Epoch,
76
+ payloadPresent: boolean
77
+ ): Promise<CachedBeaconStateAllForks | null>;
78
+ updatePreComputedCheckpoint(rootHex: RootHex, epoch: Epoch, payloadPresent: boolean): number | null;
69
79
  prune(finalizedEpoch: Epoch, justifiedEpoch: Epoch): void;
70
80
  pruneFinalized(finalizedEpoch: Epoch): void;
71
81
  processState(blockRootHex: RootHex, state: CachedBeaconStateAllForks): Promise<number>;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ BeaconStateView,
2
3
  VoluntaryExitValidity,
3
4
  getVoluntaryExitSignatureSet,
4
5
  getVoluntaryExitValidity,
@@ -59,7 +60,7 @@ async function validateVoluntaryExit(
59
60
  });
60
61
  }
61
62
 
62
- const signatureSet = getVoluntaryExitSignatureSet(chain.config, state.slot, voluntaryExit);
63
+ const signatureSet = getVoluntaryExitSignatureSet(chain.config, new BeaconStateView(state), voluntaryExit);
63
64
  if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) {
64
65
  throw new VoluntaryExitError(GossipAction.REJECT, {
65
66
  code: VoluntaryExitErrorCode.INVALID_SIGNATURE,
@@ -1,7 +0,0 @@
1
- import { CheckpointWithHex } from "@lodestar/fork-choice";
2
- import { IBeaconChain } from "../../interface.js";
3
- /**
4
- * Archives execution payload envelopes from hot DB to archive DB after finalization.
5
- */
6
- export declare function archiveExecutionPayloadEnvelopes(chain: IBeaconChain, _finalized: CheckpointWithHex): Promise<void>;
7
- //# sourceMappingURL=archivePayloads.d.ts.map