Skip to content

Commit

Permalink
feat: add support for eth_estimateGas (#5521)
Browse files Browse the repository at this point in the history
* Add eth_estimateGas support

* Fix lint error

* Fix the logger init

* Fix the browser logger
  • Loading branch information
nazarhussain authored May 26, 2023
1 parent cacb99b commit 0207d66
Show file tree
Hide file tree
Showing 20 changed files with 6,458 additions and 90 deletions.
4 changes: 2 additions & 2 deletions packages/logger/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ class BrowserConsole extends Transport {
}

log(method: string | number, message: unknown): void {
setImmediate(() => {
setTimeout(() => {
this.emit("logged", method);
});
}, 0);

const val = this.levels[method as LogLevel];
const mappedMethod = this.methods[method as LogLevel];
Expand Down
2 changes: 2 additions & 0 deletions packages/logger/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ export function getEnvLogLevel(): LogLevel | null {

export function getEnvLogger(opts?: Partial<BrowserLoggerOpts>): Logger {
const level = opts?.level ?? getEnvLogLevel();

if (level != null) {
return getBrowserLogger({...opts, level});
}

return getEmptyLogger();
}
2 changes: 1 addition & 1 deletion packages/prover/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ lodestar-prover start \
| | eth_accounts | ➡️ |
| | eth_blockNumber | ➡️ |
| Call and Estimate | eth_call || v0 |
| | eth_estimateGas | | v0 |
| | eth_estimateGas | | v0 |
| | eth_createAccessList || v2 |
| | eth_gasPrice || v1 |
| | eth_maxPriorityFeePerGas || v1 |
Expand Down
2 changes: 2 additions & 0 deletions packages/prover/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@
"generate-fixtures": "npx ts-node --esm scripts/generate_fixtures.ts"
},
"dependencies": {
"@ethereumjs/tx": "^4.1.2",
"@ethereumjs/block": "^4.2.2",
"@ethereumjs/common": "^3.1.2",
"@ethereumjs/rlp": "^4.0.1",
"@ethereumjs/trie": "^5.0.5",
"@ethereumjs/util": "^8.0.6",
"@ethereumjs/evm": "^1.3.2",
"@ethereumjs/vm": "^6.4.2",
"@ethereumjs/blockchain": "^6.2.2",
"@lodestar/api": "^1.8.0",
Expand Down
99 changes: 99 additions & 0 deletions packages/prover/scripts/generate_fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,102 @@ await generateFixture(
}),
"mainnet"
);

await generateFixture(
"eth_estimateGas_simple_transfer",
({latest}) => ({
request: {
method: "eth_estimateGas",
params: [
{
from: "0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990",
to: "0x388c818ca8b9251b393131c08a736a67ccb19297",
value: "0xFF00900",
},
latest,
],
},
slot: "head",
dependentRequests: [
{
method: "eth_createAccessList",
params: [
{
to: "0x388c818ca8b9251b393131c08a736a67ccb19297",
from: "0x690b9a9e9aa1c9db991c7721a92d351db4fac990",
value: "0xff00900",
gas: "0x1c9c380",
gasPrice: "0x0",
},
latest,
],
},
{
method: "eth_getProof",
params: ["0x690b9a9e9aa1c9db991c7721a92d351db4fac990", [], latest],
},
{
method: "eth_getCode",
params: ["0x690b9a9e9aa1c9db991c7721a92d351db4fac990", latest],
},
{
method: "eth_getProof",
params: ["0x388c818ca8b9251b393131c08a736a67ccb19297", [], latest],
},
{
method: "eth_getCode",
params: ["0x388c818ca8b9251b393131c08a736a67ccb19297", latest],
},
],
}),
"mainnet"
);

await generateFixture(
"eth_estimateGas_contract_call",
({latest}) => ({
request: {
method: "eth_estimateGas",
params: [
{
data: "0xd05c78da000000000000000000000000000000000000000000000000000000025408a08b000000000000000000000000000000000000000000000000000000cef5d5bf7f",
to: "0xade2a9c8b033d60ffcdb8cfc974dd87b2a9c1f27",
},
latest,
],
},
slot: "head",
dependentRequests: [
{
method: "eth_createAccessList",
params: [
{
to: "0xade2a9c8b033d60ffcdb8cfc974dd87b2a9c1f27",
from: "0x0000000000000000000000000000000000000000",
data: "0xd05c78da000000000000000000000000000000000000000000000000000000025408a08b000000000000000000000000000000000000000000000000000000cef5d5bf7f",
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"
);
3 changes: 2 additions & 1 deletion packages/prover/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ export type ELStorageProof = Pick<ELProof, "storageHash" | "storageProof">;
/* eslint-disable @typescript-eslint/naming-convention */
export type ELApi = {
eth_createAccessList: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => ELAccessListResponse;
call: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => HexString;
eth_call: (transaction: ELTransaction, block?: ELBlockNumberOrTag) => HexString;
eth_estimateGas: (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;
Expand Down
143 changes: 104 additions & 39 deletions packages/prover/src/utils/evm.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import {Blockchain} from "@ethereumjs/blockchain";
import {Account, Address} from "@ethereumjs/util";
import {VM} from "@ethereumjs/vm";
import {VM, RunTxResult} from "@ethereumjs/vm";
import {TransactionFactory} from "@ethereumjs/tx";
import {Block, BlockHeader} from "@ethereumjs/block";
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 {ELApiHandlers, ELBlock, ELProof, ELTransaction} from "../types.js";
import {bufferToHex, cleanObject, hexToBigInt, hexToBuffer, numberToHex, padLeft} from "./conversion.js";
import {elRpc, getChainCommon} from "./execution.js";
import {elRpc, getChainCommon, getTxType} from "./execution.js";
import {isValidResponse} from "./json_rpc.js";
import {isValidAccount, isValidCodeHash, isValidStorageKeys} from "./validation.js";
import {isNullish, isValidAccount, isValidCodeHash, isValidStorageKeys} from "./validation.js";

export async function createEVM({
export async function createVM({
proofProvider,
network,
}: {
Expand All @@ -35,15 +37,15 @@ export async function createEVM({
return vm;
}

export async function getEVMWithState({
export async function getVMWithState({
handler,
executionPayload,
tx,
evm,
vm,
logger,
}: {
handler: ELApiHandlers["eth_getProof"] | ELApiHandlers["eth_getCode"] | ELApiHandlers["eth_createAccessList"];
evm: VM;
vm: VM;
executionPayload: allForks.ExecutionPayload;
tx: ELTransaction;
logger: Logger;
Expand All @@ -66,7 +68,7 @@ export async function getEVMWithState({
]);

if (!isValidResponse(response) || response.result.error) {
throw new Error("Invalid response from RPC");
throw new Error(`Invalid response from RPC. method: eth_createAccessList, params: ${JSON.stringify(tx)}`);
}

const storageKeysMap: Record<string, string[]> = {};
Expand Down Expand Up @@ -111,7 +113,7 @@ export async function getEVMWithState({
proofsAndCodes[address] = {proof, code: codeResponse};
}

await evm.stateManager.checkpoint();
await vm.stateManager.checkpoint();
for (const [addressHex, {proof, code}] of Object.entries(proofsAndCodes)) {
const address = Address.fromString(addressHex);
const codeBuffer = hexToBuffer(code);
Expand All @@ -122,34 +124,32 @@ export async function getEVMWithState({
codeHash: proof.codeHash,
});

await evm.stateManager.putAccount(address, account);
await vm.stateManager.putAccount(address, account);

for (const {key, value} of proof.storageProof) {
await evm.stateManager.putContractStorage(
address,
padLeft(hexToBuffer(key), 32),
padLeft(hexToBuffer(value), 32)
);
await vm.stateManager.putContractStorage(address, padLeft(hexToBuffer(key), 32), padLeft(hexToBuffer(value), 32));
}

if (codeBuffer.byteLength !== 0) await evm.stateManager.putContractCode(address, codeBuffer);
if (codeBuffer.byteLength !== 0) await vm.stateManager.putContractCode(address, codeBuffer);
}

await evm.stateManager.commit();
return evm;
await vm.stateManager.commit();
return vm;
}

export async function executeEVM({
export async function executeVMCall({
handler,
tx,
evm,
vm,
executionPayload,
network,
}: {
handler: ELApiHandlers["eth_getBlockByHash"];
tx: ELTransaction;
evm: VM;
vm: VM;
executionPayload: allForks.ExecutionPayload;
}): Promise<HexString> {
network: NetworkName;
}): Promise<RunTxResult["execResult"]> {
const {from, to, gas, gasPrice, maxPriorityFeePerGas, value, data} = tx;
const {result: block} = await elRpc(
handler,
Expand All @@ -162,47 +162,112 @@ export async function executeEVM({
throw new Error(`Block not found: ${bufferToHex(executionPayload.blockHash)}`);
}

const {execResult} = await evm.evm.runCall({
const {execResult} = await vm.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),
header: getVMBlockHeaderFromELBlock(block, executionPayload, network),
},
});

if (execResult.exceptionError) {
throw new Error(execResult.exceptionError.error);
}

return bufferToHex(execResult.returnValue);
return execResult;
}

export function evmBlockHeaderFromELBlock(
export async function executeVMTx({
handler,
tx,
vm,
executionPayload,
network,
}: {
handler: ELApiHandlers["eth_getBlockByHash"];
tx: ELTransaction;
vm: VM;
executionPayload: allForks.ExecutionPayload;
network: NetworkName;
}): Promise<RunTxResult> {
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 txType = getTxType(tx);
const from = tx.from ? Address.fromString(tx.from) : Address.zero();
const to = tx.to ? Address.fromString(tx.to) : undefined;

const txData = {
...tx,
from,
to,
type: txType,
// If no gas limit is specified use the last block gas limit as an upper bound.
gasLimit: hexToBigInt(tx.gas ?? block.gasLimit),
};

if (txType === 2) {
// Handle EIP-1559 transactions
// To fix the vm error: Transaction's maxFeePerGas (0) is less than the block's baseFeePerGas
txData.maxFeePerGas = txData.maxFeePerGas ?? block.baseFeePerGas;
} else {
// Legacy transaction
txData.gasPrice = isNullish(txData.gasPrice) || txData.gasPrice === "0x0" ? block.baseFeePerGas : txData.gasPrice;
}

const txObject = TransactionFactory.fromTxData(txData, {common: getChainCommon(network), freeze: false});

// Override to avoid tx signature verification
txObject.getSenderAddress = () => (tx.from ? Address.fromString(tx.from) : Address.zero());

const result = await vm.runTx({
tx: txObject,
skipNonce: true,
skipBalance: true,
skipBlockGasLimitValidation: true,
skipHardForkValidation: true,
block: {
header: getVMBlockHeaderFromELBlock(block, executionPayload, network),
} as Block,
});

return result;
}

export function getVMBlockHeaderFromELBlock(
block: ELBlock,
executionPayload: allForks.ExecutionPayload
): {
number: bigint;
cliqueSigner(): Address;
coinbase: Address;
timestamp: bigint;
difficulty: bigint;
prevRandao: Buffer;
gasLimit: bigint;
baseFeePerGas?: bigint;
} {
return {
executionPayload: allForks.ExecutionPayload,
network: NetworkName
): BlockHeader {
const blockHeaderData = {
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,

// Use these values from the execution payload
// instead of the block values to ensure that
// the VM is using the verified values from the lightclient
prevRandao: Buffer.from(executionPayload.prevRandao),
stateRoot: Buffer.from(executionPayload.stateRoot),
parentHash: Buffer.from(executionPayload.parentHash),

// TODO: Fix the coinbase address
coinbase: Address.fromString(block.miner),
};

return BlockHeader.fromHeaderData(blockHeaderData, {common: getChainCommon(network)});
}
Loading

0 comments on commit 0207d66

Please sign in to comment.