diff --git a/docs/usage/beacon-management.md b/docs/usage/beacon-management.md index 5536fbb78a5b..7295db64098d 100644 --- a/docs/usage/beacon-management.md +++ b/docs/usage/beacon-management.md @@ -33,7 +33,7 @@ You must generate a secret 32-byte (64 characters) hexadecimal string that will ### Configure Lodestar to locate the JWT secret -When starting up a Lodestar beacon node in any configuration, ensure you add the `--jwt-secret $JWT_SECRET_PATH` flag to point to the saved secret key file. +When starting up a Lodestar beacon node in any configuration, ensure you add the `--jwtSecret $JWT_SECRET_PATH` flag to point to the saved secret key file. ### Ensure JWT is configured with your execution node @@ -54,7 +54,7 @@ Use the `--authrpc.jwtsecret` flag to configure the secret. Use their documentat To start a Lodestar beacon run the command: ```bash -./lodestar beacon --network $NETWORK_NAME --jwt-secret $JWT_SECRET_PATH +./lodestar beacon --network $NETWORK_NAME --jwtSecret $JWT_SECRET_PATH ``` This will assume an execution-layer client is available at the default @@ -63,7 +63,7 @@ location of `https://localhost:8545`. In case execution-layer clients are available at different locations, use `--execution.urls` to specify these locations in the command: ```bash -./lodestar beacon --network $NETWORK_NAME --jwt-secret $JWT_SECRET_PATH --execution.urls $EL_URL1 $EL_URL2 +./lodestar beacon --network $NETWORK_NAME --jwtSecret $JWT_SECRET_PATH --execution.urls $EL_URL1 $EL_URL2 ``` Immediately you should see confirmation that the node has started diff --git a/packages/beacon-node/src/eth1/options.ts b/packages/beacon-node/src/eth1/options.ts index 0f49f2d8a95c..2f9abdd69b45 100644 --- a/packages/beacon-node/src/eth1/options.ts +++ b/packages/beacon-node/src/eth1/options.ts @@ -7,6 +7,8 @@ export type Eth1Options = { * protected engine endpoints. */ jwtSecretHex?: string; + jwtId?: string; + jwtVersion?: string; depositContractDeployBlock?: number; unsafeAllowDepositDataOverwrite?: boolean; /** diff --git a/packages/beacon-node/src/eth1/provider/eth1Provider.ts b/packages/beacon-node/src/eth1/provider/eth1Provider.ts index eb8f37d37489..151d729e66e3 100644 --- a/packages/beacon-node/src/eth1/provider/eth1Provider.ts +++ b/packages/beacon-node/src/eth1/provider/eth1Provider.ts @@ -64,7 +64,9 @@ export class Eth1Provider implements IEth1Provider { constructor( config: Pick, - opts: Pick & {logger?: Logger}, + opts: Pick & { + logger?: Logger; + }, signal?: AbortSignal, metrics?: JsonRpcHttpClientMetrics | null ) { @@ -76,6 +78,8 @@ export class Eth1Provider implements IEth1Provider { // Don't fallback with is truncated error. Throw early and let the retry on this class handle it shouldNotFallback: isJsonRpcTruncatedError, jwtSecret: opts.jwtSecretHex ? fromHex(opts.jwtSecretHex) : undefined, + jwtId: opts.jwtId, + jwtVersion: opts.jwtVersion, metrics: metrics, }); diff --git a/packages/beacon-node/src/eth1/provider/jsonRpcHttpClient.ts b/packages/beacon-node/src/eth1/provider/jsonRpcHttpClient.ts index 272de2249686..3a1b4ddb0ce1 100644 --- a/packages/beacon-node/src/eth1/provider/jsonRpcHttpClient.ts +++ b/packages/beacon-node/src/eth1/provider/jsonRpcHttpClient.ts @@ -4,7 +4,7 @@ import {fetch} from "@lodestar/api"; import {ErrorAborted, TimeoutError, isValidHttpUrl, retry} from "@lodestar/utils"; import {IGauge, IHistogram} from "../../metrics/interface.js"; import {IJson, RpcPayload} from "../interface.js"; -import {encodeJwtToken} from "./jwt.js"; +import {JwtClaim, encodeJwtToken} from "./jwt.js"; export enum JsonRpcHttpClientEvent { /** @@ -83,6 +83,8 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { * the token freshness +-5 seconds (via `iat` property of the token claim) */ private readonly jwtSecret?: Uint8Array; + private readonly jwtId?: string; + private readonly jwtVersion?: string; private readonly metrics: JsonRpcHttpClientMetrics | null; readonly emitter = new JsonRpcHttpClientEventEmitter(); @@ -103,6 +105,10 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { * and it might deny responses to the RPC requests. */ jwtSecret?: Uint8Array; + /** If jwtSecret and jwtId are provided, jwtId will be included in JwtClaim.id */ + jwtId?: string; + /** If jwtSecret and jwtVersion are provided, jwtVersion will be included in JwtClaim.clv. */ + jwtVersion?: string; /** Retry attempts */ retryAttempts?: number; /** Retry delay, only relevant with retry attempts */ @@ -125,6 +131,8 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { } this.jwtSecret = opts?.jwtSecret; + this.jwtId = opts?.jwtId; + this.jwtVersion = opts?.jwtVersion; this.metrics = opts?.metrics ?? null; this.metrics?.configUrlsCount.set(urls.length); @@ -255,7 +263,13 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { * * Jwt auth spec: https://github.com/ethereum/execution-apis/pull/167 */ - const token = encodeJwtToken({iat: Math.floor(new Date().getTime() / 1000)}, this.jwtSecret); + const jwtClaim: JwtClaim = { + iat: Math.floor(Date.now() / 1000), + id: this.jwtId, + clv: this.jwtVersion, + }; + + const token = encodeJwtToken(jwtClaim, this.jwtSecret); headers["Authorization"] = `Bearer ${token}`; } diff --git a/packages/beacon-node/src/eth1/provider/jwt.ts b/packages/beacon-node/src/eth1/provider/jwt.ts index d9ed24ded165..1e267120957f 100644 --- a/packages/beacon-node/src/eth1/provider/jwt.ts +++ b/packages/beacon-node/src/eth1/provider/jwt.ts @@ -4,8 +4,11 @@ import jwt from "jwt-simple"; const {encode, decode} = jwt; -/** jwt token has iat which is issued at unix timestamp, and an optional exp for expiry */ -type JwtClaim = {iat: number; exp?: number}; +/** + * jwt token has iat which is issued at unix timestamp, an optional exp for expiry, + * an optional id as unique identifier, and an optional clv for client type/version + */ +export type JwtClaim = {iat: number; exp?: number; id?: string; clv?: string}; export function encodeJwtToken( claim: JwtClaim, diff --git a/packages/beacon-node/src/execution/engine/http.ts b/packages/beacon-node/src/execution/engine/http.ts index 6f5b3553dcb4..cf3865286ea5 100644 --- a/packages/beacon-node/src/execution/engine/http.ts +++ b/packages/beacon-node/src/execution/engine/http.ts @@ -55,6 +55,14 @@ export type ExecutionEngineHttpOpts = { * +-5 seconds interval. */ jwtSecretHex?: string; + /** + * An identifier string passed as CLI arg that will be set in `id` field of jwt claims + */ + jwtId?: string; + /** + * A version string that will be set in `clv` field of jwt claims + */ + jwtVersion?: string; }; export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = { diff --git a/packages/beacon-node/src/execution/engine/index.ts b/packages/beacon-node/src/execution/engine/index.ts index 210cba5fb489..743abf203de9 100644 --- a/packages/beacon-node/src/execution/engine/index.ts +++ b/packages/beacon-node/src/execution/engine/index.ts @@ -36,6 +36,8 @@ export function getExecutionEngineHttp( signal: modules.signal, metrics: modules.metrics?.executionEnginerHttpClient, jwtSecret: opts.jwtSecretHex ? fromHex(opts.jwtSecretHex) : undefined, + jwtId: opts.jwtId, + jwtVersion: opts.jwtVersion, }); return new ExecutionEngineHttp(rpc, modules); } diff --git a/packages/beacon-node/test/unit/eth1/jwt.test.ts b/packages/beacon-node/test/unit/eth1/jwt.test.ts index abf455c9e149..5ebcdc17e355 100644 --- a/packages/beacon-node/test/unit/eth1/jwt.test.ts +++ b/packages/beacon-node/test/unit/eth1/jwt.test.ts @@ -10,6 +10,14 @@ describe("ExecutionEngine / jwt", () => { expect(decoded).toEqual(claim); }); + it("encode/decode correctly with id and clv", () => { + const jwtSecret = Buffer.from(Array.from({length: 32}, () => Math.round(Math.random() * 255))); + const claim = {iat: Math.floor(new Date().getTime() / 1000), id: "4ac0", clv: "Lodestar/v0.36.0/80c248bb"}; + const token = encodeJwtToken(claim, jwtSecret); + const decoded = decodeJwtToken(token, jwtSecret); + expect(decoded).toEqual(claim); + }); + it("encode a claim correctly from a hex key", () => { const jwtSecretHex = "7e2d709fb01382352aaf830e755d33ca48cb34ba1c21d999e45c1a7a6f88b193"; const jwtSecret = Buffer.from(jwtSecretHex, "hex"); @@ -19,4 +27,29 @@ describe("ExecutionEngine / jwt", () => { "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDU1NTE0NTJ9.nUDaIyGPgRX76tQ_kDlcIGj4uyFA4lFJGKsD_GHIEzM" ); }); + + it("encode a claim with id and clv correctly from a hex key", () => { + const jwtSecretHex = "7e2d709fb01382352aaf830e755d33ca48cb34ba1c21d999e45c1a7a6f88b193"; + const jwtSecret = Buffer.from(jwtSecretHex, "hex"); + const id = "4ac0"; + const clv = "v1.11.3"; + const claimWithId = {iat: 1645551452, id: id}; + const claimWithVersion = {iat: 1645551452, clv: clv}; + const claimWithIdAndVersion = {iat: 1645551452, id: id, clv: clv}; + + const tokenWithId = encodeJwtToken(claimWithId, jwtSecret); + expect(tokenWithId).toBe( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDU1NTE0NTIsImlkIjoiNGFjMCJ9.g3iKsQk9Q1PSYaGldo9MM0Mds8E59t24K6rHdQ9HXg0" + ); + + const tokenWithVersion = encodeJwtToken(claimWithVersion, jwtSecret); + expect(tokenWithVersion).toBe( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDU1NTE0NTIsImNsdiI6InYxLjExLjMifQ.s5iLRa04o_rtATWubz6LZc27rVXhE2n9BPpXpnmnR5o" + ); + + const tokenWithIdAndVersion = encodeJwtToken(claimWithIdAndVersion, jwtSecret); + expect(tokenWithIdAndVersion).toBe( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDU1NTE0NTIsImlkIjoiNGFjMCIsImNsdiI6InYxLjExLjMifQ.QahIO3hcnMV385ES5jedRudsdECMVDembkoQv4BnSTs" + ); + }); }); diff --git a/packages/cli/src/cmds/beacon/handler.ts b/packages/cli/src/cmds/beacon/handler.ts index 4a61c4ab9dee..a56916cdb232 100644 --- a/packages/cli/src/cmds/beacon/handler.ts +++ b/packages/cli/src/cmds/beacon/handler.ts @@ -195,10 +195,14 @@ export async function beaconHandlerInit(args: BeaconArgs & GlobalArgs) { if (args.private) { beaconNodeOptions.set({network: {private: true}}); } else { + const versionStr = `Lodestar/${version}`; + const simpleVersionStr = version.split("/")[0]; // Add simple version string for libp2p agent version - beaconNodeOptions.set({network: {version: version.split("/")[0]}}); + beaconNodeOptions.set({network: {version: simpleVersionStr}}); // Add User-Agent header to all builder requests - beaconNodeOptions.set({executionBuilder: {userAgent: `Lodestar/${version}`}}); + beaconNodeOptions.set({executionBuilder: {userAgent: versionStr}}); + // Set jwt version with version string + beaconNodeOptions.set({executionEngine: {jwtVersion: versionStr}, eth1: {jwtVersion: versionStr}}); } // Render final options diff --git a/packages/cli/src/cmds/beacon/options.ts b/packages/cli/src/cmds/beacon/options.ts index fff9a0912db6..3947e2ba17d0 100644 --- a/packages/cli/src/cmds/beacon/options.ts +++ b/packages/cli/src/cmds/beacon/options.ts @@ -117,7 +117,8 @@ export const beaconExtraOptions: CliCommandOptions = { }, private: { - description: "Do not send implementation details over p2p identify protocol and in builder requests", + description: + "Do not send implementation details over p2p identify protocol and in builder, execution engine and eth1 requests", type: "boolean", }, diff --git a/packages/cli/src/options/beaconNodeOptions/eth1.ts b/packages/cli/src/options/beaconNodeOptions/eth1.ts index c6f6dd9177f8..196deb59161f 100644 --- a/packages/cli/src/options/beaconNodeOptions/eth1.ts +++ b/packages/cli/src/options/beaconNodeOptions/eth1.ts @@ -14,6 +14,8 @@ export type Eth1Args = { export function parseArgs(args: Eth1Args & Partial): IBeaconNodeOptions["eth1"] { let jwtSecretHex: string | undefined; + let jwtId: string | undefined; + let providerUrls = args["eth1.providerUrls"]; // If no providerUrls are explicitly provided, we should pick the execution endpoint @@ -22,15 +24,17 @@ export function parseArgs(args: Eth1Args & Partial): IBeaco // jwt auth mechanism. if (providerUrls === undefined && args["execution.urls"]) { providerUrls = args["execution.urls"]; - jwtSecretHex = args["jwt-secret"] - ? extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim()) + jwtSecretHex = args["jwtSecret"] + ? extractJwtHexSecret(fs.readFileSync(args["jwtSecret"], "utf-8").trim()) : undefined; + jwtId = args["jwtId"]; } return { enabled: args["eth1"], providerUrls, jwtSecretHex, + jwtId, depositContractDeployBlock: args["eth1.depositContractDeployBlock"], disableEth1DepositDataTracker: args["eth1.disableEth1DepositDataTracker"], unsafeAllowDepositDataOverwrite: args["eth1.unsafeAllowDepositDataOverwrite"], diff --git a/packages/cli/src/options/beaconNodeOptions/execution.ts b/packages/cli/src/options/beaconNodeOptions/execution.ts index b1faf482ade8..23f9e6e0706c 100644 --- a/packages/cli/src/options/beaconNodeOptions/execution.ts +++ b/packages/cli/src/options/beaconNodeOptions/execution.ts @@ -8,7 +8,8 @@ export type ExecutionEngineArgs = { "execution.retryAttempts": number; "execution.retryDelay": number; "execution.engineMock"?: boolean; - "jwt-secret"?: string; + jwtSecret?: string; + jwtId?: string; }; export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] { @@ -28,9 +29,10 @@ export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["execut * jwtSecret is parsed as hex instead of bytes because the merge with defaults * in beaconOptions messes up the bytes array as as index => value object */ - jwtSecretHex: args["jwt-secret"] - ? extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim()) + jwtSecretHex: args["jwtSecret"] + ? extractJwtHexSecret(fs.readFileSync(args["jwtSecret"], "utf-8").trim()) : undefined, + jwtId: args["jwtId"], }; } @@ -74,10 +76,17 @@ export const options: CliCommandOptions = { group: "execution", }, - "jwt-secret": { + jwtSecret: { description: "File path to a shared hex-encoded jwt secret which will be used to generate and bundle HS256 encoded jwt tokens for authentication with the EL client's rpc server hosting engine apis. Secret to be exactly same as the one used by the corresponding EL client.", type: "string", group: "execution", }, + + jwtId: { + description: + "An optional identifier to be set in the id field of the claims included in jwt tokens used for authentication with EL client's rpc server hosting engine apis", + type: "string", + group: "execution", + }, }; diff --git a/packages/cli/test/unit/options/beaconNodeOptions.test.ts b/packages/cli/test/unit/options/beaconNodeOptions.test.ts index b0f0254443dc..4f5050d87daf 100644 --- a/packages/cli/test/unit/options/beaconNodeOptions.test.ts +++ b/packages/cli/test/unit/options/beaconNodeOptions.test.ts @@ -223,7 +223,7 @@ describe("options / beaconNodeOptions", () => { const beaconNodeArgsPartial = { eth1: true, "execution.urls": ["http://my.node:8551"], - "jwt-secret": jwtSecretFile, + jwtSecret: jwtSecretFile, } as BeaconNodeArgs; const expectedOptions: RecursivePartial = { diff --git a/packages/cli/test/utils/simulation/beacon_clients/lodestar.ts b/packages/cli/test/utils/simulation/beacon_clients/lodestar.ts index e90db5ec1505..3faa0bdb0acd 100644 --- a/packages/cli/test/utils/simulation/beacon_clients/lodestar.ts +++ b/packages/cli/test/utils/simulation/beacon_clients/lodestar.ts @@ -53,7 +53,7 @@ export const generateLodestarBeaconNode: BeaconNodeGenerator