@ethereumjs/evm
Version:
JavaScript Ethereum Virtual Machine (EVM) implementation
1,375 lines (1,263 loc) • 59.4 kB
text/typescript
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