@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.
- package/lib/api/impl/lodestar/index.d.ts +5 -0
- package/lib/api/impl/lodestar/index.d.ts.map +1 -1
- package/lib/api/impl/lodestar/index.js +35 -10
- package/lib/api/impl/lodestar/index.js.map +1 -1
- package/lib/api/impl/node/utils.js +1 -1
- package/lib/api/impl/node/utils.js.map +1 -1
- package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.d.ts.map +1 -1
- package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.js +6 -1
- package/lib/chain/archiveStore/strategies/frequencyStateArchiveStrategy.js.map +1 -1
- package/lib/chain/chain.d.ts +5 -2
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +27 -12
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/forkChoice/index.d.ts +9 -1
- package/lib/chain/forkChoice/index.d.ts.map +1 -1
- package/lib/chain/forkChoice/index.js +109 -4
- package/lib/chain/forkChoice/index.js.map +1 -1
- package/lib/chain/interface.d.ts +2 -0
- package/lib/chain/interface.d.ts.map +1 -1
- package/lib/chain/stateCache/datastore/db.d.ts +12 -0
- package/lib/chain/stateCache/datastore/db.d.ts.map +1 -1
- package/lib/chain/stateCache/datastore/db.js +70 -0
- package/lib/chain/stateCache/datastore/db.js.map +1 -1
- package/lib/chain/stateCache/datastore/file.d.ts +1 -0
- package/lib/chain/stateCache/datastore/file.d.ts.map +1 -1
- package/lib/chain/stateCache/datastore/file.js +7 -0
- package/lib/chain/stateCache/datastore/file.js.map +1 -1
- package/lib/chain/stateCache/datastore/types.d.ts +1 -0
- package/lib/chain/stateCache/datastore/types.d.ts.map +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/metrics/metrics/lodestar.js +1 -1
- package/lib/metrics/metrics/lodestar.js.map +1 -1
- package/lib/network/core/networkCoreWorker.js +2 -7
- package/lib/network/core/networkCoreWorker.js.map +1 -1
- package/lib/network/discv5/worker.js +2 -7
- package/lib/network/discv5/worker.js.map +1 -1
- package/lib/network/gossip/encoding.js +1 -1
- package/lib/network/gossip/encoding.js.map +1 -1
- package/lib/network/gossip/snappy_bun.d.ts +3 -0
- package/lib/network/gossip/snappy_bun.d.ts.map +1 -0
- package/lib/network/gossip/snappy_bun.js +3 -0
- package/lib/network/gossip/snappy_bun.js.map +1 -0
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +6 -2
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/network/reqresp/handlers/beaconBlocksByRange.d.ts.map +1 -1
- package/lib/network/reqresp/handlers/beaconBlocksByRange.js +3 -1
- package/lib/network/reqresp/handlers/beaconBlocksByRange.js.map +1 -1
- package/lib/node/nodejs.d.ts +2 -1
- package/lib/node/nodejs.d.ts.map +1 -1
- package/lib/node/nodejs.js +2 -1
- package/lib/node/nodejs.js.map +1 -1
- package/lib/sync/range/range.d.ts.map +1 -1
- package/lib/sync/range/range.js +2 -1
- package/lib/sync/range/range.js.map +1 -1
- package/lib/sync/utils/remoteSyncType.d.ts +2 -1
- package/lib/sync/utils/remoteSyncType.d.ts.map +1 -1
- package/lib/sync/utils/remoteSyncType.js +19 -4
- package/lib/sync/utils/remoteSyncType.js.map +1 -1
- package/lib/util/profile.d.ts +6 -4
- package/lib/util/profile.d.ts.map +1 -1
- package/lib/util/profile.js +40 -3
- package/lib/util/profile.js.map +1 -1
- package/lib/util/sszBytes.d.ts +2 -0
- package/lib/util/sszBytes.d.ts.map +1 -1
- package/lib/util/sszBytes.js +25 -0
- package/lib/util/sszBytes.js.map +1 -1
- package/package.json +30 -24
- package/src/api/impl/lodestar/index.ts +42 -10
- package/src/api/impl/node/utils.ts +1 -1
- package/src/chain/archiveStore/strategies/frequencyStateArchiveStrategy.ts +5 -1
- package/src/chain/chain.ts +43 -18
- package/src/chain/forkChoice/index.ts +178 -2
- package/src/chain/interface.ts +2 -0
- package/src/chain/stateCache/datastore/db.ts +89 -1
- package/src/chain/stateCache/datastore/file.ts +8 -0
- package/src/chain/stateCache/datastore/types.ts +1 -0
- package/src/index.ts +2 -0
- package/src/metrics/metrics/lodestar.ts +1 -1
- package/src/network/core/networkCoreWorker.ts +2 -7
- package/src/network/discv5/worker.ts +2 -7
- package/src/network/gossip/encoding.ts +1 -1
- package/src/network/gossip/snappy_bun.ts +2 -0
- package/src/network/processor/gossipHandlers.ts +6 -1
- package/src/network/reqresp/handlers/beaconBlocksByRange.ts +3 -1
- package/src/node/nodejs.ts +3 -0
- package/src/sync/range/range.ts +2 -1
- package/src/sync/utils/remoteSyncType.ts +23 -4
- package/src/util/profile.ts +45 -3
- 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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/chain/chain.ts
CHANGED
|
@@ -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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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, //
|
|
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
|
package/src/chain/interface.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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);
|