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

exchangeTransitionConfiguration heartbeat implementation #3785

Closed
wants to merge 1 commit into from
Closed
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
71 changes: 70 additions & 1 deletion packages/lodestar/src/executionEngine/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import {
PayloadId,
PayloadAttributes,
ApiPayloadAttributes,
TransitionConfigOpts,
ApiTransitionConfig,
TransitionConfig,
} from "./interface";

export type ExecutionEngineHttpOpts = {
Expand All @@ -44,14 +47,65 @@ export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = {
*/
export class ExecutionEngineHttp implements IExecutionEngine {
private readonly rpc: IJsonRpcHttpClient;
private readonly transitionConfig: ApiTransitionConfig;
private readonly heartBeatInSec?: number;
private lastHeartBeatAt = 0;

constructor(opts: ExecutionEngineHttpOpts, signal: AbortSignal, rpc?: IJsonRpcHttpClient) {
constructor(opts: ExecutionEngineHttpOpts & TransitionConfigOpts, signal: AbortSignal, rpc?: IJsonRpcHttpClient) {
this.transitionConfig = serializeTransitionConfig(opts.transitionConfig);
this.heartBeatInSec = opts.heartBeatInSec;
this.rpc =
rpc ??
new JsonRpcHttpClient(opts.urls, {
signal,
timeout: opts.timeout,
});
void this.exchangeTransitionConfiguration();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

void Promise that may reject is a bad idea.

}

/**
* `engine_exchangeTransitionConfigurationV1`
* From: https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.7/src/engine/specification.md#engine_exchangetransitionconfigurationv1
*
* 1. Execution Layer client software MUST respond with configurable setting values that
* are set according to the Client software configuration section of EIP-3675.
*
* 2. Execution Layer client software SHOULD surface an error to the user if local
* configuration settings mismatch corresponding values received in the call of this
* method, with exception for terminalBlockNumber value.
*
* 3. Consensus Layer client software SHOULD surface an error to the user if local
* configuration settings mismatch corresponding values obtained from the response
* to the call of this method.
*/
async exchangeTransitionConfiguration(): Promise<ApiTransitionConfig> {
const method = "engine_exchangeTransitionConfigurationV1";
const transitionConfig = await this.rpc.fetch<
EngineApiRpcReturnTypes[typeof method],
EngineApiRpcParamTypes[typeof method]
>({
method,
params: [this.transitionConfig],
});
// TODO: determine the throw condition, also identify how to update terminal blocktracker hash
// and the terminal number depending upon
// 1. eth1 tracker 2. the terminal block received
// as per spec, this must not be dynamic
if (
transitionConfig.terminalTotalDifficulty !== this.transitionConfig.terminalTotalDifficulty ||
transitionConfig.terminalBlockHash !== this.transitionConfig.terminalBlockHash ||
transitionConfig.terminalBlockNumber !== this.transitionConfig.terminalBlockNumber
) {
throw Error(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This http engine should not be responsible to throw this Error. There should be something at a higher level that does this recurring calls and logs loudly if there's a mismatch. Keep in mind that there may be multiple transports in the future besides http. The return of this function could be a status

type ReturnType = {match: true} | {match: false; local: TransitionConfig; remote: TransitionConfig}

`Transition config mismatch, actual=${JSON.stringify(transitionConfig)}, expected=${JSON.stringify(
this.transitionConfig
)}`
);
}
// Use this lastHeartBeatAt in the actual engine api calls to identify if this needs to
// be called again
this.lastHeartBeatAt = Math.floor(new Date().getTime() / 1000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class should not be responsible to track the frequency of calls, this class should be state-less

return transitionConfig;
}

/**
Expand Down Expand Up @@ -262,6 +316,9 @@ export class ExecutionEngineHttp implements IExecutionEngine {
/* eslint-disable @typescript-eslint/naming-convention */

type EngineApiRpcParamTypes = {
/**
*/
engine_exchangeTransitionConfigurationV1: [ApiTransitionConfig];
/**
* 1. Object - Instance of ExecutionPayload
*/
Expand All @@ -282,6 +339,10 @@ type EngineApiRpcParamTypes = {
};

type EngineApiRpcReturnTypes = {
/**
*
*/
engine_exchangeTransitionConfigurationV1: ApiTransitionConfig;
/**
* Object - Response object:
* - status: String - the result of the payload execution:
Expand Down Expand Up @@ -355,3 +416,11 @@ export function parseExecutionPayload(data: ExecutionPayloadRpc): bellatrix.Exec
transactions: data.transactions.map((tran) => dataToBytes(tran)),
};
}

export function serializeTransitionConfig(transitionConfig: TransitionConfig): ApiTransitionConfig {
return {
terminalTotalDifficulty: numToQuantity(transitionConfig.terminalTotalDifficulty),
terminalBlockHash: bytesToData(transitionConfig.terminalBlockHash),
terminalBlockNumber: numToQuantity(transitionConfig.terminalBlockNumber),
};
}
7 changes: 5 additions & 2 deletions packages/lodestar/src/executionEngine/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {AbortSignal} from "@chainsafe/abort-controller";
import {IExecutionEngine} from "./interface";
import {IExecutionEngine, TransitionConfigOpts} from "./interface";
import {ExecutionEngineDisabled} from "./disabled";
import {ExecutionEngineHttp, ExecutionEngineHttpOpts, defaultExecutionEngineHttpOpts} from "./http";
import {ExecutionEngineMock, ExecutionEngineMockOpts} from "./mock";
Expand All @@ -13,7 +13,10 @@ export type ExecutionEngineOpts =

export const defaultExecutionEngineOpts: ExecutionEngineOpts = defaultExecutionEngineHttpOpts;

export function initializeExecutionEngine(opts: ExecutionEngineOpts, signal: AbortSignal): IExecutionEngine {
export function initializeExecutionEngine(
opts: ExecutionEngineOpts & TransitionConfigOpts,
signal: AbortSignal
): IExecutionEngine {
switch (opts.mode) {
case "mock":
return new ExecutionEngineMock(opts);
Expand Down
14 changes: 14 additions & 0 deletions packages/lodestar/src/executionEngine/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ export type PayloadAttributes = {
suggestedFeeRecipient: Uint8Array | ByteVector;
};

export type TransitionConfig = {
terminalTotalDifficulty: bigint;
terminalBlockHash: Uint8Array | ByteVector;
terminalBlockNumber: number;
};

export type TransitionConfigOpts = {transitionConfig: TransitionConfig; heartBeatInSec?: number};

export type ApiTransitionConfig = {
terminalTotalDifficulty: QUANTITY;
terminalBlockHash: DATA;
terminalBlockNumber: QUANTITY;
};

export type ApiPayloadAttributes = {
/** QUANTITY, 64 Bits - value for the timestamp field of the new payload */
timestamp: QUANTITY;
Expand Down
15 changes: 14 additions & 1 deletion packages/lodestar/src/node/nodejs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ export class BeaconNode {
initBeaconMetrics(metrics, anchorState);
}

const transitionConfig = {
terminalTotalDifficulty: config.TERMINAL_TOTAL_DIFFICULTY,
terminalBlockHash: config.TERMINAL_BLOCK_HASH,
/** terminalBlockNumber has to be set to zero for now as per specs */
terminalBlockNumber: 0,
};

const chain = new BeaconChain(opts.chain, {
config,
db,
Expand All @@ -144,7 +151,13 @@ export class BeaconNode {
{config, db, logger: logger.child(opts.logger.eth1), signal},
anchorState
),
executionEngine: initializeExecutionEngine(opts.executionEngine, signal),
executionEngine: initializeExecutionEngine(
{
...opts.executionEngine,
transitionConfig,
},
signal
),
});

// Load persisted data from disk to in-memory caches
Expand Down
40 changes: 38 additions & 2 deletions packages/lodestar/test/unit/executionEngine/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import chai, {expect} from "chai";
import chaiAsPromised from "chai-as-promised";
import {fastify} from "fastify";
import {AbortController} from "@chainsafe/abort-controller";
import {ExecutionEngineHttp, parseExecutionPayload, serializeExecutionPayload} from "../../../src/executionEngine/http";
import {
ExecutionEngineHttp,
parseExecutionPayload,
serializeExecutionPayload,
serializeTransitionConfig,
} from "../../../src/executionEngine/http";
import {TransitionConfig} from "../../../src/executionEngine/interface";
import {ZERO_HASH} from "../../../src/constants";

chai.use(chaiAsPromised);

Expand All @@ -18,6 +25,7 @@ describe("ExecutionEngine / http", () => {
let executionEngine: ExecutionEngineHttp;
let returnValue: unknown = {};
let reqJsonRpcPayload: unknown = {};
let transitionConfig: TransitionConfig;

before("Prepare server", async () => {
const controller = new AbortController();
Expand All @@ -35,8 +43,36 @@ describe("ExecutionEngine / http", () => {
});

const baseUrl = await server.listen(0);
transitionConfig = {terminalTotalDifficulty: BigInt(0), terminalBlockHash: ZERO_HASH, terminalBlockNumber: 0};

executionEngine = new ExecutionEngineHttp({urls: [baseUrl]}, controller.signal);
executionEngine = new ExecutionEngineHttp({urls: [baseUrl], transitionConfig}, controller.signal);
});

it("exchangeTransitionConfiguration", async () => {
/**
* curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"engine_exchangeTransitionConfigurationV1","params":[{"terminalTotalDifficulty":"0x0","terminalBlockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","terminalBlockNumber":"0x0"}],"id":67}' http://localhost:8550
*/

const request = {
jsonrpc: "2.0",
method: "engine_exchangeTransitionConfigurationV1",
params: [serializeTransitionConfig(transitionConfig)],
};
const response = {
jsonrpc: "2.0",
id: 67,
result: {
terminalTotalDifficulty: "0x0",
terminalBlockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
terminalBlockNumber: "0x0",
},
};
returnValue = response;

const elConfig = await executionEngine.exchangeTransitionConfiguration();

expect(elConfig).to.deep.equal(response.result, "Transition config mismatch");
expect(reqJsonRpcPayload).to.deep.equal(request, "Wrong request JSON RPC payload");
});

it("getPayload", async () => {
Expand Down