Skip to content
Merged
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/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@lodestar/light-client": "^1.5.0",
"@ethereumjs/trie": "^5.0.4",
"@ethereumjs/util": "^8.0.5",
"@ethereumjs/block": "^4.2.1",
"@ethereumjs/rlp": "^4.0.1",
"ethereum-cryptography": "^1.2.0",
"http-proxy": "^1.18.1",
Expand Down
17 changes: 10 additions & 7 deletions packages/prover/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export type RootProviderInitOptions = {
wsCheckpoint?: string;
} & ConsensusNodeOptions;

export type ELRequestHandler = (payload: ELRequestPayload) => Promise<ELResponse | undefined>;
// The `undefined` is necessary to match the types for the web3 1.x
export type ELRequestHandler<Params = unknown[], Response = unknown> = (
payload: ELRequestPayload<Params>
) => Promise<ELResponse<Response> | undefined>;

// Modern providers uses this structure e.g. Web3 4.x
export interface EIP1193Provider {
Expand Down Expand Up @@ -46,16 +49,16 @@ export interface SendAsyncProvider {

export type Web3Provider = SendProvider | EthersProvider | SendAsyncProvider | RequestProvider | EIP1193Provider;

export type ELVerifiedRequestHandlerOpts<A = unknown> = {
payload: ELRequestPayload<A>;
handler: ELRequestHandler;
export type ELVerifiedRequestHandlerOpts<Params = unknown[], Response = unknown> = {
payload: ELRequestPayload<Params>;
handler: ELRequestHandler<Params, Response>;
proofProvider: ProofProvider;
logger: Logger;
};

export type ELVerifiedRequestHandler<A = unknown, R = unknown> = (
opts: ELVerifiedRequestHandlerOpts<A>
) => Promise<ELResponse<R>>;
export type ELVerifiedRequestHandler<Params = unknown[], Response = unknown> = (
opts: ELVerifiedRequestHandlerOpts<Params, Response>
) => Promise<ELResponse<Response>>;

// Either a logger is provided by user or user specify a log level
// If both are skipped then we don't log anything (useful for browser plugins)
Expand Down
46 changes: 46 additions & 0 deletions packages/prover/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,51 @@ export interface ELProof {
readonly proof: string[];
}[];
}

export interface ELTransaction {
readonly type: string;
readonly nonce: string;
readonly to: string | null;
readonly chainId?: string;
readonly input: string;
readonly value: string;
readonly gasPrice?: string;
readonly gas: string;
readonly maxFeePerGas?: string;
readonly maxPriorityFeePerGas?: string;
readonly blockHash: string;
readonly blockNumber: string;
readonly from: string;
readonly hash: string;
readonly r: string;
readonly s: string;
readonly v: string;
readonly transactionIndex: string;
readonly accessList?: {address: string; storageKeys: string[]}[];
}

export interface ELBlock {
readonly parentHash: string;
readonly transactionsRoot: string;
readonly stateRoot: string;
readonly receiptsRoot: string;
readonly logsBloom: string;
readonly nonce: string;
readonly difficulty: string;
readonly totalDifficulty: string;
readonly number: string;
readonly gasLimit: string;
readonly gasUsed: string;
readonly timestamp: string;
readonly extraData?: Buffer | string;
readonly mixHash: string;
readonly hash: string;
readonly baseFeePerGas: string;
readonly miner: string;
readonly sha3Uncles: string;
readonly size: string;
readonly uncles: ELBlock[];
readonly transactions: ELTransaction[];
}
export type ELStorageProof = Pick<ELProof, "storageHash" | "storageProof">;
export type HexString = string;
4 changes: 4 additions & 0 deletions packages/prover/src/utils/assertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,7 @@ export function isEIP1193Provider(provider: Web3Provider): provider is EIP1193Pr
provider.request.constructor.name === "AsyncFunction"
);
}

export function isTruthy<T = unknown>(value: T): value is Exclude<T, undefined | null> {
return value !== undefined && value !== null && value !== false;
}
47 changes: 47 additions & 0 deletions packages/prover/src/utils/conversion.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {BlockData, HeaderData} from "@ethereumjs/block";
import {ELBlock, ELTransaction} from "../types.js";
import {isTruthy} from "./assertion.js";

export function numberToHex(n: number | bigint): string {
return "0x" + n.toString(16);
}
Expand All @@ -19,3 +23,46 @@ export function padLeft(v: Uint8Array, length: number): Uint8Array {
Buffer.from(v).copy(buf, length - v.length);
return buf;
}

// TODO: fix blockInfo type
export function headerDataFromELBlock(blockInfo: ELBlock): HeaderData {
return {
parentHash: blockInfo.parentHash,
uncleHash: blockInfo.sha3Uncles,
coinbase: blockInfo.miner,
stateRoot: blockInfo.stateRoot,
transactionsTrie: blockInfo.transactionsRoot,
receiptTrie: blockInfo.receiptsRoot,
logsBloom: blockInfo.logsBloom,
difficulty: BigInt(blockInfo.difficulty),
number: BigInt(blockInfo.number),
gasLimit: BigInt(blockInfo.gasLimit),
gasUsed: BigInt(blockInfo.gasUsed),
timestamp: BigInt(blockInfo.timestamp),
extraData: blockInfo.extraData,
mixHash: blockInfo.mixHash, // some reason the types are not up to date :(
nonce: blockInfo.nonce,
baseFeePerGas: blockInfo.baseFeePerGas ? BigInt(blockInfo.baseFeePerGas) : undefined,
};
}

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function txDataFromELBlock(txInfo: ELTransaction) {
return {
...txInfo,
data: txInfo.input,
gasPrice: isTruthy(txInfo.gasPrice) ? BigInt(txInfo.gasPrice) : null,
gasLimit: txInfo.gas,
to: isTruthy(txInfo.to) ? padLeft(hexToBuffer(txInfo.to), 20) : undefined,
value: BigInt(txInfo.value),
maxFeePerGas: isTruthy(txInfo.maxFeePerGas) ? BigInt(txInfo.maxFeePerGas) : undefined,
maxPriorityFeePerGas: isTruthy(txInfo.maxPriorityFeePerGas) ? BigInt(txInfo.maxPriorityFeePerGas) : undefined,
};
}

export function blockDataFromELBlock(blockInfo: ELBlock): BlockData {
return {
header: headerDataFromELBlock(blockInfo),
transactions: blockInfo.transactions.map(txDataFromELBlock) as BlockData["transactions"],
};
}
123 changes: 113 additions & 10 deletions packages/prover/src/utils/execution.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import {Block} from "@ethereumjs/block";
import {RLP} from "@ethereumjs/rlp";
import {Trie} from "@ethereumjs/trie";
import {Account} from "@ethereumjs/util";
import {keccak256} from "ethereum-cryptography/keccak.js";
import {Bytes32} from "@lodestar/types";
import {Bytes32, allForks} from "@lodestar/types";
import {Logger} from "@lodestar/utils";
import {ELRequestHandler} from "../interfaces.js";
import {ELProof, ELStorageProof, HexString} from "../types.js";
import {hexToBuffer, padLeft} from "./conversion.js";
import {ELBlock, ELProof, ELRequestPayload, ELResponse, ELStorageProof, HexString} from "../types.js";
import {ProofProvider} from "../proof_provider/proof_provider.js";
import {blockDataFromELBlock, bufferToHex, hexToBuffer, padLeft} from "./conversion.js";
import {isValidResponse} from "./json_rpc.js";

const emptyAccountSerialize = new Account().serialize();
const storageKeyLength = 32;

export async function getELProof(
handler: ELRequestHandler,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: ELRequestHandler<any, any>,
args: [address: string, storageKeys: string[], block: number | string]
): Promise<ELProof> {
// TODO: Find better way to generate random id
Expand All @@ -27,14 +32,80 @@ export async function getELProof(
return proof.result as ELProof;
}

export async function fetchAndVerifyAccount({
address,
proofProvider,
logger,
handler,
block,
}: {
address: HexString;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: ELRequestHandler<any, any>;
proofProvider: ProofProvider;
logger: Logger;
block?: number | string;
}): Promise<{data: ELProof; valid: true} | {valid: false; data?: undefined}> {
const executionPayload = await proofProvider.getExecutionPayload(block ?? "latest");
const proof = await getELProof(handler, [address, [], bufferToHex(executionPayload.blockHash)]);
if (
(await isValidAccount({
address: address,
stateRoot: executionPayload.stateRoot,
proof,
logger,
})) &&
(await isValidStorageKeys({storageKeys: [], proof, logger}))
) {
return {data: proof, valid: true};
}

return {valid: false};
}

export async function fetchAndVerifyBlock({
payload,
proofProvider,
logger,
handler,
}: {
payload: ELRequestPayload<[block: string | number, hydrated: boolean]>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: ELRequestHandler<any, any>;
proofProvider: ProofProvider;
logger: Logger;
}): Promise<{data: ELResponse<ELBlock>; valid: true} | {valid: false; data?: undefined}> {
const executionPayload = await proofProvider.getExecutionPayload(payload.params[0]);
const elResponse = await (handler as ELRequestHandler<[block: string | number, hydrated: boolean], ELBlock>)(payload);

// If response is not valid from the EL we don't need to verify it
if (elResponse && !isValidResponse(elResponse)) return {data: elResponse, valid: true};

if (
elResponse &&
elResponse.result &&
(await isValidBlock({
logger,
block: elResponse.result,
executionPayload,
}))
) {
return {data: elResponse, valid: true};
}

return {valid: false};
}

export async function isValidAccount({
address,
stateRoot,
proof,
logger,
}: {
address: HexString;
stateRoot: Bytes32;
proof: ELProof;
logger: Logger;
}): Promise<boolean> {
const trie = await Trie.create();
const key = keccak256(hexToBuffer(address));
Expand All @@ -55,18 +126,19 @@ export async function isValidAccount({
});
return account.serialize().equals(expectedAccountRLP ? expectedAccountRLP : emptyAccountSerialize);
} catch (err) {
if ((err as Error).message === "Invalid proof provided") return false;

throw err;
logger.error("Error verifying account proof", undefined, err as Error);
return false;
}
}

export async function isValidStorageKeys({
storageKeys,
proof,
logger,
}: {
storageKeys: HexString[];
proof: ELStorageProof;
logger: Logger;
}): Promise<boolean> {
const trie = await Trie.create();

Expand All @@ -85,11 +157,42 @@ export async function isValidStorageKeys({
(!!expectedStorageRLP && expectedStorageRLP.equals(RLP.encode(sp.value)));
if (!isStorageValid) return false;
} catch (err) {
if ((err as Error).message === "Invalid proof provided") return false;

throw err;
logger.error("Error verifying storage keys", undefined, err as Error);
return false;
}
}

return true;
}

export async function isValidBlock({
executionPayload,
block,
logger,
}: {
executionPayload: allForks.ExecutionPayload;
block: ELBlock;
logger: Logger;
}): Promise<boolean> {
const blockObject = Block.fromBlockData(blockDataFromELBlock(block));

if (bufferToHex(executionPayload.blockHash) !== bufferToHex(blockObject.hash())) {
logger.error("Block hash does not match", {
rpcBlockHash: bufferToHex(blockObject.hash()),
beaconExecutionBlockHash: bufferToHex(executionPayload.blockHash),
});

return false;
}

if (!(await blockObject.validateTransactionsTrie())) {
logger.error("Block transactions could not be verified.", {
blockHash: bufferToHex(blockObject.hash()),
blockNumber: blockObject.header.number,
});

return false;
}

return true;
}
4 changes: 4 additions & 0 deletions packages/prover/src/utils/json_rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ export function generateUnverifiedResponseForPayload<P, D = unknown>(
},
};
}

export function isValidResponse<R, E>(response: ELResponse<R, E>): response is ELResponse<R, never> {
return response.error === undefined;
}
4 changes: 4 additions & 0 deletions packages/prover/src/utils/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ import {ProofProvider} from "../proof_provider/proof_provider.js";
import {ELRequestPayload, ELResponse} from "../types.js";
import {eth_getBalance} from "../verified_requests/eth_getBalance.js";
import {eth_getTransactionCount} from "../verified_requests/eth_getTransactionCount.js";
import {eth_getBlockByHash} from "../verified_requests/eth_getBlockByHash.js";
import {eth_getBlockByNumber} from "../verified_requests/eth_getBlockByNumber.js";

/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any */
export const supportedELRequests: Record<string, ELVerifiedRequestHandler<any, any>> = {
eth_getBalance: eth_getBalance,
eth_getTransactionCount: eth_getTransactionCount,
eth_getBlockByHash: eth_getBlockByHash,
eth_getBlockByNumber: eth_getBlockByNumber,
};
/* eslint-enable @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any*/

Expand Down
17 changes: 4 additions & 13 deletions packages/prover/src/verified_requests/eth_getBalance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {ELVerifiedRequestHandler} from "../interfaces.js";
import {bufferToHex} from "../utils/conversion.js";
import {getELProof, isValidAccount, isValidStorageKeys} from "../utils/execution.js";
import {fetchAndVerifyAccount} from "../utils/execution.js";
import {generateRPCResponseForPayload, generateUnverifiedResponseForPayload} from "../utils/json_rpc.js";

// eslint-disable-next-line @typescript-eslint/naming-convention
Expand All @@ -13,18 +12,10 @@ export const eth_getBalance: ELVerifiedRequestHandler<[address: string, block?:
const {
params: [address, block],
} = payload;
const executionPayload = await proofProvider.getExecutionPayload(block ?? "latest");
const proof = await getELProof(handler, [address, [], bufferToHex(executionPayload.blockHash)]);
const result = await fetchAndVerifyAccount({proofProvider, logger, handler, address, block});

if (
(await isValidAccount({
address: address,
stateRoot: executionPayload.stateRoot,
proof,
})) &&
(await isValidStorageKeys({storageKeys: [], proof}))
) {
return generateRPCResponseForPayload(payload, proof.balance);
if (result.valid) {
return generateRPCResponseForPayload(payload, result.data.balance);
}

logger.error("Request could not be verified.");
Expand Down
Loading