Skip to main content

ethrex_levm/
call_frame.rs

1use crate::{
2    account::LevmAccount,
3    constants::STACK_LIMIT,
4    errors::{ExceptionalHalt, InternalError, VMError},
5    memory::Memory,
6    utils::restore_cache_state,
7    vm::VM,
8};
9use bytes::Bytes;
10use ethrex_common::types::block_access_list::BlockAccessListCheckpoint;
11use ethrex_common::{Address, H256, U256, types::Code};
12use rustc_hash::FxHashMap;
13use std::{
14    fmt,
15    hash::{Hash, Hasher},
16    hint::assert_unchecked,
17};
18
19/// [`u64`]s that make up a [`U256`]
20const U64_PER_U256: usize = U256::MAX.0.len();
21
22#[derive(Clone, PartialEq, Eq)]
23/// The EVM uses a stack-based architecture and does not use registers like some other VMs.
24///
25/// The specification says the stack is limited to 1024 items, aka. 32KiB, which is reasonable
26/// enough for allocating it all at once to make sense. Every time an item is pushed into the stack,
27/// its bounds have to be checked; by making the stack grow downwards, the underflow detection of
28/// the offset update operation can also be reused to check for stack overflow.
29///
30/// A few opcodes require pushing and/or popping multiple elements. The [`push`](Self::push) and
31/// [`pop`](Self::pop) methods support working with multiple elements instead of a single one,
32/// reducing the number of checks performed on the stack.
33pub struct Stack {
34    pub values: Box<[U256; STACK_LIMIT]>,
35    pub offset: usize,
36}
37
38impl Stack {
39    #[inline]
40    pub fn pop<const N: usize>(&mut self) -> Result<&[U256; N], ExceptionalHalt> {
41        // Compile-time check for stack underflow.
42        const {
43            assert!(N <= STACK_LIMIT);
44        }
45
46        // The following operation can never overflow as both `self.offset` and N are within
47        // STACK_LIMIT (1024).
48        let next_offset = self.offset.wrapping_add(N);
49
50        // The index cannot fail because `self.offset` is known to be valid. The `first_chunk()`
51        // method will ensure that `next_offset` is within `STACK_LIMIT`, so there's no need to
52        // check it again.
53        #[expect(unsafe_code)]
54        let values = unsafe {
55            self.values
56                .get_unchecked(self.offset..)
57                .first_chunk::<N>()
58                .ok_or(ExceptionalHalt::StackUnderflow)?
59        };
60        // Due to previous error check in first_chunk, next_offset is guaranteed to be < STACK_LIMIT
61        self.offset = next_offset;
62
63        Ok(values)
64    }
65
66    #[inline]
67    pub fn pop1(&mut self) -> Result<U256, ExceptionalHalt> {
68        let value = *self
69            .values
70            .get(self.offset)
71            .ok_or(ExceptionalHalt::StackUnderflow)?;
72        // The following operation can never overflow as both `self.offset` and N are within
73        // STACK_LIMIT (1024).
74        self.offset = self.offset.wrapping_add(1);
75
76        Ok(value)
77    }
78
79    /// Mutable reference to the top item without changing depth (one underflow check,
80    /// no `offset` write). For stack-neutral unary ops (ISZERO, NOT, CLZ), replacing
81    /// `pop1` + `push` with `*top_mut() = f(*top)` avoids the read-modify-write of the
82    /// shared `offset`, which is the per-opcode serial dependency that pins dispatch IPC.
83    #[inline]
84    pub fn top_mut(&mut self) -> Result<&mut U256, ExceptionalHalt> {
85        self.values
86            .get_mut(self.offset)
87            .ok_or(ExceptionalHalt::StackUnderflow)
88    }
89
90    /// Pop the top value and return it together with a mutable reference to the new top.
91    /// For binary ops: `let (a, b) = pop1_and_top_mut()?; *b = f(a, *b)` writes the result
92    /// in place (one `offset` write instead of `pop::<2>` + `push`'s two), where `a` is the
93    /// original top and `*b` the original second operand.
94    #[inline]
95    pub fn pop1_and_top_mut(&mut self) -> Result<(U256, &mut U256), ExceptionalHalt> {
96        let a = self.pop1()?;
97        Ok((a, self.top_mut()?))
98    }
99
100    /// Push a single U256 value to the stack, faster than the generic push.
101    #[inline]
102    pub fn push(&mut self, value: U256) -> Result<(), ExceptionalHalt> {
103        // Since the stack grows downwards, when an offset underflow is detected the stack is
104        // overflowing.
105        let next_offset = self
106            .offset
107            .checked_sub(1)
108            .ok_or(ExceptionalHalt::StackOverflow)?;
109
110        // The following index cannot fail because `next_offset` has already been checked and
111        // `self.offset` is known to be within `STACK_LIMIT`.
112        // Store each limb individually so LLVM treats them as 4 independent i64 scalars.
113        // This prevents LLVM from grouping limbs[1..3] into a [24 x i8] alloca that would
114        // then need a memset + memcpy round-trip for values with known-zero upper limbs
115        // (e.g. PUSH1-PUSH31), allowing it to emit direct zero stores instead.
116        #[expect(unsafe_code, reason = "next_offset == self.offset - 1 >= 0")]
117        unsafe {
118            let slot = self.values.get_unchecked_mut(next_offset);
119            slot.0[0] = value.0[0];
120            slot.0[1] = value.0[1];
121            slot.0[2] = value.0[2];
122            slot.0[3] = value.0[3];
123        }
124        self.offset = next_offset;
125
126        Ok(())
127    }
128
129    #[inline]
130    pub fn push_zero(&mut self) -> Result<(), ExceptionalHalt> {
131        // Since the stack grows downwards, when an offset underflow is detected the stack is
132        // overflowing.
133        let next_offset = self
134            .offset
135            .checked_sub(1)
136            .ok_or(ExceptionalHalt::StackOverflow)?;
137
138        // The following index cannot fail because `next_offset` has already been checked and
139        // `self.offset` is known to be within `STACK_LIMIT`.
140        #[expect(unsafe_code, reason = "next_offset == self.offset - 1 >= 0")]
141        unsafe {
142            *self
143                .values
144                .get_unchecked_mut(next_offset)
145                .0
146                .as_mut_ptr()
147                .cast() = [0u64; U64_PER_U256];
148        }
149        self.offset = next_offset;
150
151        Ok(())
152    }
153
154    pub fn len(&self) -> usize {
155        // The following operation cannot underflow because `self.offset` is known to be less than
156        // or equal to `self.values.len()` (aka. `STACK_LIMIT`).
157        #[expect(clippy::arithmetic_side_effects)]
158        {
159            self.values.len() - self.offset
160        }
161    }
162
163    pub fn is_empty(&self) -> bool {
164        self.offset == self.values.len()
165    }
166
167    #[inline(always)]
168    pub fn swap<const N: usize>(&mut self) -> Result<(), ExceptionalHalt> {
169        // Compile-time check that ensures `self.offset + N` is safe,
170        // since self.offset is bounded by STACK_LIMIT
171        const {
172            assert!(STACK_LIMIT.checked_add(N).is_some());
173        }
174        #[expect(clippy::arithmetic_side_effects)]
175        let index = self.offset + N;
176
177        if index >= self.values.len() {
178            return Err(ExceptionalHalt::StackUnderflow);
179        }
180
181        #[expect(unsafe_code, reason = "self.offset always < STACK_LIMIT")]
182        unsafe {
183            assert_unchecked(self.offset < STACK_LIMIT)
184        };
185
186        self.values.swap(self.offset, index);
187        Ok(())
188    }
189
190    pub fn clear(&mut self) {
191        self.offset = STACK_LIMIT;
192    }
193
194    /// Pushes a copy of the value at depth N
195    #[inline]
196    pub fn dup<const N: usize>(&mut self) -> Result<(), ExceptionalHalt> {
197        // Compile-time check that ensures `self.offset + N` is safe,
198        // since self.offset is bounded by STACK_LIMIT
199        const {
200            assert!(STACK_LIMIT.checked_add(N).is_some());
201        }
202        #[expect(clippy::arithmetic_side_effects)]
203        let index = self.offset + N;
204        if index >= self.values.len() {
205            return Err(ExceptionalHalt::StackUnderflow);
206        }
207
208        self.offset = self
209            .offset
210            .checked_sub(1)
211            .ok_or(ExceptionalHalt::StackOverflow)?;
212
213        #[expect(unsafe_code, reason = "index < size, offset-1 >= 0")]
214        unsafe {
215            std::ptr::copy_nonoverlapping(
216                self.values.get_unchecked_mut(index).0.as_mut_ptr(),
217                self.values.get_unchecked_mut(self.offset).0.as_mut_ptr(),
218                U64_PER_U256,
219            );
220        }
221        Ok(())
222    }
223}
224
225impl Default for Stack {
226    fn default() -> Self {
227        Self {
228            values: Box::new([U256::zero(); STACK_LIMIT]),
229            offset: STACK_LIMIT,
230        }
231    }
232}
233
234impl fmt::Debug for Stack {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        struct StackValues<'a>(&'a [U256]);
237
238        impl fmt::Debug for StackValues<'_> {
239            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240                f.debug_list().entries(self.0.iter().rev()).finish()
241            }
242        }
243
244        #[expect(clippy::indexing_slicing)]
245        f.debug_tuple("Stack")
246            .field(&StackValues(&self.values[self.offset..]))
247            .finish()
248    }
249}
250
251impl Hash for Stack {
252    #[expect(
253        clippy::indexing_slicing,
254        reason = "offset is always within bounds of values"
255    )]
256    fn hash<H: Hasher>(&self, state: &mut H) {
257        self.values[self.offset..].hash(state);
258    }
259}
260
261#[derive(Debug)]
262/// A call frame, or execution environment, is the context in which
263/// the EVM is currently executing.
264/// One context can trigger another with opcodes like CALL or CREATE.
265/// Call frames relationships can be thought of as a parent-child relation.
266pub struct CallFrame {
267    /// Max gas a callframe can use
268    pub gas_limit: u64,
269    /// Keeps track of the remaining gas in the current context.
270    ///
271    /// This is a i64 for performance reasons, to allow faster gas cost substraction and checks.
272    ///
273    /// Additionally, gas limit won't be a problem since https://eips.ethereum.org/EIPS/eip-7825 limits it to 2^24, which is lower than i64::MAX.
274    pub gas_remaining: i64,
275    /// Program Counter
276    pub pc: usize,
277    /// Address of the account that sent the message
278    pub msg_sender: Address,
279    /// Address of the recipient of the message
280    pub to: Address,
281    /// Address of the code to execute. Usually the same as `to`, but can be different
282    pub code_address: Address,
283    /// Bytecode to execute.
284    /// Its hash field will be bogus for initcodes, as it is inaccessible to the VM
285    /// unless associated to an account, which doesn't happen for its initcode.
286    pub bytecode: Code,
287    /// Value sent along the transaction
288    pub msg_value: U256,
289    pub stack: Stack,
290    pub memory: Memory,
291    /// Data sent along the transaction. Empty in CREATE transactions.
292    pub calldata: Bytes,
293    /// Return data of the CURRENT CONTEXT (see docs for more details)
294    pub output: Bytes,
295    /// Return data of the SUB-CONTEXT (see docs for more details)
296    pub sub_return_data: Bytes,
297    /// Indicates if current context is static (if it is, it can't alter state)
298    pub is_static: bool,
299    /// Call stack current depth
300    pub depth: usize,
301    /// This is set to true if the function that created this callframe is CREATE or CREATE2
302    pub is_create: bool,
303    /// Everytime we want to write an account during execution of a callframe we store the pre-write state so that we can restore if it reverts
304    pub call_frame_backup: CallFrameBackup,
305    /// Return data offset
306    pub ret_offset: usize,
307    /// Return data size
308    pub ret_size: usize,
309    /// If true then transfer value from caller to callee
310    pub should_transfer_value: bool,
311    /// EIP-8037: snapshot of VM.state_gas_used (signed) at child-frame entry.
312    /// Used to restore parent's state_gas_used on child revert.
313    pub state_gas_used_at_entry: i64,
314}
315
316#[derive(Debug, Clone, Eq, PartialEq, Default)]
317pub struct CallFrameBackup {
318    pub original_accounts_info: FxHashMap<Address, LevmAccount>,
319    pub original_account_storage_slots: FxHashMap<Address, FxHashMap<H256, U256>>,
320    /// BAL checkpoint for EIP-7928 - used to restore state changes on revert
321    /// while preserving touched_addresses.
322    pub bal_checkpoint: Option<BlockAccessListCheckpoint>,
323    /// Code hashes this frame inserted into the by-hash code cache
324    /// (`GeneralizedDatabase::codes`) for codes it deployed. Removed from the
325    /// cache on revert: a stale entry would make a later read of the same
326    /// hash (from a pre-existing account) hit the cache instead of the store,
327    /// hiding the read from execution-witness recording (EIP-8025).
328    pub inserted_code_hashes: Vec<H256>,
329}
330
331impl CallFrameBackup {
332    pub fn backup_account_info(
333        &mut self,
334        address: Address,
335        account: &LevmAccount,
336    ) -> Result<(), InternalError> {
337        self.original_accounts_info
338            .entry(address)
339            .or_insert_with(|| LevmAccount {
340                info: account.info.clone(),
341                storage: Default::default(),
342                status: account.status.clone(),
343                has_storage: account.has_storage,
344                exists: account.exists,
345            });
346
347        Ok(())
348    }
349
350    pub fn clear(&mut self) {
351        self.original_accounts_info.clear();
352        self.original_account_storage_slots.clear();
353        self.bal_checkpoint = None;
354        self.inserted_code_hashes.clear();
355    }
356
357    /// Merges `other` into `self`, per-address. For slots present in both,
358    /// `other`'s values win. Callers MUST pass the older/more-original backup
359    /// as `other` so the truly-original value is preserved (matches the
360    /// `or_insert` semantic in `backup_storage_slot`).
361    pub fn extend(&mut self, other: CallFrameBackup) {
362        // Per-slot merge: plain HashMap::extend would let `other`'s inner slot map
363        // replace `self`'s, dropping any slots `self` had for the same address.
364        for (address, other_storage) in other.original_account_storage_slots {
365            self.original_account_storage_slots
366                .entry(address)
367                .or_default()
368                .extend(other_storage);
369        }
370        self.original_accounts_info
371            .extend(other.original_accounts_info);
372        self.inserted_code_hashes.extend(other.inserted_code_hashes);
373        // Don't extend bal_checkpoint - it's specific to each call frame
374    }
375}
376
377impl CallFrame {
378    #[expect(
379        clippy::too_many_arguments,
380        reason = "inlined constructor, many args needed for performance"
381    )]
382    // Force inline, due to lot of arguments, inlining must be forced, and it is actually beneficial
383    // because passing so much data is costly. Verified with samply.
384    #[inline(always)]
385    pub fn new(
386        msg_sender: Address,
387        to: Address,
388        code_address: Address,
389        bytecode: Code,
390        msg_value: U256,
391        calldata: Bytes,
392        is_static: bool,
393        gas_limit: u64,
394        depth: usize,
395        should_transfer_value: bool,
396        is_create: bool,
397        ret_offset: usize,
398        ret_size: usize,
399        stack: Stack,
400        memory: Memory,
401    ) -> Self {
402        // Note: Do not use ..Default::default() because it has runtime cost.
403
404        #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
405        Self {
406            gas_limit,
407            gas_remaining: gas_limit as i64,
408            msg_sender,
409            to,
410            code_address,
411            bytecode,
412            msg_value,
413            calldata,
414            is_static,
415            depth,
416            should_transfer_value,
417            is_create,
418            ret_offset,
419            ret_size,
420            stack,
421            memory,
422            call_frame_backup: CallFrameBackup::default(),
423            output: Bytes::default(),
424            pc: 0,
425            sub_return_data: Bytes::default(),
426            state_gas_used_at_entry: 0,
427        }
428    }
429
430    #[inline(always)]
431    pub fn next_opcode(&self) -> u8 {
432        // SAFETY: pc reaches at most bytecode_len + 32 (a PUSH32 at the last real
433        // byte advances 33 total: +1 in the dispatch loop, +32 in the handler).
434        // dispatch_buf() is bytecode_len + BYTECODE_PADDING (33) long, so the read
435        // is always in bounds.
436        #[expect(unsafe_code, reason = "pc bounded by padded bytecode len")]
437        unsafe {
438            *self.bytecode.dispatch_buf().get_unchecked(self.pc)
439        }
440    }
441
442    pub fn pc(&self) -> usize {
443        self.pc
444    }
445
446    /// Increases gas consumption of CallFrame and Environment, returning an error if the callframe gas limit is reached.
447    #[inline(always)]
448    #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
449    #[expect(clippy::arithmetic_side_effects, reason = "arithmethic checked")]
450    pub fn increase_consumed_gas(&mut self, gas: u64) -> Result<(), ExceptionalHalt> {
451        self.gas_remaining -= gas as i64;
452
453        if self.gas_remaining < 0 {
454            return Err(ExceptionalHalt::OutOfGas);
455        }
456
457        Ok(())
458    }
459
460    /// EELS' `check_gas`: assert gas is available without consuming it.
461    #[inline(always)]
462    #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
463    pub fn check_gas(&self, gas: u64) -> Result<(), ExceptionalHalt> {
464        if self.gas_remaining < 0 || (self.gas_remaining as u64) < gas {
465            return Err(ExceptionalHalt::OutOfGas);
466        }
467        Ok(())
468    }
469
470    pub fn set_code(&mut self, code: Code) -> Result<(), VMError> {
471        self.bytecode = code;
472        Ok(())
473    }
474}
475
476impl<'a> VM<'a> {
477    /// Adds current calframe to call_frames, sets current call frame to the passed callframe.
478    #[inline(always)]
479    pub fn add_callframe(&mut self, new_call_frame: CallFrame) {
480        // Reserve once on the first sub-call (p99 depth ~10, max 27): keeps the ~43% of txs that
481        // never make a call alloc-free (`call_frames` starts as `Vec::new()`), while avoiding the
482        // repeated reallocs a call-heavy tx would otherwise incur as depth grows.
483        if self.call_frames.is_empty() {
484            self.call_frames.reserve(8);
485        }
486        self.call_frames.push(new_call_frame);
487        #[allow(unsafe_code, reason = "just pushed, so the vec is not empty")]
488        unsafe {
489            std::mem::swap(
490                &mut self.current_call_frame,
491                self.call_frames.last_mut().unwrap_unchecked(),
492            );
493        }
494    }
495
496    #[inline(always)]
497    pub fn pop_call_frame(&mut self) -> Result<CallFrame, InternalError> {
498        let mut new = self.call_frames.pop().ok_or(InternalError::CallFrame)?;
499
500        std::mem::swap(&mut new, &mut self.current_call_frame);
501
502        Ok(new)
503    }
504
505    pub fn is_initial_call_frame(&self) -> bool {
506        self.call_frames.is_empty()
507    }
508
509    /// Restores the cache state to the state before changes made during a callframe.
510    pub fn restore_cache_state(&mut self) -> Result<(), VMError> {
511        let callframe_backup = self.current_call_frame.call_frame_backup.clone();
512        restore_cache_state(self.db, callframe_backup)
513    }
514
515    /// Like [`Self::restore_cache_state`] but moves the current frame's backup out instead of
516    /// cloning it. Only sound when nothing reads `call_frame_backup` afterward: the inner-call
517    /// revert in `handle_return` (the frame is popped right after, so its backup is dead), and
518    /// the top-level / invalid-tx revert when no `BackupHook` is installed (normal L1 block
519    /// execution, gated on `VM::preserve_top_level_backup`).
520    ///
521    /// When a `BackupHook` IS present (L2 / stateless) the top-level paths must keep cloning,
522    /// because `BackupHook::finalize` reads the backup to build the tx-level undo snapshot.
523    pub fn restore_cache_state_consuming(&mut self) -> Result<(), VMError> {
524        let callframe_backup = std::mem::take(&mut self.current_call_frame.call_frame_backup);
525        restore_cache_state(self.db, callframe_backup)
526    }
527
528    // The CallFrameBackup of the current callframe has to be merged with the backup of its parent, in the following way:
529    //   - For every account that's present in the parent backup, do nothing (i.e. keep the one that's already there).
530    //   - For every account that's NOT present in the parent backup but is on the child backup, add the child backup to it.
531    //   - Do the same for every individual storage slot.
532    pub fn merge_call_frame_backup_with_parent(
533        &mut self,
534        child_call_frame_backup: &CallFrameBackup,
535    ) -> Result<(), VMError> {
536        let parent_backup_accounts = &mut self
537            .current_call_frame
538            .call_frame_backup
539            .original_accounts_info;
540        for (address, account) in child_call_frame_backup.original_accounts_info.iter() {
541            if parent_backup_accounts.get(address).is_none() {
542                parent_backup_accounts.insert(*address, account.clone());
543            }
544        }
545
546        let parent_backup_storage = &mut self
547            .current_call_frame
548            .call_frame_backup
549            .original_account_storage_slots;
550        for (address, storage) in child_call_frame_backup
551            .original_account_storage_slots
552            .iter()
553        {
554            let parent_storage = parent_backup_storage.entry(*address).or_default();
555            for (key, value) in storage {
556                if parent_storage.get(key).is_none() {
557                    parent_storage.insert(*key, *value);
558                }
559            }
560        }
561
562        // Propagate code-cache insertions so a revert of the parent also
563        // evicts codes deployed by the (committed) child frame.
564        self.current_call_frame
565            .call_frame_backup
566            .inserted_code_hashes
567            .extend(child_call_frame_backup.inserted_code_hashes.iter().copied());
568
569        Ok(())
570    }
571
572    #[inline(always)]
573    #[expect(
574        clippy::arithmetic_side_effects,
575        reason = "pc bounded by padded bytecode len"
576    )]
577    pub fn advance_pc(&mut self) {
578        self.current_call_frame.pc += 1;
579    }
580}