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 /// EIP-8037: EELS per-frame `state_gas_spilled`; LIFO refund source;
315 /// propagated to parent on child success.
316 pub frame_state_gas_spilled: u64,
317 /// EIP-8037 (#3002): whether this CREATE/CREATE2 child's target address was
318 /// already alive (existed and non-empty) before the create mutated state.
319 /// Read in `handle_return_create` to refund the unconditional new-account
320 /// state gas on the success path (no new account leaf is created).
321 pub target_alive: bool,
322 /// EIP-8037: whether the parent charged new-account state gas for this CALL
323 /// (value transfer to an empty account). Refunded on child revert/error,
324 /// mirroring EELS `generic_call` `credit_state_gas_refund(NEW_ACCOUNT)`.
325 pub new_account_state_gas_charged: bool,
326}
327
328#[derive(Debug, Clone, Eq, PartialEq, Default)]
329pub struct CallFrameBackup {
330 pub original_accounts_info: FxHashMap<Address, LevmAccount>,
331 pub original_account_storage_slots: FxHashMap<Address, FxHashMap<H256, U256>>,
332 /// BAL checkpoint for EIP-7928 - used to restore state changes on revert
333 /// while preserving touched_addresses.
334 pub bal_checkpoint: Option<BlockAccessListCheckpoint>,
335 /// Code hashes this frame inserted into the by-hash code cache
336 /// (`GeneralizedDatabase::codes`) for codes it deployed. Removed from the
337 /// cache on revert: a stale entry would make a later read of the same
338 /// hash (from a pre-existing account) hit the cache instead of the store,
339 /// hiding the read from execution-witness recording (EIP-8025).
340 pub inserted_code_hashes: Vec<H256>,
341}
342
343impl CallFrameBackup {
344 pub fn backup_account_info(
345 &mut self,
346 address: Address,
347 account: &LevmAccount,
348 ) -> Result<(), InternalError> {
349 self.original_accounts_info
350 .entry(address)
351 .or_insert_with(|| LevmAccount {
352 info: account.info.clone(),
353 storage: Default::default(),
354 status: account.status.clone(),
355 has_storage: account.has_storage,
356 exists: account.exists,
357 });
358
359 Ok(())
360 }
361
362 pub fn clear(&mut self) {
363 self.original_accounts_info.clear();
364 self.original_account_storage_slots.clear();
365 self.bal_checkpoint = None;
366 self.inserted_code_hashes.clear();
367 }
368
369 /// Merges `other` into `self`, per-address. For slots present in both,
370 /// `other`'s values win. Callers MUST pass the older/more-original backup
371 /// as `other` so the truly-original value is preserved (matches the
372 /// `or_insert` semantic in `backup_storage_slot`).
373 pub fn extend(&mut self, other: CallFrameBackup) {
374 // Per-slot merge: plain HashMap::extend would let `other`'s inner slot map
375 // replace `self`'s, dropping any slots `self` had for the same address.
376 for (address, other_storage) in other.original_account_storage_slots {
377 self.original_account_storage_slots
378 .entry(address)
379 .or_default()
380 .extend(other_storage);
381 }
382 self.original_accounts_info
383 .extend(other.original_accounts_info);
384 self.inserted_code_hashes.extend(other.inserted_code_hashes);
385 // Don't extend bal_checkpoint - it's specific to each call frame
386 }
387}
388
389impl CallFrame {
390 #[expect(
391 clippy::too_many_arguments,
392 reason = "inlined constructor, many args needed for performance"
393 )]
394 // Force inline, due to lot of arguments, inlining must be forced, and it is actually beneficial
395 // because passing so much data is costly. Verified with samply.
396 #[inline(always)]
397 pub fn new(
398 msg_sender: Address,
399 to: Address,
400 code_address: Address,
401 bytecode: Code,
402 msg_value: U256,
403 calldata: Bytes,
404 is_static: bool,
405 gas_limit: u64,
406 depth: usize,
407 should_transfer_value: bool,
408 is_create: bool,
409 ret_offset: usize,
410 ret_size: usize,
411 stack: Stack,
412 memory: Memory,
413 ) -> Self {
414 // Note: Do not use ..Default::default() because it has runtime cost.
415
416 #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
417 Self {
418 gas_limit,
419 gas_remaining: gas_limit as i64,
420 msg_sender,
421 to,
422 code_address,
423 bytecode,
424 msg_value,
425 calldata,
426 is_static,
427 depth,
428 should_transfer_value,
429 is_create,
430 ret_offset,
431 ret_size,
432 stack,
433 memory,
434 call_frame_backup: CallFrameBackup::default(),
435 output: Bytes::default(),
436 pc: 0,
437 sub_return_data: Bytes::default(),
438 state_gas_used_at_entry: 0,
439 frame_state_gas_spilled: 0,
440 target_alive: false,
441 new_account_state_gas_charged: false,
442 }
443 }
444
445 #[inline(always)]
446 pub fn next_opcode(&self) -> u8 {
447 // SAFETY: pc reaches at most bytecode_len + 32 (a PUSH32 at the last real
448 // byte advances 33 total: +1 in the dispatch loop, +32 in the handler).
449 // dispatch_buf() is bytecode_len + BYTECODE_PADDING (33) long, so the read
450 // is always in bounds.
451 #[expect(unsafe_code, reason = "pc bounded by padded bytecode len")]
452 unsafe {
453 *self.bytecode.dispatch_buf().get_unchecked(self.pc)
454 }
455 }
456
457 pub fn pc(&self) -> usize {
458 self.pc
459 }
460
461 /// Increases gas consumption of CallFrame and Environment, returning an error if the callframe gas limit is reached.
462 #[inline(always)]
463 #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
464 #[expect(clippy::arithmetic_side_effects, reason = "arithmethic checked")]
465 pub fn increase_consumed_gas(&mut self, gas: u64) -> Result<(), ExceptionalHalt> {
466 self.gas_remaining -= gas as i64;
467
468 if self.gas_remaining < 0 {
469 return Err(ExceptionalHalt::OutOfGas);
470 }
471
472 Ok(())
473 }
474
475 /// EELS' `check_gas`: assert gas is available without consuming it.
476 #[inline(always)]
477 #[expect(clippy::as_conversions, reason = "remaining gas conversion")]
478 pub fn check_gas(&self, gas: u64) -> Result<(), ExceptionalHalt> {
479 if self.gas_remaining < 0 || (self.gas_remaining as u64) < gas {
480 return Err(ExceptionalHalt::OutOfGas);
481 }
482 Ok(())
483 }
484
485 pub fn set_code(&mut self, code: Code) -> Result<(), VMError> {
486 self.bytecode = code;
487 Ok(())
488 }
489}
490
491impl<'a> VM<'a> {
492 /// Adds current calframe to call_frames, sets current call frame to the passed callframe.
493 #[inline(always)]
494 pub fn add_callframe(&mut self, new_call_frame: CallFrame) {
495 // Reserve once on the first sub-call (p99 depth ~10, max 27): keeps the ~43% of txs that
496 // never make a call alloc-free (`call_frames` starts as `Vec::new()`), while avoiding the
497 // repeated reallocs a call-heavy tx would otherwise incur as depth grows.
498 if self.call_frames.is_empty() {
499 self.call_frames.reserve(8);
500 }
501 self.call_frames.push(new_call_frame);
502 #[allow(unsafe_code, reason = "just pushed, so the vec is not empty")]
503 unsafe {
504 std::mem::swap(
505 &mut self.current_call_frame,
506 self.call_frames.last_mut().unwrap_unchecked(),
507 );
508 }
509 }
510
511 #[inline(always)]
512 pub fn pop_call_frame(&mut self) -> Result<CallFrame, InternalError> {
513 let mut new = self.call_frames.pop().ok_or(InternalError::CallFrame)?;
514
515 std::mem::swap(&mut new, &mut self.current_call_frame);
516
517 Ok(new)
518 }
519
520 pub fn is_initial_call_frame(&self) -> bool {
521 self.call_frames.is_empty()
522 }
523
524 /// Restores the cache state to the state before changes made during a callframe.
525 pub fn restore_cache_state(&mut self) -> Result<(), VMError> {
526 let callframe_backup = self.current_call_frame.call_frame_backup.clone();
527 restore_cache_state(self.db, callframe_backup)
528 }
529
530 /// Like [`Self::restore_cache_state`] but moves the current frame's backup out instead of
531 /// cloning it. Only sound when nothing reads `call_frame_backup` afterward: the inner-call
532 /// revert in `handle_return` (the frame is popped right after, so its backup is dead), and
533 /// the top-level / invalid-tx revert when no `BackupHook` is installed (normal L1 block
534 /// execution, gated on `VM::preserve_top_level_backup`).
535 ///
536 /// When a `BackupHook` IS present (L2 / stateless) the top-level paths must keep cloning,
537 /// because `BackupHook::finalize` reads the backup to build the tx-level undo snapshot.
538 pub fn restore_cache_state_consuming(&mut self) -> Result<(), VMError> {
539 let callframe_backup = std::mem::take(&mut self.current_call_frame.call_frame_backup);
540 restore_cache_state(self.db, callframe_backup)
541 }
542
543 // The CallFrameBackup of the current callframe has to be merged with the backup of its parent, in the following way:
544 // - For every account that's present in the parent backup, do nothing (i.e. keep the one that's already there).
545 // - For every account that's NOT present in the parent backup but is on the child backup, add the child backup to it.
546 // - Do the same for every individual storage slot.
547 pub fn merge_call_frame_backup_with_parent(
548 &mut self,
549 child_call_frame_backup: &CallFrameBackup,
550 ) -> Result<(), VMError> {
551 let parent_backup_accounts = &mut self
552 .current_call_frame
553 .call_frame_backup
554 .original_accounts_info;
555 for (address, account) in child_call_frame_backup.original_accounts_info.iter() {
556 if parent_backup_accounts.get(address).is_none() {
557 parent_backup_accounts.insert(*address, account.clone());
558 }
559 }
560
561 let parent_backup_storage = &mut self
562 .current_call_frame
563 .call_frame_backup
564 .original_account_storage_slots;
565 for (address, storage) in child_call_frame_backup
566 .original_account_storage_slots
567 .iter()
568 {
569 let parent_storage = parent_backup_storage.entry(*address).or_default();
570 for (key, value) in storage {
571 if parent_storage.get(key).is_none() {
572 parent_storage.insert(*key, *value);
573 }
574 }
575 }
576
577 // Propagate code-cache insertions so a revert of the parent also
578 // evicts codes deployed by the (committed) child frame.
579 self.current_call_frame
580 .call_frame_backup
581 .inserted_code_hashes
582 .extend(child_call_frame_backup.inserted_code_hashes.iter().copied());
583
584 Ok(())
585 }
586
587 #[inline(always)]
588 #[expect(
589 clippy::arithmetic_side_effects,
590 reason = "pc bounded by padded bytecode len"
591 )]
592 pub fn advance_pc(&mut self) {
593 self.current_call_frame.pc += 1;
594 }
595}