UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

379 lines (348 loc) • 11.6 kB
import {setMaxListeners} from "node:events"; import {PrivateKey} from "@libp2p/interface"; import {Registry} from "prom-client"; import {hasher} from "@chainsafe/persistent-merkle-tree"; import {BeaconApiMethods} from "@lodestar/api/beacon/server"; import {BeaconConfig} from "@lodestar/config"; import type {LoggerNode} from "@lodestar/logger/node"; import {ZERO_HASH_HEX} from "@lodestar/params"; import {IBeaconStateView, PubkeyCache, isStatePostBellatrix, isStatePostGloas} from "@lodestar/state-transition"; import {phase0} from "@lodestar/types"; import {sleep, toRootHex} from "@lodestar/utils"; import {ProcessShutdownCallback} from "@lodestar/validator"; import {BeaconRestApiServer, getApi} from "../api/index.js"; import {BeaconChain, IBeaconChain, initBeaconMetrics} from "../chain/index.js"; import {ValidatorMonitor, createValidatorMonitor} from "../chain/validatorMonitor.js"; import {IBeaconDb} from "../db/index.js"; import {initializeExecutionBuilder, initializeExecutionEngine} from "../execution/index.js"; import {HttpMetricsServer, Metrics, createMetrics, getHttpMetricsServer} from "../metrics/index.js"; import {MonitoringService} from "../monitoring/index.js"; import {Network, getReqRespHandlers} from "../network/index.js"; import {BackfillSync} from "../sync/backfill/index.js"; import {BeaconSync, IBeaconSync} from "../sync/index.js"; import {Clock} from "../util/clock.js"; import {runNodeNotifier} from "./notifier.js"; import {IBeaconNodeOptions} from "./options.js"; export * from "./options.js"; export type BeaconNodeModules = { opts: IBeaconNodeOptions; config: BeaconConfig; db: IBeaconDb; metrics: Metrics | null; validatorMonitor: ValidatorMonitor | null; network: Network; chain: IBeaconChain; api: BeaconApiMethods; sync: IBeaconSync; backfillSync: BackfillSync | null; metricsServer: HttpMetricsServer | null; monitoring: MonitoringService | null; restApi?: BeaconRestApiServer; controller?: AbortController; }; export type BeaconNodeInitModules = { opts: IBeaconNodeOptions; config: BeaconConfig; pubkeyCache: PubkeyCache; db: IBeaconDb; logger: LoggerNode; processShutdownCallback: ProcessShutdownCallback; privateKey: PrivateKey; dataDir: string; peerStoreDir?: string; anchorState: IBeaconStateView; isAnchorStateFinalized: boolean; wsCheckpoint?: phase0.Checkpoint; metricsRegistries?: Registry[]; }; export enum BeaconNodeStatus { started = "started", closing = "closing", closed = "closed", } enum LoggerModule { api = "api", backfill = "backfill", chain = "chain", execution = "execution", metrics = "metrics", monitoring = "monitoring", network = "network", /** validator monitor */ vmon = "vmon", rest = "rest", sync = "sync", } /** * Short delay before closing db to give async operations sufficient time to complete * and prevent "Database is not open" errors when shutting down beacon node. */ const DELAY_BEFORE_CLOSING_DB_MS = 500; /** * The main Beacon Node class. Contains various components for getting and processing data from the * Ethereum Consensus ecosystem as well as systems for getting beacon node metadata. */ export class BeaconNode { opts: IBeaconNodeOptions; config: BeaconConfig; db: IBeaconDb; metrics: Metrics | null; metricsServer: HttpMetricsServer | null; monitoring: MonitoringService | null; validatorMonitor: ValidatorMonitor | null; network: Network; chain: IBeaconChain; api: BeaconApiMethods; restApi?: BeaconRestApiServer; sync: IBeaconSync; backfillSync: BackfillSync | null; status: BeaconNodeStatus; private controller?: AbortController; constructor({ opts, config, db, metrics, metricsServer, monitoring, validatorMonitor, network, chain, api, restApi, sync, backfillSync, controller, }: BeaconNodeModules) { this.opts = opts; this.config = config; this.metrics = metrics; this.metricsServer = metricsServer; this.monitoring = monitoring; this.validatorMonitor = validatorMonitor; this.db = db; this.chain = chain; this.api = api; this.restApi = restApi; this.network = network; this.sync = sync; this.backfillSync = backfillSync; this.controller = controller; this.status = BeaconNodeStatus.started; } /** * Initialize a beacon node. Initializes and `start`s the varied sub-component services of the * beacon node */ static async init<T extends BeaconNode = BeaconNode>({ opts, config, pubkeyCache, db, logger, processShutdownCallback, privateKey, dataDir, peerStoreDir, anchorState, isAnchorStateFinalized, wsCheckpoint, metricsRegistries = [], }: BeaconNodeInitModules): Promise<T> { if (hasher.name !== "hashtree") { logger.warn(`hashtree is not supported, using hasher ${hasher.name}`); } const controller = new AbortController(); // We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10 // Since it is perfectly fine to have listeners > 10 setMaxListeners(Infinity, controller.signal); const signal = controller.signal; let metrics = null; if ( opts.metrics.enabled || // monitoring relies on metrics data opts.monitoring.endpoint ) { metrics = createMetrics(opts.metrics, anchorState.genesisTime, metricsRegistries); initBeaconMetrics(metrics, anchorState); // Since the db is instantiated before this, metrics must be injected manually afterwards db.setMetrics(metrics.db); signal.addEventListener("abort", metrics.close, {once: true}); } const validatorMonitor = opts.metrics.enabled || opts.validatorMonitor.validatorMonitorLogs ? createValidatorMonitor( metrics?.register ?? null, config, anchorState.genesisTime, logger.child({module: LoggerModule.vmon}), opts.validatorMonitor ) : null; const clock = new Clock({config, genesisTime: anchorState.genesisTime, signal}); // Prune hot db repos // TODO: Should this call be awaited? await db.pruneHotDb(); // Delete deprecated eth1 data to free up disk space for users logger.debug("Deleting deprecated eth1 data from database"); const startTime = Date.now(); db.deleteDeprecatedEth1Data() .then(() => { logger.debug("Deleted deprecated eth1 data", {durationMs: Date.now() - startTime}); }) .catch((e) => { logger.error("Failed to delete deprecated eth1 data", {}, e); }); const monitoring = opts.monitoring.endpoint ? new MonitoringService( "beacon", {...opts.monitoring, endpoint: opts.monitoring.endpoint}, {register: (metrics as Metrics).register, logger: logger.child({module: LoggerModule.monitoring})} ) : null; let executionEngineOpts = opts.executionEngine; if (opts.executionEngine.mode === "mock") { const latestEth1BlockHash = isStatePostBellatrix(anchorState) && anchorState.isExecutionStateType ? isStatePostGloas(anchorState) ? toRootHex(anchorState.latestBlockHash) : toRootHex(anchorState.latestExecutionPayloadHeader.blockHash) : undefined; executionEngineOpts = { ...opts.executionEngine, genesisBlockHash: ZERO_HASH_HEX, eth1BlockHash: opts.executionEngine.eth1BlockHash ?? latestEth1BlockHash, genesisTime: anchorState.genesisTime, config, }; } const chain = new BeaconChain(opts.chain, { privateKey, config, clock, pubkeyCache, dataDir, db, dbName: opts.db.name, logger: logger.child({module: LoggerModule.chain}), processShutdownCallback, metrics, validatorMonitor, anchorState, isAnchorStateFinalized, executionEngine: initializeExecutionEngine(executionEngineOpts, { metrics, signal, logger: logger.child({module: LoggerModule.execution}), }), executionBuilder: opts.executionBuilder.enabled ? initializeExecutionBuilder(opts.executionBuilder, config, metrics, logger) : undefined, }); // Load persisted data from disk to in-memory caches await chain.init(); // Network needs to be initialized before the sync // See https://github.com/ChainSafe/lodestar/issues/4543 const network = await Network.init({ opts: opts.network, config, logger: logger.child({module: LoggerModule.network}), metrics, chain, db, privateKey, peerStoreDir, getReqRespHandler: getReqRespHandlers({db, chain}), }); const sync = new BeaconSync(opts.sync, { config, db, chain, metrics, network, wsCheckpoint, logger: logger.child({module: LoggerModule.sync}), }); const backfillSync = opts.sync.backfillBatchSize > 0 ? await BackfillSync.init(opts.sync, { config, db, chain, metrics, network, wsCheckpoint, anchorState, logger: logger.child({module: LoggerModule.backfill}), signal, }) : null; const api = getApi(opts.api, { config, logger: logger.child({module: LoggerModule.api}), db, sync, network, chain, metrics, }); // only start server if metrics are explicitly enabled const metricsServer = opts.metrics.enabled ? await getHttpMetricsServer(opts.metrics, { register: (metrics as Metrics).register, getOtherMetrics: async () => Promise.all([network.scrapeMetrics(), chain.archiveStore.scrapeMetrics()]), logger: logger.child({module: LoggerModule.metrics}), }) : null; const restApi = new BeaconRestApiServer(opts.api.rest, { config, logger: logger.child({module: LoggerModule.rest}), api, metrics: metrics ? metrics.apiRest : null, }); if (opts.api.rest.enabled) { await restApi.registerRoutes(opts.api.version); await restApi.listen(); } void runNodeNotifier({network, chain, sync, config, logger, signal}); return new BeaconNode({ opts, config, db, metrics, metricsServer, monitoring, validatorMonitor, network, chain, api, restApi, sync, backfillSync, controller, }) as T; } /** * Stop beacon node and its sub-components. */ async close(): Promise<void> { if (this.status === BeaconNodeStatus.started) { this.status = BeaconNodeStatus.closing; this.sync.close(); this.backfillSync?.close(); if (this.restApi) await this.restApi.close(); await this.network.close(); if (this.metricsServer) await this.metricsServer.close(); if (this.monitoring) await this.monitoring.close(); await this.chain.persistToDisk(); await this.chain.close(); // Abort signal last: close() calls above clear intervals/timeouts so no new // operations get scheduled. If we aborted first, a still-pending interval could // fire and schedule a new operation after abort, leaving it stuck and delaying shutdown. if (this.controller) this.controller.abort(); await sleep(DELAY_BEFORE_CLOSING_DB_MS); await this.db.close(); this.status = BeaconNodeStatus.closed; } } }