UNPKG

@ethereumjs/evm

Version:
1,375 lines (1,263 loc) 59.4 kB
import { Hardfork } from '@ethereumjs/common' import type { BlockLevelAccessList, PrefixedHexString } from '@ethereumjs/util' import { Account, Address, BIGINT_0, BIGINT_1, EthereumJSErrorWithoutCode, KECCAK256_NULL, KECCAK256_RLP, MAX_INTEGER, bigIntToBytes, bytesToUnprefixedHex, createBlockLevelAccessList, createZeroAddress, equalsBytes, generateAddress, generateAddress2, isDebugEnabled, short, } from '@ethereumjs/util' import debugDefault from 'debug' import { EventEmitter } from 'eventemitter3' import { createEIP7708TransferLog } from './eip7708.ts' import { activeCostPerStateByte } from './eip8037.ts' import { FORMAT } from './eof/constants.ts' import { isEOF } from './eof/util.ts' import { EVMError } from './errors.ts' import { Interpreter } from './interpreter.ts' import { Journal } from './journal.ts' import { EVMPerformanceLogger } from './logger.ts' import { Message } from './message.ts' import { getOpcodesForHF } from './opcodes/index.ts' import { paramsEVM } from './params.ts' import { NobleBLS, getActivePrecompiles, getPrecompileName } from './precompiles/index.ts' import { TransientStorage } from './transientStorage.ts' import { type Block, type CustomOpcode, DELEGATION_7702_FLAG, type EVMBLSInterface, type EVMBN254Interface, type EVMEvent, type EVMInterface, type EVMMockBlockchainInterface, type EVMOpts, type EVMResult, type EVMRunCallOpts, type EVMRunCodeOpts, type ExecResult, type Log, } from './types.ts' import type { Common, StateManagerInterface } from '@ethereumjs/common' import type { BinaryTreeAccessWitness } from './binaryTreeAccessWitness.ts' import type { InterpreterOpts } from './interpreter.ts' import type { Timer } from './logger.ts' import type { MessageWithTo } from './message.ts' import type { AsyncDynamicGasHandler, SyncDynamicGasHandler } from './opcodes/gas.ts' import type { OpHandler, OpcodeList, OpcodeMap } from './opcodes/index.ts' import type { CustomPrecompile, PrecompileFunc } from './precompiles/index.ts' const debug = debugDefault('evm:evm') const debugGas = debugDefault('evm:gas') const debugPrecompiles = debugDefault('evm:precompiles') /** * Creates a standardized ExecResult for out-of-gas errors. * @param gasLimit - Gas limit consumed by the failing frame * @returns Execution result describing the OOG failure */ export function OOGResult(gasLimit: bigint): ExecResult { return { returnValue: new Uint8Array(0), executionGasUsed: gasLimit, exceptionError: new EVMError(EVMError.errorMessages.OUT_OF_GAS), } } /** * Creates an ExecResult for code-deposit out-of-gas errors (EIP-3541). * @param gasUsedCreateCode - Gas consumed while attempting to store code */ export function COOGResult(gasUsedCreateCode: bigint): ExecResult { return { returnValue: new Uint8Array(0), executionGasUsed: gasUsedCreateCode, exceptionError: new EVMError(EVMError.errorMessages.CODESTORE_OUT_OF_GAS), } } /** * Returns an ExecResult signalling invalid bytecode input. * @param gasLimit - Gas consumed up to the point of failure */ export function INVALID_BYTECODE_RESULT(gasLimit: bigint): ExecResult { return { returnValue: new Uint8Array(0), executionGasUsed: gasLimit, exceptionError: new EVMError(EVMError.errorMessages.INVALID_BYTECODE_RESULT), } } /** * Returns an ExecResult signalling invalid EOF formatting. * @param gasLimit - Gas consumed up to the point of failure */ export function INVALID_EOF_RESULT(gasLimit: bigint): ExecResult { return { returnValue: new Uint8Array(0), executionGasUsed: gasLimit, exceptionError: new EVMError(EVMError.errorMessages.INVALID_EOF_FORMAT), } } /** * Returns an ExecResult for code size violations. * @param gasUsed - Gas consumed before the violation was detected */ export function CodesizeExceedsMaximumError(gasUsed: bigint): ExecResult { return { returnValue: new Uint8Array(0), executionGasUsed: gasUsed, exceptionError: new EVMError(EVMError.errorMessages.CODESIZE_EXCEEDS_MAXIMUM), } } /** * Wraps an {@link EVMError} in an ExecResult. * @param error - Error encountered during execution * @param gasUsed - Gas consumed up to the error */ export function EVMErrorResult(error: EVMError, gasUsed: bigint): ExecResult { return { returnValue: new Uint8Array(0), executionGasUsed: gasUsed, exceptionError: error, } } /** * Creates a default block header used by stand-alone executions. * @returns Block-like object with zeroed header fields */ export function defaultBlock(): Block { return { header: { number: BIGINT_0, coinbase: createZeroAddress(), timestamp: BIGINT_0, difficulty: BIGINT_0, prevRandao: new Uint8Array(32), gasLimit: BIGINT_0, baseFeePerGas: undefined, getBlobGasPrice: () => undefined, }, } } /** * The EVM (Ethereum Virtual Machine) is responsible for executing EVM bytecode, processing transactions, and managing state changes. It handles both contract calls and contract creation operations. * * An EVM instance can be created with the constructor method: * * - {@link createEVM} */ export class EVM implements EVMInterface { protected static supportedHardforks = [ Hardfork.Chainstart, Hardfork.Homestead, Hardfork.Dao, Hardfork.TangerineWhistle, Hardfork.SpuriousDragon, Hardfork.Byzantium, Hardfork.Constantinople, Hardfork.Petersburg, Hardfork.Istanbul, Hardfork.MuirGlacier, Hardfork.Berlin, Hardfork.London, Hardfork.ArrowGlacier, Hardfork.GrayGlacier, Hardfork.MergeNetsplitBlock, Hardfork.Paris, Hardfork.Shanghai, Hardfork.Cancun, Hardfork.Prague, Hardfork.Osaka, Hardfork.Bpo1, Hardfork.Bpo2, Hardfork.Bpo3, Hardfork.Bpo4, Hardfork.Bpo5, Hardfork.Amsterdam, ] protected _tx?: { gasPrice: bigint origin: Address } protected _block?: Block public readonly common: Common public readonly events: EventEmitter<EVMEvent> public stateManager: StateManagerInterface public blockchain: EVMMockBlockchainInterface public journal: Journal public binaryAccessWitness?: BinaryTreeAccessWitness public systemBinaryAccessWitness?: BinaryTreeAccessWitness public readonly transientStorage: TransientStorage protected _opcodes!: OpcodeList public readonly allowUnlimitedContractSize: boolean public readonly allowUnlimitedInitCodeSize: boolean /** * Accumulated block access list when EIP-7928 is active. * * @remarks Experimental (Amsterdam): may change on patch releases. */ public readonly blockLevelAccessList?: BlockLevelAccessList /** * EIP-8037 transaction-level state-gas reservoir. * Holds gas paid by the user that exceeds the EIP-7825 regular-gas budget * and is reserved exclusively for state-creation charges. State-gas charges * draw from `stateGasReservoir` first; once exhausted, they fall through to * the regular `gasLeft`. Refunds (revert / exceptional halt / SELFDESTRUCT * of same-tx-created accounts) refill it. * Initialized by `runTx` at the start of each transaction; `0` when EIP-8037 is inactive. * * @remarks Experimental (Amsterdam): may change on patch releases. */ public stateGasReservoir: bigint = BIGINT_0 /** * EIP-8037 cumulative state-gas used by the current transaction. * * @remarks Experimental (Amsterdam): may change on patch releases. */ public executionStateGasUsed: bigint = BIGINT_0 /** * EIP-7928 CALL post-state OOG: set while handling post-target access failure so * `runTx` drains the state-gas reservoir and the sender pays the full tx gas limit. * * @remarks Experimental (Amsterdam): may change on patch releases. */ public eip7928CallPostTargetOog = false /** * EIP-8037 per-frame state-gas snapshots. Pushed on each message-level * journal.checkpoint() and popped on commit (drop) or revert / exceptional * halt (restore). Restoring on revert refunds all state-gas charges and * undoes all reservoir refills made within the reverted frame, per spec: * "all state-gas charged on the child frame is refunded to the parent * frame's state_gas_reservoir [...] execution_state_gas_used is * decreased consistently". */ protected _stateGasSnapshots: Array<{ reservoir: bigint used: bigint createdAccountStateGas: Map<PrefixedHexString, bigint> createdAccountIntrinsicStateGas: Map<PrefixedHexString, bigint> }> = [] /** * EIP-8037 SELFDESTRUCT deferred refund support. * Per-address record of the state-gas charged for account creation * (stateBytesPerNewAccount × costPerStateByte) plus code deposit * (L × costPerStateByte) at successful CREATE/CREATE2 frame exit. * Reset at the start of each tx and consulted by runTx to refund * state-gas for accounts that were both created and SELFDESTRUCTed * in the same tx (per EIP-6780 + EIP-8037). * Storage-slot state-gas is not tracked here yet; that is a separate * follow-up. */ public createdAccountStateGas: Map<PrefixedHexString, bigint> = new Map() /** * EIP-8037 (v7): intrinsic state-gas tracking for depth=0 creation * transactions. The intrinsic stateBytesPerNewAccount * costPerStateByte * is paid up-front in runTx and isn't part of stateGasCreate. On a same-tx * SELFDESTRUCT of the freshly-created contract, runTx refunds the STATE * dimension only (decrement execution_state_gas_used) — the reservoir is * NOT credited, so the user still pays the gross intrinsic. This realizes * the v7 spec note "tx doesn't over charge for an account that never * persists" at the block_state_gas_used level. */ public createdAccountIntrinsicStateGas: Map<PrefixedHexString, bigint> = new Map() protected readonly _customOpcodes?: CustomOpcode[] protected readonly _customPrecompiles?: CustomPrecompile[] protected _handlers!: Map<number, OpHandler> protected _dynamicGasHandlers!: Map<number, AsyncDynamicGasHandler | SyncDynamicGasHandler> protected _opcodeMap!: OpcodeMap protected _precompiles!: Map<string, PrecompileFunc> protected readonly _optsCached: EVMOpts protected performanceLogger: EVMPerformanceLogger public get precompiles() { return this._precompiles } public get opcodes() { return this._opcodes } protected readonly _bls?: EVMBLSInterface /** * EVM is run in DEBUG mode (default: false) * Taken from DEBUG environment variable * * Safeguards on debug() calls are added for * performance reasons to avoid string literal evaluation * @hidden */ readonly DEBUG: boolean = false protected readonly _emit: (topic: string, data: any) => Promise<void> private _bn254: EVMBN254Interface /** * * Creates new EVM object * * @deprecated The direct usage of this constructor is replaced since * non-finalized async initialization lead to side effects. Please * use the async {@link createEVM} constructor instead (same API). * * @param opts The EVM options * @param bn128 Initialized bn128 WASM object for precompile usage (internal) */ constructor(opts: EVMOpts) { this.common = opts.common! this.blockchain = opts.blockchain! this.stateManager = opts.stateManager! if (this.common.isActivatedEIP(7864)) { const mandatory = ['checkChunkWitnessPresent'] for (const m of mandatory) { if (!(m in this.stateManager)) { throw EthereumJSErrorWithoutCode( `State manager used must implement ${m} if Binary Trees (EIP-7864) is activated`, ) } } } if (this.common.isActivatedEIP(7928)) { this.blockLevelAccessList = opts.blockLevelAccessList ?? createBlockLevelAccessList() } this.events = new EventEmitter<EVMEvent>() this._optsCached = opts // Supported EIPs (sorted by EIP number; keep in sync with README / types.ts) const supportedEIPs = [ 1153, 1559, 2537, 2565, 2718, 2929, 2930, 2935, 3198, 3529, 3540, 3541, 3554, 3607, 3651, 3670, 3675, 3855, 3860, 4200, 4345, 4399, 4750, 4788, 4844, 4895, 5133, 5450, 5656, 6110, 6206, 6780, 7002, 7069, 7251, 7480, 7516, 7594, 7620, 7623, 7685, 7691, 7692, 7698, 7702, 7708, 7709, 7778, 7823, 7825, 7843, 7864, 7883, 7918, 7928, 7934, 7939, 7951, 7954, 7976, 7981, 8024, 8037, ] for (const eip of this.common.eips()) { if (!supportedEIPs.includes(eip)) { throw EthereumJSErrorWithoutCode(`EIP-${eip} is not supported by the EVM`) } } if (!EVM.supportedHardforks.includes(this.common.hardfork() as Hardfork)) { throw EthereumJSErrorWithoutCode( `Hardfork ${this.common.hardfork()} not set as supported in supportedHardforks`, ) } this.common.updateParams(opts.params ?? paramsEVM) this.allowUnlimitedContractSize = opts.allowUnlimitedContractSize ?? false this.allowUnlimitedInitCodeSize = opts.allowUnlimitedInitCodeSize ?? false this._customOpcodes = opts.customOpcodes this._customPrecompiles = opts.customPrecompiles this.journal = new Journal(this.stateManager, this.common) this.transientStorage = new TransientStorage() this.common.events.on('hardforkChanged', () => { this.getActiveOpcodes() this._precompiles = getActivePrecompiles(this.common, this._customPrecompiles) }) // Initialize the opcode data this.getActiveOpcodes() this._precompiles = getActivePrecompiles(this.common, this._customPrecompiles) // Precompile crypto libraries if (this.common.isActivatedEIP(2537)) { this._bls = opts.bls ?? new NobleBLS() this._bls.init?.() } this._bn254 = opts.bn254! this._emit = async (topic: string, data: any): Promise<void> => { const listeners = this.events.listeners(topic as keyof EVMEvent) for (const listener of listeners) { if (listener.length === 2) { await new Promise<void>((resolve) => { listener(data, resolve) }) } else { listener(data) } } } this.performanceLogger = new EVMPerformanceLogger() // Skip DEBUG calls unless 'ethjs' included in environmental DEBUG variables this.DEBUG = isDebugEnabled('ethjs') } /** * Returns a list with the currently activated opcodes * available for EVM execution */ getActiveOpcodes(): OpcodeList { const data = getOpcodesForHF(this.common, this._customOpcodes) this._opcodes = data.opcodes this._dynamicGasHandlers = data.dynamicGasHandlers this._handlers = data.handlers this._opcodeMap = data.opcodeMap return data.opcodes } protected async _executeCall(message: MessageWithTo): Promise<EVMResult> { let gasLimit = message.gasLimit const fromAddress = message.caller if (this.common.isActivatedEIP(7864)) { if (message.accessWitness === undefined) { throw EthereumJSErrorWithoutCode('accessWitness is required for EIP-7864') } const sendsValue = message.value !== BIGINT_0 if (message.depth === 0) { const originAccessGas = message.accessWitness.readAccountHeader(fromAddress) debugGas(`originAccessGas=${originAccessGas} waived off for origin at depth=0`) let destAccessGas = message.accessWitness.readAccountCodeHash(message.to) if (sendsValue) { destAccessGas += message.accessWitness.writeAccountBasicData(message.to) } else { destAccessGas += message.accessWitness.readAccountBasicData(message.to) } debugGas(`destAccessGas=${destAccessGas} waived off for target at depth=0`) } let callAccessGas = message.accessWitness.readAccountBasicData(message.to) if (sendsValue) { callAccessGas += message.accessWitness.writeAccountBasicData(message.to) } gasLimit -= callAccessGas if (gasLimit < BIGINT_0) { if (this.DEBUG) { debugGas(`callAccessGas charged(${callAccessGas}) caused OOG (-> ${gasLimit})`) } message.accessWitness.revert() return { execResult: OOGResult(message.gasLimit) } } else { if (this.DEBUG) { debugGas(`callAccessGas used (${callAccessGas} gas (-> ${gasLimit}))`) } message.accessWitness.commit() } } let account = await this.stateManager.getAccount(fromAddress) if (!account) { account = new Account() } let errorMessage // Reduce tx value from sender if (!message.delegatecall) { try { await this._reduceSenderBalance(account, message) } catch (e) { errorMessage = e } } // Load `to` account let toAccount = await this.stateManager.getAccount(message.to) if (!toAccount) { if (this.common.isActivatedEIP(6800) || this.common.isActivatedEIP(7864)) { const absenceProofAccessGas = message.accessWitness!.readAccountHeader(message.to) gasLimit -= absenceProofAccessGas if (gasLimit < BIGINT_0) { if (this.DEBUG) { debugGas( `Proof of absence access charged(${absenceProofAccessGas}) caused OOG (-> ${gasLimit})`, ) } message.accessWitness?.revert() return { execResult: OOGResult(message.gasLimit) } } else { if (this.DEBUG) { debugGas(`Proof of absence access used (${absenceProofAccessGas} gas (-> ${gasLimit}))`) } message.accessWitness?.commit() } } toAccount = new Account() } // Add tx value to the `to` account if (!message.delegatecall) { try { await this._addToBalance(toAccount, message) } catch (e: any) { errorMessage = e } } // EIP-7928: Add codeAddress to BAL for DELEGATECALL/CALLCODE // For these opcodes, `to` is the current contract but `codeAddress` is the target // whose code is being executed. The target MUST be included in the BAL. if ( this.common.isActivatedEIP(7928) && message.codeAddress !== undefined && message.codeAddress.toString() !== message.to.toString() ) { this.blockLevelAccessList!.addAddress(message.codeAddress.toString()) } // Load code await this._loadCode(message) let exit = false if (!message.code || (typeof message.code !== 'function' && message.code.length === 0)) { exit = true if (this.DEBUG) { debug(`Exit early on no code (CALL)`) } } if (errorMessage !== undefined) { exit = true if (this.DEBUG) { debug(`Exit early on value transfer overflowed (CALL)`) } } // EIP-7708: Create ETH transfer log for non-zero value transfers to a different account. // CALLCODE always executes in the caller's context (to == caller), so it is a self-transfer. // Self-transfers (caller == to) and DELEGATECALL do not emit a log. let eip7708Log: Log | undefined const isTransferToDifferentAccount = !equalsBytes(message.caller.bytes, message.to.bytes) if ( this.common.isActivatedEIP(7708) && !message.delegatecall && message.value > BIGINT_0 && isTransferToDifferentAccount && errorMessage === undefined ) { eip7708Log = createEIP7708TransferLog(message.caller, message.to, message.value) if (this.DEBUG) { debug( `EIP-7708: Created ETH transfer log from ${message.caller} to ${message.to} value=${message.value}`, ) } } if (exit) { // Even on early exit, we may need to return the EIP-7708 log if value // was transferred. EIP-8037: still charge state-gas if this empty-code // call created a new account (the frame "succeeded" with no code). const earlyResult: ExecResult = { gasRefund: message.gasRefund, executionGasUsed: message.gasLimit - gasLimit, exceptionError: errorMessage, returnValue: new Uint8Array(0), logs: eip7708Log ? [eip7708Log] : undefined, } // EIP-8037: new-account state-gas for inner CALLs is now pre-charged at // the CALL opcode (callFamilyGas → finalizeCallMessageGas), matching // the EELS amsterdam (tests-bal) `charge_state_gas` placement. The // charge is unconditional w.r.t. inner-frame outcome (it stays charged // even when the call fails on insufficient balance or reverts), so no // additional charge is applied on the early-exit path here. return { execResult: earlyResult } } let result: ExecResult if (message.isCompiled) { let timer: Timer let callTimer: Timer | undefined let target: string if (this._optsCached.profiler?.enabled === true) { // Using deprecated bytesToUnprefixedHex for performance: used for profiler string formatting. target = bytesToUnprefixedHex(message.codeAddress.bytes) // TODO: map target precompile not to address, but to a name target = getPrecompileName(target) ?? target.slice(20) if (this.performanceLogger.hasTimer()) { callTimer = this.performanceLogger.pauseTimer() } timer = this.performanceLogger.startTimer(target) } result = await this.runPrecompile(message.code as PrecompileFunc, message.data, gasLimit) if (eip7708Log !== undefined) { result.logs = result.logs !== undefined ? [eip7708Log, ...result.logs] : [eip7708Log] } if (this._optsCached.profiler?.enabled === true) { this.performanceLogger.stopTimer(timer!, Number(result.executionGasUsed), 'precompiles') if (callTimer !== undefined) { this.performanceLogger.unpauseTimer(callTimer) } } result.gasRefund = message.gasRefund } else { if (this.DEBUG) { debug(`Start bytecode processing...`) } result = await this.runInterpreter( { ...{ codeAddress: message.codeAddress }, ...message, gasLimit, } as Message, { initialLogs: eip7708Log ? [eip7708Log] : undefined }, ) } if (message.depth === 0) { this.postMessageCleanup() } result.executionGasUsed += message.gasLimit - gasLimit // EIP-8037: the new-account state-gas charge is now pre-charged at the // CALL opcode (see callFamilyGas → finalizeCallMessageGas), matching the // EELS amsterdam (tests-bal) `charge_state_gas` placement before the // sub-frame runs. The charge is therefore independent of inner-frame // success/failure (per spec it sticks even when the sub-call fails for // insufficient balance or reverts). No post-frame charge is needed here. return { execResult: result, } } protected async _executeCreate(message: Message): Promise<EVMResult> { let gasLimit = message.gasLimit const fromAddress = message.caller if (this.common.isActivatedEIP(6800) || this.common.isActivatedEIP(7864)) { if (message.depth === 0) { const originAccessGas = message.accessWitness!.readAccountHeader(fromAddress) debugGas(`originAccessGas=${originAccessGas} waived off for origin at depth=0`) } } let account = await this.stateManager.getAccount(message.caller) if (!account) { account = new Account() } // Reduce tx value from sender await this._reduceSenderBalance(account, message) if (this.common.isActivatedEIP(3860)) { if ( message.data.length > Number(this.common.param('maxInitCodeSize')) && !this.allowUnlimitedInitCodeSize ) { return { createdAddress: message.to, execResult: { returnValue: new Uint8Array(0), exceptionError: new EVMError(EVMError.errorMessages.INITCODE_SIZE_VIOLATION), executionGasUsed: message.gasLimit, }, } } } // TODO at some point, figure out why we swapped out data to code in the first place message.code = message.data message.data = message.eofCallData ?? new Uint8Array() message.to = await this._generateAddress(message) if (this.common.isActivatedEIP(6780)) { message.createdAddresses!.add(message.to.toString()) } if (this.DEBUG) { debug(`Generated CREATE contract address ${message.to}`) } let toAccount = await this.stateManager.getAccount(message.to) if (!toAccount) { toAccount = new Account() } if (this.common.isActivatedEIP(6800) || this.common.isActivatedEIP(7864)) { const contractCreateAccessGas = message.accessWitness!.writeAccountBasicData(message.to) + message.accessWitness!.readAccountCodeHash(message.to) gasLimit -= contractCreateAccessGas if (gasLimit < BIGINT_0) { if (this.DEBUG) { debugGas( `ContractCreateInit charge(${contractCreateAccessGas}) caused OOG (-> ${gasLimit})`, ) message.accessWitness?.revert() } return { execResult: OOGResult(message.gasLimit) } } else { if (this.DEBUG) { debugGas(`ContractCreateInit charged (${contractCreateAccessGas} gas (-> ${gasLimit}))`) } message.accessWitness?.commit() } } // Check for collision if ( (toAccount.nonce && toAccount.nonce > BIGINT_0) || !(equalsBytes(toAccount.codeHash, KECCAK256_NULL) === true) || // See EIP 7610 and the discussion `https://ethereum-magicians.org/t/eip-7610-revert-creation-in-case-of-non-empty-storage` !(equalsBytes(toAccount.storageRoot, KECCAK256_RLP) === true) ) { if (this.DEBUG) { debug(`Returning on address collision`) } if (this.common.isActivatedEIP(7928)) { this.blockLevelAccessList!.addAddress(message.to.toString()) } return { createdAddress: message.to, execResult: { returnValue: new Uint8Array(0), exceptionError: new EVMError(EVMError.errorMessages.CREATE_COLLISION), executionGasUsed: message.gasLimit, }, } } await this.journal.putAccount(message.to, toAccount) await this.stateManager.clearStorage(message.to) const newContractEvent = { address: message.to, code: message.code, } await this._emit('newContract', newContractEvent) toAccount = await this.stateManager.getAccount(message.to) if (!toAccount) { toAccount = new Account() } // EIP-161 on account creation and CREATE execution if (this.common.gteHardfork(Hardfork.SpuriousDragon)) { toAccount.nonce += BIGINT_1 } if (this.common.isActivatedEIP(7928)) { this.blockLevelAccessList!.addNonceChange( message.to.toString(), toAccount.nonce, this.blockLevelAccessList!.blockAccessIndex, ) } // Add tx value to the `to` account let errorMessage try { await this._addToBalance(toAccount, message as MessageWithTo) } catch (e: any) { errorMessage = e } let exit = false if ( message.code === undefined || (typeof message.code !== 'function' && message.code.length === 0) ) { exit = true if (this.DEBUG) { debug(`Exit early on no code (CREATE)`) } } if (errorMessage !== undefined) { exit = true if (this.DEBUG) { debug(`Exit early on value transfer overflowed (CREATE)`) } } // EIP-7708: Create ETH transfer log for contract creation with value let eip7708CreateLog: Log | undefined if ( this.common.isActivatedEIP(7708) && message.value > BIGINT_0 && message.to !== undefined && !equalsBytes(message.caller.bytes, message.to.bytes) && errorMessage === undefined ) { eip7708CreateLog = createEIP7708TransferLog(message.caller, message.to, message.value) if (this.DEBUG) { debug( `EIP-7708: Created ETH transfer log for CREATE from ${message.caller} to ${message.to} value=${message.value}`, ) } } if (exit) { if (this.common.isActivatedEIP(6800) || this.common.isActivatedEIP(7864)) { const createCompleteAccessGas = message.accessWitness!.writeAccountHeader(message.to) gasLimit -= createCompleteAccessGas if (gasLimit < BIGINT_0) { if (this.DEBUG) { debug( `ContractCreateComplete access gas (${createCompleteAccessGas}) caused OOG (-> ${gasLimit})`, ) } message.accessWitness?.revert() return { execResult: OOGResult(message.gasLimit) } } else { if (this.DEBUG) { debug( `ContractCreateComplete access used (${createCompleteAccessGas}) gas (-> ${gasLimit})`, ) } message.accessWitness?.commit() } } return { createdAddress: message.to, execResult: { executionGasUsed: message.gasLimit - gasLimit, gasRefund: message.gasRefund, exceptionError: errorMessage, // only defined if addToBalance failed returnValue: new Uint8Array(0), logs: eip7708CreateLog ? [eip7708CreateLog] : undefined, }, } } if (this.DEBUG) { debug(`Start bytecode processing...`) } // run the message with the updated gas limit and add accessed gas used to the result let result = await this.runInterpreter({ ...message, gasLimit, isCreate: true } as Message, { initialLogs: eip7708CreateLog ? [eip7708CreateLog] : undefined, }) result.executionGasUsed += message.gasLimit - gasLimit // fee for size of the return value let totalGas = result.executionGasUsed let returnFee = BIGINT_0 // EIP-8037 state-gas charge for the new account + code deposit. Computed // here (so failure modes can refund), applied below once the success // branch confirms there is enough total gas to cover regular + state. let stateGasCreate = BIGINT_0 let stateGasFromReservoir = BIGINT_0 let stateGasSpillToGasLeft = BIGINT_0 if (!result.exceptionError && !this.common.isActivatedEIP(6800)) { if (this.common.isActivatedEIP(8037)) { // Regular code-deposit cost: 6 * ceil(L / 32) hash words const L = BigInt(result.returnValue.length) const words = (L + BigInt(31)) / BigInt(32) returnFee = words * this.common.param('codeDepositHashWordGas') // State-gas at frame-exit success: only the L * costPerStateByte // code-deposit portion. The stateBytesPerNewAccount portion is // already accounted for elsewhere: // - depth=0 creation transactions: paid in intrinsic_state_gas in // runTx (refunded on top-level failure by the runCall handler). // - depth>0 inner CREATE/CREATE2: pre-charged at the opcode entry // in opcodes/gas.ts (refunded on any inner-frame failure by the // runCall handler). const costPerStateByte = activeCostPerStateByte(this.common, this._block?.header.gasLimit) stateGasCreate = L * costPerStateByte // Tentatively split the state-gas across the reservoir and the // remaining gas in this CREATE frame. Don't mutate evm.* yet — we // commit only if totalGas (including spill) <= message.gasLimit. stateGasFromReservoir = stateGasCreate < this.stateGasReservoir ? stateGasCreate : this.stateGasReservoir stateGasSpillToGasLeft = stateGasCreate - stateGasFromReservoir } else { returnFee = BigInt(result.returnValue.length) * BigInt(this.common.param('createDataGas')) } totalGas = totalGas + returnFee + stateGasSpillToGasLeft if (this.DEBUG) { debugGas(`Add return value size fee (${returnFee} to gas used (-> ${totalGas}))`) } } // Check for SpuriousDragon EIP-170 code size limit let allowedCodeSize = true if ( !result.exceptionError && this.common.gteHardfork(Hardfork.SpuriousDragon) && result.returnValue.length > Number(this.common.param('maxCodeSize')) ) { allowedCodeSize = false } // If enough gas and allowed code size let CodestoreOOG = false let createSucceeded = false if (totalGas <= message.gasLimit && (this.allowUnlimitedContractSize || allowedCodeSize)) { if (this.common.isActivatedEIP(3541) && result.returnValue[0] === FORMAT) { if (!this.common.isActivatedEIP(3540)) { result = { ...result, ...INVALID_BYTECODE_RESULT(message.gasLimit) } } else if ( // TODO check if this is correct // Also likely cleanup this eofCallData stuff /*(message.depth > 0 && message.eofCallData === undefined) || (message.depth === 0 && !isEOF(message.code))*/ !isEOF(message.code) ) { // TODO the message.eof was flagged for this to work for this first // Running into Legacy mode: unable to deploy EOF contract result = { ...result, ...INVALID_BYTECODE_RESULT(message.gasLimit) } } else { // 3541 is active and current runtime mode is EOF result.executionGasUsed = totalGas createSucceeded = true } } else { result.executionGasUsed = totalGas createSucceeded = true } } else { if (this.common.gteHardfork(Hardfork.Homestead)) { if (!allowedCodeSize) { if (this.DEBUG) { debug(`Code size exceeds maximum code size (>= SpuriousDragon)`) } result = { ...result, ...CodesizeExceedsMaximumError(message.gasLimit) } } else { if (this.DEBUG) { debug(`Contract creation: out of gas`) } message.accessWitness?.revert() result = { ...result, ...OOGResult(message.gasLimit) } } } else { // we are in Frontier if (totalGas - returnFee <= message.gasLimit) { // we cannot pay the code deposit fee (but the deposit code actually did run) if (this.DEBUG) { debug(`Not enough gas to pay the code deposit fee (Frontier)`) } message.accessWitness?.revert() result = { ...result, ...COOGResult(totalGas - returnFee) } CodestoreOOG = true } else { if (this.DEBUG) { debug(`Contract creation: out of gas`) } message.accessWitness?.revert() result = { ...result, ...OOGResult(message.gasLimit) } } } } // EIP-8037: commit state-gas accounting now that the success / failure // branch has been chosen. On success the reservoir is debited and // execution_state_gas_used is incremented. On failure the tentative // values are dropped (the journal-revert snapshot will also restore the // reservoir to its frame-entry value, so even any in-frame charges from // the body — e.g. SSTORE — are unwound). if (this.common.isActivatedEIP(8037) && createSucceeded) { if (stateGasCreate > BIGINT_0) { this.stateGasReservoir -= stateGasFromReservoir this.executionStateGasUsed += stateGasCreate } // Record the charge keyed on the freshly-created address so runTx // can refund it if this account is SELFDESTRUCTed within the same // tx (per EIP-8037 SELFDESTRUCT deferred-refund rules). // // For depth>0 inner CREATE: the new-account portion was pre-charged // at the CREATE opcode (in opcodes/gas.ts). It isn't part of // stateGasCreate (which is only L*costPerStateByte for code deposit // here), so bake it back into the deferred-refund map alongside // stateGasCreate so a same-tx SELFDESTRUCT refunds the full amount. // For depth=0 (creation tx), the new-account portion was paid via // intrinsic_state_gas — tracked separately in // createdAccountIntrinsicStateGas (which has different refund // semantics: state-dim only, no reservoir credit). const addrKey = message.to.toString() const prior = this.createdAccountStateGas.get(addrKey) ?? BIGINT_0 let bakedIn = stateGasCreate if (message.depth > 0) { const stateBytesPerNewAccount = this.common.param('stateBytesPerNewAccount') const costPerStateByte = activeCostPerStateByte(this.common, this._block?.header.gasLimit) bakedIn += stateBytesPerNewAccount * costPerStateByte } this.createdAccountStateGas.set(addrKey, prior + bakedIn) // For depth=0 creation transactions, the intrinsic // stateBytesPerNewAccount * costPerStateByte was paid up-front in // runTx and isn't covered by stateGasCreate. Track it separately so // a same-tx SELFDESTRUCT can refund the STATE dimension (decrementing // execution_state_gas_used) without crediting the reservoir — the // user still pays the gross intrinsic on their balance, but block // block_state_gas_used reflects that nothing persisted. if (message.depth === 0) { const stateBytesPerNewAccount = this.common.param('stateBytesPerNewAccount') const costPerStateByte = activeCostPerStateByte(this.common, this._block?.header.gasLimit) this.createdAccountIntrinsicStateGas.set( addrKey, stateBytesPerNewAccount * costPerStateByte, ) } } // Note: a similar refund for the intrinsic stateBytesPerNewAccount portion // of a TOP-LEVEL creation-tx failure is applied in runCall's revert // handler (after the snapshot pop), where it can survive the snapshot // restore. See the `isCreate && message.depth === 0` branch there. // get the fresh gas limit for the rest of the ops gasLimit = message.gasLimit - result.executionGasUsed if ( !result.exceptionError && (this.common.isActivatedEIP(6800) || this.common.isActivatedEIP(7864)) ) { const createCompleteAccessGas = message.accessWitness!.writeAccountHeader(message.to) gasLimit -= createCompleteAccessGas if (gasLimit < BIGINT_0) { if (this.DEBUG) { debug( `ContractCreateComplete access gas (${createCompleteAccessGas}) caused OOG (-> ${gasLimit})`, ) } message.accessWitness?.revert() result = { ...result, ...OOGResult(message.gasLimit) } } else { debug( `ContractCreateComplete access used (${createCompleteAccessGas}) gas (-> ${gasLimit})`, ) result.executionGasUsed += createCompleteAccessGas } } // Save code if a new contract was created if ( !result.exceptionError && result.returnValue !== undefined && result.returnValue.length !== 0 ) { // Add access charges for writing this code to the state if (this.common.isActivatedEIP(6800) || this.common.isActivatedEIP(7864)) { const byteCodeWriteAccessfee = message.accessWitness!.writeAccountCodeChunks( message.to, 0, result.returnValue.length - 1, ) gasLimit -= byteCodeWriteAccessfee if (gasLimit < BIGINT_0) { if (this.DEBUG) { debug( `byteCodeWrite access gas (${byteCodeWriteAccessfee}) caused OOG (-> ${gasLimit})`, ) } message.accessWitness?.revert() result = { ...result, ...OOGResult(message.gasLimit) } } else { debug(`byteCodeWrite access used (${byteCodeWriteAccessfee}) gas (-> ${gasLimit})`) message.accessWitness?.commit() result.executionGasUsed += byteCodeWriteAccessfee } } await this.stateManager.putCode(message.to, result.returnValue) if (this.common.isActivatedEIP(7928)) { this.blockLevelAccessList!.addCodeChange( message.to.toString(), result.returnValue, this.blockLevelAccessList!.blockAccessIndex, ) } if (this.DEBUG) { debug(`Code saved on new contract creation`) } } else if (CodestoreOOG) { // This only happens at Frontier. But, let's do a sanity check; if (!this.common.gteHardfork(Hardfork.Homestead)) { // Pre-Homestead behavior; put an empty contract. // This contract would be considered "DEAD" in later hard forks. // It is thus an unnecessary default item, which we have to save to disk // It does change the state root, but it only wastes storage. const account = await this.stateManager.getAccount(message.to) await this.journal.putAccount(message.to, account ?? new Account()) } } if (message.depth === 0) { this.postMessageCleanup() } return { createdAddress: message.to, execResult: result, } } /** * Starts the actual bytecode processing for a CALL or CREATE */ protected async runInterpreter( message: Message, opts: InterpreterOpts = {}, ): Promise<ExecResult> { let contract = await this.stateManager.getAccount(message.to ?? createZeroAddress()) if (!contract) { contract = new Account() } const env = { address: message.to ?? createZeroAddress(), caller: message.caller ?? createZeroAddress(), callData: message.data ?? Uint8Array.from([0]), callValue: message.value ?? BIGINT_0, code: message.code as Uint8Array, isStatic: message.isStatic ?? false, isCreate: message.isCreate ?? false, depth: message.depth ?? 0, gasPrice: this._tx!.gasPrice, origin: this._tx!.origin ?? message.caller ?? createZeroAddress(), block: this._block ?? defaultBlock(), contract, codeAddress: message.codeAddress, gasRefund: message.gasRefund, chargeCodeAccesses: message.chargeCodeAccesses, blobVersionedHashes: message.blobVersionedHashes ?? [], accessWitness: message.accessWitness, createdAddresses: message.createdAddresses, initialLogs: opts.initialLogs, } const interpreter = new Interpreter( this, this.stateManager, this.blockchain, env, message.gasLimit, this.journal, this.performanceLogger, this._optsCached.profiler, ) if (message.selfdestruct) { interpreter._result.selfdestruct = message.selfdestruct } if (message.createdAddresses) { interpreter._result.createdAddresses = message.createdAddresses } const interpreterRes = await interpreter.run(message.code as Uint8Array, opts) let result = interpreter._result let gasUsed = message.gasLimit - interpreterRes.runState!.gasLeft if (interpreterRes.exceptionError) { if ( interpreterRes.exceptionError.error !== EVMError.errorMessages.REVERT && interpreterRes.exceptionError.error !== EVMError.errorMessages.INVALID_EOF_FORMAT ) { gasUsed = message.gasLimit } // Clear the result on error result = { ...result, logs: [], selfdestruct: new Map(), createdAddresses: new Set(), } } message.accessWitness?.commit() return { ...result, runState: { ...interpreterRes.runState!, ...result, ...interpreter._env, }, exceptionError: interpreterRes.exceptionError, gas: interpreterRes.runState?.gasLeft, executionGasUsed: gasUsed, gasRefund: interpreterRes.runState!.gasRefund, returnValue: result.returnValue ?? new Uint8Array(0), } } /** * Executes an EVM message, determining whether it's a call or create * based on the `to` address. It checkpoints the state and reverts changes * if an exception happens during the message execution. */ async runCall(opts: EVMRunCallOpts): Promise<EVMResult> { let timer: Timer | undefined if ( (opts.depth === 0 || opts.message === undefined) && this._optsCached.profiler?.enabled === true ) { timer = this.performanceLogger.startTimer('Initialization') } let message = opts.message let callerAccount if (!message) { this._block = opts.block ?? defaultBlock() const caller = opts.caller ?? createZeroAddress() this._tx = { gasPrice: opts.gasPrice ?? BIGINT_0, origin: opts.origin ?? caller, } const value = opts.value ?? BIGINT_0 if (opts.skipBalance === true) { callerAccount = await this.stateManager.getAccount(caller) if (!callerAccount) { callerAccount = new Account() } const originalBalance = callerAccount.balance if (callerAccount.balance < value) { // if skipBalance and balance less than value, set caller balance to `value` to ensure sufficient funds callerAccount.balance = value await this.journal.putAccount(caller, callerAccount) if (this.common.isActivatedEIP(7928)) { this.blockLevelAccessList!.addBalanceChange( caller.toString(), callerAccount.balance, this.blockLevelAccessList!.blockAccessIndex, originalBalance, ) } } } message = new Message({ caller, gasLimit: opts.gasLimit ?? BigInt(0xffffff), to: opts.to, value, data: opts.data, code: opts.code, depth: opts.depth, isCompiled: opts.isCompiled, isStatic: opts.isStatic, salt: opts.salt, selfdestruct: opts.selfdestruct ?? new Map(), createdAddresses: opts.createdAddresses ?? new Set(), delegatecall: opts.delegatecall, blobVersionedHashes: opts.blobVersionedHashes, }) } if (message.depth === 0) { if (!callerAccount) { callerAccount = await this.stateManager.getAccount(message.caller) } if (!callerAccount) { callerAccount = new Account() } callerAccount.nonce++ await this.journal.putAccount(message.caller, callerAccount) if (this.common.isActivatedEIP(7928)) { this.blockLevelAccessList!.addNonceChange( message.caller.toString(), callerAccount.nonce, this.blockLevelAccessList!.blockAccessIndex, ) } if (this.DEBUG) { debug(`Update fromAccount (caller) nonce (-> ${callerAccount.nonce}))`) } } await this._emit('beforeMessage', message) const isCreate = !message.to if (!message.to && this.common.isActivatedEIP(2929)) { message.code = message.data this.journal.addWarmedAddress((await this._generateAddress(message)).bytes) } if (this.common.isActivatedEIP(7928)) { this.blockLevelAccessList?.checkpoint() } await this.journal.checkpoint() if (this.common.isActivatedEIP(1153)) this.transientStorage.checkpoint() if (this.common.isActivatedEIP(8037)) { // EIP-8037: snapshot reservoir + cumulative state-gas used at frame // entry so revert / exceptional halt can restore them. this._stateGasSnapshots.push({ reservoir: this.stateGasReservoir, used: this.executionStateGasUsed, createdAccountStateGas: new Map(this.createdAccountStateGas), createdAccountIntrinsicStateGas: new Map(this.createdAccountIntrinsicStateGas), }) } if (this.DEBUG) { debug('-'.repeat(100)) debug(`message checkpoint`) } let result if (this.DEBUG) { const { caller, gasLimit, to, value, delegatecall } = message debug( `New message caller=${caller} gasLimit=${gasLimit} to=${ to?.toString() ?? 'none' } value=${value} delegatecall=${delegatecall ? 'yes' : 'no'}`, ) } if (message.to) { if (this.DEBUG) { debug(`Message CALL execution (to: ${message.to})`) } result = await this._executeCall(message as MessageWithTo) } else { if (this.DEBUG) { debug(`Message CREATE execution (to: undefined)`) } result = await this._executeCreate(message) } if (this.DEBUG) { const { executionGasUsed, exceptionError, returnValue } = result.execResult debug( `Received message execResult: [ gasUsed=${executionGasUsed} exceptionError=${ exceptionError ? `'${exceptionError.error}'` : 'none' } returnValue=${short(returnValue)} gasRefund=${result.execResult.gasRefund ?? 0} ]`, ) } const err = result.execResult.exceptionError // This clause captures any error which happened during execution // If that is the case, then all refunds are forfeited // There is one exception: if the CODESTORE_OUT_OF_GAS error is thrown // (this only happens the Frontier/Chainstart fork) // then the error is dismissed if (err && err.error !== EVMError.errorMessages.CODESTORE_OUT_OF_GAS) { result.execResult.selfdestruct = new Map() result.execResult.createdAddresses = new Set() result.execResult.gasRefund = BIGINT_0 } if ( err && !( this.common.hardfork() === Hardfork.Chainstart && err.error === EVMError.errorMessages.CODESTORE_OUT_OF_GAS ) ) { result.execResult.logs = [] await this.journal.revert() if (this.common.isActivatedEIP(1153)) this.transientStorage.revert() if (this.common.isActivatedEIP(7928)) { this.blockLevelAccessList?.revert() } if (this.common.isActivatedEIP(8037)) { // EIP-8037: restore reservoir + cumulative state-gas used to their // values at frame entry, refunding any state-gas charged during the // reverted frame and undoing any in-frame reservoir refills. // // The snapshot pop restores the reservoir to its entry value, which // implicitly refunds the reservoir-paid portion. The spec also wants // the ga