Skip to main content

ethrex_levm/opcode_handlers/
stack_memory_storage_flow.rs

1//! # Control flow and memory operations
2//!
3//! Includes the following opcodes:
4//!   - `POP`
5//!   - `GAS`
6//!   - `PC`
7//!   - `MLOAD`
8//!   - `MSTORE`
9//!   - `MSTORE8`
10//!   - `MCOPY`
11//!   - `MSIZE`
12//!   - `TLOAD`
13//!   - `TSTORE`
14//!   - `SLOAD`
15//!   - `SSTORE`
16//!   - `JUMPDEST`
17//!   - `JUMP`
18//!   - `JUMPI`
19
20use crate::{
21    constants::WORD_SIZE_IN_BYTES_USIZE,
22    errors::{ExceptionalHalt, InternalError, OpcodeResult, VMError},
23    gas_cost::{self, SSTORE_DEFAULT_DYNAMIC, SSTORE_STIPEND, STORAGE_CLEAR_REFUND_AMSTERDAM},
24    memory::calculate_memory_size,
25    opcode_handlers::OpcodeHandler,
26    opcodes::Opcode,
27    utils::{size_offset_to_usize, u256_to_usize},
28    vm::VM,
29};
30use ethrex_common::{H256, U256, types::Fork};
31use std::{mem, slice};
32
33/// Implementation for the `POP` opcode.
34pub struct OpPopHandler;
35impl OpcodeHandler for OpPopHandler {
36    #[inline(always)]
37    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
38        vm.current_call_frame.increase_consumed_gas(gas_cost::POP)?;
39
40        vm.current_call_frame.stack.pop1()?;
41
42        Ok(OpcodeResult::Continue)
43    }
44}
45
46/// Implementation for the `GAS` opcode.
47pub struct OpGasHandler;
48impl OpcodeHandler for OpGasHandler {
49    #[inline(always)]
50    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
51        vm.current_call_frame.increase_consumed_gas(gas_cost::GAS)?;
52
53        vm.current_call_frame
54            .stack
55            .push(vm.current_call_frame.gas_remaining.into())?;
56
57        Ok(OpcodeResult::Continue)
58    }
59}
60
61/// Implementation for the `PC` opcode.
62pub struct OpPcHandler;
63impl OpcodeHandler for OpPcHandler {
64    #[inline(always)]
65    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
66        vm.current_call_frame.increase_consumed_gas(gas_cost::PC)?;
67
68        // Note: Since the PC has been preincremented, subtracting 1 from it to get the operation's
69        //   offset will never cause an underflow condition.
70        vm.current_call_frame
71            .stack
72            .push(vm.current_call_frame.pc.wrapping_sub(1).into())?;
73
74        Ok(OpcodeResult::Continue)
75    }
76}
77
78/// Implementation for the `MLOAD` opcode.
79pub struct OpMLoadHandler;
80impl OpcodeHandler for OpMLoadHandler {
81    #[inline(always)]
82    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
83        // Stack-neutral: replace the top (the offset) with the loaded word in place.
84        let offset = u256_to_usize(*vm.current_call_frame.stack.top_mut()?)?;
85        vm.current_call_frame
86            .increase_consumed_gas(gas_cost::mload(
87                calculate_memory_size(offset, WORD_SIZE_IN_BYTES_USIZE)?,
88                vm.current_call_frame.memory.len(),
89            )?)?;
90
91        let word = vm.current_call_frame.memory.load_word(offset)?;
92        *vm.current_call_frame.stack.top_mut()? = word;
93
94        Ok(OpcodeResult::Continue)
95    }
96}
97
98/// Implementation for the `MSTORE` opcode.
99pub struct OpMStoreHandler;
100impl OpcodeHandler for OpMStoreHandler {
101    #[inline(always)]
102    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
103        let [offset, value] = *vm.current_call_frame.stack.pop()?;
104
105        // Handle debug text printing for solidity contracts that enable it.
106        if vm.debug_mode.enabled && vm.debug_mode.handle_debug(offset, value)? {
107            return Ok(OpcodeResult::Continue);
108        }
109
110        let offset = u256_to_usize(offset)?;
111        vm.current_call_frame
112            .increase_consumed_gas(gas_cost::mstore(
113                calculate_memory_size(offset, WORD_SIZE_IN_BYTES_USIZE)?,
114                vm.current_call_frame.memory.len(),
115            )?)?;
116
117        vm.current_call_frame.memory.store_word(offset, value)?;
118
119        Ok(OpcodeResult::Continue)
120    }
121}
122
123/// Implementation for the `MSTORE8` opcode.
124pub struct OpMStore8Handler;
125impl OpcodeHandler for OpMStore8Handler {
126    #[inline(always)]
127    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
128        let [offset, value] = *vm.current_call_frame.stack.pop()?;
129        let offset = u256_to_usize(offset)?;
130        let value = value.byte(0);
131
132        vm.current_call_frame
133            .increase_consumed_gas(gas_cost::mstore8(
134                calculate_memory_size(offset, size_of::<u8>())?,
135                vm.current_call_frame.memory.len(),
136            )?)?;
137
138        vm.current_call_frame
139            .memory
140            .store_data(offset, slice::from_ref(&value))?;
141
142        Ok(OpcodeResult::Continue)
143    }
144}
145
146/// Implementation for the `MCOPY` opcode.
147pub struct OpMCopyHandler;
148impl OpcodeHandler for OpMCopyHandler {
149    #[inline(always)]
150    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
151        let [dst_offset, src_offset, len] = *vm.current_call_frame.stack.pop()?;
152        let (len, dst_offset) = size_offset_to_usize(len, dst_offset)?;
153        let src_offset = u256_to_usize(src_offset).unwrap_or(usize::MAX);
154
155        vm.current_call_frame
156            .increase_consumed_gas(gas_cost::mcopy(
157                calculate_memory_size(src_offset.max(dst_offset), len)?,
158                vm.current_call_frame.memory.len(),
159                len,
160            )?)?;
161
162        vm.current_call_frame
163            .memory
164            .copy_within(src_offset, dst_offset, len)?;
165
166        Ok(OpcodeResult::Continue)
167    }
168}
169
170/// Implementation for the `MSIZE` opcode.
171pub struct OpMSizeHandler;
172impl OpcodeHandler for OpMSizeHandler {
173    #[inline(always)]
174    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
175        vm.current_call_frame
176            .increase_consumed_gas(gas_cost::MSIZE)?;
177
178        vm.current_call_frame
179            .stack
180            .push(vm.current_call_frame.memory.len().into())?;
181
182        Ok(OpcodeResult::Continue)
183    }
184}
185
186/// Implementation for the `TLOAD` opcode.
187pub struct OpTLoadHandler;
188impl OpcodeHandler for OpTLoadHandler {
189    #[inline(always)]
190    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
191        vm.current_call_frame
192            .increase_consumed_gas(gas_cost::TLOAD)?;
193
194        let key = vm.current_call_frame.stack.pop1()?;
195        vm.current_call_frame
196            .stack
197            .push(vm.substate.get_transient(&vm.current_call_frame.to, &key))?;
198
199        Ok(OpcodeResult::Continue)
200    }
201}
202
203/// Implementation for the `TSTORE` opcode.
204pub struct OpTStoreHandler;
205impl OpcodeHandler for OpTStoreHandler {
206    #[inline(always)]
207    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
208        if vm.current_call_frame.is_static {
209            return Err(ExceptionalHalt::OpcodeNotAllowedInStaticContext.into());
210        }
211
212        vm.current_call_frame
213            .increase_consumed_gas(gas_cost::TSTORE)?;
214
215        let [key, value] = *vm.current_call_frame.stack.pop()?;
216        vm.substate
217            .set_transient(&vm.current_call_frame.to, &key, value);
218
219        Ok(OpcodeResult::Continue)
220    }
221}
222
223/// Implementation for the `SLOAD` opcode.
224pub struct OpSLoadHandler;
225impl OpcodeHandler for OpSLoadHandler {
226    #[inline(always)]
227    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
228        let storage_slot_key = vm.current_call_frame.stack.pop1()?;
229        let address = vm.current_call_frame.to;
230        let key = {
231            #[expect(unsafe_code)]
232            unsafe {
233                let mut hash = mem::transmute::<U256, H256>(storage_slot_key);
234                hash.0.reverse();
235                hash
236            }
237        };
238
239        vm.current_call_frame
240            .increase_consumed_gas(gas_cost::sload(
241                vm.substate.add_accessed_slot(address, key),
242                vm.env.config.fork,
243            )?)?;
244
245        // Record to BAL AFTER gas check passes per EIP-7928
246        vm.record_storage_slot_to_bal(address, storage_slot_key);
247
248        // EIP-8141 mempool validation-trace: SLOAD restricted to the sender's storage.
249        if vm.validation_observer.active {
250            vm.validation_check_sload(address, key);
251        }
252
253        let value = vm.get_storage_value(address, key)?;
254        vm.current_call_frame.stack.push(value)?;
255
256        Ok(OpcodeResult::Continue)
257    }
258}
259
260/// Implementation for the `SSTORE` opcode.
261pub struct OpSStoreHandler;
262impl OpcodeHandler for OpSStoreHandler {
263    #[inline(always)]
264    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
265        if vm.current_call_frame.is_static {
266            return Err(ExceptionalHalt::OpcodeNotAllowedInStaticContext.into());
267        }
268
269        let fork = vm.env.config.fork;
270
271        // EIP-2200. Pre-Amsterdam, COLD_STORAGE_ACCESS (2100) never exceeds the
272        // 2300 stipend, so gating on the flat stipend alone is sufficient.
273        if fork < Fork::Amsterdam && vm.current_call_frame.gas_remaining <= SSTORE_STIPEND {
274            return Err(ExceptionalHalt::OutOfGas.into());
275        }
276
277        let [storage_slot_key, value] = *vm.current_call_frame.stack.pop()?;
278        let to = vm.current_call_frame.to;
279        #[expect(unsafe_code)]
280        let key = unsafe {
281            let mut hash = mem::transmute::<U256, H256>(storage_slot_key);
282            hash.0.reverse();
283            hash
284        };
285
286        if fork >= Fork::Amsterdam {
287            // EIP-8038 (review CRITICAL #2) raises COLD_STORAGE_ACCESS to 3000,
288            // above the 2300 stipend, so the flat pre-Amsterdam gate above no
289            // longer suffices as the EIP-2200 sentry. Peek warmth WITHOUT
290            // marking the slot accessed, gate on
291            // `max(access_cost, SSTORE_STIPEND + 1)` per EELS `check_gas`
292            // (amsterdam/vm/instructions/storage.py::sstore), and only mark the
293            // slot accessed / record the BAL read once this gate passes.
294            let is_cold = !vm.substate.is_slot_accessed(&to, &key);
295            let access_cost = if is_cold {
296                gas_cost::cold_storage_access_cost(fork)
297            } else {
298                SSTORE_DEFAULT_DYNAMIC
299            };
300            let sstore_stipend_floor = u64::try_from(SSTORE_STIPEND)
301                .map_err(|_| InternalError::TypeConversion)?
302                .checked_add(1)
303                .ok_or(InternalError::Overflow)?;
304            vm.current_call_frame
305                .check_gas(access_cost.max(sstore_stipend_floor))?;
306        }
307
308        let (current_value, original_value, storage_slot_was_cold) =
309            vm.access_storage_slot_for_sstore(to, key)?;
310
311        // Record storage read to BAL AFTER the stipend/access-cost gate passes,
312        // BEFORE the main gas check. Per EIP-7928: if SSTORE passes that gate
313        // but fails the main gas charge, the slot MUST appear as a read
314        // because the implicit SLOAD has already happened.
315        vm.record_storage_slot_to_bal(to, storage_slot_key);
316
317        // EIP-8141 mempool validation-trace: SSTORE allowed only inside the
318        // deploy frame and only against the sender's own storage.
319        if vm.validation_observer.active {
320            vm.validation_check_sstore(to, key);
321        }
322
323        // EIP-8037 (Amsterdam+): check if state gas is needed for new storage slot (0 -> nonzero),
324        // but charge it AFTER regular gas per EELS ordering (ethereum/EIPs#11421).
325        // Regular gas OOG must not consume state gas that would inflate the parent's reservoir.
326        let needs_state_gas = fork >= Fork::Amsterdam
327            && value != current_value
328            && current_value == original_value
329            && original_value.is_zero()
330            && !value.is_zero();
331
332        vm.current_call_frame
333            .increase_consumed_gas(gas_cost::sstore(
334                original_value,
335                current_value,
336                value,
337                storage_slot_was_cold,
338                fork,
339            )?)?;
340
341        if needs_state_gas {
342            vm.increase_state_gas(vm.state_gas_storage_set)?;
343        }
344        // EIP-8037 (Amsterdam+) 0→N→0: the slot was created in this tx (original == 0),
345        // dirtied to N (current_value != 0), and now being reset to 0 (value == original == 0).
346        // The creation state gas is refunded via clamp-and-spill, not the regular refund counter.
347        let is_zero_to_n_to_zero_amsterdam = fork >= Fork::Amsterdam
348            && value != current_value
349            && current_value != original_value
350            && value == original_value
351            && original_value.is_zero();
352
353        if value != current_value {
354            // Net refund deltas accumulated across a tx's SSTOREs, matching EELS
355            // (execution-specs amsterdam/vm/instructions/storage.py::sstore): +clear-refund on a
356            // first-time clear, -clear-refund when that clear is reversed, and +STORAGE_WRITE when
357            // a slot is restored to its original value (access is charged separately). EIP-8037
358            // state gas is handled via the reservoir, not these regular deltas.
359            let (remove_slot_cost, restore_empty_slot_cost, restore_slot_cost): (i64, i64, i64) =
360                if fork >= Fork::Amsterdam {
361                    // Amsterdam: clear refund 12480; both restore deltas are the full STORAGE_WRITE.
362                    (STORAGE_CLEAR_REFUND_AMSTERDAM, 10000, 10000)
363                } else {
364                    // EIP-2929
365                    (4800, 19900, 2800)
366                };
367
368            // The operations on `delta` cannot overflow.
369            let mut delta = 0i64;
370            #[expect(
371                clippy::arithmetic_side_effects,
372                reason = "delta additions are bounded by known constants"
373            )]
374            if current_value == original_value {
375                if !original_value.is_zero() && value.is_zero() {
376                    delta += remove_slot_cost;
377                }
378            } else {
379                if !original_value.is_zero() {
380                    if current_value.is_zero() {
381                        delta -= remove_slot_cost;
382                    } else if value.is_zero() {
383                        delta += remove_slot_cost;
384                    }
385                }
386
387                if value == original_value {
388                    if original_value.is_zero() {
389                        delta += restore_empty_slot_cost;
390                    } else {
391                        delta += restore_slot_cost;
392                    }
393                }
394            }
395
396            // Update refunded gas after checking for overflow or underflow.
397            match vm.substate.refunded_gas.checked_add_signed(delta) {
398                Some(refunded_gas) => vm.substate.refunded_gas = refunded_gas,
399                None if delta < 0 => return Err(InternalError::Underflow.into()),
400                None => return Err(InternalError::Overflow.into()),
401            }
402        }
403
404        // EIP-8037: credit the state gas refund via clamp-and-spill (after regular gas processing).
405        if is_zero_to_n_to_zero_amsterdam {
406            vm.credit_state_gas_refund(vm.state_gas_storage_set)?;
407        }
408
409        if value != current_value {
410            vm.update_account_storage(to, key, storage_slot_key, value, current_value)?;
411        }
412
413        Ok(OpcodeResult::Continue)
414    }
415}
416
417/// Implementation for the `JUMPDEST` opcode.
418pub struct OpJumpDestHandler;
419impl OpcodeHandler for OpJumpDestHandler {
420    #[inline(always)]
421    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
422        vm.current_call_frame
423            .increase_consumed_gas(gas_cost::JUMPDEST)?;
424
425        Ok(OpcodeResult::Continue)
426    }
427}
428
429/// Implementation for the `JUMP` opcode.
430pub struct OpJumpHandler;
431impl OpcodeHandler for OpJumpHandler {
432    #[inline(always)]
433    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
434        vm.current_call_frame
435            .increase_consumed_gas(gas_cost::JUMP)?;
436
437        let target = vm.current_call_frame.stack.pop1()?;
438        jump(vm, target.try_into().unwrap_or(usize::MAX), gas_cost::JUMP)?;
439
440        Ok(OpcodeResult::Continue)
441    }
442}
443
444/// Implementation for the `JUMPI` opcode.
445pub struct OpJumpIHandler;
446impl OpcodeHandler for OpJumpIHandler {
447    #[inline(always)]
448    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
449        vm.current_call_frame
450            .increase_consumed_gas(gas_cost::JUMPI)?;
451
452        let [target, condition] = *vm.current_call_frame.stack.pop()?;
453        if !condition.is_zero() {
454            jump(vm, target.try_into().unwrap_or(usize::MAX), gas_cost::JUMPI)?;
455        }
456
457        Ok(OpcodeResult::Continue)
458    }
459}
460
461/// Validate and take a jump. Fuses the destination JUMPDEST (advances PC past
462/// it and charges its 1 gas inline) to save a dispatch cycle on the hot path.
463///
464/// When the tracer is active we keep the fusion for performance and *synthesize*
465/// a JUMPDEST entry in the trace log: `parent_gas_cost` is recorded as the
466/// override for the parent JUMP/JUMPI step (so its `gasCost` doesn't absorb the
467/// JUMPDEST charge), and the JUMPDEST step is pushed directly via
468/// `synthesize_step` after the gas is charged.
469fn jump(vm: &mut VM<'_>, target: usize, parent_gas_cost: u64) -> Result<(), VMError> {
470    // Check target address validity.
471    //   - Target bytecode has to be a JUMPDEST.
472    //   - Target address must not be blacklisted (aka. the JUMPDEST must not be part of a literal).
473    #[expect(clippy::as_conversions, reason = "safe")]
474    if vm
475        .current_call_frame
476        .bytecode
477        .dispatch_buf()
478        .get(target)
479        .is_some_and(|&value| {
480            value == Opcode::JUMPDEST as u8
481                && vm
482                    .current_call_frame
483                    .bytecode
484                    .jump_targets
485                    .binary_search(&(target as u32))
486                    .is_ok()
487        })
488    {
489        if vm.opcode_tracer.active {
490            // Override the parent JUMP/JUMPI's gasCost so the dispatch loop
491            // doesn't roll the upcoming JUMPDEST charge into it.
492            vm.opcode_tracer.last_opcode_gas_cost = Some(parent_gas_cost);
493
494            // Capture the synthetic JUMPDEST step's state BEFORE charging its gas.
495            let synth = build_jumpdest_step(vm, target);
496
497            // Fuse: charge JUMPDEST + advance PC past it.
498            vm.current_call_frame.pc = target.wrapping_add(1);
499            vm.current_call_frame
500                .increase_consumed_gas(gas_cost::JUMPDEST)?;
501
502            vm.opcode_tracer.synthesize_step(synth);
503        } else {
504            // Hot path: fuse JUMP/JUMPI + JUMPDEST without any trace bookkeeping.
505            vm.current_call_frame.pc = target.wrapping_add(1);
506            vm.current_call_frame
507                .increase_consumed_gas(gas_cost::JUMPDEST)?;
508        }
509        Ok(())
510    } else {
511        // Target address is invalid.
512        Err(ExceptionalHalt::InvalidJump.into())
513    }
514}
515
516/// Builds a synthetic JUMPDEST trace entry. Captures gas/stack/memory/return-data
517/// state at the moment of the call (i.e. *before* the JUMPDEST gas has been
518/// charged) and hands them to the shared [`opcode_tracer::build_step`] so the
519/// cfg-driven conditionals (disable_stack, enable_memory, enable_return_data)
520/// live in exactly one place.
521#[expect(
522    clippy::as_conversions,
523    reason = "pc/depth/mem_size bounded; fit in target types"
524)]
525fn build_jumpdest_step(vm: &VM<'_>, target: usize) -> ethrex_common::tracing::OpcodeStep {
526    use crate::opcode_tracer::build_step;
527    use bytes::Bytes;
528
529    let cfg = &vm.opcode_tracer.cfg;
530    let gas = vm.current_call_frame.gas_remaining.max(0) as u64;
531    let depth = (vm.call_frames.len() as u32).saturating_add(1);
532    let refund = vm.substate.refunded_gas;
533    let mem_size = vm.current_call_frame.memory.len() as u64;
534
535    let stack_view = if cfg.disable_stack {
536        Vec::new()
537    } else {
538        vm.collect_stack_for_trace()
539    };
540    let mem_view = if cfg.enable_memory {
541        vm.collect_memory_for_trace()
542    } else {
543        Vec::new()
544    };
545    let return_data = if cfg.enable_return_data {
546        vm.current_call_frame.sub_return_data.clone()
547    } else {
548        Bytes::new()
549    };
550
551    build_step(
552        cfg,
553        target as u64,
554        Opcode::JUMPDEST as u8,
555        gas,
556        gas_cost::JUMPDEST,
557        depth,
558        refund,
559        &stack_view,
560        &mem_view,
561        mem_size,
562        &return_data,
563        None,
564    )
565}