diff --git a/packages/beacon-node/src/execution/engine/http.ts b/packages/beacon-node/src/execution/engine/http.ts index 0d2656ae46e..d3bbb0a02f3 100644 --- a/packages/beacon-node/src/execution/engine/http.ts +++ b/packages/beacon-node/src/execution/engine/http.ts @@ -30,12 +30,16 @@ import { } from "./interface.js"; import {PayloadIdCache} from "./payloadIdCache.js"; import { + EngineApiParamTypes, EngineApiRpcParamTypes, EngineApiRpcReturnTypes, + EngineNewPayloadMethod, ExecutionPayloadBody, assertReqSizeLimit, deserializeBlobAndProofs, deserializeExecutionPayloadBody, + getPayloadMethodByFork, + newPayloadMethodByFork, parseExecutionPayload, serializeBeaconBlockRoot, serializeExecutionPayload, @@ -204,58 +208,44 @@ export class ExecutionEngineHttp implements IExecutionEngine { parentBlockRoot?: Root, executionRequests?: ExecutionRequests ): Promise { - const method = - ForkSeq[fork] >= ForkSeq.electra - ? "engine_newPayloadV4" - : ForkSeq[fork] >= ForkSeq.deneb - ? "engine_newPayloadV3" - : ForkSeq[fork] >= ForkSeq.capella - ? "engine_newPayloadV2" - : "engine_newPayloadV1"; - - const serializedExecutionPayload = serializeExecutionPayload(fork, executionPayload); + const method = newPayloadMethodByFork(fork); let engineRequest: EngineRequest; - if (ForkSeq[fork] >= ForkSeq.deneb) { - if (versionedHashes === undefined) { - throw Error(`versionedHashes required in notifyNewPayload for fork=${fork}`); - } - if (parentBlockRoot === undefined) { - throw Error(`parentBlockRoot required in notifyNewPayload for fork=${fork}`); - } - const serializedVersionedHashes = serializeVersionedHashes(versionedHashes); - const parentBeaconBlockRoot = serializeBeaconBlockRoot(parentBlockRoot); - - if (ForkSeq[fork] >= ForkSeq.electra) { - if (executionRequests === undefined) { - throw Error(`executionRequests required in notifyNewPayload for fork=${fork}`); - } - const serializedExecutionRequests = serializeExecutionRequests(executionRequests); + switch (method) { + case "engine_newPayloadV1": + case "engine_newPayloadV2": engineRequest = { - method: "engine_newPayloadV4", - params: [ - serializedExecutionPayload, - serializedVersionedHashes, - parentBeaconBlockRoot, - serializedExecutionRequests, - ], + method, + params: generateSerializedNewPayloadParams(fork, method, {executionPayload}), methodOpts: notifyNewPayloadOpts, }; - } else { + break; + case "engine_newPayloadV3": engineRequest = { - method: "engine_newPayloadV3", - params: [serializedExecutionPayload, serializedVersionedHashes, parentBeaconBlockRoot], + method, + params: generateSerializedNewPayloadParams(fork, method, { + executionPayload, + versionedHashes, + parentBlockRoot, + }), methodOpts: notifyNewPayloadOpts, }; - } - } else { - const method = ForkSeq[fork] >= ForkSeq.capella ? "engine_newPayloadV2" : "engine_newPayloadV1"; - engineRequest = { - method, - params: [serializedExecutionPayload], - methodOpts: notifyNewPayloadOpts, - }; + break; + case "engine_newPayloadV4": + engineRequest = { + method, + params: generateSerializedNewPayloadParams(fork, method, { + executionPayload, + versionedHashes, + parentBlockRoot, + executionRequests, + }), + methodOpts: notifyNewPayloadOpts, + }; + break; + default: + throw Error(`Unsupported method: ${method}`); } const {status, latestValidHash, validationError} = await ( @@ -415,14 +405,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { executionRequests?: ExecutionRequests; shouldOverrideBuilder?: boolean; }> { - const method = - ForkSeq[fork] >= ForkSeq.electra - ? "engine_getPayloadV4" - : ForkSeq[fork] >= ForkSeq.deneb - ? "engine_getPayloadV3" - : ForkSeq[fork] >= ForkSeq.capella - ? "engine_getPayloadV2" - : "engine_getPayloadV1"; + const method = getPayloadMethodByFork(fork); const payloadResponse = await this.rpc.fetchWithRetries< EngineApiRpcReturnTypes[typeof method], EngineApiRpcParamTypes[typeof method] @@ -575,6 +558,45 @@ type EngineRequestKey = keyof EngineApiRpcParamTypes; type EngineRequestByKey = { [K in EngineRequestKey]: {method: K; params: EngineApiRpcParamTypes[K]; methodOpts: ReqOpts}; }; -type EngineRequest = EngineRequestByKey[EngineRequestKey]; +export type EngineRequest = EngineRequestByKey[EngineRequestKey]; type EngineResponseByKey = {[K in EngineRequestKey]: EngineApiRpcReturnTypes[K]}; type EngineResponse = EngineResponseByKey[EngineRequestKey]; + +/** + * + * Generate serialized params according to `method`. + * + * `params` must not allow numeric keys because we rely Object.keys() to return keys in insertion order. + * eg. {versionedHashes, parentBlockRoot, executionRequests} has to return [versionedHashes, parentBlockRoot, executionRequests] + * Having numeric key will not guarantee keys in insertion order. + */ +export function generateSerializedNewPayloadParams( + fork: ForkName, + method: T, + params: {[K in keyof EngineApiParamTypes[T]]: EngineApiParamTypes[T][K] | undefined} +): EngineApiRpcParamTypes[T] { + const result = []; + for (const [key, value] of Object.entries(params)) { + if (value === undefined) { + throw new Error(`${key} is required for method=${method} in fork=${fork}`); + } + switch (key) { + case "executionPayload": + result.push(serializeExecutionPayload(fork, value as ExecutionPayload)); + break; + case "versionedHashes": + result.push(serializeVersionedHashes(value as VersionedHashes)); + break; + case "parentBlockRoot": + result.push(serializeBeaconBlockRoot(value as Root)); + break; + case "executionRequests": + result.push(serializeExecutionRequests(value as ExecutionRequests)); + break; + default: + result.push(value); + } + } + + return result as EngineApiRpcParamTypes[T]; +} diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index 24de1b9e657..addec7700fd 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -7,6 +7,9 @@ import { ForkName, ForkSeq, WITHDRAWAL_REQUEST_TYPE, + isForkBlobs, + isForkPostElectra, + isForkWithdrawals, } from "@lodestar/params"; import {ExecutionPayload, ExecutionRequests, Root, Wei, bellatrix, capella, deneb, electra, ssz} from "@lodestar/types"; import {BlobAndProof} from "@lodestar/types/deneb"; @@ -82,6 +85,22 @@ export type EngineApiRpcParamTypes = { engine_getBlobsV1: [DATA[]]; }; +// Can extend keys to other engine API whenever it sees fit +export type EngineApiParamTypes = { + engine_newPayloadV1: {executionPayload: ExecutionPayload}; + engine_newPayloadV2: {executionPayload: ExecutionPayload}; + engine_newPayloadV3: {executionPayload: ExecutionPayload; versionedHashes: VersionedHashes; parentBlockRoot: Root}; + engine_newPayloadV4: { + executionPayload: ExecutionPayload; + versionedHashes: VersionedHashes; + parentBlockRoot: Root; + executionRequests: ExecutionRequests; + }; +}; + +export type EngineGetPayloadMethod = keyof EngineApiRpcParamTypes & `engine_getPayloadV${number}`; +export type EngineNewPayloadMethod = keyof EngineApiRpcParamTypes & `engine_newPayloadV${number}`; + export type PayloadStatus = { status: ExecutionPayloadStatus; latestValidHash: DATA | null; @@ -222,6 +241,32 @@ export interface BlobsBundleRpc { proofs: DATA[]; // some ELs could also provide proofs, each 48 bytes } +export function getPayloadMethodByFork(fork: ForkName): EngineGetPayloadMethod { + switch (true) { + case isForkPostElectra(fork): + return "engine_getPayloadV4"; + case isForkBlobs(fork): + return "engine_getPayloadV3"; + case isForkWithdrawals(fork): + return "engine_getPayloadV2"; + default: + return "engine_getPayloadV1"; + } +} + +export function newPayloadMethodByFork(fork: ForkName): EngineNewPayloadMethod { + switch (true) { + case isForkPostElectra(fork): + return "engine_newPayloadV4"; + case isForkBlobs(fork): + return "engine_newPayloadV3"; + case isForkWithdrawals(fork): + return "engine_newPayloadV2"; + default: + return "engine_newPayloadV1"; + } +} + export function serializeExecutionPayload(fork: ForkName, data: ExecutionPayload): ExecutionPayloadRpc { const payload: ExecutionPayloadRpc = { parentHash: bytesToData(data.parentHash), diff --git a/packages/beacon-node/test/unit/execution/engine/params.test.ts b/packages/beacon-node/test/unit/execution/engine/params.test.ts new file mode 100644 index 00000000000..42ca2b472ad --- /dev/null +++ b/packages/beacon-node/test/unit/execution/engine/params.test.ts @@ -0,0 +1,165 @@ +import {ForkName} from "@lodestar/params"; +import {describe, expect, it} from "vitest"; +import {dataToBytes} from "../../../../src/eth1/provider/utils.js"; +import {EngineRequest, generateSerializedNewPayloadParams} from "../../../../src/execution/engine/http.js"; +import { + deserializeExecutionRequests, + parseExecutionPayload, + serializeBeaconBlockRoot, + serializeExecutionPayload, + serializeExecutionRequests, + serializeVersionedHashes, +} from "../../../../src/execution/engine/types.js"; + +describe("execution / engine / types", () => { + describe("generateSerializedNewPayloadParams", () => { + const executionPayloadV1 = { + parentHash: "0x3b8fb240d288781d4aac94d3fd16809ee413bc99294a085798a589dae51ddd4a", + feeRecipient: "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + stateRoot: "0xca3149fa9e37db08d1cd49c9061db1002ef1cd58db2210f2115c8c989b2bdf45", + receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + logsBloom: + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + prevRandao: "0x0000000000000000000000000000000000000000000000000000000000000000", + blockNumber: "0x1", + gasLimit: "0x1c9c380", + gasUsed: "0x0", + timestamp: "0x5", + extraData: "0x", + baseFeePerGas: "0x7", + blockHash: "0x3559e851470f6e7bbed1db474980683e8c315bfce99b2a6ef47c057c04de7858", + transactions: [ + "0x03f88f0780843b9aca008506fc23ac00830186a09400000000000000000000000000000000000001008080c001e1a0010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c44401401a0840650aa8f74d2b07f40067dc33b715078d73422f01da17abdbd11e02bbdfda9a04b2260f6022bf53eadb337b3e59514936f7317d872defb891a708ee279bdca90", + "0x03f88f0701843b9aca008506fc23ac00830186a09400000000000000000000000000000000000001008080c001e1a001521d528ad0c760354a4f0496776cf14a92fe1fb5d50e959dcea1a489c7c83101a0a86c1fd8c2e74820686937f5c1bfe836e2fb622ac9fcbebdc4ab4357f2dbbc61a05c3b2b44ff8252f78d70aeb33f8ba09beaeadad1b376a57d34fa720bbc4a18ee", + "0x03f88f0702843b9aca008506fc23ac00830186a09400000000000000000000000000000000000001008080c001e1a001453362c360fdd8832e3539d463e6d64b2ee320ac6a08885df6083644a063e701a037a728aec08aefffa702a2ca620db89caf3e46ab7f25f7646fc951510991badca065d846f046357af39bb739b161233fce73ddfe0bb87f2d28ef60dfe6dbb0128d", + ], + }; + const executionPayloadV2 = { + ...executionPayloadV1, + withdrawals: [ + {index: "0xf0", validatorIndex: "0xf0", address: "0x00000000000000000000000000000000000010f0", amount: "0x1"}, + {index: "0xf1", validatorIndex: "0xf1", address: "0x00000000000000000000000000000000000010f1", amount: "0x1"}, + ], + }; + const executionPayloadV3 = { + ...executionPayloadV2, + blobGasUsed: "0x0", + excessBlobGas: "0x0", + }; + + it("should serialize V1 params", () => { + const fork = ForkName.bellatrix; + const method = "engine_newPayloadV1"; + + const executionPayload = parseExecutionPayload(fork, executionPayloadV1).executionPayload; + const params = generateSerializedNewPayloadParams(fork, method, {executionPayload}); + + const engineRequest: EngineRequest = { + method, + params, + methodOpts: {}, + }; + + expect(engineRequest.params.length).toBe(1); + expect(engineRequest.params[0]).toStrictEqual(serializeExecutionPayload(fork, executionPayload)); + }); + it("should serialize V2 params", () => { + const fork = ForkName.capella; + const method = "engine_newPayloadV2"; + const executionPayload = parseExecutionPayload(fork, executionPayloadV2).executionPayload; + const params = generateSerializedNewPayloadParams(fork, method, {executionPayload}); + + const engineRequest: EngineRequest = { + method, + params, + methodOpts: {}, + }; + + expect(engineRequest.params.length).toBe(1); + expect(engineRequest.params[0]).toStrictEqual(serializeExecutionPayload(fork, executionPayload)); + }); + it("should serialize V3 params", () => { + const fork = ForkName.deneb; + const method = "engine_newPayloadV3"; + const executionPayload = parseExecutionPayload(fork, executionPayloadV3).executionPayload; + const versionedHashes = [dataToBytes("0x000657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014", null)]; + const parentBlockRoot = dataToBytes("0x169630f535b4a41330164c6e5c92b1224c0c407f582d407d0ac3d206cd32fd52", null); + const params = generateSerializedNewPayloadParams(fork, method, { + executionPayload, + versionedHashes, + parentBlockRoot, + }); + + const engineRequest: EngineRequest = { + method, + params, + methodOpts: {}, + }; + + expect(engineRequest.params.length).toBe(3); + expect(engineRequest.params[0]).toStrictEqual(serializeExecutionPayload(fork, executionPayload)); + expect(engineRequest.params[1]).toStrictEqual(serializeVersionedHashes(versionedHashes)); + expect(engineRequest.params[2]).toStrictEqual(serializeBeaconBlockRoot(parentBlockRoot)); + }); + it("should throw error on invalid V3 params", () => { + const fork = ForkName.deneb; + const method = "engine_newPayloadV3"; + const executionPayload = parseExecutionPayload(fork, executionPayloadV3).executionPayload; + const versionedHashes = undefined; + const parentBlockRoot = undefined; + + expect(() => + generateSerializedNewPayloadParams(fork, method, {executionPayload, versionedHashes, parentBlockRoot}) + ).toThrow("versionedHashes is required for method=engine_newPayloadV3 in fork=deneb"); + }); + it("should serialize V4 params", () => { + const fork = ForkName.electra; + const method = "engine_newPayloadV4"; + + const {executionPayload} = parseExecutionPayload(fork, executionPayloadV3); + const versionedHashes = [dataToBytes("0x000657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014", null)]; + const parentBlockRoot = dataToBytes("0x169630f535b4a41330164c6e5c92b1224c0c407f582d407d0ac3d206cd32fd52", null); + const executionRequests = deserializeExecutionRequests([ + "0x0096a96086cff07df17668f35f7418ef8798079167e3f4f9b72ecde17b28226137cf454ab1dd20ef5d924786ab3483c2f9003f5102dabe0a27b1746098d1dc17a5d3fbd478759fea9287e4e419b3c3cef20100000000000000b1acdb2c4d3df3f1b8d3bfd33421660df358d84d78d16c4603551935f4b67643373e7eb63dcb16ec359be0ec41fee33b03a16e80745f2374ff1d3c352508ac5d857c6476d3c3bcf7e6ca37427c9209f17be3af5264c0e2132b3dd1156c28b4e9f000000000000000a5c85a60ba2905c215f6a12872e62b1ee037051364244043a5f639aa81b04a204c55e7cc851f29c7c183be253ea1510b001db70c485b6264692f26b8aeaab5b0c384180df8e2184a21a808a3ec8e86ca01000000000000009561731785b48cf1886412234531e4940064584463e96ac63a1a154320227e333fb51addc4a89b7e0d3f862d7c1fd4ea03bd8eb3d8806f1e7daf591cbbbb92b0beb74d13c01617f22c5026b4f9f9f294a8a7c32db895de3b01bee0132c9209e1f100000000000000", + "0x01a94f5374fce5edbc8e2a8697c15331677e6ebf0b85103a5617937691dfeeb89b86a80d5dc9e3c9d3a1a0e7ce311e26e0bb732eabaa47ffa288f0d54de28209a62a7d29d0000000000000000000000000000000000000000000000000000010f698daeed734da114470da559bd4b4c7259e1f7952555241dcbc90cf194a2ef676fc6005f3672fada2a3645edb297a75530100000000000000", + "0x02a94f5374fce5edbc8e2a8697c15331677e6ebf0b85103a5617937691dfeeb89b86a80d5dc9e3c9d3a1a0e7ce311e26e0bb732eabaa47ffa288f0d54de28209a62a7d29d098daeed734da114470da559bd4b4c7259e1f7952555241dcbc90cf194a2ef676fc6005f3672fada2a3645edb297a7553", + ]); + + const params = generateSerializedNewPayloadParams(fork, method, { + executionPayload, + versionedHashes, + parentBlockRoot, + executionRequests, + }); + + const engineRequest: EngineRequest = { + method, + params, + methodOpts: {}, + }; + + expect(engineRequest.params.length).toBe(4); + expect(engineRequest.params[0]).toStrictEqual(serializeExecutionPayload(fork, executionPayload)); + expect(engineRequest.params[1]).toStrictEqual(serializeVersionedHashes(versionedHashes)); + expect(engineRequest.params[2]).toStrictEqual(serializeBeaconBlockRoot(parentBlockRoot)); + expect(engineRequest.params[3]).toStrictEqual(serializeExecutionRequests(executionRequests)); + }); + it("should throw error on invalid V4 params", () => { + const fork = ForkName.electra; + const method = "engine_newPayloadV4"; + const versionedHashes = [dataToBytes("0x000657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014", null)]; + const parentBlockRoot = dataToBytes("0x169630f535b4a41330164c6e5c92b1224c0c407f582d407d0ac3d206cd32fd52", null); + const executionPayload = parseExecutionPayload(fork, executionPayloadV3).executionPayload; + const executionRequests = undefined; + + expect(() => + generateSerializedNewPayloadParams(fork, method, { + executionPayload, + versionedHashes, + parentBlockRoot, + executionRequests, + }) + ).toThrow("executionRequests is required for method=engine_newPayloadV4 in fork=electra"); + }); + }); +});