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

persistNetworkIdentity cli option #5175

Merged
merged 4 commits into from
Mar 2, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
70 changes: 28 additions & 42 deletions packages/cli/src/cmds/beacon/handler.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,34 @@
import path from "node:path";
import {Registry} from "prom-client";
import {createSecp256k1PeerId} from "@libp2p/peer-id-factory";
import {createKeypairFromPeerId, SignableENR} from "@chainsafe/discv5";
import {ErrorAborted} from "@lodestar/utils";
import {ErrorAborted, Logger} from "@lodestar/utils";
import {LevelDbController} from "@lodestar/db";
import {BeaconNode, BeaconDb} from "@lodestar/beacon-node";
import {createBeaconConfig} from "@lodestar/config";
import {ChainForkConfig, createBeaconConfig} from "@lodestar/config";
import {ACTIVE_PRESET, PresetName} from "@lodestar/params";
import {ProcessShutdownCallback} from "@lodestar/validator";

import {GlobalArgs, parseBeaconNodeArgs} from "../../options/index.js";
import {BeaconNodeOptions, exportToJSON, getBeaconConfigFromArgs} from "../../config/index.js";
import {BeaconNodeOptions, getBeaconConfigFromArgs} from "../../config/index.js";
import {getNetworkBootnodes, getNetworkData, isKnownNetworkName, readBootnodes} from "../../networks/index.js";
import {onGracefulShutdown, getCliLogger, mkdir, writeFile600Perm, cleanOldLogFiles} from "../../util/index.js";
import {getVersionData} from "../../util/version.js";
import {defaultP2pPort} from "../../options/beaconNodeOptions/network.js";
import {BeaconArgs} from "./options.js";
import {getBeaconPaths} from "./paths.js";
import {initBeaconState} from "./initBeaconState.js";
import {initPeerIdAndEnr} from "./initPeerIdAndEnr.js";

/**
* Runs a beacon node.
*/
export async function beaconHandler(args: BeaconArgs & GlobalArgs): Promise<void> {
const {config, options, beaconPaths, network, version, commit, peerId} = await beaconHandlerInit(args);
const {config, options, beaconPaths, network, version, commit, peerId, logger} = await beaconHandlerInit(args);

// initialize directories
mkdir(beaconPaths.dataDir);
mkdir(beaconPaths.beaconDir);
mkdir(beaconPaths.dbDir);

const abortController = new AbortController();
const {logger, logParams} = getCliLogger(
args,
{defaultLogFilepath: path.join(beaconPaths.dataDir, "beacon.log")},
config
);
try {
cleanOldLogFiles(logParams.filename, logParams.rotateMaxFiles);
} catch (e) {
logger.debug("Not able to delete log files", logParams, e as Error);
}

onGracefulShutdown(async () => {
abortController.abort();
}, logger.info.bind(logger));

logger.info("Lodestar", {network, version, commit});
// Callback for beacon to request forced exit, for e.g. in case of irrecoverable
Expand Down Expand Up @@ -97,6 +81,17 @@ export async function beaconHandler(args: BeaconArgs & GlobalArgs): Promise<void

if (args.attachToGlobalThis) ((globalThis as unknown) as {bn: BeaconNode}).bn = node;

onGracefulShutdown(async () => {
if (args.persistNetworkIdentity) {
const enr = await node.network.getEnr().catch((e) => logger.warn("Unable to persist enr", {}, e));
if (enr) {
const enrPath = path.join(beaconPaths.beaconDir, "enr");
writeFile600Perm(enrPath, enr.encodeTxt());
}
}
abortController.abort();
}, logger.info.bind(logger));

abortController.signal.addEventListener("abort", () => node.close(), {once: true});
} catch (e) {
await db.stop();
Expand Down Expand Up @@ -139,17 +134,8 @@ export async function beaconHandlerInit(args: BeaconArgs & GlobalArgs) {
beaconNodeOptions.set({eth1: {depositContractDeployBlock}});
}

// Create new PeerId everytime by default, unless peerIdFile is provided
const peerId = await createSecp256k1PeerId();
const enr = SignableENR.createV4(createKeypairFromPeerId(peerId));
overwriteEnrWithCliArgs(enr, args);

// Persist ENR and PeerId in beaconDir fixed paths for debugging
const pIdPath = path.join(beaconPaths.beaconDir, "peer_id.json");
const enrPath = path.join(beaconPaths.beaconDir, "enr");
writeFile600Perm(pIdPath, exportToJSON(peerId));
writeFile600Perm(enrPath, enr.encodeTxt());

const logger = initLogger(args, beaconPaths.dataDir, config);
const {peerId, enr} = await initPeerIdAndEnr(args, beaconPaths.beaconDir, logger);
// Inject ENR to beacon options
beaconNodeOptions.set({network: {discv5: {enr, enrUpdate: !enr.ip && !enr.ip6}}});
// Add simple version string for libp2p agent version
Expand All @@ -158,16 +144,16 @@ export async function beaconHandlerInit(args: BeaconArgs & GlobalArgs) {
// Render final options
const options = beaconNodeOptions.getWithDefaults();

return {config, options, beaconPaths, network, version, commit, peerId};
return {config, options, beaconPaths, network, version, commit, peerId, logger};
}

export function overwriteEnrWithCliArgs(enr: SignableENR, args: BeaconArgs): void {
// TODO: Not sure if we should propagate port/defaultP2pPort options to the ENR
enr.tcp = args["enr.tcp"] ?? args.port ?? defaultP2pPort;
const udpPort = args["enr.udp"] ?? args.discoveryPort ?? args.port ?? defaultP2pPort;
if (udpPort != null) enr.udp = udpPort;
if (args["enr.ip"] != null) enr.ip = args["enr.ip"];
if (args["enr.ip6"] != null) enr.ip6 = args["enr.ip6"];
if (args["enr.tcp6"] != null) enr.tcp6 = args["enr.tcp6"];
if (args["enr.udp6"] != null) enr.udp6 = args["enr.udp6"];
export function initLogger(args: BeaconArgs, dataDir: string, config: ChainForkConfig): Logger {
const {logger, logParams} = getCliLogger(args, {defaultLogFilepath: path.join(dataDir, "beacon.log")}, config);
try {
cleanOldLogFiles(logParams.filename, logParams.rotateMaxFiles);
} catch (e) {
logger.debug("Not able to delete log files", logParams, e as Error);
}

return logger;
}
84 changes: 84 additions & 0 deletions packages/cli/src/cmds/beacon/initPeerIdAndEnr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import fs from "node:fs";
import path from "node:path";
import {PeerId} from "@libp2p/interface-peer-id";
import {createSecp256k1PeerId} from "@libp2p/peer-id-factory";
import {createKeypairFromPeerId, SignableENR} from "@chainsafe/discv5";
import {Logger} from "@lodestar/utils";
import {exportToJSON, readPeerId} from "../../config/index.js";
import {writeFile600Perm} from "../../util/file.js";
import {defaultP2pPort} from "../../options/beaconNodeOptions/network.js";
import {BeaconArgs} from "./options.js";

export function overwriteEnrWithCliArgs(enr: SignableENR, args: BeaconArgs): void {
// TODO: Not sure if we should propagate port/defaultP2pPort options to the ENR
enr.tcp = args["enr.tcp"] ?? args.port ?? defaultP2pPort;
const udpPort = args["enr.udp"] ?? args.discoveryPort ?? args.port ?? defaultP2pPort;
if (udpPort != null) enr.udp = udpPort;
if (args["enr.ip"] != null) enr.ip = args["enr.ip"];
if (args["enr.ip6"] != null) enr.ip6 = args["enr.ip6"];
if (args["enr.tcp6"] != null) enr.tcp6 = args["enr.tcp6"];
if (args["enr.udp6"] != null) enr.udp6 = args["enr.udp6"];
}

/**
* Create new PeerId and ENR by default, unless persistNetworkIdentity is provided
*/
export async function initPeerIdAndEnr(
args: BeaconArgs,
beaconDir: string,
logger: Logger
): Promise<{peerId: PeerId; enr: SignableENR}> {
const {persistNetworkIdentity} = args;

const newPeerIdAndENR = async (): Promise<{peerId: PeerId; enr: SignableENR}> => {
const peerId = await createSecp256k1PeerId();
const enr = SignableENR.createV4(createKeypairFromPeerId(peerId));
return {peerId, enr};
};

const readPersistedPeerIdAndENR = async (
peerIdFile: string,
enrFile: string
): Promise<{peerId: PeerId; enr: SignableENR}> => {
let peerId: PeerId;
let enr: SignableENR;

// attempt to read stored peer id
try {
peerId = await readPeerId(peerIdFile);
} catch (e) {
logger.warn("Unable to read peerIdFile, creating a new peer id");
return newPeerIdAndENR();
}
// attempt to read stored enr
try {
enr = SignableENR.decodeTxt(fs.readFileSync(enrFile, "utf-8"), createKeypairFromPeerId(peerId));
} catch (e) {
logger.warn("Unable to decode stored local ENR, creating a new ENR");
enr = SignableENR.createV4(createKeypairFromPeerId(peerId));
return {peerId, enr};
}
// check stored peer id against stored enr
if (!peerId.equals(await enr.peerId())) {
logger.warn("Stored local ENR doesn't match peerIdFile, creating a new ENR");
enr = SignableENR.createV4(createKeypairFromPeerId(peerId));
return {peerId, enr};
}
return {peerId, enr};
};

if (persistNetworkIdentity) {
const enrFile = path.join(beaconDir, "enr");
const peerIdFile = path.join(beaconDir, "peer-id.json");
const {peerId, enr} = await readPersistedPeerIdAndENR(peerIdFile, enrFile);
overwriteEnrWithCliArgs(enr, args);
// Re-persist peer-id and enr
writeFile600Perm(peerIdFile, exportToJSON(peerId));
writeFile600Perm(enrFile, enr.encodeTxt());
return {peerId, enr};
} else {
const {peerId, enr} = await newPeerIdAndENR();
overwriteEnrWithCliArgs(enr, args);
return {peerId, enr};
}
}
7 changes: 7 additions & 0 deletions packages/cli/src/cmds/beacon/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type BeaconExtraArgs = {
dbDir?: string;
persistInvalidSszObjectsDir?: string;
peerStoreDir?: string;
persistNetworkIdentity?: boolean;
};

export const beaconExtraOptions: CliCommandOptions<BeaconExtraArgs> = {
Expand Down Expand Up @@ -90,6 +91,12 @@ export const beaconExtraOptions: CliCommandOptions<BeaconExtraArgs> = {
defaultDescription: defaultBeaconPaths.peerStoreDir,
type: "string",
},

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

type ENRArgs = {
Expand Down
92 changes: 83 additions & 9 deletions packages/cli/test/unit/cmds/beacon.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import path from "node:path";
import fs from "node:fs";
import {expect} from "chai";
import {createSecp256k1PeerId} from "@libp2p/peer-id-factory";
import {createFromJSON, createSecp256k1PeerId} from "@libp2p/peer-id-factory";
import {chainConfig} from "@lodestar/config/default";
import {chainConfigToJson} from "@lodestar/config";
import {ENR} from "@chainsafe/discv5";
import {createKeypairFromPeerId, SignableENR} from "@chainsafe/discv5";
import {exportToJSON} from "../../../src/config/peerId.js";
import {beaconHandlerInit} from "../../../src/cmds/beacon/handler.js";
import {initPeerIdAndEnr} from "../../../src/cmds/beacon/initPeerIdAndEnr.js";
import {BeaconArgs} from "../../../src/cmds/beacon/options.js";
import {GlobalArgs} from "../../../src/options/globalOptions.js";
import {testFilesDir} from "../../utils.js";
import {testFilesDir, testLogger} from "../../utils.js";

describe("cmds / beacon / args handler", () => {
// Make tests faster skipping a network call
Expand All @@ -34,14 +35,13 @@ describe("cmds / beacon / args handler", () => {
const enrIp = "10.20.30.40";
const enrTcp = 4000;

const {beaconPaths} = await runBeaconHandlerInit({
const {options} = await runBeaconHandlerInit({
listenAddress: "0.0.0.0",
"enr.ip": enrIp,
"enr.tcp": enrTcp,
});

const enrTxt = fs.readFileSync(path.join(beaconPaths.beaconDir, "enr"), "utf8");
const enr = ENR.decodeTxt(enrTxt);
const enr = options.network.discv5?.enr as SignableENR;

expect(enr.ip).to.equal(enrIp, "wrong enr.ip");
expect(enr.tcp).to.equal(enrTcp, "wrong enr.tcp");
Expand All @@ -54,14 +54,17 @@ describe("cmds / beacon / args handler", () => {
expect(peerId1.toString()).not.equal(peerId2.toString(), "peer ids must be different");
});

it.skip("Re-use existing peer", async () => {
it("Re-use existing peer", async () => {
const prevPeerId = await createSecp256k1PeerId();

const peerIdFile = path.join(testFilesDir, "prev_peerid.json");
const peerIdFile = path.join(testFilesDir, "peer-id.json");
fs.writeFileSync(peerIdFile, JSON.stringify(exportToJSON(prevPeerId)));
const enr = SignableENR.createV4(createKeypairFromPeerId(prevPeerId));
const enrFilePath = path.join(testFilesDir, "enr");
fs.writeFileSync(enrFilePath, enr.encodeTxt());

const {peerId} = await runBeaconHandlerInit({
// peerIdFile,
persistNetworkIdentity: true,
});

expect(peerId.toString()).equal(prevPeerId.toString(), "peer must be equal to persisted");
Expand Down Expand Up @@ -93,6 +96,77 @@ describe("cmds / beacon / args handler", () => {
});
});

describe("initPeerIdAndEnr", () => {
it("should not reuse peer id, persistNetworkIdentity=false", async () => {
const {peerId: peerId1} = await initPeerIdAndEnr(
{persistNetworkIdentity: false} as BeaconArgs,
testFilesDir,
testLogger()
);
const {peerId: peerId2} = await initPeerIdAndEnr(
{persistNetworkIdentity: false} as BeaconArgs,
testFilesDir,
testLogger()
);

expect(peerId1.toString()).not.equal(peerId2.toString(), "peer ids must be different");
});

it("should reuse peer id, persistNetworkIdentity=true", async () => {
const {peerId: peerId1} = await initPeerIdAndEnr(
{persistNetworkIdentity: true} as BeaconArgs,
testFilesDir,
testLogger()
);
const {peerId: peerId2} = await initPeerIdAndEnr(
{persistNetworkIdentity: true} as BeaconArgs,
testFilesDir,
testLogger()
);

expect(peerId1.toString()).to.equal(peerId2.toString(), "peer ids must be equal");
});

it("should overwrite invalid peer id", async () => {
const peerIdFile = path.join(testFilesDir, "peer-id.json");
const peerId1Str = "wrong peer id file content";
fs.writeFileSync(peerIdFile, peerId1Str);
const {peerId: peerId2} = await initPeerIdAndEnr(
{persistNetworkIdentity: true} as BeaconArgs,
testFilesDir,
testLogger()
);
const filePeerId = await createFromJSON(JSON.parse(fs.readFileSync(peerIdFile, "utf-8")));

expect(peerId1Str).not.equal(peerId2.toString(), "peer ids must be different");
expect(filePeerId.toString()).to.equal(peerId2.toString(), "peer ids must be equal");
});

it("should overwrite invalid enr", async () => {
const enrFilePath = path.join(testFilesDir, "enr");
const invalidEnr = "wrong enr file content";
fs.writeFileSync(enrFilePath, invalidEnr);

await initPeerIdAndEnr({persistNetworkIdentity: true} as BeaconArgs, testFilesDir, testLogger());

const validEnr = fs.readFileSync(enrFilePath, "utf-8");

expect(validEnr).not.equal(invalidEnr, "enrs must be different");
});

it("should overwrite enr that doesn't match peer id", async () => {
const otherPeerId = await createSecp256k1PeerId();
const otherEnr = SignableENR.createFromPeerId(otherPeerId);
const enrFilePath = path.join(testFilesDir, "enr");
const otherEnrStr = otherEnr.encodeTxt();
fs.writeFileSync(enrFilePath, otherEnrStr);

const {enr} = await initPeerIdAndEnr({persistNetworkIdentity: true} as BeaconArgs, testFilesDir, testLogger());

expect(enr.nodeId).not.equal(otherEnr, "enrs must be different");
});
});

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
async function runBeaconHandlerInit(args: Partial<BeaconArgs & GlobalArgs>) {
return beaconHandlerInit({
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/test/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import fs from "node:fs";
import path from "node:path";
import tmp from "tmp";
import {createWinstonLogger, Logger} from "@lodestar/utils";

export const networkDev = "dev";

const tmpDir = tmp.dirSync({unsafeCleanup: true});
export const testFilesDir = tmpDir.name;

export const testLogger = (): Logger => createWinstonLogger();

export function getTestdirPath(filepath: string): string {
const fullpath = path.join(testFilesDir, filepath);
fs.mkdirSync(path.dirname(fullpath), {recursive: true});
Expand Down
Loading