import path from "node:path"; import {PrivateKey} from "@libp2p/interface"; import {Multiaddr, multiaddr} from "@multiformats/multiaddr"; import {Discv5, Discv5EventEmitter} from "@chainsafe/discv5"; import {ENR, SignableENR} from "@chainsafe/enr"; import { HttpMetricsServer, IBeaconNodeOptions, RegistryMetricCreator, getHttpMetricsServer, } from "@lodestar/beacon-node"; import {ErrorAborted, Logger} from "@lodestar/utils"; import {getBeaconConfigFromArgs} from "../../config/index.js"; import {getNetworkBootnodes, isKnownNetworkName, readBootnodes} from "../../networks/index.js"; import {parseArgs as parseMetricsArgs} from "../../options/beaconNodeOptions/metrics.js"; import {parseArgs as parseNetworkArgs} from "../../options/beaconNodeOptions/network.js"; import {GlobalArgs} from "../../options/index.js"; import {mkdir, onGracefulShutdown, writeFile600Perm} from "../../util/index.js"; import {getVersionData} from "../../util/version.js"; import {initLogger} from "../beacon/handler.js"; import {initPrivateKeyAndEnr} from "../beacon/initPeerIdAndEnr.js"; import {BeaconArgs} from "../beacon/options.js"; import {getBeaconPaths} from "../beacon/paths.js"; import {BootnodeArgs} from "./options.js"; /** * Runs a bootnode. */ export async function bootnodeHandler(args: BootnodeArgs & GlobalArgs): Promise { const {discv5Args, metricsArgs, bootnodeDir, network, version, commit, privateKey, enr, logger} = await bootnodeHandlerInit(args); const abortController = new AbortController(); const bindAddrs = discv5Args.bindAddrs; logger.info("Lodestar Bootnode", {network, version, commit}); logger.info("Bind address", bindAddrs); logger.info("Advertised address", { ip4: enr.getLocationMultiaddr("udp4")?.toString(), ip6: enr.getLocationMultiaddr("udp6")?.toString(), }); logger.info("Identity", {peerId: enr.peerId.toString(), nodeId: enr.nodeId}); logger.info("ENR", {enr: enr.encodeTxt()}); // bootnode setup try { let metricsRegistry: RegistryMetricCreator | undefined; let metricsServer: HttpMetricsServer | undefined; if (metricsArgs.enabled) { metricsRegistry = new RegistryMetricCreator(); metricsRegistry.static({ name: "bootnode_version", help: "Bootnode version", value: {version, commit, network}, }); metricsServer = await getHttpMetricsServer(metricsArgs, {register: metricsRegistry, logger}); } const discv5 = Discv5.create({ enr, privateKey, bindAddrs: { ip4: (bindAddrs.ip4 ? multiaddr(bindAddrs.ip4) : undefined) as Multiaddr, ip6: bindAddrs.ip6 ? multiaddr(bindAddrs.ip6) : undefined, }, config: {enrUpdate: !enr.ip && !enr.ip6}, metricsRegistry, }) as Discv5 & Discv5EventEmitter; // If there are any bootnodes, add them to the routing table for (const bootEnrStr of Array.from(new Set(discv5Args.bootEnrs).values())) { const bootEnr = ENR.decodeTxt(bootEnrStr); logger.info("Adding bootnode", { ip4: bootEnr.getLocationMultiaddr("udp4")?.toString(), ip6: bootEnr.getLocationMultiaddr("udp6")?.toString(), peerId: bootEnr.peerId.toString(), nodeId: enr.nodeId, }); discv5.addEnr(bootEnr); } // start the server await discv5.start(); // if there are peers in the local routing table, establish a session by running a query if (discv5.kadValues().length) { void discv5.findRandomNode(); } discv5.on("multiaddrUpdated", (addr: Multiaddr) => { logger.info("Advertised socket address updated", {addr: addr.toString()}); }); // respond with metrics every 10 seconds const printInterval = setInterval(() => { let ip4Only = 0; let ip6Only = 0; let ip4ip6 = 0; let unreachable = 0; for (const kadEnr of discv5.kadValues()) { const hasIp4 = kadEnr.getLocationMultiaddr("udp4"); const hasIp6 = kadEnr.getLocationMultiaddr("udp6"); if (hasIp4 && hasIp6) { ip4ip6++; } else if (hasIp4) { ip4Only++; } else if (hasIp6) { ip6Only++; } else { unreachable++; } } logger.info("Server metrics", { connectedPeers: discv5.connectedPeerCount, activeSessions: discv5.sessionService.sessionsSize(), ip4Nodes: ip4Only, ip6Nodes: ip6Only, ip4AndIp6Nodes: ip4ip6, unreachableNodes: unreachable, }); }, 10_000); // Intercept SIGINT signal, to perform final ops before exiting onGracefulShutdown(async () => { if (args.persistNetworkIdentity) { try { const enrPath = path.join(bootnodeDir, "enr"); writeFile600Perm(enrPath, enr.encodeTxt()); } catch (e) { logger.warn("Unable to persist enr", {}, e as Error); } } abortController.abort(); }, logger.info.bind(logger)); abortController.signal.addEventListener( "abort", async () => { try { discv5.removeAllListeners(); clearInterval(printInterval); await metricsServer?.close(); await discv5.stop(); logger.debug("Bootnode closed"); } catch (e) { logger.error("Error closing bootnode", {}, e as Error); // Must explicitly exit process due to potential active handles process.exit(1); } }, {once: true} ); } catch (e) { if (e instanceof ErrorAborted) { logger.info(e.message); // Let the user know the abort was received but don't print as error } else { throw e; } } } export type BootNodeInitOptions = { discv5Args: Exclude; metricsArgs: IBeaconNodeOptions["metrics"]; bootnodeDir: string; network: string; version: string; commit: string; privateKey: PrivateKey; enr: SignableENR; logger: Logger; }; /** Separate function to simplify unit testing of options merging */ export async function bootnodeHandlerInit(args: BootnodeArgs & GlobalArgs): Promise { const {config, network} = getBeaconConfigFromArgs(args); const {version, commit} = getVersionData(); const beaconPaths = getBeaconPaths(args, network); // Use a separate directory to store bootnode enr + peer-id const bootnodeDir = path.join(beaconPaths.dataDir, "bootnode"); const {discv5: discv5Args} = parseNetworkArgs(args); const metricsArgs = parseMetricsArgs(args); if (!discv5Args) { // Unreachable because bootnode requires discv5 to be enabled - duh throw new Error("unreachable - bootnode requires discv5 to be enabled"); } // initialize directories mkdir(beaconPaths.dataDir); mkdir(bootnodeDir); // Fetch extra bootnodes discv5Args.bootEnrs = (discv5Args.bootEnrs ?? []).concat( args.bootnodesFile ? readBootnodes(args.bootnodesFile) : [], isKnownNetworkName(network) ? await getNetworkBootnodes(network) : [] ); const logger = initLogger(args, beaconPaths.dataDir, config, "bootnode.log"); const {privateKey, enr} = await initPrivateKeyAndEnr(args as unknown as BeaconArgs, bootnodeDir, logger, true); return {discv5Args, metricsArgs, bootnodeDir, network, version, commit, privateKey, enr, logger}; }