@lodestar/prover
Version:
A Typescript implementation of the Ethereum Consensus light client
285 lines (244 loc) • 9.77 kB
text/typescript
import {Block, BlockHeader} from "@ethereumjs/block";
import {Blockchain} from "@ethereumjs/blockchain";
import {TransactionFactory} from "@ethereumjs/tx";
import {Account, Address} from "@ethereumjs/util";
import {RunTxResult, VM} from "@ethereumjs/vm";
import {NetworkName} from "@lodestar/config/networks";
import {ExecutionPayload} from "@lodestar/types";
import {Logger} from "@lodestar/utils";
import {ZERO_ADDRESS} from "../constants.js";
import {ProofProvider} from "../proof_provider/proof_provider.js";
import {ELBlock, ELProof, ELTransaction, JsonRpcVersion} from "../types.js";
import {bufferToHex, chunkIntoN, cleanObject, hexToBigInt, hexToBuffer, numberToHex, padLeft} from "./conversion.js";
import {getChainCommon, getTxType} from "./execution.js";
import {isValidResponse} from "./json_rpc.js";
import {ELRpcProvider} from "./rpc_provider.js";
import {isNullish, isValidAccount, isValidCodeHash, isValidStorageKeys} from "./validation.js";
export async function createVM({proofProvider}: {proofProvider: ProofProvider}): Promise<VM> {
const common = getChainCommon(proofProvider.config.PRESET_BASE as string);
const blockchain = await Blockchain.create({common});
// Connect blockchain object with existing proof provider for block history
// biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here
(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 getVMWithState({
rpc,
executionPayload,
tx,
vm,
logger,
}: {
rpc: ELRpcProvider;
vm: VM;
executionPayload: ExecutionPayload;
tx: ELTransaction;
logger: Logger;
}): Promise<VM> {
const {stateRoot, blockHash, gasLimit} = executionPayload;
const blockHashHex = bufferToHex(blockHash);
// If tx does not have a from address then it must be initiated via zero address
const from = tx.from ?? ZERO_ADDRESS;
const to = tx.to;
// Create Access List for the contract call
const accessListTx = cleanObject({
to,
from,
data: tx.input ? tx.input : tx.data,
value: tx.value,
gas: tx.gas ? tx.gas : numberToHex(gasLimit),
gasPrice: "0x0",
}) as ELTransaction;
const response = await rpc.request("eth_createAccessList", [accessListTx, blockHashHex], {raiseError: false});
if (!isValidResponse(response) || response.result.error) {
throw new Error(`Invalid response from RPC. method: eth_createAccessList, params: ${JSON.stringify(tx)}`);
}
const storageKeysMap: Record<string, string[]> = {};
for (const {address, storageKeys} of response.result.accessList) {
storageKeysMap[address] = storageKeys;
}
// If from address is not present then we have to fetch it for all keys
if (isNullish(storageKeysMap[from])) {
storageKeysMap[from] = [];
}
// If to address is not present then we have to fetch it with for all keys
if (to && isNullish(storageKeysMap[to])) {
storageKeysMap[to] = [];
}
const batchRequests = [];
for (const [address, storageKeys] of Object.entries(storageKeysMap)) {
batchRequests.push({
jsonrpc: "2.0" as JsonRpcVersion,
id: rpc.getRequestId(),
method: "eth_getProof",
params: [address, storageKeys, blockHashHex],
});
batchRequests.push({
jsonrpc: "2.0" as JsonRpcVersion,
id: rpc.getRequestId(),
method: "eth_getCode",
params: [address, blockHashHex],
});
}
// If all responses are valid then we will have even number of responses
// For each address, one response for eth_getProof and one for eth_getCode
const batchResponse = await rpc.batchRequest(batchRequests, {raiseError: true});
const batchResponseInChunks = chunkIntoN(batchResponse, 2);
const vmState: VMState = {};
for (const [proofResponse, codeResponse] of batchResponseInChunks) {
const addressHex = proofResponse.request.params[0] as string;
if (!isNullish(vmState[addressHex])) continue;
const proof = proofResponse.response.result as ELProof;
const storageKeys = proofResponse.request.params[1] as string[];
const code = codeResponse.response.result as string;
const validAccount = await isValidAccount({address: addressHex, proof, logger, stateRoot});
const validStorage = validAccount && (await isValidStorageKeys({storageKeys, proof, logger}));
if (!validAccount || !validStorage) {
throw new Error(`Invalid account: ${addressHex}`);
}
if (!(await isValidCodeHash({codeResponse: code, logger, codeHash: proof.codeHash}))) {
throw new Error(`Invalid code hash: ${addressHex}`);
}
vmState[addressHex] = {code, proof};
}
return updateVMWithState({vm, state: vmState, logger});
}
type VMState = Record<string, {code: string; proof: ELProof}>;
export async function updateVMWithState({vm, state}: {logger: Logger; state: VMState; vm: VM}): Promise<VM> {
await vm.stateManager.checkpoint();
for (const [addressHex, {proof, code}] of Object.entries(state)) {
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 vm.stateManager.putAccount(address, account);
for (const {key, value} of proof.storageProof) {
await vm.stateManager.putContractStorage(address, padLeft(hexToBuffer(key), 32), padLeft(hexToBuffer(value), 32));
}
if (codeBuffer.byteLength !== 0) await vm.stateManager.putContractCode(address, codeBuffer);
}
await vm.stateManager.commit();
return vm;
}
export async function executeVMCall({
rpc,
tx,
vm,
executionPayload,
network,
}: {
rpc: ELRpcProvider;
tx: ELTransaction;
vm: VM;
executionPayload: ExecutionPayload;
network: NetworkName;
}): Promise<RunTxResult["execResult"]> {
const {from, to, gas, gasPrice, maxPriorityFeePerGas, value, data, input} = tx;
const blockHash = bufferToHex(executionPayload.blockHash);
const {result: block} = await rpc.request("eth_getBlockByHash", [blockHash, true], {
raiseError: true,
});
if (!block) {
throw new Error(`Block not found: ${blockHash}`);
}
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: input ? hexToBuffer(input) : data ? hexToBuffer(data) : undefined,
block: {
header: getVMBlockHeaderFromELBlock(block, executionPayload, network),
},
});
if (execResult.exceptionError) {
throw new Error(execResult.exceptionError.error);
}
return execResult;
}
export async function executeVMTx({
rpc,
tx,
vm,
executionPayload,
network,
}: {
rpc: ELRpcProvider;
tx: ELTransaction;
vm: VM;
executionPayload: ExecutionPayload;
network: NetworkName;
}): Promise<RunTxResult> {
const {result: block} = await rpc.request("eth_getBlockByHash", [bufferToHex(executionPayload.blockHash), true], {
raiseError: 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: 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)});
}