import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import {generateKeyPair} from "@libp2p/crypto/keys"; import type {PrivateKey} from "@libp2p/interface"; import {peerIdFromPrivateKey} from "@libp2p/peer-id"; import {Multiaddr} from "@multiformats/multiaddr"; import {SignableENR} from "@chainsafe/enr"; import {defaultOptions} from "@lodestar/beacon-node"; import {Logger} from "@lodestar/utils"; import {exportToJSON, readPrivateKey} from "../../config/index.js"; import {parseListenArgs} from "../../options/beaconNodeOptions/network.js"; import {writeFile600Perm} from "../../util/file.js"; import {BeaconArgs} from "./options.js"; /** * Check if multiaddr belongs to the local network interfaces. */ export function isLocalMultiAddr(multiaddr: Multiaddr | undefined): boolean { if (!multiaddr) return false; const components = multiaddr.getComponents(); if (components.length !== 2 && components[1].name !== "udp") { throw new Error("Invalid udp multiaddr"); } const interfaces = os.networkInterfaces(); const family = components[0].name === "ip4" ? 4 : 6; const ipStr = components[0].value; if (!ipStr) { return false; } for (const networkInterfaces of Object.values(interfaces)) { for (const networkInterface of networkInterfaces || []) { // since node version 18, the netowrkinterface family returns 4 | 6 instead of ipv4 | ipv6, // even though the documentation says otherwise. // This might be a bug that would be corrected in future version, in the meantime // the check using endsWith ensures things work in node version 18 and earlier if (String(networkInterface.family).endsWith(String(family)) && networkInterface.address === ipStr) { return true; } } } return false; } /** * Only update the enr if the value has changed */ function maybeUpdateEnr( enr: SignableENR, key: T, value: SignableENR[T] | undefined ): void { if (enr[key] !== value) { enr[key] = value; } } export function overwriteEnrWithCliArgs( enr: SignableENR, args: BeaconArgs, logger: Logger, opts?: {newEnr?: boolean; bootnode?: boolean} ): void { const preSeq = enr.seq; const {port, discoveryPort, quicPort, port6, discoveryPort6, quicPort6} = parseListenArgs(args); const tcp = args.tcp ?? defaultOptions.network.tcp; const quic = args.quic ?? defaultOptions.network.quic; maybeUpdateEnr(enr, "ip", args["enr.ip"] ?? enr.ip); maybeUpdateEnr(enr, "ip6", args["enr.ip6"] ?? enr.ip6); maybeUpdateEnr(enr, "udp", args["enr.udp"] ?? discoveryPort ?? enr.udp); maybeUpdateEnr(enr, "udp6", args["enr.udp6"] ?? discoveryPort6 ?? enr.udp6); if (!opts?.bootnode) { maybeUpdateEnr(enr, "tcp", tcp ? (args["enr.tcp"] ?? port ?? enr.tcp) : undefined); maybeUpdateEnr(enr, "tcp6", tcp ? (args["enr.tcp6"] ?? port6 ?? enr.tcp6) : undefined); maybeUpdateEnr(enr, "quic", quic ? (args["enr.quic"] ?? quicPort ?? enr.quic) : undefined); maybeUpdateEnr(enr, "quic6", quic ? (args["enr.quic6"] ?? quicPort6 ?? enr.quic6) : undefined); } function testMultiaddrForLocal(mu: Multiaddr, ip4: boolean): void { const isLocal = isLocalMultiAddr(mu); if (args.nat) { if (isLocal) { logger.warn("--nat flag is set with no purpose"); } } else { if (!isLocal) { logger.warn( `Configured ENR ${ip4 ? "IPv4" : "IPv6"} address is not local, clearing ENR ${ip4 ? "ip" : "ip6"} and ${ ip4 ? "udp" : "udp6" }. Set the --nat flag to prevent this` ); if (ip4) { enr.delete("ip"); enr.delete("udp"); enr.delete("tcp"); enr.delete("quic"); } else { enr.delete("ip6"); enr.delete("udp6"); enr.delete("tcp6"); enr.delete("quic6"); } } } } const udpMultiaddr4 = enr.getLocationMultiaddr("udp4"); if (udpMultiaddr4) { testMultiaddrForLocal(udpMultiaddr4, true); } const udpMultiaddr6 = enr.getLocationMultiaddr("udp6"); if (udpMultiaddr6) { testMultiaddrForLocal(udpMultiaddr6, false); } if (enr.seq !== preSeq) { // If the enr is newly created, its sequence number can be set to 1 // It's especially clean for fully configured bootnodes whose enrs never change // Otherwise, we can increment the sequence number as little as possible if (opts?.newEnr) { enr.seq = BigInt(1); } else { enr.seq = preSeq + BigInt(1); } // invalidate cached signature // biome-ignore lint/complexity/useLiteralKeys: `_signature` is a private attribute delete enr["_signature"]; } } /** * Create new PeerId and ENR by default, unless persistNetworkIdentity is provided */ export async function initPrivateKeyAndEnr( args: BeaconArgs, beaconDir: string, logger: Logger, bootnode?: boolean ): Promise<{privateKey: PrivateKey; enr: SignableENR}> { const {persistNetworkIdentity} = args; const newPrivateKeyAndENR = async (): Promise<{privateKey: PrivateKey; enr: SignableENR}> => { const privateKey = await generateKeyPair("secp256k1"); const enr = SignableENR.createFromPrivateKey(privateKey); return {privateKey, enr}; }; const readPersistedPrivateKeyAndENR = async ( peerIdFile: string, enrFile: string ): Promise<{privateKey: PrivateKey; enr: SignableENR; newEnr: boolean}> => { let privateKey: PrivateKey; let enr: SignableENR; // attempt to read stored private key try { privateKey = readPrivateKey(peerIdFile); } catch (e) { if ((e as {code: string}).code === "ENOENT") { logger.debug("peerIdFile not found, creating a new peer id", {peerIdFile}); } else { logger.warn("Unable to read peerIdFile, creating a new peer id", {peerIdFile}, e as Error); } return {...(await newPrivateKeyAndENR()), newEnr: true}; } // attempt to read stored enr try { enr = SignableENR.decodeTxt(fs.readFileSync(enrFile, "utf-8"), privateKey.raw); } catch (_e) { logger.warn("Unable to decode stored local ENR, creating a new ENR"); enr = SignableENR.createFromPrivateKey(privateKey); return {privateKey, enr, newEnr: true}; } // check stored peer id against stored enr if (!peerIdFromPrivateKey(privateKey).equals(enr.peerId)) { logger.warn("Stored local ENR doesn't match peerIdFile, creating a new ENR"); enr = SignableENR.createFromPrivateKey(privateKey); return {privateKey, enr, newEnr: true}; } return {privateKey, enr, newEnr: false}; }; if (persistNetworkIdentity) { const enrFile = path.join(beaconDir, "enr"); const peerIdFile = path.join(beaconDir, "peer-id.json"); const {privateKey, enr, newEnr} = await readPersistedPrivateKeyAndENR(peerIdFile, enrFile); overwriteEnrWithCliArgs(enr, args, logger, {newEnr, bootnode}); // Re-persist peer-id and enr writeFile600Perm(peerIdFile, exportToJSON(privateKey)); writeFile600Perm(enrFile, enr.encodeTxt()); return {privateKey, enr}; } const {privateKey, enr} = await newPrivateKeyAndENR(); overwriteEnrWithCliArgs(enr, args, logger, {newEnr: true, bootnode}); return {privateKey, enr}; }