From 7ab28268ccb342cf2f5d2be6c7101509fcf757ca Mon Sep 17 00:00:00 2001 From: automoffgpt <131298582+automoffgpt@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:08:08 -0600 Subject: [PATCH 1/4] fix(prover): implement eth_getBlockNumber verified request handler Resolves #5796. The prover was forwarding eth_getBlockNumber to the execution layer instead of serving it locally from the PayloadStore, which defeats the purpose of the verified light client proxy. Changes: - Add eth_getBlockNumber handler in verified_requests/ that reads the latest block number from proofProvider.getExecutionPayload('latest') and returns it as a hex string - Register the handler in verifiableMethodHandlers in utils/process.ts - Add unit tests for happy path and verification-failed error path Note: Implementation assisted with AI (Claude). --- packages/prover/src/utils/process.ts | 2 + .../verified_requests/eth_getBlockNumber.ts | 23 ++++++ .../eth_getBlockNumber.test.ts | 73 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 packages/prover/src/verified_requests/eth_getBlockNumber.ts create mode 100644 packages/prover/test/unit/verified_requests/eth_getBlockNumber.test.ts diff --git a/packages/prover/src/utils/process.ts b/packages/prover/src/utils/process.ts index 5ed547e87d77..d8ed54346fe1 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_getBlockNumber} from "../verified_requests/eth_getBlockNumber.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, logger, proofProvider}) => { + try { + const executionPayload = await proofProvider.getExecutionPayload("latest"); + const blockNumber = numberToHex(executionPayload.blockNumber); + + return getResponseForRequest(payload, blockNumber); + } catch (err) { + logger.error("Request could not be verified.", {method: payload.method}, err as Error); + return getErrorResponseForRequestWithFailedVerification( + payload, + getVerificationFailedMessage("eth_getBlockNumber") + ); + } +}; diff --git a/packages/prover/test/unit/verified_requests/eth_getBlockNumber.test.ts b/packages/prover/test/unit/verified_requests/eth_getBlockNumber.test.ts new file mode 100644 index 000000000000..fad5dd8ef92e --- /dev/null +++ b/packages/prover/test/unit/verified_requests/eth_getBlockNumber.test.ts @@ -0,0 +1,73 @@ +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 {eth_getBlockNumber} from "../../../src/verified_requests/eth_getBlockNumber.js"; + +describe("verified_requests / eth_getBlockNumber", () => { + it("should return the valid json-rpc response with the latest block number", async () => { + const expectedBlockNumber = 1234567; + const options = { + logger: getEmptyLogger(), + proofProvider: { + getExecutionPayload: vi.fn().mockResolvedValue({blockNumber: expectedBlockNumber}), + } as unknown as ProofProvider, + rpc: { + request: vi.fn(), + batchRequest: vi.fn(), + getRequestId: () => (Math.random() * 10000).toFixed(0), + }, + }; + + const response = await eth_getBlockNumber({ + ...options, + payload: { + jsonrpc: "2.0", + id: 1, + method: "eth_getBlockNumber", + params: [] as [], + }, + }); + + expect(response).toEqual({ + jsonrpc: "2.0", + id: 1, + result: "0x12d687", + }); + expect(options.proofProvider.getExecutionPayload).toHaveBeenCalledWith("latest"); + }); + + it("should return the json-rpc response with error when no latest payload is available", async () => { + const options = { + logger: getEmptyLogger(), + proofProvider: { + getExecutionPayload: vi.fn().mockRejectedValue(new Error("No latest payload")), + } as unknown as ProofProvider, + rpc: { + request: vi.fn(), + batchRequest: vi.fn(), + getRequestId: () => (Math.random() * 10000).toFixed(0), + }, + }; + + const response = await eth_getBlockNumber({ + ...options, + payload: { + jsonrpc: "2.0", + id: 2, + method: "eth_getBlockNumber", + params: [] as [], + }, + }); + + expect(response).toEqual({ + jsonrpc: "2.0", + id: 2, + error: { + code: VERIFICATION_FAILED_RESPONSE_CODE, + message: getVerificationFailedMessage("eth_getBlockNumber"), + }, + }); + }); +}); From 27861c52258343fe893e7fb0caf5b89b5e957922 Mon Sep 17 00:00:00 2001 From: automoffgpt <131298582+automoffgpt@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:23:34 -0600 Subject: [PATCH 2/4] fix(prover): rename eth_getBlockNumber handler to eth_blockNumber The standard Ethereum JSON-RPC method is eth_blockNumber, not eth_getBlockNumber. The incorrect key in verifiableMethodHandlers caused standard-compliant requests to bypass the verified handler entirely. Changes: - Rename eth_getBlockNumber.ts -> eth_blockNumber.ts - Rename eth_getBlockNumber.test.ts -> eth_blockNumber.test.ts - Rename exported handler constant to eth_blockNumber - Update registration key in verifiableMethodHandlers - Update error message string to eth_blockNumber - Update test: method field, import, and expected error message Addresses bot review feedback on #9203. --- packages/prover/src/utils/process.ts | 4 ++-- .../{eth_getBlockNumber.ts => eth_blockNumber.ts} | 4 ++-- ...BlockNumber.test.ts => eth_blockNumber.test.ts} | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) rename packages/prover/src/verified_requests/{eth_getBlockNumber.ts => eth_blockNumber.ts} (80%) rename packages/prover/test/unit/verified_requests/{eth_getBlockNumber.test.ts => eth_blockNumber.test.ts} (82%) diff --git a/packages/prover/src/utils/process.ts b/packages/prover/src/utils/process.ts index d8ed54346fe1..fa405e5872d9 100644 --- a/packages/prover/src/utils/process.ts +++ b/packages/prover/src/utils/process.ts @@ -7,7 +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_getBlockNumber} from "../verified_requests/eth_getBlockNumber.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"; @@ -20,7 +20,7 @@ export const verifiableMethodHandlers: Record = async ({payload, logger, proofProvider}) => { +export const eth_blockNumber: ELVerifiedRequestHandler<[], string> = async ({payload, logger, proofProvider}) => { try { const executionPayload = await proofProvider.getExecutionPayload("latest"); const blockNumber = numberToHex(executionPayload.blockNumber); @@ -17,7 +17,7 @@ export const eth_getBlockNumber: ELVerifiedRequestHandler<[], string> = async ({ logger.error("Request could not be verified.", {method: payload.method}, err as Error); return getErrorResponseForRequestWithFailedVerification( payload, - getVerificationFailedMessage("eth_getBlockNumber") + getVerificationFailedMessage("eth_blockNumber") ); } }; diff --git a/packages/prover/test/unit/verified_requests/eth_getBlockNumber.test.ts b/packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts similarity index 82% rename from packages/prover/test/unit/verified_requests/eth_getBlockNumber.test.ts rename to packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts index fad5dd8ef92e..0d52b44ca0f8 100644 --- a/packages/prover/test/unit/verified_requests/eth_getBlockNumber.test.ts +++ b/packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts @@ -3,9 +3,9 @@ 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 {eth_getBlockNumber} from "../../../src/verified_requests/eth_getBlockNumber.js"; +import {eth_blockNumber} from "../../../src/verified_requests/eth_blockNumber.js"; -describe("verified_requests / eth_getBlockNumber", () => { +describe("verified_requests / eth_blockNumber", () => { it("should return the valid json-rpc response with the latest block number", async () => { const expectedBlockNumber = 1234567; const options = { @@ -20,12 +20,12 @@ describe("verified_requests / eth_getBlockNumber", () => { }, }; - const response = await eth_getBlockNumber({ + const response = await eth_blockNumber({ ...options, payload: { jsonrpc: "2.0", id: 1, - method: "eth_getBlockNumber", + method: "eth_blockNumber", params: [] as [], }, }); @@ -51,12 +51,12 @@ describe("verified_requests / eth_getBlockNumber", () => { }, }; - const response = await eth_getBlockNumber({ + const response = await eth_blockNumber({ ...options, payload: { jsonrpc: "2.0", id: 2, - method: "eth_getBlockNumber", + method: "eth_blockNumber", params: [] as [], }, }); @@ -66,7 +66,7 @@ describe("verified_requests / eth_getBlockNumber", () => { id: 2, error: { code: VERIFICATION_FAILED_RESPONSE_CODE, - message: getVerificationFailedMessage("eth_getBlockNumber"), + message: getVerificationFailedMessage("eth_blockNumber"), }, }); }); From 1095a692d60ccaa884d74faa2d7f559dede7717a Mon Sep 17 00:00:00 2001 From: Sam Hartley Date: Mon, 13 Apr 2026 10:08:37 -0600 Subject: [PATCH 3/4] fix(prover): add EL verification to eth_blockNumber handler Fetch block number from EL node and verify against CL light client before returning, matching the pattern used by other verified_requests handlers. Add eth_blockNumber to ELApi type map. --- packages/prover/src/types.ts | 1 + .../src/verified_requests/eth_blockNumber.ts | 20 +++- .../verified_requests/eth_blockNumber.test.ts | 107 ++++++++++++++++-- 3 files changed, 116 insertions(+), 12 deletions(-) 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/verified_requests/eth_blockNumber.ts b/packages/prover/src/verified_requests/eth_blockNumber.ts index f9cb1224e042..8f04467ec427 100644 --- a/packages/prover/src/verified_requests/eth_blockNumber.ts +++ b/packages/prover/src/verified_requests/eth_blockNumber.ts @@ -4,15 +4,29 @@ import { getErrorResponseForRequestWithFailedVerification, getResponseForRequest, getVerificationFailedMessage, + isValidResponse, } from "../utils/json_rpc.js"; // eslint-disable-next-line @typescript-eslint/naming-convention -export const eth_blockNumber: ELVerifiedRequestHandler<[], string> = async ({payload, logger, proofProvider}) => { +export const eth_blockNumber: ELVerifiedRequestHandler<[], string> = 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 blockNumber = numberToHex(executionPayload.blockNumber); + const clBlockNumber = executionPayload.blockNumber; + + if (elBlockNumber < clBlockNumber) { + throw new Error( + `EL block number (${elBlockNumber}) is behind CL block number (${clBlockNumber})` + ); + } - return getResponseForRequest(payload, blockNumber); + return getResponseForRequest(payload, numberToHex(elBlockNumber)); } catch (err) { logger.error("Request could not be verified.", {method: payload.method}, err as Error); return getErrorResponseForRequestWithFailedVerification( diff --git a/packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts b/packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts index 0d52b44ca0f8..4cff0b702011 100644 --- a/packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts +++ b/packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts @@ -3,21 +3,28 @@ 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", () => { - it("should return the valid json-rpc response with the latest block number", async () => { - const expectedBlockNumber = 1234567; + it("should return verified block number when EL is at or ahead of CL", async () => { + const elBlockNumber = 1234567; + const clBlockNumber = 1234560; + const options = { logger: getEmptyLogger(), proofProvider: { - getExecutionPayload: vi.fn().mockResolvedValue({blockNumber: expectedBlockNumber}), + getExecutionPayload: vi.fn().mockResolvedValue({blockNumber: clBlockNumber}), } as unknown as ProofProvider, rpc: { - request: vi.fn(), + request: vi.fn().mockResolvedValue({ + jsonrpc: "2.0", + id: "1", + result: "0x12d687", // 1234567 + }), batchRequest: vi.fn(), getRequestId: () => (Math.random() * 10000).toFixed(0), - }, + } as unknown as ELRpcProvider, }; const response = await eth_blockNumber({ @@ -35,20 +42,28 @@ describe("verified_requests / eth_blockNumber", () => { id: 1, result: "0x12d687", }); + expect(options.rpc.request).toHaveBeenCalledWith("eth_blockNumber", [], {raiseError: false}); expect(options.proofProvider.getExecutionPayload).toHaveBeenCalledWith("latest"); }); - it("should return the json-rpc response with error when no latest payload is available", async () => { + it("should return error when EL block number is behind CL", async () => { + const elBlockNumber = 1000; + const clBlockNumber = 2000; + const options = { logger: getEmptyLogger(), proofProvider: { - getExecutionPayload: vi.fn().mockRejectedValue(new Error("No latest payload")), + getExecutionPayload: vi.fn().mockResolvedValue({blockNumber: clBlockNumber}), } as unknown as ProofProvider, rpc: { - request: vi.fn(), + request: vi.fn().mockResolvedValue({ + jsonrpc: "2.0", + id: "1", + result: "0x3e8", // 1000 + }), batchRequest: vi.fn(), getRequestId: () => (Math.random() * 10000).toFixed(0), - }, + } as unknown as ELRpcProvider, }; const response = await eth_blockNumber({ @@ -70,4 +85,78 @@ describe("verified_requests / eth_blockNumber", () => { }, }); }); + + it("should return error when EL returns invalid response", async () => { + const options = { + logger: getEmptyLogger(), + proofProvider: { + getExecutionPayload: vi.fn().mockResolvedValue({blockNumber: 1000}), + } as unknown as ProofProvider, + rpc: { + request: vi.fn().mockResolvedValue({ + jsonrpc: "2.0", + id: "1", + error: {code: -32000, message: "server error"}, + }), + batchRequest: vi.fn(), + getRequestId: () => (Math.random() * 10000).toFixed(0), + } as unknown as ELRpcProvider, + }; + + const response = await eth_blockNumber({ + ...options, + payload: { + jsonrpc: "2.0", + id: 3, + method: "eth_blockNumber", + params: [] as [], + }, + }); + + expect(response).toEqual({ + jsonrpc: "2.0", + id: 3, + error: { + code: VERIFICATION_FAILED_RESPONSE_CODE, + message: getVerificationFailedMessage("eth_blockNumber"), + }, + }); + }); + + it("should return error when CL has no latest payload", async () => { + const options = { + logger: getEmptyLogger(), + proofProvider: { + getExecutionPayload: vi.fn().mockRejectedValue(new Error("No latest payload")), + } as unknown as ProofProvider, + rpc: { + request: vi.fn().mockResolvedValue({ + jsonrpc: "2.0", + id: "1", + result: "0x12d687", + }), + batchRequest: vi.fn(), + getRequestId: () => (Math.random() * 10000).toFixed(0), + } as unknown as ELRpcProvider, + }; + + const response = await eth_blockNumber({ + ...options, + payload: { + jsonrpc: "2.0", + id: 4, + method: "eth_blockNumber", + params: [] as [], + }, + }); + + expect(response).toEqual({ + jsonrpc: "2.0", + id: 4, + error: { + code: VERIFICATION_FAILED_RESPONSE_CODE, + message: getVerificationFailedMessage("eth_blockNumber"), + }, + }); + }); }); From 92bf3ac49f3142973522e612ac196fefc760d687 Mon Sep 17 00:00:00 2001 From: Sam Hartley Date: Tue, 14 Apr 2026 13:57:29 -0600 Subject: [PATCH 4/4] fix(prover): return CL-verified block number with EL drift window check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous logic returned EL's block number when EL >= CL, but that value was not actually CL-verified — only 'not contradicted by CL'. Now we return the CL block number directly and use the EL response only as a sanity check that CL isn't too stale (drift window of 2 blocks). --- .../src/verified_requests/eth_blockNumber.ts | 20 +- .../verified_requests/eth_blockNumber.test.ts | 194 ++++++++---------- 2 files changed, 96 insertions(+), 118 deletions(-) diff --git a/packages/prover/src/verified_requests/eth_blockNumber.ts b/packages/prover/src/verified_requests/eth_blockNumber.ts index 8f04467ec427..19ae046aa64d 100644 --- a/packages/prover/src/verified_requests/eth_blockNumber.ts +++ b/packages/prover/src/verified_requests/eth_blockNumber.ts @@ -7,6 +7,10 @@ import { isValidResponse, } from "../utils/json_rpc.js"; +// Maximum blocks EL can be ahead of CL before we consider CL too stale to call "latest". +// EL is typically 0–2 blocks ahead of the CL light client view under normal operation. +const EL_CL_DRIFT_WINDOW = 2; + // eslint-disable-next-line @typescript-eslint/naming-convention export const eth_blockNumber: ELVerifiedRequestHandler<[], string> = async ({payload, rpc, logger, proofProvider}) => { try { @@ -20,18 +24,22 @@ export const eth_blockNumber: ELVerifiedRequestHandler<[], string> = async ({pay 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( - `EL block number (${elBlockNumber}) is behind CL block number (${clBlockNumber})` + `CL too stale: EL (${elBlockNumber}) is more than ${EL_CL_DRIFT_WINDOW} blocks ahead of CL (${clBlockNumber})` ); } - return getResponseForRequest(payload, numberToHex(elBlockNumber)); + // 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") - ); + 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 index 4cff0b702011..e722c882692a 100644 --- a/packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts +++ b/packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts @@ -7,78 +7,86 @@ import {ELRpcProvider} from "../../../src/utils/rpc_provider.js"; import {eth_blockNumber} from "../../../src/verified_requests/eth_blockNumber.js"; describe("verified_requests / eth_blockNumber", () => { - it("should return verified block number when EL is at or ahead of CL", async () => { - const elBlockNumber = 1234567; - const clBlockNumber = 1234560; - - const options = { - logger: getEmptyLogger(), - proofProvider: { - getExecutionPayload: vi.fn().mockResolvedValue({blockNumber: clBlockNumber}), - } as unknown as ProofProvider, - rpc: { - request: vi.fn().mockResolvedValue({ - jsonrpc: "2.0", - id: "1", - result: "0x12d687", // 1234567 - }), - batchRequest: vi.fn(), - getRequestId: () => (Math.random() * 10000).toFixed(0), - } as unknown as ELRpcProvider, - }; - - const response = await eth_blockNumber({ - ...options, - payload: { - jsonrpc: "2.0", - id: 1, - method: "eth_blockNumber", - params: [] as [], - }, + 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, - result: "0x12d687", + error: { + code: VERIFICATION_FAILED_RESPONSE_CODE, + message: getVerificationFailedMessage("eth_blockNumber"), + }, }); - expect(options.rpc.request).toHaveBeenCalledWith("eth_blockNumber", [], {raiseError: false}); - expect(options.proofProvider.getExecutionPayload).toHaveBeenCalledWith("latest"); }); - it("should return error when EL block number is behind CL", async () => { - const elBlockNumber = 1000; - const clBlockNumber = 2000; - - const options = { - logger: getEmptyLogger(), - proofProvider: { - getExecutionPayload: vi.fn().mockResolvedValue({blockNumber: clBlockNumber}), - } as unknown as ProofProvider, - rpc: { - request: vi.fn().mockResolvedValue({ - jsonrpc: "2.0", - id: "1", - result: "0x3e8", // 1000 - }), - batchRequest: vi.fn(), - getRequestId: () => (Math.random() * 10000).toFixed(0), - } as unknown as ELRpcProvider, - }; - - const response = await eth_blockNumber({ - ...options, - payload: { - jsonrpc: "2.0", - id: 2, - method: "eth_blockNumber", - params: [] as [], - }, + 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: 2, + id: 1, error: { code: VERIFICATION_FAILED_RESPONSE_CODE, message: getVerificationFailedMessage("eth_blockNumber"), @@ -86,36 +94,17 @@ describe("verified_requests / eth_blockNumber", () => { }); }); - it("should return error when EL returns invalid response", async () => { - const options = { - logger: getEmptyLogger(), - proofProvider: { - getExecutionPayload: vi.fn().mockResolvedValue({blockNumber: 1000}), - } as unknown as ProofProvider, - rpc: { - request: vi.fn().mockResolvedValue({ - jsonrpc: "2.0", - id: "1", - error: {code: -32000, message: "server error"}, - }), - batchRequest: vi.fn(), - getRequestId: () => (Math.random() * 10000).toFixed(0), - } as unknown as ELRpcProvider, - }; - - const response = await eth_blockNumber({ - ...options, - payload: { - jsonrpc: "2.0", - id: 3, - method: "eth_blockNumber", - params: [] as [], - }, + 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: 3, + id: 1, error: { code: VERIFICATION_FAILED_RESPONSE_CODE, message: getVerificationFailedMessage("eth_blockNumber"), @@ -123,36 +112,17 @@ describe("verified_requests / eth_blockNumber", () => { }); }); - it("should return error when CL has no latest payload", async () => { - const options = { - logger: getEmptyLogger(), - proofProvider: { - getExecutionPayload: vi.fn().mockRejectedValue(new Error("No latest payload")), - } as unknown as ProofProvider, - rpc: { - request: vi.fn().mockResolvedValue({ - jsonrpc: "2.0", - id: "1", - result: "0x12d687", - }), - batchRequest: vi.fn(), - getRequestId: () => (Math.random() * 10000).toFixed(0), - } as unknown as ELRpcProvider, - }; - - const response = await eth_blockNumber({ - ...options, - payload: { - jsonrpc: "2.0", - id: 4, - method: "eth_blockNumber", - params: [] as [], - }, + 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: 4, + id: 1, error: { code: VERIFICATION_FAILED_RESPONSE_CODE, message: getVerificationFailedMessage("eth_blockNumber"),