mirror of
https://github.com/ChainSafe/lodestar.git
synced 2026-01-10 08:08:16 -05:00
feat: add call support to verified requests for prover (#5462)
* Update the readme with details * Restructure utility methods to make consistent pattern * Add call support for verified requests * Update the evm logic * Update the logger data type * Add more structured tests * Move e2e tests to mainnet to make them stable for now till setup CI * Update readme * Fix a typo
This commit is contained in:
@@ -18,12 +18,14 @@ import Web3 from "web3";
|
||||
import {createVerifiedExecutionProvider, LCTransport} from "@lodestar/prover";
|
||||
|
||||
const {provider, proofProvider} = createVerifiedExecutionProvider(
|
||||
new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"), {
|
||||
transport: LCTransport.Rest,
|
||||
urls: ["https://lodestar-sepolia.chainsafe.io"],
|
||||
network: "sepolia",
|
||||
wsCheckpoint: "trusted-checkpoint"
|
||||
});
|
||||
new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"),
|
||||
{
|
||||
transport: LCTransport.Rest,
|
||||
urls: ["https://lodestar-sepolia.chainsafe.io"],
|
||||
network: "sepolia",
|
||||
wsCheckpoint: "trusted-checkpoint",
|
||||
}
|
||||
);
|
||||
|
||||
const web3 = new Web3(provider);
|
||||
|
||||
@@ -35,7 +37,7 @@ console.log({balance, address});
|
||||
You can also invoke the package as binary.
|
||||
|
||||
```bash
|
||||
npm -i g @lodestar/prover
|
||||
npm -i g @lodestar/prover
|
||||
|
||||
lodestar-prover start \
|
||||
--network sepolia \
|
||||
@@ -45,6 +47,67 @@ lodestar-prover start \
|
||||
--port 8080
|
||||
```
|
||||
|
||||
## Supported Web3 Methods
|
||||
|
||||
✅ - Completed
|
||||
|
||||
⌛ - Todo
|
||||
|
||||
➡️ - Request will be forward to EL without any intermediary manipulations.
|
||||
|
||||
| Group | Method | Status | Version |
|
||||
| ----------------- | --------------------------------------- | ------ | ------- |
|
||||
| Block | eth_getBlockByHash | ✅ | v0 |
|
||||
| | eth_getBlockByNumber | ✅ | v0 |
|
||||
| | eth_getBlockTransactionCountByHash | ⌛ | v2 |
|
||||
| | eth_getBlockTransactionCountByNumber | ⌛ | v2 |
|
||||
| | eth_getUncleCountByBlockHash | ⌛ | v2 |
|
||||
| | eth_getUncleCountByBlockNumber | ⌛ | v2 |
|
||||
| Chain/Network | eth_chainId | ➡️ |
|
||||
| | eth_syncing | ⌛ | v1 |
|
||||
| | eth_coinbase | ⌛ | v2 |
|
||||
| | eth_accounts | ➡️ |
|
||||
| | eth_blockNumber | ➡️ |
|
||||
| Call and Estimate | eth_call | ✅ | v0 |
|
||||
| | eth_estimateGas | ⌛ | v0 |
|
||||
| | eth_createAccessList | ⌛ | v2 |
|
||||
| | eth_gasPrice | ⌛ | v1 |
|
||||
| | eth_maxPriorityFeePerGas | ⌛ | v1 |
|
||||
| | eth_feeHistory | ⌛ | v2 |
|
||||
| Filters | eth_newFilter | ⌛ | v2 |
|
||||
| | eth_newBlockFilter | ⌛ | v2 |
|
||||
| | eth_newPendingTransactionFilter | ⌛ | v2 |
|
||||
| | eth_uninstallFilter | ⌛ | v2 |
|
||||
| | eth_getFilterChanges | ⌛ | v2 |
|
||||
| | eth_getFilterLogs | ⌛ | v2 |
|
||||
| | eth_getLogs | ⌛ | v1 |
|
||||
| Mining | eth_mining | ➡️ |
|
||||
| | eth_hashrate | ➡️ |
|
||||
| | eth_getWork | ➡️ |
|
||||
| | eth_submitWork | ➡️ |
|
||||
| | eth_submitHashrate | ➡️ |
|
||||
| Signing | eth_sign | ➡️ |
|
||||
| | eth_signTransaction | ➡️ |
|
||||
| State | eth_getBalance | ✅ | v0 |
|
||||
| | eth_getStorageAt | ⌛ | v1 |
|
||||
| | eth_getTransactionCount | ⌛ | v2 |
|
||||
| | eth_getCode | ✅ | v0 |
|
||||
| | eth_getProof | ➡️ |
|
||||
| Transactions | eth_sendTransaction | ➡️ |
|
||||
| | eth_sendRawTransaction | ➡️ |
|
||||
| | eth_getTransactionByHash | ⌛ | v2 |
|
||||
| | eth_getTransactionByBlockHashAndIndex | ⌛ | v2 |
|
||||
| | eth_getTransactionByBlockNumberAndIndex | ⌛ | v2 |
|
||||
| | eth_getTransactionReceipt | ⌛ | v2 |
|
||||
|
||||
## Non-supported features
|
||||
|
||||
- Currently does not support batch requests.
|
||||
|
||||
## Warnings
|
||||
|
||||
- To use this prover the ehtereum provider must support the `eth_getProof` method. Unfortunately, Infura does not currently support this endpoint. As an alternative, we suggest using Alchemy.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Lerna](https://github.com/lerna/lerna)
|
||||
|
||||
@@ -58,11 +58,13 @@
|
||||
"generate-fixtures": "npx ts-node --esm scripts/generate_fixtures.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ethereumjs/block": "^4.2.1",
|
||||
"@ethereumjs/common": "^3.1.1",
|
||||
"@ethereumjs/block": "^4.2.2",
|
||||
"@ethereumjs/common": "^3.1.2",
|
||||
"@ethereumjs/rlp": "^4.0.1",
|
||||
"@ethereumjs/trie": "^5.0.4",
|
||||
"@ethereumjs/util": "^8.0.5",
|
||||
"@ethereumjs/trie": "^5.0.5",
|
||||
"@ethereumjs/util": "^8.0.6",
|
||||
"@ethereumjs/vm": "^6.4.2",
|
||||
"@ethereumjs/blockchain": "^6.2.2",
|
||||
"@lodestar/api": "^1.8.0",
|
||||
"@lodestar/config": "^1.8.0",
|
||||
"@lodestar/light-client": "^1.8.0",
|
||||
|
||||
@@ -23,41 +23,104 @@ const networkURLs: Record<NETWORK, {beacon: string; rpc: string}> = {
|
||||
|
||||
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 rawEth(
|
||||
network: NETWORK,
|
||||
method: string,
|
||||
params: unknown[]
|
||||
): Promise<{payload: Record<string, unknown>; response: Record<string, unknown>}> {
|
||||
const payload = {id: idIndex++, jsonrpc: "2.0", method, params};
|
||||
|
||||
const response = (
|
||||
await axios({
|
||||
url: networkURLs[network].rpc,
|
||||
method: "post",
|
||||
data: payload,
|
||||
responseType: "json",
|
||||
})
|
||||
).data as Record<string, unknown>;
|
||||
|
||||
return {payload, response};
|
||||
}
|
||||
|
||||
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;
|
||||
type Generator = (opts: {latest: string; finalized: string}) => {
|
||||
request: {method: string; params: unknown[]};
|
||||
slot: number | string;
|
||||
dependentRequests?: {method: string; params: unknown[]}[];
|
||||
};
|
||||
|
||||
// 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;
|
||||
type Block = {hash: string; number: string};
|
||||
|
||||
const getBlockByNumber = async (network: NETWORK, number: string | number, dehydrate = false): Promise<Block> => {
|
||||
const {response} = await rawEth(network, "eth_getBlockByNumber", [number, dehydrate]);
|
||||
|
||||
if (response.error || !response.result) {
|
||||
throw new Error("Invalid response" + JSON.stringify(response));
|
||||
}
|
||||
|
||||
return response.result as Block;
|
||||
};
|
||||
|
||||
const getBlockByHash = async (network: NETWORK, hash: string | number, dehydrate = false): Promise<Block> => {
|
||||
const {response} = await rawEth(network, "eth_getBlockByHash", [hash, dehydrate]);
|
||||
|
||||
if (response.error || !response.result) {
|
||||
throw new Error("Invalid response" + JSON.stringify(response));
|
||||
}
|
||||
|
||||
return response.result as Block;
|
||||
};
|
||||
|
||||
async function generateFixture(label: string, generator: Generator, network: NETWORK = "sepolia"): Promise<void> {
|
||||
const latest = await getBlockByNumber(network, "latest");
|
||||
const finalized = await getBlockByNumber(network, "finalized");
|
||||
|
||||
const opts = generator({latest: latest.hash, finalized: finalized.hash});
|
||||
const slot = opts.slot;
|
||||
const {payload: request, response} = await rawEth(network, opts.request.method, opts.request.params);
|
||||
if (response.error || !response.result) {
|
||||
throw new Error("Invalid response" + JSON.stringify(response));
|
||||
}
|
||||
|
||||
const beacon = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
|
||||
executionPayload: ((await rawBeacon(network, `eth/v2/beacon/blocks/${slot}`)) as any).data.message.body
|
||||
.execution_payload,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
|
||||
headers: ((await rawBeacon(network, `eth/v1/beacon/headers/${slot}`)) as any).data,
|
||||
};
|
||||
|
||||
const payloadBlock = await getBlockByHash(
|
||||
network,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
(beacon.executionPayload as {block_hash: string}).block_hash,
|
||||
true
|
||||
);
|
||||
|
||||
const execution = {
|
||||
block: payloadBlock,
|
||||
};
|
||||
|
||||
const dependentRequests = [];
|
||||
for (const {method, params} of opts.dependentRequests ?? []) {
|
||||
const {payload, response} = await rawEth(network, method, params);
|
||||
if (response.error || !response.result) {
|
||||
throw new Error("Invalid response" + JSON.stringify(response));
|
||||
}
|
||||
dependentRequests.push({payload, response});
|
||||
}
|
||||
|
||||
const fixture = {
|
||||
label,
|
||||
network,
|
||||
request,
|
||||
response,
|
||||
executionPayload,
|
||||
headers,
|
||||
beacon,
|
||||
execution,
|
||||
dependentRequests,
|
||||
};
|
||||
|
||||
const dir = path.join(__dirname, "..", "test/fixtures", network);
|
||||
@@ -67,56 +130,92 @@ async function generateFixture(
|
||||
console.info("Generated fixture:", label);
|
||||
}
|
||||
|
||||
await generateFixture(
|
||||
"eth_getBlock_with_no_accessList",
|
||||
{
|
||||
await generateFixture("eth_getBlock_with_no_accessList", () => ({
|
||||
request: {
|
||||
method: "eth_getBlockByHash",
|
||||
params: ["0x75b10426177f0f4bd8683999e2c7c597007c6e7c4551d6336c0f880b12c6f3bf", true],
|
||||
},
|
||||
{slot: 2144468}
|
||||
);
|
||||
slot: 2144468,
|
||||
}));
|
||||
|
||||
await generateFixture(
|
||||
"eth_getBlock_with_contractCreation",
|
||||
{
|
||||
await generateFixture("eth_getBlock_with_contractCreation", () => ({
|
||||
request: {
|
||||
method: "eth_getBlockByHash",
|
||||
params: ["0x3a0225b38d5927a37cc95fd48254e83c4e9b70115918a103d9fd7e36464030d4", true],
|
||||
},
|
||||
{slot: 625024}
|
||||
);
|
||||
slot: 625024,
|
||||
}));
|
||||
|
||||
await generateFixture("eth_getBalance_eoa", ({latest}) => ({
|
||||
request: {method: "eth_getBalance", params: ["0xC4bFccB1668d6E464F33a76baDD8C8D7D341e04A", latest]},
|
||||
slot: "head",
|
||||
dependentRequests: [{method: "eth_getProof", params: ["0xC4bFccB1668d6E464F33a76baDD8C8D7D341e04A", [], latest]}],
|
||||
}));
|
||||
|
||||
await generateFixture("eth_getBalance_contract", ({latest}) => ({
|
||||
request: {method: "eth_getBalance", params: ["0xa54aeF0dAB669e8e1A164BCcB323549a818a0497", latest]},
|
||||
slot: "head",
|
||||
dependentRequests: [{method: "eth_getProof", params: ["0xa54aeF0dAB669e8e1A164BCcB323549a818a0497", [], latest]}],
|
||||
}));
|
||||
|
||||
await generateFixture("eth_getCode", ({latest}) => ({
|
||||
request: {method: "eth_getCode", params: ["0xa54aeF0dAB669e8e1A164BCcB323549a818a0497", latest]},
|
||||
slot: "head",
|
||||
dependentRequests: [{method: "eth_getProof", params: ["0xa54aeF0dAB669e8e1A164BCcB323549a818a0497", [], latest]}],
|
||||
}));
|
||||
|
||||
await generateFixture("eth_getTransactionCount", ({latest}) => ({
|
||||
request: {method: "eth_getTransactionCount", params: ["0xC4bFccB1668d6E464F33a76baDD8C8D7D341e04A", latest]},
|
||||
slot: "head",
|
||||
dependentRequests: [{method: "eth_getProof", params: ["0xC4bFccB1668d6E464F33a76baDD8C8D7D341e04A", [], latest]}],
|
||||
}));
|
||||
|
||||
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"}
|
||||
"eth_call",
|
||||
({latest}) => ({
|
||||
request: {
|
||||
method: "eth_call",
|
||||
params: [
|
||||
{
|
||||
data: "0xe6cb901300000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000005",
|
||||
to: "0xade2a9c8b033d60ffcdb8cfc974dd87b2a9c1f27",
|
||||
},
|
||||
latest,
|
||||
],
|
||||
},
|
||||
slot: "head",
|
||||
dependentRequests: [
|
||||
{
|
||||
method: "eth_createAccessList",
|
||||
params: [
|
||||
{
|
||||
to: "0xade2a9c8b033d60ffcdb8cfc974dd87b2a9c1f27",
|
||||
from: "0x0000000000000000000000000000000000000000",
|
||||
data: "0xe6cb901300000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000005",
|
||||
value: undefined,
|
||||
gas: "0x1c9c380",
|
||||
gasPrice: "0x0",
|
||||
},
|
||||
latest,
|
||||
],
|
||||
},
|
||||
{
|
||||
method: "eth_getProof",
|
||||
params: ["0x0000000000000000000000000000000000000000", [], latest],
|
||||
},
|
||||
{
|
||||
method: "eth_getCode",
|
||||
params: ["0x0000000000000000000000000000000000000000", latest],
|
||||
},
|
||||
{
|
||||
method: "eth_getProof",
|
||||
params: ["0xade2a9c8b033d60ffcdb8cfc974dd87b2a9c1f27", [], latest],
|
||||
},
|
||||
{
|
||||
method: "eth_getCode",
|
||||
params: ["0xade2a9c8b033d60ffcdb8cfc974dd87b2a9c1f27", latest],
|
||||
},
|
||||
],
|
||||
}),
|
||||
"mainnet"
|
||||
);
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
export const MAX_REQUEST_LIGHT_CLIENT_UPDATES = 128;
|
||||
export const MAX_PAYLOAD_HISTORY = 32;
|
||||
export const UNVERIFIED_RESPONSE_CODE = -33091;
|
||||
export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
||||
|
||||
@@ -22,6 +22,9 @@ export type ELRequestHandler<Params = unknown[], Response = unknown> = (
|
||||
payload: ELRequestPayload<Params>
|
||||
) => Promise<ELResponse<Response> | undefined>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type ELRequestHandlerAny = ELRequestHandler<any, any>;
|
||||
|
||||
// Modern providers uses this structure e.g. Web3 4.x
|
||||
export interface EIP1193Provider {
|
||||
request: (payload: ELRequestPayload) => Promise<ELResponse>;
|
||||
|
||||
@@ -100,6 +100,8 @@ export class PayloadStore {
|
||||
if (latestPayload && latestPayload.blockNumber < payload.blockNumber) {
|
||||
this.latestBlockRoot = blockRoot;
|
||||
}
|
||||
} else {
|
||||
this.latestBlockRoot = blockRoot;
|
||||
}
|
||||
|
||||
if (finalized) {
|
||||
|
||||
@@ -1,23 +1,39 @@
|
||||
import {ELRequestHandler} from "./interfaces.js";
|
||||
|
||||
export interface ELRequestPayload<T = unknown[]> {
|
||||
readonly jsonrpc: string & ("2.0" | "1.0");
|
||||
readonly jsonrpc: string | ("2.0" | "1.0");
|
||||
readonly id: number | string;
|
||||
readonly method: string;
|
||||
readonly params: T;
|
||||
readonly requestOptions?: unknown;
|
||||
}
|
||||
|
||||
// Make the very flexible el response type to match different libraries easily
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type ELResponse<T = any, E = any> = {
|
||||
export type ELResponseWithResult<T> = {
|
||||
readonly id: number | string;
|
||||
jsonrpc: string;
|
||||
result?: T;
|
||||
error?: {
|
||||
result: T;
|
||||
error?: never;
|
||||
};
|
||||
|
||||
export type ELResponseWithError<T> = {
|
||||
readonly id: number | string;
|
||||
jsonrpc: string;
|
||||
result?: never;
|
||||
error: {
|
||||
readonly code?: number;
|
||||
readonly data?: E;
|
||||
readonly data?: T;
|
||||
readonly message: string;
|
||||
};
|
||||
};
|
||||
|
||||
// Make the very flexible el response type to match different libraries easily
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type ELResponse<T = any, E = any> = ELResponseWithResult<T> | ELResponseWithError<E>;
|
||||
|
||||
export type HexString = string;
|
||||
|
||||
export type ELBlockNumberOrTag = number | string | "latest" | "earliest" | "pending";
|
||||
|
||||
export interface ELProof {
|
||||
readonly address: string;
|
||||
readonly balance: string;
|
||||
@@ -52,14 +68,20 @@ export interface ELTransaction {
|
||||
readonly v: string;
|
||||
readonly transactionIndex: string;
|
||||
readonly accessList?: {address: string; storageKeys: string[]}[];
|
||||
readonly data?: string;
|
||||
}
|
||||
|
||||
export interface ELWithdrawal {
|
||||
readonly index: string;
|
||||
readonly validatorIndex: string;
|
||||
readonly address: string;
|
||||
readonly amount: string;
|
||||
}
|
||||
|
||||
export interface ELBlock {
|
||||
readonly parentHash: string;
|
||||
readonly transactionsRoot: string;
|
||||
readonly stateRoot: string;
|
||||
readonly receiptsRoot: string;
|
||||
readonly withdrawalsRoot: string;
|
||||
readonly logsBloom: string;
|
||||
readonly nonce: string;
|
||||
readonly difficulty: string;
|
||||
@@ -77,6 +99,40 @@ export interface ELBlock {
|
||||
readonly size: string;
|
||||
readonly uncles: ELBlock[];
|
||||
readonly transactions: ELTransaction[];
|
||||
readonly transactionsRoot: string;
|
||||
readonly withdrawals?: ELWithdrawal[];
|
||||
readonly withdrawalsRoot?: string;
|
||||
}
|
||||
|
||||
export interface ELAccessList {
|
||||
readonly address: HexString;
|
||||
readonly storageKeys: HexString[];
|
||||
}
|
||||
|
||||
export interface ELAccessListResponse {
|
||||
readonly error: string;
|
||||
readonly gasUsed: HexString;
|
||||
readonly accessList: ELAccessList[];
|
||||
}
|
||||
|
||||
export type ELStorageProof = Pick<ELProof, "storageHash" | "storageProof">;
|
||||
export type HexString = string;
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
export type ELApi = {
|
||||
eth_createAccessList: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => ELAccessListResponse;
|
||||
call: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => HexString;
|
||||
eth_getCode: (address: string, block?: ELBlockNumberOrTag) => HexString;
|
||||
eth_getProof: (address: string, storageKeys: string[], block?: ELBlockNumberOrTag) => ELProof;
|
||||
eth_getBlockByNumber: (block: ELBlockNumberOrTag, hydrated?: boolean) => ELBlock | undefined;
|
||||
eth_getBlockByHash: (block: string, hydrated?: boolean) => ELBlock | undefined;
|
||||
};
|
||||
export type ELApiParams = {
|
||||
[K in keyof ELApi]: Parameters<ELApi[K]>;
|
||||
};
|
||||
export type ELApiReturn = {
|
||||
[K in keyof ELApi]: ReturnType<ELApi[K]>;
|
||||
};
|
||||
export type ELApiHandlers = {
|
||||
[K in keyof ELApi]: ELRequestHandler<ELApiParams[K], ELApiReturn[K]>;
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
@@ -45,7 +45,12 @@ export async function getExecutionPayloads({
|
||||
logger: Logger;
|
||||
}): Promise<Record<number, allForks.ExecutionPayload>> {
|
||||
[startSlot, endSlot] = [Math.min(startSlot, endSlot), Math.max(startSlot, endSlot)];
|
||||
logger.debug("Fetching EL payloads", {startSlot, endSlot});
|
||||
if (startSlot === endSlot) {
|
||||
logger.debug("Fetching EL payload", {slot: startSlot});
|
||||
} else {
|
||||
logger.debug("Fetching EL payloads", {startSlot, endSlot});
|
||||
}
|
||||
|
||||
const payloads: Record<number, allForks.ExecutionPayload> = {};
|
||||
|
||||
let slot = endSlot;
|
||||
@@ -95,14 +100,13 @@ export async function getGenesisData(api: Pick<Api, "beacon">): Promise<GenesisD
|
||||
}
|
||||
|
||||
export async function getSyncCheckpoint(api: Pick<Api, "beacon">, checkpoint?: string): Promise<Bytes32> {
|
||||
if (checkpoint && checkpoint.length !== 32) {
|
||||
throw Error(`Checkpoint root must be 32 bytes long: ${checkpoint.length}`);
|
||||
}
|
||||
let syncCheckpoint: Uint8Array;
|
||||
let syncCheckpoint: Bytes32 | undefined = checkpoint ? hexToBuffer(checkpoint) : undefined;
|
||||
|
||||
if (checkpoint) {
|
||||
syncCheckpoint = hexToBuffer(checkpoint);
|
||||
} else {
|
||||
if (syncCheckpoint && syncCheckpoint.byteLength !== 32) {
|
||||
throw Error(`Checkpoint root must be 32 bytes. length=${syncCheckpoint.byteLength}`);
|
||||
}
|
||||
|
||||
if (!syncCheckpoint) {
|
||||
const res = await api.beacon.getStateFinalityCheckpoints("head");
|
||||
ApiError.assert(res);
|
||||
syncCheckpoint = res.response.data.finalized.root;
|
||||
|
||||
@@ -2,29 +2,39 @@ 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);
|
||||
export function numberToHex(num: number | bigint): string {
|
||||
return "0x" + num.toString(16);
|
||||
}
|
||||
|
||||
export function hexToNumber(n: string): number {
|
||||
return n.startsWith("0x") ? parseInt(n.slice(2), 16) : parseInt(n, 16);
|
||||
export function hexToNumber(num: string): number {
|
||||
return num.startsWith("0x") ? parseInt(num.slice(2), 16) : parseInt(num, 16);
|
||||
}
|
||||
|
||||
export function hexToBigInt(num: string): bigint {
|
||||
return num.startsWith("0x") ? BigInt(num) : BigInt(`0x${num}`);
|
||||
}
|
||||
|
||||
export function bigIntToHex(num: bigint): string {
|
||||
return `0x${num.toString(16)}`;
|
||||
}
|
||||
|
||||
export function bufferToHex(buffer: Buffer | Uint8Array): string {
|
||||
return "0x" + Buffer.from(buffer).toString("hex");
|
||||
}
|
||||
|
||||
export function hexToBuffer(v: string): Buffer {
|
||||
return Buffer.from(v.replace("0x", ""), "hex");
|
||||
export function hexToBuffer(val: string): Buffer {
|
||||
return Buffer.from(val.replace("0x", ""), "hex");
|
||||
}
|
||||
|
||||
export function padLeft(v: Uint8Array, length: number): Uint8Array {
|
||||
export function padLeft<T extends Buffer | Uint8Array>(v: T, length: number): T {
|
||||
const buf = Buffer.alloc(length);
|
||||
Buffer.from(v).copy(buf, length - v.length);
|
||||
return buf;
|
||||
|
||||
if (Buffer.isBuffer(v)) return buf as T;
|
||||
|
||||
return Uint8Array.from(buf) as T;
|
||||
}
|
||||
|
||||
// TODO: fix blockInfo type
|
||||
export function headerDataFromELBlock(blockInfo: ELBlock): HeaderData {
|
||||
return {
|
||||
parentHash: blockInfo.parentHash,
|
||||
@@ -55,7 +65,7 @@ export function txDataFromELBlock(txInfo: ELTransaction) {
|
||||
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),
|
||||
value: txInfo.value ? BigInt(txInfo.value) : undefined,
|
||||
maxFeePerGas: isTruthy(txInfo.maxFeePerGas) ? BigInt(txInfo.maxFeePerGas) : undefined,
|
||||
maxPriorityFeePerGas: isTruthy(txInfo.maxPriorityFeePerGas) ? BigInt(txInfo.maxPriorityFeePerGas) : undefined,
|
||||
};
|
||||
@@ -67,3 +77,21 @@ export function blockDataFromELBlock(blockInfo: ELBlock): BlockData {
|
||||
transactions: blockInfo.transactions.map(txDataFromELBlock) as BlockData["transactions"],
|
||||
};
|
||||
}
|
||||
|
||||
export function cleanObject<T extends Record<string, unknown> | unknown[]>(obj: T): T {
|
||||
const isNullify = (v: unknown): boolean => v === undefined || v === null;
|
||||
|
||||
if (Array.isArray(obj)) return obj.filter((v) => isNullify(v)) as T;
|
||||
|
||||
if (typeof obj === "object") {
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (isNullify(obj[key])) {
|
||||
delete obj[key];
|
||||
} else if (typeof obj[key] === "object") {
|
||||
cleanObject(obj[key] as Record<string, unknown>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
208
packages/prover/src/utils/evm.ts
Normal file
208
packages/prover/src/utils/evm.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import {Blockchain} from "@ethereumjs/blockchain";
|
||||
import {Account, Address} from "@ethereumjs/util";
|
||||
import {VM} from "@ethereumjs/vm";
|
||||
import {NetworkName} from "@lodestar/config/networks";
|
||||
import {allForks} from "@lodestar/types";
|
||||
import {Logger} from "@lodestar/utils";
|
||||
import {ZERO_ADDRESS} from "../constants.js";
|
||||
import {ProofProvider} from "../proof_provider/proof_provider.js";
|
||||
import {ELApiHandlers, ELBlock, ELProof, ELTransaction, HexString} from "../types.js";
|
||||
import {bufferToHex, cleanObject, hexToBigInt, hexToBuffer, numberToHex, padLeft} from "./conversion.js";
|
||||
import {elRpc, getChainCommon} from "./execution.js";
|
||||
import {isValidResponse} from "./json_rpc.js";
|
||||
import {isValidAccount, isValidCodeHash, isValidStorageKeys} from "./validation.js";
|
||||
|
||||
export async function createEVM({
|
||||
proofProvider,
|
||||
network,
|
||||
}: {
|
||||
proofProvider: ProofProvider;
|
||||
network: NetworkName;
|
||||
}): Promise<VM> {
|
||||
const common = getChainCommon(network);
|
||||
const blockchain = await Blockchain.create({common});
|
||||
|
||||
// Connect blockchain object with existing proof provider for block history
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
|
||||
(blockchain as any).getBlock = async (blockId: number) => {
|
||||
const payload = await proofProvider.getExecutionPayload(blockId);
|
||||
return {
|
||||
hash: () => payload.blockHash,
|
||||
};
|
||||
};
|
||||
const vm = await VM.create({common, blockchain});
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
export async function getEVMWithState({
|
||||
handler,
|
||||
executionPayload,
|
||||
tx,
|
||||
evm,
|
||||
logger,
|
||||
}: {
|
||||
handler: ELApiHandlers["eth_getProof"] | ELApiHandlers["eth_getCode"] | ELApiHandlers["eth_createAccessList"];
|
||||
evm: VM;
|
||||
executionPayload: allForks.ExecutionPayload;
|
||||
tx: ELTransaction;
|
||||
logger: Logger;
|
||||
}): Promise<VM> {
|
||||
const {stateRoot, blockHash, gasLimit} = executionPayload;
|
||||
const blockHashHex = bufferToHex(blockHash);
|
||||
|
||||
// Create Access List for the contract call
|
||||
const accessListTx = cleanObject({
|
||||
to: tx.to,
|
||||
from: tx.from ?? ZERO_ADDRESS,
|
||||
data: tx.data,
|
||||
value: tx.value,
|
||||
gas: tx.gas ? tx.gas : numberToHex(gasLimit),
|
||||
gasPrice: "0x0",
|
||||
}) as ELTransaction;
|
||||
const response = await elRpc(handler as ELApiHandlers["eth_createAccessList"], "eth_createAccessList", [
|
||||
accessListTx,
|
||||
blockHashHex,
|
||||
]);
|
||||
|
||||
if (!isValidResponse(response) || response.result.error) {
|
||||
throw new Error("Invalid response from RPC");
|
||||
}
|
||||
|
||||
const storageKeysMap: Record<string, string[]> = {};
|
||||
for (const {address, storageKeys} of response.result.accessList) {
|
||||
storageKeysMap[address] = storageKeys;
|
||||
}
|
||||
|
||||
if (storageKeysMap[tx.from ?? ZERO_ADDRESS] === undefined) {
|
||||
storageKeysMap[tx.from ?? ZERO_ADDRESS] = [];
|
||||
}
|
||||
|
||||
if (tx.to && storageKeysMap[tx.to] === undefined) {
|
||||
storageKeysMap[tx.to] = [];
|
||||
}
|
||||
|
||||
// TODO: When we support batch requests, process with a single request
|
||||
const proofsAndCodes: Record<string, {proof: ELProof; code: string}> = {};
|
||||
for (const [address, storageKeys] of Object.entries(storageKeysMap)) {
|
||||
const {result: proof} = await elRpc(
|
||||
handler as ELApiHandlers["eth_getProof"],
|
||||
"eth_getProof",
|
||||
[address, storageKeys, blockHashHex],
|
||||
true
|
||||
);
|
||||
const validAccount = await isValidAccount({address, proof, logger, stateRoot});
|
||||
const validStorage = validAccount && (await isValidStorageKeys({storageKeys, proof, logger}));
|
||||
if (!validAccount || !validStorage) {
|
||||
throw new Error(`Invalid account: ${address}`);
|
||||
}
|
||||
|
||||
const {result: codeResponse} = await elRpc(
|
||||
handler as ELApiHandlers["eth_getCode"],
|
||||
"eth_getCode",
|
||||
[address, blockHashHex],
|
||||
true
|
||||
);
|
||||
|
||||
if (!(await isValidCodeHash({codeResponse, logger, codeHash: proof.codeHash}))) {
|
||||
throw new Error(`Invalid code hash: ${address}`);
|
||||
}
|
||||
|
||||
proofsAndCodes[address] = {proof, code: codeResponse};
|
||||
}
|
||||
|
||||
await evm.stateManager.checkpoint();
|
||||
for (const [addressHex, {proof, code}] of Object.entries(proofsAndCodes)) {
|
||||
const address = Address.fromString(addressHex);
|
||||
const codeBuffer = hexToBuffer(code);
|
||||
|
||||
const account = Account.fromAccountData({
|
||||
nonce: BigInt(proof.nonce),
|
||||
balance: BigInt(proof.balance),
|
||||
codeHash: proof.codeHash,
|
||||
});
|
||||
|
||||
await evm.stateManager.putAccount(address, account);
|
||||
|
||||
for (const {key, value} of proof.storageProof) {
|
||||
await evm.stateManager.putContractStorage(
|
||||
address,
|
||||
padLeft(hexToBuffer(key), 32),
|
||||
padLeft(hexToBuffer(value), 32)
|
||||
);
|
||||
}
|
||||
|
||||
if (codeBuffer.byteLength !== 0) await evm.stateManager.putContractCode(address, codeBuffer);
|
||||
}
|
||||
|
||||
await evm.stateManager.commit();
|
||||
return evm;
|
||||
}
|
||||
|
||||
export async function executeEVM({
|
||||
handler,
|
||||
tx,
|
||||
evm,
|
||||
executionPayload,
|
||||
}: {
|
||||
handler: ELApiHandlers["eth_getBlockByHash"];
|
||||
tx: ELTransaction;
|
||||
evm: VM;
|
||||
executionPayload: allForks.ExecutionPayload;
|
||||
}): Promise<HexString> {
|
||||
const {from, to, gas, gasPrice, maxPriorityFeePerGas, value, data} = tx;
|
||||
const {result: block} = await elRpc(
|
||||
handler,
|
||||
"eth_getBlockByHash",
|
||||
[bufferToHex(executionPayload.blockHash), true],
|
||||
true
|
||||
);
|
||||
|
||||
if (!block) {
|
||||
throw new Error(`Block not found: ${bufferToHex(executionPayload.blockHash)}`);
|
||||
}
|
||||
|
||||
const {execResult} = await evm.evm.runCall({
|
||||
caller: from ? Address.fromString(from) : undefined,
|
||||
to: to ? Address.fromString(to) : undefined,
|
||||
gasLimit: hexToBigInt(gas ?? block.gasLimit),
|
||||
gasPrice: hexToBigInt(gasPrice ?? maxPriorityFeePerGas ?? "0x0"),
|
||||
value: hexToBigInt(value ?? "0x0"),
|
||||
data: data ? hexToBuffer(data) : undefined,
|
||||
block: {
|
||||
header: evmBlockHeaderFromELBlock(block, executionPayload),
|
||||
},
|
||||
});
|
||||
|
||||
if (execResult.exceptionError) {
|
||||
throw new Error(execResult.exceptionError.error);
|
||||
}
|
||||
|
||||
return bufferToHex(execResult.returnValue);
|
||||
}
|
||||
|
||||
export function evmBlockHeaderFromELBlock(
|
||||
block: ELBlock,
|
||||
executionPayload: allForks.ExecutionPayload
|
||||
): {
|
||||
number: bigint;
|
||||
cliqueSigner(): Address;
|
||||
coinbase: Address;
|
||||
timestamp: bigint;
|
||||
difficulty: bigint;
|
||||
prevRandao: Buffer;
|
||||
gasLimit: bigint;
|
||||
baseFeePerGas?: bigint;
|
||||
} {
|
||||
return {
|
||||
number: hexToBigInt(block.number),
|
||||
cliqueSigner: () => Address.fromString(block.miner),
|
||||
timestamp: hexToBigInt(block.timestamp),
|
||||
difficulty: hexToBigInt(block.difficulty),
|
||||
gasLimit: hexToBigInt(block.gasLimit),
|
||||
baseFeePerGas: block.baseFeePerGas ? hexToBigInt(block.baseFeePerGas) : undefined,
|
||||
prevRandao: Buffer.from(executionPayload.prevRandao),
|
||||
// TODO: Fix the coinbase address
|
||||
coinbase: Address.fromString(block.miner),
|
||||
};
|
||||
}
|
||||
@@ -1,140 +1,84 @@
|
||||
import {Logger} from "@lodestar/utils";
|
||||
import {Common, CustomChain, Hardfork} from "@ethereumjs/common";
|
||||
import {NetworkName} from "@lodestar/config/networks";
|
||||
import {ELRequestHandler} from "../interfaces.js";
|
||||
import {ProofProvider} from "../proof_provider/proof_provider.js";
|
||||
import {ELBlock, ELProof, ELRequestPayload, ELResponse, HexString} from "../types.js";
|
||||
import {bufferToHex} from "./conversion.js";
|
||||
import {ELApiHandlers, ELApiParams, ELApiReturn, ELResponse, ELResponseWithResult} from "../types.js";
|
||||
import {isValidResponse} from "./json_rpc.js";
|
||||
import {isValidAccount, isValidBlock, isValidCodeHash, isValidStorageKeys} from "./verification.js";
|
||||
import {isBlockNumber} from "./validation.js";
|
||||
|
||||
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> {
|
||||
export function getRequestId(): string {
|
||||
// TODO: Find better way to generate random id
|
||||
const codeResult = await handler({
|
||||
return (Math.random() * 10000).toFixed(0);
|
||||
}
|
||||
|
||||
export async function elRpc<
|
||||
P,
|
||||
R,
|
||||
E extends boolean = false,
|
||||
Return = E extends true ? ELResponseWithResult<R> : ELResponse<R> | undefined
|
||||
>(handler: ELRequestHandler<P, R>, method: string, args: P, raiseError?: E): Promise<Return> {
|
||||
const response = await handler({
|
||||
jsonrpc: "2.0",
|
||||
method: "eth_getCode",
|
||||
method,
|
||||
params: args,
|
||||
id: (Math.random() * 10000).toFixed(0),
|
||||
id: getRequestId(),
|
||||
});
|
||||
|
||||
if (!codeResult || !codeResult.result) {
|
||||
throw new Error("Can not find code for given address.");
|
||||
if (raiseError && !isValidResponse(response)) {
|
||||
throw new Error(`Invalid response from RPC. method=${method} args=${JSON.stringify(args)}`);
|
||||
}
|
||||
|
||||
return response as Return;
|
||||
}
|
||||
|
||||
export async function getELCode(
|
||||
handler: ELApiHandlers["eth_getCode"],
|
||||
args: ELApiParams["eth_getCode"]
|
||||
): Promise<ELApiReturn["eth_getCode"]> {
|
||||
const codeResult = await elRpc(handler, "eth_getCode", args);
|
||||
|
||||
if (!isValidResponse(codeResult)) {
|
||||
throw new Error(`Can not find code. address=${args[0]}`);
|
||||
}
|
||||
|
||||
return codeResult.result;
|
||||
}
|
||||
|
||||
export async function getELProof(
|
||||
// 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
|
||||
const proof = await handler({
|
||||
jsonrpc: "2.0",
|
||||
method: "eth_getProof",
|
||||
params: args,
|
||||
id: (Math.random() * 10000).toFixed(0),
|
||||
});
|
||||
if (!proof) {
|
||||
throw new Error("Can not find proof for given address.");
|
||||
handler: ELApiHandlers["eth_getProof"],
|
||||
args: ELApiParams["eth_getProof"]
|
||||
): Promise<ELApiReturn["eth_getProof"]> {
|
||||
const proof = await elRpc(handler, "eth_getProof", args);
|
||||
|
||||
if (!isValidResponse(proof)) {
|
||||
throw new Error(`Can not find proof for address=${args[0]}`);
|
||||
}
|
||||
return proof.result as ELProof;
|
||||
return proof.result;
|
||||
}
|
||||
|
||||
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)]);
|
||||
export async function getELBlock(
|
||||
handler: ELApiHandlers["eth_getBlockByNumber"],
|
||||
args: ELApiParams["eth_getBlockByNumber"]
|
||||
): Promise<ELApiReturn["eth_getBlockByNumber"]> {
|
||||
const block = await elRpc(handler, isBlockNumber(args[0]) ? "eth_getBlockByNumber" : "eth_getBlockByHash", args);
|
||||
|
||||
if (
|
||||
(await isValidAccount({
|
||||
address: address,
|
||||
stateRoot: executionPayload.stateRoot,
|
||||
proof,
|
||||
logger,
|
||||
})) &&
|
||||
(await isValidStorageKeys({storageKeys: [], proof, logger}))
|
||||
) {
|
||||
return {data: proof, valid: true};
|
||||
if (!isValidResponse(block)) {
|
||||
throw new Error(`Can not find block. id=${args[0]}`);
|
||||
}
|
||||
|
||||
return {valid: false};
|
||||
return block.result;
|
||||
}
|
||||
|
||||
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};
|
||||
export function getChainCommon(network: NetworkName): Common {
|
||||
switch (network) {
|
||||
case "mainnet":
|
||||
case "goerli":
|
||||
case "ropsten":
|
||||
case "sepolia":
|
||||
// TODO: Not sure how to detect the fork during runtime
|
||||
return new Common({chain: network, hardfork: Hardfork.Shanghai});
|
||||
case "gnosis":
|
||||
return new Common({chain: CustomChain.xDaiChain});
|
||||
default:
|
||||
throw new Error(`Non supported network "${network}"`);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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,
|
||||
network,
|
||||
}))
|
||||
) {
|
||||
return {data: elResponse, valid: true};
|
||||
}
|
||||
|
||||
return {valid: false};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {LogData, Logger} from "@lodestar/utils";
|
||||
import {UNVERIFIED_RESPONSE_CODE} from "../constants.js";
|
||||
import {ELRequestPayload, ELResponse} from "../types.js";
|
||||
import {ELResponseWithError} from "../types.js";
|
||||
import {ELRequestPayload, ELResponse, ELResponseWithResult} from "../types.js";
|
||||
|
||||
export function generateRPCResponseForPayload<P, R, E = unknown>(
|
||||
payload: ELRequestPayload<P>,
|
||||
@@ -9,25 +11,42 @@ export function generateRPCResponseForPayload<P, R, E = unknown>(
|
||||
readonly data?: E;
|
||||
readonly message: string;
|
||||
}
|
||||
): ELResponse<R> {
|
||||
return error
|
||||
? {
|
||||
jsonrpc: payload.jsonrpc,
|
||||
id: payload.id,
|
||||
error,
|
||||
}
|
||||
: {
|
||||
jsonrpc: payload.jsonrpc,
|
||||
id: payload.id,
|
||||
result: res,
|
||||
};
|
||||
): ELResponse<R, E> {
|
||||
if (res !== undefined && error === undefined) {
|
||||
return {
|
||||
jsonrpc: payload.jsonrpc,
|
||||
id: payload.id,
|
||||
result: res,
|
||||
};
|
||||
}
|
||||
|
||||
if (error !== undefined) {
|
||||
return {
|
||||
jsonrpc: payload.jsonrpc,
|
||||
id: payload.id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error("Either result or error must be defined.");
|
||||
}
|
||||
|
||||
export function generateVerifiedResponseForPayload<D, P>(
|
||||
payload: ELRequestPayload<P>,
|
||||
res: D
|
||||
): ELResponseWithResult<D> {
|
||||
return {
|
||||
jsonrpc: payload.jsonrpc,
|
||||
id: payload.id,
|
||||
result: res,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateUnverifiedResponseForPayload<P, D = unknown>(
|
||||
payload: ELRequestPayload<P>,
|
||||
message: string,
|
||||
data?: D
|
||||
): ELResponse<never, D> {
|
||||
): ELResponseWithError<D> {
|
||||
return data !== undefined || data !== null
|
||||
? {
|
||||
jsonrpc: payload.jsonrpc,
|
||||
@@ -48,6 +67,33 @@ export function generateUnverifiedResponseForPayload<P, D = unknown>(
|
||||
};
|
||||
}
|
||||
|
||||
export function isValidResponse<R, E>(response: ELResponse<R, E>): response is ELResponse<R, never> {
|
||||
return response.error === undefined;
|
||||
export function isValidResponse<R, E>(response: ELResponse<R, E> | undefined): response is ELResponseWithResult<R> {
|
||||
return response !== undefined && response.error === undefined;
|
||||
}
|
||||
|
||||
export function logRequest(payload: ELRequestPayload, logger: Logger): void {
|
||||
logger.debug("PR -> EL", {
|
||||
id: payload.id,
|
||||
method: payload.method,
|
||||
params: payload.params,
|
||||
} as unknown as LogData);
|
||||
}
|
||||
|
||||
export function logResponse(response: ELResponse | null | undefined, logger: Logger): void {
|
||||
if (response === undefined || response === null) {
|
||||
logger.debug("PR <- EL (empty response)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValidResponse(response)) {
|
||||
logger.debug("PR <- EL", {
|
||||
id: response.id,
|
||||
result: response.result as Record<string, string> | string,
|
||||
} as unknown as LogData);
|
||||
} else {
|
||||
logger.debug("PR <- E:", {
|
||||
id: response.id,
|
||||
error: response.error,
|
||||
} as unknown as LogData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Logger} from "@lodestar/utils";
|
||||
import {LogData, Logger} from "@lodestar/utils";
|
||||
import {NetworkName} from "@lodestar/config/networks";
|
||||
import {ELRequestHandler, ELVerifiedRequestHandler} from "../interfaces.js";
|
||||
import {ProofProvider} from "../proof_provider/proof_provider.js";
|
||||
@@ -8,6 +8,7 @@ import {eth_getTransactionCount} from "../verified_requests/eth_getTransactionCo
|
||||
import {eth_getBlockByHash} from "../verified_requests/eth_getBlockByHash.js";
|
||||
import {eth_getBlockByNumber} from "../verified_requests/eth_getBlockByNumber.js";
|
||||
import {eth_getCode} from "../verified_requests/eth_getCode.js";
|
||||
import {eth_call} from "../verified_requests/eth_call.js";
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any */
|
||||
export const supportedELRequests: Record<string, ELVerifiedRequestHandler<any, any>> = {
|
||||
@@ -16,6 +17,7 @@ export const supportedELRequests: Record<string, ELVerifiedRequestHandler<any, a
|
||||
eth_getBlockByHash: eth_getBlockByHash,
|
||||
eth_getBlockByNumber: eth_getBlockByNumber,
|
||||
eth_getCode: eth_getCode,
|
||||
eth_call: eth_call,
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any*/
|
||||
|
||||
@@ -33,11 +35,11 @@ export async function processAndVerifyRequest({
|
||||
network: NetworkName;
|
||||
}): Promise<ELResponse | undefined> {
|
||||
await proofProvider.waitToBeReady();
|
||||
logger.debug("Processing request", {method: payload.method, params: JSON.stringify(payload.params)});
|
||||
logger.debug("Processing request", {method: payload.method, params: payload.params} as unknown as LogData);
|
||||
const verifiedHandler = supportedELRequests[payload.method];
|
||||
|
||||
if (verifiedHandler !== undefined) {
|
||||
logger.verbose("Verified request handler found", {method: payload.method});
|
||||
logger.debug("Verified request handler found", {method: payload.method});
|
||||
return verifiedHandler({payload, handler, proofProvider, logger, network});
|
||||
}
|
||||
|
||||
|
||||
152
packages/prover/src/utils/validation.ts
Normal file
152
packages/prover/src/utils/validation.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import {Block} from "@ethereumjs/block";
|
||||
import {RLP} from "@ethereumjs/rlp";
|
||||
import {Trie} from "@ethereumjs/trie";
|
||||
import {Account, KECCAK256_NULL_S} from "@ethereumjs/util";
|
||||
import {keccak256} from "ethereum-cryptography/keccak.js";
|
||||
import {NetworkName} from "@lodestar/config/networks";
|
||||
import {Bytes32, allForks} from "@lodestar/types";
|
||||
import {Logger} from "@lodestar/utils";
|
||||
import {ELBlock, ELProof, ELStorageProof, HexString} from "../types.js";
|
||||
import {blockDataFromELBlock, bufferToHex, hexToBuffer, padLeft} from "./conversion.js";
|
||||
import {getChainCommon} from "./execution.js";
|
||||
|
||||
const emptyAccountSerialize = new Account().serialize();
|
||||
const storageKeyLength = 32;
|
||||
|
||||
export function isBlockNumber(block: number | string): boolean {
|
||||
if (typeof block === "number") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If block is hex and less than 32 byte long it is a block number, else it's a block hash
|
||||
return hexToBuffer(block).byteLength < 32;
|
||||
}
|
||||
|
||||
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,
|
||||
network,
|
||||
}: {
|
||||
executionPayload: allForks.ExecutionPayload;
|
||||
block: ELBlock;
|
||||
logger: Logger;
|
||||
network: NetworkName;
|
||||
}): Promise<boolean> {
|
||||
const common = getChainCommon(network);
|
||||
common.setHardforkByBlockNumber(executionPayload.blockNumber, undefined, executionPayload.timestamp);
|
||||
|
||||
const blockObject = Block.fromBlockData(blockDataFromELBlock(block), {common});
|
||||
|
||||
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 (bufferToHex(executionPayload.parentHash) !== bufferToHex(blockObject.header.parentHash)) {
|
||||
logger.error("Block parent hash does not match", {
|
||||
rpcBlockHash: bufferToHex(blockObject.header.parentHash),
|
||||
beaconExecutionBlockHash: bufferToHex(executionPayload.parentHash),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export async function isValidCodeHash({
|
||||
codeHash,
|
||||
codeResponse,
|
||||
}: {
|
||||
codeHash: string;
|
||||
codeResponse: string;
|
||||
logger: Logger;
|
||||
}): Promise<boolean> {
|
||||
// if there is no code hash for that address
|
||||
if (codeResponse === "0x" && codeHash === `0x${KECCAK256_NULL_S}`) return true;
|
||||
|
||||
return bufferToHex(keccak256(hexToBuffer(codeResponse))) === codeHash;
|
||||
}
|
||||
@@ -1,158 +1,100 @@
|
||||
import {Block} from "@ethereumjs/block";
|
||||
import {Common, CustomChain, Hardfork} from "@ethereumjs/common";
|
||||
import {RLP} from "@ethereumjs/rlp";
|
||||
import {Trie} from "@ethereumjs/trie";
|
||||
import {Account, KECCAK256_NULL_S} from "@ethereumjs/util";
|
||||
import {keccak256} from "ethereum-cryptography/keccak.js";
|
||||
import {NetworkName} from "@lodestar/config/networks";
|
||||
import {Bytes32, allForks} from "@lodestar/types";
|
||||
import {Logger} from "@lodestar/utils";
|
||||
import {ELBlock, ELProof, ELStorageProof, HexString} from "../types.js";
|
||||
import {blockDataFromELBlock, bufferToHex, hexToBuffer, padLeft} from "./conversion.js";
|
||||
import {ELRequestHandlerAny} from "../interfaces.js";
|
||||
import {ProofProvider} from "../proof_provider/proof_provider.js";
|
||||
import {ELBlock, ELProof, ELRequestPayload, HexString} from "../types.js";
|
||||
import {bufferToHex} from "./conversion.js";
|
||||
import {getELBlock, getELCode, getELProof} from "./execution.js";
|
||||
import {isValidAccount, isValidBlock, isValidCodeHash, isValidStorageKeys} from "./validation.js";
|
||||
|
||||
const emptyAccountSerialize = new Account().serialize();
|
||||
const storageKeyLength = 32;
|
||||
type VerificationResult<T> = {data: T; valid: true} | {valid: false; data?: undefined};
|
||||
|
||||
export async function isValidAccount({
|
||||
export async function verifyAccount({
|
||||
address,
|
||||
stateRoot,
|
||||
proof,
|
||||
proofProvider,
|
||||
logger,
|
||||
handler,
|
||||
block,
|
||||
}: {
|
||||
address: HexString;
|
||||
stateRoot: Bytes32;
|
||||
proof: ELProof;
|
||||
handler: ELRequestHandlerAny;
|
||||
proofProvider: ProofProvider;
|
||||
logger: Logger;
|
||||
}): Promise<boolean> {
|
||||
const trie = await Trie.create();
|
||||
const key = keccak256(hexToBuffer(address));
|
||||
block?: number | string;
|
||||
}): Promise<VerificationResult<ELProof>> {
|
||||
const executionPayload = await proofProvider.getExecutionPayload(block ?? "latest");
|
||||
const proof = await getELProof(handler, [address, [], bufferToHex(executionPayload.blockHash)]);
|
||||
const validAccount = await isValidAccount({
|
||||
address: address,
|
||||
stateRoot: executionPayload.stateRoot,
|
||||
proof,
|
||||
logger,
|
||||
});
|
||||
|
||||
try {
|
||||
const expectedAccountRLP = await trie.verifyProof(
|
||||
Buffer.from(stateRoot),
|
||||
Buffer.from(key),
|
||||
proof.accountProof.map(hexToBuffer)
|
||||
);
|
||||
// If account is invalid don't check the storage
|
||||
const validStorage = validAccount && (await isValidStorageKeys({storageKeys: [], proof, logger}));
|
||||
|
||||
// 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;
|
||||
if (validAccount && validStorage) {
|
||||
return {data: proof, valid: true};
|
||||
}
|
||||
|
||||
return {valid: false};
|
||||
}
|
||||
|
||||
export async function isValidStorageKeys({
|
||||
storageKeys,
|
||||
proof,
|
||||
export async function verifyCode({
|
||||
address,
|
||||
proofProvider,
|
||||
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;
|
||||
}
|
||||
|
||||
function networkToChainCommon(network: NetworkName): Common {
|
||||
switch (network) {
|
||||
case "mainnet":
|
||||
case "goerli":
|
||||
case "ropsten":
|
||||
case "sepolia":
|
||||
// TODO: Not sure how to detect the fork during runtime
|
||||
return new Common({chain: network, hardfork: Hardfork.Shanghai});
|
||||
case "gnosis":
|
||||
return new Common({chain: CustomChain.xDaiChain});
|
||||
default:
|
||||
throw new Error(`Non supported network "${network}"`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function isValidBlock({
|
||||
executionPayload,
|
||||
handler,
|
||||
codeHash,
|
||||
block,
|
||||
}: {
|
||||
address: HexString;
|
||||
handler: ELRequestHandlerAny;
|
||||
proofProvider: ProofProvider;
|
||||
logger: Logger;
|
||||
codeHash: HexString;
|
||||
block?: number | string;
|
||||
}): Promise<VerificationResult<string>> {
|
||||
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 verifyBlock({
|
||||
payload,
|
||||
proofProvider,
|
||||
logger,
|
||||
handler,
|
||||
network,
|
||||
}: {
|
||||
executionPayload: allForks.ExecutionPayload;
|
||||
block: ELBlock;
|
||||
payload: ELRequestPayload<[block: string | number, hydrated: boolean]>;
|
||||
handler: ELRequestHandlerAny;
|
||||
proofProvider: ProofProvider;
|
||||
logger: Logger;
|
||||
network: NetworkName;
|
||||
}): Promise<boolean> {
|
||||
const common = networkToChainCommon(network);
|
||||
common.setHardforkByBlockNumber(executionPayload.blockNumber, undefined, executionPayload.timestamp);
|
||||
}): Promise<VerificationResult<ELBlock>> {
|
||||
const executionPayload = await proofProvider.getExecutionPayload(payload.params[0]);
|
||||
const block = await getELBlock(handler, payload.params);
|
||||
|
||||
const blockObject = Block.fromBlockData(blockDataFromELBlock(block), {common});
|
||||
// If response is not valid from the EL we don't need to verify it
|
||||
if (!block) return {data: block, valid: false};
|
||||
|
||||
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 isValidBlock({
|
||||
logger,
|
||||
block,
|
||||
executionPayload,
|
||||
network,
|
||||
})
|
||||
) {
|
||||
return {data: block, valid: true};
|
||||
}
|
||||
|
||||
if (bufferToHex(executionPayload.parentHash) !== bufferToHex(blockObject.header.parentHash)) {
|
||||
logger.error("Block parent hash does not match", {
|
||||
rpcBlockHash: bufferToHex(blockObject.header.parentHash),
|
||||
beaconExecutionBlockHash: bufferToHex(executionPayload.parentHash),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export async function isValidCodeHash({
|
||||
codeHash,
|
||||
codeResponse,
|
||||
}: {
|
||||
codeHash: string;
|
||||
codeResponse: string;
|
||||
logger: Logger;
|
||||
}): Promise<boolean> {
|
||||
// if there is no code hash for that address
|
||||
if (codeResponse === "0x" && codeHash === `0x${KECCAK256_NULL_S}`) return true;
|
||||
|
||||
return bufferToHex(keccak256(hexToBuffer(codeResponse))) === codeHash;
|
||||
return {valid: false};
|
||||
}
|
||||
|
||||
46
packages/prover/src/verified_requests/eth_call.ts
Normal file
46
packages/prover/src/verified_requests/eth_call.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {ELVerifiedRequestHandler} from "../interfaces.js";
|
||||
import {ELApiHandlers, ELApiParams, ELApiReturn} from "../types.js";
|
||||
import {createEVM, executeEVM, getEVMWithState} from "../utils/evm.js";
|
||||
import {generateRPCResponseForPayload, generateUnverifiedResponseForPayload} from "../utils/json_rpc.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const eth_call: ELVerifiedRequestHandler<ELApiParams["call"], ELApiReturn["call"]> = async ({
|
||||
handler,
|
||||
payload,
|
||||
logger,
|
||||
proofProvider,
|
||||
network,
|
||||
}) => {
|
||||
const {
|
||||
params: [tx, block],
|
||||
} = payload;
|
||||
// We assume that standard tx validation already been done by the caller (web3, ethers.js, etc.)
|
||||
const executionPayload = await proofProvider.getExecutionPayload(block ?? "latest");
|
||||
|
||||
try {
|
||||
// TODO: Optimize the creation of the evm
|
||||
const evm = await createEVM({proofProvider, network});
|
||||
const evmWithState = await getEVMWithState({
|
||||
handler: handler as unknown as ELApiHandlers["eth_getProof"],
|
||||
executionPayload,
|
||||
tx,
|
||||
evm,
|
||||
logger,
|
||||
});
|
||||
const result = await executeEVM({
|
||||
evm: evmWithState,
|
||||
tx,
|
||||
handler: handler as unknown as ELApiHandlers["eth_getBlockByHash"],
|
||||
executionPayload,
|
||||
});
|
||||
|
||||
return generateRPCResponseForPayload(payload, result);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
"Request could not be verified.",
|
||||
{method: payload.method, params: JSON.stringify(payload.params)},
|
||||
err as Error
|
||||
);
|
||||
return generateUnverifiedResponseForPayload(payload, "eth_call request can not be verified.");
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import {ELVerifiedRequestHandler} from "../interfaces.js";
|
||||
import {fetchAndVerifyAccount} from "../utils/execution.js";
|
||||
import {verifyAccount} from "../utils/verification.js";
|
||||
import {generateRPCResponseForPayload, generateUnverifiedResponseForPayload} from "../utils/json_rpc.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -12,12 +12,12 @@ export const eth_getBalance: ELVerifiedRequestHandler<[address: string, block?:
|
||||
const {
|
||||
params: [address, block],
|
||||
} = payload;
|
||||
const result = await fetchAndVerifyAccount({proofProvider, logger, handler, address, block});
|
||||
const result = await verifyAccount({proofProvider, logger, handler, address, block});
|
||||
|
||||
if (result.valid) {
|
||||
return generateRPCResponseForPayload(payload, result.data.balance);
|
||||
}
|
||||
|
||||
logger.error("Request could not be verified.");
|
||||
logger.error("Request could not be verified.", {method: payload.method, params: JSON.stringify(payload.params)});
|
||||
return generateUnverifiedResponseForPayload(payload, "eth_getBalance request can not be verified.");
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {ELVerifiedRequestHandler} from "../interfaces.js";
|
||||
import {ELBlock} from "../types.js";
|
||||
import {fetchAndVerifyBlock} from "../utils/execution.js";
|
||||
import {generateUnverifiedResponseForPayload} from "../utils/json_rpc.js";
|
||||
import {verifyBlock} from "../utils/verification.js";
|
||||
import {generateUnverifiedResponseForPayload, generateVerifiedResponseForPayload} from "../utils/json_rpc.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const eth_getBlockByHash: ELVerifiedRequestHandler<[block: string, hydrated: boolean], ELBlock> = async ({
|
||||
@@ -11,12 +11,12 @@ export const eth_getBlockByHash: ELVerifiedRequestHandler<[block: string, hydrat
|
||||
proofProvider,
|
||||
network,
|
||||
}) => {
|
||||
const result = await fetchAndVerifyBlock({payload, proofProvider, logger, handler, network});
|
||||
const result = await verifyBlock({payload, proofProvider, logger, handler, network});
|
||||
|
||||
if (result.valid) {
|
||||
return result.data;
|
||||
return generateVerifiedResponseForPayload(payload, result.data);
|
||||
}
|
||||
|
||||
logger.error("Request could not be verified.");
|
||||
logger.error("Request could not be verified.", {method: payload.method, params: JSON.stringify(payload.params)});
|
||||
return generateUnverifiedResponseForPayload(payload, "eth_getBlockByHash request can not be verified.");
|
||||
};
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import {ELVerifiedRequestHandler} from "../interfaces.js";
|
||||
import {ELBlock} from "../types.js";
|
||||
import {fetchAndVerifyBlock} from "../utils/execution.js";
|
||||
import {generateUnverifiedResponseForPayload} from "../utils/json_rpc.js";
|
||||
import {verifyBlock} from "../utils/verification.js";
|
||||
import {generateUnverifiedResponseForPayload, generateVerifiedResponseForPayload} from "../utils/json_rpc.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const eth_getBlockByNumber: ELVerifiedRequestHandler<
|
||||
[block: string | number, hydrated: boolean],
|
||||
ELBlock
|
||||
> = async ({handler, payload, logger, proofProvider, network}) => {
|
||||
const result = await fetchAndVerifyBlock({payload, proofProvider, logger, handler, network});
|
||||
const result = await verifyBlock({payload, proofProvider, logger, handler, network});
|
||||
|
||||
if (result.valid) {
|
||||
return result.data;
|
||||
return generateVerifiedResponseForPayload(payload, result.data);
|
||||
}
|
||||
|
||||
logger.error("Request could not be verified.");
|
||||
logger.error("Request could not be verified.", {method: payload.method, params: JSON.stringify(payload.params)});
|
||||
return generateUnverifiedResponseForPayload(payload, "eth_getBlockByNumber request can not be verified.");
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {ELVerifiedRequestHandler} from "../interfaces.js";
|
||||
import {fetchAndVerifyAccount, fetchAndVerifyCode} from "../utils/execution.js";
|
||||
import {verifyAccount, verifyCode} from "../utils/verification.js";
|
||||
import {generateRPCResponseForPayload, generateUnverifiedResponseForPayload} from "../utils/json_rpc.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -13,7 +13,7 @@ export const eth_getCode: ELVerifiedRequestHandler<[address: string, block?: num
|
||||
params: [address, block],
|
||||
} = payload;
|
||||
// TODO: When batch requests are supported merged these two requests into one
|
||||
const accountProof = await fetchAndVerifyAccount({
|
||||
const accountProof = await verifyAccount({
|
||||
proofProvider,
|
||||
logger,
|
||||
handler,
|
||||
@@ -22,11 +22,11 @@ export const eth_getCode: ELVerifiedRequestHandler<[address: string, block?: num
|
||||
});
|
||||
|
||||
if (!accountProof.valid) {
|
||||
logger.error("Request could not be verified.");
|
||||
logger.error("Request could not be verified.", {method: payload.method, params: JSON.stringify(payload.params)});
|
||||
return generateUnverifiedResponseForPayload(payload, "account for eth_getCode request can not be verified.");
|
||||
}
|
||||
|
||||
const codeProof = await fetchAndVerifyCode({
|
||||
const codeProof = await verifyCode({
|
||||
proofProvider,
|
||||
logger,
|
||||
handler,
|
||||
@@ -39,6 +39,6 @@ export const eth_getCode: ELVerifiedRequestHandler<[address: string, block?: num
|
||||
return generateRPCResponseForPayload(payload, codeProof.data);
|
||||
}
|
||||
|
||||
logger.error("Request could not be verified.");
|
||||
logger.error("Request could not be verified.", {method: payload.method, params: JSON.stringify(payload.params)});
|
||||
return generateUnverifiedResponseForPayload(payload, "eth_getCode request can not be verified.");
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {ELVerifiedRequestHandler} from "../interfaces.js";
|
||||
import {fetchAndVerifyAccount} from "../utils/execution.js";
|
||||
import {verifyAccount} from "../utils/verification.js";
|
||||
import {generateRPCResponseForPayload, generateUnverifiedResponseForPayload} from "../utils/json_rpc.js";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -10,12 +10,12 @@ export const eth_getTransactionCount: ELVerifiedRequestHandler<
|
||||
const {
|
||||
params: [address, block],
|
||||
} = payload;
|
||||
const result = await fetchAndVerifyAccount({proofProvider, logger, handler, address, block});
|
||||
const result = await verifyAccount({proofProvider, logger, handler, address, block});
|
||||
|
||||
if (result.valid) {
|
||||
return generateRPCResponseForPayload(payload, result.data.nonce);
|
||||
}
|
||||
|
||||
logger.error("Request could not be verified.");
|
||||
logger.error("Request could not be verified.", {method: payload.method, params: JSON.stringify(payload.params)});
|
||||
return generateUnverifiedResponseForPayload(payload, "eth_getTransactionCount request can not be verified.");
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "./utils/assertion.js";
|
||||
import {getLogger} from "./utils/logger.js";
|
||||
import {processAndVerifyRequest} from "./utils/process.js";
|
||||
import {logRequest, logResponse} from "./utils/json_rpc.js";
|
||||
|
||||
type ProvableProviderInitOpts = {network?: NetworkName; wsCheckpoint?: string; signal?: AbortSignal} & LogOptions &
|
||||
ConsensusNodeOptions;
|
||||
@@ -79,7 +80,9 @@ function handleSendProvider(
|
||||
const send = provider.send.bind(provider);
|
||||
const handler = (payload: ELRequestPayload): Promise<ELResponse | undefined> =>
|
||||
new Promise((resolve, reject) => {
|
||||
logRequest(payload, logger);
|
||||
send(payload, (err, response) => {
|
||||
logResponse(response, logger);
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
@@ -106,7 +109,9 @@ function handleRequestProvider(
|
||||
const request = provider.request.bind(provider);
|
||||
const handler = (payload: ELRequestPayload): Promise<ELResponse | undefined> =>
|
||||
new Promise((resolve, reject) => {
|
||||
logRequest(payload, logger);
|
||||
request(payload, (err, response) => {
|
||||
logResponse(response, logger);
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
@@ -131,7 +136,12 @@ function handleSendAsyncProvider(
|
||||
network: NetworkName
|
||||
): SendAsyncProvider {
|
||||
const sendAsync = provider.sendAsync.bind(provider);
|
||||
const handler = (payload: ELRequestPayload): Promise<ELResponse | undefined> => sendAsync(payload);
|
||||
const handler = async (payload: ELRequestPayload): Promise<ELResponse | undefined> => {
|
||||
logRequest(payload, logger);
|
||||
const response = await sendAsync(payload);
|
||||
logResponse(response, logger);
|
||||
return response;
|
||||
};
|
||||
|
||||
async function newSendAsync(payload: ELRequestPayload): Promise<ELResponse | undefined> {
|
||||
return processAndVerifyRequest({payload, handler, proofProvider, logger, network});
|
||||
@@ -147,7 +157,12 @@ function handleEIP1193Provider(
|
||||
network: NetworkName
|
||||
): EIP1193Provider {
|
||||
const request = provider.request.bind(provider);
|
||||
const handler = (payload: ELRequestPayload): Promise<ELResponse | undefined> => request(payload);
|
||||
const handler = async (payload: ELRequestPayload): Promise<ELResponse | undefined> => {
|
||||
logRequest(payload, logger);
|
||||
const response = await request(payload);
|
||||
logResponse(response, logger);
|
||||
return response;
|
||||
};
|
||||
|
||||
async function newRequest(payload: ELRequestPayload): Promise<ELResponse | undefined> {
|
||||
return processAndVerifyRequest({payload, handler, proofProvider, logger, network});
|
||||
@@ -163,7 +178,12 @@ function handleEthersProvider(
|
||||
network: NetworkName
|
||||
): EthersProvider {
|
||||
const send = provider.send.bind(provider);
|
||||
const handler = (payload: ELRequestPayload): Promise<ELResponse | undefined> => send(payload.method, payload.params);
|
||||
const handler = async (payload: ELRequestPayload): Promise<ELResponse | undefined> => {
|
||||
logRequest(payload, logger);
|
||||
const response = await send(payload.method, payload.params);
|
||||
logResponse(response, logger);
|
||||
return response;
|
||||
};
|
||||
|
||||
async function newSend(method: string, params: Array<unknown>): Promise<ELResponse | undefined> {
|
||||
return processAndVerifyRequest({
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 {generateRPCResponseForPayload} from "./utils/json_rpc.js";
|
||||
import {generateRPCResponseForPayload, logRequest, logResponse} 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";
|
||||
@@ -61,10 +61,15 @@ export function createVerifiedExecutionProxy(opts: VerifiedProxyOptions): {
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
fetchResponseBody(res).then(resolve).catch(reject);
|
||||
fetchResponseBody(res)
|
||||
.then((response) => {
|
||||
logResponse(response, logger);
|
||||
resolve(response);
|
||||
})
|
||||
.catch(reject);
|
||||
}
|
||||
);
|
||||
logger.debug("Sending request to proxy endpoint", {method: payload.method});
|
||||
logRequest(payload, logger);
|
||||
req.write(JSON.stringify(payload));
|
||||
req.end();
|
||||
});
|
||||
@@ -73,7 +78,7 @@ export function createVerifiedExecutionProxy(opts: VerifiedProxyOptions): {
|
||||
logger.info("Creating http server");
|
||||
const proxyServer = http.createServer(function proxyRequestHandler(req, res) {
|
||||
if (req.url === "/proxy") {
|
||||
logger.verbose("Forwarding request to execution layer");
|
||||
logger.debug("Forwarding request to execution layer");
|
||||
proxy.web(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ describe("web3_provider", () => {
|
||||
describe("web3", () => {
|
||||
it("should connect to the network and call non-verified method", async () => {
|
||||
const {provider} = createVerifiedExecutionProvider(
|
||||
new Web3.providers.HttpProvider("https://lodestar-sepoliarpc.chainsafe.io"),
|
||||
new Web3.providers.HttpProvider("https://lodestar-mainnetrpc.chainsafe.io"),
|
||||
{
|
||||
transport: LCTransport.Rest,
|
||||
urls: ["https://lodestar-sepolia.chainsafe.io"],
|
||||
network: "sepolia",
|
||||
urls: ["https://lodestar-mainnet.chainsafe.io"],
|
||||
network: "mainnet",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -30,11 +30,11 @@ describe("web3_provider", () => {
|
||||
describe("ethers", () => {
|
||||
it("should connect to the network and call non-verified method", async () => {
|
||||
const {provider} = createVerifiedExecutionProvider(
|
||||
new ethers.JsonRpcProvider("https://lodestar-sepoliarpc.chainsafe.io"),
|
||||
new ethers.JsonRpcProvider("https://lodestar-mainnetrpc.chainsafe.io"),
|
||||
{
|
||||
transport: LCTransport.Rest,
|
||||
urls: ["https://lodestar-sepolia.chainsafe.io"],
|
||||
network: "sepolia",
|
||||
urls: ["https://lodestar-mainnet.chainsafe.io"],
|
||||
network: "mainnet",
|
||||
}
|
||||
);
|
||||
await expect(provider.send("eth_getProof", ["0xf97e180c050e5Ab072211Ad2C213Eb5AEE4DF134", [], "latest"]))
|
||||
|
||||
5356
packages/prover/test/fixtures/mainnet/eth_call.json
vendored
Normal file
5356
packages/prover/test/fixtures/mainnet/eth_call.json
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2848
packages/prover/test/fixtures/sepolia/eth_getCode.json
vendored
2848
packages/prover/test/fixtures/sepolia/eth_getCode.json
vendored
File diff suppressed because one or more lines are too long
2706
packages/prover/test/fixtures/sepolia/eth_getTransactionCount.json
vendored
Normal file
2706
packages/prover/test/fixtures/sepolia/eth_getTransactionCount.json
vendored
Normal file
File diff suppressed because one or more lines are too long
75
packages/prover/test/mocks/request_handler.ts
Normal file
75
packages/prover/test/mocks/request_handler.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import sinon from "sinon";
|
||||
import {NetworkName} from "@lodestar/config/networks";
|
||||
import {ForkConfig} from "@lodestar/config";
|
||||
import {ELVerifiedRequestHandlerOpts} from "../../src/interfaces.js";
|
||||
import {createMockLogger} from "../mocks/logger_mock.js";
|
||||
import {ProofProvider} from "../../src/proof_provider/proof_provider.js";
|
||||
import {ELRequestPayload, ELResponse} from "../../src/types.js";
|
||||
import {ELBlock} from "../../src/types.js";
|
||||
|
||||
type Writeable<T> = {
|
||||
-readonly [K in keyof T]?: T[K] extends object ? Writeable<T[K]> : T[K];
|
||||
};
|
||||
|
||||
export interface TestFixture<R = unknown, P = unknown[]> {
|
||||
label: string;
|
||||
network: string;
|
||||
request: ELRequestPayload<P>;
|
||||
response: Writeable<ELResponse<R>>;
|
||||
execution: {
|
||||
block: ELBlock;
|
||||
};
|
||||
beacon: {
|
||||
executionPayload: Record<string, unknown>;
|
||||
headers: {header: {message: {slot: string}}};
|
||||
};
|
||||
dependentRequests: {payload: ELRequestPayload; response: Writeable<ELResponse>}[];
|
||||
}
|
||||
|
||||
export function generateReqHandlerOptionsMock(
|
||||
data: TestFixture,
|
||||
config: ForkConfig
|
||||
): Omit<ELVerifiedRequestHandlerOpts<any, any>, "payload"> {
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(data.beacon.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(data.beacon.executionPayload);
|
||||
|
||||
const options = {
|
||||
handler: sinon.stub(),
|
||||
logger: createMockLogger(),
|
||||
proofProvider: {
|
||||
getExecutionPayload: sinon.stub().resolves(executionPayload),
|
||||
} as unknown as ProofProvider,
|
||||
network: data.network as NetworkName,
|
||||
};
|
||||
|
||||
options.handler
|
||||
.withArgs({jsonrpc: sinon.match.any, id: sinon.match.any, method: data.request.method, params: data.request.params})
|
||||
.resolves(data.response);
|
||||
|
||||
for (const req of data.dependentRequests) {
|
||||
options.handler
|
||||
.withArgs({jsonrpc: sinon.match.any, id: sinon.match.any, method: req.payload.method, params: req.payload.params})
|
||||
.resolves(req.response);
|
||||
}
|
||||
|
||||
options.handler
|
||||
.withArgs({
|
||||
jsonrpc: sinon.match.any,
|
||||
id: sinon.match.any,
|
||||
method: "eth_getBlockByNumber",
|
||||
params: [data.execution.block.number, true],
|
||||
})
|
||||
.resolves({id: 1233, jsonrpc: "2.0", result: data.execution.block});
|
||||
|
||||
options.handler
|
||||
.withArgs({
|
||||
jsonrpc: sinon.match.any,
|
||||
id: sinon.match.any,
|
||||
method: "eth_getBlockByHash",
|
||||
params: [data.execution.block.hash, true],
|
||||
})
|
||||
.resolves({id: 1233, jsonrpc: "2.0", result: data.execution.block});
|
||||
|
||||
return options;
|
||||
}
|
||||
@@ -3,15 +3,15 @@ import chai from "chai";
|
||||
import chaiAsPromised from "chai-as-promised";
|
||||
import deepmerge from "deepmerge";
|
||||
import {ELProof, ELStorageProof} from "../../../src/types.js";
|
||||
import {isValidAccount, isValidStorageKeys} from "../../../src/utils/verification.js";
|
||||
import {isValidAccount, isValidStorageKeys} from "../../../src/utils/validation.js";
|
||||
import {invalidStorageProof, validStorageProof} from "../../fixtures/index.js";
|
||||
import {createMockLogger} from "../../mocks/logger_mock.js";
|
||||
import eoaProof from "../../fixtures/sepolia/eth_getBalance_eoa_proof.json" assert {type: "json"};
|
||||
import eoaProof from "../../fixtures/sepolia/eth_getBalance_eoa.json" assert {type: "json"};
|
||||
import {hexToBuffer} from "../../../src/utils/conversion.js";
|
||||
|
||||
const address = eoaProof.request.params[0] as string;
|
||||
const validAccountProof = eoaProof.response.result as unknown as ELProof;
|
||||
const validStateRoot = hexToBuffer(eoaProof.executionPayload.state_root);
|
||||
const validAccountProof = eoaProof.dependentRequests[0].response.result as unknown as ELProof;
|
||||
const validStateRoot = hexToBuffer(eoaProof.beacon.executionPayload.state_root);
|
||||
|
||||
const invalidAccountProof = deepmerge(validAccountProof, {});
|
||||
delete invalidAccountProof.accountProof[0];
|
||||
|
||||
62
packages/prover/test/unit/verified_requests/eth_call.test.ts
Normal file
62
packages/prover/test/unit/verified_requests/eth_call.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import {expect} from "chai";
|
||||
import deepmerge from "deepmerge";
|
||||
import {createForkConfig} from "@lodestar/config";
|
||||
import {NetworkName, networksChainConfig} from "@lodestar/config/networks";
|
||||
import {ELTransaction} from "../../../lib/types.js";
|
||||
import {UNVERIFIED_RESPONSE_CODE} from "../../../src/constants.js";
|
||||
import {eth_call} from "../../../src/verified_requests/eth_call.js";
|
||||
import ethCallCase1 from "../../fixtures/mainnet/eth_call.json" assert {type: "json"};
|
||||
import {generateReqHandlerOptionsMock} from "../../mocks/request_handler.js";
|
||||
|
||||
const testCases = [ethCallCase1];
|
||||
|
||||
describe("verified_requests / eth_call", () => {
|
||||
for (const t of testCases) {
|
||||
describe(t.label, () => {
|
||||
it("should return the valid json-rpc response for a valid call", async () => {
|
||||
const testCase = deepmerge({}, t);
|
||||
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
const response = await eth_call({
|
||||
...options,
|
||||
payload: {
|
||||
...testCase.request,
|
||||
params: testCase.request.params as [ELTransaction, string],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response).to.eql(testCase.response);
|
||||
});
|
||||
|
||||
it("should return the json-rpc response with error for an invalid call", async () => {
|
||||
const testCase = deepmerge(t, {});
|
||||
|
||||
// Temper the responses to make them invalid
|
||||
for (const tx of testCase.dependentRequests) {
|
||||
if (tx.payload.method === "eth_getCode") {
|
||||
tx.response.result = `${tx.response.result}12`;
|
||||
}
|
||||
}
|
||||
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
const response = await eth_call({
|
||||
...options,
|
||||
payload: {
|
||||
...testCase.request,
|
||||
params: testCase.request.params as [ELTransaction, string],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response).to.eql({
|
||||
jsonrpc: "2.0",
|
||||
id: testCase.request.id,
|
||||
error: {code: UNVERIFIED_RESPONSE_CODE, message: "eth_call request can not be verified."},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,79 +1,51 @@
|
||||
import {expect} from "chai";
|
||||
import sinon from "sinon";
|
||||
import deepmerge from "deepmerge";
|
||||
import {createForkConfig} from "@lodestar/config";
|
||||
import {NetworkName, networksChainConfig} from "@lodestar/config/networks";
|
||||
import {Logger} from "@lodestar/utils";
|
||||
import {UNVERIFIED_RESPONSE_CODE} from "../../../src/constants.js";
|
||||
import {ELVerifiedRequestHandlerOpts} from "../../../src/interfaces.js";
|
||||
import {eth_getBalance} from "../../../src/verified_requests/eth_getBalance.js";
|
||||
import eth_getBalance_eoa from "../../fixtures/sepolia/eth_getBalance_eoa_proof.json" assert {type: "json"};
|
||||
import eth_getBalance_contract from "../../fixtures/sepolia/eth_getBalance_contract_proof.json" assert {type: "json"};
|
||||
import {createMockLogger} from "../../mocks/logger_mock.js";
|
||||
import eth_getBalance_eoa from "../../fixtures/sepolia/eth_getBalance_eoa.json" assert {type: "json"};
|
||||
import eth_getBalance_contract from "../../fixtures/sepolia/eth_getBalance_contract.json" assert {type: "json"};
|
||||
import {generateReqHandlerOptionsMock} from "../../mocks/request_handler.js";
|
||||
|
||||
const testCases = [eth_getBalance_eoa, eth_getBalance_contract];
|
||||
|
||||
describe("verified_requests / eth_getBalance", () => {
|
||||
let options: {handler: sinon.SinonStub; logger: Logger; proofProvider: {getExecutionPayload: sinon.SinonStub}};
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
handler: sinon.stub(),
|
||||
logger: createMockLogger(),
|
||||
proofProvider: {getExecutionPayload: sinon.stub()},
|
||||
};
|
||||
});
|
||||
|
||||
for (const testCase of testCases) {
|
||||
describe(testCase.label, () => {
|
||||
it("should return the valid json-rpc response for a valid account", async () => {
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(testCase.executionPayload);
|
||||
const data = deepmerge({}, testCase);
|
||||
const config = createForkConfig(networksChainConfig[data.network as NetworkName]);
|
||||
const options = generateReqHandlerOptionsMock(data, config);
|
||||
|
||||
options.handler.resolves(testCase.response);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getBalance({
|
||||
const response = await eth_getBalance({
|
||||
...options,
|
||||
payload: {
|
||||
...testCase.request,
|
||||
method: "eth_getBalance",
|
||||
params: [testCase.request.params[0], "latest"],
|
||||
...data.request,
|
||||
params: [data.request.params[0], data.request.params[1]],
|
||||
},
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[address: string, block?: string | number | undefined], string>);
|
||||
|
||||
expect(result).to.eql({...result, result: testCase.response.result.balance});
|
||||
});
|
||||
expect(response).to.eql(data.response);
|
||||
});
|
||||
|
||||
it("should return the json-rpc response with error for an invalid account", async () => {
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(testCase.executionPayload);
|
||||
const data = deepmerge({}, testCase);
|
||||
// Temporarily remove the accountProof to make the request invalid
|
||||
delete data.dependentRequests[0].response.result.accountProof[0];
|
||||
const config = createForkConfig(networksChainConfig[data.network as NetworkName]);
|
||||
const options = generateReqHandlerOptionsMock(data, config);
|
||||
|
||||
const response = deepmerge({}, testCase.response);
|
||||
// Change the proof to be invalidated with state
|
||||
delete response.result.accountProof[0];
|
||||
|
||||
options.handler.resolves(response);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getBalance({
|
||||
const response = await eth_getBalance({
|
||||
...options,
|
||||
payload: {
|
||||
...testCase.request,
|
||||
method: "eth_getBalance",
|
||||
params: [testCase.request.params[0], "latest"],
|
||||
...data.request,
|
||||
params: [data.request.params[0], data.request.params[1]],
|
||||
},
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[address: string, block?: string | number | undefined], string>);
|
||||
});
|
||||
|
||||
expect(result).to.eql({
|
||||
expect(response).to.eql({
|
||||
jsonrpc: "2.0",
|
||||
id: testCase.request.id,
|
||||
id: data.request.id,
|
||||
error: {code: UNVERIFIED_RESPONSE_CODE, message: "eth_getBalance request can not be verified."},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,74 +1,54 @@
|
||||
import {expect} from "chai";
|
||||
import sinon from "sinon";
|
||||
import deepmerge from "deepmerge";
|
||||
import {createForkConfig} from "@lodestar/config";
|
||||
import {NetworkName, networksChainConfig} from "@lodestar/config/networks";
|
||||
import {Logger} from "@lodestar/utils";
|
||||
import {UNVERIFIED_RESPONSE_CODE} from "../../../src/constants.js";
|
||||
import {ELVerifiedRequestHandlerOpts} from "../../../src/interfaces.js";
|
||||
import {ELBlock} from "../../../src/types.js";
|
||||
import {eth_getBlockByHash} from "../../../src/verified_requests/eth_getBlockByHash.js";
|
||||
import eth_getBlock_with_contractCreation from "../../fixtures/sepolia/eth_getBlock_with_contractCreation.json" assert {type: "json"};
|
||||
import eth_getBlock_with_no_accessList from "../../fixtures/sepolia/eth_getBlock_with_no_accessList.json" assert {type: "json"};
|
||||
import {createMockLogger} from "../../mocks/logger_mock.js";
|
||||
import {TestFixture, generateReqHandlerOptionsMock} from "../../mocks/request_handler.js";
|
||||
import {ELBlock} from "../../../src/types.js";
|
||||
|
||||
const testCases = [eth_getBlock_with_no_accessList, eth_getBlock_with_contractCreation];
|
||||
const testCases = [eth_getBlock_with_no_accessList, eth_getBlock_with_contractCreation] as [
|
||||
TestFixture<ELBlock>,
|
||||
TestFixture<ELBlock>
|
||||
];
|
||||
|
||||
describe("verified_requests / eth_getBlockByHash", () => {
|
||||
let options: {handler: sinon.SinonStub; logger: Logger; proofProvider: {getExecutionPayload: sinon.SinonStub}};
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
handler: sinon.stub(),
|
||||
logger: createMockLogger(),
|
||||
proofProvider: {getExecutionPayload: sinon.stub()},
|
||||
};
|
||||
});
|
||||
|
||||
for (const testCase of testCases) {
|
||||
describe(testCase.label, () => {
|
||||
for (const t of testCases) {
|
||||
describe(t.label, () => {
|
||||
it("should return the valid json-rpc response for a valid block", async () => {
|
||||
const testCase = deepmerge({}, t);
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(testCase.executionPayload);
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
options.handler.resolves(testCase.response);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getBlockByHash({
|
||||
const response = await eth_getBlockByHash({
|
||||
...options,
|
||||
payload: testCase.request,
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[block: string, hydrated: boolean], ELBlock>);
|
||||
|
||||
expect(result).to.eql(testCase.response);
|
||||
payload: {
|
||||
...testCase.request,
|
||||
params: testCase.request.params as [string, boolean],
|
||||
},
|
||||
});
|
||||
expect(response).to.eql(testCase.response);
|
||||
});
|
||||
|
||||
it("should return the json-rpc response with error for an invalid block header with valid execution payload", async () => {
|
||||
// Temper the block body
|
||||
const testCase = deepmerge(t, {
|
||||
execution: {block: {parentHash: "0x123490ab601a073c3d128111eafb12fa7ece4af239abdc8be60184a08c6d1234"}},
|
||||
});
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(testCase.executionPayload);
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
const temperedResponse = {
|
||||
...testCase.response,
|
||||
result: {
|
||||
...testCase.response.result,
|
||||
parentHash: "0xbdbd90ab601a073c3d128111eafb12fa7ece4af239abdc8be60184a08c6d7ef4",
|
||||
},
|
||||
};
|
||||
|
||||
options.handler.resolves(temperedResponse);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getBlockByHash({
|
||||
const response = await eth_getBlockByHash({
|
||||
...options,
|
||||
payload: testCase.request,
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[block: string, hydrated: boolean], ELBlock>);
|
||||
payload: {
|
||||
...testCase.request,
|
||||
params: testCase.request.params as [string, boolean],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).to.eql({
|
||||
expect(response).to.eql({
|
||||
jsonrpc: "2.0",
|
||||
id: testCase.request.id,
|
||||
error: {code: UNVERIFIED_RESPONSE_CODE, message: "eth_getBlockByHash request can not be verified."},
|
||||
@@ -76,24 +56,22 @@ describe("verified_requests / eth_getBlockByHash", () => {
|
||||
});
|
||||
|
||||
it("should return the json-rpc response with error for an invalid block body with valid execution payload", async () => {
|
||||
// Temper the block body
|
||||
const testCase = deepmerge(t, {
|
||||
execution: {block: {transactions: [{to: "0xd86e1fedb1120369ff5175b74f4413cb74fcacdb"}]}},
|
||||
});
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(testCase.executionPayload);
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
const temperedResponse = deepmerge<typeof testCase.response, unknown>(testCase.response, {});
|
||||
temperedResponse.result.transactions[0].to = "0xd86e1fedb1120369ff5175b74f4413cb74fcacdb";
|
||||
|
||||
options.handler.resolves(temperedResponse);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getBlockByHash({
|
||||
const response = await eth_getBlockByHash({
|
||||
...options,
|
||||
payload: testCase.request,
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[block: string, hydrated: boolean], ELBlock>);
|
||||
payload: {
|
||||
...testCase.request,
|
||||
params: testCase.request.params as [string, boolean],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).to.eql({
|
||||
expect(response).to.eql({
|
||||
jsonrpc: "2.0",
|
||||
id: testCase.request.id,
|
||||
error: {code: UNVERIFIED_RESPONSE_CODE, message: "eth_getBlockByHash request can not be verified."},
|
||||
@@ -101,27 +79,22 @@ describe("verified_requests / eth_getBlockByHash", () => {
|
||||
});
|
||||
|
||||
it("should return the json-rpc response with error for an valid block with invalid execution payload", async () => {
|
||||
const testCase = deepmerge({}, t);
|
||||
// Temper the execution payload
|
||||
testCase.beacon.executionPayload.parent_hash =
|
||||
"0xbdbd90ab601a073c3d128111eafb12fa7ece4af239abdc8be60184a08c6d7ef4";
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const temperedPayload = {
|
||||
...testCase.executionPayload,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
parent_hash: "0xbdbd90ab601a073c3d128111eafb12fa7ece4af239abdc8be60184a08c6d7ef4",
|
||||
};
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(temperedPayload);
|
||||
|
||||
options.handler.resolves(testCase.response);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getBlockByHash({
|
||||
const response = await eth_getBlockByHash({
|
||||
...options,
|
||||
payload: testCase.request,
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[block: string, hydrated: boolean], ELBlock>);
|
||||
payload: {
|
||||
...testCase.request,
|
||||
params: testCase.request.params as [string, boolean],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).to.eql({
|
||||
expect(response).to.eql({
|
||||
jsonrpc: "2.0",
|
||||
id: testCase.request.id,
|
||||
error: {code: UNVERIFIED_RESPONSE_CODE, message: "eth_getBlockByHash request can not be verified."},
|
||||
|
||||
@@ -1,80 +1,54 @@
|
||||
import {expect} from "chai";
|
||||
import sinon from "sinon";
|
||||
import deepmerge from "deepmerge";
|
||||
import {createForkConfig} from "@lodestar/config";
|
||||
import {NetworkName, networksChainConfig} from "@lodestar/config/networks";
|
||||
import {Logger} from "@lodestar/utils";
|
||||
import {UNVERIFIED_RESPONSE_CODE} from "../../../src/constants.js";
|
||||
import {ELVerifiedRequestHandlerOpts} from "../../../src/interfaces.js";
|
||||
import {ELBlock} from "../../../src/types.js";
|
||||
import {eth_getBlockByNumber} from "../../../src/verified_requests/eth_getBlockByNumber.js";
|
||||
import eth_getBlock_with_contractCreation from "../../fixtures/sepolia/eth_getBlock_with_contractCreation.json" assert {type: "json"};
|
||||
import eth_getBlock_with_no_accessList from "../../fixtures/sepolia/eth_getBlock_with_no_accessList.json" assert {type: "json"};
|
||||
import {createMockLogger} from "../../mocks/logger_mock.js";
|
||||
import {eth_getBlockByNumber} from "../../../src/verified_requests/eth_getBlockByNumber.js";
|
||||
import {TestFixture, generateReqHandlerOptionsMock} from "../../mocks/request_handler.js";
|
||||
|
||||
const testCases = [eth_getBlock_with_no_accessList, eth_getBlock_with_contractCreation];
|
||||
const testCases = [eth_getBlock_with_no_accessList, eth_getBlock_with_contractCreation] as [
|
||||
TestFixture<ELBlock>,
|
||||
TestFixture<ELBlock>
|
||||
];
|
||||
|
||||
describe("verified_requests / eth_getBlockByNumber", () => {
|
||||
let options: {handler: sinon.SinonStub; logger: Logger; proofProvider: {getExecutionPayload: sinon.SinonStub}};
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
handler: sinon.stub(),
|
||||
logger: createMockLogger(),
|
||||
proofProvider: {getExecutionPayload: sinon.stub()},
|
||||
};
|
||||
});
|
||||
|
||||
for (const testCase of testCases) {
|
||||
describe(testCase.label, () => {
|
||||
for (const t of testCases) {
|
||||
describe(t.label, () => {
|
||||
it("should return the valid json-rpc response for a valid block", async () => {
|
||||
const testCase = deepmerge({}, t);
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(testCase.executionPayload);
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
options.handler.resolves(testCase.response);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getBlockByNumber({
|
||||
const response = await eth_getBlockByNumber({
|
||||
...options,
|
||||
payload: {
|
||||
...testCase.request,
|
||||
params: [testCase.executionPayload.block_number, false],
|
||||
params: testCase.request.params as [string | number, boolean],
|
||||
},
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[block: string | number, hydrated: boolean], ELBlock>);
|
||||
|
||||
expect(result).to.eql(testCase.response);
|
||||
});
|
||||
expect(response).to.eql(testCase.response);
|
||||
});
|
||||
|
||||
it("should return the json-rpc response with error for an invalid block header with valid execution payload", async () => {
|
||||
// Temper the block body
|
||||
const testCase = deepmerge(t, {
|
||||
execution: {block: {parentHash: "0xbdbd90ab601a073c3d128111eafb12fa7ece4af239abdc8be60184a08c6d7ef4"}},
|
||||
});
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(testCase.executionPayload);
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
const temperedResponse = {
|
||||
...testCase.response,
|
||||
result: {
|
||||
...testCase.response.result,
|
||||
parentHash: "0xbdbd90ab601a073c3d128111eafb12fa7ece4af239abdc8be60184a08c6d7ef4",
|
||||
},
|
||||
};
|
||||
|
||||
options.handler.resolves(temperedResponse);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getBlockByNumber({
|
||||
const response = await eth_getBlockByNumber({
|
||||
...options,
|
||||
payload: {
|
||||
...testCase.request,
|
||||
params: [testCase.executionPayload.block_number, false],
|
||||
params: testCase.request.params as [string | number, boolean],
|
||||
},
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[block: string | number, hydrated: boolean], ELBlock>);
|
||||
});
|
||||
|
||||
expect(result).to.eql({
|
||||
expect(response).to.eql({
|
||||
jsonrpc: "2.0",
|
||||
id: testCase.request.id,
|
||||
error: {code: UNVERIFIED_RESPONSE_CODE, message: "eth_getBlockByNumber request can not be verified."},
|
||||
@@ -82,27 +56,22 @@ describe("verified_requests / eth_getBlockByNumber", () => {
|
||||
});
|
||||
|
||||
it("should return the json-rpc response with error for an invalid block body with valid execution payload", async () => {
|
||||
// Temper the block body
|
||||
const testCase = deepmerge(t, {
|
||||
execution: {block: {transactions: [{to: "0xd86e1fedb1120369ff5175b74f4413cb74fcacdb"}]}},
|
||||
});
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(testCase.executionPayload);
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
const temperedResponse = deepmerge<typeof testCase.response, unknown>(testCase.response, {});
|
||||
temperedResponse.result.transactions[0].to = "0xd86e1fedb1120369ff5175b74f4413cb74fcacdb";
|
||||
|
||||
options.handler.resolves(temperedResponse);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getBlockByNumber({
|
||||
const response = await eth_getBlockByNumber({
|
||||
...options,
|
||||
payload: {
|
||||
...testCase.request,
|
||||
params: [testCase.executionPayload.block_number, false],
|
||||
params: testCase.request.params as [string | number, boolean],
|
||||
},
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[block: string | number, hydrated: boolean], ELBlock>);
|
||||
});
|
||||
|
||||
expect(result).to.eql({
|
||||
expect(response).to.eql({
|
||||
jsonrpc: "2.0",
|
||||
id: testCase.request.id,
|
||||
error: {code: UNVERIFIED_RESPONSE_CODE, message: "eth_getBlockByNumber request can not be verified."},
|
||||
@@ -110,30 +79,25 @@ describe("verified_requests / eth_getBlockByNumber", () => {
|
||||
});
|
||||
|
||||
it("should return the json-rpc response with error for an valid block with invalid execution payload", async () => {
|
||||
// Temper the execution payload
|
||||
const testCase = deepmerge(t, {
|
||||
beacon: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
executionPayload: {parent_hash: "0xbdbd90ab601a073c3d128111eafb12fa7ece4af239abdc8be60184a08c6d7ef4"},
|
||||
},
|
||||
});
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const temperedPayload = {
|
||||
...testCase.executionPayload,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
parent_hash: "0xbdbd90ab601a073c3d128111eafb12fa7ece4af239abdc8be60184a08c6d7ef4",
|
||||
};
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(temperedPayload);
|
||||
|
||||
options.handler.resolves(testCase.response);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getBlockByNumber({
|
||||
const response = await eth_getBlockByNumber({
|
||||
...options,
|
||||
payload: {
|
||||
...testCase.request,
|
||||
params: [testCase.executionPayload.block_number, false],
|
||||
params: testCase.request.params as [string | number, boolean],
|
||||
},
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[block: string | number, hydrated: boolean], ELBlock>);
|
||||
});
|
||||
|
||||
expect(result).to.eql({
|
||||
expect(response).to.eql({
|
||||
jsonrpc: "2.0",
|
||||
id: testCase.request.id,
|
||||
error: {code: UNVERIFIED_RESPONSE_CODE, message: "eth_getBlockByNumber request can not be verified."},
|
||||
|
||||
@@ -1,70 +1,47 @@
|
||||
import {expect} from "chai";
|
||||
import sinon from "sinon";
|
||||
import deepmerge from "deepmerge";
|
||||
import {createForkConfig} from "@lodestar/config";
|
||||
import {NetworkName, networksChainConfig} from "@lodestar/config/networks";
|
||||
import {Logger} from "@lodestar/utils";
|
||||
import {UNVERIFIED_RESPONSE_CODE} from "../../../src/constants.js";
|
||||
import {ELVerifiedRequestHandlerOpts} from "../../../src/interfaces.js";
|
||||
import {eth_getCode} from "../../../src/verified_requests/eth_getCode.js";
|
||||
import eth_getCodeCase1 from "../../fixtures/sepolia/eth_getCode.json" assert {type: "json"};
|
||||
import ethContractProof from "../../fixtures/sepolia/eth_getBalance_contract_proof.json" assert {type: "json"};
|
||||
import {createMockLogger} from "../../mocks/logger_mock.js";
|
||||
import ethGetCodeCase1 from "../../fixtures/sepolia/eth_getCode.json" assert {type: "json"};
|
||||
import {generateReqHandlerOptionsMock} from "../../mocks/request_handler.js";
|
||||
|
||||
const testCases = [eth_getCodeCase1];
|
||||
const testCases = [ethGetCodeCase1];
|
||||
|
||||
describe("verified_requests / eth_getCode", () => {
|
||||
let options: {handler: sinon.SinonStub; logger: Logger; proofProvider: {getExecutionPayload: sinon.SinonStub}};
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
handler: sinon.stub(),
|
||||
logger: createMockLogger(),
|
||||
proofProvider: {getExecutionPayload: sinon.stub()},
|
||||
};
|
||||
});
|
||||
|
||||
for (const testCase of testCases) {
|
||||
describe(testCase.label, () => {
|
||||
for (const t of testCases) {
|
||||
describe(t.label, () => {
|
||||
it("should return the valid json-rpc response for a valid account", async () => {
|
||||
const testCase = deepmerge({}, t);
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(testCase.executionPayload);
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
options.handler.onFirstCall().resolves(ethContractProof.response);
|
||||
options.handler.onSecondCall().resolves(testCase.response);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getCode({
|
||||
const response = await eth_getCode({
|
||||
...options,
|
||||
payload: testCase.request,
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[address: string, block?: string | number | undefined], string>);
|
||||
payload: {
|
||||
...testCase.request,
|
||||
params: testCase.request.params as [string, string],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).to.eql(testCase.response);
|
||||
expect(response).to.eql(testCase.response);
|
||||
});
|
||||
|
||||
it("should return the json-rpc response with error for an invalid account", async () => {
|
||||
const testCase = deepmerge(t, {response: {result: t.response.result + "1234fe"}});
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(testCase.executionPayload);
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
const temperedResponse = deepmerge({}, testCase.response);
|
||||
temperedResponse.result = temperedResponse.result + "1234fe";
|
||||
|
||||
options.handler.onFirstCall().resolves(ethContractProof.response);
|
||||
options.handler.onSecondCall().resolves(temperedResponse);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getCode({
|
||||
const response = await eth_getCode({
|
||||
...options,
|
||||
payload: testCase.request,
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[address: string, block?: string | number | undefined], string>);
|
||||
payload: {
|
||||
...testCase.request,
|
||||
params: testCase.request.params as [string, string],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).to.eql({
|
||||
expect(response).to.eql({
|
||||
jsonrpc: "2.0",
|
||||
id: testCase.request.id,
|
||||
error: {code: UNVERIFIED_RESPONSE_CODE, message: "eth_getCode request can not be verified."},
|
||||
|
||||
@@ -1,77 +1,49 @@
|
||||
import {expect} from "chai";
|
||||
import sinon from "sinon";
|
||||
import deepmerge from "deepmerge";
|
||||
import {createForkConfig} from "@lodestar/config";
|
||||
import {NetworkName, networksChainConfig} from "@lodestar/config/networks";
|
||||
import {Logger} from "@lodestar/utils";
|
||||
import {UNVERIFIED_RESPONSE_CODE} from "../../../src/constants.js";
|
||||
import {ELVerifiedRequestHandlerOpts} from "../../../src/interfaces.js";
|
||||
import eth_getBalance_eoa from "../../fixtures/sepolia/eth_getBalance_eoa_proof.json" assert {type: "json"};
|
||||
import eth_getBalance_contract from "../../fixtures/sepolia/eth_getBalance_contract_proof.json" assert {type: "json"};
|
||||
import {createMockLogger} from "../../mocks/logger_mock.js";
|
||||
import {eth_getTransactionCount} from "../../../src/verified_requests/eth_getTransactionCount.js";
|
||||
import getTransactionCountCase1 from "../../fixtures/sepolia/eth_getTransactionCount.json" assert {type: "json"};
|
||||
import {generateReqHandlerOptionsMock} from "../../mocks/request_handler.js";
|
||||
|
||||
const testCases = [eth_getBalance_eoa, eth_getBalance_contract];
|
||||
const testCases = [getTransactionCountCase1];
|
||||
|
||||
describe("verified_requests / eth_getTransactionCount", () => {
|
||||
let options: {handler: sinon.SinonStub; logger: Logger; proofProvider: {getExecutionPayload: sinon.SinonStub}};
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
handler: sinon.stub(),
|
||||
logger: createMockLogger(),
|
||||
proofProvider: {getExecutionPayload: sinon.stub()},
|
||||
};
|
||||
});
|
||||
|
||||
for (const testCase of testCases) {
|
||||
describe(testCase.label, () => {
|
||||
for (const t of testCases) {
|
||||
describe(t.label, () => {
|
||||
it("should return the valid json-rpc response for a valid account", async () => {
|
||||
const testCase = deepmerge({}, t);
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(testCase.executionPayload);
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
options.handler.resolves(testCase.response);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getTransactionCount({
|
||||
const response = await eth_getTransactionCount({
|
||||
...options,
|
||||
payload: {
|
||||
...testCase.request,
|
||||
method: "eth_getTransactionCount",
|
||||
params: [testCase.request.params[0], "latest"],
|
||||
params: testCase.request.params as [string, string],
|
||||
},
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[address: string, block?: string | number | undefined], string>);
|
||||
});
|
||||
|
||||
expect(result).to.eql({...result, result: testCase.response.result.nonce});
|
||||
expect(response).to.eql(testCase.response);
|
||||
});
|
||||
|
||||
it("should return the json-rpc response with error for an invalid account", async () => {
|
||||
const testCase = deepmerge({}, t);
|
||||
delete testCase.dependentRequests[0].response.result.accountProof[0];
|
||||
|
||||
const config = createForkConfig(networksChainConfig[testCase.network as NetworkName]);
|
||||
const executionPayload = config
|
||||
.getExecutionForkTypes(parseInt(testCase.headers.header.message.slot))
|
||||
.ExecutionPayload.fromJson(testCase.executionPayload);
|
||||
const options = generateReqHandlerOptionsMock(testCase, config);
|
||||
|
||||
const response = deepmerge({}, testCase.response);
|
||||
// Change the proof to be invalidated with state
|
||||
delete response.result.accountProof[0];
|
||||
|
||||
options.handler.resolves(response);
|
||||
options.proofProvider.getExecutionPayload.resolves(executionPayload);
|
||||
|
||||
const result = await eth_getTransactionCount({
|
||||
const response = await eth_getTransactionCount({
|
||||
...options,
|
||||
payload: {
|
||||
...testCase.request,
|
||||
method: "eth_getTransactionCount",
|
||||
params: [testCase.request.params[0], "latest"],
|
||||
params: testCase.request.params as [string, string],
|
||||
},
|
||||
network: testCase.network,
|
||||
} as unknown as ELVerifiedRequestHandlerOpts<[address: string, block?: string | number | undefined], string>);
|
||||
});
|
||||
|
||||
expect(result).to.eql({
|
||||
expect(response).to.eql({
|
||||
jsonrpc: "2.0",
|
||||
id: testCase.request.id,
|
||||
error: {code: UNVERIFIED_RESPONSE_CODE, message: "eth_getTransactionCount request can not be verified."},
|
||||
|
||||
@@ -6,7 +6,13 @@ const MAX_DEPTH = 0;
|
||||
|
||||
type LogDataBasic = string | number | bigint | boolean | null | undefined;
|
||||
|
||||
export type LogData = LogDataBasic | Record<string, LogDataBasic> | LogDataBasic[] | Record<string, LogDataBasic>[];
|
||||
export type LogData =
|
||||
| LogDataBasic
|
||||
| LogDataBasic[]
|
||||
| Record<string, LogDataBasic>
|
||||
| Record<string, LogDataBasic>[]
|
||||
| Record<string, LogDataBasic[]>
|
||||
| Record<string, LogDataBasic[]>[];
|
||||
|
||||
/**
|
||||
* Renders any log Context to JSON up to one level of depth.
|
||||
|
||||
277
yarn.lock
277
yarn.lock
@@ -429,6 +429,11 @@
|
||||
resolved "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz"
|
||||
integrity sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg==
|
||||
|
||||
"@chainsafe/as-sha256@^0.4.1":
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@chainsafe/as-sha256/-/as-sha256-0.4.1.tgz#cfc0737e25f8c206767bdb6703e7943e5d44513e"
|
||||
integrity sha512-IqeeGwQihK6Y2EYLFofqs2eY2ep1I2MvQXHzOAI+5iQN51OZlUkrLgyAugu2x86xZewDk5xas7lNczkzFzF62w==
|
||||
|
||||
"@chainsafe/bls-hd-key@^0.2.0":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.npmjs.org/@chainsafe/bls-hd-key/-/bls-hd-key-0.2.1.tgz"
|
||||
@@ -601,13 +606,6 @@
|
||||
dependencies:
|
||||
"@chainsafe/is-ip" "^2.0.1"
|
||||
|
||||
"@chainsafe/persistent-merkle-tree@^0.4.2":
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz#4c9ee80cc57cd3be7208d98c40014ad38f36f7ff"
|
||||
integrity sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ==
|
||||
dependencies:
|
||||
"@chainsafe/as-sha256" "^0.3.1"
|
||||
|
||||
"@chainsafe/persistent-merkle-tree@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.5.0.tgz#2b4a62c9489a5739dedd197250d8d2f5427e9f63"
|
||||
@@ -615,6 +613,14 @@
|
||||
dependencies:
|
||||
"@chainsafe/as-sha256" "^0.3.1"
|
||||
|
||||
"@chainsafe/persistent-merkle-tree@^0.6.1":
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.6.1.tgz#37bde25cf6cbe1660ad84311aa73157dc86ec7f2"
|
||||
integrity sha512-gcENLemRR13+1MED2NeZBMA7FRS0xQPM7L2vhMqvKkjqtFT4YfjSVADq5U0iLuQLhFUJEMVuA8fbv5v+TN6O9A==
|
||||
dependencies:
|
||||
"@chainsafe/as-sha256" "^0.4.1"
|
||||
"@noble/hashes" "^1.3.0"
|
||||
|
||||
"@chainsafe/persistent-ts@^0.19.1":
|
||||
version "0.19.1"
|
||||
resolved "https://registry.npmjs.org/@chainsafe/persistent-ts/-/persistent-ts-0.19.1.tgz"
|
||||
@@ -632,15 +638,6 @@
|
||||
buffer-from "^1.1.1"
|
||||
snappy "^6.3.5"
|
||||
|
||||
"@chainsafe/ssz@0.9.4":
|
||||
version "0.9.4"
|
||||
resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.9.4.tgz#696a8db46d6975b600f8309ad3a12f7c0e310497"
|
||||
integrity sha512-77Qtg2N1ayqs4Bg/wvnWfg5Bta7iy7IRh8XqXh7oNMeP2HBbBwx8m6yTpA8p0EHItWPEBkgZd5S5/LSlp3GXuQ==
|
||||
dependencies:
|
||||
"@chainsafe/as-sha256" "^0.3.1"
|
||||
"@chainsafe/persistent-merkle-tree" "^0.4.2"
|
||||
case "^1.6.3"
|
||||
|
||||
"@chainsafe/ssz@^0.10.2":
|
||||
version "0.10.2"
|
||||
resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.10.2.tgz#c782929e1bb25fec66ba72e75934b31fd087579e"
|
||||
@@ -649,6 +646,14 @@
|
||||
"@chainsafe/as-sha256" "^0.3.1"
|
||||
"@chainsafe/persistent-merkle-tree" "^0.5.0"
|
||||
|
||||
"@chainsafe/ssz@^0.11.1":
|
||||
version "0.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.11.1.tgz#d4aec883af2ec5196ae67b96242c467da20b2476"
|
||||
integrity sha512-cB8dBkgGN6ZoeOKuk+rIRHKN0L5i9JLGeC0Lui71QX0TuLcQKwgbfkUexpyJxnGFatWf8yeJxlOjozMn/OTP0g==
|
||||
dependencies:
|
||||
"@chainsafe/as-sha256" "^0.4.1"
|
||||
"@chainsafe/persistent-merkle-tree" "^0.6.1"
|
||||
|
||||
"@chainsafe/threads@^1.10.0":
|
||||
version "1.10.0"
|
||||
resolved "https://registry.npmjs.org/@chainsafe/threads/-/threads-1.10.0.tgz"
|
||||
@@ -743,18 +748,36 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.37.0.tgz#cf1b5fa24217fe007f6487a26d765274925efa7d"
|
||||
integrity sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==
|
||||
|
||||
"@ethereumjs/block@^4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/block/-/block-4.2.1.tgz#cb0d8b770fe69c61e6e41d6693d285de76b3a7cb"
|
||||
integrity sha512-Z/Ty8EkD8o5tvEX5JPrr0pvf60JkSxmwV231aBZ744N75SLvq54dTu/Gk7azC/2xaWhSu1dOp5D5+bryqgG5Cg==
|
||||
"@ethereumjs/block@^4.2.2":
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/block/-/block-4.2.2.tgz#fddecd34ed559f84ab8eb13098a6dee51a1360ae"
|
||||
integrity sha512-kMxjeUwJSuLMwnavok5W17ayMNXXsu3hWsllK33XtZgoqt4ywvGo6ABh+xVEqwq/nn/iKuryCpDYYKEyXeFOlA==
|
||||
dependencies:
|
||||
"@ethereumjs/common" "^3.1.1"
|
||||
"@ethereumjs/common" "^3.1.2"
|
||||
"@ethereumjs/rlp" "^4.0.1"
|
||||
"@ethereumjs/trie" "^5.0.4"
|
||||
"@ethereumjs/tx" "^4.1.1"
|
||||
"@ethereumjs/util" "^8.0.5"
|
||||
ethereum-cryptography "^1.1.2"
|
||||
ethers "^5.7.1"
|
||||
"@ethereumjs/trie" "^5.0.5"
|
||||
"@ethereumjs/tx" "^4.1.2"
|
||||
"@ethereumjs/util" "^8.0.6"
|
||||
ethereum-cryptography "^2.0.0"
|
||||
|
||||
"@ethereumjs/blockchain@^6.2.2":
|
||||
version "6.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/blockchain/-/blockchain-6.2.2.tgz#68897a802839b217967083958022601a12afa0ed"
|
||||
integrity sha512-w1Zjskk35hr0qe0Zfwb88qrEFQJNMo73YrsqtJuBap+WamibEsw0rVuN4Ch+o8Dc66An+8rpk5SxEIK7PHF7KQ==
|
||||
dependencies:
|
||||
"@ethereumjs/block" "^4.2.2"
|
||||
"@ethereumjs/common" "^3.1.2"
|
||||
"@ethereumjs/ethash" "^2.0.5"
|
||||
"@ethereumjs/rlp" "^4.0.1"
|
||||
"@ethereumjs/trie" "^5.0.5"
|
||||
"@ethereumjs/tx" "^4.1.2"
|
||||
"@ethereumjs/util" "^8.0.6"
|
||||
abstract-level "^1.0.3"
|
||||
debug "^4.3.3"
|
||||
ethereum-cryptography "^2.0.0"
|
||||
level "^8.0.0"
|
||||
lru-cache "^5.1.1"
|
||||
memory-level "^1.0.0"
|
||||
|
||||
"@ethereumjs/common@2.5.0":
|
||||
version "2.5.0"
|
||||
@@ -772,28 +795,66 @@
|
||||
crc-32 "^1.2.0"
|
||||
ethereumjs-util "^7.1.5"
|
||||
|
||||
"@ethereumjs/common@^3.1.1":
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-3.1.1.tgz#6f754c8933727ad781f63ca3929caab542fe184e"
|
||||
integrity sha512-iEl4gQtcrj2udNhEizs04z7WA15ez1QoXL0XzaCyaNgwRyXezIg1DnfNeZUUpJnkrOF/0rYXyq2UFSLxt1NPQg==
|
||||
"@ethereumjs/common@^3.1.2":
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/common/-/common-3.1.2.tgz#c810301b78bcb7526bd690c6d7eb3f4a3c70839d"
|
||||
integrity sha512-YV+bZfRlFhAXg+FfwC5r4UQKVj4OG7vDP5/JvvNXLLbYpNplH5Vca9jD0L+ab8y0YlTYJMQM1ALyHFu3AE3eBA==
|
||||
dependencies:
|
||||
"@ethereumjs/util" "^8.0.5"
|
||||
"@ethereumjs/util" "^8.0.6"
|
||||
crc-32 "^1.2.0"
|
||||
|
||||
"@ethereumjs/ethash@^2.0.5":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/ethash/-/ethash-2.0.5.tgz#577b9d470eea6b61f77d624b58ac90929d6e857d"
|
||||
integrity sha512-JIPr39Zd9lULLftyzPGHUQmdziElqNWk0EkO1BAw3yns4TVx+BxCYZOkRQ55fuIFeKcXBupAI9V+7xdvIT2CPw==
|
||||
dependencies:
|
||||
"@ethereumjs/block" "^4.2.2"
|
||||
"@ethereumjs/rlp" "^4.0.1"
|
||||
"@ethereumjs/util" "^8.0.6"
|
||||
abstract-level "^1.0.3"
|
||||
bigint-crypto-utils "^3.2.2"
|
||||
ethereum-cryptography "^2.0.0"
|
||||
|
||||
"@ethereumjs/evm@^1.3.2":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/evm/-/evm-1.3.2.tgz#3123190b0d021122b183534d7b040a3b241905b8"
|
||||
integrity sha512-9PzshkvwO8YBkSD9+vyhJuzM6hxfZlljGnuUbXQlTSGEod7we8BRyzJW53W7nw/WRw5U6wf9Q2fpWypfZFkrbw==
|
||||
dependencies:
|
||||
"@ethereumjs/common" "^3.1.2"
|
||||
"@ethereumjs/tx" "^4.1.2"
|
||||
"@ethereumjs/util" "^8.0.6"
|
||||
"@ethersproject/providers" "^5.7.1"
|
||||
debug "^4.3.3"
|
||||
ethereum-cryptography "^2.0.0"
|
||||
mcl-wasm "^0.7.1"
|
||||
rustbn.js "~0.2.0"
|
||||
|
||||
"@ethereumjs/rlp@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41"
|
||||
integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==
|
||||
|
||||
"@ethereumjs/trie@^5.0.4":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/trie/-/trie-5.0.4.tgz#eb06ce2c7957f6a8f07c0db43fb0335b256f3705"
|
||||
integrity sha512-ycYtAF7BJAu9eaCtrEX+efE5xEQEfItRXXHBcTSMHsF7NfLHcniI0S7KUVYXbJ6imczBmnMHeggCqv8PYQbbOw==
|
||||
"@ethereumjs/statemanager@^1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/statemanager/-/statemanager-1.0.5.tgz#4496a315d27e60d9a3a036dbe82899f6f20dd2df"
|
||||
integrity sha512-TVkx9Kgc2NtObCzUTTqrpUggNLnftdmxZybzKPd565Bh98FJJB30FrVkWdPwaIV8oB1d9ADtthttfx5Y/kY9gw==
|
||||
dependencies:
|
||||
"@ethereumjs/common" "^3.1.2"
|
||||
"@ethereumjs/rlp" "^4.0.1"
|
||||
debug "^4.3.3"
|
||||
ethereum-cryptography "^2.0.0"
|
||||
ethers "^5.7.1"
|
||||
js-sdsl "^4.1.4"
|
||||
|
||||
"@ethereumjs/trie@^5.0.5":
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/trie/-/trie-5.0.5.tgz#c232a4913871ffc45bf52cccd214fe5aa24cb3e2"
|
||||
integrity sha512-H3gHtYxJVGfkT4H05LTJfD1W6h9WZYNkfhTUyAYruNZKFitkSHUM/bEFWH/GIhxt5SAkf283F5uJOx7X2Fr6pQ==
|
||||
dependencies:
|
||||
"@ethereumjs/rlp" "^4.0.1"
|
||||
"@ethereumjs/util" "^8.0.5"
|
||||
"@ethereumjs/util" "^8.0.6"
|
||||
"@types/readable-stream" "^2.3.13"
|
||||
ethereum-cryptography "^1.1.2"
|
||||
ethereum-cryptography "^2.0.0"
|
||||
readable-stream "^3.6.0"
|
||||
|
||||
"@ethereumjs/tx@3.3.2":
|
||||
@@ -804,26 +865,45 @@
|
||||
"@ethereumjs/common" "^2.5.0"
|
||||
ethereumjs-util "^7.1.2"
|
||||
|
||||
"@ethereumjs/tx@^4.1.1":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-4.1.1.tgz#d1b5bf2c4fd3618f2f333b66e262848530d4686a"
|
||||
integrity sha512-QDj7nuROfoeyK83RObMA0XCZ+LUDdneNkSCIekO498uEKTY25FxI4Whduc/6j0wdd4IqpQvkq+/7vxSULjGIBQ==
|
||||
"@ethereumjs/tx@^4.1.2":
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-4.1.2.tgz#10bc6741b74d2404331b82b87f9b2c26177b6f90"
|
||||
integrity sha512-PWWyO9lAFOiLwk7nB9OQisoJUsuvMz2PN2v4/ILbBpzamC5Ug79OddVq9r4rKvIDLPY+bn4NFerxBJg29+sjaA==
|
||||
dependencies:
|
||||
"@chainsafe/ssz" "0.9.4"
|
||||
"@ethereumjs/common" "^3.1.1"
|
||||
"@chainsafe/ssz" "^0.11.1"
|
||||
"@ethereumjs/common" "^3.1.2"
|
||||
"@ethereumjs/rlp" "^4.0.1"
|
||||
"@ethereumjs/util" "^8.0.5"
|
||||
"@ethersproject/providers" "^5.7.2"
|
||||
ethereum-cryptography "^1.1.2"
|
||||
"@ethereumjs/util" "^8.0.6"
|
||||
ethereum-cryptography "^2.0.0"
|
||||
|
||||
"@ethereumjs/util@^8.0.5":
|
||||
version "8.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.0.5.tgz#b9088fc687cc13f0c1243d6133d145dfcf3fe446"
|
||||
integrity sha512-259rXKK3b3D8HRVdRmlOEi6QFvwxdt304hhrEAmpZhsj7ufXEOTIc9JRZPMnXatKjECokdLNBcDOFBeBSzAIaw==
|
||||
"@ethereumjs/util@^8.0.6":
|
||||
version "8.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.0.6.tgz#f9716ed34235ea05eff8353bc5d483e5a6455989"
|
||||
integrity sha512-zFLG/gXtF3QUC7iKFn4PT6HCr+DEnlCbwUGKGtXoqjA+64T+e0FuqMjlo4bQIY2ngRzk3EtudKdGYC4g31ehhg==
|
||||
dependencies:
|
||||
"@chainsafe/ssz" "0.9.4"
|
||||
"@chainsafe/ssz" "^0.11.1"
|
||||
"@ethereumjs/rlp" "^4.0.1"
|
||||
ethereum-cryptography "^1.1.2"
|
||||
ethereum-cryptography "^2.0.0"
|
||||
micro-ftch "^0.3.1"
|
||||
|
||||
"@ethereumjs/vm@^6.4.2":
|
||||
version "6.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@ethereumjs/vm/-/vm-6.4.2.tgz#9898105a96f0975d561db69319331944db4bfafc"
|
||||
integrity sha512-kTzOvJfNpUQHi2a0SbglYNWHIEOg5j3NlN80KU0IrdagWAeaEqz6Jj5XVN5lBs4VAfwXNdf+56xYtMg8Nate7Q==
|
||||
dependencies:
|
||||
"@ethereumjs/block" "^4.2.2"
|
||||
"@ethereumjs/blockchain" "^6.2.2"
|
||||
"@ethereumjs/common" "^3.1.2"
|
||||
"@ethereumjs/evm" "^1.3.2"
|
||||
"@ethereumjs/rlp" "^4.0.1"
|
||||
"@ethereumjs/statemanager" "^1.0.5"
|
||||
"@ethereumjs/trie" "^5.0.5"
|
||||
"@ethereumjs/tx" "^4.1.2"
|
||||
"@ethereumjs/util" "^8.0.6"
|
||||
debug "^4.3.3"
|
||||
ethereum-cryptography "^2.0.0"
|
||||
mcl-wasm "^0.7.1"
|
||||
rustbn.js "~0.2.0"
|
||||
|
||||
"@ethersproject/abi@5.1.2", "@ethersproject/abi@^5.1.0":
|
||||
version "5.1.2"
|
||||
@@ -1231,7 +1311,7 @@
|
||||
bech32 "1.1.4"
|
||||
ws "7.2.3"
|
||||
|
||||
"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.2":
|
||||
"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.7.1":
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb"
|
||||
integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==
|
||||
@@ -2281,6 +2361,13 @@
|
||||
uint8arrays "^4.0.2"
|
||||
varint "^6.0.0"
|
||||
|
||||
"@noble/curves@1.0.0", "@noble/curves@~1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932"
|
||||
integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw==
|
||||
dependencies:
|
||||
"@noble/hashes" "1.3.0"
|
||||
|
||||
"@noble/ed25519@^1.6.0":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.6.0.tgz#b55f7c9e532b478bf1d7c4f609e1f3a37850b583"
|
||||
@@ -2296,16 +2383,16 @@
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12"
|
||||
integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==
|
||||
|
||||
"@noble/hashes@1.3.0", "@noble/hashes@^1.2.0", "@noble/hashes@^1.3.0", "@noble/hashes@~1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1"
|
||||
integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==
|
||||
|
||||
"@noble/hashes@^1.0.0", "@noble/hashes@~1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.0.0.tgz"
|
||||
integrity sha512-DZVbtY62kc3kkBtMHqwCOfXrT/hnoORy5BJ4+HU1IR59X0KWAOqsfzQPcUl/lQLlG7qXbe/fZ3r/emxtAl+sqg==
|
||||
|
||||
"@noble/hashes@^1.2.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1"
|
||||
integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==
|
||||
|
||||
"@noble/secp256k1@1.7.1", "@noble/secp256k1@~1.7.0":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c"
|
||||
@@ -3004,6 +3091,15 @@
|
||||
"@noble/secp256k1" "~1.7.0"
|
||||
"@scure/base" "~1.1.0"
|
||||
|
||||
"@scure/bip32@1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.0.tgz#6c8d980ef3f290987736acd0ee2e0f0d50068d87"
|
||||
integrity sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q==
|
||||
dependencies:
|
||||
"@noble/curves" "~1.0.0"
|
||||
"@noble/hashes" "~1.3.0"
|
||||
"@scure/base" "~1.1.0"
|
||||
|
||||
"@scure/bip39@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5"
|
||||
@@ -3012,6 +3108,14 @@
|
||||
"@noble/hashes" "~1.2.0"
|
||||
"@scure/base" "~1.1.0"
|
||||
|
||||
"@scure/bip39@1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.0.tgz#a207e2ef96de354de7d0002292ba1503538fc77b"
|
||||
integrity sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg==
|
||||
dependencies:
|
||||
"@noble/hashes" "~1.3.0"
|
||||
"@scure/base" "~1.1.0"
|
||||
|
||||
"@scure/bip39@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/@scure/bip39/-/bip39-1.0.0.tgz"
|
||||
@@ -4056,7 +4160,7 @@ abortcontroller-polyfill@^1.7.3:
|
||||
resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed"
|
||||
integrity sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==
|
||||
|
||||
abstract-level@^1.0.2, abstract-level@^1.0.3:
|
||||
abstract-level@^1.0.0, abstract-level@^1.0.2, abstract-level@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/abstract-level/-/abstract-level-1.0.3.tgz#78a67d3d84da55ee15201486ab44c09560070741"
|
||||
integrity sha512-t6jv+xHy+VYwc4xqZMn2Pa9DjcdzvzZmQGRjTFc8spIbRGHgBrEKbPq+rYXc7CCo0lxgYvSgKVg9qZAhpVQSjA==
|
||||
@@ -4710,6 +4814,11 @@ bigint-buffer@^1.1.5:
|
||||
dependencies:
|
||||
bindings "^1.3.0"
|
||||
|
||||
bigint-crypto-utils@^3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/bigint-crypto-utils/-/bigint-crypto-utils-3.2.2.tgz#e30a49ec38357c6981cd3da5aaa6480b1f752ee4"
|
||||
integrity sha512-U1RbE3aX9ayCUVcIPHuPDPKcK3SFOXf93J1UK/iHlJuQB7bhagPIX06/CLpLEsDThJ7KA4Dhrnzynl+d2weTiw==
|
||||
|
||||
bignumber.js@^9.0.0:
|
||||
version "9.1.1"
|
||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6"
|
||||
@@ -7166,7 +7275,7 @@ ethereum-cryptography@^0.1.3:
|
||||
secp256k1 "^4.0.1"
|
||||
setimmediate "^1.0.5"
|
||||
|
||||
ethereum-cryptography@^1.1.2, ethereum-cryptography@^1.2.0:
|
||||
ethereum-cryptography@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz#5ccfa183e85fdaf9f9b299a79430c044268c9b3a"
|
||||
integrity sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==
|
||||
@@ -7176,6 +7285,16 @@ ethereum-cryptography@^1.1.2, ethereum-cryptography@^1.2.0:
|
||||
"@scure/bip32" "1.1.5"
|
||||
"@scure/bip39" "1.1.1"
|
||||
|
||||
ethereum-cryptography@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.0.0.tgz#e052b49fa81affae29402e977b8d3a31f88612b6"
|
||||
integrity sha512-g25m4EtfQGjstWgVE1aIz7XYYjf3kH5kG17ULWVB5dH6uLahsoltOhACzSxyDV+fhn4gbR4xRrOXGe6r2uh4Bg==
|
||||
dependencies:
|
||||
"@noble/curves" "1.0.0"
|
||||
"@noble/hashes" "1.3.0"
|
||||
"@scure/bip32" "1.3.0"
|
||||
"@scure/bip39" "1.2.0"
|
||||
|
||||
ethereumjs-util@^7.1.0, ethereumjs-util@^7.1.1, ethereumjs-util@^7.1.2, ethereumjs-util@^7.1.5:
|
||||
version "7.1.5"
|
||||
resolved "https://registry.yarnpkg.com/ethereumjs-util/-/ethereumjs-util-7.1.5.tgz#9ecf04861e4fbbeed7465ece5f23317ad1129181"
|
||||
@@ -7962,6 +8081,11 @@ function.prototype.name@^1.1.5:
|
||||
es-abstract "^1.19.0"
|
||||
functions-have-names "^1.2.2"
|
||||
|
||||
functional-red-black-tree@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||
integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==
|
||||
|
||||
functions-have-names@^1.2.2:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
|
||||
@@ -10399,6 +10523,13 @@ lowercase-keys@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2"
|
||||
integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==
|
||||
|
||||
lru-cache@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
|
||||
integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
@@ -10545,6 +10676,11 @@ matcher@^3.0.0:
|
||||
dependencies:
|
||||
escape-string-regexp "^4.0.0"
|
||||
|
||||
mcl-wasm@^0.7.1:
|
||||
version "0.7.9"
|
||||
resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-0.7.9.tgz#c1588ce90042a8700c3b60e40efb339fc07ab87f"
|
||||
integrity sha512-iJIUcQWA88IJB/5L15GnJVnSQJmf/YaxxV6zRavv83HILHaJQb6y0iFyDMdDO0gN8X37tdxmAOrH/P8B6RB8sQ==
|
||||
|
||||
md5.js@^1.3.4:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz"
|
||||
@@ -10559,6 +10695,15 @@ media-typer@0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
|
||||
|
||||
memory-level@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/memory-level/-/memory-level-1.0.0.tgz#7323c3fd368f9af2f71c3cd76ba403a17ac41692"
|
||||
integrity sha512-UXzwewuWeHBz5krr7EvehKcmLFNoXxGcvuYhC41tRnkrTbJohtS7kVn9akmgirtRygg+f7Yjsfi8Uu5SGSQ4Og==
|
||||
dependencies:
|
||||
abstract-level "^1.0.0"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
module-error "^1.0.1"
|
||||
|
||||
memorystream@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz"
|
||||
@@ -10608,6 +10753,11 @@ methods@^1.1.2, methods@~1.1.2:
|
||||
resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
|
||||
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
|
||||
|
||||
micro-ftch@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/micro-ftch/-/micro-ftch-0.3.1.tgz#6cb83388de4c1f279a034fb0cf96dfc050853c5f"
|
||||
integrity sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==
|
||||
|
||||
micromatch@^4.0.0, micromatch@^4.0.4:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
|
||||
@@ -13382,6 +13532,11 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
rustbn.js@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/rustbn.js/-/rustbn.js-0.2.0.tgz#8082cb886e707155fd1cb6f23bd591ab8d55d0ca"
|
||||
integrity sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==
|
||||
|
||||
rxjs@^7.2.0:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.3.0.tgz"
|
||||
@@ -15917,7 +16072,7 @@ yaeti@^0.0.6:
|
||||
resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
|
||||
integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==
|
||||
|
||||
yallist@^3.0.0, yallist@^3.1.1:
|
||||
yallist@^3.0.0, yallist@^3.0.2, yallist@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
|
||||
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
||||
|
||||
Reference in New Issue
Block a user