Skip to main content

ethrex_levm/opcode_handlers/
push.rs

1//! # Stack push operations
2//!
3//! Includes the following opcodes:
4//!   - `PUSH0`
5//!   - `PUSH1` to `PUSH32`
6
7use crate::{
8    errors::{InternalError, OpcodeResult, VMError},
9    gas_cost,
10    opcode_handlers::OpcodeHandler,
11    vm::VM,
12};
13use ethrex_common::{types::BYTECODE_PADDING, utils::u256_from_big_endian_const};
14
15/// Implementation for the `PUSH0` opcode.
16pub struct OpPush0Handler;
17impl OpcodeHandler for OpPush0Handler {
18    #[inline(always)]
19    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
20        vm.current_call_frame
21            .increase_consumed_gas(gas_cost::PUSH0)?;
22
23        vm.current_call_frame.stack.push_zero()?;
24
25        Ok(OpcodeResult::Continue)
26    }
27}
28
29/// Implementation for the `PUSHn` opcode.
30pub struct OpPushHandler<const N: usize>;
31impl<const N: usize> OpcodeHandler for OpPushHandler<N> {
32    #[inline(always)]
33    fn eval(vm: &mut VM<'_>) -> Result<OpcodeResult, VMError> {
34        // PUSHn reads up to 32 immediate bytes without a bounds check, relying on
35        // the trailing zero padding appended to every bytecode. After the read the
36        // dispatch loop fetches the next opcode at `pc + N`, which needs one byte
37        // beyond the immediates, so the padding must exceed 32 (i.e. be >= 33).
38        // Keep the unchecked read below sound if the padding ever shrinks.
39        const { assert!(BYTECODE_PADDING > 32) };
40
41        let literal_offset = vm.current_call_frame.pc;
42        // Use a *checked* add for the pc advance, not unchecked `+= N`. Both
43        // compute the same value (pc never overflows in practice), but the
44        // checked form is required for good codegen here: with unchecked/wrapping
45        // arithmetic LLVM can no longer prove the immediate slice length is the
46        // constant `N`, so the `get_unchecked` read below degrades to a
47        // runtime-length memcpy and the PUSH hot loop runs ~2x slower (IPC
48        // collapses 3.4 -> 1.2). The overflow branch is free (perfectly
49        // predicted) and never taken.
50        vm.current_call_frame.pc = literal_offset
51            .checked_add(N)
52            .ok_or(InternalError::Overflow)?;
53        // `pc` is now exactly `literal_offset + N`; reuse it as the immediate end.
54        let literal_end = vm.current_call_frame.pc;
55
56        vm.current_call_frame
57            .increase_consumed_gas(gas_cost::PUSHN)?;
58
59        let bytecode = vm.current_call_frame.bytecode.dispatch_buf();
60        // SAFETY: PUSH only dispatches on a real opcode byte, so
61        // `literal_offset <= bytecode_len`; the buffer is padded with
62        // BYTECODE_PADDING (>= N) trailing zeros, so N bytes are always in bounds.
63        // Immediate bytes past the real code end read as zero, matching EVM
64        // PUSH-past-end semantics (the padding is zeroed).
65        let mut buf = [0u8; N];
66        #[expect(unsafe_code, reason = "read bounded by padded bytecode len")]
67        buf.copy_from_slice(unsafe { bytecode.get_unchecked(literal_offset..literal_end) });
68        let value = u256_from_big_endian_const(buf);
69        vm.current_call_frame.stack.push(value)?;
70
71        Ok(OpcodeResult::Continue)
72    }
73}