From 6cf7c2c3f3df6012bd4ce773ab3b5310c347e884 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Thu, 16 Sep 2021 23:33:04 +0200 Subject: [PATCH] Add merge fork transition process (#3197) * Add upgrade state logic * Initialize TransitionStore from db * Update stub type --- .../src/allForks/stateTransition.ts | 19 ++++++----- .../src/merge/index.ts | 1 + .../src/merge/upgradeState.ts | 34 +++++++++++++++++++ packages/db/src/schema.ts | 4 ++- .../fork-choice/src/forkChoice/forkChoice.ts | 8 ++++- .../fork-choice/src/forkChoice/interface.ts | 3 ++ packages/fork-choice/src/index.ts | 1 + packages/lodestar/src/chain/chain.ts | 31 +++++++++++------ .../lodestar/src/chain/forkChoice/index.ts | 27 ++++++++++++--- packages/lodestar/src/chain/options.ts | 12 ++++--- packages/lodestar/src/db/beacon.ts | 4 +++ packages/lodestar/src/db/interface.ts | 2 ++ packages/lodestar/src/db/single/index.ts | 1 + .../src/db/single/totalTerminalDifficulty.ts | 33 ++++++++++++++++++ packages/lodestar/src/node/nodejs.ts | 3 +- .../unit/chain/forkChoice/forkChoice.test.ts | 3 +- .../lodestar/test/utils/mocks/chain/chain.ts | 1 + .../test/spec/allForks/forkChoice.ts | 3 +- 18 files changed, 156 insertions(+), 34 deletions(-) create mode 100644 packages/beacon-state-transition/src/merge/upgradeState.ts create mode 100644 packages/lodestar/src/db/single/totalTerminalDifficulty.ts diff --git a/packages/beacon-state-transition/src/allForks/stateTransition.ts b/packages/beacon-state-transition/src/allForks/stateTransition.ts index d14fbbca405b..c1f5f12d55aa 100644 --- a/packages/beacon-state-transition/src/allForks/stateTransition.ts +++ b/packages/beacon-state-transition/src/allForks/stateTransition.ts @@ -1,6 +1,6 @@ /* eslint-disable import/namespace */ -import {allForks, phase0 as phase0Types, Slot, ssz} from "@chainsafe/lodestar-types"; -import {ForkName, GENESIS_EPOCH, SLOTS_PER_EPOCH} from "@chainsafe/lodestar-params"; +import {allForks, Slot, ssz} from "@chainsafe/lodestar-types"; +import {ForkName, SLOTS_PER_EPOCH} from "@chainsafe/lodestar-params"; import * as phase0 from "../phase0"; import * as altair from "../altair"; import * as merge from "../merge"; @@ -12,7 +12,8 @@ import {computeEpochAtSlot} from "../util"; import {toHexString} from "@chainsafe/ssz"; type StateAllForks = CachedBeaconState; -type StatePhase0 = CachedBeaconState; +type StatePhase0 = CachedBeaconState; +type StateAltair = CachedBeaconState; type ProcessBlockFn = (state: StateAllForks, block: allForks.BeaconBlock, verifySignatures: boolean) => void; type ProcessEpochFn = (state: StateAllForks, epochProcess: IEpochProcess) => void; @@ -159,12 +160,12 @@ function processSlotsWithTransientCache( } // Upgrade state if exactly at epoch boundary - switch (computeEpochAtSlot(postState.slot)) { - case GENESIS_EPOCH: - break; // Don't do any upgrades at genesis epoch - case config.ALTAIR_FORK_EPOCH: - postState = altair.upgradeState(postState as StatePhase0) as StateAllForks; - break; + const stateSlot = computeEpochAtSlot(postState.slot); + if (stateSlot === config.ALTAIR_FORK_EPOCH) { + postState = altair.upgradeState(postState as StatePhase0) as StateAllForks; + } + if (stateSlot === config.MERGE_FORK_EPOCH) { + postState = merge.upgradeState(postState as StateAltair) as StateAllForks; } } else { postState.slot++; diff --git a/packages/beacon-state-transition/src/merge/index.ts b/packages/beacon-state-transition/src/merge/index.ts index a6f88aa27d61..e94ddb31653a 100644 --- a/packages/beacon-state-transition/src/merge/index.ts +++ b/packages/beacon-state-transition/src/merge/index.ts @@ -1,4 +1,5 @@ export {processBlock} from "./block"; +export {upgradeState} from "./upgradeState"; export * from "./utils"; // re-export merge lodestar types for ergonomic usage downstream diff --git a/packages/beacon-state-transition/src/merge/upgradeState.ts b/packages/beacon-state-transition/src/merge/upgradeState.ts new file mode 100644 index 000000000000..19a2f71166a1 --- /dev/null +++ b/packages/beacon-state-transition/src/merge/upgradeState.ts @@ -0,0 +1,34 @@ +import {altair, merge, ssz} from "@chainsafe/lodestar-types"; +import {CachedBeaconState, createCachedBeaconState} from "../allForks/util"; +import {getCurrentEpoch} from "../util"; +import {TreeBacked} from "@chainsafe/ssz"; +import {IBeaconConfig} from "@chainsafe/lodestar-config"; + +/** + * Upgrade a state from altair to merge. + */ +export function upgradeState(state: CachedBeaconState): CachedBeaconState { + const {config} = state; + const postTreeBackedState = upgradeTreeBackedState(config, state); + // TODO: This seems very sub-optimal, review + return createCachedBeaconState(config, postTreeBackedState); +} + +function upgradeTreeBackedState( + config: IBeaconConfig, + state: CachedBeaconState +): TreeBacked { + const stateTB = ssz.phase0.BeaconState.createTreeBacked(state.tree); + + // TODO: Does this preserve the hashing cache? In altair devnets memory spikes on the fork transition + const postState = ssz.merge.BeaconState.createTreeBacked(stateTB.tree); + postState.fork = { + previousVersion: stateTB.fork.currentVersion, + currentVersion: config.MERGE_FORK_VERSION, + epoch: getCurrentEpoch(stateTB), + }; + // Execution-layer + postState.latestExecutionPayloadHeader = ssz.merge.ExecutionPayloadHeader.defaultTreeBacked(); + + return postState; +} diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 769298905aaa..17e2b86af7ff 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -58,7 +58,9 @@ export enum Bucket { altair_lightclientFinalizedCheckpoint = 33, // Epoch -> FinalizedCheckpointData // Note: this is the state root for the checkpoint block, NOT necessarily the state root at the epoch boundary altair_lightClientInitProof = 34, // Epoch -> Proof - altair_lightClientSyncCommitteeProof = 35, // SyncPeriod -> Proof + altair_lightClientSyncCommitteeProof = 35, // SyncPeriod -> + + merge_totalTerminalDifficulty = 40, // Single item -> Uint256 } export enum Key { diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 977877ceb129..910170ce6c16 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -75,7 +75,7 @@ export class ForkChoice implements IForkChoice { private readonly config: IChainForkConfig, private readonly fcStore: IForkChoiceStore, /** Nullable until merge time comes */ - private readonly transitionStore: ITransitionStore | null, + private transitionStore: ITransitionStore | null, /** The underlying representation of the block DAG. */ private readonly protoArray: ProtoArray, /** @@ -91,6 +91,12 @@ export class ForkChoice implements IForkChoice { this.head = this.updateHead(); } + /** For merge transition. Initialize transition store when merge fork condition is met */ + initializeTransitionStore(transitionStore: ITransitionStore): void { + if (this.transitionStore !== null) throw Error("transitionStore already initialized"); + this.transitionStore = transitionStore; + } + /** * Returns the block root of an ancestor of `blockRoot` at the given `slot`. * (Note: `slot` refers to the block that is *returned*, not the one that is supplied.) diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index d443565e5987..48c52f3351af 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -1,6 +1,7 @@ import {Epoch, Slot, ValidatorIndex, phase0, allForks, Root, RootHex} from "@chainsafe/lodestar-types"; import {IProtoBlock} from "../protoArray/interface"; import {CheckpointWithHex} from "./store"; +import {ITransitionStore} from "./transitionStore"; export type CheckpointHex = { epoch: Epoch; @@ -8,6 +9,8 @@ export type CheckpointHex = { }; export interface IForkChoice { + /** For merge transition. Initialize transition store when merge fork condition is met */ + initializeTransitionStore(transitionStore: ITransitionStore): void; /** * Returns the block root of an ancestor of `block_root` at the given `slot`. (Note: `slot` refers * to the block that is *returned*, not the one that is supplied.) diff --git a/packages/fork-choice/src/index.ts b/packages/fork-choice/src/index.ts index 73afa12793c6..51403608ec53 100644 --- a/packages/fork-choice/src/index.ts +++ b/packages/fork-choice/src/index.ts @@ -4,6 +4,7 @@ export {IProtoBlock, IProtoNode} from "./protoArray/interface"; export {ForkChoice} from "./forkChoice/forkChoice"; export {IForkChoice} from "./forkChoice/interface"; export {ForkChoiceStore, IForkChoiceStore, CheckpointWithHex} from "./forkChoice/store"; +export {ITransitionStore} from "./forkChoice/transitionStore"; export {InvalidAttestation, InvalidAttestationCode, InvalidBlock, InvalidBlockCode} from "./forkChoice/errors"; export {IForkChoiceMetrics} from "./metrics"; diff --git a/packages/lodestar/src/chain/chain.ts b/packages/lodestar/src/chain/chain.ts index c40ed885994c..26e4130d55be 100644 --- a/packages/lodestar/src/chain/chain.ts +++ b/packages/lodestar/src/chain/chain.ts @@ -6,7 +6,7 @@ import fs from "fs"; import {ForkName} from "@chainsafe/lodestar-params"; import {CachedBeaconState, computeStartSlotAtEpoch} from "@chainsafe/lodestar-beacon-state-transition"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; -import {IForkChoice} from "@chainsafe/lodestar-fork-choice"; +import {IForkChoice, ITransitionStore} from "@chainsafe/lodestar-fork-choice"; import {allForks, ForkDigest, Number64, Root, phase0, Slot} from "@chainsafe/lodestar-types"; import {ILogger} from "@chainsafe/lodestar-utils"; import {fromHexString, TreeBacked} from "@chainsafe/ssz"; @@ -44,14 +44,6 @@ import {ForkDigestContext, IForkDigestContext} from "../util/forkDigestContext"; import {LightClientIniter} from "./lightClient"; import {Archiver} from "./archiver"; -export interface IBeaconChainModules { - config: IBeaconConfig; - db: IBeaconDb; - logger: ILogger; - metrics: IMetrics | null; - anchorState: TreeBacked; -} - export class BeaconChain implements IBeaconChain { readonly genesisTime: Number64; readonly genesisValidatorsRoot: Root; @@ -96,7 +88,24 @@ export class BeaconChain implements IBeaconChain { private readonly archiver: Archiver; private abortController = new AbortController(); - constructor(opts: IChainOptions, {config, db, logger, metrics, anchorState}: IBeaconChainModules) { + constructor( + opts: IChainOptions, + { + config, + db, + logger, + metrics, + anchorState, + transitionStore, + }: { + config: IBeaconConfig; + db: IBeaconDb; + logger: ILogger; + metrics: IMetrics | null; + anchorState: TreeBacked; + transitionStore: ITransitionStore | null; + } + ) { this.opts = opts; this.config = config; this.db = db; @@ -117,7 +126,7 @@ export class BeaconChain implements IBeaconChain { const stateCache = new StateContextCache({metrics}); const checkpointStateCache = new CheckpointStateCache({metrics}); const cachedState = restoreStateCaches(config, stateCache, checkpointStateCache, anchorState); - const forkChoice = initializeForkChoice(config, emitter, clock.currentSlot, cachedState, metrics); + const forkChoice = initializeForkChoice(config, transitionStore, emitter, clock.currentSlot, cachedState, metrics); const regen = new QueuedStateRegenerator({ config, emitter, diff --git a/packages/lodestar/src/chain/forkChoice/index.ts b/packages/lodestar/src/chain/forkChoice/index.ts index a92c26fd2907..fa18524d3219 100644 --- a/packages/lodestar/src/chain/forkChoice/index.ts +++ b/packages/lodestar/src/chain/forkChoice/index.ts @@ -5,19 +5,25 @@ import {toHexString} from "@chainsafe/ssz"; import {allForks, Slot} from "@chainsafe/lodestar-types"; import {IChainForkConfig} from "@chainsafe/lodestar-config"; -import {ForkChoice, ProtoArray, ForkChoiceStore} from "@chainsafe/lodestar-fork-choice"; +import {ForkChoice, ProtoArray, ForkChoiceStore, ITransitionStore} from "@chainsafe/lodestar-fork-choice"; import {computeAnchorCheckpoint} from "../initState"; import {ChainEventEmitter} from "../emitter"; import {getEffectiveBalances, CachedBeaconState} from "@chainsafe/lodestar-beacon-state-transition"; import {IMetrics} from "../../metrics"; import {ChainEvent} from "../emitter"; +import {IBeaconDb} from "../../db"; + +export type ForkChoiceOpts = { + terminalTotalDifficulty?: bigint; +}; /** * Fork Choice extended with a ChainEventEmitter */ export function initializeForkChoice( config: IChainForkConfig, + transitionStore: ITransitionStore | null, emitter: ChainEventEmitter, currentSlot: Slot, state: CachedBeaconState, @@ -37,9 +43,6 @@ export function initializeForkChoice( // TODO - PERFORMANCE WARNING - NAIVE CODE const justifiedBalances = getEffectiveBalances(state); - // TODO: Create and persist transition store - const transitionStore = null; - return new ForkChoice( config, @@ -65,3 +68,19 @@ export function initializeForkChoice( metrics ); } + +/** + * Initialize TransitionStore with locally persisted value, overriding it with user provided option. + */ +export async function initializeTransitionStore(opts: ForkChoiceOpts, db: IBeaconDb): Promise { + if (opts.terminalTotalDifficulty !== undefined) { + return {terminalTotalDifficulty: opts.terminalTotalDifficulty}; + } + + const terminalTotalDifficulty = await db.totalTerminalDifficulty.get(); + if (terminalTotalDifficulty !== null) { + return {terminalTotalDifficulty: opts.terminalTotalDifficulty ?? terminalTotalDifficulty}; + } + + return null; +} diff --git a/packages/lodestar/src/chain/options.ts b/packages/lodestar/src/chain/options.ts index 4c2fb69c5b18..27cf6cbe4922 100644 --- a/packages/lodestar/src/chain/options.ts +++ b/packages/lodestar/src/chain/options.ts @@ -1,11 +1,13 @@ import {BlockProcessOpts} from "./blocks/process"; +import {ForkChoiceOpts} from "./forkChoice"; // eslint-disable-next-line @typescript-eslint/ban-types -export type IChainOptions = BlockProcessOpts & { - useSingleThreadVerifier?: boolean; - persistInvalidSszObjects?: boolean; - persistInvalidSszObjectsDir: string; -}; +export type IChainOptions = BlockProcessOpts & + ForkChoiceOpts & { + useSingleThreadVerifier?: boolean; + persistInvalidSszObjects?: boolean; + persistInvalidSszObjectsDir: string; + }; export const defaultChainOptions: IChainOptions = { useSingleThreadVerifier: false, diff --git a/packages/lodestar/src/db/beacon.ts b/packages/lodestar/src/db/beacon.ts index 17710d56ae1c..e8c6389638dc 100644 --- a/packages/lodestar/src/db/beacon.ts +++ b/packages/lodestar/src/db/beacon.ts @@ -25,6 +25,7 @@ import { PreGenesisStateLastProcessedBlock, LatestFinalizedUpdate, LatestNonFinalizedUpdate, + TotalTerminalDifficulty, } from "./single"; import {PendingBlockRepository} from "./repositories/pendingBlock"; @@ -53,6 +54,7 @@ export class BeaconDb extends DatabaseService implements IBeaconDb { lightclientFinalizedCheckpoint: LightclientFinalizedCheckpoint; lightClientInitProof: LightClientInitProofRepository; lightClientSyncCommitteeProof: LightClientSyncCommitteeProofRepository; + totalTerminalDifficulty: TotalTerminalDifficulty; constructor(opts: IDatabaseApiOptions) { super(opts); @@ -81,6 +83,8 @@ export class BeaconDb extends DatabaseService implements IBeaconDb { this.db, this.metrics ); + // merge + this.totalTerminalDifficulty = new TotalTerminalDifficulty(this.config, this.db, this.metrics); } /** diff --git a/packages/lodestar/src/db/interface.ts b/packages/lodestar/src/db/interface.ts index 8839cc7a6758..899b4b33add3 100644 --- a/packages/lodestar/src/db/interface.ts +++ b/packages/lodestar/src/db/interface.ts @@ -25,6 +25,7 @@ import { PreGenesisStateLastProcessedBlock, LatestFinalizedUpdate, LatestNonFinalizedUpdate, + TotalTerminalDifficulty, } from "./single"; import {PendingBlockRepository} from "./repositories/pendingBlock"; @@ -69,6 +70,7 @@ export interface IBeaconDb { lightclientFinalizedCheckpoint: LightclientFinalizedCheckpoint; lightClientInitProof: LightClientInitProofRepository; lightClientSyncCommitteeProof: LightClientSyncCommitteeProofRepository; + totalTerminalDifficulty: TotalTerminalDifficulty; processBlockOperations(signedBlock: allForks.SignedBeaconBlock): Promise; diff --git a/packages/lodestar/src/db/single/index.ts b/packages/lodestar/src/db/single/index.ts index f1be22cb9a60..d90cc4400b13 100644 --- a/packages/lodestar/src/db/single/index.ts +++ b/packages/lodestar/src/db/single/index.ts @@ -2,3 +2,4 @@ export * from "./latestFinalizedUpdate"; export * from "./latestNonFinalizedUpdate"; export * from "./preGenesisState"; export * from "./preGenesisStateLastProcessedBlock"; +export {TotalTerminalDifficulty} from "./totalTerminalDifficulty"; diff --git a/packages/lodestar/src/db/single/totalTerminalDifficulty.ts b/packages/lodestar/src/db/single/totalTerminalDifficulty.ts new file mode 100644 index 000000000000..e5eb172daa0f --- /dev/null +++ b/packages/lodestar/src/db/single/totalTerminalDifficulty.ts @@ -0,0 +1,33 @@ +import {Type} from "@chainsafe/ssz"; +import {IChainForkConfig} from "@chainsafe/lodestar-config"; +import {ssz} from "@chainsafe/lodestar-types"; +import {IDatabaseController, Bucket, IDbMetrics} from "@chainsafe/lodestar-db"; + +export class TotalTerminalDifficulty { + private readonly bucket = Bucket.merge_totalTerminalDifficulty; + private readonly key = Buffer.from(new Uint8Array([this.bucket])); + private readonly type: Type; + private readonly db: IDatabaseController; + private readonly metrics?: IDbMetrics; + + constructor(config: IChainForkConfig, db: IDatabaseController, metrics?: IDbMetrics) { + this.db = db; + this.type = ssz.Uint256; + this.metrics = metrics; + } + + async put(value: bigint): Promise { + this.metrics?.dbWrites.labels({bucket: "merge_totalTerminalDifficulty"}).inc(); + await this.db.put(this.key, this.type.serialize(value) as Buffer); + } + + async get(): Promise { + this.metrics?.dbReads.labels({bucket: "merge_totalTerminalDifficulty"}).inc(); + const value = await this.db.get(this.key); + return value ? this.type.deserialize(value) : null; + } + + async delete(): Promise { + await this.db.delete(this.key); + } +} diff --git a/packages/lodestar/src/node/nodejs.ts b/packages/lodestar/src/node/nodejs.ts index ce288c77c55e..20e1b7cbac91 100644 --- a/packages/lodestar/src/node/nodejs.ts +++ b/packages/lodestar/src/node/nodejs.ts @@ -14,7 +14,7 @@ import {Api} from "@chainsafe/lodestar-api"; import {IBeaconDb} from "../db"; import {INetwork, Network, getReqRespHandlers} from "../network"; import {BeaconSync, IBeaconSync} from "../sync"; -import {BeaconChain, IBeaconChain, initBeaconMetrics} from "../chain"; +import {BeaconChain, IBeaconChain, initBeaconMetrics, initializeTransitionStore} from "../chain"; import {createMetrics, IMetrics, HttpMetricsServer} from "../metrics"; import {getApi, RestApi} from "../api"; import {IBeaconNodeOptions} from "./options"; @@ -131,6 +131,7 @@ export class BeaconNode { logger: logger.child(opts.logger.chain), metrics, anchorState, + transitionStore: await initializeTransitionStore(opts.chain, db), }); // Load persisted data from disk to in-memory caches diff --git a/packages/lodestar/test/unit/chain/forkChoice/forkChoice.test.ts b/packages/lodestar/test/unit/chain/forkChoice/forkChoice.test.ts index 6aed48e64daa..8891a466b216 100644 --- a/packages/lodestar/test/unit/chain/forkChoice/forkChoice.test.ts +++ b/packages/lodestar/test/unit/chain/forkChoice/forkChoice.test.ts @@ -46,7 +46,8 @@ describe("LodestarForkChoice", function () { beforeEach(() => { const emitter = new ChainEventEmitter(); const currentSlot = 40; - forkChoice = initializeForkChoice(config, emitter, currentSlot, state); + const transitionStore = null; + forkChoice = initializeForkChoice(config, transitionStore, emitter, currentSlot, state); }); describe("forkchoice", function () { diff --git a/packages/lodestar/test/utils/mocks/chain/chain.ts b/packages/lodestar/test/utils/mocks/chain/chain.ts index af27566be5e4..2e4783a5f02f 100644 --- a/packages/lodestar/test/utils/mocks/chain/chain.ts +++ b/packages/lodestar/test/utils/mocks/chain/chain.ts @@ -204,6 +204,7 @@ function mockForkChoice(): IForkChoice { const checkpoint: CheckpointWithHex = {epoch: 0, root, rootHex}; return { + initializeTransitionStore: () => {}, getAncestor: () => rootHex, getHeadRoot: () => rootHex, getHead: () => block, diff --git a/packages/spec-test-runner/test/spec/allForks/forkChoice.ts b/packages/spec-test-runner/test/spec/allForks/forkChoice.ts index 15146f3e78e0..c2de6231b17d 100644 --- a/packages/spec-test-runner/test/spec/allForks/forkChoice.ts +++ b/packages/spec-test-runner/test/spec/allForks/forkChoice.ts @@ -46,7 +46,8 @@ export function forkChoiceTest(fork: ForkName): void { let state = createCachedBeaconState(config, tbState); const emitter = new ChainEventEmitter(); - const forkchoice = initializeForkChoice(config, emitter, currentSlot, state); + const transitionStore = null; + const forkchoice = initializeForkChoice(config, transitionStore, emitter, currentSlot, state); const checkpointStateCache = new CheckpointStateCache({}); const stateCache = new Map>();