@lodestar/beacon-node 1.36.0-dev.43f2c7cbbd → 1.36.0-dev.52be14b24f

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 (93) hide show
  1. package/lib/api/impl/lodestar/index.d.ts +5 -0
  2. package/lib/api/impl/lodestar/index.d.ts.map +1 -1
  3. package/lib/api/impl/lodestar/index.js +35 -10
  4. package/lib/api/impl/lodestar/index.js.map +1 -1
  5. package/lib/api/impl/node/utils.js +1 -1
  6. package/lib/api/impl/node/utils.js.map +1 -1
  7. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.d.ts.map +1 -1
  8. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.js +6 -1
  9. package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.js.map +1 -1
  10. package/lib/chain/chain.d.ts +5 -2
  11. package/lib/chain/chain.d.ts.map +1 -1
  12. package/lib/chain/chain.js +27 -12
  13. package/lib/chain/chain.js.map +1 -1
  14. package/lib/chain/forkChoice/index.d.ts +9 -1
  15. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  16. package/lib/chain/forkChoice/index.js +109 -4
  17. package/lib/chain/forkChoice/index.js.map +1 -1
  18. package/lib/chain/interface.d.ts +2 -0
  19. package/lib/chain/interface.d.ts.map +1 -1
  20. package/lib/chain/stateCache/datastore/db.d.ts +12 -0
  21. package/lib/chain/stateCache/datastore/db.d.ts.map +1 -1
  22. package/lib/chain/stateCache/datastore/db.js +70 -0
  23. package/lib/chain/stateCache/datastore/db.js.map +1 -1
  24. package/lib/chain/stateCache/datastore/file.d.ts +1 -0
  25. package/lib/chain/stateCache/datastore/file.d.ts.map +1 -1
  26. package/lib/chain/stateCache/datastore/file.js +7 -0
  27. package/lib/chain/stateCache/datastore/file.js.map +1 -1
  28. package/lib/chain/stateCache/datastore/types.d.ts +1 -0
  29. package/lib/chain/stateCache/datastore/types.d.ts.map +1 -1
  30. package/lib/index.d.ts +2 -0
  31. package/lib/index.d.ts.map +1 -1
  32. package/lib/index.js +2 -0
  33. package/lib/index.js.map +1 -1
  34. package/lib/metrics/metrics/lodestar.js +1 -1
  35. package/lib/metrics/metrics/lodestar.js.map +1 -1
  36. package/lib/network/core/networkCoreWorker.js +2 -7
  37. package/lib/network/core/networkCoreWorker.js.map +1 -1
  38. package/lib/network/discv5/worker.js +2 -7
  39. package/lib/network/discv5/worker.js.map +1 -1
  40. package/lib/network/gossip/encoding.js +1 -1
  41. package/lib/network/gossip/encoding.js.map +1 -1
  42. package/lib/network/gossip/snappy_bun.d.ts +3 -0
  43. package/lib/network/gossip/snappy_bun.d.ts.map +1 -0
  44. package/lib/network/gossip/snappy_bun.js +3 -0
  45. package/lib/network/gossip/snappy_bun.js.map +1 -0
  46. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  47. package/lib/network/processor/gossipHandlers.js +6 -2
  48. package/lib/network/processor/gossipHandlers.js.map +1 -1
  49. package/lib/network/reqresp/handlers/beaconBlocksByRange.d.ts.map +1 -1
  50. package/lib/network/reqresp/handlers/beaconBlocksByRange.js +3 -1
  51. package/lib/network/reqresp/handlers/beaconBlocksByRange.js.map +1 -1
  52. package/lib/node/nodejs.d.ts +2 -1
  53. package/lib/node/nodejs.d.ts.map +1 -1
  54. package/lib/node/nodejs.js +2 -1
  55. package/lib/node/nodejs.js.map +1 -1
  56. package/lib/sync/range/range.d.ts.map +1 -1
  57. package/lib/sync/range/range.js +2 -1
  58. package/lib/sync/range/range.js.map +1 -1
  59. package/lib/sync/utils/remoteSyncType.d.ts +2 -1
  60. package/lib/sync/utils/remoteSyncType.d.ts.map +1 -1
  61. package/lib/sync/utils/remoteSyncType.js +19 -4
  62. package/lib/sync/utils/remoteSyncType.js.map +1 -1
  63. package/lib/util/profile.d.ts +6 -4
  64. package/lib/util/profile.d.ts.map +1 -1
  65. package/lib/util/profile.js +40 -3
  66. package/lib/util/profile.js.map +1 -1
  67. package/lib/util/sszBytes.d.ts +2 -0
  68. package/lib/util/sszBytes.d.ts.map +1 -1
  69. package/lib/util/sszBytes.js +25 -0
  70. package/lib/util/sszBytes.js.map +1 -1
  71. package/package.json +30 -24
  72. package/src/api/impl/lodestar/index.ts +42 -10
  73. package/src/api/impl/node/utils.ts +1 -1
  74. package/src/chain/archiveStore/strategies/frequencyStateArchiveStrategy.ts +5 -1
  75. package/src/chain/chain.ts +43 -18
  76. package/src/chain/forkChoice/index.ts +178 -2
  77. package/src/chain/interface.ts +2 -0
  78. package/src/chain/stateCache/datastore/db.ts +89 -1
  79. package/src/chain/stateCache/datastore/file.ts +8 -0
  80. package/src/chain/stateCache/datastore/types.ts +1 -0
  81. package/src/index.ts +2 -0
  82. package/src/metrics/metrics/lodestar.ts +1 -1
  83. package/src/network/core/networkCoreWorker.ts +2 -7
  84. package/src/network/discv5/worker.ts +2 -7
  85. package/src/network/gossip/encoding.ts +1 -1
  86. package/src/network/gossip/snappy_bun.ts +2 -0
  87. package/src/network/processor/gossipHandlers.ts +6 -1
  88. package/src/network/reqresp/handlers/beaconBlocksByRange.ts +3 -1
  89. package/src/node/nodejs.ts +3 -0
  90. package/src/sync/range/range.ts +2 -1
  91. package/src/sync/utils/remoteSyncType.ts +23 -4
  92. package/src/util/profile.ts +45 -3
  93. package/src/util/sszBytes.ts +30 -0
@@ -1,5 +1,3 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
1
  import {Tree} from "@chainsafe/persistent-merkle-tree";
4
2
  import {routes} from "@lodestar/api";
5
3
  import {ApplicationMethods} from "@lodestar/api/server";
@@ -8,13 +6,16 @@ import {Repository} from "@lodestar/db";
8
6
  import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params";
9
7
  import {BeaconStateCapella, getLatestWeakSubjectivityCheckpointEpoch, loadState} from "@lodestar/state-transition";
10
8
  import {ssz} from "@lodestar/types";
11
- import {toHex, toRootHex} from "@lodestar/utils";
9
+ import {Checkpoint} from "@lodestar/types/phase0";
10
+ import {fromHex, toHex, toRootHex} from "@lodestar/utils";
12
11
  import {BeaconChain} from "../../../chain/index.js";
13
12
  import {QueuedStateRegenerator, RegenRequest} from "../../../chain/regen/index.js";
14
13
  import {IBeaconDb} from "../../../db/interface.js";
15
14
  import {GossipType} from "../../../network/index.js";
16
- import {profileNodeJS, writeHeapSnapshot} from "../../../util/profile.js";
15
+ import {getStateSlotFromBytes} from "../../../util/multifork.js";
16
+ import {ProfileThread, profileThread, writeHeapSnapshot} from "../../../util/profile.js";
17
17
  import {getStateResponseWithRegen} from "../beacon/state/utils.js";
18
+ import {ApiError} from "../errors.js";
18
19
  import {ApiModules} from "../types.js";
19
20
 
20
21
  export function getLodestarApi({
@@ -26,7 +27,9 @@ export function getLodestarApi({
26
27
  }: Pick<ApiModules, "chain" | "config" | "db" | "network" | "sync">): ApplicationMethods<routes.lodestar.Endpoints> {
27
28
  let writingHeapdump = false;
28
29
  let writingProfile = false;
29
- const defaultProfileMs = SLOTS_PER_EPOCH * config.SLOT_DURATION_MS;
30
+ // for NodeJS, profile the whole epoch
31
+ // for Bun, profile 1 slot. Otherwise it will either crash the app, and/or inspector cannot render the profile
32
+ const defaultProfileMs = globalThis.Bun ? config.SLOT_DURATION_MS : SLOTS_PER_EPOCH * config.SLOT_DURATION_MS;
30
33
 
31
34
  return {
32
35
  async writeHeapdump({thread = "main", dirpath = "."}) {
@@ -63,7 +66,6 @@ export function getLodestarApi({
63
66
 
64
67
  try {
65
68
  let filepath: string;
66
- let profile: string;
67
69
  switch (thread) {
68
70
  case "network":
69
71
  filepath = await network.writeNetworkThreadProfile(duration, dirpath);
@@ -73,12 +75,10 @@ export function getLodestarApi({
73
75
  break;
74
76
  default:
75
77
  // main thread
76
- profile = await profileNodeJS(duration);
77
- filepath = path.join(dirpath, `main_thread_${new Date().toISOString()}.cpuprofile`);
78
- fs.writeFileSync(filepath, profile);
78
+ filepath = await profileThread(ProfileThread.MAIN, duration, dirpath);
79
79
  break;
80
80
  }
81
- return {data: {filepath}};
81
+ return {data: {result: filepath}};
82
82
  } finally {
83
83
  writingProfile = false;
84
84
  }
@@ -218,6 +218,26 @@ export function getLodestarApi({
218
218
  meta: {executionOptimistic, finalized, version: fork},
219
219
  };
220
220
  },
221
+
222
+ // the optional checkpoint is in root:epoch format
223
+ async getPersistedCheckpointState({checkpointId}) {
224
+ const checkpoint = checkpointId ? getCheckpointFromArg(checkpointId) : undefined;
225
+ const stateBytes = await chain.getPersistedCheckpointState(checkpoint);
226
+ if (stateBytes === null) {
227
+ throw new ApiError(
228
+ 404,
229
+ checkpointId ? `Checkpoint state not found for id ${checkpointId}` : "Latest safe checkpoint state not found"
230
+ );
231
+ }
232
+
233
+ const slot = getStateSlotFromBytes(stateBytes);
234
+ return {
235
+ data: stateBytes,
236
+ meta: {
237
+ version: config.getForkName(slot),
238
+ },
239
+ };
240
+ },
221
241
  };
222
242
  }
223
243
 
@@ -247,6 +267,18 @@ function regenRequestToJson(config: ChainForkConfig, regenRequest: RegenRequest)
247
267
  }
248
268
  }
249
269
 
270
+ const CHECKPOINT_REGEX = /^(?:0x)?([0-9a-f]{64}):([0-9]+)$/;
271
+ /**
272
+ * Extract a checkpoint from a string in the format `rootHex:epoch`.
273
+ */
274
+ export function getCheckpointFromArg(checkpointStr: string): Checkpoint {
275
+ const match = CHECKPOINT_REGEX.exec(checkpointStr.toLowerCase());
276
+ if (!match) {
277
+ throw new ApiError(400, `Could not parse checkpoint string: ${checkpointStr}`);
278
+ }
279
+ return {root: fromHex(match[1]), epoch: parseInt(match[2])};
280
+ }
281
+
250
282
  function stringifyKeys(keys: (Uint8Array | number | string)[]): string[] {
251
283
  return keys.map((key) => {
252
284
  if (key instanceof Uint8Array) {
@@ -10,7 +10,7 @@ export function formatNodePeer(peerIdStr: string, connections: Connection[]): ro
10
10
  return {
11
11
  peerId: conn ? conn.remotePeer.toString() : peerIdStr,
12
12
  // TODO: figure out how to get enr of peer
13
- enr: "",
13
+ enr: null,
14
14
  lastSeenP2pAddress: conn ? conn.remoteAddr.toString() : "",
15
15
  direction: conn ? (conn.direction as routes.node.PeerDirection) : null,
16
16
  state: conn ? getPeerState(conn.status) : "disconnected",
@@ -63,7 +63,9 @@ export class FrequencyStateArchiveStrategy implements StateArchiveStrategy {
63
63
  const lastStoredEpoch = computeEpochAtSlot(lastStoredSlot ?? 0);
64
64
  const {archiveStateEpochFrequency} = this.opts;
65
65
 
66
+ const logCtx = {finalizedEpoch: finalized.epoch, lastStoredEpoch, archiveStateEpochFrequency};
66
67
  if (finalized.epoch - lastStoredEpoch >= Math.min(PERSIST_TEMP_STATE_EVERY_EPOCHS, archiveStateEpochFrequency)) {
68
+ this.logger.verbose("Start archiving state", logCtx);
67
69
  await this.archiveState(finalized, metrics);
68
70
 
69
71
  // Only check the current and previous intervals
@@ -88,11 +90,13 @@ export class FrequencyStateArchiveStrategy implements StateArchiveStrategy {
88
90
 
89
91
  // More logs to investigate the rss spike issue https://github.com/ChainSafe/lodestar/issues/5591
90
92
  this.logger.verbose("Archived state completed", {
91
- finalizedEpoch: finalized.epoch,
93
+ ...logCtx,
92
94
  minEpoch,
93
95
  storedStateSlots: storedStateSlots.join(","),
94
96
  statesSlotsToDelete: statesSlotsToDelete.join(","),
95
97
  });
98
+ } else {
99
+ this.logger.verbose("Skip archiving state", logCtx);
96
100
  }
97
101
  }
98
102
 
@@ -37,6 +37,7 @@ import {
37
37
  ValidatorIndex,
38
38
  Wei,
39
39
  isBlindedBeaconBlock,
40
+ phase0,
40
41
  } from "@lodestar/types";
41
42
  import {Logger, fromHex, gweiToWei, isErrorAborted, pruneSetToMax, sleep, toRootHex} from "@lodestar/utils";
42
43
  import {ProcessShutdownCallback} from "@lodestar/validator";
@@ -95,11 +96,13 @@ import {SeenBlockAttesters} from "./seenCache/seenBlockAttesters.js";
95
96
  import {SeenBlockInput} from "./seenCache/seenGossipBlockInput.js";
96
97
  import {ShufflingCache} from "./shufflingCache.js";
97
98
  import {BlockStateCacheImpl} from "./stateCache/blockStateCacheImpl.js";
98
- import {DbCPStateDatastore} from "./stateCache/datastore/db.js";
99
+ import {DbCPStateDatastore, checkpointToDatastoreKey} from "./stateCache/datastore/db.js";
99
100
  import {FileCPStateDatastore} from "./stateCache/datastore/file.js";
101
+ import {CPStateDatastore} from "./stateCache/datastore/types.js";
100
102
  import {FIFOBlockStateCache} from "./stateCache/fifoBlockStateCache.js";
101
103
  import {InMemoryCheckpointStateCache} from "./stateCache/inMemoryCheckpointsCache.js";
102
104
  import {PersistentCheckpointStateCache} from "./stateCache/persistentCheckpointsCache.js";
105
+ import {CheckpointStateCache} from "./stateCache/types.js";
103
106
  import {ValidatorMonitor} from "./validatorMonitor.js";
104
107
 
105
108
  /**
@@ -181,6 +184,8 @@ export class BeaconChain implements IBeaconChain {
181
184
 
182
185
  protected readonly blockProcessor: BlockProcessor;
183
186
  protected readonly db: IBeaconDb;
187
+ // this is only available if nHistoricalStates is enabled
188
+ private readonly cpStateDatastore?: CPStateDatastore;
184
189
  private abortController = new AbortController();
185
190
  private processShutdownCallback: ProcessShutdownCallback;
186
191
  private _earliestAvailableSlot: Slot;
@@ -210,6 +215,7 @@ export class BeaconChain implements IBeaconChain {
210
215
  metrics,
211
216
  validatorMonitor,
212
217
  anchorState,
218
+ isAnchorStateFinalized,
213
219
  eth1,
214
220
  executionEngine,
215
221
  executionBuilder,
@@ -226,6 +232,7 @@ export class BeaconChain implements IBeaconChain {
226
232
  metrics: Metrics | null;
227
233
  validatorMonitor: ValidatorMonitor | null;
228
234
  anchorState: BeaconStateAllForks;
235
+ isAnchorStateFinalized: boolean;
229
236
  eth1: IEth1ForBlockProduction;
230
237
  executionEngine: IExecutionEngine;
231
238
  executionBuilder?: IExecutionBuilder;
@@ -326,23 +333,26 @@ export class BeaconChain implements IBeaconChain {
326
333
  this.bufferPool = this.opts.nHistoricalStates
327
334
  ? new BufferPool(anchorState.type.tree_serializedSize(anchorState.node), metrics)
328
335
  : null;
329
- const checkpointStateCache = this.opts.nHistoricalStates
330
- ? new PersistentCheckpointStateCache(
331
- {
332
- metrics,
333
- logger,
334
- clock,
335
- blockStateCache,
336
- bufferPool: this.bufferPool,
337
- datastore: fileDataStore
338
- ? // debug option if we want to investigate any issues with the DB
339
- new FileCPStateDatastore(dataDir)
340
- : // production option
341
- new DbCPStateDatastore(this.db),
342
- },
343
- this.opts
344
- )
345
- : new InMemoryCheckpointStateCache({metrics});
336
+
337
+ let checkpointStateCache: CheckpointStateCache;
338
+ this.cpStateDatastore = undefined;
339
+ if (this.opts.nHistoricalStates) {
340
+ this.cpStateDatastore = fileDataStore ? new FileCPStateDatastore(dataDir) : new DbCPStateDatastore(this.db);
341
+ checkpointStateCache = new PersistentCheckpointStateCache(
342
+ {
343
+ metrics,
344
+ logger,
345
+ clock,
346
+ blockStateCache,
347
+ bufferPool: this.bufferPool,
348
+ datastore: this.cpStateDatastore,
349
+ },
350
+ this.opts
351
+ );
352
+ } else {
353
+ checkpointStateCache = new InMemoryCheckpointStateCache({metrics});
354
+ }
355
+
346
356
  const {checkpoint} = computeAnchorCheckpoint(config, anchorState);
347
357
  blockStateCache.add(cachedState);
348
358
  blockStateCache.setHeadState(cachedState);
@@ -353,6 +363,7 @@ export class BeaconChain implements IBeaconChain {
353
363
  emitter,
354
364
  clock.currentSlot,
355
365
  cachedState,
366
+ isAnchorStateFinalized,
356
367
  opts,
357
368
  this.justifiedBalancesGetter.bind(this),
358
369
  metrics,
@@ -604,6 +615,20 @@ export class BeaconChain implements IBeaconChain {
604
615
  return data && {state: data, executionOptimistic: false, finalized: true};
605
616
  }
606
617
 
618
+ async getPersistedCheckpointState(checkpoint?: phase0.Checkpoint): Promise<Uint8Array | null> {
619
+ if (!this.cpStateDatastore) {
620
+ throw new Error("n-historical-state flag is not enabled");
621
+ }
622
+
623
+ if (checkpoint == null) {
624
+ // return the last safe checkpoint state by default
625
+ return this.cpStateDatastore.readLatestSafe();
626
+ }
627
+
628
+ const persistedKey = checkpointToDatastoreKey(checkpoint);
629
+ return this.cpStateDatastore.read(persistedKey);
630
+ }
631
+
607
632
  getStateByCheckpoint(
608
633
  checkpoint: CheckpointWithHex
609
634
  ): {state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null {
@@ -5,17 +5,22 @@ import {
5
5
  ForkChoiceStore,
6
6
  JustifiedBalancesGetter,
7
7
  ProtoArray,
8
+ ProtoBlock,
8
9
  ForkChoiceOpts as RawForkChoiceOpts,
9
10
  } from "@lodestar/fork-choice";
11
+ import {ZERO_HASH_HEX} from "@lodestar/params";
10
12
  import {
11
13
  CachedBeaconStateAllForks,
12
14
  DataAvailabilityStatus,
13
15
  computeAnchorCheckpoint,
16
+ computeEpochAtSlot,
17
+ computeStartSlotAtEpoch,
18
+ getBlockRootAtSlot,
14
19
  getEffectiveBalanceIncrementsZeroInactive,
15
20
  isExecutionStateType,
16
21
  isMergeTransitionComplete,
17
22
  } from "@lodestar/state-transition";
18
- import {Slot} from "@lodestar/types";
23
+ import {Slot, ssz} from "@lodestar/types";
19
24
  import {Logger, toRootHex} from "@lodestar/utils";
20
25
  import {GENESIS_SLOT} from "../../constants/index.js";
21
26
  import {Metrics} from "../../metrics/index.js";
@@ -35,6 +40,43 @@ export enum ForkchoiceCaller {
35
40
  * Fork Choice extended with a ChainEventEmitter
36
41
  */
37
42
  export function initializeForkChoice(
43
+ config: ChainForkConfig,
44
+ emitter: ChainEventEmitter,
45
+ currentSlot: Slot,
46
+ state: CachedBeaconStateAllForks,
47
+ isFinalizedState: boolean,
48
+ opts: ForkChoiceOpts,
49
+ justifiedBalancesGetter: JustifiedBalancesGetter,
50
+ metrics: Metrics | null,
51
+ logger?: Logger
52
+ ): ForkChoice {
53
+ return isFinalizedState
54
+ ? initializeForkChoiceFromFinalizedState(
55
+ config,
56
+ emitter,
57
+ currentSlot,
58
+ state,
59
+ opts,
60
+ justifiedBalancesGetter,
61
+ metrics,
62
+ logger
63
+ )
64
+ : initializeForkChoiceFromUnfinalizedState(
65
+ config,
66
+ emitter,
67
+ currentSlot,
68
+ state,
69
+ opts,
70
+ justifiedBalancesGetter,
71
+ metrics,
72
+ logger
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Initialize forkchoice from a finalized state.
78
+ */
79
+ export function initializeForkChoiceFromFinalizedState(
38
80
  config: ChainForkConfig,
39
81
  emitter: ChainEventEmitter,
40
82
  currentSlot: Slot,
@@ -82,7 +124,7 @@ export function initializeForkChoice(
82
124
  parentRoot: toRootHex(blockHeader.parentRoot),
83
125
  stateRoot: toRootHex(blockHeader.stateRoot),
84
126
  blockRoot: toRootHex(checkpoint.root),
85
- timeliness: true, // Optimisitcally assume is timely
127
+ timeliness: true, // Optimistically assume is timely
86
128
 
87
129
  justifiedEpoch: justifiedCheckpoint.epoch,
88
130
  justifiedRoot: toRootHex(justifiedCheckpoint.root),
@@ -105,6 +147,140 @@ export function initializeForkChoice(
105
147
  },
106
148
  currentSlot
107
149
  ),
150
+ state.validators.length,
151
+ metrics,
152
+ opts,
153
+ logger
154
+ );
155
+ }
156
+
157
+ /**
158
+ * Initialize forkchoice from an unfinalized state.
159
+ */
160
+ export function initializeForkChoiceFromUnfinalizedState(
161
+ config: ChainForkConfig,
162
+ emitter: ChainEventEmitter,
163
+ currentSlot: Slot,
164
+ unfinalizedState: CachedBeaconStateAllForks,
165
+ opts: ForkChoiceOpts,
166
+ justifiedBalancesGetter: JustifiedBalancesGetter,
167
+ metrics: Metrics | null,
168
+ logger?: Logger
169
+ ): ForkChoice {
170
+ const {blockHeader} = computeAnchorCheckpoint(config, unfinalizedState);
171
+ const finalizedCheckpoint = unfinalizedState.finalizedCheckpoint.toValue();
172
+ const justifiedCheckpoint = unfinalizedState.currentJustifiedCheckpoint.toValue();
173
+ const headRoot = toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader));
174
+
175
+ const logCtx = {
176
+ currentSlot: currentSlot,
177
+ stateSlot: unfinalizedState.slot,
178
+ headSlot: blockHeader.slot,
179
+ headRoot: headRoot,
180
+ finalizedEpoch: finalizedCheckpoint.epoch,
181
+ finalizedRoot: toRootHex(finalizedCheckpoint.root),
182
+ justifiedEpoch: justifiedCheckpoint.epoch,
183
+ justifiedRoot: toRootHex(justifiedCheckpoint.root),
184
+ };
185
+ logger?.warn("Initializing fork choice from unfinalized state", logCtx);
186
+
187
+ // this is not the justified state, but there is no other ways to get justified balances
188
+ const justifiedBalances = getEffectiveBalanceIncrementsZeroInactive(unfinalizedState);
189
+ const store = new ForkChoiceStore(
190
+ currentSlot,
191
+ justifiedCheckpoint,
192
+ finalizedCheckpoint,
193
+ justifiedBalances,
194
+ justifiedBalancesGetter,
195
+ {
196
+ onJustified: (cp) => emitter.emit(ChainEvent.forkChoiceJustified, cp),
197
+ onFinalized: (cp) => emitter.emit(ChainEvent.forkChoiceFinalized, cp),
198
+ }
199
+ );
200
+
201
+ // this is the same to the finalized state
202
+ const headBlock: ProtoBlock = {
203
+ slot: blockHeader.slot,
204
+ parentRoot: toRootHex(blockHeader.parentRoot),
205
+ stateRoot: toRootHex(blockHeader.stateRoot),
206
+ blockRoot: headRoot,
207
+ targetRoot: headRoot,
208
+ timeliness: true, // Optimistically assume is timely
209
+
210
+ justifiedEpoch: justifiedCheckpoint.epoch,
211
+ justifiedRoot: toRootHex(justifiedCheckpoint.root),
212
+ finalizedEpoch: finalizedCheckpoint.epoch,
213
+ finalizedRoot: toRootHex(finalizedCheckpoint.root),
214
+ unrealizedJustifiedEpoch: justifiedCheckpoint.epoch,
215
+ unrealizedJustifiedRoot: toRootHex(justifiedCheckpoint.root),
216
+ unrealizedFinalizedEpoch: finalizedCheckpoint.epoch,
217
+ unrealizedFinalizedRoot: toRootHex(finalizedCheckpoint.root),
218
+
219
+ ...(isExecutionStateType(unfinalizedState) && isMergeTransitionComplete(unfinalizedState)
220
+ ? {
221
+ executionPayloadBlockHash: toRootHex(unfinalizedState.latestExecutionPayloadHeader.blockHash),
222
+ executionPayloadNumber: unfinalizedState.latestExecutionPayloadHeader.blockNumber,
223
+ executionStatus: blockHeader.slot === GENESIS_SLOT ? ExecutionStatus.Valid : ExecutionStatus.Syncing,
224
+ }
225
+ : {executionPayloadBlockHash: null, executionStatus: ExecutionStatus.PreMerge}),
226
+
227
+ dataAvailabilityStatus: DataAvailabilityStatus.PreData,
228
+ };
229
+
230
+ const parentSlot = blockHeader.slot - 1;
231
+ const parentEpoch = computeEpochAtSlot(parentSlot);
232
+ // parent of head block
233
+ const parentBlock: ProtoBlock = {
234
+ ...headBlock,
235
+ slot: parentSlot,
236
+ // link this to the dummy justified block
237
+ parentRoot: toRootHex(justifiedCheckpoint.root),
238
+ // dummy data, we're not able to regen state before headBlock
239
+ stateRoot: ZERO_HASH_HEX,
240
+ blockRoot: headBlock.parentRoot,
241
+ targetRoot: toRootHex(getBlockRootAtSlot(unfinalizedState, computeStartSlotAtEpoch(parentEpoch))),
242
+ };
243
+
244
+ const justifiedBlock: ProtoBlock = {
245
+ ...headBlock,
246
+ slot: computeStartSlotAtEpoch(justifiedCheckpoint.epoch),
247
+ // link this to the finalized root so that getAncestors can find the finalized block
248
+ parentRoot: toRootHex(finalizedCheckpoint.root),
249
+ // dummy data, we're not able to regen state before headBlock
250
+ stateRoot: ZERO_HASH_HEX,
251
+ blockRoot: toRootHex(justifiedCheckpoint.root),
252
+ // same to blockRoot
253
+ targetRoot: toRootHex(justifiedCheckpoint.root),
254
+ };
255
+
256
+ const finalizedBlock: ProtoBlock = {
257
+ ...headBlock,
258
+ slot: computeStartSlotAtEpoch(finalizedCheckpoint.epoch),
259
+ // we don't care parent of finalized block
260
+ parentRoot: ZERO_HASH_HEX,
261
+ // dummy data, we're not able to regen state before headBlock
262
+ stateRoot: ZERO_HASH_HEX,
263
+ blockRoot: toRootHex(finalizedCheckpoint.root),
264
+ // same to blockRoot
265
+ targetRoot: toRootHex(finalizedCheckpoint.root),
266
+ };
267
+
268
+ const protoArray = ProtoArray.initialize(finalizedBlock, currentSlot);
269
+ protoArray.onBlock(justifiedBlock, currentSlot);
270
+ protoArray.onBlock(parentBlock, currentSlot);
271
+ protoArray.onBlock(headBlock, currentSlot);
272
+
273
+ logger?.verbose("Initialized protoArray successfully", {...logCtx, length: protoArray.length()});
274
+
275
+ // forkchoiceConstructor is only used for some test cases
276
+ // production code use ForkChoice constructor directly
277
+ const forkchoiceConstructor = opts.forkchoiceConstructor ?? ForkChoice;
278
+
279
+ return new forkchoiceConstructor(
280
+ config,
281
+ store,
282
+ protoArray,
283
+ unfinalizedState.validators.length,
108
284
  metrics,
109
285
  opts,
110
286
  logger
@@ -178,6 +178,8 @@ export interface IBeaconChain {
178
178
  stateRoot: RootHex,
179
179
  opts?: StateGetOpts
180
180
  ): Promise<{state: BeaconStateAllForks; executionOptimistic: boolean; finalized: boolean} | null>;
181
+ /** Return serialized bytes of a persisted checkpoint state */
182
+ getPersistedCheckpointState(checkpoint?: phase0.Checkpoint): Promise<Uint8Array | null>;
181
183
  /** Returns a cached state by checkpoint */
182
184
  getStateByCheckpoint(
183
185
  checkpoint: CheckpointWithHex
@@ -1,5 +1,11 @@
1
- import {phase0, ssz} from "@lodestar/types";
1
+ import {SLOTS_PER_EPOCH} from "@lodestar/params";
2
+ import {Epoch, phase0, ssz} from "@lodestar/types";
3
+ import {MapDef} from "@lodestar/utils";
2
4
  import {IBeaconDb} from "../../../db/interface.js";
5
+ import {
6
+ getLastProcessedSlotFromBeaconStateSerialized,
7
+ getSlotFromBeaconStateSerialized,
8
+ } from "../../../util/sszBytes.js";
3
9
  import {CPStateDatastore, DatastoreKey} from "./types.js";
4
10
 
5
11
  /**
@@ -22,6 +28,13 @@ export class DbCPStateDatastore implements CPStateDatastore {
22
28
  return this.db.checkpointState.getBinary(serializedCheckpoint);
23
29
  }
24
30
 
31
+ async readLatestSafe(): Promise<Uint8Array | null> {
32
+ const allKeys = await this.readKeys();
33
+ if (allKeys.length === 0) return null;
34
+
35
+ return getLatestSafeDatastoreKey(allKeys, this.read.bind(this));
36
+ }
37
+
25
38
  async readKeys(): Promise<DatastoreKey[]> {
26
39
  return this.db.checkpointState.keys();
27
40
  }
@@ -34,3 +47,78 @@ export function datastoreKeyToCheckpoint(key: DatastoreKey): phase0.Checkpoint {
34
47
  export function checkpointToDatastoreKey(cp: phase0.Checkpoint): DatastoreKey {
35
48
  return ssz.phase0.Checkpoint.serialize(cp);
36
49
  }
50
+
51
+ /**
52
+ * Get the latest safe checkpoint state the node can use to boot from
53
+ * - it should be the checkpoint state that's unique in its epoch
54
+ * - its last processed block slot should be at epoch boundary or last slot of previous epoch
55
+ * - state slot should be at epoch boundary
56
+ * - state slot should be equal to epoch * SLOTS_PER_EPOCH
57
+ *
58
+ * return the serialized data of Current Root Checkpoint State (CRCS) or Previous Root Checkpoint State (PRCS)
59
+ *
60
+ */
61
+ export async function getLatestSafeDatastoreKey(
62
+ allKeys: DatastoreKey[],
63
+ readFn: (key: DatastoreKey) => Promise<Uint8Array | null>
64
+ ): Promise<Uint8Array | null> {
65
+ const checkpointsByEpoch = new MapDef<Epoch, DatastoreKey[]>(() => []);
66
+ for (const key of allKeys) {
67
+ const cp = datastoreKeyToCheckpoint(key);
68
+ checkpointsByEpoch.getOrDefault(cp.epoch).push(key);
69
+ }
70
+
71
+ const dataStoreKeyByEpoch: Map<Epoch, DatastoreKey> = new Map();
72
+ for (const [epoch, keys] of checkpointsByEpoch.entries()) {
73
+ // only consider epochs with a single checkpoint to avoid ambiguity from forks
74
+ if (keys.length === 1) {
75
+ dataStoreKeyByEpoch.set(epoch, keys[0]);
76
+ }
77
+ }
78
+
79
+ const epochsDesc = Array.from(dataStoreKeyByEpoch.keys()).sort((a, b) => b - a);
80
+ for (const epoch of epochsDesc) {
81
+ const datastoreKey = dataStoreKeyByEpoch.get(epoch);
82
+ if (datastoreKey == null) {
83
+ // should not happen
84
+ continue;
85
+ }
86
+
87
+ const stateBytes = await readFn(datastoreKey);
88
+ if (stateBytes == null) {
89
+ // should not happen
90
+ continue;
91
+ }
92
+
93
+ const lastProcessedSlot = getLastProcessedSlotFromBeaconStateSerialized(stateBytes);
94
+ if (lastProcessedSlot == null) {
95
+ // cannot extract last processed slot from serialized state, skip
96
+ continue;
97
+ }
98
+
99
+ const stateSlot = getSlotFromBeaconStateSerialized(stateBytes);
100
+ if (stateSlot == null) {
101
+ // cannot extract slot from serialized state, skip
102
+ continue;
103
+ }
104
+
105
+ if (lastProcessedSlot !== stateSlot && lastProcessedSlot !== stateSlot - 1) {
106
+ // not CRCS or PRCS, skip
107
+ continue;
108
+ }
109
+
110
+ if (stateSlot % SLOTS_PER_EPOCH !== 0) {
111
+ // not at epoch boundary, skip
112
+ continue;
113
+ }
114
+
115
+ if (stateSlot !== SLOTS_PER_EPOCH * epoch) {
116
+ // should not happen after above checks, but just to be safe
117
+ continue;
118
+ }
119
+
120
+ return stateBytes;
121
+ }
122
+
123
+ return null;
124
+ }
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import {phase0, ssz} from "@lodestar/types";
3
3
  import {fromHex, toHex} from "@lodestar/utils";
4
4
  import {ensureDir, readFile, readFileNames, removeFile, writeIfNotExist} from "../../../util/file.js";
5
+ import {getLatestSafeDatastoreKey} from "./db.js";
5
6
  import {CPStateDatastore, DatastoreKey} from "./types.js";
6
7
 
7
8
  const CHECKPOINT_STATES_FOLDER = "checkpoint_states";
@@ -44,6 +45,13 @@ export class FileCPStateDatastore implements CPStateDatastore {
44
45
  return readFile(filePath);
45
46
  }
46
47
 
48
+ async readLatestSafe(): Promise<Uint8Array | null> {
49
+ const allKeys = await this.readKeys();
50
+ if (allKeys.length === 0) return null;
51
+
52
+ return getLatestSafeDatastoreKey(allKeys, this.read.bind(this));
53
+ }
54
+
47
55
  async readKeys(): Promise<DatastoreKey[]> {
48
56
  const fileNames = await readFileNames(this.folderPath);
49
57
  return fileNames
@@ -8,6 +8,7 @@ export interface CPStateDatastore {
8
8
  write: (cpKey: phase0.Checkpoint, stateBytes: Uint8Array) => Promise<DatastoreKey>;
9
9
  remove: (key: DatastoreKey) => Promise<void>;
10
10
  read: (key: DatastoreKey) => Promise<Uint8Array | null>;
11
+ readLatestSafe: () => Promise<Uint8Array | null>;
11
12
  readKeys: () => Promise<DatastoreKey[]>;
12
13
  init?: () => Promise<void>;
13
14
  }
package/src/index.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  export type {RestApiServerMetrics, RestApiServerModules, RestApiServerOpts} from "./api/rest/base.js";
4
4
  export {RestApiServer} from "./api/rest/base.js";
5
5
  export {checkAndPersistAnchorState, initStateFromDb, initStateFromEth1} from "./chain/index.js";
6
+ export {DbCPStateDatastore} from "./chain/stateCache/datastore/db.js";
7
+ export {FileCPStateDatastore} from "./chain/stateCache/datastore/file.js";
6
8
  export {BeaconDb, type IBeaconDb} from "./db/index.js";
7
9
  export {Eth1Provider, type IEth1Provider} from "./eth1/index.js";
8
10
  // Export metrics utilities to de-duplicate validator metrics
@@ -1878,7 +1878,7 @@ export function createLodestarMetrics(
1878
1878
  fetchKeys: register.histogram({
1879
1879
  name: "lodestar_prune_history_fetch_keys_time_seconds",
1880
1880
  help: "Time to fetch keys in seconds",
1881
- buckets: [0.001, 0.01, 0.1, 1],
1881
+ buckets: [0.001, 0.01, 0.1, 0.3, 0.5, 1],
1882
1882
  }),
1883
1883
 
1884
1884
  pruneKeys: register.histogram({
@@ -1,5 +1,3 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
1
  import worker from "node:worker_threads";
4
2
  import {privateKeyFromProtobuf} from "@libp2p/crypto/keys";
5
3
  import {peerIdFromPrivateKey} from "@libp2p/peer-id";
@@ -11,7 +9,7 @@ import {RegistryMetricCreator, collectNodeJSMetrics} from "../../metrics/index.j
11
9
  import {AsyncIterableBridgeCaller, AsyncIterableBridgeHandler} from "../../util/asyncIterableToEvents.js";
12
10
  import {Clock} from "../../util/clock.js";
13
11
  import {peerIdToString} from "../../util/peerId.js";
14
- import {profileNodeJS, writeHeapSnapshot} from "../../util/profile.js";
12
+ import {ProfileThread, profileThread, writeHeapSnapshot} from "../../util/profile.js";
15
13
  import {wireEventsOnWorkerThread} from "../../util/workerEvents.js";
16
14
  import {NetworkEventBus, NetworkEventData, networkEventDirection} from "../events.js";
17
15
  import {
@@ -157,10 +155,7 @@ const libp2pWorkerApi: NetworkWorkerApi = {
157
155
  dumpDiscv5KadValues: () => core.dumpDiscv5KadValues(),
158
156
  dumpMeshPeers: () => core.dumpMeshPeers(),
159
157
  writeProfile: async (durationMs: number, dirpath: string) => {
160
- const profile = await profileNodeJS(durationMs);
161
- const filePath = path.join(dirpath, `network_thread_${new Date().toISOString()}.cpuprofile`);
162
- fs.writeFileSync(filePath, profile);
163
- return filePath;
158
+ return profileThread(ProfileThread.NETWORK, durationMs, dirpath);
164
159
  },
165
160
  writeDiscv5Profile: async (durationMs: number, dirpath: string) => {
166
161
  return core.writeDiscv5Profile(durationMs, dirpath);