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