diff --git a/packages/prover/src/types.ts b/packages/prover/src/types.ts index bc9de5dc7df9..ccba44419442 100644 --- a/packages/prover/src/types.ts +++ b/packages/prover/src/types.ts @@ -146,6 +146,7 @@ export interface ELAccessListResponse { export type ELStorageProof = Pick; export type ELApi = { + eth_blockNumber: () => HexString; eth_getBalance: (address: string, block?: number | string) => string; eth_createAccessList: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => ELAccessListResponse; eth_call: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => HexString; diff --git a/packages/prover/src/utils/process.ts b/packages/prover/src/utils/process.ts index 5ed547e87d77..fa405e5872d9 100644 --- a/packages/prover/src/utils/process.ts +++ b/packages/prover/src/utils/process.ts @@ -7,6 +7,7 @@ import {eth_estimateGas} from "../verified_requests/eth_estimateGas.js"; import {eth_getBalance} from "../verified_requests/eth_getBalance.js"; import {eth_getBlockByHash} from "../verified_requests/eth_getBlockByHash.js"; import {eth_getBlockByNumber} from "../verified_requests/eth_getBlockByNumber.js"; +import {eth_blockNumber} from "../verified_requests/eth_blockNumber.js"; import {eth_getCode} from "../verified_requests/eth_getCode.js"; import {eth_getTransactionCount} from "../verified_requests/eth_getTransactionCount.js"; import {getResponseForRequest, isBatchRequest, isRequest} from "./json_rpc.js"; @@ -19,6 +20,7 @@ export const verifiableMethodHandlers: Record = async ({payload, rpc, logger, proofProvider}) => { + try { + const elResponse = await rpc.request("eth_blockNumber", [], {raiseError: false}); + + if (!isValidResponse(elResponse)) { + throw new Error("Invalid response from EL for eth_blockNumber"); + } + + const elBlockNumber = parseInt(elResponse.result, 16); + const executionPayload = await proofProvider.getExecutionPayload("latest"); + const clBlockNumber = executionPayload.blockNumber; + + // EL must not be behind the CL's trusted view + if (elBlockNumber < clBlockNumber) { + throw new Error(`EL (${elBlockNumber}) is behind CL (${clBlockNumber})`); + } + + // EL must be within the drift window; beyond that, CL is too stale to verify "latest" + if (elBlockNumber - clBlockNumber > EL_CL_DRIFT_WINDOW) { + throw new Error( + `CL too stale: EL (${elBlockNumber}) is more than ${EL_CL_DRIFT_WINDOW} blocks ahead of CL (${clBlockNumber})` + ); + } + + // Return the CL-verified block number, not EL's — only the CL view is actually verified + return getResponseForRequest(payload, numberToHex(clBlockNumber)); + } catch (err) { + logger.error("Request could not be verified.", {method: payload.method}, err as Error); + return getErrorResponseForRequestWithFailedVerification(payload, getVerificationFailedMessage("eth_blockNumber")); + } +}; diff --git a/packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts b/packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts new file mode 100644 index 000000000000..e722c882692a --- /dev/null +++ b/packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts @@ -0,0 +1,132 @@ +import {describe, expect, it, vi} from "vitest"; +import {getEmptyLogger} from "@lodestar/logger/empty"; +import {VERIFICATION_FAILED_RESPONSE_CODE} from "../../../src/constants.js"; +import {ProofProvider} from "../../../src/proof_provider/proof_provider.js"; +import {getVerificationFailedMessage} from "../../../src/utils/json_rpc.js"; +import {ELRpcProvider} from "../../../src/utils/rpc_provider.js"; +import {eth_blockNumber} from "../../../src/verified_requests/eth_blockNumber.js"; + +describe("verified_requests / eth_blockNumber", () => { + const buildOptions = ({ + elResult, + clBlockNumber, + rejectCl, + }: { + elResult: unknown; + clBlockNumber?: number; + rejectCl?: boolean; + }) => ({ + logger: getEmptyLogger(), + proofProvider: { + getExecutionPayload: rejectCl + ? vi.fn().mockRejectedValue(new Error("No latest payload")) + : vi.fn().mockResolvedValue({blockNumber: clBlockNumber}), + } as unknown as ProofProvider, + rpc: { + request: vi.fn().mockResolvedValue(elResult), + batchRequest: vi.fn(), + getRequestId: () => (Math.random() * 10000).toFixed(0), + } as unknown as ELRpcProvider, + }); + + const payload = {jsonrpc: "2.0" as const, id: 1, method: "eth_blockNumber", params: [] as []}; + + it("returns the CL-verified block number when EL is within drift window (EL ahead by 2)", async () => { + // EL = 1002, CL = 1000; within drift window of 2 + const options = buildOptions({ + elResult: {jsonrpc: "2.0", id: "1", result: "0x3ea"}, // 1002 + clBlockNumber: 1000, + }); + + const response = await eth_blockNumber({...options, payload}); + + // Returns CL's block number (0x3e8 = 1000), not EL's + expect(response).toEqual({jsonrpc: "2.0", id: 1, result: "0x3e8"}); + expect(options.rpc.request).toHaveBeenCalledWith("eth_blockNumber", [], {raiseError: false}); + expect(options.proofProvider.getExecutionPayload).toHaveBeenCalledWith("latest"); + }); + + it("returns the CL-verified block number when EL equals CL", async () => { + const options = buildOptions({ + elResult: {jsonrpc: "2.0", id: "1", result: "0x3e8"}, // 1000 + clBlockNumber: 1000, + }); + + const response = await eth_blockNumber({...options, payload}); + + expect(response).toEqual({jsonrpc: "2.0", id: 1, result: "0x3e8"}); + }); + + it("returns error when EL is behind CL", async () => { + const options = buildOptions({ + elResult: {jsonrpc: "2.0", id: "1", result: "0x3e8"}, // 1000 + clBlockNumber: 2000, + }); + + const response = await eth_blockNumber({...options, payload}); + + expect(response).toEqual({ + jsonrpc: "2.0", + id: 1, + error: { + code: VERIFICATION_FAILED_RESPONSE_CODE, + message: getVerificationFailedMessage("eth_blockNumber"), + }, + }); + }); + + it("returns error when EL is more than 2 blocks ahead of CL (CL too stale)", async () => { + // EL = 1005, CL = 1000; drift of 5 exceeds window of 2 + const options = buildOptions({ + elResult: {jsonrpc: "2.0", id: "1", result: "0x3ed"}, // 1005 + clBlockNumber: 1000, + }); + + const response = await eth_blockNumber({...options, payload}); + + expect(response).toEqual({ + jsonrpc: "2.0", + id: 1, + error: { + code: VERIFICATION_FAILED_RESPONSE_CODE, + message: getVerificationFailedMessage("eth_blockNumber"), + }, + }); + }); + + it("returns error when EL returns an invalid response", async () => { + const options = buildOptions({ + elResult: {jsonrpc: "2.0", id: "1", error: {code: -32000, message: "server error"}}, + clBlockNumber: 1000, + }); + + const response = await eth_blockNumber({...options, payload}); + + expect(response).toEqual({ + jsonrpc: "2.0", + id: 1, + error: { + code: VERIFICATION_FAILED_RESPONSE_CODE, + message: getVerificationFailedMessage("eth_blockNumber"), + }, + }); + }); + + it("returns error when CL has no latest payload", async () => { + const options = buildOptions({ + elResult: {jsonrpc: "2.0", id: "1", result: "0x3e8"}, + rejectCl: true, + }); + + const response = await eth_blockNumber({...options, payload}); + + expect(response).toEqual({ + jsonrpc: "2.0", + id: 1, + error: { + code: VERIFICATION_FAILED_RESPONSE_CODE, + message: getVerificationFailedMessage("eth_blockNumber"), + }, + }); + }); +});