@chainsafe/lodestar 1.50.0-dev.ebecebb9ba → 1.50.0-dev.f0a5c36612

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 (75) 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 +13 -21
  5. package/lib/cmds/beacon/handler.js.map +1 -1
  6. package/lib/cmds/beacon/initBeaconState.d.ts +13 -17
  7. package/lib/cmds/beacon/initBeaconState.d.ts.map +1 -1
  8. package/lib/cmds/beacon/initBeaconState.js +84 -275
  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.d.ts +7 -0
  23. package/lib/cmds/beacon/stateInitialization/checkpointState.d.ts.map +1 -0
  24. package/lib/cmds/beacon/stateInitialization/checkpointState.js +184 -0
  25. package/lib/cmds/beacon/stateInitialization/checkpointState.js.map +1 -0
  26. package/lib/cmds/beacon/stateInitialization/errors.d.ts +17 -0
  27. package/lib/cmds/beacon/stateInitialization/errors.d.ts.map +1 -0
  28. package/lib/cmds/beacon/stateInitialization/errors.js +16 -0
  29. package/lib/cmds/beacon/stateInitialization/errors.js.map +1 -0
  30. package/lib/cmds/beacon/stateInitialization/genesisState.d.ts +3 -0
  31. package/lib/cmds/beacon/stateInitialization/genesisState.d.ts.map +1 -0
  32. package/lib/cmds/beacon/stateInitialization/genesisState.js +79 -0
  33. package/lib/cmds/beacon/stateInitialization/genesisState.js.map +1 -0
  34. package/lib/cmds/beacon/stateInitialization/prepareStateInitialization.d.ts +4 -0
  35. package/lib/cmds/beacon/stateInitialization/prepareStateInitialization.d.ts.map +1 -0
  36. package/lib/cmds/beacon/stateInitialization/prepareStateInitialization.js +67 -0
  37. package/lib/cmds/beacon/stateInitialization/prepareStateInitialization.js.map +1 -0
  38. package/lib/cmds/beacon/stateInitialization/types.d.ts +42 -0
  39. package/lib/cmds/beacon/stateInitialization/types.d.ts.map +1 -0
  40. package/lib/cmds/beacon/stateInitialization/types.js +2 -0
  41. package/lib/cmds/beacon/stateInitialization/types.js.map +1 -0
  42. package/lib/cmds/beacon/stateInitialization/validation.d.ts +4 -0
  43. package/lib/cmds/beacon/stateInitialization/validation.d.ts.map +1 -0
  44. package/lib/cmds/beacon/stateInitialization/validation.js +12 -0
  45. package/lib/cmds/beacon/stateInitialization/validation.js.map +1 -0
  46. package/lib/cmds/dev/handler.d.ts.map +1 -1
  47. package/lib/cmds/dev/handler.js +3 -1
  48. package/lib/cmds/dev/handler.js.map +1 -1
  49. package/lib/cmds/dev/options.d.ts +2 -1
  50. package/lib/cmds/dev/options.d.ts.map +1 -1
  51. package/lib/networks/index.d.ts +3 -9
  52. package/lib/networks/index.d.ts.map +1 -1
  53. package/lib/networks/index.js +15 -28
  54. package/lib/networks/index.js.map +1 -1
  55. package/lib/options/beaconNodeOptions/chain.d.ts +2 -1
  56. package/lib/options/beaconNodeOptions/chain.d.ts.map +1 -1
  57. package/lib/options/beaconNodeOptions/chain.js +12 -5
  58. package/lib/options/beaconNodeOptions/chain.js.map +1 -1
  59. package/lib/options/beaconNodeOptions/index.d.ts +2 -1
  60. package/lib/options/beaconNodeOptions/index.d.ts.map +1 -1
  61. package/package.json +15 -15
  62. package/src/cmds/beacon/handler.ts +19 -22
  63. package/src/cmds/beacon/initBeaconState.ts +136 -387
  64. package/src/cmds/beacon/options.ts +8 -7
  65. package/src/cmds/beacon/paths.ts +5 -0
  66. package/src/cmds/beacon/pubkeysFile.ts +50 -0
  67. package/src/cmds/beacon/stateInitialization/checkpointState.ts +272 -0
  68. package/src/cmds/beacon/stateInitialization/errors.ts +15 -0
  69. package/src/cmds/beacon/stateInitialization/genesisState.ts +99 -0
  70. package/src/cmds/beacon/stateInitialization/prepareStateInitialization.ts +79 -0
  71. package/src/cmds/beacon/stateInitialization/types.ts +60 -0
  72. package/src/cmds/beacon/stateInitialization/validation.ts +16 -0
  73. package/src/cmds/dev/handler.ts +3 -1
  74. package/src/networks/index.ts +25 -40
  75. package/src/options/beaconNodeOptions/chain.ts +15 -6
@@ -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
+ }
@@ -0,0 +1,272 @@
1
+ import {DbCPStateDatastore, FileCPStateDatastore, persistAnchorState} from "@lodestar/beacon-node";
2
+ import {createBeaconConfig} from "@lodestar/config";
3
+ import {
4
+ IBeaconStateView,
5
+ computeEpochAtSlot,
6
+ computeWeakSubjectivitySummaryFromStateBytes,
7
+ getCurrentSlot,
8
+ isWithinWeakSubjectivityPeriodFromSummary,
9
+ readBeaconStateBytesMetadata,
10
+ } from "@lodestar/state-transition";
11
+ import {ssz} from "@lodestar/types";
12
+ import {Checkpoint} from "@lodestar/types/phase0";
13
+ import {formatBytes, toRootHex} from "@lodestar/utils";
14
+ import {fetchWeakSubjectivityStateBytes, getCheckpointFromArg} from "../../../networks/index.js";
15
+ import {downloadOrLoadFile} from "../../../util/index.js";
16
+ import {StateInitializationError, StateInitializationErrorCode} from "./errors.js";
17
+ import {
18
+ ArchivedStateBytes,
19
+ CheckpointStartupPolicy,
20
+ CheckpointStateBytesCandidate,
21
+ StateInitialization,
22
+ StateInitializationOptions,
23
+ StatePreparationContext,
24
+ } from "./types.js";
25
+ import {assertAnchorStateForkMatchesConfig} from "./validation.js";
26
+
27
+ type CheckpointOptions = Pick<
28
+ StateInitializationOptions,
29
+ "wssCheckpoint" | "forceCheckpointSync" | "ignoreWeakSubjectivityCheck"
30
+ >;
31
+
32
+ export async function prepareCheckpointFileInitialization(
33
+ checkpointState: string,
34
+ options: CheckpointOptions,
35
+ archived: ArchivedStateBytes | null,
36
+ context: StatePreparationContext
37
+ ): Promise<StateInitialization> {
38
+ context.logger.info("Loading checkpoint state", {checkpointState});
39
+ const bytes = await downloadOrLoadFile(checkpointState);
40
+ const stateBytes = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
41
+ context.logger.info("Loaded checkpoint state", {checkpointState, size: formatBytes(stateBytes.length)});
42
+ return prepareCheckpointInitialization(
43
+ {
44
+ stateBytes,
45
+ expectedCheckpoint: options.wssCheckpoint ? getCheckpointFromArg(options.wssCheckpoint) : null,
46
+ source: "checkpoint file",
47
+ },
48
+ archived,
49
+ {
50
+ isFinalized: true,
51
+ forceCheckpointSync: Boolean(options.forceCheckpointSync),
52
+ ignoreWeakSubjectivityCheck: Boolean(options.ignoreWeakSubjectivityCheck),
53
+ },
54
+ context
55
+ );
56
+ }
57
+
58
+ export async function prepareCheckpointApiInitialization(
59
+ checkpointSyncUrl: string,
60
+ options: CheckpointOptions,
61
+ archived: ArchivedStateBytes | null,
62
+ context: StatePreparationContext
63
+ ): Promise<StateInitialization> {
64
+ const {chainForkConfig, logger} = context;
65
+ try {
66
+ const url = new URL(checkpointSyncUrl);
67
+ logger.info("Fetching checkpoint state", {checkpointSyncUrl: url.origin});
68
+ } catch (error) {
69
+ logger.error("Invalid checkpoint sync URL", {checkpointSyncUrl}, error as Error);
70
+ throw error;
71
+ }
72
+ const {stateBytes, expectedCheckpoint} = await fetchWeakSubjectivityStateBytes(chainForkConfig, logger, {
73
+ checkpointSyncUrl,
74
+ wssCheckpoint: options.wssCheckpoint,
75
+ });
76
+ return prepareCheckpointInitialization(
77
+ {stateBytes, expectedCheckpoint, source: "checkpointSyncUrl"},
78
+ archived,
79
+ {
80
+ isFinalized: true,
81
+ forceCheckpointSync: Boolean(options.forceCheckpointSync),
82
+ ignoreWeakSubjectivityCheck: Boolean(options.ignoreWeakSubjectivityCheck),
83
+ },
84
+ context
85
+ );
86
+ }
87
+
88
+ export async function prepareUnfinalizedCheckpointInitialization(
89
+ options: CheckpointOptions &
90
+ Pick<
91
+ StateInitializationOptions,
92
+ "lastPersistedCheckpointState" | "unsafeCheckpointState" | "chain.nHistoricalStatesFileDataStore"
93
+ >,
94
+ archived: ArchivedStateBytes | null,
95
+ context: StatePreparationContext
96
+ ): Promise<StateInitialization | null> {
97
+ const {db, dataDir, logger} = context;
98
+ let bytes: Uint8Array | null = null;
99
+ if (options.lastPersistedCheckpointState && !options.forceCheckpointSync) {
100
+ const store = options["chain.nHistoricalStatesFileDataStore"]
101
+ ? new FileCPStateDatastore(dataDir)
102
+ : new DbCPStateDatastore(db);
103
+ logger.verbose(`Finding last persisted checkpoint state from ${store.constructor.name}`);
104
+ bytes = await store.readLatestSafe();
105
+ if (bytes === null) {
106
+ logger.warn("Last persisted checkpoint state not found");
107
+ } else {
108
+ logger.info("Found last persisted checkpoint state", {size: formatBytes(bytes.length)});
109
+ }
110
+ }
111
+ if (bytes === null && options.unsafeCheckpointState) {
112
+ logger.info("Loading checkpoint state", {unsafeCheckpointState: options.unsafeCheckpointState});
113
+ bytes = await downloadOrLoadFile(options.unsafeCheckpointState);
114
+ logger.info("Loaded checkpoint state", {size: formatBytes(bytes.length)});
115
+ }
116
+ if (bytes === null) return null;
117
+ logger.warn(
118
+ "Initializing from unfinalized checkpoint state is unsafe and may cause the node to follow a minority chain"
119
+ );
120
+ const stateBytes = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
121
+ return prepareCheckpointInitialization(
122
+ {
123
+ stateBytes,
124
+ expectedCheckpoint: options.wssCheckpoint ? getCheckpointFromArg(options.wssCheckpoint) : null,
125
+ source: "unfinalized source",
126
+ },
127
+ archived,
128
+ {
129
+ isFinalized: false,
130
+ forceCheckpointSync: Boolean(options.forceCheckpointSync),
131
+ ignoreWeakSubjectivityCheck: Boolean(options.ignoreWeakSubjectivityCheck),
132
+ },
133
+ context
134
+ );
135
+ }
136
+
137
+ function prepareCheckpointInitialization(
138
+ candidate: CheckpointStateBytesCandidate,
139
+ archived: ArchivedStateBytes | null,
140
+ policy: CheckpointStartupPolicy,
141
+ {chainForkConfig, db, logger}: StatePreparationContext
142
+ ): StateInitialization {
143
+ const candidateMetadata = readBeaconStateBytesMetadata(candidate.stateBytes);
144
+ if (candidateMetadata === null) {
145
+ throw new StateInitializationError(
146
+ {code: StateInitializationErrorCode.MALFORMED_STATE_BYTES},
147
+ `Cannot read state metadata from ${candidate.source}`
148
+ );
149
+ }
150
+ if (
151
+ archived !== null &&
152
+ (archived.metadata.genesisTime !== candidateMetadata.genesisTime ||
153
+ !ssz.Root.equals(archived.metadata.genesisValidatorsRoot, candidateMetadata.genesisValidatorsRoot))
154
+ ) {
155
+ throw new StateInitializationError(
156
+ {code: StateInitializationErrorCode.INCOMPATIBLE_GENESIS},
157
+ "Db state and checkpoint state are not compatible, either clear the db or verify your checkpoint source"
158
+ );
159
+ }
160
+ const useArchived =
161
+ archived !== null && archived.metadata.slot > candidateMetadata.slot && !policy.forceCheckpointSync;
162
+ if (useArchived) {
163
+ logger.verbose(
164
+ "Db state is ahead of the provided checkpoint state, using the db state to initialize the beacon chain"
165
+ );
166
+ }
167
+ const {stateBytes, metadata, weakSubjectivity} = useArchived
168
+ ? archived
169
+ : {
170
+ stateBytes: candidate.stateBytes,
171
+ metadata: candidateMetadata,
172
+ weakSubjectivity: computeWeakSubjectivitySummaryFromStateBytes(
173
+ chainForkConfig,
174
+ candidate.stateBytes,
175
+ candidateMetadata
176
+ ),
177
+ };
178
+ if (weakSubjectivity === null) {
179
+ throw new StateInitializationError(
180
+ {code: StateInitializationErrorCode.MALFORMED_STATE_BYTES},
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
+ );
183
+ }
184
+ const expectedCheckpoint = useArchived ? null : candidate.expectedCheckpoint;
185
+ const source = useArchived ? "db" : candidate.source;
186
+ const {isFinalized, ignoreWeakSubjectivityCheck} = policy;
187
+ const config = createBeaconConfig(chainForkConfig, metadata.genesisValidatorsRoot);
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
+ }
199
+ // DB-selected anchors need no persistence, including at slot zero.
200
+ const shouldPersist = isFinalized && !useArchived;
201
+ return {
202
+ stateBytes,
203
+ config,
204
+ isFinalized,
205
+ validate(state) {
206
+ // A supplied checkpoint must match even when the period check is ignored.
207
+ if (expectedCheckpoint !== null) assertStateMatchesCheckpoint(state, expectedCheckpoint);
208
+ assertAnchorStateForkMatchesConfig(config, state);
209
+ if (isFinalized) {
210
+ const source = useArchived ? "db" : "checkpoint";
211
+ const logData = {
212
+ slot: state.slot,
213
+ epoch: computeEpochAtSlot(state.slot),
214
+ stateSize: formatBytes(stateBytes.length),
215
+ stateRoot: toRootHex(state.hashTreeRoot()),
216
+ isWithinWeakSubjectivityPeriod: passedValidation,
217
+ };
218
+ if (passedValidation) {
219
+ logger.info(`Initializing beacon from a valid ${source} state`, logData);
220
+ } else {
221
+ logger.warn(`Initializing from a stale ${source} state vulnerable to long range attacks`, logData);
222
+ logger.warn("Checkpoint sync recommended, please use --help to see checkpoint sync options");
223
+ }
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
+ },
240
+ persist: shouldPersist ? (state, bytes) => persistAnchorState(config, db, state, bytes) : null,
241
+ log(state, nativeStateTransition) {
242
+ const {checkpoint} = state.computeAnchorCheckpoint();
243
+ logger.info("Initialized checkpoint state", {
244
+ source,
245
+ slot: state.slot,
246
+ epoch: checkpoint.epoch,
247
+ stateSize: formatBytes(stateBytes.length),
248
+ stateRoot: toRootHex(state.hashTreeRoot()),
249
+ checkpointRoot: toRootHex(checkpoint.root),
250
+ isFinalized,
251
+ nativeStateTransition,
252
+ ...(isFinalized ? {} : {lastProcessedSlot: state.latestBlockHeader.slot}),
253
+ });
254
+ },
255
+ };
256
+ }
257
+
258
+ function assertStateMatchesCheckpoint(state: IBeaconStateView, checkpoint: Checkpoint): void {
259
+ const {root: blockRoot, epoch: stateEpoch} = state.computeAnchorCheckpoint().checkpoint;
260
+ if (!ssz.Root.equals(blockRoot, checkpoint.root)) {
261
+ throw new StateInitializationError(
262
+ {code: StateInitializationErrorCode.CHECKPOINT_ROOT_MISMATCH},
263
+ `Roots do not match. expected=${toRootHex(checkpoint.root)}, actual=${toRootHex(blockRoot)}`
264
+ );
265
+ }
266
+ if (!ssz.Epoch.equals(stateEpoch, checkpoint.epoch)) {
267
+ throw new StateInitializationError(
268
+ {code: StateInitializationErrorCode.CHECKPOINT_EPOCH_MISMATCH},
269
+ `Epochs do not match. expected=${checkpoint.epoch}, actual=${stateEpoch}`
270
+ );
271
+ }
272
+ }
@@ -0,0 +1,15 @@
1
+ import {LodestarError} from "@lodestar/utils";
2
+
3
+ export enum StateInitializationErrorCode {
4
+ INVALID_CHECKPOINT_SOURCE = "INVALID_CHECKPOINT_SOURCE",
5
+ MALFORMED_STATE_BYTES = "MALFORMED_STATE_BYTES",
6
+ INCOMPATIBLE_GENESIS = "INCOMPATIBLE_GENESIS",
7
+ CHECKPOINT_ROOT_MISMATCH = "CHECKPOINT_ROOT_MISMATCH",
8
+ CHECKPOINT_EPOCH_MISMATCH = "CHECKPOINT_EPOCH_MISMATCH",
9
+ STALE_CHECKPOINT = "STALE_CHECKPOINT",
10
+ ANCHOR_STATE_FORK_MISMATCH = "ANCHOR_STATE_FORK_MISMATCH",
11
+ GENESIS_ROOT_MISMATCH = "GENESIS_ROOT_MISMATCH",
12
+ MISSING_INITIALIZATION_SOURCE = "MISSING_INITIALIZATION_SOURCE",
13
+ }
14
+
15
+ export class StateInitializationError extends LodestarError<{code: StateInitializationErrorCode}> {}
@@ -0,0 +1,99 @@
1
+ import {persistAnchorState} from "@lodestar/beacon-node";
2
+ import {createBeaconConfig} from "@lodestar/config";
3
+ import {
4
+ computeEpochAtSlot,
5
+ computeWeakSubjectivitySummaryFromStateBytes,
6
+ isWithinWeakSubjectivityPeriodFromSummary,
7
+ readBeaconStateBytesMetadata,
8
+ } from "@lodestar/state-transition";
9
+ import {formatBytes, toRootHex} from "@lodestar/utils";
10
+ import {getGenesisFileUrl, getGenesisStateRoot} from "../../../networks/index.js";
11
+ import {defaultNetwork} from "../../../options/globalOptions.js";
12
+ import {downloadOrLoadFile} from "../../../util/index.js";
13
+ import {StateInitializationError, StateInitializationErrorCode} from "./errors.js";
14
+ import {StateInitialization, StateInitializationOptions, StatePreparationContext} from "./types.js";
15
+ import {assertAnchorStateForkMatchesConfig} from "./validation.js";
16
+
17
+ export async function prepareGenesisInitialization(
18
+ options: Pick<StateInitializationOptions, "genesisStateFile" | "network">,
19
+ {chainForkConfig, db, logger}: StatePreparationContext
20
+ ): Promise<StateInitialization> {
21
+ const source = options.genesisStateFile || getGenesisFileUrl(options.network || defaultNetwork);
22
+ if (!source) {
23
+ throw new StateInitializationError(
24
+ {code: StateInitializationErrorCode.MISSING_INITIALIZATION_SOURCE},
25
+ "Failed to initialize beacon state, please provide a genesis state file or use checkpoint sync"
26
+ );
27
+ }
28
+ logger.info("Loading genesis state", {genesisStateFile: source});
29
+ const bytes = await downloadOrLoadFile(source);
30
+ const stateBytes = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
31
+ logger.info("Loaded genesis state", {size: formatBytes(stateBytes.length)});
32
+ const metadata = readBeaconStateBytesMetadata(stateBytes);
33
+ if (metadata === null) {
34
+ throw new StateInitializationError(
35
+ {code: StateInitializationErrorCode.MALFORMED_STATE_BYTES},
36
+ "Cannot read metadata from genesis state bytes"
37
+ );
38
+ }
39
+ const weakSubjectivity = computeWeakSubjectivitySummaryFromStateBytes(chainForkConfig, stateBytes, metadata);
40
+ if (weakSubjectivity === null) {
41
+ throw new StateInitializationError(
42
+ {code: StateInitializationErrorCode.MALFORMED_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
+ );
45
+ }
46
+ const config = createBeaconConfig(chainForkConfig, metadata.genesisValidatorsRoot);
47
+ const expectedRoot = getGenesisStateRoot(options.network);
48
+ return {
49
+ stateBytes,
50
+ config,
51
+ isFinalized: true,
52
+ validate(state) {
53
+ const stateRoot = toRootHex(state.hashTreeRoot());
54
+ if (expectedRoot !== null && stateRoot !== expectedRoot) {
55
+ throw new StateInitializationError(
56
+ {code: StateInitializationErrorCode.GENESIS_ROOT_MISMATCH},
57
+ `Genesis state root mismatch expected=${expectedRoot} received=${stateRoot}`
58
+ );
59
+ }
60
+ assertAnchorStateForkMatchesConfig(config, state);
61
+ const passedValidation = isWithinWeakSubjectivityPeriodFromSummary(config, weakSubjectivity);
62
+ const logData = {
63
+ slot: state.slot,
64
+ epoch: computeEpochAtSlot(state.slot),
65
+ stateSize: formatBytes(stateBytes.length),
66
+ stateRoot,
67
+ isWithinWeakSubjectivityPeriod: passedValidation,
68
+ };
69
+ if (passedValidation) {
70
+ logger.info("Initializing beacon from a valid checkpoint state", logData);
71
+ } else {
72
+ logger.warn("Initializing from a stale checkpoint state vulnerable to long range attacks", logData);
73
+ logger.warn("Checkpoint sync recommended, please use --help to see checkpoint sync options");
74
+ }
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
+ },
89
+ persist: (state, bytes) => persistAnchorState(config, db, state, bytes),
90
+ log(state, nativeStateTransition) {
91
+ logger.info("Initialized genesis state", {
92
+ slot: state.slot,
93
+ stateRoot: toRootHex(state.hashTreeRoot()),
94
+ isFinalized: true,
95
+ nativeStateTransition,
96
+ });
97
+ },
98
+ };
99
+ }
@@ -0,0 +1,79 @@
1
+ import {createBeaconConfig} from "@lodestar/config";
2
+ import {computeEpochAtSlot} from "@lodestar/state-transition";
3
+ import {formatBytes, toRootHex} from "@lodestar/utils";
4
+ import {
5
+ prepareCheckpointApiInitialization,
6
+ prepareCheckpointFileInitialization,
7
+ prepareUnfinalizedCheckpointInitialization,
8
+ } from "./checkpointState.js";
9
+ import {ArchivedStateBytes, StateInitialization, StateInitializationOptions, StatePreparationContext} from "./types.js";
10
+ import {assertAnchorStateForkMatchesConfig} from "./validation.js";
11
+
12
+ export function prepareArchivedStateInitialization(
13
+ {stateBytes, metadata, isWithinWeakSubjectivityPeriod}: ArchivedStateBytes,
14
+ {chainForkConfig, db, logger}: StatePreparationContext
15
+ ): StateInitialization {
16
+ const config = createBeaconConfig(chainForkConfig, metadata.genesisValidatorsRoot);
17
+ return {
18
+ stateBytes,
19
+ config,
20
+ isFinalized: true,
21
+ validate(state) {
22
+ assertAnchorStateForkMatchesConfig(config, state);
23
+ const logData = {
24
+ slot: state.slot,
25
+ epoch: computeEpochAtSlot(state.slot),
26
+ stateSize: formatBytes(stateBytes.length),
27
+ stateRoot: toRootHex(state.hashTreeRoot()),
28
+ isWithinWeakSubjectivityPeriod,
29
+ };
30
+ if (isWithinWeakSubjectivityPeriod) {
31
+ logger.info("Initializing beacon from a valid db state", logData);
32
+ } else {
33
+ logger.warn("Initializing from a stale db state vulnerable to long range attacks", logData);
34
+ logger.warn("Checkpoint sync recommended, please use --help to see checkpoint sync options");
35
+ }
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
+ },
51
+ persist: null,
52
+ log(state, nativeStateTransition) {
53
+ logger.info("Initialized state from db", {
54
+ slot: state.slot,
55
+ epoch: computeEpochAtSlot(state.slot),
56
+ stateRoot: toRootHex(state.hashTreeRoot()),
57
+ isFinalized: true,
58
+ nativeStateTransition,
59
+ });
60
+ },
61
+ };
62
+ }
63
+
64
+ export async function prepareCheckpointSourceInitialization(
65
+ options: StateInitializationOptions,
66
+ archived: ArchivedStateBytes | null,
67
+ context: StatePreparationContext
68
+ ): Promise<StateInitialization | null> {
69
+ if (options.checkpointState) {
70
+ return prepareCheckpointFileInitialization(options.checkpointState, options, archived, context);
71
+ }
72
+ if (options.checkpointSyncUrl) {
73
+ return prepareCheckpointApiInitialization(options.checkpointSyncUrl, options, archived, context);
74
+ }
75
+ if (options.lastPersistedCheckpointState || options.unsafeCheckpointState) {
76
+ return prepareUnfinalizedCheckpointInitialization(options, archived, context);
77
+ }
78
+ return null;
79
+ }
@@ -0,0 +1,60 @@
1
+ import {IBeaconDb} from "@lodestar/beacon-node";
2
+ import {BeaconConfig, ChainForkConfig} from "@lodestar/config";
3
+ import {IBeaconStateView, StateBytesMetadata, WeakSubjectivitySummary} from "@lodestar/state-transition";
4
+ import {Slot} from "@lodestar/types";
5
+ import {Checkpoint} from "@lodestar/types/phase0";
6
+ import {Logger} from "@lodestar/utils";
7
+ import {GlobalArgs} from "../../../options/globalOptions.js";
8
+ import {BeaconArgs} from "../options.js";
9
+
10
+ export type StateInitializationOptions = Pick<
11
+ BeaconArgs & GlobalArgs,
12
+ | "checkpointState"
13
+ | "checkpointSyncUrl"
14
+ | "unsafeCheckpointState"
15
+ | "lastPersistedCheckpointState"
16
+ | "genesisStateFile"
17
+ | "wssCheckpoint"
18
+ | "forceCheckpointSync"
19
+ | "ignoreWeakSubjectivityCheck"
20
+ | "network"
21
+ | "chain.nHistoricalStatesFileDataStore"
22
+ | "chain.nativeStateTransition"
23
+ >;
24
+
25
+ export type StatePreparationContext = {
26
+ chainForkConfig: ChainForkConfig;
27
+ db: IBeaconDb;
28
+ dataDir: string;
29
+ logger: Logger;
30
+ };
31
+
32
+ export type ArchivedStateBytes = {
33
+ stateBytes: Uint8Array;
34
+ metadata: StateBytesMetadata;
35
+ weakSubjectivity: WeakSubjectivitySummary;
36
+ isWithinWeakSubjectivityPeriod: boolean;
37
+ };
38
+
39
+ export type CheckpointStateBytesCandidate = {
40
+ stateBytes: Uint8Array;
41
+ expectedCheckpoint: Checkpoint | null;
42
+ source: "checkpoint file" | "checkpointSyncUrl" | "unfinalized source";
43
+ };
44
+
45
+ export type CheckpointStartupPolicy = {
46
+ isFinalized: boolean;
47
+ forceCheckpointSync: boolean;
48
+ ignoreWeakSubjectivityCheck: boolean;
49
+ };
50
+
51
+ /** Selected state bytes and source-specific initialization steps. */
52
+ export type StateInitialization = {
53
+ stateBytes: Uint8Array;
54
+ config: BeaconConfig;
55
+ isFinalized: boolean;
56
+ validate: (state: IBeaconStateView) => void;
57
+ initializeEarliestAvailableSlot: (state: IBeaconStateView) => Promise<Slot>;
58
+ persist: ((state: IBeaconStateView, stateBytes: Uint8Array) => Promise<void>) | null;
59
+ log: (state: IBeaconStateView, nativeStateTransition: boolean) => void;
60
+ };
@@ -0,0 +1,16 @@
1
+ import {ChainForkConfig} from "@lodestar/config";
2
+ import {IBeaconStateView, computeStartSlotAtEpoch} from "@lodestar/state-transition";
3
+ import {toHex} from "@lodestar/utils";
4
+ import {StateInitializationError, StateInitializationErrorCode} from "./errors.js";
5
+
6
+ export function assertAnchorStateForkMatchesConfig(config: ChainForkConfig, anchorState: IBeaconStateView): void {
7
+ const expectedFork = config.getForkInfo(computeStartSlotAtEpoch(anchorState.fork.epoch));
8
+ const expectedForkVersion = toHex(expectedFork.version);
9
+ const stateFork = toHex(anchorState.fork.currentVersion);
10
+ if (stateFork !== expectedForkVersion) {
11
+ throw new StateInitializationError(
12
+ {code: StateInitializationErrorCode.ANCHOR_STATE_FORK_MISMATCH},
13
+ `State current fork version ${stateFork} not equal to current config ${expectedForkVersion}. Maybe caused by importing a state from a different network`
14
+ );
15
+ }
16
+ }