Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add bootnode cli command #5876

Merged
merged 4 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/cli/src/cmds/beacon/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
pruneOldFilesInDir,
} from "../../util/index.js";
import {getVersionData} from "../../util/version.js";
import {LogArgs} from "../../options/logOptions.js";
import {BeaconArgs} from "./options.js";
import {getBeaconPaths} from "./paths.js";
import {initBeaconState} from "./initBeaconState.js";
Expand Down Expand Up @@ -205,8 +206,13 @@ export async function beaconHandlerInit(args: BeaconArgs & GlobalArgs) {
return {config, options, beaconPaths, network, version, commit, peerId, logger};
}

export function initLogger(args: BeaconArgs, dataDir: string, config: ChainForkConfig): LoggerNode {
const defaultLogFilepath = path.join(dataDir, "beacon.log");
export function initLogger(
args: LogArgs & Pick<GlobalArgs, "dataDir">,
dataDir: string,
config: ChainForkConfig,
fileName = "beacon.log"
): LoggerNode {
const defaultLogFilepath = path.join(dataDir, fileName);
const logger = getNodeLogger(parseLoggerArgs(args, {defaultLogFilepath}, config));
try {
cleanOldLogFiles(args, {defaultLogFilepath});
Expand Down
139 changes: 139 additions & 0 deletions packages/cli/src/cmds/bootnode/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import path from "node:path";
import {Multiaddr, multiaddr} from "@multiformats/multiaddr";
import {Discv5} from "@chainsafe/discv5";
import {ErrorAborted} from "@lodestar/utils";
import {HttpMetricsServer, RegistryMetricCreator, getHttpMetricsServer} from "@lodestar/beacon-node";

import {GlobalArgs} from "../../options/index.js";
import {getBeaconConfigFromArgs} from "../../config/index.js";
import {getNetworkBootnodes, isKnownNetworkName, readBootnodes} from "../../networks/index.js";
import {onGracefulShutdown, mkdir, writeFile600Perm} from "../../util/index.js";
import {getVersionData} from "../../util/version.js";
import {initPeerIdAndEnr} from "../beacon/initPeerIdAndEnr.js";
import {parseArgs as parseMetricsArgs} from "../../options/beaconNodeOptions/metrics.js";
import {parseArgs as parseNetworkArgs} from "../../options/beaconNodeOptions/network.js";
import {getBeaconPaths} from "../beacon/paths.js";
import {BeaconArgs} from "../beacon/options.js";
import {initLogger} from "../beacon/handler.js";
import {BootnodeArgs} from "./options.js";

/**
* Runs a bootnode.
*/
export async function bootnodeHandler(args: BootnodeArgs & GlobalArgs): Promise<void> {
const {discv5Args, metricsArgs, bootnodeDir, network, version, commit, peerId, 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);
const advertisedAddrs = Object.fromEntries(
[
["ip4", enr.getLocationMultiaddr("udp4")?.toString()],
["ip6", enr.getLocationMultiaddr("udp6")?.toString()],
].filter(([_, v]) => v)
) as Record<string, string>;
logger.info("Advertised address", advertisedAddrs);
logger.info("Identity", {peerId: 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,
peerId,
bindAddrs: {
ip4: (bindAddrs.ip4 ? multiaddr(bindAddrs.ip4) : undefined) as Multiaddr,
ip6: bindAddrs.ip6 ? multiaddr(bindAddrs.ip6) : undefined,
},
config: {enrUpdate: !enr.ip && !enr.ip6},
metricsRegistry,
});
for (const bootEnr of discv5Args.bootEnrs) {
discv5.addEnr(bootEnr);
}

// Intercept SIGINT signal, to perform final ops before exiting
onGracefulShutdown(async () => {
if (args.persistNetworkIdentity) {
try {
const enrPath = path.join(bootnodeDir, "enr");
writeFile600Perm(enrPath, enr);
} catch (e) {
logger.warn("Unable to persist enr", {}, e as Error);
}
}
abortController.abort();
}, logger.info.bind(logger));

abortController.signal.addEventListener(
"abort",
async () => {
try {
await metricsServer?.close();
await discv5.stop();
logger.debug("Bootnode closed");
// Explicitly exit until active handles issue is resolved
// See https://github.com/ChainSafe/lodestar/issues/5642
process.exit(0);
wemeetagain marked this conversation as resolved.
Show resolved Hide resolved
} 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;
}
}
}

/** Separate function to simplify unit testing of options merging */
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export async function bootnodeHandlerInit(args: BootnodeArgs & GlobalArgs) {
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 {peerId, enr} = await initPeerIdAndEnr(args as unknown as BeaconArgs, bootnodeDir, logger);

return {discv5Args, metricsArgs, bootnodeDir, network, version, commit, peerId, enr, logger};
}
12 changes: 12 additions & 0 deletions packages/cli/src/cmds/bootnode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {CliCommand, CliCommandOptions} from "../../util/index.js";
import {GlobalArgs} from "../../options/index.js";
import {bootnodeOptions, BootnodeArgs} from "./options.js";
import {bootnodeHandler} from "./handler.js";

export const beacon: CliCommand<BootnodeArgs, GlobalArgs> = {
command: "bootnode",
describe:
"Start a discv5 bootnode. This will NOT perform any beacon node functions, rather, it will run a discv5 service that allows nodes on the network to discover one another.",
options: bootnodeOptions as CliCommandOptions<BootnodeArgs>,
handler: bootnodeHandler,
};
65 changes: 65 additions & 0 deletions packages/cli/src/cmds/bootnode/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {Options} from "yargs";
import {LogArgs, logOptions} from "../../options/logOptions.js";
import {CliCommandOptions} from "../../util/index.js";
import {NetworkArgs, options as networkOptions} from "../../options/beaconNodeOptions/network.js";
import {MetricsArgs, options as metricsOptions} from "../../options/beaconNodeOptions/metrics.js";

type BootnodeExtraArgs = {
bootnodesFile?: string;
persistNetworkIdentity?: boolean;
"enr.ip"?: string;
"enr.ip6"?: string;
"enr.udp"?: number;
"enr.udp6"?: number;
nat?: boolean;
};

export const bootnodeExtraOptions: CliCommandOptions<BootnodeExtraArgs> = {
wemeetagain marked this conversation as resolved.
Show resolved Hide resolved
bootnodesFile: {
hidden: true,
description: "Bootnodes file path",
type: "string",
},

persistNetworkIdentity: {
hidden: true,
description: "Whether to reuse the same peer-id across restarts",
default: true,
type: "boolean",
},

"enr.ip": {
description: "Override ENR IP entry",
type: "string",
group: "enr",
},
"enr.udp": {
description: "Override ENR UDP entry",
type: "number",
group: "enr",
},
"enr.ip6": {
description: "Override ENR IPv6 entry",
type: "string",
group: "enr",
},
"enr.udp6": {
description: "Override ENR (IPv6-specific) UDP entry",
type: "number",
group: "enr",
},
nat: {
type: "boolean",
description: "Allow configuration of non-local addresses",
group: "enr",
},
};

export type BootnodeArgs = BootnodeExtraArgs & LogArgs & NetworkArgs & MetricsArgs;

export const bootnodeOptions: {[k: string]: Options} = {
...bootnodeExtraOptions,
...logOptions,
...networkOptions,
...metricsOptions,
};
Loading