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
12 changes: 8 additions & 4 deletions packages/prover/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type RootProviderInitOptions = {
wsCheckpoint?: string;
} & ConsensusNodeOptions;

export type ELRequestMethod = (payload: ELRequestPayload) => Promise<ELResponse | undefined>;
export type ELRequestHandler = (payload: ELRequestPayload) => Promise<ELResponse | undefined>;

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

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

export type ELVerifiedRequestHandler<A = unknown, R = unknown> = (opts: {
export type ELVerifiedRequestHandlerOpts<A = unknown> = {
payload: ELRequestPayload<A>;
handler: ELRequestMethod;
handler: ELRequestHandler;
proofProvider: ProofProvider;
logger: Logger;
}) => Promise<ELResponse<R>>;
};

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

// 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
93 changes: 38 additions & 55 deletions packages/prover/src/utils/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,15 @@ import {Trie} from "@ethereumjs/trie";
import {Account} from "@ethereumjs/util";
import {keccak256} from "ethereum-cryptography/keccak.js";
import {Bytes32} from "@lodestar/types";
import {Logger} from "@lodestar/utils";
import {ethGetBalance} from "../verified_requests/eth_getBalance.js";
import {ELRequestPayload, ELResponse, ELProof, ELStorageProof, HexString} from "../types.js";
import {ProofProvider} from "../proof_provider/proof_provider.js";
import {ELRequestMethod, ELVerifiedRequestHandler} from "../interfaces.js";
import {ELRequestHandler} from "../interfaces.js";
import {ELProof, ELStorageProof, HexString} from "../types.js";
import {hexToBuffer, padLeft} from "./conversion.js";

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

// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any
const supportedELRequests: Record<string, ELVerifiedRequestHandler<any, any>> = {eth_getBalance: ethGetBalance};

export async function processAndVerifyRequest({
payload,
handler,
proofProvider,
logger,
}: {
payload: ELRequestPayload;
handler: ELRequestMethod;
proofProvider: ProofProvider;
logger: Logger;
}): Promise<ELResponse | undefined> {
await proofProvider.waitToBeReady();
logger.debug("Processing request", {method: payload.method, params: JSON.stringify(payload.params)});
const verifiedHandler = supportedELRequests[payload.method];

if (verifiedHandler !== undefined) {
logger.verbose("Verified request handler found", {method: payload.method});
return verifiedHandler({payload, handler, proofProvider, logger});
}

logger.warn("Verified request handler not found. Falling back to proxy.", {method: payload.method});
return handler(payload);
}

export async function getELProof(
handler: ELRequestMethod,
handler: ELRequestHandler,
args: [address: string, storageKeys: string[], block: number | string]
): Promise<ELProof> {
// TODO: Find better way to generate random id
Expand Down Expand Up @@ -69,20 +39,26 @@ export async function isValidAccount({
const trie = await Trie.create();
const key = keccak256(hexToBuffer(address));

const expectedAccountRLP = await trie.verifyProof(
Buffer.from(stateRoot),
Buffer.from(key),
proof.accountProof.map(hexToBuffer)
);
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) {
if ((err as Error).message === "Invalid proof provided") return false;

// 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);
throw err;
}
}

export async function isValidStorageKeys({
Expand All @@ -97,15 +73,22 @@ export async function isValidStorageKeys({
for (let i = 0; i < storageKeys.length; i++) {
const sp = proof.storageProof[i];
const key = keccak256(padLeft(hexToBuffer(storageKeys[i]), storageKeyLength));
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;
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) {
if ((err as Error).message === "Invalid proof provided") return false;

throw err;
}
}

return true;
Expand Down
44 changes: 29 additions & 15 deletions packages/prover/src/utils/json_rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,40 @@ export function generateRPCResponseForPayload<P, R, E = unknown>(
readonly message: string;
}
): ELResponse<R> {
return {
jsonrpc: payload.jsonrpc,
id: payload.id,
result: res,
error,
};
return error
? {
jsonrpc: payload.jsonrpc,
id: payload.id,
error,
}
: {
jsonrpc: payload.jsonrpc,
id: payload.id,
result: res,
};
}

export function generateUnverifiedResponseForPayload<P, D = unknown>(
payload: ELRequestPayload<P>,
message: string,
data?: D
): ELResponse<never, D> {
return {
jsonrpc: payload.jsonrpc,
id: payload.id,
error: {
code: UNVERIFIED_RESPONSE_CODE,
message,
data,
},
};
return data !== undefined || data !== null
? {
jsonrpc: payload.jsonrpc,
id: payload.id,
error: {
code: UNVERIFIED_RESPONSE_CODE,
message,
},
}
: {
jsonrpc: payload.jsonrpc,
id: payload.id,
error: {
code: UNVERIFIED_RESPONSE_CODE,
message,
data,
},
};
}
37 changes: 37 additions & 0 deletions packages/prover/src/utils/process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {Logger} from "@lodestar/utils";
import {ELRequestHandler, ELVerifiedRequestHandler} from "../interfaces.js";
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";

/* 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,
};
/* eslint-enable @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any*/

export async function processAndVerifyRequest({
payload,
handler,
proofProvider,
logger,
}: {
payload: ELRequestPayload;
handler: ELRequestHandler;
proofProvider: ProofProvider;
logger: Logger;
}): Promise<ELResponse | undefined> {
await proofProvider.waitToBeReady();
logger.debug("Processing request", {method: payload.method, params: JSON.stringify(payload.params)});
const verifiedHandler = supportedELRequests[payload.method];

if (verifiedHandler !== undefined) {
logger.verbose("Verified request handler found", {method: payload.method});
return verifiedHandler({payload, handler, proofProvider, logger});

Check failure

Code scanning / CodeQL

Unvalidated dynamic method call

Invocation of method with [user-controlled](1) name may dispatch to unexpected target and cause an exception.
}

logger.warn("Verified request handler not found. Falling back to proxy.", {method: payload.method});
return handler(payload);
}
3 changes: 2 additions & 1 deletion packages/prover/src/verified_requests/eth_getBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {bufferToHex} from "../utils/conversion.js";
import {getELProof, isValidAccount, isValidStorageKeys} from "../utils/execution.js";
import {generateRPCResponseForPayload, generateUnverifiedResponseForPayload} from "../utils/json_rpc.js";

export const ethGetBalance: ELVerifiedRequestHandler<[address: string, block?: number | string], string> = async ({
// eslint-disable-next-line @typescript-eslint/naming-convention
export const eth_getBalance: ELVerifiedRequestHandler<[address: string, block?: number | string], string> = async ({
handler,
payload,
logger,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {ELVerifiedRequestHandler} from "../interfaces.js";
import {bufferToHex} from "../utils/conversion.js";
import {getELProof, isValidAccount, isValidStorageKeys} from "../utils/execution.js";
import {generateRPCResponseForPayload, generateUnverifiedResponseForPayload} from "../utils/json_rpc.js";

// eslint-disable-next-line @typescript-eslint/naming-convention
export const eth_getTransactionCount: ELVerifiedRequestHandler<

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.

@nazarhussain since it shares most of the steps from ethGetBalance, can we refactor to use a common method? could be in a separate PR

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.

Sure will refactor in upcoming PRs.

[address: string, block?: number | string],
string
> = async ({handler, payload, logger, proofProvider}) => {
const {
params: [address, block],
} = payload;
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,
})) &&
(await isValidStorageKeys({storageKeys: [], proof}))
) {
return generateRPCResponseForPayload(payload, proof.nonce);
}

logger.error("Request could not be verified.");
return generateUnverifiedResponseForPayload(payload, "eth_getTransactionCount request can not be verified.");
};
2 changes: 1 addition & 1 deletion packages/prover/src/web3_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
isSendAsyncProvider,
isSendProvider,
} from "./utils/assertion.js";
import {processAndVerifyRequest} from "./utils/execution.js";
import {getLogger} from "./utils/logger.js";
import {processAndVerifyRequest} from "./utils/process.js";

type ProvableProviderInitOpts = {network?: NetworkName; wsCheckpoint?: string; signal?: AbortSignal} & LogOptions &
ConsensusNodeOptions;
Expand Down
2 changes: 1 addition & 1 deletion packages/prover/src/web3_proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import {NetworkName} from "@lodestar/config/networks";
import {ConsensusNodeOptions, LogOptions} from "./interfaces.js";
import {ProofProvider} from "./proof_provider/proof_provider.js";
import {ELRequestPayload, ELResponse} from "./types.js";
import {processAndVerifyRequest} from "./utils/execution.js";
import {generateRPCResponseForPayload} from "./utils/json_rpc.js";
import {getLogger} from "./utils/logger.js";
import {fetchRequestPayload, fetchResponseBody} from "./utils/req_resp.js";
import {processAndVerifyRequest} from "./utils/process.js";

export type VerifiedProxyOptions = {
network: NetworkName;
Expand Down
11 changes: 11 additions & 0 deletions packages/prover/test/fixtures/cl_payload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {allForks} from "@lodestar/types";

export const validExecutionPayload: allForks.ExecutionPayload = ({
blockHash: Buffer.alloc(32),
stateRoot: Buffer.from("7c0f9a6f21d82c2d7690db7aa36c9938de11891071eed6e50ff8b06b5ae7018a", "hex"),
} as unknown) as allForks.ExecutionPayload;

export const invalidExecutionPayload: allForks.ExecutionPayload = ({
blockHash: Buffer.alloc(32),
stateRoot: Buffer.from("ac0d3a6f21d82c2d7690db7aa36c9938de11891071eed6e50ff8b06b5ae7018a", "hex"),
} as unknown) as allForks.ExecutionPayload;
Loading