Skip to main content

ethrex_levm/
vm.rs

1use crate::{
2    TransientStorage,
3    call_frame::{CallFrame, Stack},
4    db::gen_db::GeneralizedDatabase,
5    debug::DebugMode,
6    environment::Environment,
7    errors::{
8        ContextResult, ExceptionalHalt, ExecutionReport, InternalError, OpcodeResult, TxResult,
9        VMError,
10    },
11    gas_cost::{
12        STATE_BYTES_PER_AUTH_BASE, STATE_BYTES_PER_AUTH_TOTAL, STATE_BYTES_PER_NEW_ACCOUNT,
13        STATE_BYTES_PER_STORAGE_SET, cost_per_state_byte as compute_cost_per_state_byte,
14    },
15    hooks::{
16        backup_hook::BackupHook,
17        hook::{Hook, get_hooks},
18    },
19    memory::Memory,
20    opcode_tracer::LevmOpcodeTracer,
21    opcodes::OpCodeFn,
22    precompiles::{
23        self, SIZE_PRECOMPILES_CANCUN, SIZE_PRECOMPILES_PRAGUE, SIZE_PRECOMPILES_PRE_CANCUN,
24    },
25    tracing::LevmCallTracer,
26};
27use bytes::Bytes;
28use ethrex_common::{
29    Address, BigEndianHash, H160, H256, U256,
30    tracing::CallType,
31    types::{AccessListEntry, Code, Fork, Log, Transaction, fee_config::FeeConfig},
32};
33use ethrex_crypto::Crypto;
34use rustc_hash::{FxHashMap, FxHashSet};
35use std::{
36    cell::{OnceCell, RefCell},
37    collections::{BTreeMap, BTreeSet},
38    mem,
39    rc::Rc,
40};
41
42/// Storage mapping from slot key to value.
43pub type Storage = FxHashMap<U256, H256>;
44
45/// Specifies whether the VM operates in L1 or L2 mode.
46#[derive(Debug, Clone, Copy, Default)]
47pub enum VMType {
48    /// Standard Ethereum L1 execution.
49    #[default]
50    L1,
51    /// L2 rollup execution with additional fee handling.
52    L2(FeeConfig),
53}
54
55/// Execution substate that tracks changes during transaction execution.
56///
57/// The substate maintains all information that may need to be reverted if a
58/// call fails, including:
59/// - Self-destructed accounts
60/// - Accessed addresses and storage slots (for EIP-2929 gas accounting)
61/// - Created accounts
62/// - Gas refunds
63/// - Transient storage (EIP-1153)
64/// - Event logs
65///
66/// # Backup Mechanism
67///
68/// The substate supports checkpointing via [`push_backup`] and restoration via
69/// [`revert_backup`] or commitment via [`commit_backup`]. This is used to handle
70/// nested calls where inner calls may fail and need to be reverted.
71///
72/// Most fields are private by design. The backup mechanism only works correctly
73/// if data modifications are append-only.
74#[derive(Debug, Default)]
75pub struct Substate {
76    /// Parent checkpoint for reverting on failure.
77    parent: Option<Box<Self>>,
78    /// Fork of the enclosing transaction. Lets the warmth helpers treat precompile addresses as
79    /// always-warm without occupying a hashset slot (EIP-2929). Constant for a tx, so it is
80    /// carried forward across `push_backup` checkpoints.
81    fork: Fork,
82    /// Accounts marked for self-destruction (deleted at end of transaction).
83    selfdestruct_set: FxHashSet<Address>,
84    /// Addresses accessed during execution (for EIP-2929 warm/cold gas costs).
85    /// Precompiles are NOT stored here; they are warm by construction (see `is_warm_precompile`).
86    accessed_addresses: FxHashSet<Address>,
87    /// Storage slots accessed per address (for EIP-2929 warm/cold gas costs).
88    accessed_storage_slots: FxHashMap<Address, FxHashSet<H256>>,
89    /// Accounts created during this transaction.
90    created_accounts: FxHashSet<Address>,
91    /// Accumulated gas refund (e.g., from storage clears).
92    pub refunded_gas: u64,
93    /// Transient storage (EIP-1153), cleared at end of transaction.
94    transient_storage: TransientStorage,
95    /// Event logs emitted during execution.
96    logs: Vec<Log>,
97}
98
99impl Substate {
100    pub fn from_accesses(
101        fork: Fork,
102        accessed_addresses: FxHashSet<Address>,
103        accessed_storage_slots: FxHashMap<Address, FxHashSet<H256>>,
104    ) -> Self {
105        Self {
106            parent: None,
107            fork,
108            selfdestruct_set: FxHashSet::default(),
109            accessed_addresses,
110            accessed_storage_slots,
111            created_accounts: FxHashSet::default(),
112            refunded_gas: 0,
113            transient_storage: TransientStorage::default(),
114            logs: Vec::new(),
115        }
116    }
117
118    /// Whether `address` is a precompile that the EVM treats as warm from the start of the tx
119    /// (EIP-2929), exactly matching the addresses `Substate::initialize` used to pre-seed.
120    ///
121    /// Replicates the pre-seed *precisely* — the contiguous range `0x01..=max_for_fork` plus the
122    /// post-Osaka P256VERIFY address `0x100` — and is intentionally `vm_type`-independent, since
123    /// the old pre-seed was too. (Using `precompiles::is_precompile`, which gates `0x100` on L2
124    /// for any fork, would change L2 pre-Osaka warmth — a consensus difference, not an opt.)
125    #[inline]
126    fn is_warm_precompile(&self, address: &Address) -> bool {
127        // Fast reject: every pre-seeded precompile has 18 leading zero bytes (max is `0x01_00`),
128        // so real contract/EOA addresses bail out here, off the hot warmth path.
129        if address.0[..18] != [0u8; 18] {
130            return false;
131        }
132        let n = u16::from_be_bytes([address.0[18], address.0[19]]);
133        let max_contiguous: u64 = match self.fork {
134            f if f >= Fork::Prague => SIZE_PRECOMPILES_PRAGUE,
135            f if f >= Fork::Cancun => SIZE_PRECOMPILES_CANCUN,
136            _ => SIZE_PRECOMPILES_PRE_CANCUN,
137        };
138        (n >= 1 && u64::from(n) <= max_contiguous) || (n == 0x100 && self.fork >= Fork::Osaka)
139    }
140
141    /// Push a checkpoint that can be either reverted or committed. All data up to this point is
142    /// still accessible.
143    pub fn push_backup(&mut self) {
144        let parent = mem::take(self);
145        self.refunded_gas = parent.refunded_gas;
146        // Carry the fork forward so child checkpoints keep the same precompile-warmth view.
147        self.fork = parent.fork;
148        self.parent = Some(Box::new(parent));
149    }
150
151    /// Pop and merge with the last backup.
152    ///
153    /// Does nothing if the substate has no backup.
154    pub fn commit_backup(&mut self) {
155        if let Some(parent) = self.parent.as_mut() {
156            let mut delta = mem::take(parent);
157            mem::swap(self, &mut delta);
158
159            self.selfdestruct_set.extend(delta.selfdestruct_set);
160            self.accessed_addresses.extend(delta.accessed_addresses);
161            for (address, slot_set) in delta.accessed_storage_slots {
162                self.accessed_storage_slots
163                    .entry(address)
164                    .or_default()
165                    .extend(slot_set);
166            }
167            self.created_accounts.extend(delta.created_accounts);
168            self.refunded_gas = delta.refunded_gas;
169            self.transient_storage.extend(delta.transient_storage);
170            self.logs.extend(delta.logs);
171        }
172    }
173
174    /// Discard current changes and revert to last backup.
175    ///
176    /// Does nothing if the substate has no backup.
177    pub fn revert_backup(&mut self) {
178        if let Some(parent) = self.parent.as_mut() {
179            *self = mem::take(parent);
180        }
181    }
182
183    /// Return an iterator over all selfdestruct addresses.
184    pub fn iter_selfdestruct(&self) -> impl Iterator<Item = &Address> {
185        struct Iter<'a> {
186            parent: Option<&'a Substate>,
187            iter: std::collections::hash_set::Iter<'a, Address>,
188        }
189
190        impl<'a> Iterator for Iter<'a> {
191            type Item = &'a Address;
192
193            fn next(&mut self) -> Option<Self::Item> {
194                let next_item = self.iter.next();
195                if next_item.is_none()
196                    && let Some(parent) = self.parent
197                {
198                    self.parent = parent.parent.as_deref();
199                    self.iter = parent.selfdestruct_set.iter();
200
201                    return self.next();
202                }
203
204                next_item
205            }
206        }
207
208        Iter {
209            parent: self.parent.as_deref(),
210            iter: self.selfdestruct_set.iter(),
211        }
212    }
213
214    /// Mark an address as selfdestructed and return whether is was already marked.
215    pub fn add_selfdestruct(&mut self, address: Address) -> bool {
216        if self.selfdestruct_set.contains(&address) {
217            return true;
218        }
219
220        let is_present = self
221            .parent
222            .as_ref()
223            .map(|parent| parent.is_selfdestruct(&address))
224            .unwrap_or_default();
225
226        is_present || !self.selfdestruct_set.insert(address)
227    }
228
229    /// Return whether an address is already marked as selfdestructed.
230    pub fn is_selfdestruct(&self, address: &Address) -> bool {
231        self.selfdestruct_set.contains(address)
232            || self
233                .parent
234                .as_ref()
235                .map(|parent| parent.is_selfdestruct(address))
236                .unwrap_or_default()
237    }
238
239    /// Build an access list from all accessed storage slots.
240    pub fn make_access_list(&self) -> Vec<AccessListEntry> {
241        let mut entries = BTreeMap::<Address, BTreeSet<H256>>::new();
242
243        let mut current = self;
244        loop {
245            for (address, slot_set) in &current.accessed_storage_slots {
246                entries
247                    .entry(*address)
248                    .or_default()
249                    .extend(slot_set.iter().copied());
250            }
251
252            current = match current.parent.as_deref() {
253                Some(x) => x,
254                None => break,
255            };
256        }
257
258        entries
259            .into_iter()
260            .map(|(address, storage_keys)| AccessListEntry {
261                address,
262                storage_keys: storage_keys.into_iter().collect(),
263            })
264            .collect()
265    }
266
267    /// Mark an address as accessed and return whether the slot was cold.
268    pub fn add_accessed_slot(&mut self, address: Address, key: H256) -> bool {
269        if self
270            .accessed_storage_slots
271            .get(&address)
272            .is_some_and(|set| set.contains(&key))
273        {
274            return false;
275        }
276
277        let is_present = self
278            .parent
279            .as_ref()
280            .map(|parent| parent.is_slot_accessed(&address, &key))
281            .unwrap_or_default();
282
283        // Note: Do not simplify this expression, it uses `||` to avoid executing the right hand
284        //   expression if not necessary.
285        #[expect(clippy::nonminimal_bool, reason = "order of evaluation matters")]
286        !(is_present
287            || !self
288                .accessed_storage_slots
289                .entry(address)
290                .or_default()
291                .insert(key))
292    }
293
294    /// Return whether an address has already been accessed.
295    pub fn is_slot_accessed(&self, address: &Address, key: &H256) -> bool {
296        self.accessed_storage_slots
297            .get(address)
298            .map(|slot_set| slot_set.contains(key))
299            .unwrap_or_default()
300            || self
301                .parent
302                .as_ref()
303                .map(|parent| parent.is_slot_accessed(address, key))
304                .unwrap_or_default()
305    }
306
307    /// Returns all accessed storage slots for a given address.
308    /// Used by SELFDESTRUCT to record storage reads in BAL per EIP-7928:
309    /// "SELFDESTRUCT: Include modified/read storage keys as storage_read"
310    pub fn get_accessed_storage_slots(&self, address: &Address) -> BTreeSet<H256> {
311        let mut slots = BTreeSet::new();
312
313        // Collect from current substate
314        if let Some(slot_set) = self.accessed_storage_slots.get(address) {
315            slots.extend(slot_set.iter().copied());
316        }
317
318        // Collect from parent substates recursively
319        if let Some(parent) = self.parent.as_ref() {
320            slots.extend(parent.get_accessed_storage_slots(address));
321        }
322
323        slots
324    }
325
326    /// Mark an address as accessed and return whether the address was cold.
327    pub fn add_accessed_address(&mut self, address: Address) -> bool {
328        // Precompiles are warm from tx start (EIP-2929) without occupying a hashset slot. Returns
329        // `false` (not cold) so cold-access gas is never charged — identical to the old pre-seed.
330        if self.is_warm_precompile(&address) {
331            return false;
332        }
333
334        if self.accessed_addresses.contains(&address) {
335            return false;
336        }
337
338        let is_present = self
339            .parent
340            .as_ref()
341            .map(|parent| parent.is_address_accessed(&address))
342            .unwrap_or_default();
343
344        // Note: Do not simplify this expression, it uses `||` to avoid executing the right hand
345        //   expression if not necessary.
346        #[expect(clippy::nonminimal_bool, reason = "order of evaluation matters")]
347        !(is_present || !self.accessed_addresses.insert(address))
348    }
349
350    /// Return whether an address has already been accessed.
351    pub fn is_address_accessed(&self, address: &Address) -> bool {
352        // Precompiles are always warm; the chain shares one `fork`, so this is consistent across
353        // sub-frame substates.
354        self.is_warm_precompile(address)
355            || self.accessed_addresses.contains(address)
356            || self
357                .parent
358                .as_ref()
359                .map(|parent| parent.is_address_accessed(address))
360                .unwrap_or_default()
361    }
362
363    /// Mark an address as a new account and return whether is was already marked.
364    pub fn add_created_account(&mut self, address: Address) -> bool {
365        if self.created_accounts.contains(&address) {
366            return true;
367        }
368
369        let is_present = self
370            .parent
371            .as_ref()
372            .map(|parent| parent.is_account_created(&address))
373            .unwrap_or_default();
374
375        is_present || !self.created_accounts.insert(address)
376    }
377
378    /// Return whether an address has already been marked as a new account.
379    pub fn is_account_created(&self, address: &Address) -> bool {
380        self.created_accounts.contains(address)
381            || self
382                .parent
383                .as_ref()
384                .map(|parent| parent.is_account_created(address))
385                .unwrap_or_default()
386    }
387
388    /// Return the data associated with a transient storage entry, or zero if not present.
389    pub fn get_transient(&self, to: &Address, key: &U256) -> U256 {
390        self.transient_storage
391            .get(&(*to, *key))
392            .copied()
393            .unwrap_or_else(|| {
394                self.parent
395                    .as_ref()
396                    .map(|parent| parent.get_transient(to, key))
397                    .unwrap_or_default()
398            })
399    }
400
401    /// Return the data associated with a transient storage entry, or zero if not present.
402    pub fn set_transient(&mut self, to: &Address, key: &U256, value: U256) {
403        self.transient_storage.insert((*to, *key), value);
404    }
405
406    /// Extract all logs in order.
407    pub fn extract_logs(&self) -> Vec<Log> {
408        fn inner(substrate: &Substate, target: &mut Vec<Log>) {
409            if let Some(parent) = substrate.parent.as_deref() {
410                inner(parent, target);
411            }
412
413            target.extend_from_slice(&substrate.logs);
414        }
415
416        let mut logs = Vec::new();
417        inner(self, &mut logs);
418
419        logs
420    }
421
422    /// Push a log record.
423    pub fn add_log(&mut self, log: Log) {
424        self.logs.push(log);
425    }
426}
427
428/// The LEVM (Lambda EVM) execution engine.
429///
430/// The VM executes Ethereum transactions by processing EVM bytecode. It maintains
431/// a call stack, memory, and tracks all state changes during execution.
432///
433/// # Execution Model
434///
435/// 1. Transaction is validated (nonce, balance, gas limit)
436/// 2. Initial call frame is created with transaction data
437/// 3. Opcodes are executed sequentially until completion or error
438/// 4. State changes are committed or reverted based on success
439///
440/// # Call Stack
441///
442/// Nested calls (CALL, DELEGATECALL, etc.) push new frames onto `call_frames`.
443/// Each frame has its own memory, stack, and execution context. The `current_call_frame`
444/// is always the active frame being executed.
445///
446/// # Hooks
447///
448/// The VM supports hooks for extending functionality (e.g., tracing, debugging).
449/// Hooks are called at various points during execution and implement pre/post-execution
450/// logic. L2-specific behavior (such as fee handling) is implemented via hooks.
451///
452/// # Example
453///
454/// ```ignore
455/// let mut vm = VM::new(env, db, &tx, tracer, vm_type, &NativeCrypto);
456/// let report = vm.execute()?;
457/// if report.is_success() {
458///     println!("Gas used: {}, Output: {:?}", report.gas_used, report.output);
459/// } else {
460///     println!("Transaction reverted");
461/// }
462/// ```
463pub struct VM<'a> {
464    /// Stack of parent call frames (for nested calls).
465    pub call_frames: Vec<CallFrame>,
466    /// The currently executing call frame.
467    pub current_call_frame: CallFrame,
468    /// Block and transaction environment.
469    pub env: Environment,
470    /// Execution substate (accessed addresses, logs, refunds, etc.).
471    pub substate: Substate,
472    /// Database for reading/writing account state.
473    pub db: &'a mut GeneralizedDatabase,
474    /// The transaction being executed. Borrowed for the VM's lifetime (the caller owns it for at
475    /// least that long), avoiding a per-tx deep clone of the access/authorization lists.
476    pub tx: &'a Transaction,
477    /// Execution hooks for tracing and debugging.
478    pub hooks: Vec<Rc<RefCell<dyn Hook>>>,
479    /// Original storage values before transaction (for SSTORE gas calculation),
480    /// keyed first by account to avoid hashing the full tuple on each access.
481    pub storage_original_values: FxHashMap<Address, FxHashMap<H256, U256>>,
482    /// Call tracer for execution tracing.
483    pub tracer: LevmCallTracer,
484    /// Opcode (EIP-3155) tracer.  Disabled by default; zero overhead when inactive.
485    pub opcode_tracer: LevmOpcodeTracer,
486    /// Debug mode for development diagnostics.
487    pub debug_mode: DebugMode,
488    /// Pool of reusable stacks to reduce allocations.
489    pub stack_pool: Vec<Stack>,
490    /// VM type (L1 or L2 with fee config).
491    pub vm_type: VMType,
492    /// Whether the top-level call-frame backup must be PRESERVED (deep-cloned) on the
493    /// revert / invalid-tx paths because a `BackupHook` will read it in `finalize_execution`
494    /// to build the tx-level undo snapshot. Derived from the installed `hooks` (via
495    /// [`Hook::reads_top_level_backup`]) rather than from `vm_type`, so it stays correct if
496    /// hook wiring changes; `add_hook` keeps it in sync for the `BackupHook` that
497    /// `stateless_execute` installs after construction. False for normal L1 block execution
498    /// (no `BackupHook`), where the backup is dead once the cache is restored and can be moved
499    /// out instead of cloned.
500    pub(crate) preserve_top_level_backup: bool,
501    /// EIP-8037: Accumulated state gas for this transaction (Amsterdam+).
502    /// Signed: goes negative when inline refunds exceed gross charges in the local frame
503    /// (e.g. SSTORE 0→x→0 restoration matching an ancestor's charge).
504    pub state_gas_used: i64,
505    /// EIP-8037: State gas reservoir pre-funded from excess gas_limit (Amsterdam+).
506    pub state_gas_reservoir: u64,
507    /// EIP-8037: Initial reservoir at tx start (before any execution). Captured in
508    /// add_intrinsic_gas so block-dimensional regular gas can be computed
509    /// independently of mid-tx reservoir activity (auth refunds, SSTORE credits).
510    pub state_gas_reservoir_initial: u64,
511    /// EIP-8037: Cumulative state gas that spilled to regular gas during execution
512    /// (when reservoir was insufficient). Subtracted when computing dimensional
513    /// regular gas for block accounting — EELS charge_state_gas spills don't
514    /// increment regular_gas_used.
515    pub state_gas_spill: u64,
516    /// EIP-8037: Dynamic cost per state byte (computed from block_gas_limit, Amsterdam+).
517    pub cost_per_state_byte: u64,
518    /// EIP-8037: State gas for new account creation (STATE_BYTES_PER_NEW_ACCOUNT * cost_per_state_byte).
519    pub state_gas_new_account: u64,
520    /// EIP-8037: State gas for storage slot creation (STATE_BYTES_PER_STORAGE_SET * cost_per_state_byte).
521    pub state_gas_storage_set: u64,
522    /// EIP-8037: State gas for EIP-7702 auth total (STATE_BYTES_PER_AUTH_TOTAL * cost_per_state_byte).
523    pub state_gas_auth_total: u64,
524    /// EIP-8037: State gas for the 23-byte EIP-7702 delegation indicator
525    /// (STATE_BYTES_PER_AUTH_BASE * cost_per_state_byte). Refunded by
526    /// `set_delegation` when no new delegation indicator bytes are written —
527    /// either the authority's code slot already holds an indicator or the
528    /// auth clears against an empty authority.
529    pub state_gas_auth_base: u64,
530    /// EIP-8037: state-gas refund channel.
531    /// Mirrors EELS `MessageCallOutput.state_refund` — a separate, monotonic accumulator
532    /// for refunds that bypass per-frame `state_gas_used` accounting. Populated by
533    /// `set_delegation` for existing-authority refunds, subtracted from block-level
534    /// state-gas at the end of `refund_sender`. Survives revert/halt/OOG since it lives
535    /// on the VM, not in any call-frame backup.
536    pub state_refund: u64,
537    /// EIP-8037: intrinsic state gas (`tx_env.intrinsic_state_gas` in EELS). Captured at
538    /// `add_intrinsic_gas` time. ethrex lumps intrinsic + execution into `state_gas_used`,
539    /// so on top-level error this field is what we leave behind when refunding the
540    /// execution portion to the reservoir — block accounting then bills the intrinsic
541    /// (matches EELS `tx_state_gas = intrinsic_state_gas + tx_output.state_gas_used`).
542    pub intrinsic_state_gas: u64,
543    /// The opcode table mapping opcodes to opcode handlers for fast lookup.
544    /// A shared `&'static` reference to a per-fork table that is `const`-built once for the
545    /// whole process (immutable), so each VM holds only a pointer instead of a 2 KB inline copy.
546    pub(crate) opcode_table: &'static [OpCodeFn; 256],
547    /// Crypto provider for cryptographic operations.
548    pub crypto: &'a dyn Crypto,
549}
550
551impl<'a> VM<'a> {
552    /// Constructs a VM, allocating a fresh 32 KB root call-frame stack.
553    ///
554    /// Hot block execution should prefer [`VM::new_pooled`], which draws the root stack from a
555    /// reusable pool instead of allocating + zeroing one per transaction.
556    pub fn new(
557        env: Environment,
558        db: &'a mut GeneralizedDatabase,
559        tx: &'a Transaction,
560        tracer: LevmCallTracer,
561        vm_type: VMType,
562        crypto: &'a dyn Crypto,
563    ) -> Result<Self, VMError> {
564        Self::new_with_root_stack(
565            env,
566            db,
567            tx,
568            tracer,
569            vm_type,
570            crypto,
571            Stack::default(),
572            Memory::default(),
573        )
574    }
575
576    /// Like [`VM::new`], but draws the root call-frame stack from `stack_pool` (falling back to a
577    /// fresh `Stack::default()` only when the pool is empty) and adopts the remaining pooled
578    /// stacks for sub-call frames. This avoids the per-tx 32 KB stack alloc+zero on a warm pool —
579    /// the dominant allocation for transfer-heavy blocks, where the root frame is the only frame.
580    ///
581    /// Pair with [`VM::reclaim_into`] after execution to return every stack (root + sub-frame)
582    /// to `stack_pool` and the root memory buffer to `memory_pool` so the next tx reuses them.
583    #[allow(clippy::too_many_arguments)]
584    pub fn new_pooled(
585        env: Environment,
586        db: &'a mut GeneralizedDatabase,
587        tx: &'a Transaction,
588        tracer: LevmCallTracer,
589        vm_type: VMType,
590        crypto: &'a dyn Crypto,
591        stack_pool: &mut Vec<Stack>,
592        memory_pool: &mut Vec<Memory>,
593    ) -> Result<Self, VMError> {
594        // Reuse a pooled stack for the root frame. `clear()` only resets the offset (no zeroing),
595        // which is sound because the EVM never reads stack slots it didn't write — the same
596        // invariant that already makes sub-frame pooling safe.
597        let mut root_stack = stack_pool.pop().unwrap_or_default();
598        root_stack.clear();
599        // Reuse a pooled root memory buffer (capacity retained from a prior tx, contents dropped).
600        // `reclaim_into` truncates it to length 0, so `resize`'s zero-fill invariant holds. Only
601        // the root buffer is pooled: sub-frame memories are `Rc` clones of it (`next_memory`).
602        let mut root_memory = memory_pool.pop().unwrap_or_default();
603        root_memory.reset_for_reuse();
604        let mut vm = Self::new_with_root_stack(
605            env,
606            db,
607            tx,
608            tracer,
609            vm_type,
610            crypto,
611            root_stack,
612            root_memory,
613        )?;
614        // Adopt the caller's pooled stacks for sub-frames; returned via `reclaim_into`.
615        mem::swap(&mut vm.stack_pool, stack_pool);
616        Ok(vm)
617    }
618
619    /// Returns this VM's reusable buffers to the caller's pools so the next transaction reuses
620    /// them instead of allocating: every stack (root call-frame stack plus any sub-frame stacks
621    /// still pooled internally) to `stack_pool`, and the root memory buffer to `memory_pool`.
622    /// Must run on both the success and error paths of [`VM::execute`].
623    pub fn reclaim_into(mut self, stack_pool: &mut Vec<Stack>, memory_pool: &mut Vec<Memory>) {
624        // Hand the internal sub-frame pool back to the caller first.
625        mem::swap(&mut self.stack_pool, stack_pool);
626        // Then reclaim the root frame's stack. Moving it out by value (VM/CallFrame have no Drop)
627        // avoids leaving a fresh 32 KB `Stack::default()` placeholder behind — which a
628        // `mem::take`/`mem::replace` against an empty pool would force, defeating the win on
629        // exactly the transfer-only blocks (no sub-frames ever seed the pool) we target.
630        let mut root_stack = self.current_call_frame.stack;
631        root_stack.clear();
632        stack_pool.push(root_stack);
633        // Reclaim the root memory buffer with its grown capacity. `reset_for_reuse` truncates it
634        // to length 0 (capacity kept) so the next tx's `resize` zero-fills correctly.
635        //
636        // Every call frame shares the same `Rc<RefCell<Vec<u8>>>` buffer, so on the error path the
637        // ancestor frames left in `call_frames` (error propagation unwinds out of `execute` without
638        // popping them) still hold clones. Drop them first so the buffer is `Rc`-unique on BOTH
639        // paths before we clear it — otherwise the clear would propagate to a frame still holding a
640        // reference. `CallFrame` has no `Drop` and these frames are never read again, so dropping
641        // them early is free.
642        self.call_frames.clear();
643        let mut root_memory = self.current_call_frame.memory;
644        debug_assert_eq!(
645            Rc::strong_count(&root_memory.buffer),
646            1,
647            "root memory buffer must be Rc-unique at reclaim; a frame is still holding it and \
648             would observe the reset_for_reuse clear",
649        );
650        root_memory.reset_for_reuse();
651        memory_pool.push(root_memory);
652    }
653
654    #[allow(clippy::too_many_arguments)]
655    fn new_with_root_stack(
656        env: Environment,
657        db: &'a mut GeneralizedDatabase,
658        tx: &'a Transaction,
659        tracer: LevmCallTracer,
660        vm_type: VMType,
661        crypto: &'a dyn Crypto,
662        root_stack: Stack,
663        root_memory: Memory,
664    ) -> Result<Self, VMError> {
665        db.tx_backup = None; // If BackupHook is enabled, it will contain backup at the end of tx execution.
666
667        let mut substate = Substate::initialize(&env, tx)?;
668
669        let (callee, is_create) = Self::get_tx_callee(tx, db, &env, &mut substate)?;
670
671        let fork = env.config.fork;
672
673        #[expect(
674            clippy::arithmetic_side_effects,
675            reason = "byte-count constants are small (<200) and cpsb is bounded by block_gas_limit/year formula"
676        )]
677        let (
678            cpsb,
679            state_gas_new_account,
680            state_gas_storage_set,
681            state_gas_auth_total,
682            state_gas_auth_base,
683        ) = if fork >= Fork::Amsterdam {
684            let cpsb = compute_cost_per_state_byte(env.block_gas_limit);
685            (
686                cpsb,
687                STATE_BYTES_PER_NEW_ACCOUNT * cpsb,
688                STATE_BYTES_PER_STORAGE_SET * cpsb,
689                STATE_BYTES_PER_AUTH_TOTAL * cpsb,
690                STATE_BYTES_PER_AUTH_BASE * cpsb,
691            )
692        } else {
693            (0, 0, 0, 0, 0)
694        };
695
696        // Derive whether the top-level backup must be preserved from the installed hooks rather
697        // than from `vm_type`. The flag's real meaning is "a hook reads the top-level backup in
698        // `finalize_execution`," which today is the `BackupHook` on L2 / stateless. Deriving it
699        // keeps the flag correct if hook wiring ever changes (e.g. a future `vm_type` that adds
700        // `BackupHook`, or L2 dropping it), and `add_hook` keeps it in sync for the `BackupHook`
701        // that `stateless_execute` installs after construction. L1 block execution installs no
702        // `BackupHook` (see `l1_hooks`), so the backup is dead once the cache is restored.
703        let hooks = get_hooks(&vm_type);
704        let preserve_top_level_backup = hooks
705            .iter()
706            .any(|hook| hook.borrow().reads_top_level_backup());
707
708        let mut vm = Self {
709            call_frames: Vec::new(),
710            substate,
711            db,
712            tx,
713            hooks,
714            storage_original_values: FxHashMap::default(),
715            tracer,
716            opcode_tracer: LevmOpcodeTracer::disabled(),
717            debug_mode: DebugMode::disabled(),
718            stack_pool: Vec::new(),
719            vm_type,
720            preserve_top_level_backup,
721            state_gas_used: 0,
722            state_gas_reservoir: 0,
723            state_gas_reservoir_initial: 0,
724            state_gas_spill: 0,
725            cost_per_state_byte: cpsb,
726            state_gas_new_account,
727            state_gas_storage_set,
728            state_gas_auth_total,
729            state_gas_auth_base,
730            state_refund: 0,
731            intrinsic_state_gas: 0,
732            current_call_frame: CallFrame::new(
733                env.origin,
734                callee,
735                Address::default(), // Will be assigned at the end of prepare_execution
736                Code::default(),    // Will be assigned at the end of prepare_execution
737                tx.value(),
738                tx.data().clone(),
739                false,
740                env.gas_limit,
741                0,
742                true,
743                is_create,
744                0,
745                0,
746                root_stack,
747                root_memory,
748            ),
749            env,
750            opcode_table: VM::build_opcode_table(fork),
751            crypto,
752        };
753
754        let call_type = if is_create {
755            CallType::CREATE
756        } else {
757            CallType::CALL
758        };
759        vm.tracer.enter(
760            call_type,
761            vm.env.origin,
762            callee,
763            vm.tx.value(),
764            vm.env.gas_limit,
765            vm.tx.data(),
766        );
767
768        #[cfg(feature = "debug")]
769        {
770            // Enable debug mode for printing in Solidity contracts.
771            vm.debug_mode.enabled = true;
772        }
773
774        Ok(vm)
775    }
776
777    fn add_hook(&mut self, hook: impl Hook + 'static) {
778        // Keep `preserve_top_level_backup` in sync: a hook added after construction (e.g. the
779        // `BackupHook` in `stateless_execute`) may read the top-level backup in `finalize_execution`.
780        self.preserve_top_level_backup |= hook.reads_top_level_backup();
781        self.hooks.push(Rc::new(RefCell::new(hook)));
782    }
783
784    /// EIP-8037: Charge state gas, drawing from reservoir first, spilling to gas_remaining if exhausted.
785    ///
786    /// Must only be called for Amsterdam+ forks. All call sites must guard with
787    /// `fork >= Fork::Amsterdam` before invoking this method.
788    #[expect(
789        clippy::arithmetic_side_effects,
790        reason = "arithmetic proven safe by min()"
791    )]
792    pub fn increase_state_gas(&mut self, gas: u64) -> Result<(), VMError> {
793        debug_assert!(
794            self.env.config.fork >= Fork::Amsterdam,
795            "increase_state_gas called pre-Amsterdam"
796        );
797        // Draw from reservoir first; only spill to gas_remaining if reservoir exhausted
798        let from_reservoir = self.state_gas_reservoir.min(gas);
799        // Safe: from_reservoir <= gas
800        let spill = gas - from_reservoir;
801        if spill > 0 {
802            // Charge spill from gas_remaining first — if OOG, return early
803            // without mutating reservoir or state_gas_used (matches EELS behavior)
804            self.current_call_frame.increase_consumed_gas(spill)?;
805        }
806        // Safe: from_reservoir = min(reservoir, gas) so reservoir >= from_reservoir
807        self.state_gas_reservoir -= from_reservoir;
808        // Only increment state_gas_used AFTER the charge succeeds.
809        // state_gas_used is i64; tx gas_limit caps charges well below i64::MAX.
810        self.state_gas_used = self
811            .state_gas_used
812            .checked_add(i64::try_from(gas).map_err(|_| InternalError::Overflow)?)
813            .ok_or(InternalError::Overflow)?;
814        // Track the spill for block-accounting: EELS charge_state_gas spills
815        // don't count toward regular_gas_used for the regular dimension.
816        self.state_gas_spill = self
817            .state_gas_spill
818            .checked_add(spill)
819            .ok_or(InternalError::Overflow)?;
820        Ok(())
821    }
822
823    /// EIP-8037: credit `amount` directly to the local frame's reservoir; `state_gas_used`
824    /// may go negative when the matching charge lives in an ancestor frame.
825    ///
826    /// Must only be called for Amsterdam+ forks.
827    pub fn credit_state_gas_refund(&mut self, amount: u64) -> Result<(), VMError> {
828        debug_assert!(
829            self.env.config.fork >= Fork::Amsterdam,
830            "credit_state_gas_refund called pre-Amsterdam"
831        );
832        self.state_gas_reservoir = self
833            .state_gas_reservoir
834            .checked_add(amount)
835            .ok_or(InternalError::Overflow)?;
836        self.state_gas_used = self
837            .state_gas_used
838            .checked_sub(i64::try_from(amount).map_err(|_| InternalError::Overflow)?)
839            .ok_or(InternalError::Overflow)?;
840        Ok(())
841    }
842
843    /// EIP-8037 `incorporate_child_on_error`: on child revert, restore the parent's
844    /// `state_gas_used` to its pre-child value and refund the child's net
845    /// `(state_gas_used + state_gas_left)` back into the parent's reservoir.
846    ///
847    /// In ethrex's shared-VM model the child holds the entire reservoir during its
848    /// execution, so `child.state_gas_left == self.state_gas_reservoir` (absolute,
849    /// not a delta against entry). `child.state_gas_used` can be negative when
850    /// inline refunds inside the child exceeded its gross charges.
851    pub fn incorporate_child_state_gas_on_revert(
852        &mut self,
853        state_gas_used_at_entry: i64,
854    ) -> Result<(), VMError> {
855        let child_state_gas_used = self
856            .state_gas_used
857            .checked_sub(state_gas_used_at_entry)
858            .ok_or(InternalError::Overflow)?;
859        let child_state_gas_left =
860            i64::try_from(self.state_gas_reservoir).map_err(|_| InternalError::Overflow)?;
861        self.state_gas_used = state_gas_used_at_entry;
862        let net_return = child_state_gas_used
863            .checked_add(child_state_gas_left)
864            .ok_or(InternalError::Overflow)?;
865        // net_return is always >= 0 by the spec invariant (reservoir conservation
866        // means a child cannot refund more than its ancestors charged); clamp
867        // defensively and cast — `as u64` is sound because of the `.max(0)`.
868        #[expect(clippy::as_conversions, reason = ".max(0) proves non-negativity")]
869        {
870            self.state_gas_reservoir = net_return.max(0) as u64;
871        }
872        Ok(())
873    }
874
875    /// Executes a whole external transaction. Performing validations at the beginning.
876    pub fn execute(&mut self) -> Result<ExecutionReport, VMError> {
877        if let Err(e) = self.prepare_execution() {
878            // Restore cache to state previous to this Tx execution because this Tx is invalid.
879            // Consume the backup unless a `BackupHook` will read it (L2 / stateless); on L1 it
880            // is dead once the cache is restored.
881            if self.preserve_top_level_backup {
882                self.restore_cache_state()?;
883            } else {
884                self.restore_cache_state_consuming()?;
885            }
886            return Err(e);
887        }
888
889        // Clear callframe backup so that changes made in prepare_execution are written in stone.
890        // We want to apply these changes even if the Tx reverts. E.g. Incrementing sender nonce
891        self.current_call_frame.call_frame_backup.clear();
892
893        // Empty bytecode would only execute STOP; skip the dispatch loop.
894        // The BAL checkpoint below is intentionally skipped: a codeless transfer cannot
895        // fail past this point and has no inner calls, so there's nothing to roll back.
896        if self.is_simple_transfer_fast_path() {
897            #[expect(clippy::as_conversions, reason = "gas_remaining is non-negative here")]
898            let gas_used = self
899                .current_call_frame
900                .gas_limit
901                .checked_sub(self.current_call_frame.gas_remaining as u64)
902                .ok_or(InternalError::Underflow)?;
903            let context_result = ContextResult {
904                result: TxResult::Success,
905                gas_used,
906                gas_spent: gas_used,
907                output: Bytes::new(),
908            };
909            return self.finalize_execution(context_result);
910        }
911
912        // EIP-7928: Take a BAL checkpoint AFTER clearing the backup. This captures the state
913        // after prepare_execution (nonce increment, etc.) but before actual execution.
914        // When the top-level call fails, we restore to this checkpoint so that inner call
915        // state changes (like value transfers) are reverted from the BAL.
916        self.current_call_frame.call_frame_backup.bal_checkpoint =
917            self.db.bal_recorder.as_ref().map(|r| r.checkpoint());
918
919        if self.is_create()? {
920            // Create contract, reverting the Tx if address is already occupied.
921            if let Some(context_result) = self.handle_create_transaction()? {
922                let report = self.finalize_execution(context_result)?;
923                return Ok(report);
924            }
925        }
926
927        self.substate.push_backup();
928        let context_result = self.run_execution()?;
929
930        let report = self.finalize_execution(context_result)?;
931
932        Ok(report)
933    }
934
935    /// Must run after `prepare_execution` so EIP-7702 delegation is already resolved into
936    /// `bytecode`.
937    #[inline(always)]
938    fn is_simple_transfer_fast_path(&self) -> bool {
939        !self.current_call_frame.is_create
940            && self.current_call_frame.bytecode.is_empty()
941            // Privileged L2 txs can leave gas negative; let the slow path surface that as OOG.
942            && self.current_call_frame.gas_remaining >= 0
943            && self.tx.authorization_list().is_none()
944            // Precompiles dispatch via run_execution even with empty bytecode.
945            && !precompiles::is_precompile(
946                &self.current_call_frame.to,
947                self.env.config.fork,
948                self.vm_type,
949            )
950    }
951
952    /// Main execution loop.
953    pub fn run_execution(&mut self) -> Result<ContextResult, VMError> {
954        // If gas is already exhausted (negative), fail immediately.
955        // This can happen when intrinsic gas exceeds the gas limit in privileged L2 transactions.
956        // Without this check, casting negative gas_remaining to u64 would wrap to a huge value.
957        if self.current_call_frame.gas_remaining < 0 {
958            return Ok(ContextResult {
959                result: TxResult::Revert(ExceptionalHalt::OutOfGas.into()),
960                gas_used: self.current_call_frame.gas_limit,
961                gas_spent: self.current_call_frame.gas_limit,
962                output: Bytes::new(),
963            });
964        }
965
966        #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
967        if precompiles::is_precompile(
968            &self.current_call_frame.to,
969            self.env.config.fork,
970            self.vm_type,
971        ) {
972            let call_frame = &mut self.current_call_frame;
973
974            let mut gas_remaining = call_frame.gas_remaining as u64;
975            let result = Self::execute_precompile(
976                call_frame.code_address,
977                &call_frame.calldata,
978                call_frame.gas_limit,
979                &mut gas_remaining,
980                self.env.config.fork,
981                self.db.store.precompile_cache(),
982                self.crypto,
983            );
984
985            // EIP-8037 Amsterdam 2D accounting recomputes `block_gas_used` from
986            // `raw_consumed = gas_limit - gas_remaining` inside `refund_sender`. On a
987            // top-level precompile exceptional halt, `handle_precompile_result` already
988            // sets `ContextResult.gas_used = gas_limit`, but `gas_remaining` retains the
989            // untouched forwarded amount — under Amsterdam that would make the block
990            // report only the intrinsic portion. Zero it here so the block matches the
991            // `gas_used = gas_limit` contract from `handle_precompile_result`. Pre-Amsterdam
992            // reads `ctx_result.gas_used` directly and is unaffected by this path either way.
993            if self.env.config.fork >= Fork::Amsterdam
994                && let Ok(ctx) = &result
995                && !ctx.is_success()
996            {
997                gas_remaining = 0;
998            }
999
1000            call_frame.gas_remaining = gas_remaining as i64;
1001
1002            return result;
1003        }
1004
1005        // Specialize the dispatch loop on whether a struct-log tracer is active.
1006        // The `!TRACED` variant compiles out every tracer branch and capture call,
1007        // leaving a minimal hot loop (the common, non-traced case).
1008        if self.opcode_tracer.active {
1009            self.run_dispatch::<true>()
1010        } else {
1011            self.run_dispatch::<false>()
1012        }
1013    }
1014
1015    /// Opcode dispatch loop, monomorphized over whether a struct-log tracer is
1016    /// active. With `TRACED = false` the compiler eliminates the tracer branches
1017    /// and the cold `trace_*_step` calls entirely, so the hot loop body stays
1018    /// minimal; the traced variant keeps the cold helpers out of line.
1019    fn run_dispatch<const TRACED: bool>(&mut self) -> Result<ContextResult, VMError> {
1020        let mut error = OnceCell::<VMError>::new();
1021
1022        #[cfg(feature = "perf_opcode_timings")]
1023        let mut timings = crate::timings::OPCODE_TIMINGS.lock().expect("poison");
1024
1025        // Copy the `&'static` table pointer once; it doesn't borrow `self`, so dispatch can still
1026        // pass `self` mutably to the handler without reloading the pointer each iteration.
1027        let opcode_table = self.opcode_table;
1028
1029        loop {
1030            // Capture pc BEFORE advance_pc() — this is the address of the current opcode.
1031            let pc_of_current_op = self.current_call_frame.pc;
1032            let opcode = self.current_call_frame.next_opcode();
1033            self.advance_pc();
1034
1035            // Struct-log pre-step capture (compiled out entirely when !TRACED).
1036            let gas_before_op = if TRACED {
1037                self.trace_pre_step(opcode, pc_of_current_op)
1038            } else {
1039                0
1040            };
1041
1042            #[cfg(feature = "perf_opcode_timings")]
1043            let opcode_time_start = std::time::Instant::now();
1044
1045            #[allow(clippy::indexing_slicing, clippy::as_conversions)]
1046            let op_result = opcode_table[opcode as usize].call(self, &mut error);
1047
1048            #[cfg(feature = "perf_opcode_timings")]
1049            {
1050                let time = opcode_time_start.elapsed();
1051                timings.update(opcode, time);
1052            }
1053
1054            // Struct-log post-step (compiled out entirely when !TRACED).
1055            if TRACED {
1056                self.trace_post_step(gas_before_op, &error);
1057            }
1058
1059            let result = match op_result {
1060                OpcodeResult::Continue => continue,
1061                OpcodeResult::Halt => match error.take() {
1062                    None => self.handle_opcode_result()?,
1063                    Some(error) => self.handle_opcode_error(error)?,
1064                },
1065            };
1066
1067            // Return the ExecutionReport if the executed callframe was the first one.
1068            if self.is_initial_call_frame() {
1069                // Consume the backup (move it out) unless a `BackupHook` will read it afterward
1070                // to build the tx-level undo snapshot (L2 / stateless). On L1 nothing reads it
1071                // once the cache is restored, so cloning it would be dead work.
1072                self.handle_state_backup(&result, !self.preserve_top_level_backup)?;
1073                return Ok(result);
1074            }
1075
1076            // Handle interaction between child and parent callframe.
1077            self.handle_return(&result)?;
1078        }
1079    }
1080
1081    /// Struct-log pre-step capture, split out of the interpreter loop and kept
1082    /// cold + non-inlined so the hot dispatch loop stays small (this code is
1083    /// only reached when a struct-log tracer is active). Returns `gas_before`.
1084    #[cold]
1085    #[inline(never)]
1086    fn trace_pre_step(&mut self, opcode: u8, pc_of_current_op: usize) -> u64 {
1087        #[expect(
1088            clippy::as_conversions,
1089            reason = "gas_remaining is i64; clamp to 0 before converting to u64"
1090        )]
1091        let gas_before = self.current_call_frame.gas_remaining.max(0) as u64;
1092        #[expect(
1093            clippy::as_conversions,
1094            reason = "call depth bounded by STACK_LIMIT=1024, fits in u32"
1095        )]
1096        let depth = (self.call_frames.len() as u32).saturating_add(1);
1097        let refund = self.substate.refunded_gas;
1098        let stack_view = self.collect_stack_for_trace();
1099        let mem_view = self.collect_memory_for_trace();
1100        // mem_size always reflects actual memory size, regardless of enable_memory.
1101        #[expect(
1102            clippy::as_conversions,
1103            reason = "memory size is bounded by gas; fits in u64"
1104        )]
1105        let mem_size_for_trace = self.current_call_frame.memory.len() as u64;
1106        let storage_kv = self.read_storage_for_trace(opcode);
1107        let return_data = if self.opcode_tracer.cfg.enable_return_data {
1108            self.current_call_frame.sub_return_data.clone()
1109        } else {
1110            Bytes::new()
1111        };
1112        #[expect(
1113            clippy::as_conversions,
1114            reason = "pc is usize, fits in u64 on supported targets"
1115        )]
1116        let pc_u64 = pc_of_current_op as u64;
1117        self.opcode_tracer.pre_step_capture(
1118            pc_u64,
1119            opcode,
1120            gas_before,
1121            depth,
1122            refund,
1123            &stack_view,
1124            &mem_view,
1125            mem_size_for_trace,
1126            &return_data,
1127            storage_kv,
1128        );
1129        gas_before
1130    }
1131
1132    /// Struct-log post-step: patch gas_cost, refund-after-op, and error into the
1133    /// buffered entry. Cold + non-inlined for the same reason as `trace_pre_step`.
1134    #[cold]
1135    #[inline(never)]
1136    fn trace_post_step(&mut self, gas_before_op: u64, error: &OnceCell<VMError>) {
1137        #[expect(
1138            clippy::as_conversions,
1139            reason = "gas_remaining is i64; clamp to 0 before converting to u64"
1140        )]
1141        let gas_after = self.current_call_frame.gas_remaining.max(0) as u64;
1142        // Prefer the explicit opcode-overhead cost written by CALL/CREATE handlers;
1143        // fall back to the gas diff for all other opcodes.
1144        let gas_cost = self
1145            .opcode_tracer
1146            .last_opcode_gas_cost
1147            .take()
1148            .unwrap_or_else(|| gas_before_op.saturating_sub(gas_after));
1149        // refund-after-op matches geth's structLogger timing: for SSTORE and
1150        // (pre-London) SELFDESTRUCT, the refund counter shown is the value
1151        // *after* the opcode's accounting applied.
1152        let refund_after = self.substate.refunded_gas;
1153        let err_str = error.get().map(|e| e.to_string());
1154        self.opcode_tracer
1155            .finalize_step(gas_cost, refund_after, err_str.as_deref());
1156    }
1157
1158    /// Executes precompile and handles the output that it returns, generating a report.
1159    pub fn execute_precompile(
1160        code_address: H160,
1161        calldata: &Bytes,
1162        gas_limit: u64,
1163        gas_remaining: &mut u64,
1164        fork: Fork,
1165        cache: Option<&precompiles::PrecompileCache>,
1166        crypto: &dyn Crypto,
1167    ) -> Result<ContextResult, VMError> {
1168        Self::handle_precompile_result(
1169            precompiles::execute_precompile(
1170                code_address,
1171                calldata,
1172                gas_remaining,
1173                fork,
1174                cache,
1175                crypto,
1176            ),
1177            gas_limit,
1178            *gas_remaining,
1179        )
1180    }
1181
1182    /// True if external transaction is a contract creation
1183    pub fn is_create(&self) -> Result<bool, InternalError> {
1184        Ok(self.current_call_frame.is_create)
1185    }
1186
1187    /// Executes without making changes to the cache.
1188    pub fn stateless_execute(&mut self) -> Result<ExecutionReport, VMError> {
1189        // Add backup hook to restore state after execution. `add_hook` flips
1190        // `preserve_top_level_backup` on via `Hook::reads_top_level_backup`, so the backup is
1191        // cloned (not moved out) on the revert paths even though this VM was built with L1 `vm_type`.
1192        self.add_hook(BackupHook::default());
1193        let report = self.execute()?;
1194        // Restore cache to the state before execution.
1195        self.db.undo_last_transaction()?;
1196        Ok(report)
1197    }
1198
1199    fn prepare_execution(&mut self) -> Result<(), VMError> {
1200        // Clone each hook's `Rc` (cheap refcount bump) so the borrow on `self.hooks` is released
1201        // and `self` can be passed mutably — without `self.hooks.clone()`'s per-tx `Vec` realloc.
1202        // `self.hooks` is not mutated during the loop, so `get(i)` is always `Some` in range.
1203        for i in 0..self.hooks.len() {
1204            if let Some(hook) = self.hooks.get(i).map(Rc::clone) {
1205                hook.borrow_mut().prepare_execution(self)?;
1206            }
1207        }
1208
1209        Ok(())
1210    }
1211
1212    fn finalize_execution(
1213        &mut self,
1214        mut ctx_result: ContextResult,
1215    ) -> Result<ExecutionReport, VMError> {
1216        // EIP-8037: On top-level tx failure (REVERT, ExceptionalHalt, or OOG),
1217        // refund only the EXECUTION portion of state gas to the reservoir; the intrinsic
1218        // stays in `state_gas_used` so block accounting bills it. EELS keeps these in
1219        // separate fields (`tx_output.state_gas_used` vs `tx_env.intrinsic_state_gas`);
1220        // ethrex lumps them so we split on the way out:
1221        //   tx_output.state_gas_left += tx_output.state_gas_used
1222        //   tx_output.state_gas_used  = 0
1223        // becomes in lumped form (with intrinsic preserved):
1224        //   reservoir   += signed(state_gas_used − intrinsic)   [clamped at 0]
1225        //   state_gas_used = intrinsic
1226        // Collision is handled separately in the hook.
1227        if self.env.config.fork >= Fork::Amsterdam && !ctx_result.is_success() {
1228            if !ctx_result.is_collision() {
1229                let intrinsic_signed =
1230                    i64::try_from(self.intrinsic_state_gas).map_err(|_| InternalError::Overflow)?;
1231                let execution_state_gas_used = self.state_gas_used.saturating_sub(intrinsic_signed);
1232                let reservoir_signed = i64::try_from(self.state_gas_reservoir)
1233                    .map_err(|_| InternalError::Overflow)?
1234                    .saturating_add(execution_state_gas_used);
1235                self.state_gas_reservoir =
1236                    u64::try_from(reservoir_signed.max(0)).map_err(|_| InternalError::Overflow)?;
1237                self.state_gas_used = intrinsic_signed;
1238            }
1239
1240            // EIP-8037: on ANY top-level CREATE-tx
1241            // failure (revert / halt / OOG / collision), refund the intrinsic
1242            // `STATE_BYTES_PER_NEW_ACCOUNT * cost_per_state_byte` charge to the reservoir.
1243            // Also add to `state_refund` so block-level accounting subtracts it.
1244            // EELS reference: fork.py::process_transaction:
1245            //   if isinstance(tx.to, Bytes0):
1246            //       new_account_refund = STATE_BYTES_PER_NEW_ACCOUNT * COST_PER_STATE_BYTE
1247            //       tx_output.state_gas_left += new_account_refund
1248            //       tx_output.state_refund   += new_account_refund
1249            if self.is_create()? {
1250                let new_account_refund = self.state_gas_new_account;
1251                self.state_gas_reservoir = self
1252                    .state_gas_reservoir
1253                    .checked_add(new_account_refund)
1254                    .ok_or(InternalError::Overflow)?;
1255                self.state_refund = self
1256                    .state_refund
1257                    .checked_add(new_account_refund)
1258                    .ok_or(InternalError::Overflow)?;
1259            }
1260        }
1261
1262        // See `prepare_execution`: per-hook `Rc::clone` avoids the `self.hooks.clone()` realloc.
1263        for i in 0..self.hooks.len() {
1264            if let Some(hook) = self.hooks.get(i).map(Rc::clone) {
1265                hook.borrow_mut()
1266                    .finalize_execution(self, &mut ctx_result)?;
1267            }
1268        }
1269
1270        self.tracer.exit_context(&ctx_result, true)?;
1271
1272        // Struct-log end-of-tx capture: record final output, gas used, and revert error.
1273        // gas matches geth's `executionResult.Gas` which is post-refund (`receipt.GasUsed`).
1274        if self.opcode_tracer.active {
1275            self.opcode_tracer.output = ctx_result.output.clone();
1276            self.opcode_tracer.gas_used = ctx_result.gas_spent;
1277            self.opcode_tracer.error = match ctx_result.result {
1278                TxResult::Revert(ref err) => Some(err.to_string()),
1279                _ => None,
1280            };
1281        }
1282
1283        // Only include logs if transaction succeeded. When a transaction reverts,
1284        // no logs should be emitted (including EIP-7708 Transfer logs).
1285        let logs = if ctx_result.is_success() {
1286            self.substate.extract_logs()
1287        } else {
1288            Vec::new()
1289        };
1290
1291        // EIP-8037: `state_gas_used` is already net (signed; credits
1292        // decrement it inline). Subtract `state_refund` (EIP-7702 tx-level channel) and
1293        // clamp at zero for block accounting — `state_gas_used` may be negative when inline
1294        // refunds exceed gross charges.
1295        let state_refund_signed =
1296            i64::try_from(self.state_refund).map_err(|_| InternalError::Overflow)?;
1297        let net_state_gas_used: u64 = u64::try_from(
1298            self.state_gas_used
1299                .saturating_sub(state_refund_signed)
1300                .max(0),
1301        )
1302        .map_err(|_| InternalError::Overflow)?;
1303
1304        let report = ExecutionReport {
1305            result: ctx_result.result.clone(),
1306            gas_used: ctx_result.gas_used,
1307            gas_spent: ctx_result.gas_spent,
1308            gas_refunded: self.substate.refunded_gas,
1309            state_gas_used: net_state_gas_used,
1310            output: std::mem::take(&mut ctx_result.output),
1311            logs,
1312        };
1313
1314        Ok(report)
1315    }
1316
1317    // ── Struct-log helper methods ─────────────────────────────────────────────
1318
1319    /// Collects the current stack in bottom-first order for struct-log emission.
1320    ///
1321    /// LEVM stack is top-first in memory (`values[offset]` = top), so we reverse
1322    /// the active slice to produce the bottom-first wire format geth uses.
1323    /// Returns an empty `Vec` when `cfg.disable_stack` is true.
1324    pub fn collect_stack_for_trace(&self) -> Vec<U256> {
1325        use crate::constants::STACK_LIMIT;
1326        if self.opcode_tracer.cfg.disable_stack {
1327            return Vec::new();
1328        }
1329        let s = &self.current_call_frame.stack;
1330        // offset <= STACK_LIMIT by stack invariant.
1331        s.values
1332            .get(s.offset..STACK_LIMIT)
1333            .map(|slice| slice.iter().rev().copied().collect())
1334            .unwrap_or_default()
1335    }
1336
1337    /// Collects the live memory bytes for the current frame.
1338    ///
1339    /// Returns an empty `Vec` when `cfg.enable_memory` is false or memory is empty.
1340    pub fn collect_memory_for_trace(&self) -> Vec<u8> {
1341        if !self.opcode_tracer.cfg.enable_memory {
1342            return Vec::new();
1343        }
1344        self.current_call_frame.memory.live_bytes()
1345    }
1346
1347    /// Pre-reads the storage key/value for the current SLOAD or SSTORE opcode.
1348    ///
1349    /// Returns `None` when:
1350    /// - `cfg.disable_storage` is set, or
1351    /// - `opcode` is not SLOAD (0x54) or SSTORE (0x55), or
1352    /// - the stack is empty (guard against underflow before the handler runs), or
1353    /// - the storage read fails for any reason (including `AccountNotFound` —
1354    ///   the trace omits the entry rather than emitting an ambiguous zero).
1355    ///
1356    /// For SLOAD: key = `stack.top`; value = the *current* stored value read from the DB.
1357    /// For SSTORE: key = `stack.top`, value = `stack[top-1]` (the new value being written).
1358    pub fn read_storage_for_trace(&mut self, opcode: u8) -> Option<(H256, H256)> {
1359        const SLOAD: u8 = 0x54;
1360        const SSTORE: u8 = 0x55;
1361
1362        if self.opcode_tracer.cfg.disable_storage {
1363            return None;
1364        }
1365        if opcode != SLOAD && opcode != SSTORE {
1366            return None;
1367        }
1368
1369        // Need at least one element on stack for SLOAD, two for SSTORE.
1370        use crate::constants::STACK_LIMIT;
1371        let offset = self.current_call_frame.stack.offset;
1372        if offset >= STACK_LIMIT {
1373            return None; // stack empty
1374        }
1375
1376        // SLOAD/SSTORE operate on the call's storage context (`to`), not the code's
1377        // address. Under DELEGATECALL/CALLCODE these differ.
1378        let addr = self.current_call_frame.to;
1379
1380        let stack_values = &self.current_call_frame.stack.values;
1381        let key_u256 = *stack_values.get(offset)?;
1382        let key = BigEndianHash::from_uint(&key_u256);
1383
1384        if opcode == SLOAD {
1385            // Omit the entry on any read failure (incl. account not yet cached);
1386            // a zero value would be indistinguishable from a legitimate never-written slot.
1387            let v = self.get_storage_value(addr, key).ok()?;
1388            let value = BigEndianHash::from_uint(&v);
1389            Some((key, value))
1390        } else {
1391            // SSTORE: need two stack elements.
1392            let next_offset = offset.checked_add(1)?;
1393            if next_offset >= STACK_LIMIT {
1394                return None;
1395            }
1396            // values[offset+1] is the new value being written (second from top = stack[top-1]).
1397            let value_u256 = *self.current_call_frame.stack.values.get(next_offset)?;
1398            let value = BigEndianHash::from_uint(&value_u256);
1399            Some((key, value))
1400        }
1401    }
1402}
1403
1404impl Substate {
1405    /// Initializes the VM substate, mainly adding addresses to the "accessed_addresses" field and the same with storage slots
1406    pub fn initialize(env: &Environment, tx: &Transaction) -> Result<Substate, VMError> {
1407        let fork = env.config.fork;
1408
1409        // Add sender and recipient to accessed accounts [https://www.evm.codes/about#access_list]
1410        // Precompiles are NO LONGER inserted here — they are warm by construction (see
1411        // `is_warm_precompile`), removing the ~20-entry floor that used to dominate this set. The
1412        // remaining working set is small (sender + coinbase + recipient + access-list/touched
1413        // addresses; real p99 ~7), so a capacity of 8 covers most txs with little waste.
1414        let mut initial_accessed_addresses =
1415            FxHashSet::with_capacity_and_hasher(8, Default::default());
1416        // Storage slots are ~98% empty (p95 0, p99 4), so `default()` (alloc-free until first
1417        // insert) beats pre-sizing, which would tax the common empty case.
1418        let mut initial_accessed_storage_slots: FxHashMap<Address, FxHashSet<H256>> =
1419            FxHashMap::default();
1420
1421        // Add Tx sender to accessed accounts
1422        initial_accessed_addresses.insert(env.origin);
1423
1424        // [EIP-3651] - Add coinbase to accessed accounts after Shanghai
1425        if fork >= Fork::Shanghai {
1426            initial_accessed_addresses.insert(env.coinbase);
1427        }
1428
1429        // Add access lists contents to accessed accounts and accessed storage slots.
1430        // Iterate by reference (`Address`/`H256` are `Copy`); the old `.clone()` deep-copied
1431        // the whole `Vec<(Address, Vec<H256>)>` per tx just to read it.
1432        for (address, keys) in tx.access_list() {
1433            initial_accessed_addresses.insert(*address);
1434            // Access lists can have different entries even for the same address, that's why we check if there's an existing set instead of considering it empty
1435            let warm_slots = initial_accessed_storage_slots.entry(*address).or_default();
1436            for slot in keys {
1437                warm_slots.insert(*slot);
1438            }
1439        }
1440
1441        let substate = Substate::from_accesses(
1442            fork,
1443            initial_accessed_addresses,
1444            initial_accessed_storage_slots,
1445        );
1446
1447        Ok(substate)
1448    }
1449}