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
9 changes: 7 additions & 2 deletions packages/prover/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"test:unit": "nyc --cache-dir .nyc_output/.cache -e .ts mocha 'test/unit/**/*.test.ts'",
"test:browsers": "yarn karma start karma.config.cjs",
"test:e2e": "mocha 'test/e2e/**/*.test.ts'",
"check-readme": "typescript-docs-verifier"
"check-readme": "typescript-docs-verifier",
"generate-fixtures": "npx ts-node --esm scripts/generate_fixtures.ts"
},
"dependencies": {
"@ethereumjs/block": "^4.2.1",
Expand All @@ -66,6 +67,7 @@
"@lodestar/light-client": "^1.7.2",
"@lodestar/types": "^1.7.2",
"@lodestar/utils": "^1.7.2",
"@ethereumjs/common": "^3.1.1",
"ethereum-cryptography": "^1.2.0",
"find-up": "^6.3.0",
"http-proxy": "^1.18.1",
Expand All @@ -78,7 +80,10 @@
"@types/http-proxy": "^1.17.10",
"@types/yargs": "^15.0.9",
"ethers": "^6.2.3",
"web3": "^1.9.0"
"web3": "^1.9.0",
"axios": "^1.3.4"
},
"peerDependencies": {
},
"keywords": [
"ethereum",
Expand Down
122 changes: 122 additions & 0 deletions packages/prover/scripts/generate_fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/* eslint-disable no-console */
import {writeFile, mkdir} from "node:fs/promises";
import path from "node:path";
import url from "node:url";
// eslint-disable-next-line import/no-extraneous-dependencies

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this disable necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It requires to specify axios and few other packages as dependencies, while we just need it for development. So why had to disable this.

import axios from "axios";
// eslint-disable-next-line @typescript-eslint/naming-convention
const __filename = url.fileURLToPath(import.meta.url);
// eslint-disable-next-line @typescript-eslint/naming-convention
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));

type NETWORK = "sepolia" | "mainnet";
const networkURLs: Record<NETWORK, {beacon: string; rpc: string}> = {
sepolia: {
beacon: "https://lodestar-sepolia.chainsafe.io",
rpc: "https://lodestar-sepoliarpc.chainsafe.io",
},
mainnet: {
beacon: "https://lodestar-mainnet.chainsafe.io",
rpc: "https://lodestar-mainnetrpc.chainsafe.io",
},
};

let idIndex = Math.floor(Math.random() * 1000000);

async function rawEth(network: NETWORK, payload: Record<string, unknown>): Promise<Record<string, unknown>> {
return (await axios({url: networkURLs[network].rpc, method: "post", data: payload, responseType: "json"}))
.data as Record<string, unknown>;
}

async function rawBeacon(network: NETWORK, path: string): Promise<Record<string, unknown>> {
return (await axios.get(`${networkURLs[network].beacon}/${path}`)).data as Record<string, unknown>;
}

async function generateFixture(
label: string,
{method, params}: {method: string; params: unknown[]},
{slot}: {slot: number | string},
network: NETWORK = "sepolia"
): Promise<void> {
const request = {id: idIndex++, jsonrpc: "2.0", method, params};
const response = await rawEth(network, request);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
const executionPayload: unknown = ((await rawBeacon(network, `eth/v2/beacon/blocks/${slot}`)) as any).data.message
.body.execution_payload;

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
const headers: unknown = ((await rawBeacon(network, `eth/v1/beacon/headers/${slot}`)) as any).data;

if (response.error || !response.result) {
throw new Error("Invalid response" + JSON.stringify(response));
}

const fixture = {
label,
network,
request,
response,
executionPayload,
headers,
};

const dir = path.join(__dirname, "..", "test/fixtures", network);
await mkdir(dir, {recursive: true});
await writeFile(path.join(dir, `${label}.json`), JSON.stringify(fixture, null, 2));

console.info("Generated fixture:", label);
}

await generateFixture(
"eth_getBlock_with_no_accessList",
{
method: "eth_getBlockByHash",
params: ["0x75b10426177f0f4bd8683999e2c7c597007c6e7c4551d6336c0f880b12c6f3bf", true],
},
{slot: 2144468}
);

await generateFixture(
"eth_getBlock_with_contractCreation",
{
method: "eth_getBlockByHash",
params: ["0x3a0225b38d5927a37cc95fd48254e83c4e9b70115918a103d9fd7e36464030d4", true],
},
{slot: 625024}
);

await generateFixture(
"eth_getBalance_eoa",
{method: "eth_getBalance", params: ["0xC4bFccB1668d6E464F33a76baDD8C8D7D341e04A", "latest"]},
{slot: "head"}
);

await generateFixture(
"eth_getBalance_eoa",
{method: "eth_getBalance", params: ["0xC4bFccB1668d6E464F33a76baDD8C8D7D341e04A", "latest"]},
{slot: "head"}
);

await generateFixture(
"eth_getBalance_eoa_proof",
{method: "eth_getProof", params: ["0xC4bFccB1668d6E464F33a76baDD8C8D7D341e04A", [], "latest"]},
{slot: "head"}
);

await generateFixture(
"eth_getBalance_contract",
{method: "eth_getBalance", params: ["0xa54aeF0dAB669e8e1A164BCcB323549a818a0497", "latest"]},
{slot: "head"}
);

await generateFixture(
"eth_getBalance_contract_proof",
{method: "eth_getProof", params: ["0xa54aeF0dAB669e8e1A164BCcB323549a818a0497", [], "latest"]},
{slot: "head"}
);

await generateFixture(
"eth_getCode",
{method: "eth_getCode", params: ["0xa54aeF0dAB669e8e1A164BCcB323549a818a0497", "latest"]},
{slot: "head"}
);
1 change: 1 addition & 0 deletions packages/prover/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type ELVerifiedRequestHandlerOpts<Params = unknown[], Response = unknown>
handler: ELRequestHandler<Params, Response>;
proofProvider: ProofProvider;
logger: Logger;
network: NetworkName;
};

export type ELVerifiedRequestHandler<Params = unknown[], Response = unknown> = (
Expand Down
1 change: 1 addition & 0 deletions packages/prover/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface ELBlock {
readonly transactionsRoot: string;
readonly stateRoot: string;
readonly receiptsRoot: string;
readonly withdrawalsRoot: string;
readonly logsBloom: string;
readonly nonce: string;
readonly difficulty: string;
Expand Down
1 change: 1 addition & 0 deletions packages/prover/src/utils/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function headerDataFromELBlock(blockInfo: ELBlock): HeaderData {
mixHash: blockInfo.mixHash, // some reason the types are not up to date :(
nonce: blockInfo.nonce,
baseFeePerGas: blockInfo.baseFeePerGas ? BigInt(blockInfo.baseFeePerGas) : undefined,
withdrawalsRoot: blockInfo.withdrawalsRoot ?? undefined,
};
}

Expand Down
164 changes: 53 additions & 111 deletions packages/prover/src/utils/execution.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
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, allForks} from "@lodestar/types";
import {Logger} from "@lodestar/utils";
import {NetworkName} from "@lodestar/config/networks";
import {ELRequestHandler} from "../interfaces.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 {ELBlock, ELProof, ELRequestPayload, ELResponse, HexString} from "../types.js";
import {bufferToHex} from "./conversion.js";
import {isValidResponse} from "./json_rpc.js";
import {isValidAccount, isValidBlock, isValidCodeHash, isValidStorageKeys} from "./verification.js";

const emptyAccountSerialize = new Account().serialize();
const storageKeyLength = 32;
export async function getELCode(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: ELRequestHandler<[address: string, block: number | string], string>,
args: [address: string, block: number | string]
): Promise<string> {
// TODO: Find better way to generate random id
const codeResult = await handler({
jsonrpc: "2.0",
method: "eth_getCode",
params: args,
id: (Math.random() * 10000).toFixed(0),
});

if (!codeResult || !codeResult.result) {
throw new Error("Can not find code for given address.");
}

return codeResult.result;
}

export async function getELProof(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -48,6 +61,7 @@ export async function fetchAndVerifyAccount({
}): 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,
Expand All @@ -63,17 +77,45 @@ export async function fetchAndVerifyAccount({
return {valid: false};
}

export async function fetchAndVerifyCode({
address,
proofProvider,
logger,
handler,
codeHash,
block,
}: {
address: HexString;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: ELRequestHandler<any, any>;
proofProvider: ProofProvider;
logger: Logger;
codeHash: HexString;
block?: number | string;
}): Promise<{data: string; valid: true} | {valid: false; data?: undefined}> {
const executionPayload = await proofProvider.getExecutionPayload(block ?? "latest");
const code = await getELCode(handler, [address, bufferToHex(executionPayload.blockHash)]);

if (await isValidCodeHash({codeHash, codeResponse: code, logger})) {
return {data: code, valid: true};
}

return {valid: false};
}

export async function fetchAndVerifyBlock({
payload,
proofProvider,
logger,
handler,
network,
}: {
payload: ELRequestPayload<[block: string | number, hydrated: boolean]>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
handler: ELRequestHandler<any, any>;
proofProvider: ProofProvider;
logger: Logger;
network: NetworkName;
}): 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);
Expand All @@ -88,111 +130,11 @@ export async function fetchAndVerifyBlock({
logger,
block: elResponse.result,
executionPayload,
network,
}))
) {
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));

try {
const expectedAccountRLP = await trie.verifyProof(
Buffer.from(stateRoot),
Buffer.from(key),
proof.accountProof.map(hexToBuffer)
);

// Shresth Agrawal (2022) Patronum source code. https://github.com/lightclients/patronum
const account = Account.fromAccountData({
nonce: BigInt(proof.nonce),
balance: BigInt(proof.balance),
storageRoot: proof.storageHash,
codeHash: proof.codeHash,
});
return account.serialize().equals(expectedAccountRLP ? expectedAccountRLP : emptyAccountSerialize);
} catch (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();

for (let i = 0; i < storageKeys.length; i++) {
const sp = proof.storageProof[i];
const key = keccak256(padLeft(hexToBuffer(storageKeys[i]), storageKeyLength));
try {
const expectedStorageRLP = await trie.verifyProof(
hexToBuffer(proof.storageHash),
Buffer.from(key),
sp.proof.map(hexToBuffer)
);

const isStorageValid =
(!expectedStorageRLP && sp.value === "0x0") ||
(!!expectedStorageRLP && expectedStorageRLP.equals(RLP.encode(sp.value)));
if (!isStorageValid) return false;
} catch (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;
}
Loading