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