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
- Transaction is validated (nonce, balance, gas limit)
- Initial call frame is created with transaction data
- Opcodes are executed sequentially until completion or error
- 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: CallFrameThe currently executing call frame.
env: EnvironmentBlock and transaction environment.
substate: SubstateExecution substate (accessed addresses, logs, refunds, etc.).
db: &'a mut GeneralizedDatabaseDatabase for reading/writing account state.
tx: &'a TransactionThe 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: LevmCallTracerCall tracer for execution tracing.
opcode_tracer: LevmOpcodeTracerOpcode (EIP-3155) tracer. Disabled by default; zero overhead when inactive.
debug_mode: DebugModeDebug mode for development diagnostics.
stack_pool: Vec<Stack>Pool of reusable stacks to reduce allocations.
vm_type: VMTypeVM type (L1 or L2 with fee config).
state_gas_used: i64EIP-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: u64EIP-8037: State gas reservoir pre-funded from excess gas_limit (Amsterdam+).
state_gas_reservoir_initial: u64EIP-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: u64EIP-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: u64EIP-8037: Dynamic cost per state byte (computed from block_gas_limit, Amsterdam+).
state_gas_new_account: u64EIP-8037: State gas for new account creation (STATE_BYTES_PER_NEW_ACCOUNT * cost_per_state_byte).
state_gas_storage_set: u64EIP-8037: State gas for storage slot creation (STATE_BYTES_PER_STORAGE_SET * cost_per_state_byte).
state_gas_auth_total: u64EIP-8037: State gas for EIP-7702 auth total (STATE_BYTES_PER_AUTH_TOTAL * cost_per_state_byte).
state_gas_auth_base: u64EIP-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: u64EIP-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: u64EIP-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 CryptoCrypto provider for cryptographic operations.
Implementations§
Source§impl<'a> VM<'a>
impl<'a> VM<'a>
Sourcepub fn add_callframe(&mut self, new_call_frame: CallFrame)
pub fn add_callframe(&mut self, new_call_frame: CallFrame)
Adds current calframe to call_frames, sets current call frame to the passed callframe.
pub fn pop_call_frame(&mut self) -> Result<CallFrame, InternalError>
pub fn is_initial_call_frame(&self) -> bool
Sourcepub fn restore_cache_state(&mut self) -> Result<(), VMError>
pub fn restore_cache_state(&mut self) -> Result<(), VMError>
Restores the cache state to the state before changes made during a callframe.
Sourcepub fn restore_cache_state_consuming(&mut self) -> Result<(), VMError>
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.
pub fn merge_call_frame_backup_with_parent( &mut self, child_call_frame_backup: &CallFrameBackup, ) -> Result<(), VMError>
pub fn advance_pc(&mut self)
Source§impl<'a> VM<'a>
impl<'a> VM<'a>
pub fn get_account_mut( &mut self, address: Address, ) -> Result<&mut LevmAccount, InternalError>
pub fn increase_account_balance( &mut self, address: Address, increase: U256, ) -> Result<(), InternalError>
pub fn decrease_account_balance( &mut self, address: Address, decrease: U256, ) -> Result<(), InternalError>
pub fn transfer( &mut self, from: Address, to: Address, value: U256, ) -> Result<(), InternalError>
Sourcepub fn update_account_bytecode(
&mut self,
address: Address,
new_bytecode: Code,
) -> Result<(), InternalError>
pub fn update_account_bytecode( &mut self, address: Address, new_bytecode: Code, ) -> Result<(), InternalError>
Updates bytecode of given account.
Sourcepub fn increment_account_nonce(
&mut self,
address: Address,
) -> Result<u64, InternalError>
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
Sourcepub fn access_storage_slot_for_sstore(
&mut self,
address: Address,
key: H256,
) -> Result<(U256, U256, bool), InternalError>
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.
Sourcepub fn record_storage_slot_to_bal(&mut self, address: Address, key: U256)
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.
Sourcepub fn get_storage_value(
&mut self,
address: Address,
key: H256,
) -> Result<U256, InternalError>
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.
Sourcepub fn update_account_storage(
&mut self,
address: Address,
key: H256,
slot_key: U256,
new_value: U256,
current_value: U256,
) -> Result<(), InternalError>
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.
pub fn backup_storage_slot( &mut self, address: Address, key: H256, current_value: U256, ) -> Result<(), InternalError>
Source§impl<'a> VM<'a>
impl<'a> VM<'a>
pub fn handle_precompile_result( precompile_result: Result<Bytes, VMError>, gas_limit: u64, gas_remaining: u64, ) -> Result<ContextResult, VMError>
pub fn handle_opcode_result(&mut self) -> Result<ContextResult, VMError>
pub fn handle_opcode_error( &mut self, error: VMError, ) -> Result<ContextResult, VMError>
Sourcepub fn handle_create_transaction(
&mut self,
) -> Result<Option<ContextResult>, VMError>
pub fn handle_create_transaction( &mut self, ) -> Result<Option<ContextResult>, VMError>
Handles external create transaction.
Source§impl<'a> VM<'a>
impl<'a> VM<'a>
Sourcepub fn generic_create(
&mut self,
value: U256,
code_offset_in_memory: usize,
code_size_in_memory: usize,
salt: Option<U256>,
) -> Result<OpcodeResult, VMError>
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
Sourcepub 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>
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.
Sourcepub fn handle_state_backup(
&mut self,
ctx_result: &ContextResult,
consume_backup: bool,
) -> Result<(), VMError>
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).
Sourcepub fn handle_return(
&mut self,
ctx_result: &ContextResult,
) -> Result<(), VMError>
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.
pub fn handle_return_call( &mut self, executed_call_frame: CallFrame, ctx_result: &ContextResult, ) -> Result<(), VMError>
pub fn handle_return_create( &mut self, executed_call_frame: CallFrame, ctx_result: &ContextResult, ) -> Result<(), VMError>
Source§impl<'a> VM<'a>
impl<'a> VM<'a>
Sourcepub fn get_trace_result(&mut self) -> Result<CallTraceFrame, VMError>
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>
impl<'a> VM<'a>
Sourcepub fn eip7702_set_access_code(&mut self) -> Result<(), VMError>
pub fn eip7702_set_access_code(&mut self) -> Result<(), VMError>
Sets the account code as the EIP7702 determines.
pub fn add_intrinsic_gas( &mut self, intrinsic: &IntrinsicGas, ) -> Result<(), VMError>
Sourcepub fn get_intrinsic_gas(&self) -> Result<IntrinsicGas, VMError>
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.
Sourcepub fn get_min_gas_used(&self) -> Result<u64, VMError>
pub fn get_min_gas_used(&self) -> Result<u64, VMError>
Calculates the minimum gas to be consumed in the transaction.
Sourcepub fn get_tx_callee(
tx: &Transaction,
db: &mut GeneralizedDatabase,
env: &Environment,
substate: &mut Substate,
) -> Result<(Address, bool), VMError>
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>
impl<'a> VM<'a>
Sourcepub fn new(
env: Environment,
db: &'a mut GeneralizedDatabase,
tx: &'a Transaction,
tracer: LevmCallTracer,
vm_type: VMType,
crypto: &'a dyn Crypto,
) -> Result<Self, VMError>
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.
Sourcepub 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>
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.
Sourcepub fn reclaim_into(
self,
stack_pool: &mut Vec<Stack>,
memory_pool: &mut Vec<Memory>,
)
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.
Sourcepub fn increase_state_gas(&mut self, gas: u64) -> Result<(), VMError>
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.
Sourcepub fn credit_state_gas_refund(&mut self, amount: u64) -> Result<(), VMError>
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.
Sourcepub fn incorporate_child_state_gas_on_revert(
&mut self,
state_gas_used_at_entry: i64,
) -> Result<(), VMError>
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.
Sourcepub fn execute(&mut self) -> Result<ExecutionReport, VMError>
pub fn execute(&mut self) -> Result<ExecutionReport, VMError>
Executes a whole external transaction. Performing validations at the beginning.
Sourcepub fn run_execution(&mut self) -> Result<ContextResult, VMError>
pub fn run_execution(&mut self) -> Result<ContextResult, VMError>
Main execution loop.
Sourcepub 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>
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.
Sourcepub fn is_create(&self) -> Result<bool, InternalError>
pub fn is_create(&self) -> Result<bool, InternalError>
True if external transaction is a contract creation
Sourcepub fn stateless_execute(&mut self) -> Result<ExecutionReport, VMError>
pub fn stateless_execute(&mut self) -> Result<ExecutionReport, VMError>
Executes without making changes to the cache.
Sourcepub fn collect_stack_for_trace(&self) -> Vec<U256>
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.
Sourcepub fn collect_memory_for_trace(&self) -> Vec<u8> ⓘ
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.
Sourcepub fn read_storage_for_trace(&mut self, opcode: u8) -> Option<(H256, H256)>
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_storageis set, oropcodeis 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> ArchivePointee for T
impl<T> ArchivePointee for T
Source§type ArchivedMetadata = ()
type ArchivedMetadata = ()
Source§fn pointer_metadata(
_: &<T as ArchivePointee>::ArchivedMetadata,
) -> <T as Pointee>::Metadata
fn pointer_metadata( _: &<T as ArchivePointee>::ArchivedMetadata, ) -> <T as Pointee>::Metadata
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
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 moreSource§impl<T> LayoutRaw for T
impl<T> LayoutRaw for T
Source§fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>
fn layout_raw(_: <T as Pointee>::Metadata) -> Result<Layout, LayoutError>
Source§impl<T, N1, N2> Niching<NichedOption<T, N1>> for N2
impl<T, N1, N2> Niching<NichedOption<T, N1>> for N2
Source§unsafe fn is_niched(niched: *const NichedOption<T, N1>) -> bool
unsafe fn is_niched(niched: *const NichedOption<T, N1>) -> bool
Source§fn resolve_niched(out: Place<NichedOption<T, N1>>)
fn resolve_niched(out: Place<NichedOption<T, N1>>)
out indicating that a T is niched.