@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
379 lines (348 loc) • 11.6 kB
text/typescript
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;
}
}
}