Skip to content

Commit 6c1e335

Browse files
authored
fix: single state tree at start up (#7056)
* feat: use db state to load ws state * feat: log state size * fix: rename initStateFromAnchorState to checkAndPersistAnchorState * fix: only persist anchor state if it's cp state * fix: avoid redundant anchor state serialization
1 parent cbc00c7 commit 6c1e335

File tree

10 files changed

+222
-50
lines changed

10 files changed

+222
-50
lines changed

packages/beacon-node/src/chain/initState.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,18 @@ export async function persistGenesisResult(
3737
export async function persistAnchorState(
3838
config: ChainForkConfig,
3939
db: IBeaconDb,
40-
anchorState: BeaconStateAllForks
40+
anchorState: BeaconStateAllForks,
41+
anchorStateBytes: Uint8Array
4142
): Promise<void> {
4243
if (anchorState.slot === GENESIS_SLOT) {
4344
const genesisBlock = createGenesisBlock(config, anchorState);
4445
await Promise.all([
4546
db.blockArchive.add(genesisBlock),
4647
db.block.add(genesisBlock),
47-
db.stateArchive.add(anchorState),
48+
db.stateArchive.putBinary(anchorState.slot, anchorStateBytes),
4849
]);
4950
} else {
50-
await db.stateArchive.add(anchorState);
51+
await db.stateArchive.putBinary(anchorState.slot, anchorStateBytes);
5152
}
5253
}
5354

@@ -154,16 +155,17 @@ export async function initStateFromDb(
154155
/**
155156
* Initialize and persist an anchor state (either weak subjectivity or genesis)
156157
*/
157-
export async function initStateFromAnchorState(
158+
export async function checkAndPersistAnchorState(
158159
config: ChainForkConfig,
159160
db: IBeaconDb,
160161
logger: Logger,
161162
anchorState: BeaconStateAllForks,
163+
anchorStateBytes: Uint8Array,
162164
{
163165
isWithinWeakSubjectivityPeriod,
164166
isCheckpointState,
165167
}: {isWithinWeakSubjectivityPeriod: boolean; isCheckpointState: boolean}
166-
): Promise<BeaconStateAllForks> {
168+
): Promise<void> {
167169
const expectedFork = config.getForkInfo(computeStartSlotAtEpoch(anchorState.fork.epoch));
168170
const expectedForkVersion = toHex(expectedFork.version);
169171
const stateFork = toHex(anchorState.fork.currentVersion);
@@ -191,9 +193,9 @@ export async function initStateFromAnchorState(
191193
logger.warn("Checkpoint sync recommended, please use --help to see checkpoint sync options");
192194
}
193195

194-
await persistAnchorState(config, db, anchorState);
195-
196-
return anchorState;
196+
if (isCheckpointState || anchorState.slot === GENESIS_SLOT) {
197+
await persistAnchorState(config, db, anchorState, anchorStateBytes);
198+
}
197199
}
198200

199201
export function initBeaconMetrics(metrics: Metrics, state: BeaconStateAllForks): void {

packages/beacon-node/src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export {initStateFromAnchorState, initStateFromDb, initStateFromEth1} from "./chain/index.js";
1+
export {checkAndPersistAnchorState, initStateFromDb, initStateFromEth1} from "./chain/index.js";
22
export {BeaconDb, type IBeaconDb} from "./db/index.js";
33
export {Eth1Provider, type IEth1Provider} from "./eth1/index.js";
44
export {createNodeJsLibp2p, type NodeJsLibp2pOpts} from "./network/index.js";
@@ -20,4 +20,4 @@ export {RestApiServer} from "./api/rest/base.js";
2020
export type {RestApiServerOpts, RestApiServerModules, RestApiServerMetrics} from "./api/rest/base.js";
2121

2222
// Export type util for CLI - TEMP move to lodestar-types eventually
23-
export {getStateTypeFromBytes} from "./util/multifork.js";
23+
export {getStateTypeFromBytes, getStateSlotFromBytes} from "./util/multifork.js";

packages/cli/src/cmds/beacon/initBeaconState.ts

+71-30
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import {ssz} from "@lodestar/types";
22
import {createBeaconConfig, BeaconConfig, ChainForkConfig} from "@lodestar/config";
3-
import {Logger} from "@lodestar/utils";
3+
import {Logger, formatBytes} from "@lodestar/utils";
44
import {
55
isWithinWeakSubjectivityPeriod,
66
ensureWithinWeakSubjectivityPeriod,
77
BeaconStateAllForks,
8+
loadState,
9+
loadStateAndValidators,
810
} from "@lodestar/state-transition";
911
import {
1012
IBeaconDb,
1113
IBeaconNodeOptions,
12-
initStateFromAnchorState,
14+
checkAndPersistAnchorState,
1315
initStateFromEth1,
1416
getStateTypeFromBytes,
1517
} from "@lodestar/beacon-node";
@@ -25,32 +27,36 @@ import {
2527
} from "../../networks/index.js";
2628
import {BeaconArgs} from "./options.js";
2729

30+
type StateWithBytes = {state: BeaconStateAllForks; stateBytes: Uint8Array};
31+
2832
async function initAndVerifyWeakSubjectivityState(
2933
config: BeaconConfig,
3034
db: IBeaconDb,
3135
logger: Logger,
32-
store: BeaconStateAllForks,
33-
wsState: BeaconStateAllForks,
36+
dbStateBytes: StateWithBytes,
37+
wsStateBytes: StateWithBytes,
3438
wsCheckpoint: Checkpoint,
3539
opts: {ignoreWeakSubjectivityCheck?: boolean} = {}
3640
): Promise<{anchorState: BeaconStateAllForks; wsCheckpoint: Checkpoint}> {
41+
const dbState = dbStateBytes.state;
42+
const wsState = wsStateBytes.state;
3743
// Check if the store's state and wsState are compatible
3844
if (
39-
store.genesisTime !== wsState.genesisTime ||
40-
!ssz.Root.equals(store.genesisValidatorsRoot, wsState.genesisValidatorsRoot)
45+
dbState.genesisTime !== wsState.genesisTime ||
46+
!ssz.Root.equals(dbState.genesisValidatorsRoot, wsState.genesisValidatorsRoot)
4147
) {
4248
throw new Error(
4349
"Db state and checkpoint state are not compatible, either clear the db or verify your checkpoint source"
4450
);
4551
}
4652

4753
// Pick the state which is ahead as an anchor to initialize the beacon chain
48-
let anchorState = wsState;
54+
let anchorState = wsStateBytes;
4955
let anchorCheckpoint = wsCheckpoint;
5056
let isCheckpointState = true;
51-
if (store.slot > wsState.slot) {
52-
anchorState = store;
53-
anchorCheckpoint = getCheckpointFromState(store);
57+
if (dbState.slot > wsState.slot) {
58+
anchorState = dbStateBytes;
59+
anchorCheckpoint = getCheckpointFromState(dbState);
5460
isCheckpointState = false;
5561
logger.verbose(
5662
"Db state is ahead of the provided checkpoint state, using the db state to initialize the beacon chain"
@@ -59,19 +65,19 @@ async function initAndVerifyWeakSubjectivityState(
5965

6066
// Throw error unless user explicitly asked not to, in testnets can happen that wss period is too small
6167
// that even some epochs of non finalization can cause finalized checkpoint to be out of valid range
62-
const wssCheck = wrapFnError(() => ensureWithinWeakSubjectivityPeriod(config, anchorState, anchorCheckpoint));
68+
const wssCheck = wrapFnError(() => ensureWithinWeakSubjectivityPeriod(config, anchorState.state, anchorCheckpoint));
6369
const isWithinWeakSubjectivityPeriod = wssCheck.err === null;
6470
if (!isWithinWeakSubjectivityPeriod && !opts.ignoreWeakSubjectivityCheck) {
6571
throw wssCheck.err;
6672
}
6773

68-
anchorState = await initStateFromAnchorState(config, db, logger, anchorState, {
74+
await checkAndPersistAnchorState(config, db, logger, anchorState.state, anchorState.stateBytes, {
6975
isWithinWeakSubjectivityPeriod,
7076
isCheckpointState,
7177
});
7278

7379
// Return the latest anchorState but still return original wsCheckpoint to validate in backfill
74-
return {anchorState, wsCheckpoint};
80+
return {anchorState: anchorState.state, wsCheckpoint};
7581
}
7682

7783
/**
@@ -96,8 +102,20 @@ export async function initBeaconState(
96102
}
97103
// fetch the latest state stored in the db which will be used in all cases, if it exists, either
98104
// i) used directly as the anchor state
99-
// ii) used during verification of a weak subjectivity state,
100-
const lastDbState = await db.stateArchive.lastValue();
105+
// ii) used to load and verify a weak subjectivity state,
106+
const lastDbSlot = await db.stateArchive.lastKey();
107+
const stateBytes = lastDbSlot !== null ? await db.stateArchive.getBinary(lastDbSlot) : null;
108+
let lastDbState: BeaconStateAllForks | null = null;
109+
let lastDbValidatorsBytes: Uint8Array | null = null;
110+
let lastDbStateWithBytes: StateWithBytes | null = null;
111+
if (stateBytes) {
112+
logger.verbose("Found the last archived state", {slot: lastDbSlot, size: formatBytes(stateBytes.length)});
113+
const {state, validatorsBytes} = loadStateAndValidators(chainForkConfig, stateBytes);
114+
lastDbState = state;
115+
lastDbValidatorsBytes = validatorsBytes;
116+
lastDbStateWithBytes = {state, stateBytes: stateBytes};
117+
}
118+
101119
if (lastDbState) {
102120
const config = createBeaconConfig(chainForkConfig, lastDbState.genesisValidatorsRoot);
103121
const wssCheck = isWithinWeakSubjectivityPeriod(config, lastDbState, getCheckpointFromState(lastDbState));
@@ -107,27 +125,34 @@ export async function initBeaconState(
107125
// Forcing to sync from checkpoint is only recommended if node is taking too long to sync from last db state.
108126
// It is important to remind the user to remove this flag again unless it is absolutely necessary.
109127
if (wssCheck) {
110-
logger.warn("Forced syncing from checkpoint even though db state is within weak subjectivity period");
128+
logger.warn(
129+
`Forced syncing from checkpoint even though db state at slot ${lastDbState.slot} is within weak subjectivity period`
130+
);
111131
logger.warn("Please consider removing --forceCheckpointSync flag unless absolutely necessary");
112132
}
113133
} else {
114134
// All cases when we want to directly use lastDbState as the anchor state:
115135
// - if no checkpoint sync args provided, or
116136
// - the lastDbState is within weak subjectivity period:
117137
if ((!args.checkpointState && !args.checkpointSyncUrl) || wssCheck) {
118-
const anchorState = await initStateFromAnchorState(config, db, logger, lastDbState, {
138+
if (stateBytes === null) {
139+
// this never happens
140+
throw Error(`There is no stateBytes for the lastDbState at slot ${lastDbState.slot}`);
141+
}
142+
await checkAndPersistAnchorState(config, db, logger, lastDbState, stateBytes, {
119143
isWithinWeakSubjectivityPeriod: wssCheck,
120144
isCheckpointState: false,
121145
});
122-
return {anchorState};
146+
return {anchorState: lastDbState};
123147
}
124148
}
125149
}
126150

127151
// See if we can sync state using checkpoint sync args or else start from genesis
128152
if (args.checkpointState) {
129153
return readWSState(
130-
lastDbState,
154+
lastDbStateWithBytes,
155+
lastDbValidatorsBytes,
131156
{
132157
checkpointState: args.checkpointState,
133158
wssCheckpoint: args.wssCheckpoint,
@@ -139,7 +164,8 @@ export async function initBeaconState(
139164
);
140165
} else if (args.checkpointSyncUrl) {
141166
return fetchWSStateFromBeaconApi(
142-
lastDbState,
167+
lastDbStateWithBytes,
168+
lastDbValidatorsBytes,
143169
{
144170
checkpointSyncUrl: args.checkpointSyncUrl,
145171
wssCheckpoint: args.wssCheckpoint,
@@ -153,10 +179,10 @@ export async function initBeaconState(
153179
const genesisStateFile = args.genesisStateFile || getGenesisFileUrl(args.network || defaultNetwork);
154180
if (genesisStateFile && !args.forceGenesis) {
155181
const stateBytes = await downloadOrLoadFile(genesisStateFile);
156-
let anchorState = getStateTypeFromBytes(chainForkConfig, stateBytes).deserializeToViewDU(stateBytes);
182+
const anchorState = getStateTypeFromBytes(chainForkConfig, stateBytes).deserializeToViewDU(stateBytes);
157183
const config = createBeaconConfig(chainForkConfig, anchorState.genesisValidatorsRoot);
158184
const wssCheck = isWithinWeakSubjectivityPeriod(config, anchorState, getCheckpointFromState(anchorState));
159-
anchorState = await initStateFromAnchorState(config, db, logger, anchorState, {
185+
await checkAndPersistAnchorState(config, db, logger, anchorState, stateBytes, {
160186
isWithinWeakSubjectivityPeriod: wssCheck,
161187
isCheckpointState: true,
162188
});
@@ -170,7 +196,8 @@ export async function initBeaconState(
170196
}
171197

172198
async function readWSState(
173-
lastDbState: BeaconStateAllForks | null,
199+
lastDbStateBytes: StateWithBytes | null,
200+
lastDbValidatorsBytes: Uint8Array | null,
174201
wssOpts: {checkpointState: string; wssCheckpoint?: string; ignoreWeakSubjectivityCheck?: boolean},
175202
chainForkConfig: ChainForkConfig,
176203
db: IBeaconDb,
@@ -180,19 +207,28 @@ async function readWSState(
180207
// if a weak subjectivity checkpoint has been provided, it is used for additional verification
181208
// otherwise, the state itself is used for verification (not bad, because the trusted state has been explicitly provided)
182209
const {checkpointState, wssCheckpoint, ignoreWeakSubjectivityCheck} = wssOpts;
210+
const lastDbState = lastDbStateBytes?.state ?? null;
183211

184212
const stateBytes = await downloadOrLoadFile(checkpointState);
185-
const wsState = getStateTypeFromBytes(chainForkConfig, stateBytes).deserializeToViewDU(stateBytes);
213+
let wsState: BeaconStateAllForks;
214+
if (lastDbState && lastDbValidatorsBytes) {
215+
// use lastDbState to load wsState if possible to share the same state tree
216+
wsState = loadState(chainForkConfig, lastDbState, stateBytes, lastDbValidatorsBytes).state;
217+
} else {
218+
wsState = getStateTypeFromBytes(chainForkConfig, stateBytes).deserializeToViewDU(stateBytes);
219+
}
186220
const config = createBeaconConfig(chainForkConfig, wsState.genesisValidatorsRoot);
187-
const store = lastDbState ?? wsState;
221+
const wsStateBytes = {state: wsState, stateBytes};
222+
const store = lastDbStateBytes ?? wsStateBytes;
188223
const checkpoint = wssCheckpoint ? getCheckpointFromArg(wssCheckpoint) : getCheckpointFromState(wsState);
189-
return initAndVerifyWeakSubjectivityState(config, db, logger, store, wsState, checkpoint, {
224+
return initAndVerifyWeakSubjectivityState(config, db, logger, store, wsStateBytes, checkpoint, {
190225
ignoreWeakSubjectivityCheck,
191226
});
192227
}
193228

194229
async function fetchWSStateFromBeaconApi(
195-
lastDbState: BeaconStateAllForks | null,
230+
lastDbStateBytes: StateWithBytes | null,
231+
lastDbValidatorsBytes: Uint8Array | null,
196232
wssOpts: {checkpointSyncUrl: string; wssCheckpoint?: string; ignoreWeakSubjectivityCheck?: boolean},
197233
chainForkConfig: ChainForkConfig,
198234
db: IBeaconDb,
@@ -213,10 +249,15 @@ async function fetchWSStateFromBeaconApi(
213249
throw e;
214250
}
215251

216-
const {wsState, wsCheckpoint} = await fetchWeakSubjectivityState(chainForkConfig, logger, wssOpts);
252+
const {wsState, wsStateBytes, wsCheckpoint} = await fetchWeakSubjectivityState(chainForkConfig, logger, wssOpts, {
253+
lastDbState: lastDbStateBytes?.state ?? null,
254+
lastDbValidatorsBytes,
255+
});
256+
217257
const config = createBeaconConfig(chainForkConfig, wsState.genesisValidatorsRoot);
218-
const store = lastDbState ?? wsState;
219-
return initAndVerifyWeakSubjectivityState(config, db, logger, store, wsState, wsCheckpoint, {
258+
const wsStateWithBytes = {state: wsState, stateBytes: wsStateBytes};
259+
const store = lastDbStateBytes ?? wsStateWithBytes;
260+
return initAndVerifyWeakSubjectivityState(config, db, logger, store, wsStateWithBytes, wsCheckpoint, {
220261
ignoreWeakSubjectivityCheck: wssOpts.ignoreWeakSubjectivityCheck,
221262
});
222263
}

packages/cli/src/networks/index.ts

+27-8
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ import got from "got";
33
import {ENR} from "@chainsafe/enr";
44
import {SLOTS_PER_EPOCH} from "@lodestar/params";
55
import {HttpHeader, MediaType, WireFormat, getClient} from "@lodestar/api";
6-
import {getStateTypeFromBytes} from "@lodestar/beacon-node";
6+
import {getStateSlotFromBytes} from "@lodestar/beacon-node";
77
import {ChainConfig, ChainForkConfig} from "@lodestar/config";
88
import {Checkpoint} from "@lodestar/types/phase0";
99
import {Slot} from "@lodestar/types";
10-
import {fromHex, callFnWhenAwait, Logger} from "@lodestar/utils";
11-
import {BeaconStateAllForks, getLatestBlockRoot, computeCheckpointEpochAtStateSlot} from "@lodestar/state-transition";
10+
import {fromHex, callFnWhenAwait, Logger, formatBytes} from "@lodestar/utils";
11+
import {
12+
BeaconStateAllForks,
13+
getLatestBlockRoot,
14+
computeCheckpointEpochAtStateSlot,
15+
loadState,
16+
} from "@lodestar/state-transition";
1217
import {parseBootnodesFile} from "../util/format.js";
1318
import * as mainnet from "./mainnet.js";
1419
import * as dev from "./dev.js";
@@ -140,8 +145,12 @@ export function readBootnodes(bootnodesFilePath: string): string[] {
140145
export async function fetchWeakSubjectivityState(
141146
config: ChainForkConfig,
142147
logger: Logger,
143-
{checkpointSyncUrl, wssCheckpoint}: {checkpointSyncUrl: string; wssCheckpoint?: string}
144-
): Promise<{wsState: BeaconStateAllForks; wsCheckpoint: Checkpoint}> {
148+
{checkpointSyncUrl, wssCheckpoint}: {checkpointSyncUrl: string; wssCheckpoint?: string},
149+
{
150+
lastDbState,
151+
lastDbValidatorsBytes,
152+
}: {lastDbState: BeaconStateAllForks | null; lastDbValidatorsBytes: Uint8Array | null}
153+
): Promise<{wsState: BeaconStateAllForks; wsStateBytes: Uint8Array; wsCheckpoint: Checkpoint}> {
145154
try {
146155
let wsCheckpoint: Checkpoint | null;
147156
let stateId: Slot | "finalized";
@@ -169,21 +178,31 @@ export async function fetchWeakSubjectivityState(
169178
}
170179
);
171180

172-
const stateBytes = await callFnWhenAwait(
181+
const wsStateBytes = await callFnWhenAwait(
173182
getStatePromise,
174183
() => logger.info("Download in progress, please wait..."),
175184
GET_STATE_LOG_INTERVAL
176185
).then((res) => {
177186
return res.ssz();
178187
});
179188

180-
logger.info("Download completed", {stateId});
189+
const wsSlot = getStateSlotFromBytes(wsStateBytes);
190+
const logData = {stateId, size: formatBytes(wsStateBytes.length)};
191+
logger.info("Download completed", typeof stateId === "number" ? logData : {...logData, slot: wsSlot});
181192
// It should not be required to get fork type from bytes but Checkpointz does not return
182193
// Eth-Consensus-Version header, see https://github.com/ethpandaops/checkpointz/issues/164
183-
const wsState = getStateTypeFromBytes(config, stateBytes).deserializeToViewDU(stateBytes);
194+
let wsState: BeaconStateAllForks;
195+
if (lastDbState && lastDbValidatorsBytes) {
196+
// use lastDbState to load wsState if possible to share the same state tree
197+
wsState = loadState(config, lastDbState, wsStateBytes, lastDbValidatorsBytes).state;
198+
} else {
199+
const stateType = config.getForkTypes(wsSlot).BeaconState;
200+
wsState = stateType.deserializeToViewDU(wsStateBytes);
201+
}
184202

185203
return {
186204
wsState,
205+
wsStateBytes,
187206
wsCheckpoint: wsCheckpoint ?? getCheckpointFromState(wsState),
188207
};
189208
} catch (e) {

packages/state-transition/src/util/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ export * from "./validator.js";
2525
export * from "./weakSubjectivity.js";
2626
export * from "./deposit.js";
2727
export * from "./electra.js";
28+
export * from "./loadState/index.js";
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export {loadState} from "./loadState.js";
1+
export {loadState, loadStateAndValidators} from "./loadState.js";

0 commit comments

Comments
 (0)