Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/prover/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export interface ELAccessListResponse {
export type ELStorageProof = Pick<ELProof, "storageHash" | "storageProof">;

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;
Expand Down
2 changes: 2 additions & 0 deletions packages/prover/src/utils/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,6 +20,7 @@ export const verifiableMethodHandlers: Record<string, ELVerifiedRequestHandler<a
eth_getTransactionCount: eth_getTransactionCount,
eth_getBlockByHash: eth_getBlockByHash,
eth_getBlockByNumber: eth_getBlockByNumber,
eth_blockNumber: eth_blockNumber,
eth_getCode: eth_getCode,
eth_call: eth_call,
eth_estimateGas: eth_estimateGas,
Expand Down
45 changes: 45 additions & 0 deletions packages/prover/src/verified_requests/eth_blockNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {ELVerifiedRequestHandler} from "../interfaces.js";
import {numberToHex} from "../utils/conversion.js";
import {
getErrorResponseForRequestWithFailedVerification,
getResponseForRequest,
getVerificationFailedMessage,
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 {
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"));
}
};
132 changes: 132 additions & 0 deletions packages/prover/test/unit/verified_requests/eth_blockNumber.test.ts
Original file line number Diff line number Diff line change
@@ -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"),
},
});
});
});
Loading