UNPKG

@lodestar/prover

Version:

A Typescript implementation of the Ethereum Consensus light client

201 lines • 9.21 kB
import { BlockHeader } from "@ethereumjs/block"; import { Blockchain } from "@ethereumjs/blockchain"; import { TransactionFactory } from "@ethereumjs/tx"; import { Account, Address } from "@ethereumjs/util"; import { VM } from "@ethereumjs/vm"; import { ZERO_ADDRESS } from "../constants.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 { isNullish, isValidAccount, isValidCodeHash, isValidStorageKeys } from "./validation.js"; export async function createVM({ proofProvider }) { const common = getChainCommon(proofProvider.config.PRESET_BASE); const blockchain = await Blockchain.create({ common }); // Connect blockchain object with existing proof provider for block history // biome-ignore lint/suspicious/noExplicitAny: <explanation> blockchain.getBlock = async (blockId) => { 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, }) { 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", }); 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 = {}; 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", id: rpc.getRequestId(), method: "eth_getProof", params: [address, storageKeys, blockHashHex], }); batchRequests.push({ jsonrpc: "2.0", 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 = {}; for (const [proofResponse, codeResponse] of batchResponseInChunks) { const addressHex = proofResponse.request.params[0]; if (!isNullish(vmState[addressHex])) continue; const proof = proofResponse.response.result; const storageKeys = proofResponse.request.params[1]; const code = codeResponse.response.result; 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 }); } export async function updateVMWithState({ vm, state }) { 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, }) { 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, }) { 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), }, }); return result; } export function getVMBlockHeaderFromELBlock(block, executionPayload, network) { 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) }); } //# sourceMappingURL=evm.js.map