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 gas_left-spilled portion to be refunded to the parent's
        // reservoir, so the parent (or tx) effectively gets credited for
        // all state-gas charged on the reverted frame, not just the part
        // that came from the reservoir. Compute the spilled portion and add
        // it back on top.
        const snap = this._stateGasSnapshots.pop()
        if (snap !== undefined) {
          const usedThisFrame = this.executionStateGasUsed - snap.used
          const reservoirPaidThisFrame = snap.reservoir - this.stateGasReservoir
          const spilledThisFrame =
            usedThisFrame > reservoirPaidThisFrame
              ? usedThisFrame - reservoirPaidThisFrame
              : BIGINT_0
          this.stateGasReservoir = snap.reservoir + spilledThisFrame
          this.executionStateGasUsed = snap.used
          this.createdAccountStateGas = snap.createdAccountStateGas
          this.createdAccountIntrinsicStateGas = snap.createdAccountIntrinsicStateGas
        }
        // EIP-8037: on ANY creation-frame failure (revert / halt / OOG /
        // collision / oversized code etc.), refund the
        // stateBytesPerNewAccount * costPerStateByte portion that was paid
        // for this frame's new-account allocation. The pre-charge happened
        // either as intrinsic_state_gas (depth=0 creation tx, paid up-front
        // in runTx) or as the CREATE/CREATE2 opcode pre-charge (depth>0,
        // applied in opcodes/gas.ts). In neither case is it part of the
        // per-frame snapshot, so the snapshot pop doesn't credit it; do it
        // explicitly here.
        if (isCreate) {
          const stateBytesPerNewAccount = this.common.param('stateBytesPerNewAccount')
          const costPerStateByte = activeCostPerStateByte(this.common, this._block?.header.gasLimit)
          const newAccountStateGas = stateBytesPerNewAccount * costPerStateByte
          this.stateGasReservoir += newAccountStateGas
          this.executionStateGasUsed -= newAccountStateGas
        }
      }
      if (this.DEBUG) {
        debug(`message checkpoint reverted`)
      }
    } else {
      await this.journal.commit()
      if (this.common.isActivatedEIP(1153)) this.transientStorage.commit()
      if (this.common.isActivatedEIP(7928)) {
        this.blockLevelAccessList?.commit()
      }
      if (this.common.isActivatedEIP(8037)) {
        // EIP-8037: drop the frame's snapshot; current reservoir / used
        // values flow up to the parent frame.
        this._stateGasSnapshots.pop()
      }
      if (this.DEBUG) {
        debug(`message checkpoint committed`)
      }
    }
    await this._emit('afterMessage', result)

    if (message.depth === 0 && this._optsCached.profiler?.enabled === true) {
      this.performanceLogger.stopTimer(timer!, 0)
    }

    message.accessWitness?.commit()
    return result
  }

  /**
   * Bound to the global VM and therefore
   * shouldn't be used directly from the evm class
   */
  async runCode(opts: EVMRunCodeOpts): Promise<ExecResult> {
    this._block = opts.block ?? defaultBlock()

    this._tx = {
      gasPrice: opts.gasPrice ?? BIGINT_0,
      origin: opts.origin ?? opts.caller ?? createZeroAddress(),
    }

    const message = new Message({
      code: opts.code,
      data: opts.data,
      gasLimit: opts.gasLimit ?? BigInt(0xffffff),
      to: opts.to ?? createZeroAddress(),
      caller: opts.caller,
      value: opts.value,
      depth: opts.depth,
      selfdestruct: opts.selfdestruct ?? new Map(),
      isStatic: opts.isStatic,
      blobVersionedHashes: opts.blobVersionedHashes,
    })

    return this.runInterpreter(message, { pc: opts.pc })
  }

  /**
   * Returns the precompile function registered at the given address,
   * or `undefined` if no precompile is active there.
   *
   * Accepts either an `Address` instance or a `0x`-prefixed hex string.
   *
   * ```ts
   * const evm = await createEVM({
   *   customPrecompiles: [{ address: '0x000000000000000000000000000000000000ff01', function: myFn }],
   * })
   * const fn = evm.getPrecompile('0x000000000000000000000000000000000000ff01')
   * ```
   */
  getPrecompile(address: Address | PrefixedHexString): PrecompileFunc | undefined {
    if (typeof address === 'string') {
      return this.precompiles.get(address.slice(2).padStart(40, '0').toLowerCase())
    }
    return this.precompiles.get(bytesToUnprefixedHex(address.bytes))
  }

  /**
   * Executes a precompiled contract with given data and gas limit.
   */
  protected runPrecompile(
    code: PrecompileFunc,
    data: Uint8Array,
    gasLimit: bigint,
  ): Promise<ExecResult> | ExecResult {
    if (typeof code !== 'function') {
      throw EthereumJSErrorWithoutCode('Invalid precompile')
    }

    const opts = {
      data,
      gasLimit,
      common: this.common,
      _EVM: this,
      _debug: this.DEBUG ? debugPrecompiles : undefined,
      stateManager: this.stateManager,
    }

    return code(opts)
  }

  protected async _loadCode(message: Message): Promise<void> {
    if (!message.code) {
      const precompile = this.getPrecompile(message.codeAddress)
      if (precompile) {
        message.code = precompile
        message.isCompiled = true
      } else {
        message.code = await this.stateManager.getCode(message.codeAddress)

        // EIP-7702 delegation check
        if (
          this.common.isActivatedEIP(7702) &&
          equalsBytes(message.code.slice(0, 3), DELEGATION_7702_FLAG)
        ) {
          const address = new Address(message.code.slice(3, 24))
          message.code = await this.stateManager.getCode(address)
          // EIP-7928: Track delegation target access in BAL
          if (this.common.isActivatedEIP(7928)) {
            this.blockLevelAccessList?.addAddress(address.toString())
          }
          if (message.depth === 0) {
            this.journal.addAlwaysWarmAddress(address.toString())
          }
        }

        message.isCompiled = false
        message.chargeCodeAccesses = true
      }
    }
  }

  protected async _generateAddress(message: Message): Promise<Address> {
    let addr
    if (message.salt) {
      addr = generateAddress2(message.caller.bytes, message.salt, message.code as Uint8Array)
    } else {
      let acc = await this.stateManager.getAccount(message.caller)
      if (!acc) {
        acc = new Account()
      }
      const newNonce = acc.nonce - BIGINT_1
      addr = generateAddress(message.caller.bytes, bigIntToBytes(newNonce))
    }
    return new Address(addr)
  }

  protected async _reduceSenderBalance(account: Account, message: Message): Promise<void> {
    const originalBalance = account.balance
    account.balance -= message.value
    if (account.balance < BIGINT_0) {
      throw new EVMError(EVMError.errorMessages.INSUFFICIENT_BALANCE)
    }
    // EIP-7928: Record the sender's reduced balance in BAL
    // Per spec, CALL/CALLCODE senders must have their balance recorded
    if (this.common.isActivatedEIP(7928)) {
      this.blockLevelAccessList!.addBalanceChange(
        message.caller.toString(),
        account.balance,
        this.blockLevelAccessList!.blockAccessIndex,
        originalBalance,
      )
    }
    const result = this.journal.putAccount(message.caller, account)
    if (this.DEBUG) {
      debug(`Reduced sender (${message.caller}) balance (-> ${account.balance})`)
    }
    return result
  }

  protected async _addToBalance(toAccount: Account, message: MessageWithTo): Promise<void> {
    const originalBalance = toAccount.balance
    const newBalance = toAccount.balance + message.value
    if (newBalance > MAX_INTEGER) {
      throw new EVMError(EVMError.errorMessages.VALUE_OVERFLOW)
    }
    toAccount.balance = newBalance
    if (this.common.isActivatedEIP(7928)) {
      this.blockLevelAccessList!.addAddress(message.to.toString())
      if (message.value !== BIGINT_0) {
        this.blockLevelAccessList!.addBalanceChange(
          message.to.toString(),
          newBalance,
          this.blockLevelAccessList!.blockAccessIndex,
          originalBalance,
        )
      }
    }
    // putAccount as the nonce may have changed for contract creation
    await this.journal.putAccount(message.to, toAccount)
    if (this.DEBUG) {
      debug(`Added toAccount (${message.to}) balance (-> ${toAccount.balance})`)
    }
  }

  /**
   * Once the interpreter has finished depth 0, a post-message cleanup should be done
   */
  private postMessageCleanup() {
    if (this.common.isActivatedEIP(1153)) this.transientStorage.clear()
  }

  /**
   * This method copies the EVM, current HF and EIP settings
   * and returns a new EVM instance.
   *
   * Note: this is only a shallow copy and both EVM instances
   * will point to the same underlying state DB.
   *
   * @returns EVM
   */
  public shallowCopy(): EVM {
    const common = this.common.copy()
    common.setHardfork(this.common.hardfork())

    const opts = {
      ...this._optsCached,
      common,
      stateManager: this.stateManager.shallowCopy(),
    }
    // @ts-expect-error -- Assigning a StateManager property that is absent from the interface
    opts.stateManager['common'] = common
    return new EVM(opts)
  }

  public getPerformanceLogs() {
    return this.performanceLogger.getLogs()
  }

  public clearPerformanceLogs() {
    this.performanceLogger.clear()
  }
}
