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_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        let value = vm.get_storage_value(address, key)?;
249        vm.current_call_frame.stack.push(value)?;
250
251        Ok(OpcodeResult::Continue)
252    }
253}
254
255/// Implementation for the `SSTORE` opcode.
256pub struct OpSStoreHandler;
257impl OpcodeHandler for OpSStoreHandler {
258    #[inline(always)]
259    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
260        if vm.current_call_frame.is_static {
261            return Err(ExceptionalHalt::OpcodeNotAllowedInStaticContext.into());
262        }
263
264        // EIP-2200
265        if vm.current_call_frame.gas_remaining <= SSTORE_STIPEND {
266            return Err(ExceptionalHalt::OutOfGas.into());
267        }
268
269        let [storage_slot_key, value] = *vm.current_call_frame.stack.pop()?;
270        let to = vm.current_call_frame.to;
271        #[expect(unsafe_code)]
272        let key = unsafe {
273            let mut hash = mem::transmute::<U256, H256>(storage_slot_key);
274            hash.0.reverse();
275            hash
276        };
277
278        let (current_value, original_value, storage_slot_was_cold) =
279            vm.access_storage_slot_for_sstore(to, key)?;
280
281        // Record storage read to BAL AFTER SSTORE_STIPEND check passes, BEFORE main gas check.
282        // Per EIP-7928: if SSTORE passes the stipend check but fails the main gas charge,
283        // the slot MUST appear as a read because the implicit SLOAD has already happened.
284        vm.record_storage_slot_to_bal(to, storage_slot_key);
285
286        let fork = vm.env.config.fork;
287
288        // EIP-8037 (Amsterdam+): check if state gas is needed for new storage slot (0 -> nonzero),
289        // but charge it AFTER regular gas per EELS ordering (ethereum/EIPs#11421).
290        // Regular gas OOG must not consume state gas that would inflate the parent's reservoir.
291        let needs_state_gas = fork >= Fork::Amsterdam
292            && value != current_value
293            && current_value == original_value
294            && original_value.is_zero()
295            && !value.is_zero();
296
297        vm.current_call_frame
298            .increase_consumed_gas(gas_cost::sstore(
299                original_value,
300                current_value,
301                value,
302                storage_slot_was_cold,
303                fork,
304            )?)?;
305
306        if needs_state_gas {
307            vm.increase_state_gas(vm.state_gas_storage_set)?;
308        }
309        // EIP-8037 (Amsterdam+) 0→N→0: the slot was created in this tx (original == 0),
310        // dirtied to N (current_value != 0), and now being reset to 0 (value == original == 0).
311        // The creation state gas is refunded via clamp-and-spill, not the regular refund counter.
312        let is_zero_to_n_to_zero_amsterdam = fork >= Fork::Amsterdam
313            && value != current_value
314            && current_value != original_value
315            && value == original_value
316            && original_value.is_zero();
317
318        if value != current_value {
319            // Net refund deltas accumulated across a tx's SSTOREs, matching EELS
320            // (execution-specs amsterdam/vm/instructions/storage.py::sstore): +clear-refund on a
321            // first-time clear, -clear-refund when that clear is reversed, and +STORAGE_WRITE when
322            // a slot is restored to its original value (access is charged separately). EIP-8037
323            // state gas is handled via the reservoir, not these regular deltas.
324            let (remove_slot_cost, restore_empty_slot_cost, restore_slot_cost): (i64, i64, i64) =
325                if fork >= Fork::Amsterdam {
326                    // Amsterdam: clear refund 12480; both restore deltas are the full STORAGE_WRITE.
327                    (STORAGE_CLEAR_REFUND_AMSTERDAM, 10000, 10000)
328                } else {
329                    // EIP-2929
330                    (4800, 19900, 2800)
331                };
332
333            // The operations on `delta` cannot overflow.
334            let mut delta = 0i64;
335            #[expect(
336                clippy::arithmetic_side_effects,
337                reason = "delta additions are bounded by known constants"
338            )]
339            if current_value == original_value {
340                if !original_value.is_zero() && value.is_zero() {
341                    delta += remove_slot_cost;
342                }
343            } else {
344                if !original_value.is_zero() {
345                    if current_value.is_zero() {
346                        delta -= remove_slot_cost;
347                    } else if value.is_zero() {
348                        delta += remove_slot_cost;
349                    }
350                }
351
352                if value == original_value {
353                    if original_value.is_zero() {
354                        delta += restore_empty_slot_cost;
355                    } else {
356                        delta += restore_slot_cost;
357                    }
358                }
359            }
360
361            // Update refunded gas after checking for overflow or underflow.
362            match vm.substate.refunded_gas.checked_add_signed(delta) {
363                Some(refunded_gas) => vm.substate.refunded_gas = refunded_gas,
364                None if delta < 0 => return Err(InternalError::Underflow.into()),
365                None => return Err(InternalError::Overflow.into()),
366            }
367        }
368
369        // EIP-8037: credit the state gas refund via clamp-and-spill (after regular gas processing).
370        if is_zero_to_n_to_zero_amsterdam {
371            vm.credit_state_gas_refund(vm.state_gas_storage_set)?;
372        }
373
374        if value != current_value {
375            vm.update_account_storage(to, key, storage_slot_key, value, current_value)?;
376        }
377
378        Ok(OpcodeResult::Continue)
379    }
380}
381
382/// Implementation for the `JUMPDEST` opcode.
383pub struct OpJumpDestHandler;
384impl OpcodeHandler for OpJumpDestHandler {
385    #[inline(always)]
386    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
387        vm.current_call_frame
388            .increase_consumed_gas(gas_cost::JUMPDEST)?;
389
390        Ok(OpcodeResult::Continue)
391    }
392}
393
394/// Implementation for the `JUMP` opcode.
395pub struct OpJumpHandler;
396impl OpcodeHandler for OpJumpHandler {
397    #[inline(always)]
398    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
399        vm.current_call_frame
400            .increase_consumed_gas(gas_cost::JUMP)?;
401
402        let target = vm.current_call_frame.stack.pop1()?;
403        jump(vm, target.try_into().unwrap_or(usize::MAX), gas_cost::JUMP)?;
404
405        Ok(OpcodeResult::Continue)
406    }
407}
408
409/// Implementation for the `JUMPI` opcode.
410pub struct OpJumpIHandler;
411impl OpcodeHandler for OpJumpIHandler {
412    #[inline(always)]
413    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
414        vm.current_call_frame
415            .increase_consumed_gas(gas_cost::JUMPI)?;
416
417        let [target, condition] = *vm.current_call_frame.stack.pop()?;
418        if !condition.is_zero() {
419            jump(vm, target.try_into().unwrap_or(usize::MAX), gas_cost::JUMPI)?;
420        }
421
422        Ok(OpcodeResult::Continue)
423    }
424}
425
426/// Validate and take a jump. Fuses the destination JUMPDEST (advances PC past
427/// it and charges its 1 gas inline) to save a dispatch cycle on the hot path.
428///
429/// When the tracer is active we keep the fusion for performance and *synthesize*
430/// a JUMPDEST entry in the trace log: `parent_gas_cost` is recorded as the
431/// override for the parent JUMP/JUMPI step (so its `gasCost` doesn't absorb the
432/// JUMPDEST charge), and the JUMPDEST step is pushed directly via
433/// `synthesize_step` after the gas is charged.
434fn jump(vm: &mut VM<'_>, target: usize, parent_gas_cost: u64) -> Result<(), VMError> {
435    // Check target address validity.
436    //   - Target bytecode has to be a JUMPDEST.
437    //   - Target address must not be blacklisted (aka. the JUMPDEST must not be part of a literal).
438    #[expect(clippy::as_conversions, reason = "safe")]
439    if vm
440        .current_call_frame
441        .bytecode
442        .dispatch_buf()
443        .get(target)
444        .is_some_and(|&value| {
445            value == Opcode::JUMPDEST as u8
446                && vm
447                    .current_call_frame
448                    .bytecode
449                    .jump_targets
450                    .binary_search(&(target as u32))
451                    .is_ok()
452        })
453    {
454        if vm.opcode_tracer.active {
455            // Override the parent JUMP/JUMPI's gasCost so the dispatch loop
456            // doesn't roll the upcoming JUMPDEST charge into it.
457            vm.opcode_tracer.last_opcode_gas_cost = Some(parent_gas_cost);
458
459            // Capture the synthetic JUMPDEST step's state BEFORE charging its gas.
460            let synth = build_jumpdest_step(vm, target);
461
462            // Fuse: charge JUMPDEST + advance PC past it.
463            vm.current_call_frame.pc = target.wrapping_add(1);
464            vm.current_call_frame
465                .increase_consumed_gas(gas_cost::JUMPDEST)?;
466
467            vm.opcode_tracer.synthesize_step(synth);
468        } else {
469            // Hot path: fuse JUMP/JUMPI + JUMPDEST without any trace bookkeeping.
470            vm.current_call_frame.pc = target.wrapping_add(1);
471            vm.current_call_frame
472                .increase_consumed_gas(gas_cost::JUMPDEST)?;
473        }
474        Ok(())
475    } else {
476        // Target address is invalid.
477        Err(ExceptionalHalt::InvalidJump.into())
478    }
479}
480
481/// Builds a synthetic JUMPDEST trace entry. Captures gas/stack/memory/return-data
482/// state at the moment of the call (i.e. *before* the JUMPDEST gas has been
483/// charged) and hands them to the shared [`opcode_tracer::build_step`] so the
484/// cfg-driven conditionals (disable_stack, enable_memory, enable_return_data)
485/// live in exactly one place.
486#[expect(
487    clippy::as_conversions,
488    reason = "pc/depth/mem_size bounded; fit in target types"
489)]
490fn build_jumpdest_step(vm: &VM<'_>, target: usize) -> ethrex_common::tracing::OpcodeStep {
491    use crate::opcode_tracer::build_step;
492    use bytes::Bytes;
493
494    let cfg = &vm.opcode_tracer.cfg;
495    let gas = vm.current_call_frame.gas_remaining.max(0) as u64;
496    let depth = (vm.call_frames.len() as u32).saturating_add(1);
497    let refund = vm.substate.refunded_gas;
498    let mem_size = vm.current_call_frame.memory.len() as u64;
499
500    let stack_view = if cfg.disable_stack {
501        Vec::new()
502    } else {
503        vm.collect_stack_for_trace()
504    };
505    let mem_view = if cfg.enable_memory {
506        vm.collect_memory_for_trace()
507    } else {
508        Vec::new()
509    };
510    let return_data = if cfg.enable_return_data {
511        vm.current_call_frame.sub_return_data.clone()
512    } else {
513        Bytes::new()
514    };
515
516    build_step(
517        cfg,
518        target as u64,
519        Opcode::JUMPDEST as u8,
520        gas,
521        gas_cost::JUMPDEST,
522        depth,
523        refund,
524        &stack_view,
525        &mem_view,
526        mem_size,
527        &return_data,
528        None,
529    )
530}