@chainsafe/lodestar 1.50.0-dev.53ad0a2af7 → 1.50.0-dev.56f65bd6e9

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 (60) hide show
  1. package/.git-data.json +1 -1
  2. package/lib/cmds/beacon/handler.d.ts +1 -0
  3. package/lib/cmds/beacon/handler.d.ts.map +1 -1
  4. package/lib/cmds/beacon/handler.js +11 -6
  5. package/lib/cmds/beacon/handler.js.map +1 -1
  6. package/lib/cmds/beacon/initBeaconState.d.ts +4 -2
  7. package/lib/cmds/beacon/initBeaconState.d.ts.map +1 -1
  8. package/lib/cmds/beacon/initBeaconState.js +26 -13
  9. package/lib/cmds/beacon/initBeaconState.js.map +1 -1
  10. package/lib/cmds/beacon/options.d.ts +1 -1
  11. package/lib/cmds/beacon/options.d.ts.map +1 -1
  12. package/lib/cmds/beacon/options.js +6 -5
  13. package/lib/cmds/beacon/options.js.map +1 -1
  14. package/lib/cmds/beacon/paths.d.ts +4 -0
  15. package/lib/cmds/beacon/paths.d.ts.map +1 -1
  16. package/lib/cmds/beacon/paths.js +3 -0
  17. package/lib/cmds/beacon/paths.js.map +1 -1
  18. package/lib/cmds/beacon/pubkeysFile.d.ts +10 -0
  19. package/lib/cmds/beacon/pubkeysFile.d.ts.map +1 -0
  20. package/lib/cmds/beacon/pubkeysFile.js +39 -0
  21. package/lib/cmds/beacon/pubkeysFile.js.map +1 -0
  22. package/lib/cmds/beacon/stateInitialization/checkpointState.js +24 -8
  23. package/lib/cmds/beacon/stateInitialization/checkpointState.js.map +1 -1
  24. package/lib/cmds/beacon/stateInitialization/genesisState.d.ts +1 -1
  25. package/lib/cmds/beacon/stateInitialization/genesisState.d.ts.map +1 -1
  26. package/lib/cmds/beacon/stateInitialization/genesisState.js +17 -3
  27. package/lib/cmds/beacon/stateInitialization/genesisState.js.map +1 -1
  28. package/lib/cmds/beacon/stateInitialization/prepareStateInitialization.d.ts +2 -2
  29. package/lib/cmds/beacon/stateInitialization/prepareStateInitialization.d.ts.map +1 -1
  30. package/lib/cmds/beacon/stateInitialization/prepareStateInitialization.js +20 -8
  31. package/lib/cmds/beacon/stateInitialization/prepareStateInitialization.js.map +1 -1
  32. package/lib/cmds/beacon/stateInitialization/types.d.ts +4 -2
  33. package/lib/cmds/beacon/stateInitialization/types.d.ts.map +1 -1
  34. package/lib/cmds/dev/handler.d.ts.map +1 -1
  35. package/lib/cmds/dev/handler.js +3 -1
  36. package/lib/cmds/dev/handler.js.map +1 -1
  37. package/lib/cmds/dev/options.d.ts +2 -1
  38. package/lib/cmds/dev/options.d.ts.map +1 -1
  39. package/lib/networks/index.d.ts.map +1 -1
  40. package/lib/networks/index.js +5 -0
  41. package/lib/networks/index.js.map +1 -1
  42. package/lib/options/beaconNodeOptions/chain.d.ts +2 -1
  43. package/lib/options/beaconNodeOptions/chain.d.ts.map +1 -1
  44. package/lib/options/beaconNodeOptions/chain.js +11 -4
  45. package/lib/options/beaconNodeOptions/chain.js.map +1 -1
  46. package/lib/options/beaconNodeOptions/index.d.ts +2 -1
  47. package/lib/options/beaconNodeOptions/index.d.ts.map +1 -1
  48. package/package.json +15 -15
  49. package/src/cmds/beacon/handler.ts +15 -6
  50. package/src/cmds/beacon/initBeaconState.ts +40 -13
  51. package/src/cmds/beacon/options.ts +8 -7
  52. package/src/cmds/beacon/paths.ts +5 -0
  53. package/src/cmds/beacon/pubkeysFile.ts +50 -0
  54. package/src/cmds/beacon/stateInitialization/checkpointState.ts +28 -12
  55. package/src/cmds/beacon/stateInitialization/genesisState.ts +18 -4
  56. package/src/cmds/beacon/stateInitialization/prepareStateInitialization.ts +21 -8
  57. package/src/cmds/beacon/stateInitialization/types.ts +4 -3
  58. package/src/cmds/dev/handler.ts +3 -1
  59. package/src/networks/index.ts +9 -0
  60. package/src/options/beaconNodeOptions/chain.ts +14 -5
@@ -10,13 +10,16 @@ import {
10
10
  isWithinWeakSubjectivityPeriodFromSummary,
11
11
  readBeaconStateBytesMetadata,
12
12
  } from "@lodestar/state-transition";
13
+ import {Slot} from "@lodestar/types";
13
14
  import {Logger, formatBytes} from "@lodestar/utils";
14
15
  import {GlobalArgs} from "../../options/globalOptions.js";
15
16
  import {BeaconArgs} from "./options.js";
17
+ import {loadPubkeysFile} from "./pubkeysFile.js";
16
18
  import {StateInitializationError, StateInitializationErrorCode} from "./stateInitialization/errors.js";
19
+ import {prepareGenesisInitialization} from "./stateInitialization/genesisState.js";
17
20
  import {
18
21
  prepareArchivedStateInitialization,
19
- prepareCheckpointOrGenesisInitialization,
22
+ prepareCheckpointSourceInitialization,
20
23
  } from "./stateInitialization/prepareStateInitialization.js";
21
24
  import {
22
25
  ArchivedStateBytes,
@@ -25,21 +28,27 @@ import {
25
28
  StatePreparationContext,
26
29
  } from "./stateInitialization/types.js";
27
30
 
28
- type InitBeaconStateResult = {anchorState: IBeaconStateView; config: BeaconConfig; isFinalized: boolean};
31
+ type InitBeaconStateResult = {
32
+ anchorState: IBeaconStateView;
33
+ config: BeaconConfig;
34
+ isFinalized: boolean;
35
+ earliestAvailableSlot: Slot;
36
+ };
29
37
 
30
38
  /**
31
39
  * Select serialized anchor bytes before constructing the state used for validation, persistence, and return.
32
- * Populates the global `pubkeyCache` with the anchor state's validators.
40
+ * Populates the global `pubkeyCache` with the anchor state's validators, reusing `pubkeysFile` if it matches.
33
41
  */
34
42
  export async function initBeaconState(
35
43
  args: BeaconArgs & GlobalArgs,
36
44
  dataDir: string,
45
+ pubkeysFile: string,
37
46
  chainForkConfig: ChainForkConfig,
38
47
  db: IBeaconDb,
39
48
  logger: Logger
40
49
  ): Promise<InitBeaconStateResult> {
41
50
  const options: StateInitializationOptions = args;
42
- const useNative = options["chain.nativeStateView"] ?? false;
51
+ const nativeStateTransition = options["chain.nativeStateTransition"] ?? false;
43
52
  if (
44
53
  options.forceCheckpointSync &&
45
54
  !(options.checkpointState || options.checkpointSyncUrl || options.unsafeCheckpointState)
@@ -65,11 +74,23 @@ export async function initBeaconState(
65
74
  options.lastPersistedCheckpointState
66
75
  );
67
76
  if (!options.forceCheckpointSync && (!hasCheckpointSource || archived.isWithinWeakSubjectivityPeriod)) {
68
- return executeStateInitialization(prepareArchivedStateInitialization(archived, context), useNative);
77
+ return executeStateInitialization(
78
+ prepareArchivedStateInitialization(archived, context),
79
+ nativeStateTransition,
80
+ pubkeysFile,
81
+ logger
82
+ );
69
83
  }
70
84
  }
71
- const stateInit = await prepareCheckpointOrGenesisInitialization(options, archived, context);
72
- return executeStateInitialization(stateInit, useNative);
85
+ let stateInit = await prepareCheckpointSourceInitialization(options, archived, context);
86
+ if (stateInit === null) {
87
+ // Without a usable checkpoint state, resume from the db as if no checkpoint source was set instead of genesis
88
+ stateInit =
89
+ archived !== null
90
+ ? prepareArchivedStateInitialization(archived, context)
91
+ : await prepareGenesisInitialization(options, context);
92
+ }
93
+ return executeStateInitialization(stateInit, nativeStateTransition, pubkeysFile, logger);
73
94
  }
74
95
 
75
96
  /**
@@ -97,7 +118,7 @@ async function readLatestArchivedStateBytes({
97
118
  if (weakSubjectivity === null) {
98
119
  throw new StateInitializationError(
99
120
  {code: StateInitializationErrorCode.MALFORMED_STATE_BYTES},
100
- "Cannot extract weak subjectivity summary from archived state bytes"
121
+ `Cannot extract weak subjectivity summary from archived state bytes, expected ${chainForkConfig.getForkName(metadata.slot)} state at slot ${metadata.slot}, possible fork or network mismatch`
101
122
  );
102
123
  }
103
124
  return {
@@ -114,7 +135,9 @@ async function readLatestArchivedStateBytes({
114
135
  */
115
136
  async function executeStateInitialization(
116
137
  stateInit: StateInitialization,
117
- useNative: boolean
138
+ nativeStateTransition: boolean,
139
+ pubkeysFile: string,
140
+ logger: Logger
118
141
  ): Promise<InitBeaconStateResult> {
119
142
  const {config, stateBytes} = stateInit;
120
143
  const validatorCount = getValidatorCountFromStateBytes(config, stateBytes);
@@ -129,10 +152,14 @@ async function executeStateInitialization(
129
152
  // by its native lock; if this headroom is exceeded, it grows by the same fixed step.
130
153
  // The view syncs pubkeys during construction, so capacity must be reserved first.
131
154
  const headroomEpochs = (90 * 24 * 60 * 60) / (config.SECONDS_PER_SLOT * SLOTS_PER_EPOCH);
132
- pubkeyCache.ensureCapacity(validatorCount + MAX_PENDING_DEPOSITS_PER_EPOCH * Math.ceil(headroomEpochs));
133
- const anchorState = createBeaconStateView({useNative, config, stateBytes});
155
+ const pubkeyCacheCapacity = validatorCount + MAX_PENDING_DEPOSITS_PER_EPOCH * Math.ceil(headroomEpochs);
156
+ loadPubkeysFile(pubkeyCache, pubkeysFile, pubkeyCacheCapacity, config, stateBytes, validatorCount, logger);
157
+ // unilaterally expand capacity after best-effort pubkey file loading
158
+ pubkeyCache.ensureCapacity(pubkeyCacheCapacity);
159
+ const anchorState = createBeaconStateView({nativeStateTransition, config, stateBytes});
134
160
  stateInit.validate(anchorState);
161
+ const earliestAvailableSlot = await stateInit.initializeEarliestAvailableSlot(anchorState);
135
162
  await stateInit.persist?.(anchorState, stateBytes);
136
- stateInit.log(anchorState);
137
- return {anchorState, config, isFinalized: stateInit.isFinalized};
163
+ stateInit.log(anchorState, nativeStateTransition);
164
+ return {anchorState, config, isFinalized: stateInit.isFinalized, earliestAvailableSlot};
138
165
  }
@@ -4,7 +4,6 @@ import {LogArgs, logOptions} from "../../options/logOptions.js";
4
4
  import {BeaconPaths, defaultBeaconPaths} from "./paths.js";
5
5
 
6
6
  type BeaconExtraArgs = {
7
- forceGenesis?: boolean;
8
7
  genesisStateFile?: string;
9
8
  configFile?: string;
10
9
  bootnodesFile?: string;
@@ -22,6 +21,7 @@ type BeaconExtraArgs = {
22
21
  persistInvalidSszObjectsRetentionHours?: number;
23
22
  persistOrphanedBlocksDir?: string;
24
23
  peerStoreDir?: string;
24
+ pubkeysFile?: string;
25
25
  persistNetworkIdentity?: boolean;
26
26
  private?: boolean;
27
27
  validatorMonitorLogs?: boolean;
@@ -30,12 +30,6 @@ type BeaconExtraArgs = {
30
30
  };
31
31
 
32
32
  export const beaconExtraOptions: CliCommandOptions<BeaconExtraArgs> = {
33
- forceGenesis: {
34
- description: "Force beacon to create genesis without file",
35
- type: "boolean",
36
- hidden: true,
37
- },
38
-
39
33
  genesisStateFile: {
40
34
  description: "Path or URL to download a genesis state file in ssz-encoded format",
41
35
  type: "string",
@@ -148,6 +142,13 @@ export const beaconExtraOptions: CliCommandOptions<BeaconExtraArgs> = {
148
142
  type: "string",
149
143
  },
150
144
 
145
+ pubkeysFile: {
146
+ hidden: true,
147
+ description: "Pubkey cache file, loaded on startup and saved on graceful shutdown",
148
+ defaultDescription: defaultBeaconPaths.pubkeysFile,
149
+ type: "string",
150
+ },
151
+
151
152
  persistNetworkIdentity: {
152
153
  description:
153
154
  "Whether to reuse the same peer-id across restarts. Validator custody requires custody group count to persist relative to a given ENR. Setting to false will reset ENR and validator custody requirements on restarts.",
@@ -9,6 +9,7 @@ export type BeaconPathsPartial = Partial<{
9
9
  dataColumnDir: string;
10
10
  persistInvalidSszObjectsDir: string;
11
11
  persistOrphanedBlocksDir?: string;
12
+ pubkeysFile: string;
12
13
  }>;
13
14
 
14
15
  export type BeaconPaths = {
@@ -18,6 +19,7 @@ export type BeaconPaths = {
18
19
  dataColumnDir: string;
19
20
  persistInvalidSszObjectsDir: string;
20
21
  persistOrphanedBlocksDir: string;
22
+ pubkeysFile: string;
21
23
  };
22
24
 
23
25
  /**
@@ -29,6 +31,7 @@ export type BeaconPaths = {
29
31
  * ├── beacon.config.json
30
32
  * ├── peer-id.json
31
33
  * ├── enr
34
+ * ├── pubkeys
32
35
  * ├── chain-db
33
36
  * └── beacon.log
34
37
  * ```
@@ -48,6 +51,7 @@ export function getBeaconPaths(
48
51
  const persistInvalidSszObjectsDir = args.persistInvalidSszObjectsDir ?? path.join(beaconDir, "invalidSszObjects");
49
52
  const peerStoreDir = args.peerStoreDir ?? path.join(beaconDir, "peerstore");
50
53
  const persistOrphanedBlocksDir = args.persistOrphanedBlocksDir ?? path.join(beaconDir, "orphaned_blocks");
54
+ const pubkeysFile = args.pubkeysFile ?? path.join(beaconDir, "pubkeys");
51
55
 
52
56
  return {
53
57
  ...globalPaths,
@@ -57,6 +61,7 @@ export function getBeaconPaths(
57
61
  persistInvalidSszObjectsDir,
58
62
  peerStoreDir,
59
63
  persistOrphanedBlocksDir,
64
+ pubkeysFile,
60
65
  };
61
66
  }
62
67
 
@@ -0,0 +1,50 @@
1
+ import {type PubkeyCache} from "@chainsafe/lodestar-z/pubkeys";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
+ import {getValidatorPubkeyFromStateBytes} from "@lodestar/state-transition";
4
+ import {Logger, byteArrayEquals} from "@lodestar/utils";
5
+
6
+ /**
7
+ * Best effort load of a trusted pubkey cache saved by a previous run, to skip decompressing every validator
8
+ * pubkey on boot. Must be called before any worker uses the cache.
9
+ */
10
+ export function loadPubkeysFile(
11
+ pubkeyCache: PubkeyCache,
12
+ filepath: string,
13
+ maxCapacity: number,
14
+ config: ChainForkConfig,
15
+ anchorStateBytes: Uint8Array,
16
+ anchorValidatorCount: number,
17
+ logger: Logger
18
+ ): void {
19
+ const start = Date.now();
20
+ try {
21
+ pubkeyCache.load(filepath, maxCapacity);
22
+ } catch (e) {
23
+ logger.debug("Unable to load pubkeys file", {filepath}, e as Error);
24
+ return;
25
+ }
26
+
27
+ // load() only checks framing and checksum, so reject a file from another chain. Checking the endpoints
28
+ // suffices since the registry is append-only.
29
+ const overlap = Math.min(pubkeyCache.size, anchorValidatorCount);
30
+ for (const index of overlap > 0 ? [0, overlap - 1] : []) {
31
+ const anchorPubkey = getValidatorPubkeyFromStateBytes(config, anchorStateBytes, index);
32
+ if (anchorPubkey === null || !byteArrayEquals(pubkeyCache.getPubkeyBytesOrThrow(index), anchorPubkey)) {
33
+ pubkeyCache.reset();
34
+ logger.debug("Discarded pubkeys file not matching anchor state", {filepath, index});
35
+ return;
36
+ }
37
+ }
38
+
39
+ logger.debug("Loaded pubkeys file", {filepath, count: pubkeyCache.size, durationMs: Date.now() - start});
40
+ }
41
+
42
+ export function savePubkeysFile(pubkeyCache: PubkeyCache, filepath: string, logger: Logger): void {
43
+ const start = Date.now();
44
+ try {
45
+ pubkeyCache.save(filepath);
46
+ logger.debug("Saved pubkeys file", {filepath, count: pubkeyCache.size, durationMs: Date.now() - start});
47
+ } catch (e) {
48
+ logger.debug("Unable to save pubkeys file", {filepath}, e as Error);
49
+ }
50
+ }
@@ -178,7 +178,7 @@ function prepareCheckpointInitialization(
178
178
  if (weakSubjectivity === null) {
179
179
  throw new StateInitializationError(
180
180
  {code: StateInitializationErrorCode.MALFORMED_STATE_BYTES},
181
- `Cannot extract weak subjectivity summary from ${candidate.source}`
181
+ `Cannot extract weak subjectivity summary from ${candidate.source}, expected ${chainForkConfig.getForkName(metadata.slot)} state at slot ${metadata.slot}, possible fork or network mismatch`
182
182
  );
183
183
  }
184
184
  const expectedCheckpoint = useArchived ? null : candidate.expectedCheckpoint;
@@ -186,6 +186,16 @@ function prepareCheckpointInitialization(
186
186
  const {isFinalized, ignoreWeakSubjectivityCheck} = policy;
187
187
  const config = createBeaconConfig(chainForkConfig, metadata.genesisValidatorsRoot);
188
188
  const archivedWithinWeakSubjectivityPeriod = useArchived ? archived.isWithinWeakSubjectivityPeriod : null;
189
+ const passedValidation =
190
+ archivedWithinWeakSubjectivityPeriod ?? isWithinWeakSubjectivityPeriodFromSummary(config, weakSubjectivity);
191
+ // Fast-fail stale checkpoints before deserializing the state or reserving pubkey-cache capacity.
192
+ if (!passedValidation && !ignoreWeakSubjectivityCheck) {
193
+ const clockEpoch = computeEpochAtSlot(getCurrentSlot(config, weakSubjectivity.genesisTime));
194
+ throw new StateInitializationError(
195
+ {code: StateInitializationErrorCode.STALE_CHECKPOINT},
196
+ `The selected state with epoch ${weakSubjectivity.checkpointEpoch} is not within weak subjectivity period of ${weakSubjectivity.period} epochs from the current epoch ${clockEpoch}. Please verify your checkpoint source`
197
+ );
198
+ }
189
199
  // DB-selected anchors need no persistence, including at slot zero.
190
200
  const shouldPersist = isFinalized && !useArchived;
191
201
  return {
@@ -195,17 +205,8 @@ function prepareCheckpointInitialization(
195
205
  validate(state) {
196
206
  // A supplied checkpoint must match even when the period check is ignored.
197
207
  if (expectedCheckpoint !== null) assertStateMatchesCheckpoint(state, expectedCheckpoint);
198
- const passedValidation =
199
- archivedWithinWeakSubjectivityPeriod ?? isWithinWeakSubjectivityPeriodFromSummary(config, weakSubjectivity);
200
- if (!passedValidation && !ignoreWeakSubjectivityCheck) {
201
- const clockEpoch = computeEpochAtSlot(getCurrentSlot(config, weakSubjectivity.genesisTime));
202
- throw new StateInitializationError(
203
- {code: StateInitializationErrorCode.STALE_CHECKPOINT},
204
- `The selected state with epoch ${weakSubjectivity.checkpointEpoch} is not within weak subjectivity period of ${weakSubjectivity.period} epochs from the current epoch ${clockEpoch}. Please verify your checkpoint source`
205
- );
206
- }
208
+ assertAnchorStateForkMatchesConfig(config, state);
207
209
  if (isFinalized) {
208
- assertAnchorStateForkMatchesConfig(config, state);
209
210
  const source = useArchived ? "db" : "checkpoint";
210
211
  const logData = {
211
212
  slot: state.slot,
@@ -222,8 +223,22 @@ function prepareCheckpointInitialization(
222
223
  }
223
224
  }
224
225
  },
226
+ async initializeEarliestAvailableSlot(state) {
227
+ const stored = await db.earliestAvailableSlot.get();
228
+ const floor = useArchived ? (stored ?? state.slot) : Math.max(stored ?? 0, state.slot);
229
+ if (stored !== floor) {
230
+ await db.earliestAvailableSlot.set(floor);
231
+ }
232
+ logger.verbose("Initialized earliest available slot", {
233
+ source,
234
+ anchorSlot: state.slot,
235
+ previousSlot: stored,
236
+ earliestAvailableSlot: floor,
237
+ });
238
+ return floor;
239
+ },
225
240
  persist: shouldPersist ? (state, bytes) => persistAnchorState(config, db, state, bytes) : null,
226
- log(state) {
241
+ log(state, nativeStateTransition) {
227
242
  const {checkpoint} = state.computeAnchorCheckpoint();
228
243
  logger.info("Initialized checkpoint state", {
229
244
  source,
@@ -233,6 +248,7 @@ function prepareCheckpointInitialization(
233
248
  stateRoot: toRootHex(state.hashTreeRoot()),
234
249
  checkpointRoot: toRootHex(checkpoint.root),
235
250
  isFinalized,
251
+ nativeStateTransition,
236
252
  ...(isFinalized ? {} : {lastProcessedSlot: state.latestBlockHeader.slot}),
237
253
  });
238
254
  },
@@ -15,11 +15,11 @@ import {StateInitialization, StateInitializationOptions, StatePreparationContext
15
15
  import {assertAnchorStateForkMatchesConfig} from "./validation.js";
16
16
 
17
17
  export async function prepareGenesisInitialization(
18
- options: Pick<StateInitializationOptions, "genesisStateFile" | "network" | "forceGenesis">,
18
+ options: Pick<StateInitializationOptions, "genesisStateFile" | "network">,
19
19
  {chainForkConfig, db, logger}: StatePreparationContext
20
20
  ): Promise<StateInitialization> {
21
21
  const source = options.genesisStateFile || getGenesisFileUrl(options.network || defaultNetwork);
22
- if (!source || options.forceGenesis) {
22
+ if (!source) {
23
23
  throw new StateInitializationError(
24
24
  {code: StateInitializationErrorCode.MISSING_INITIALIZATION_SOURCE},
25
25
  "Failed to initialize beacon state, please provide a genesis state file or use checkpoint sync"
@@ -40,7 +40,7 @@ export async function prepareGenesisInitialization(
40
40
  if (weakSubjectivity === null) {
41
41
  throw new StateInitializationError(
42
42
  {code: StateInitializationErrorCode.MALFORMED_STATE_BYTES},
43
- "Cannot extract weak subjectivity summary from genesis state bytes"
43
+ `Cannot extract weak subjectivity summary from genesis state bytes, expected ${chainForkConfig.getForkName(metadata.slot)} state at slot ${metadata.slot}, possible fork or network mismatch`
44
44
  );
45
45
  }
46
46
  const config = createBeaconConfig(chainForkConfig, metadata.genesisValidatorsRoot);
@@ -73,12 +73,26 @@ export async function prepareGenesisInitialization(
73
73
  logger.warn("Checkpoint sync recommended, please use --help to see checkpoint sync options");
74
74
  }
75
75
  },
76
+ async initializeEarliestAvailableSlot(state) {
77
+ const stored = await db.earliestAvailableSlot.get();
78
+ if (stored !== null && stored !== state.slot) {
79
+ logger.warn("Resetting earliest available slot for genesis initialization", {
80
+ previousSlot: stored,
81
+ earliestAvailableSlot: state.slot,
82
+ });
83
+ }
84
+ if (stored !== state.slot) {
85
+ await db.earliestAvailableSlot.set(state.slot);
86
+ }
87
+ return state.slot;
88
+ },
76
89
  persist: (state, bytes) => persistAnchorState(config, db, state, bytes),
77
- log(state) {
90
+ log(state, nativeStateTransition) {
78
91
  logger.info("Initialized genesis state", {
79
92
  slot: state.slot,
80
93
  stateRoot: toRootHex(state.hashTreeRoot()),
81
94
  isFinalized: true,
95
+ nativeStateTransition,
82
96
  });
83
97
  },
84
98
  };
@@ -6,13 +6,12 @@ import {
6
6
  prepareCheckpointFileInitialization,
7
7
  prepareUnfinalizedCheckpointInitialization,
8
8
  } from "./checkpointState.js";
9
- import {prepareGenesisInitialization} from "./genesisState.js";
10
9
  import {ArchivedStateBytes, StateInitialization, StateInitializationOptions, StatePreparationContext} from "./types.js";
11
10
  import {assertAnchorStateForkMatchesConfig} from "./validation.js";
12
11
 
13
12
  export function prepareArchivedStateInitialization(
14
13
  {stateBytes, metadata, isWithinWeakSubjectivityPeriod}: ArchivedStateBytes,
15
- {chainForkConfig, logger}: StatePreparationContext
14
+ {chainForkConfig, db, logger}: StatePreparationContext
16
15
  ): StateInitialization {
17
16
  const config = createBeaconConfig(chainForkConfig, metadata.genesisValidatorsRoot);
18
17
  return {
@@ -35,23 +34,38 @@ export function prepareArchivedStateInitialization(
35
34
  logger.warn("Checkpoint sync recommended, please use --help to see checkpoint sync options");
36
35
  }
37
36
  },
37
+ async initializeEarliestAvailableSlot(state) {
38
+ const stored = await db.earliestAvailableSlot.get();
39
+ // we bootstrap the node with archived db, use the previous earliestAvailableSLot
40
+ if (stored !== null) {
41
+ logger.verbose("Reusing persisted earliest available slot", {
42
+ anchorSlot: state.slot,
43
+ earliestAvailableSlot: stored,
44
+ });
45
+ return stored;
46
+ }
47
+ // legacy db: blockArchive.firstKey() is not a safe floor, a past checkpoint sync may have left a gap.
48
+ await db.earliestAvailableSlot.set(state.slot);
49
+ return state.slot;
50
+ },
38
51
  persist: null,
39
- log(state) {
52
+ log(state, nativeStateTransition) {
40
53
  logger.info("Initialized state from db", {
41
54
  slot: state.slot,
42
55
  epoch: computeEpochAtSlot(state.slot),
43
56
  stateRoot: toRootHex(state.hashTreeRoot()),
44
57
  isFinalized: true,
58
+ nativeStateTransition,
45
59
  });
46
60
  },
47
61
  };
48
62
  }
49
63
 
50
- export async function prepareCheckpointOrGenesisInitialization(
64
+ export async function prepareCheckpointSourceInitialization(
51
65
  options: StateInitializationOptions,
52
66
  archived: ArchivedStateBytes | null,
53
67
  context: StatePreparationContext
54
- ): Promise<StateInitialization> {
68
+ ): Promise<StateInitialization | null> {
55
69
  if (options.checkpointState) {
56
70
  return prepareCheckpointFileInitialization(options.checkpointState, options, archived, context);
57
71
  }
@@ -59,8 +73,7 @@ export async function prepareCheckpointOrGenesisInitialization(
59
73
  return prepareCheckpointApiInitialization(options.checkpointSyncUrl, options, archived, context);
60
74
  }
61
75
  if (options.lastPersistedCheckpointState || options.unsafeCheckpointState) {
62
- const stateInit = await prepareUnfinalizedCheckpointInitialization(options, archived, context);
63
- if (stateInit !== null) return stateInit;
76
+ return prepareUnfinalizedCheckpointInitialization(options, archived, context);
64
77
  }
65
- return prepareGenesisInitialization(options, context);
78
+ return null;
66
79
  }
@@ -1,6 +1,7 @@
1
1
  import {IBeaconDb} from "@lodestar/beacon-node";
2
2
  import {BeaconConfig, ChainForkConfig} from "@lodestar/config";
3
3
  import {IBeaconStateView, StateBytesMetadata, WeakSubjectivitySummary} from "@lodestar/state-transition";
4
+ import {Slot} from "@lodestar/types";
4
5
  import {Checkpoint} from "@lodestar/types/phase0";
5
6
  import {Logger} from "@lodestar/utils";
6
7
  import {GlobalArgs} from "../../../options/globalOptions.js";
@@ -16,10 +17,9 @@ export type StateInitializationOptions = Pick<
16
17
  | "wssCheckpoint"
17
18
  | "forceCheckpointSync"
18
19
  | "ignoreWeakSubjectivityCheck"
19
- | "forceGenesis"
20
20
  | "network"
21
21
  | "chain.nHistoricalStatesFileDataStore"
22
- | "chain.nativeStateView"
22
+ | "chain.nativeStateTransition"
23
23
  >;
24
24
 
25
25
  export type StatePreparationContext = {
@@ -54,6 +54,7 @@ export type StateInitialization = {
54
54
  config: BeaconConfig;
55
55
  isFinalized: boolean;
56
56
  validate: (state: IBeaconStateView) => void;
57
+ initializeEarliestAvailableSlot: (state: IBeaconStateView) => Promise<Slot>;
57
58
  persist: ((state: IBeaconStateView, stateBytes: Uint8Array) => Promise<void>) | null;
58
- log: (state: IBeaconStateView) => void;
59
+ log: (state: IBeaconStateView, nativeStateTransition: boolean) => void;
59
60
  };
@@ -31,7 +31,7 @@ export async function devHandler(args: IDevArgs & GlobalArgs): Promise<void> {
31
31
 
32
32
  // Note: defaults to network "dev", to all paths are custom and don't conflict with networks.
33
33
  // Flag --reset cleans up the custom dirs on dev stop
34
- const beaconDbDir = getBeaconPaths(args, network).dbDir;
34
+ const {dbDir: beaconDbDir, pubkeysFile} = getBeaconPaths(args, network);
35
35
  const validatorsDbDir = getValidatorPaths(args, network).validatorsDbDir;
36
36
 
37
37
  // Remove slashing protection db. Otherwise the validators won't be able to propose nor attest
@@ -39,6 +39,8 @@ export async function devHandler(args: IDevArgs & GlobalArgs): Promise<void> {
39
39
  if (args.genesisTime === undefined) {
40
40
  await rimraf(beaconDbDir);
41
41
  await rimraf(validatorsDbDir);
42
+ // Validators deposited in the previous run do not exist on the new chain
43
+ await rimraf(pubkeysFile);
42
44
  }
43
45
 
44
46
  mkdir(beaconDbDir);
@@ -182,11 +182,20 @@ export async function fetchWeakSubjectivityStateBytes(
182
182
  GET_STATE_LOG_INTERVAL
183
183
  );
184
184
  const wsStateBytes = response.ssz();
185
+ const {version} = response.meta();
185
186
 
186
187
  const wsSlot = getStateSlotFromBytes(wsStateBytes);
187
188
  const logData = {stateId, size: formatBytes(wsStateBytes.length)};
188
189
  logger.info("Download completed", typeof stateId === "number" ? logData : {...logData, slot: wsSlot});
189
190
 
191
+ const expectedFork = config.getForkName(wsSlot);
192
+ if (version !== expectedFork) {
193
+ throw new LodestarError(
194
+ {code: "CHECKPOINT_FORK_MISMATCH"},
195
+ `Checkpoint sync server returned ${version} state at slot ${wsSlot} but local config expects ${expectedFork}, verify --network or the chain config`
196
+ );
197
+ }
198
+
190
199
  return {
191
200
  stateBytes: new Uint8Array(wsStateBytes.buffer, wsStateBytes.byteOffset, wsStateBytes.byteLength),
192
201
  expectedCheckpoint: wsCheckpoint,
@@ -12,6 +12,7 @@ export type ChainArgs = {
12
12
  "chain.blsVerifyAllMainThread"?: boolean;
13
13
  "chain.disableBlsBatchVerify"?: boolean;
14
14
  "chain.persistProducedBlocks"?: boolean;
15
+ "chain.persistProducedPayloadEnvelopes"?: boolean;
15
16
  "chain.persistInvalidSszObjects"?: boolean;
16
17
  // No need to define chain.persistInvalidSszObjects as part of ChainArgs
17
18
  // as this is defined as part of BeaconPaths
@@ -35,7 +36,7 @@ export type ChainArgs = {
35
36
  "chain.archiveDataEpochs"?: number;
36
37
  "chain.archiveMode": ArchiveMode;
37
38
  "chain.nHistoricalStatesFileDataStore"?: boolean;
38
- "chain.nativeStateView"?: boolean;
39
+ "chain.nativeStateTransition"?: boolean;
39
40
  "chain.maxBlockStates"?: number;
40
41
  "chain.maxCPStateEpochsInMemory"?: number;
41
42
  "chain.maxCPStateEpochsOnDisk"?: number;
@@ -53,6 +54,7 @@ export function parseArgs(args: ChainArgs & CircuitBreakerArgs): IBeaconNodeOpti
53
54
  blsVerifyAllMainThread: args["chain.blsVerifyAllMainThread"],
54
55
  disableBlsBatchVerify: args["chain.disableBlsBatchVerify"],
55
56
  persistProducedBlocks: args["chain.persistProducedBlocks"],
57
+ persistProducedPayloadEnvelopes: args["chain.persistProducedPayloadEnvelopes"],
56
58
  persistInvalidSszObjects: args["chain.persistInvalidSszObjects"],
57
59
  // biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here
58
60
  persistInvalidSszObjectsDir: undefined as any,
@@ -79,7 +81,7 @@ export function parseArgs(args: ChainArgs & CircuitBreakerArgs): IBeaconNodeOpti
79
81
  archiveMode: args["chain.archiveMode"] ?? defaultOptions.chain.archiveMode,
80
82
  nHistoricalStatesFileDataStore:
81
83
  args["chain.nHistoricalStatesFileDataStore"] ?? defaultOptions.chain.nHistoricalStatesFileDataStore,
82
- nativeStateView: args["chain.nativeStateView"] ?? defaultOptions.chain.nativeStateView,
84
+ nativeStateTransition: args["chain.nativeStateTransition"] ?? defaultOptions.chain.nativeStateTransition,
83
85
  maxBlockStates: args["chain.maxBlockStates"] ?? defaultOptions.chain.maxBlockStates,
84
86
  maxCPStateEpochsInMemory: args["chain.maxCPStateEpochsInMemory"] ?? defaultOptions.chain.maxCPStateEpochsInMemory,
85
87
  maxCPStateEpochsOnDisk: args["chain.maxCPStateEpochsOnDisk"] ?? defaultOptions.chain.maxCPStateEpochsOnDisk,
@@ -170,6 +172,13 @@ Will double processing times. Use only for debugging purposes.",
170
172
  group: "chain",
171
173
  },
172
174
 
175
+ "chain.persistProducedPayloadEnvelopes": {
176
+ hidden: true,
177
+ type: "boolean",
178
+ description: "Persist produced execution payload envelopes as SSZ files for debugging",
179
+ group: "chain",
180
+ },
181
+
173
182
  "chain.persistInvalidSszObjects": {
174
183
  hidden: true,
175
184
  type: "boolean",
@@ -318,11 +327,11 @@ Will double processing times. Use only for debugging purposes.",
318
327
  group: "chain",
319
328
  },
320
329
 
321
- "chain.nativeStateView": {
330
+ "chain.nativeStateTransition": {
322
331
  hidden: true,
323
- description: "Use native (Zig) BeaconStateView instead of JS implementation",
332
+ description: "Use native (Zig) state transition instead of JS implementation",
324
333
  type: "boolean",
325
- default: defaultOptions.chain.nativeStateView,
334
+ default: defaultOptions.chain.nativeStateTransition,
326
335
  group: "chain",
327
336
  },
328
337