diff --git a/.github/workflows/test-sim-merge.yml b/.github/workflows/test-sim-merge.yml index 077a24a310c0..d7399bd30aad 100644 --- a/.github/workflows/test-sim-merge.yml +++ b/.github/workflows/test-sim-merge.yml @@ -3,7 +3,7 @@ name: Sim merge tests on: [pull_request, push] env: - GETH_COMMIT: 0569213dc4032da83abed44fab7f65794a526f21 + GETH_COMMIT: 815a414312db9a922c5a34ac034fb7aa8861f2e7 NETHERMIND_COMMIT: 78ae2353be5d05a285d6aaa1826910489d381a3e jobs: @@ -37,8 +37,10 @@ jobs: # Install Geth merge interop - uses: actions/setup-go@v2 + with: + go-version: '1.17' - name: Clone Geth merge interop branch - run: git clone -b merge-kiln https://github.com/g11tech/go-ethereum.git && cd go-ethereum && git reset --hard $GETH_COMMIT && git submodule update --init --recursive + run: git clone -b merge-kiln-jwt https://github.com/g11tech/go-ethereum.git && cd go-ethereum && git reset --hard $GETH_COMMIT && git submodule update --init --recursive - name: Build Geth run: cd go-ethereum && make @@ -49,6 +51,7 @@ jobs: EL_BINARY_DIR: ../../go-ethereum/build/bin EL_SCRIPT_DIR: kiln/geth EL_PORT: 8545 + ENGINE_PORT: 8551 TX_SCENARIOS: simple # Install Nethermind merge interop diff --git a/kiln/geth/common-setup.sh b/kiln/geth/common-setup.sh index 029f34be21e7..c979928ce6b7 100755 --- a/kiln/geth/common-setup.sh +++ b/kiln/geth/common-setup.sh @@ -2,13 +2,20 @@ echo $TTD echo $DATA_DIR -echo $scriptDir echo $EL_BINARY_DIR +echo $JWT_SECRET_HEX + +echo $scriptDir +echo $currentDir + env TTD=$TTD envsubst < $scriptDir/genesisPre.tmpl > $DATA_DIR/genesis.json echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json echo "12345678" > $DATA_DIR/password.txt pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" +# echo a hex encoded 256 bit secret into a file +echo $JWT_SECRET_HEX> $DATA_DIR/jwtsecret + $EL_BINARY_DIR/geth --datadir $DATA_DIR init $DATA_DIR/genesis.json $EL_BINARY_DIR/geth --datadir $DATA_DIR account import $DATA_DIR/sk.json --password $DATA_DIR/password.txt diff --git a/kiln/geth/post-merge.sh b/kiln/geth/post-merge.sh index 3a402321e1a0..2c87450c69b0 100755 --- a/kiln/geth/post-merge.sh +++ b/kiln/geth/post-merge.sh @@ -1,6 +1,8 @@ #!/bin/bash -x scriptDir=$(dirname $0) +currentDir=$(pwd) + . $scriptDir/common-setup.sh -$EL_BINARY_DIR/geth --http --ws -http.api "engine,net,eth" --datadir $DATA_DIR --allow-insecure-unlock --unlock $pubKey --password $DATA_DIR/password.txt +$EL_BINARY_DIR/geth --http --ws -http.api "engine,net,eth" --datadir $DATA_DIR --allow-insecure-unlock --unlock $pubKey --password $DATA_DIR/password.txt --jwt-secret $JWT_SECRET_HEX diff --git a/kiln/geth/pre-merge.sh b/kiln/geth/pre-merge.sh index 492fab66497c..b4d80ef03591 100755 --- a/kiln/geth/pre-merge.sh +++ b/kiln/geth/pre-merge.sh @@ -1,6 +1,8 @@ #!/bin/bash -x scriptDir=$(dirname $0) +currentDir=$(pwd) + . $scriptDir/common-setup.sh -$EL_BINARY_DIR/geth --http --ws -http.api "engine,net,eth,miner" --datadir $DATA_DIR --allow-insecure-unlock --unlock $pubKey --password $DATA_DIR/password.txt --nodiscover --mine +$EL_BINARY_DIR/geth --http --ws -http.api "engine,net,eth,miner" --datadir $DATA_DIR --allow-insecure-unlock --unlock $pubKey --password $DATA_DIR/password.txt --nodiscover --mine --jwt-secret $JWT_SECRET_HEX diff --git a/kiln/gethdocker/Dockerfile b/kiln/gethdocker/Dockerfile index 8ae825f087c6..eb87fca8814c 100644 --- a/kiln/gethdocker/Dockerfile +++ b/kiln/gethdocker/Dockerfile @@ -1,9 +1,9 @@ # Build Geth in a stock Go builder container FROM golang:1.17-alpine as builder -RUN apk add --no-cache gcc musl-dev linux-headers git +RUN apk add --no-cache gcc musl-dev linux-headers git bash -RUN git clone --depth 1 -b merge-kiln https://github.com/MariusVanDerWijden/go-ethereum.git /go-ethereum +RUN git clone --depth 1 -b merge-kiln-jwt https://github.com/MariusVanDerWijden/go-ethereum.git /go-ethereum RUN cd /go-ethereum && go run build/ci.go install ./cmd/geth FROM alpine:latest diff --git a/kiln/gethdocker/README.md b/kiln/gethdocker/README.md index 117e9007b730..92643769bdc9 100644 --- a/kiln/gethdocker/README.md +++ b/kiln/gethdocker/README.md @@ -3,13 +3,13 @@ ###### Build geth docker image ```bash -cd kintsugi/gethdocker -docker build . --tag geth:kintsugi +cd kiln/gethdocker +docker build . --tag geth:kiln ``` ###### Run test scripts ```bash cd packages/lodestar -EL_BINARY_DIR=geth:kintsugi EL_SCRIPT_DIR=kiln/gethdocker EL_PORT=8545 TX_SCENARIOS=simple yarn mocha test/sim/merge-interop.test.ts +EL_BINARY_DIR=geth:kiln EL_SCRIPT_DIR=kiln/gethdocker EL_PORT=8545 ENGINE_PORT=8551 TX_SCENARIOS=simple yarn mocha test/sim/merge-interop.test.ts ``` diff --git a/kiln/gethdocker/common-setup.sh b/kiln/gethdocker/common-setup.sh index f0f5bb922130..159faa05962b 100755 --- a/kiln/gethdocker/common-setup.sh +++ b/kiln/gethdocker/common-setup.sh @@ -3,6 +3,7 @@ echo $TTD echo $DATA_DIR echo $EL_BINARY_DIR +echo $JWT_SECRET_HEX echo $scriptDir echo $currentDir @@ -13,6 +14,8 @@ echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_ echo "12345678" > $DATA_DIR/password.txt pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" +# echo a hex encoded 256 bit secret into a file +echo $JWT_SECRET_HEX> $DATA_DIR/jwtsecret docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR geth --datadir /data init /data/genesis.json docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR geth --datadir /data account import /data/sk.json --password /data/password.txt diff --git a/kiln/gethdocker/post-merge.sh b/kiln/gethdocker/post-merge.sh index cd16cc8f59e3..d615759361ce 100755 --- a/kiln/gethdocker/post-merge.sh +++ b/kiln/gethdocker/post-merge.sh @@ -5,4 +5,4 @@ currentDir=$(pwd) . $scriptDir/common-setup.sh -docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --network host -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR geth --http --ws -http.api "engine,net,eth" --allow-insecure-unlock --unlock $pubKey --password /data/password.txt --datadir /data +docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --network host -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR geth --http --ws -http.api "engine,net,eth" --allow-insecure-unlock --unlock $pubKey --password /data/password.txt --datadir /data --jwt-secret $JWT_SECRET_HEX diff --git a/kiln/gethdocker/pre-merge.sh b/kiln/gethdocker/pre-merge.sh index 2818b9098186..dc498388ba2e 100755 --- a/kiln/gethdocker/pre-merge.sh +++ b/kiln/gethdocker/pre-merge.sh @@ -6,4 +6,4 @@ currentDir=$(pwd) . $scriptDir/common-setup.sh # EL_BINARY_DIR refers to the local docker image build from kintsugi/gethdocker folder -docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --network host -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR geth --http --ws -http.api "engine,net,eth,miner" --allow-insecure-unlock --unlock $pubKey --password /data/password.txt --datadir /data --nodiscover --mine +docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --network host -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR geth --http --ws -http.api "engine,net,eth,miner" --allow-insecure-unlock --unlock $pubKey --password /data/password.txt --datadir /data --nodiscover --mine --jwt-secret $JWT_SECRET_HEX diff --git a/packages/cli/src/options/beaconNodeOptions/execution.ts b/packages/cli/src/options/beaconNodeOptions/execution.ts index c0b9cb0d69a0..5c94c248f60b 100644 --- a/packages/cli/src/options/beaconNodeOptions/execution.ts +++ b/packages/cli/src/options/beaconNodeOptions/execution.ts @@ -1,15 +1,20 @@ +import fs from "node:fs"; import {defaultOptions, IBeaconNodeOptions} from "@chainsafe/lodestar"; -import {ICliCommandOptions} from "../../util"; +import {ICliCommandOptions, extractJwtHexSecret} from "../../util"; export type ExecutionEngineArgs = { "execution.urls": string[]; "execution.timeout": number; + "jwt-secret"?: string; }; export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] { return { urls: args["execution.urls"], timeout: args["execution.timeout"], + jwtSecretHex: args["jwt-secret"] + ? extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim()) + : undefined, }; } @@ -29,4 +34,11 @@ export const options: ICliCommandOptions = { defaultOptions.executionEngine.mode === "http" ? String(defaultOptions.executionEngine.timeout) : "", group: "execution", }, + + "jwt-secret": { + 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", + }, }; diff --git a/packages/cli/src/util/index.ts b/packages/cli/src/util/index.ts index 82359892ff2a..e7d6d32b62d8 100644 --- a/packages/cli/src/util/index.ts +++ b/packages/cli/src/util/index.ts @@ -16,3 +16,4 @@ export * from "./sleep"; export * from "./stripOffNewlines"; export * from "./types"; export * from "./bls"; +export * from "./jwt"; diff --git a/packages/cli/src/util/jwt.ts b/packages/cli/src/util/jwt.ts new file mode 100644 index 000000000000..e77e9e692cd6 --- /dev/null +++ b/packages/cli/src/util/jwt.ts @@ -0,0 +1,10 @@ +export function extractJwtHexSecret(jwtSecretContents: string): string { + const hexPattern = new RegExp(/^(0x|0X)?(?[a-fA-F0-9]+)$/, "g"); + const jwtSecretHexMatch = hexPattern.exec(jwtSecretContents); + const jwtSecret = jwtSecretHexMatch?.groups?.jwtSecret; + if (!jwtSecret || jwtSecret.length != 64) { + throw Error(`Need a valid 256 bit hex encoded secret ${jwtSecret} ${jwtSecretContents}`); + } + // Return the secret in proper hex format + return `0x${jwtSecret}`; +} diff --git a/packages/cli/test/unit/config/beaconNodeOptions.test.ts b/packages/cli/test/unit/config/beaconNodeOptions.test.ts index a673d94e9af5..3d14016cae43 100644 --- a/packages/cli/test/unit/config/beaconNodeOptions.test.ts +++ b/packages/cli/test/unit/config/beaconNodeOptions.test.ts @@ -7,6 +7,7 @@ import {BeaconNodeOptions, mergeBeaconNodeOptions} from "../../../src/config"; import {enrsToNetworkConfig, parseBootnodesFile} from "../../../src/networks"; import {bootEnrs as praterBootEnrs} from "../../../src/networks/prater"; import {testFilesDir} from "../../utils"; +import {extractJwtHexSecret} from "../../../src/util"; describe("config / beaconNodeOptions", () => { it("Should return prater options", () => { @@ -210,3 +211,37 @@ describe("mergeBeaconNodeOptions", () => { }); } }); + +describe("parseJwtHexSecret", () => { + const testCases: {raw: string; parsed: string}[] = [ + { + raw: "c58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + parsed: "0xc58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + }, + { + raw: "0xc58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + parsed: "0xc58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + }, + { + raw: "0Xc58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + parsed: "0xc58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + }, + ]; + for (const {raw, parsed} of testCases) { + it(`parse ${raw}`, () => { + expect(parsed).to.be.equal(extractJwtHexSecret(raw)); + }); + } +}); + +describe("invalid jwtHexSecret", () => { + const testCases: {raw: string; error: string}[] = [ + {raw: "c58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b23", error: "invalid length"}, + {raw: "X58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", error: "invalid hex"}, + ]; + for (const {raw, error} of testCases) { + it(`should error on ${error}: ${raw}`, () => { + expect(() => extractJwtHexSecret(raw)).to.throw(); + }); + } +}); diff --git a/packages/lodestar/package.json b/packages/lodestar/package.json index c49257daad2a..5606345efa69 100644 --- a/packages/lodestar/package.json +++ b/packages/lodestar/package.json @@ -91,6 +91,7 @@ "interface-datastore": "^5.1.2", "it-all": "^1.0.2", "it-pipe": "^1.1.0", + "jwt-simple": "0.5.6", "libp2p": "^0.32.4", "libp2p-bootstrap": "^0.13.0", "libp2p-gossipsub": "^0.11.1", @@ -114,6 +115,7 @@ "@types/eventsource": "^1.1.5", "@types/http-terminator": "^2.0.1", "@types/it-all": "^1.0.0", + "@types/jwt-simple": "0.5.33", "@types/leveldown": "^4.0.2", "@types/prometheus-gc-stats": "^0.6.1", "@types/supertest": "^2.0.8", diff --git a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts index db48677b1cea..a8de849d0772 100644 --- a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts +++ b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts @@ -2,9 +2,10 @@ // Note: isomorphic-fetch is not well mantained and does not support abort signals import fetch from "cross-fetch"; import {AbortController, AbortSignal} from "@chainsafe/abort-controller"; + import {ErrorAborted, TimeoutError} from "@chainsafe/lodestar-utils"; import {IJson, IRpcPayload, ReqOpts} from "../interface"; - +import {encodeJwtToken} from "./jwt"; /** * Limits the amount of response text printed with RPC or parsing errors */ @@ -31,6 +32,13 @@ export interface IJsonRpcHttpClient { export class JsonRpcHttpClient implements IJsonRpcHttpClient { private id = 1; + /** + * Optional: If provided, use this jwt secret to HS256 encode and add a jwt token in the + * request header which can be authenticated by the RPC server to provide access. + * A fresh token is generated on each requests as EL spec mandates the ELs to check + * the token freshness +-5 seconds (via `iat` property of the token claim) + */ + private jwtSecret?: Uint8Array; constructor( private readonly urls: string[], @@ -39,6 +47,12 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { timeout?: number; /** If returns true, do not fallback to other urls and throw early */ shouldNotFallback?: (error: Error) => boolean; + /** + * If provided, the requests to the RPC server will be bundled with a HS256 encoded + * token using this secret. Otherwise the requests to the RPC server will be unauthorized + * and it might deny responses to the RPC requests. + */ + jwtSecret?: Uint8Array; } ) { // Sanity check for all URLs to be properly defined. Otherwise it will error in loop on fetch @@ -50,6 +64,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { throw Error(`JsonRpcHttpClient.urls[${i}] is empty or undefined: ${url}`); } } + this.jwtSecret = opts?.jwtSecret; } /** @@ -121,10 +136,24 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { } try { + const headers: Record = {"Content-Type": "application/json"}; + if (this.jwtSecret) { + /** + * ELs have a tight +-5 second freshness check on token's iat i.e. issued at + * so its better to generate a new token each time. Currently iat is the only claim + * we are encoding but potentially we can encode more claims. + * Also currently the algorithm for the token generation is mandated to HS256 + * + * Jwt auth spec: https://github.com/ethereum/execution-apis/pull/167 + */ + const token = encodeJwtToken({iat: Math.floor(new Date().getTime() / 1000)}, this.jwtSecret); + headers["Authorization"] = `Bearer ${token}`; + } + const res = await fetch(url, { method: "post", body: JSON.stringify(json), - headers: {"Content-Type": "application/json"}, + headers, signal: controller.signal, }).finally(() => { clearTimeout(timeout); diff --git a/packages/lodestar/src/eth1/provider/jwt.ts b/packages/lodestar/src/eth1/provider/jwt.ts new file mode 100644 index 000000000000..551009b3c31d --- /dev/null +++ b/packages/lodestar/src/eth1/provider/jwt.ts @@ -0,0 +1,28 @@ +import {encode, decode, TAlgorithm} from "jwt-simple"; + +/** jwt token has iat which is issued at unix timestamp, and an optional exp for expiry */ +type JwtClaim = {iat: number; exp?: number}; + +export function encodeJwtToken( + claim: Record & JwtClaim, + jwtSecret: Buffer | Uint8Array | string, + algorithm: TAlgorithm = "HS256" +): string { + const token = encode( + claim, + // Note: This type casting is required as even though jwt-simple accepts a buffer as a + // secret types definitions exposed by @types/jwt-simple only takes a string + (jwtSecret as unknown) as string, + algorithm + ); + return token; +} + +export function decodeJwtToken( + token: string, + jwtSecret: Buffer | Uint8Array | string, + algorithm: TAlgorithm = "HS256" +): JwtClaim { + const claim = decode(token, (jwtSecret as never) as string, false, algorithm) as JwtClaim; + return claim; +} diff --git a/packages/lodestar/src/executionEngine/http.ts b/packages/lodestar/src/executionEngine/http.ts index f6b47cd9b829..52a672cda8d0 100644 --- a/packages/lodestar/src/executionEngine/http.ts +++ b/packages/lodestar/src/executionEngine/http.ts @@ -1,6 +1,7 @@ import {AbortSignal} from "@chainsafe/abort-controller"; import {bellatrix, RootHex, Root} from "@chainsafe/lodestar-types"; import {BYTES_PER_LOGS_BLOOM} from "@chainsafe/lodestar-params"; +import {fromHex} from "@chainsafe/lodestar-utils"; import {ErrorJsonRpcResponse, HttpRpcError, JsonRpcHttpClient} from "../eth1/provider/jsonRpcHttpClient"; import { @@ -26,6 +27,13 @@ import { export type ExecutionEngineHttpOpts = { urls: string[]; timeout?: number; + /** + * 256 bit jwt secret in hex format without the leading 0x. If provided, the execution engine + * rpc requests will be bundled by an authorization header having a fresh jwt token on each + * request, as the EL auth specs mandate the fresh of the token (iat) to be checked within + * +-5 seconds interval. + */ + jwtSecretHex?: string; }; export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = { @@ -51,6 +59,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { new JsonRpcHttpClient(opts.urls, { signal, timeout: opts.timeout, + jwtSecret: opts.jwtSecretHex ? fromHex(opts.jwtSecretHex) : undefined, }); } diff --git a/packages/lodestar/test/sim/merge-interop.test.ts b/packages/lodestar/test/sim/merge-interop.test.ts index 31f247971fc4..d685b25c2781 100644 --- a/packages/lodestar/test/sim/merge-interop.test.ts +++ b/packages/lodestar/test/sim/merge-interop.test.ts @@ -29,25 +29,31 @@ import {bytesToData, dataToBytes, quantityToNum} from "../../src/eth1/provider/u // EL_SCRIPT_DIR: Directory in packages/lodestar for the EL client, from where to // execute post-merge/pre-merge EL scenario scripts // EL_PORT: EL port on localhost for hosting both engine & json rpc endpoints +// ENGINE_PORT: Specify the port on which an jwt auth protected engine api is being hosted, +// typically by default at 8551 for geth. Some ELs could host it as same port as eth_ apis, +// but just with the engine_ methods protected. In that case this param can be skipped // TX_SCENARIOS: comma seprated transaction scenarios this EL client build supports // Example: // ``` -// $ EL_BINARY_DIR=/home/lion/Code/eth2.0/merge-interop/go-ethereum/build/bin EL_SCRIPT_DIR=kiln/geth EL_PORT=8545 TX_SCENARIOS=simple ../../node_modules/.bin/mocha test/sim/merge.test.ts +// $ EL_BINARY_DIR=/home/lion/Code/eth2.0/merge-interop/go-ethereum/build/bin \ +// EL_SCRIPT_DIR=kiln/geth EL_PORT=8545 ENGINE_PORT=8551 TX_SCENARIOS=simple \ +// ../../node_modules/.bin/mocha test/sim/merge.test.ts // ``` /* eslint-disable no-console, @typescript-eslint/naming-convention, quotes */ // BELLATRIX_EPOCH will happen at 2 sec * 8 slots = 16 sec // 10 ttd / 2 difficulty per block = 5 blocks * 5 sec = 25 sec -const terminalTotalDifficultyPreMerge = 20; +const terminalTotalDifficultyPreMerge = 10; const TX_SCENARIOS = process.env.TX_SCENARIOS?.split(",") || []; +const jwtSecretHex = "0xdc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d"; describe("executionEngine / ExecutionEngineHttp", function () { this.timeout("10min"); const dataPath = fs.mkdtempSync("lodestar-test-merge-interop"); const jsonRpcPort = process.env.EL_PORT; - const enginePort = process.env.EL_PORT; + const enginePort = process.env.ENGINE_PORT ?? jsonRpcPort; const jsonRpcUrl = `http://localhost:${jsonRpcPort}`; const engineApiUrl = `http://localhost:${enginePort}`; @@ -73,6 +79,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { ...process.env, TTD, DATA_DIR, + JWT_SECRET_HEX: `${jwtSecretHex}`, }, }); @@ -149,7 +156,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { } const controller = new AbortController(); - const executionEngine = new ExecutionEngineHttp({urls: [engineApiUrl]}, controller.signal); + const executionEngine = new ExecutionEngineHttp({urls: [engineApiUrl], jwtSecretHex}, controller.signal); // 1. Prepare a payload @@ -313,7 +320,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { sync: {isSingleNode: true}, network: {discv5: null}, eth1: {enabled: true, providerUrls: [jsonRpcUrl]}, - executionEngine: {urls: [engineApiUrl]}, + executionEngine: {urls: [engineApiUrl], jwtSecretHex}, }, validatorCount: validatorClientCount * validatorsPerClient, logger: loggerNodeA, diff --git a/packages/lodestar/test/unit/eth1/jwt.test.ts b/packages/lodestar/test/unit/eth1/jwt.test.ts new file mode 100644 index 000000000000..0fefbc4ee7c2 --- /dev/null +++ b/packages/lodestar/test/unit/eth1/jwt.test.ts @@ -0,0 +1,23 @@ +import {expect} from "chai"; +import {encodeJwtToken, decodeJwtToken} from "../../../src/eth1/provider/jwt"; + +describe("ExecutionEngine / jwt", () => { + it("encode/decode correctly", () => { + const jwtSecret = Buffer.from(Array.from({length: 32}, () => Math.round(Math.random() * 255))); + const claim = {iat: Math.floor(new Date().getTime() / 1000)}; + const token = encodeJwtToken(claim, jwtSecret); + const decoded = decodeJwtToken(token, jwtSecret); + expect(decoded).to.be.deep.equal(claim, "Invalid encoding/decoding of claim"); + }); + + it("encode a claim correctly from a hex key", () => { + const jwtSecretHex = "7e2d709fb01382352aaf830e755d33ca48cb34ba1c21d999e45c1a7a6f88b193"; + const jwtSecret = Buffer.from(jwtSecretHex, "hex"); + const claim = {iat: 1645551452}; + const token = encodeJwtToken(claim, jwtSecret); + expect(token).to.be.equal( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDU1NTE0NTJ9.nUDaIyGPgRX76tQ_kDlcIGj4uyFA4lFJGKsD_GHIEzM", + "Invalid encoding of claim" + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index a239582afae6..4021ef224afd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2363,6 +2363,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/jwt-simple@^0.5.33": + version "0.5.33" + resolved "https://registry.yarnpkg.com/@types/jwt-simple/-/jwt-simple-0.5.33.tgz#fb839cabe81437954f7d0cd01760ad8096ec526e" + integrity sha1-+4Ocq+gUN5VPfQzQF2CtgJbsUm4= + "@types/keyv@*": version "3.1.1" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" @@ -6779,6 +6784,11 @@ just-extend@^4.0.2: resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== +jwt-simple@0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/jwt-simple/-/jwt-simple-0.5.6.tgz#3357adec55b26547114157be66748995b75b333a" + integrity sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg== + keccak@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.1.tgz#ae30a0e94dbe43414f741375cff6d64c8bea0bff"