From ac9e4147f3ff42a16e9002824b59b4b167c0d3f9 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 27 Aug 2024 15:04:25 +0700 Subject: [PATCH 1/5] feat: use db state to load ws state --- packages/beacon-node/src/index.ts | 2 +- .../cli/src/cmds/beacon/initBeaconState.ts | 36 ++++++++++++++--- packages/cli/src/networks/index.ts | 29 +++++++++++--- packages/state-transition/src/util/index.ts | 1 + .../src/util/loadState/index.ts | 2 +- .../src/util/loadState/loadState.ts | 19 +++++++++ .../test/unit/util/loadState.test.ts | 40 +++++++++++++++++++ 7 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 packages/state-transition/test/unit/util/loadState.test.ts diff --git a/packages/beacon-node/src/index.ts b/packages/beacon-node/src/index.ts index aa555a1ab0ca..b694a363312f 100644 --- a/packages/beacon-node/src/index.ts +++ b/packages/beacon-node/src/index.ts @@ -20,4 +20,4 @@ export {RestApiServer} from "./api/rest/base.js"; export type {RestApiServerOpts, RestApiServerModules, RestApiServerMetrics} from "./api/rest/base.js"; // Export type util for CLI - TEMP move to lodestar-types eventually -export {getStateTypeFromBytes} from "./util/multifork.js"; +export {getStateTypeFromBytes, getStateSlotFromBytes} from "./util/multifork.js"; diff --git a/packages/cli/src/cmds/beacon/initBeaconState.ts b/packages/cli/src/cmds/beacon/initBeaconState.ts index c8c444778991..f1dfb6256899 100644 --- a/packages/cli/src/cmds/beacon/initBeaconState.ts +++ b/packages/cli/src/cmds/beacon/initBeaconState.ts @@ -5,6 +5,8 @@ import { isWithinWeakSubjectivityPeriod, ensureWithinWeakSubjectivityPeriod, BeaconStateAllForks, + loadState, + loadStateAndValidators, } from "@lodestar/state-transition"; import { IBeaconDb, @@ -96,8 +98,17 @@ export async function initBeaconState( } // fetch the latest state stored in the db which will be used in all cases, if it exists, either // i) used directly as the anchor state - // ii) used during verification of a weak subjectivity state, - const lastDbState = await db.stateArchive.lastValue(); + // ii) used to load and verify a weak subjectivity state, + const lastDbSlot = await db.stateArchive.lastKey(); + const lastDbStateBytes = lastDbSlot !== null ? await db.stateArchive.getBinary(lastDbSlot) : null; + let lastDbState: BeaconStateAllForks | null = null; + let lastDbValidatorsBytes: Uint8Array | null = null; + if (lastDbStateBytes) { + const {state, validatorsBytes} = loadStateAndValidators(chainForkConfig, lastDbStateBytes); + lastDbState = state; + lastDbValidatorsBytes = validatorsBytes; + } + if (lastDbState) { const config = createBeaconConfig(chainForkConfig, lastDbState.genesisValidatorsRoot); const wssCheck = isWithinWeakSubjectivityPeriod(config, lastDbState, getCheckpointFromState(lastDbState)); @@ -107,7 +118,9 @@ export async function initBeaconState( // Forcing to sync from checkpoint is only recommended if node is taking too long to sync from last db state. // It is important to remind the user to remove this flag again unless it is absolutely necessary. if (wssCheck) { - logger.warn("Forced syncing from checkpoint even though db state is within weak subjectivity period"); + logger.warn( + `Forced syncing from checkpoint even though db state at slot ${lastDbState.slot} is within weak subjectivity period` + ); logger.warn("Please consider removing --forceCheckpointSync flag unless absolutely necessary"); } } else { @@ -128,6 +141,7 @@ export async function initBeaconState( if (args.checkpointState) { return readWSState( lastDbState, + lastDbValidatorsBytes, { checkpointState: args.checkpointState, wssCheckpoint: args.wssCheckpoint, @@ -140,6 +154,7 @@ export async function initBeaconState( } else if (args.checkpointSyncUrl) { return fetchWSStateFromBeaconApi( lastDbState, + lastDbValidatorsBytes, { checkpointSyncUrl: args.checkpointSyncUrl, wssCheckpoint: args.wssCheckpoint, @@ -171,6 +186,7 @@ export async function initBeaconState( async function readWSState( lastDbState: BeaconStateAllForks | null, + lastDbValidatorsBytes: Uint8Array | null, wssOpts: {checkpointState: string; wssCheckpoint?: string; ignoreWeakSubjectivityCheck?: boolean}, chainForkConfig: ChainForkConfig, db: IBeaconDb, @@ -182,7 +198,13 @@ async function readWSState( const {checkpointState, wssCheckpoint, ignoreWeakSubjectivityCheck} = wssOpts; const stateBytes = await downloadOrLoadFile(checkpointState); - const wsState = getStateTypeFromBytes(chainForkConfig, stateBytes).deserializeToViewDU(stateBytes); + let wsState: BeaconStateAllForks; + if (lastDbState && lastDbValidatorsBytes) { + // use lastDbState to load wsState if possible to share the same state tree + wsState = loadState(chainForkConfig, lastDbState, stateBytes, lastDbValidatorsBytes).state; + } else { + wsState = getStateTypeFromBytes(chainForkConfig, stateBytes).deserializeToViewDU(stateBytes); + } const config = createBeaconConfig(chainForkConfig, wsState.genesisValidatorsRoot); const store = lastDbState ?? wsState; const checkpoint = wssCheckpoint ? getCheckpointFromArg(wssCheckpoint) : getCheckpointFromState(wsState); @@ -193,6 +215,7 @@ async function readWSState( async function fetchWSStateFromBeaconApi( lastDbState: BeaconStateAllForks | null, + lastDbValidatorsBytes: Uint8Array | null, wssOpts: {checkpointSyncUrl: string; wssCheckpoint?: string; ignoreWeakSubjectivityCheck?: boolean}, chainForkConfig: ChainForkConfig, db: IBeaconDb, @@ -213,7 +236,10 @@ async function fetchWSStateFromBeaconApi( throw e; } - const {wsState, wsCheckpoint} = await fetchWeakSubjectivityState(chainForkConfig, logger, wssOpts); + const {wsState, wsCheckpoint} = await fetchWeakSubjectivityState(chainForkConfig, logger, wssOpts, { + lastDbState, + lastDbValidatorsBytes, + }); const config = createBeaconConfig(chainForkConfig, wsState.genesisValidatorsRoot); const store = lastDbState ?? wsState; return initAndVerifyWeakSubjectivityState(config, db, logger, store, wsState, wsCheckpoint, { diff --git a/packages/cli/src/networks/index.ts b/packages/cli/src/networks/index.ts index 2d605335b0e8..577ab61de2e6 100644 --- a/packages/cli/src/networks/index.ts +++ b/packages/cli/src/networks/index.ts @@ -3,12 +3,17 @@ import got from "got"; import {ENR} from "@chainsafe/enr"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {HttpHeader, MediaType, WireFormat, getClient} from "@lodestar/api"; -import {getStateTypeFromBytes} from "@lodestar/beacon-node"; +import {getStateSlotFromBytes} from "@lodestar/beacon-node"; import {ChainConfig, ChainForkConfig} from "@lodestar/config"; import {Checkpoint} from "@lodestar/types/phase0"; import {Slot} from "@lodestar/types"; import {fromHex, callFnWhenAwait, Logger} from "@lodestar/utils"; -import {BeaconStateAllForks, getLatestBlockRoot, computeCheckpointEpochAtStateSlot} from "@lodestar/state-transition"; +import { + BeaconStateAllForks, + getLatestBlockRoot, + computeCheckpointEpochAtStateSlot, + loadState, +} from "@lodestar/state-transition"; import {parseBootnodesFile} from "../util/format.js"; import * as mainnet from "./mainnet.js"; import * as dev from "./dev.js"; @@ -140,7 +145,11 @@ export function readBootnodes(bootnodesFilePath: string): string[] { export async function fetchWeakSubjectivityState( config: ChainForkConfig, logger: Logger, - {checkpointSyncUrl, wssCheckpoint}: {checkpointSyncUrl: string; wssCheckpoint?: string} + {checkpointSyncUrl, wssCheckpoint}: {checkpointSyncUrl: string; wssCheckpoint?: string}, + { + lastDbState, + lastDbValidatorsBytes, + }: {lastDbState: BeaconStateAllForks | null; lastDbValidatorsBytes: Uint8Array | null} ): Promise<{wsState: BeaconStateAllForks; wsCheckpoint: Checkpoint}> { try { let wsCheckpoint: Checkpoint | null; @@ -169,7 +178,7 @@ export async function fetchWeakSubjectivityState( } ); - const stateBytes = await callFnWhenAwait( + const wsStateBytes = await callFnWhenAwait( getStatePromise, () => logger.info("Download in progress, please wait..."), GET_STATE_LOG_INTERVAL @@ -177,10 +186,18 @@ export async function fetchWeakSubjectivityState( return res.ssz(); }); - logger.info("Download completed", {stateId}); + const wsSlot = getStateSlotFromBytes(wsStateBytes); + logger.info("Download completed", typeof stateId === "number" ? {stateId} : {stateId, slot: wsSlot}); // It should not be required to get fork type from bytes but Checkpointz does not return // Eth-Consensus-Version header, see https://github.com/ethpandaops/checkpointz/issues/164 - const wsState = getStateTypeFromBytes(config, stateBytes).deserializeToViewDU(stateBytes); + let wsState: BeaconStateAllForks; + if (lastDbState && lastDbValidatorsBytes) { + // use lastDbState to load wsState if possible to share the same state tree + wsState = loadState(config, lastDbState, wsStateBytes, lastDbValidatorsBytes).state; + } else { + const stateType = config.getForkTypes(wsSlot).BeaconState; + wsState = stateType.deserializeToViewDU(wsStateBytes); + } return { wsState, diff --git a/packages/state-transition/src/util/index.ts b/packages/state-transition/src/util/index.ts index 6a839fbe103d..9b9916f1d49e 100644 --- a/packages/state-transition/src/util/index.ts +++ b/packages/state-transition/src/util/index.ts @@ -25,3 +25,4 @@ export * from "./validator.js"; export * from "./weakSubjectivity.js"; export * from "./deposit.js"; export * from "./electra.js"; +export * from "./loadState/index.js"; diff --git a/packages/state-transition/src/util/loadState/index.ts b/packages/state-transition/src/util/loadState/index.ts index 706de3c11540..78ffe7877c09 100644 --- a/packages/state-transition/src/util/loadState/index.ts +++ b/packages/state-transition/src/util/loadState/index.ts @@ -1 +1 @@ -export {loadState} from "./loadState.js"; +export {loadState, loadStateAndValidators} from "./loadState.js"; diff --git a/packages/state-transition/src/util/loadState/loadState.ts b/packages/state-transition/src/util/loadState/loadState.ts index dc9f8fe4fcab..6e3e9c6719fa 100644 --- a/packages/state-transition/src/util/loadState/loadState.ts +++ b/packages/state-transition/src/util/loadState/loadState.ts @@ -66,6 +66,25 @@ export function loadState( return {state: migratedState, modifiedValidators}; } +/** + * Load state and validators Uint8Array from state bytes. + */ +export function loadStateAndValidators( + chainForkConfig: ChainForkConfig, + stateBytes: Uint8Array +): {state: BeaconStateAllForks; validatorsBytes: Uint8Array} { + // stateType could be any types, casting just to make typescript happy + const stateType = getStateTypeFromBytes(chainForkConfig, stateBytes) as typeof ssz.phase0.BeaconState; + const state = stateType.deserializeToViewDU(stateBytes); + const dataView = new DataView(stateBytes.buffer, stateBytes.byteOffset, stateBytes.byteLength); + const fieldRanges = stateType.getFieldRanges(dataView, 0, stateBytes.length); + const allFields = Object.keys(stateType.fields); + const validatorFieldIndex = allFields.indexOf("validators"); + const validatorRange = fieldRanges[validatorFieldIndex]; + const validatorsBytes = stateBytes.subarray(validatorRange.start, validatorRange.end); + return {state, validatorsBytes}; +} + /** * This value is rarely changed as monitored 3 month state diffs on mainnet as of Sep 2023. * Reusing this data helps save hashTreeRoot time of state ~500ms diff --git a/packages/state-transition/test/unit/util/loadState.test.ts b/packages/state-transition/test/unit/util/loadState.test.ts new file mode 100644 index 000000000000..97a792a28adb --- /dev/null +++ b/packages/state-transition/test/unit/util/loadState.test.ts @@ -0,0 +1,40 @@ +import {describe, it, expect} from "vitest"; +import {ssz} from "@lodestar/types"; +import {mainnetChainConfig} from "@lodestar/config/networks"; +import {createChainForkConfig} from "@lodestar/config"; +import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; +import {loadStateAndValidators} from "../../../src/util/loadState/loadState.js"; + +describe("loadStateAndValidators", () => { + const numValidator = 10; + const config = createChainForkConfig(mainnetChainConfig); + + const testCases: {name: ForkName; slot: number}[] = [ + {name: ForkName.phase0, slot: 100}, + {name: ForkName.altair, slot: mainnetChainConfig.ALTAIR_FORK_EPOCH * SLOTS_PER_EPOCH + 100}, + {name: ForkName.capella, slot: mainnetChainConfig.CAPELLA_FORK_EPOCH * SLOTS_PER_EPOCH + 100}, + {name: ForkName.deneb, slot: mainnetChainConfig.DENEB_FORK_EPOCH * SLOTS_PER_EPOCH + 100}, + ]; + + for (const {name, slot} of testCases) { + it(`fork: ${name}, slot: ${slot}`, () => { + const state = config.getForkTypes(slot).BeaconState.defaultViewDU(); + state.slot = slot; + for (let i = 0; i < numValidator; i++) { + const validator = ssz.phase0.Validator.defaultViewDU(); + validator.pubkey = Buffer.alloc(48, i); + state.validators.push(validator); + state.balances.push(32 * 1e9); + } + state.commit(); + + const stateBytes = state.serialize(); + const stateRoot = state.hashTreeRoot(); + const {state: loadedState, validatorsBytes} = loadStateAndValidators(config, stateBytes); + expect(loadedState.hashTreeRoot()).toEqual(stateRoot); + // serialize() somehow takes time, however comparing state root would be enough + // expect(loadedState.serialize()).toEqual(stateBytes); + expect(validatorsBytes).toEqual(state.validators.serialize()); + }); + } +}); From a20480deae72ec88581bba9f938a42d022fe8309 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 27 Aug 2024 15:30:47 +0700 Subject: [PATCH 2/5] feat: log state size --- .../cli/src/cmds/beacon/initBeaconState.ts | 3 +- packages/cli/src/networks/index.ts | 5 +-- packages/utils/src/bytes.ts | 20 ++++++++++++ packages/utils/test/unit/bytes.test.ts | 32 ++++++++++++++++++- 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/cmds/beacon/initBeaconState.ts b/packages/cli/src/cmds/beacon/initBeaconState.ts index f1dfb6256899..1b78e5efd9fe 100644 --- a/packages/cli/src/cmds/beacon/initBeaconState.ts +++ b/packages/cli/src/cmds/beacon/initBeaconState.ts @@ -1,6 +1,6 @@ import {ssz} from "@lodestar/types"; import {createBeaconConfig, BeaconConfig, ChainForkConfig} from "@lodestar/config"; -import {Logger} from "@lodestar/utils"; +import {Logger, formatBytes} from "@lodestar/utils"; import { isWithinWeakSubjectivityPeriod, ensureWithinWeakSubjectivityPeriod, @@ -104,6 +104,7 @@ export async function initBeaconState( let lastDbState: BeaconStateAllForks | null = null; let lastDbValidatorsBytes: Uint8Array | null = null; if (lastDbStateBytes) { + logger.verbose("Found the last archived state", {slot: lastDbSlot, size: formatBytes(lastDbStateBytes.length)}); const {state, validatorsBytes} = loadStateAndValidators(chainForkConfig, lastDbStateBytes); lastDbState = state; lastDbValidatorsBytes = validatorsBytes; diff --git a/packages/cli/src/networks/index.ts b/packages/cli/src/networks/index.ts index 577ab61de2e6..dc012e915c37 100644 --- a/packages/cli/src/networks/index.ts +++ b/packages/cli/src/networks/index.ts @@ -7,7 +7,7 @@ import {getStateSlotFromBytes} from "@lodestar/beacon-node"; import {ChainConfig, ChainForkConfig} from "@lodestar/config"; import {Checkpoint} from "@lodestar/types/phase0"; import {Slot} from "@lodestar/types"; -import {fromHex, callFnWhenAwait, Logger} from "@lodestar/utils"; +import {fromHex, callFnWhenAwait, Logger, formatBytes} from "@lodestar/utils"; import { BeaconStateAllForks, getLatestBlockRoot, @@ -187,7 +187,8 @@ export async function fetchWeakSubjectivityState( }); const wsSlot = getStateSlotFromBytes(wsStateBytes); - logger.info("Download completed", typeof stateId === "number" ? {stateId} : {stateId, slot: wsSlot}); + const logData = {stateId, size: formatBytes(wsStateBytes.length)}; + logger.info("Download completed", typeof stateId === "number" ? logData : {...logData, slot: wsSlot}); // It should not be required to get fork type from bytes but Checkpointz does not return // Eth-Consensus-Version header, see https://github.com/ethpandaops/checkpointz/issues/164 let wsState: BeaconStateAllForks; diff --git a/packages/utils/src/bytes.ts b/packages/utils/src/bytes.ts index 6670b115ff8f..95bb62ebd548 100644 --- a/packages/utils/src/bytes.ts +++ b/packages/utils/src/bytes.ts @@ -48,3 +48,23 @@ export function bytesToBigInt(value: Uint8Array, endianness: Endianness = "le"): } throw new Error("endianness must be either 'le' or 'be'"); } + +export function formatBytes(bytes: number): string { + if (bytes < 0) { + throw new Error("bytes must be a positive number, got " + bytes); + } + + if (bytes === 0) { + return "0 Bytes"; + } + + // size of a kb + const k = 1024; + + // only support up to GB + const units = ["Bytes", "KB", "MB", "GB"]; + const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), units.length - 1); + const formattedSize = (bytes / Math.pow(k, i)).toFixed(2); + + return `${formattedSize} ${units[i]}`; +} diff --git a/packages/utils/test/unit/bytes.test.ts b/packages/utils/test/unit/bytes.test.ts index 05789b839cfd..af4df6652f13 100644 --- a/packages/utils/test/unit/bytes.test.ts +++ b/packages/utils/test/unit/bytes.test.ts @@ -1,5 +1,14 @@ import {describe, it, expect} from "vitest"; -import {intToBytes, bytesToInt, toHex, fromHex, toHexString, toRootHex, toPubkeyHex} from "../../src/index.js"; +import { + intToBytes, + bytesToInt, + toHex, + fromHex, + toHexString, + toRootHex, + toPubkeyHex, + formatBytes, +} from "../../src/index.js"; describe("intToBytes", () => { const zeroedArray = (length: number): number[] => Array.from({length}, () => 0); @@ -135,3 +144,24 @@ describe("toHexString", () => { }); } }); + +describe("formatBytes", () => { + const testCases: {input: number; output: string}[] = [ + {input: 0, output: "0 Bytes"}, + {input: 1, output: "1.00 Bytes"}, + {input: 1024, output: "1.00 KB"}, + {input: 1024 + 0.12 * 1024, output: "1.12 KB"}, + {input: 1024 * 1024, output: "1.00 MB"}, + {input: 1024 * 1024 + 0.12 * (1024 * 1024), output: "1.12 MB"}, + {input: 1024 * 1024 * 1024, output: "1.00 GB"}, + {input: 1024 * 1024 * 1024 + 0.12 * 1024 * 1024 * 1024, output: "1.12 GB"}, + // too big + {input: 1024 * 1024 * 1024 * 1024, output: "1024.00 GB"}, + ]; + + for (const {input, output} of testCases) { + it(`should format ${input} bytes as ${output}`, () => { + expect(formatBytes(input)).toBe(output); + }); + } +}); From ec99f06bbf1e9de182f8891c8c6d8ab65ace7861 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Wed, 28 Aug 2024 10:54:58 +0700 Subject: [PATCH 3/5] fix: rename initStateFromAnchorState to checkAndPersistAnchorState --- packages/beacon-node/src/chain/initState.ts | 2 +- packages/beacon-node/src/index.ts | 2 +- packages/cli/src/cmds/beacon/initBeaconState.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/beacon-node/src/chain/initState.ts b/packages/beacon-node/src/chain/initState.ts index 19f0f1cc959d..4ce4fab1ee51 100644 --- a/packages/beacon-node/src/chain/initState.ts +++ b/packages/beacon-node/src/chain/initState.ts @@ -154,7 +154,7 @@ export async function initStateFromDb( /** * Initialize and persist an anchor state (either weak subjectivity or genesis) */ -export async function initStateFromAnchorState( +export async function checkAndPersistAnchorState( config: ChainForkConfig, db: IBeaconDb, logger: Logger, diff --git a/packages/beacon-node/src/index.ts b/packages/beacon-node/src/index.ts index b694a363312f..723b56d0b488 100644 --- a/packages/beacon-node/src/index.ts +++ b/packages/beacon-node/src/index.ts @@ -1,4 +1,4 @@ -export {initStateFromAnchorState, initStateFromDb, initStateFromEth1} from "./chain/index.js"; +export {checkAndPersistAnchorState, initStateFromDb, initStateFromEth1} from "./chain/index.js"; export {BeaconDb, type IBeaconDb} from "./db/index.js"; export {Eth1Provider, type IEth1Provider} from "./eth1/index.js"; export {createNodeJsLibp2p, type NodeJsLibp2pOpts} from "./network/index.js"; diff --git a/packages/cli/src/cmds/beacon/initBeaconState.ts b/packages/cli/src/cmds/beacon/initBeaconState.ts index 1b78e5efd9fe..975be9856a71 100644 --- a/packages/cli/src/cmds/beacon/initBeaconState.ts +++ b/packages/cli/src/cmds/beacon/initBeaconState.ts @@ -11,7 +11,7 @@ import { import { IBeaconDb, IBeaconNodeOptions, - initStateFromAnchorState, + checkAndPersistAnchorState, initStateFromEth1, getStateTypeFromBytes, } from "@lodestar/beacon-node"; @@ -67,7 +67,7 @@ async function initAndVerifyWeakSubjectivityState( throw wssCheck.err; } - anchorState = await initStateFromAnchorState(config, db, logger, anchorState, { + anchorState = await checkAndPersistAnchorState(config, db, logger, anchorState, { isWithinWeakSubjectivityPeriod, isCheckpointState, }); @@ -129,7 +129,7 @@ export async function initBeaconState( // - if no checkpoint sync args provided, or // - the lastDbState is within weak subjectivity period: if ((!args.checkpointState && !args.checkpointSyncUrl) || wssCheck) { - const anchorState = await initStateFromAnchorState(config, db, logger, lastDbState, { + const anchorState = await checkAndPersistAnchorState(config, db, logger, lastDbState, { isWithinWeakSubjectivityPeriod: wssCheck, isCheckpointState: false, }); @@ -172,7 +172,7 @@ export async function initBeaconState( let anchorState = getStateTypeFromBytes(chainForkConfig, stateBytes).deserializeToViewDU(stateBytes); const config = createBeaconConfig(chainForkConfig, anchorState.genesisValidatorsRoot); const wssCheck = isWithinWeakSubjectivityPeriod(config, anchorState, getCheckpointFromState(anchorState)); - anchorState = await initStateFromAnchorState(config, db, logger, anchorState, { + anchorState = await checkAndPersistAnchorState(config, db, logger, anchorState, { isWithinWeakSubjectivityPeriod: wssCheck, isCheckpointState: true, }); From 07546f2c90f06faa0f18bb6cefa12a9cece4fb69 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Wed, 28 Aug 2024 11:01:50 +0700 Subject: [PATCH 4/5] fix: only persist anchor state if it's cp state --- packages/beacon-node/src/chain/initState.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/src/chain/initState.ts b/packages/beacon-node/src/chain/initState.ts index 4ce4fab1ee51..6a8912946cf9 100644 --- a/packages/beacon-node/src/chain/initState.ts +++ b/packages/beacon-node/src/chain/initState.ts @@ -191,7 +191,9 @@ export async function checkAndPersistAnchorState( logger.warn("Checkpoint sync recommended, please use --help to see checkpoint sync options"); } - await persistAnchorState(config, db, anchorState); + if (isCheckpointState || anchorState.slot === GENESIS_SLOT) { + await persistAnchorState(config, db, anchorState); + } return anchorState; } From 067f62048666ed66bae8c0f774d78e326dc12c0a Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Wed, 28 Aug 2024 14:42:19 +0700 Subject: [PATCH 5/5] fix: avoid redundant anchor state serialization --- packages/beacon-node/src/chain/initState.ts | 14 ++-- .../cli/src/cmds/beacon/initBeaconState.ts | 72 +++++++++++-------- packages/cli/src/networks/index.ts | 3 +- 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/packages/beacon-node/src/chain/initState.ts b/packages/beacon-node/src/chain/initState.ts index 6a8912946cf9..e413bdff0f7d 100644 --- a/packages/beacon-node/src/chain/initState.ts +++ b/packages/beacon-node/src/chain/initState.ts @@ -37,17 +37,18 @@ export async function persistGenesisResult( export async function persistAnchorState( config: ChainForkConfig, db: IBeaconDb, - anchorState: BeaconStateAllForks + anchorState: BeaconStateAllForks, + anchorStateBytes: Uint8Array ): Promise { if (anchorState.slot === GENESIS_SLOT) { const genesisBlock = createGenesisBlock(config, anchorState); await Promise.all([ db.blockArchive.add(genesisBlock), db.block.add(genesisBlock), - db.stateArchive.add(anchorState), + db.stateArchive.putBinary(anchorState.slot, anchorStateBytes), ]); } else { - await db.stateArchive.add(anchorState); + await db.stateArchive.putBinary(anchorState.slot, anchorStateBytes); } } @@ -159,11 +160,12 @@ export async function checkAndPersistAnchorState( db: IBeaconDb, logger: Logger, anchorState: BeaconStateAllForks, + anchorStateBytes: Uint8Array, { isWithinWeakSubjectivityPeriod, isCheckpointState, }: {isWithinWeakSubjectivityPeriod: boolean; isCheckpointState: boolean} -): Promise { +): Promise { const expectedFork = config.getForkInfo(computeStartSlotAtEpoch(anchorState.fork.epoch)); const expectedForkVersion = toHex(expectedFork.version); const stateFork = toHex(anchorState.fork.currentVersion); @@ -192,10 +194,8 @@ export async function checkAndPersistAnchorState( } if (isCheckpointState || anchorState.slot === GENESIS_SLOT) { - await persistAnchorState(config, db, anchorState); + await persistAnchorState(config, db, anchorState, anchorStateBytes); } - - return anchorState; } export function initBeaconMetrics(metrics: Metrics, state: BeaconStateAllForks): void { diff --git a/packages/cli/src/cmds/beacon/initBeaconState.ts b/packages/cli/src/cmds/beacon/initBeaconState.ts index 975be9856a71..67b578e9fdb9 100644 --- a/packages/cli/src/cmds/beacon/initBeaconState.ts +++ b/packages/cli/src/cmds/beacon/initBeaconState.ts @@ -27,19 +27,23 @@ import { } from "../../networks/index.js"; import {BeaconArgs} from "./options.js"; +type StateWithBytes = {state: BeaconStateAllForks; stateBytes: Uint8Array}; + async function initAndVerifyWeakSubjectivityState( config: BeaconConfig, db: IBeaconDb, logger: Logger, - store: BeaconStateAllForks, - wsState: BeaconStateAllForks, + dbStateBytes: StateWithBytes, + wsStateBytes: StateWithBytes, wsCheckpoint: Checkpoint, opts: {ignoreWeakSubjectivityCheck?: boolean} = {} ): Promise<{anchorState: BeaconStateAllForks; wsCheckpoint: Checkpoint}> { + const dbState = dbStateBytes.state; + const wsState = wsStateBytes.state; // Check if the store's state and wsState are compatible if ( - store.genesisTime !== wsState.genesisTime || - !ssz.Root.equals(store.genesisValidatorsRoot, wsState.genesisValidatorsRoot) + dbState.genesisTime !== wsState.genesisTime || + !ssz.Root.equals(dbState.genesisValidatorsRoot, wsState.genesisValidatorsRoot) ) { throw new Error( "Db state and checkpoint state are not compatible, either clear the db or verify your checkpoint source" @@ -47,12 +51,12 @@ async function initAndVerifyWeakSubjectivityState( } // Pick the state which is ahead as an anchor to initialize the beacon chain - let anchorState = wsState; + let anchorState = wsStateBytes; let anchorCheckpoint = wsCheckpoint; let isCheckpointState = true; - if (store.slot > wsState.slot) { - anchorState = store; - anchorCheckpoint = getCheckpointFromState(store); + if (dbState.slot > wsState.slot) { + anchorState = dbStateBytes; + anchorCheckpoint = getCheckpointFromState(dbState); isCheckpointState = false; logger.verbose( "Db state is ahead of the provided checkpoint state, using the db state to initialize the beacon chain" @@ -61,19 +65,19 @@ async function initAndVerifyWeakSubjectivityState( // Throw error unless user explicitly asked not to, in testnets can happen that wss period is too small // that even some epochs of non finalization can cause finalized checkpoint to be out of valid range - const wssCheck = wrapFnError(() => ensureWithinWeakSubjectivityPeriod(config, anchorState, anchorCheckpoint)); + const wssCheck = wrapFnError(() => ensureWithinWeakSubjectivityPeriod(config, anchorState.state, anchorCheckpoint)); const isWithinWeakSubjectivityPeriod = wssCheck.err === null; if (!isWithinWeakSubjectivityPeriod && !opts.ignoreWeakSubjectivityCheck) { throw wssCheck.err; } - anchorState = await checkAndPersistAnchorState(config, db, logger, anchorState, { + await checkAndPersistAnchorState(config, db, logger, anchorState.state, anchorState.stateBytes, { isWithinWeakSubjectivityPeriod, isCheckpointState, }); // Return the latest anchorState but still return original wsCheckpoint to validate in backfill - return {anchorState, wsCheckpoint}; + return {anchorState: anchorState.state, wsCheckpoint}; } /** @@ -100,14 +104,16 @@ export async function initBeaconState( // i) used directly as the anchor state // ii) used to load and verify a weak subjectivity state, const lastDbSlot = await db.stateArchive.lastKey(); - const lastDbStateBytes = lastDbSlot !== null ? await db.stateArchive.getBinary(lastDbSlot) : null; + const stateBytes = lastDbSlot !== null ? await db.stateArchive.getBinary(lastDbSlot) : null; let lastDbState: BeaconStateAllForks | null = null; let lastDbValidatorsBytes: Uint8Array | null = null; - if (lastDbStateBytes) { - logger.verbose("Found the last archived state", {slot: lastDbSlot, size: formatBytes(lastDbStateBytes.length)}); - const {state, validatorsBytes} = loadStateAndValidators(chainForkConfig, lastDbStateBytes); + let lastDbStateWithBytes: StateWithBytes | null = null; + if (stateBytes) { + logger.verbose("Found the last archived state", {slot: lastDbSlot, size: formatBytes(stateBytes.length)}); + const {state, validatorsBytes} = loadStateAndValidators(chainForkConfig, stateBytes); lastDbState = state; lastDbValidatorsBytes = validatorsBytes; + lastDbStateWithBytes = {state, stateBytes: stateBytes}; } if (lastDbState) { @@ -129,11 +135,15 @@ export async function initBeaconState( // - if no checkpoint sync args provided, or // - the lastDbState is within weak subjectivity period: if ((!args.checkpointState && !args.checkpointSyncUrl) || wssCheck) { - const anchorState = await checkAndPersistAnchorState(config, db, logger, lastDbState, { + if (stateBytes === null) { + // this never happens + throw Error(`There is no stateBytes for the lastDbState at slot ${lastDbState.slot}`); + } + await checkAndPersistAnchorState(config, db, logger, lastDbState, stateBytes, { isWithinWeakSubjectivityPeriod: wssCheck, isCheckpointState: false, }); - return {anchorState}; + return {anchorState: lastDbState}; } } } @@ -141,7 +151,7 @@ export async function initBeaconState( // See if we can sync state using checkpoint sync args or else start from genesis if (args.checkpointState) { return readWSState( - lastDbState, + lastDbStateWithBytes, lastDbValidatorsBytes, { checkpointState: args.checkpointState, @@ -154,7 +164,7 @@ export async function initBeaconState( ); } else if (args.checkpointSyncUrl) { return fetchWSStateFromBeaconApi( - lastDbState, + lastDbStateWithBytes, lastDbValidatorsBytes, { checkpointSyncUrl: args.checkpointSyncUrl, @@ -169,10 +179,10 @@ export async function initBeaconState( const genesisStateFile = args.genesisStateFile || getGenesisFileUrl(args.network || defaultNetwork); if (genesisStateFile && !args.forceGenesis) { const stateBytes = await downloadOrLoadFile(genesisStateFile); - let anchorState = getStateTypeFromBytes(chainForkConfig, stateBytes).deserializeToViewDU(stateBytes); + const anchorState = getStateTypeFromBytes(chainForkConfig, stateBytes).deserializeToViewDU(stateBytes); const config = createBeaconConfig(chainForkConfig, anchorState.genesisValidatorsRoot); const wssCheck = isWithinWeakSubjectivityPeriod(config, anchorState, getCheckpointFromState(anchorState)); - anchorState = await checkAndPersistAnchorState(config, db, logger, anchorState, { + await checkAndPersistAnchorState(config, db, logger, anchorState, stateBytes, { isWithinWeakSubjectivityPeriod: wssCheck, isCheckpointState: true, }); @@ -186,7 +196,7 @@ export async function initBeaconState( } async function readWSState( - lastDbState: BeaconStateAllForks | null, + lastDbStateBytes: StateWithBytes | null, lastDbValidatorsBytes: Uint8Array | null, wssOpts: {checkpointState: string; wssCheckpoint?: string; ignoreWeakSubjectivityCheck?: boolean}, chainForkConfig: ChainForkConfig, @@ -197,6 +207,7 @@ async function readWSState( // if a weak subjectivity checkpoint has been provided, it is used for additional verification // otherwise, the state itself is used for verification (not bad, because the trusted state has been explicitly provided) const {checkpointState, wssCheckpoint, ignoreWeakSubjectivityCheck} = wssOpts; + const lastDbState = lastDbStateBytes?.state ?? null; const stateBytes = await downloadOrLoadFile(checkpointState); let wsState: BeaconStateAllForks; @@ -207,15 +218,16 @@ async function readWSState( wsState = getStateTypeFromBytes(chainForkConfig, stateBytes).deserializeToViewDU(stateBytes); } const config = createBeaconConfig(chainForkConfig, wsState.genesisValidatorsRoot); - const store = lastDbState ?? wsState; + const wsStateBytes = {state: wsState, stateBytes}; + const store = lastDbStateBytes ?? wsStateBytes; const checkpoint = wssCheckpoint ? getCheckpointFromArg(wssCheckpoint) : getCheckpointFromState(wsState); - return initAndVerifyWeakSubjectivityState(config, db, logger, store, wsState, checkpoint, { + return initAndVerifyWeakSubjectivityState(config, db, logger, store, wsStateBytes, checkpoint, { ignoreWeakSubjectivityCheck, }); } async function fetchWSStateFromBeaconApi( - lastDbState: BeaconStateAllForks | null, + lastDbStateBytes: StateWithBytes | null, lastDbValidatorsBytes: Uint8Array | null, wssOpts: {checkpointSyncUrl: string; wssCheckpoint?: string; ignoreWeakSubjectivityCheck?: boolean}, chainForkConfig: ChainForkConfig, @@ -237,13 +249,15 @@ async function fetchWSStateFromBeaconApi( throw e; } - const {wsState, wsCheckpoint} = await fetchWeakSubjectivityState(chainForkConfig, logger, wssOpts, { - lastDbState, + const {wsState, wsStateBytes, wsCheckpoint} = await fetchWeakSubjectivityState(chainForkConfig, logger, wssOpts, { + lastDbState: lastDbStateBytes?.state ?? null, lastDbValidatorsBytes, }); + const config = createBeaconConfig(chainForkConfig, wsState.genesisValidatorsRoot); - const store = lastDbState ?? wsState; - return initAndVerifyWeakSubjectivityState(config, db, logger, store, wsState, wsCheckpoint, { + const wsStateWithBytes = {state: wsState, stateBytes: wsStateBytes}; + const store = lastDbStateBytes ?? wsStateWithBytes; + return initAndVerifyWeakSubjectivityState(config, db, logger, store, wsStateWithBytes, wsCheckpoint, { ignoreWeakSubjectivityCheck: wssOpts.ignoreWeakSubjectivityCheck, }); } diff --git a/packages/cli/src/networks/index.ts b/packages/cli/src/networks/index.ts index dc012e915c37..0831b78cd2f6 100644 --- a/packages/cli/src/networks/index.ts +++ b/packages/cli/src/networks/index.ts @@ -150,7 +150,7 @@ export async function fetchWeakSubjectivityState( lastDbState, lastDbValidatorsBytes, }: {lastDbState: BeaconStateAllForks | null; lastDbValidatorsBytes: Uint8Array | null} -): Promise<{wsState: BeaconStateAllForks; wsCheckpoint: Checkpoint}> { +): Promise<{wsState: BeaconStateAllForks; wsStateBytes: Uint8Array; wsCheckpoint: Checkpoint}> { try { let wsCheckpoint: Checkpoint | null; let stateId: Slot | "finalized"; @@ -202,6 +202,7 @@ export async function fetchWeakSubjectivityState( return { wsState, + wsStateBytes, wsCheckpoint: wsCheckpoint ?? getCheckpointFromState(wsState), }; } catch (e) {