Skip to content

Commit

Permalink
Merge 46e723b into 07fbc13
Browse files Browse the repository at this point in the history
  • Loading branch information
g11tech authored Feb 28, 2022
2 parents 07fbc13 + 46e723b commit 8aaf754
Show file tree
Hide file tree
Showing 20 changed files with 203 additions and 20 deletions.
7 changes: 5 additions & 2 deletions .github/workflows/test-sim-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Sim merge tests
on: [pull_request, push]

env:
GETH_COMMIT: 0569213dc4032da83abed44fab7f65794a526f21
GETH_COMMIT: 815a414312db9a922c5a34ac034fb7aa8861f2e7
NETHERMIND_COMMIT: 78ae2353be5d05a285d6aaa1826910489d381a3e

jobs:
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
9 changes: 8 additions & 1 deletion kiln/geth/common-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion kiln/geth/post-merge.sh
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion kiln/geth/pre-merge.sh
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions kiln/gethdocker/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 3 additions & 3 deletions kiln/gethdocker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
3 changes: 3 additions & 0 deletions kiln/gethdocker/common-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
echo $TTD
echo $DATA_DIR
echo $EL_BINARY_DIR
echo $JWT_SECRET_HEX

echo $scriptDir
echo $currentDir
Expand All @@ -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
2 changes: 1 addition & 1 deletion kiln/gethdocker/post-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion kiln/gethdocker/pre-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 13 additions & 1 deletion packages/cli/src/options/beaconNodeOptions/execution.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}

Expand All @@ -29,4 +34,11 @@ export const options: ICliCommandOptions<ExecutionEngineArgs> = {
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",
},
};
1 change: 1 addition & 0 deletions packages/cli/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from "./sleep";
export * from "./stripOffNewlines";
export * from "./types";
export * from "./bls";
export * from "./jwt";
10 changes: 10 additions & 0 deletions packages/cli/src/util/jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function extractJwtHexSecret(jwtSecretContents: string): string {
const hexPattern = new RegExp(/^(0x|0X)?(?<jwtSecret>[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}`;
}
35 changes: 35 additions & 0 deletions packages/cli/test/unit/config/beaconNodeOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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();
});
}
});
2 changes: 2 additions & 0 deletions packages/lodestar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
33 changes: 31 additions & 2 deletions packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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[],
Expand All @@ -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
Expand All @@ -50,6 +64,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient {
throw Error(`JsonRpcHttpClient.urls[${i}] is empty or undefined: ${url}`);
}
}
this.jwtSecret = opts?.jwtSecret;
}

/**
Expand Down Expand Up @@ -121,10 +136,24 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient {
}

try {
const headers: Record<string, string> = {"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);
Expand Down
28 changes: 28 additions & 0 deletions packages/lodestar/src/eth1/provider/jwt.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> & 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;
}
9 changes: 9 additions & 0 deletions packages/lodestar/src/executionEngine/http.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 = {
Expand All @@ -51,6 +59,7 @@ export class ExecutionEngineHttp implements IExecutionEngine {
new JsonRpcHttpClient(opts.urls, {
signal,
timeout: opts.timeout,
jwtSecret: opts.jwtSecretHex ? fromHex(opts.jwtSecretHex) : undefined,
});
}

Expand Down
Loading

0 comments on commit 8aaf754

Please sign in to comment.