Skip to main content

VM

Struct VM 

Source
pub struct VM<'a> {
Show 25 fields pub call_frames: Vec<CallFrame>, pub current_call_frame: CallFrame, pub env: Environment, pub substate: Substate, pub db: &'a mut GeneralizedDatabase, pub tx: &'a Transaction, pub hooks: Vec<Rc<RefCell<dyn Hook>>>, pub storage_original_values: FxHashMap<Address, FxHashMap<H256, U256>>, pub tracer: LevmCallTracer, pub opcode_tracer: LevmOpcodeTracer, pub debug_mode: DebugMode, pub stack_pool: Vec<Stack>, pub vm_type: VMType, pub state_gas_used: i64, pub state_gas_reservoir: u64, pub state_gas_reservoir_initial: u64, pub state_gas_spill: u64, pub cost_per_state_byte: u64, pub state_gas_new_account: u64, pub state_gas_storage_set: u64, pub state_gas_auth_total: u64, pub state_gas_auth_base: u64, pub state_refund: u64, pub intrinsic_state_gas: u64, pub crypto: &'a dyn Crypto, /* private fields */
}
Expand description

The LEVM (Lambda EVM) execution engine.

The VM executes Ethereum transactions by processing EVM bytecode. It maintains a call stack, memory, and tracks all state changes during execution.

§Execution Model

  1. Transaction is validated (nonce, balance, gas limit)
  2. Initial call frame is created with transaction data
  3. Opcodes are executed sequentially until completion or error
  4. State changes are committed or reverted based on success

§Call Stack

Nested calls (CALL, DELEGATECALL, etc.) push new frames onto call_frames. Each frame has its own memory, stack, and execution context. The current_call_frame is always the active frame being executed.

§Hooks

The VM supports hooks for extending functionality (e.g., tracing, debugging). Hooks are called at various points during execution and implement pre/post-execution logic. L2-specific behavior (such as fee handling) is implemented via hooks.

§Example

let mut vm = VM::new(env, db, &tx, tracer, vm_type, &NativeCrypto);
let report = vm.execute()?;
if report.is_success() {
    println!("Gas used: {}, Output: {:?}", report.gas_used, report.output);
} else {
    println!("Transaction reverted");
}

Fields§

§call_frames: Vec<CallFrame>

Stack of parent call frames (for nested calls).

§current_call_frame: CallFrame

The currently executing call frame.

§env: Environment

Block and transaction environment.

§substate: Substate

Execution substate (accessed addresses, logs, refunds, etc.).

§db: &'a mut GeneralizedDatabase

Database for reading/writing account state.

§tx: &'a Transaction

The transaction being executed. Borrowed for the VM’s lifetime (the caller owns it for at least that long), avoiding a per-tx deep clone of the access/authorization lists.

§hooks: Vec<Rc<RefCell<dyn Hook>>>

Execution hooks for tracing and debugging.

§storage_original_values: FxHashMap<Address, FxHashMap<H256, U256>>

Original storage values before transaction (for SSTORE gas calculation), keyed first by account to avoid hashing the full tuple on each access.

§tracer: LevmCallTracer

Call tracer for execution tracing.

§opcode_tracer: LevmOpcodeTracer

Opcode (EIP-3155) tracer. Disabled by default; zero overhead when inactive.

§debug_mode: DebugMode

Debug mode for development diagnostics.

§stack_pool: Vec<Stack>

Pool of reusable stacks to reduce allocations.

§vm_type: VMType

VM type (L1 or L2 with fee config).

§state_gas_used: i64

EIP-8037: Accumulated state gas for this transaction (Amsterdam+). Signed: goes negative when inline refunds exceed gross charges in the local frame (e.g. SSTORE 0→x→0 restoration matching an ancestor’s charge).

§state_gas_reservoir: u64

EIP-8037: State gas reservoir pre-funded from excess gas_limit (Amsterdam+).

§state_gas_reservoir_initial: u64

EIP-8037: Initial reservoir at tx start (before any execution). Captured in add_intrinsic_gas so block-dimensional regular gas can be computed independently of mid-tx reservoir activity (auth refunds, SSTORE credits).

§state_gas_spill: u64

EIP-8037: Cumulative state gas that spilled to regular gas during execution (when reservoir was insufficient). Subtracted when computing dimensional regular gas for block accounting — EELS charge_state_gas spills don’t increment regular_gas_used.

§cost_per_state_byte: u64

EIP-8037: Dynamic cost per state byte (computed from block_gas_limit, Amsterdam+).

§state_gas_new_account: u64

EIP-8037: State gas for new account creation (STATE_BYTES_PER_NEW_ACCOUNT * cost_per_state_byte).

§state_gas_storage_set: u64

EIP-8037: State gas for storage slot creation (STATE_BYTES_PER_STORAGE_SET * cost_per_state_byte).

§state_gas_auth_total: u64

EIP-8037: State gas for EIP-7702 auth total (STATE_BYTES_PER_AUTH_TOTAL * cost_per_state_byte).

§state_gas_auth_base: u64

EIP-8037: State gas for the 23-byte EIP-7702 delegation indicator (STATE_BYTES_PER_AUTH_BASE * cost_per_state_byte). Refunded by set_delegation when no new delegation indicator bytes are written — either the authority’s code slot already holds an indicator or the auth clears against an empty authority.

§state_refund: u64

EIP-8037: state-gas refund channel. Mirrors EELS MessageCallOutput.state_refund — a separate, monotonic accumulator for refunds that bypass per-frame state_gas_used accounting. Populated by set_delegation for existing-authority refunds, subtracted from block-level state-gas at the end of refund_sender. Survives revert/halt/OOG since it lives on the VM, not in any call-frame backup.

§intrinsic_state_gas: u64

EIP-8037: intrinsic state gas (tx_env.intrinsic_state_gas in EELS). Captured at add_intrinsic_gas time. ethrex lumps intrinsic + execution into state_gas_used, so on top-level error this field is what we leave behind when refunding the execution portion to the reservoir — block accounting then bills the intrinsic (matches EELS tx_state_gas = intrinsic_state_gas + tx_output.state_gas_used).

§crypto: &'a dyn Crypto

Crypto provider for cryptographic operations.

Implementations§

Source§

impl<'a> VM<'a>

Source

pub fn add_callframe(&mut self, new_call_frame: CallFrame)

Adds current calframe to call_frames, sets current call frame to the passed callframe.

Source

pub fn pop_call_frame(&mut self) -> Result<CallFrame, InternalError>

Source

pub fn is_initial_call_frame(&self) -> bool

Source

pub fn restore_cache_state(&mut self) -> Result<(), VMError>

Restores the cache state to the state before changes made during a callframe.

Source

pub fn restore_cache_state_consuming(&mut self) -> Result<(), VMError>

Like Self::restore_cache_state but moves the current frame’s backup out instead of cloning it. Only sound when nothing reads call_frame_backup afterward: the inner-call revert in handle_return (the frame is popped right after, so its backup is dead), and the top-level / invalid-tx revert when no BackupHook is installed (normal L1 block execution, gated on VM::preserve_top_level_backup).

When a BackupHook IS present (L2 / stateless) the top-level paths must keep cloning, because BackupHook::finalize reads the backup to build the tx-level undo snapshot.

Source

pub fn merge_call_frame_backup_with_parent( &mut self, child_call_frame_backup: &CallFrameBackup, ) -> Result<(), VMError>

Source

pub fn advance_pc(&mut self)

Source§

impl<'a> VM<'a>

Source

pub fn get_account_mut( &mut self, address: Address, ) -> Result<&mut LevmAccount, InternalError>

Source

pub fn increase_account_balance( &mut self, address: Address, increase: U256, ) -> Result<(), InternalError>

Source

pub fn decrease_account_balance( &mut self, address: Address, decrease: U256, ) -> Result<(), InternalError>

Source

pub fn transfer( &mut self, from: Address, to: Address, value: U256, ) -> Result<(), InternalError>

Source

pub fn update_account_bytecode( &mut self, address: Address, new_bytecode: Code, ) -> Result<(), InternalError>

Updates bytecode of given account.

Source

pub fn increment_account_nonce( &mut self, address: Address, ) -> Result<u64, InternalError>

Increments the nonce of the given account. Per EIP-7928, nonce changes are recorded for:

  • EOA senders
  • Contracts performing CREATE/CREATE2
  • Deployed contracts
  • EIP-7702 authorities
Source

pub fn access_storage_slot_for_sstore( &mut self, address: Address, key: H256, ) -> Result<(U256, U256, bool), InternalError>

SSTORE-specialized storage access path that returns current and original values together. This keeps the SSTORE hot path tighter by avoiding extra method-level plumbing.

Source

pub fn record_storage_slot_to_bal(&mut self, address: Address, key: U256)

Records a storage slot read to BAL after gas checks have passed. Per EIP-7928: “If pre-state validation fails, the target is never accessed and must not appear in BAL.” This function should be called AFTER the gas check succeeds.

Source

pub fn get_storage_value( &mut self, address: Address, key: H256, ) -> Result<U256, InternalError>

Gets storage value of an account, caching it if not already cached.

Source

pub fn update_account_storage( &mut self, address: Address, key: H256, slot_key: U256, new_value: U256, current_value: U256, ) -> Result<(), InternalError>

Updates storage of an account, caching it if not already cached.

Source

pub fn backup_storage_slot( &mut self, address: Address, key: H256, current_value: U256, ) -> Result<(), InternalError>

Source§

impl<'a> VM<'a>

Source

pub fn handle_precompile_result( precompile_result: Result<Bytes, VMError>, gas_limit: u64, gas_remaining: u64, ) -> Result<ContextResult, VMError>

Source

pub fn handle_opcode_result(&mut self) -> Result<ContextResult, VMError>

Source

pub fn handle_opcode_error( &mut self, error: VMError, ) -> Result<ContextResult, VMError>

Source

pub fn handle_create_transaction( &mut self, ) -> Result<Option<ContextResult>, VMError>

Handles external create transaction.

Source§

impl<'a> VM<'a>

Source

pub fn generic_create( &mut self, value: U256, code_offset_in_memory: usize, code_size_in_memory: usize, salt: Option<U256>, ) -> Result<OpcodeResult, VMError>

Common behavior for CREATE and CREATE2 opcodes

Source

pub fn generic_call( &mut self, gas_limit: u64, value: U256, msg_sender: Address, to: Address, code_address: Address, should_transfer_value: bool, is_static: bool, calldata: Bytes, ret_offset: usize, ret_size: usize, bytecode: Code, is_delegation_7702: bool, ) -> Result<OpcodeResult, VMError>

This (should) be the only function where gas is used as a U256. This is because we have to use the values that are pushed to the stack.

Source

pub fn handle_state_backup( &mut self, ctx_result: &ContextResult, consume_backup: bool, ) -> Result<(), VMError>

Pop backup from stack and restore substate and cache if transaction reverted.

consume_backup lets the caller move the frame’s backup out (no clone) on the revert path when nothing reads it afterward; see VM::restore_cache_state_consuming. The top-level call passes true for normal L1 execution and false when a BackupHook is installed (L2 / stateless), since that hook reads the backup in finalize_execution (gated on VM::preserve_top_level_backup).

Source

pub fn handle_return( &mut self, ctx_result: &ContextResult, ) -> Result<(), VMError>

Handles case in which callframe was initiated by another callframe (with CALL or CREATE family opcodes)

Returns the pc increment.

Source

pub fn handle_return_call( &mut self, executed_call_frame: CallFrame, ctx_result: &ContextResult, ) -> Result<(), VMError>

Source

pub fn handle_return_create( &mut self, executed_call_frame: CallFrame, ctx_result: &ContextResult, ) -> Result<(), VMError>

Source§

impl<'a> VM<'a>

Source

pub fn get_trace_result(&mut self) -> Result<CallTraceFrame, VMError>

This method is intended to be accessed after transaction execution

Source§

impl<'a> VM<'a>

Source

pub fn eip7702_set_access_code(&mut self) -> Result<(), VMError>

Sets the account code as the EIP7702 determines.

Source

pub fn add_intrinsic_gas( &mut self, intrinsic: &IntrinsicGas, ) -> Result<(), VMError>

Source

pub fn get_intrinsic_gas(&self) -> Result<IntrinsicGas, VMError>

Returns (regular_gas, state_gas) intrinsic gas for the transaction. For Amsterdam+, state_gas is the EIP-8037 state portion. For pre-Amsterdam, state_gas is always 0.

Source

pub fn get_min_gas_used(&self) -> Result<u64, VMError>

Calculates the minimum gas to be consumed in the transaction.

Source

pub fn get_tx_callee( tx: &Transaction, db: &mut GeneralizedDatabase, env: &Environment, substate: &mut Substate, ) -> Result<(Address, bool), VMError>

Gets transaction callee, calculating create address if it’s a “Create” transaction. Bool indicates whether it is a create transaction or not.

Source§

impl<'a> VM<'a>

Source

pub fn new( env: Environment, db: &'a mut GeneralizedDatabase, tx: &'a Transaction, tracer: LevmCallTracer, vm_type: VMType, crypto: &'a dyn Crypto, ) -> Result<Self, VMError>

Constructs a VM, allocating a fresh 32 KB root call-frame stack.

Hot block execution should prefer VM::new_pooled, which draws the root stack from a reusable pool instead of allocating + zeroing one per transaction.

Source

pub fn new_pooled( env: Environment, db: &'a mut GeneralizedDatabase, tx: &'a Transaction, tracer: LevmCallTracer, vm_type: VMType, crypto: &'a dyn Crypto, stack_pool: &mut Vec<Stack>, memory_pool: &mut Vec<Memory>, ) -> Result<Self, VMError>

Like VM::new, but draws the root call-frame stack from stack_pool (falling back to a fresh Stack::default() only when the pool is empty) and adopts the remaining pooled stacks for sub-call frames. This avoids the per-tx 32 KB stack alloc+zero on a warm pool — the dominant allocation for transfer-heavy blocks, where the root frame is the only frame.

Pair with VM::reclaim_into after execution to return every stack (root + sub-frame) to stack_pool and the root memory buffer to memory_pool so the next tx reuses them.

Source

pub fn reclaim_into( self, stack_pool: &mut Vec<Stack>, memory_pool: &mut Vec<Memory>, )

Returns this VM’s reusable buffers to the caller’s pools so the next transaction reuses them instead of allocating: every stack (root call-frame stack plus any sub-frame stacks still pooled internally) to stack_pool, and the root memory buffer to memory_pool. Must run on both the success and error paths of VM::execute.

Source

pub fn increase_state_gas(&mut self, gas: u64) -> Result<(), VMError>

EIP-8037: Charge state gas, drawing from reservoir first, spilling to gas_remaining if exhausted.

Must only be called for Amsterdam+ forks. All call sites must guard with fork >= Fork::Amsterdam before invoking this method.

Source

pub fn credit_state_gas_refund(&mut self, amount: u64) -> Result<(), VMError>

EIP-8037: credit amount directly to the local frame’s reservoir; state_gas_used may go negative when the matching charge lives in an ancestor frame.

Must only be called for Amsterdam+ forks.

Source

pub fn incorporate_child_state_gas_on_revert( &mut self, state_gas_used_at_entry: i64, ) -> Result<(), VMError>

EIP-8037 incorporate_child_on_error: on child revert, restore the parent’s state_gas_used to its pre-child value and refund the child’s net (state_gas_used + state_gas_left) back into the parent’s reservoir.

In ethrex’s shared-VM model the child holds the entire reservoir during its execution, so child.state_gas_left == self.state_gas_reservoir (absolute, not a delta against entry). child.state_gas_used can be negative when inline refunds inside the child exceeded its gross charges.

Source

pub fn execute(&mut self) -> Result<ExecutionReport, VMError>

Executes a whole external transaction. Performing validations at the beginning.

Source

pub fn run_execution(&mut self) -> Result<ContextResult, VMError>

Main execution loop.

Source

pub fn execute_precompile( code_address: H160, calldata: &Bytes, gas_limit: u64, gas_remaining: &mut u64, fork: Fork, cache: Option<&PrecompileCache>, crypto: &dyn Crypto, ) -> Result<ContextResult, VMError>

Executes precompile and handles the output that it returns, generating a report.

Source

pub fn is_create(&self) -> Result<bool, InternalError>

True if external transaction is a contract creation

Source

pub fn stateless_execute(&mut self) -> Result<ExecutionReport, VMError>

Executes without making changes to the cache.

Source

pub fn collect_stack_for_trace(&self) -> Vec<U256>

Collects the current stack in bottom-first order for struct-log emission.

LEVM stack is top-first in memory (values[offset] = top), so we reverse the active slice to produce the bottom-first wire format geth uses. Returns an empty Vec when cfg.disable_stack is true.

Source

pub fn collect_memory_for_trace(&self) -> Vec<u8>

Collects the live memory bytes for the current frame.

Returns an empty Vec when cfg.enable_memory is false or memory is empty.

Source

pub fn read_storage_for_trace(&mut self, opcode: u8) -> Option<(H256, H256)>

Pre-reads the storage key/value for the current SLOAD or SSTORE opcode.

Returns None when:

  • cfg.disable_storage is set, or
  • opcode is not SLOAD (0x54) or SSTORE (0x55), or
  • the stack is empty (guard against underflow before the handler runs), or
  • the storage read fails for any reason (including AccountNotFound — the trace omits the entry rather than emitting an ambiguous zero).

For SLOAD: key = stack.top; value = the current stored value read from the DB. For SSTORE: key = stack.top, value = stack[top-1] (the new value being written).

Auto Trait Implementations§

§

impl<'a> !Freeze for VM<'a>

§

impl<'a> !RefUnwindSafe for VM<'a>

§

impl<'a> !Send for VM<'a>

§

impl<'a> !Sync for VM<'a>

§

impl<'a> !UnwindSafe for VM<'a>

§

impl<'a> Unpin for VM<'a>

§

impl<'a> UnsafeUnpin for VM<'a>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> ArchivePointee for T

Source§

type ArchivedMetadata = ()

The archived version of the pointer metadata for this type.
Source§

fn pointer_metadata( _: &<T as ArchivePointee>::ArchivedMetadata, ) -> <T as Pointee>::Metadata

Converts some archived metadata to the pointer metadata for itself.
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T, U> ExactFrom<T> for U
where U: TryFrom<T>,

Source§

fn exact_from(value: T) -> U

Source§

impl<T, U> ExactInto<U> for T
where U: ExactFrom<T>,

Source§

fn exact_into(self) -> U

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> LayoutRaw for T

Source§

fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>

Returns the layout of the type.
Source§

impl<T, N1, N2> Niching<NichedOption<T, N1>> for N2
where T: SharedNiching<N1, N2>, N1: Niching<T>, N2: Niching<T>,

Source§

unsafe fn is_niched(niched: *const NichedOption<T, N1>) -> bool

Returns whether the given value has been niched. Read more
Source§

fn resolve_niched(out: Place<NichedOption<T, N1>>)

Writes data to out indicating that a T is niched.
Source§

impl<T, U> OverflowingInto<U> for T
where U: OverflowingFrom<T>,

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Pointee for T

Source§

type Metadata = ()

The metadata type for pointers and references to this type.
Source§

impl<T, U> RoundingInto<U> for T
where U: RoundingFrom<T>,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> SaturatingInto<U> for T
where U: SaturatingFrom<T>,

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

impl<T, U> WrappingInto<U> for T
where U: WrappingFrom<T>,

Source§

fn wrapping_into(self) -> U