Skip to main content

ethrex_levm/
memory.rs

1use std::{cell::RefCell, rc::Rc};
2
3use crate::{
4    constants::{MEMORY_EXPANSION_QUOTIENT, WORD_SIZE_IN_BYTES_U64, WORD_SIZE_IN_BYTES_USIZE},
5    errors::{ExceptionalHalt, InternalError, VMError},
6};
7use ExceptionalHalt::OutOfBounds;
8use bytes::Bytes;
9use ethrex_common::{
10    U256,
11    utils::{u256_from_big_endian_const, u256_to_big_endian},
12};
13
14/// A cheaply clonable callframe-shared memory buffer.
15///
16/// When a new callframe is created a RC clone of this memory is made, with the current base offset at the length of the buffer at that time.
17#[derive(Debug, Clone)]
18pub struct Memory {
19    pub buffer: Rc<RefCell<Vec<u8>>>,
20    pub len: usize,
21    current_base: usize,
22}
23
24impl Memory {
25    #[inline]
26    pub fn new() -> Self {
27        Self {
28            buffer: Rc::new(RefCell::new(Vec::new())),
29            len: 0,
30            current_base: 0,
31        }
32    }
33
34    /// Resets this memory so its buffer can be reused from a pool by the next transaction:
35    /// drops all contents (length → 0, capacity retained) and rebases to 0.
36    ///
37    /// Truncating the buffer to length 0 is REQUIRED for correctness, not just hygiene:
38    /// [`Memory::resize`] only zero-fills bytes grown *past* `buffer.len()`, so handing a
39    /// non-empty buffer to the next tx would expose stale data from the previous one (a
40    /// consensus bug). Capacity is kept so the grown allocation is reused.
41    #[inline]
42    pub fn reset_for_reuse(&mut self) {
43        self.buffer.borrow_mut().clear();
44        self.len = 0;
45        self.current_base = 0;
46    }
47
48    /// Gets the Memory for the next children callframe.
49    #[inline]
50    pub fn next_memory(&self) -> Memory {
51        let mut mem = self.clone();
52        mem.current_base = mem.buffer.borrow().len();
53        mem.len = 0;
54        mem
55    }
56
57    /// Cleans the memory from base onwards, this must be used in callframes when handling returns.
58    ///
59    /// On the callframe that is about to be dropped.
60    #[inline]
61    pub fn clean_from_base(&self) {
62        #[expect(unsafe_code)]
63        unsafe {
64            self.buffer
65                .borrow_mut()
66                .get_unchecked_mut(self.current_base..(self.current_base.wrapping_add(self.len)))
67                .fill(0);
68        }
69    }
70
71    /// Truncates the memory back to base. This is crucial for constrained
72    /// memory in zkVMs. The memory is not freed, but rather shrunk in `len`,
73    /// so that the already allocated `capacity` is reused.
74    #[cfg(target_arch = "riscv64")]
75    #[inline]
76    pub fn truncate_to_base(&self) {
77        self.buffer.borrow_mut().truncate(self.current_base);
78    }
79
80    /// Returns the len of the current memory, from the current base.
81    #[inline]
82    pub fn len(&self) -> usize {
83        self.len
84    }
85
86    #[inline]
87    pub fn is_empty(&self) -> bool {
88        self.len() == 0
89    }
90
91    /// Returns a copy of the live byte slice for this frame (from `current_base` to
92    /// `current_base + len`).  Used by the struct-log tracer for memory capture.
93    pub fn live_bytes(&self) -> Vec<u8> {
94        if self.len == 0 {
95            return Vec::new();
96        }
97        let buf = self.buffer.borrow();
98        let end = self.current_base.saturating_add(self.len);
99        buf.get(self.current_base..end)
100            .map(<[u8]>::to_vec)
101            .unwrap_or_default()
102    }
103
104    /// Resizes the from the current base to fit the memory specified at new_memory_size.
105    ///
106    /// Note: new_memory_size is increased to the next 32 byte multiple.
107    #[inline(always)]
108    pub fn resize(&mut self, new_memory_size: usize) -> Result<(), VMError> {
109        if new_memory_size == 0 {
110            return Ok(());
111        }
112
113        let new_memory_size = new_memory_size
114            .checked_next_multiple_of(WORD_SIZE_IN_BYTES_USIZE)
115            .ok_or(OutOfBounds)?;
116
117        let current_len = self.len();
118
119        if new_memory_size <= current_len {
120            return Ok(());
121        }
122
123        self.len = new_memory_size;
124
125        let mut buffer = self.buffer.borrow_mut();
126
127        #[allow(clippy::arithmetic_side_effects)]
128        let real_new_memory_size = new_memory_size + self.current_base;
129
130        if real_new_memory_size > buffer.len() {
131            // when resizing, avoid really small resizes.
132            let new_size = real_new_memory_size.next_multiple_of(64);
133            buffer.resize(new_size, 0);
134        }
135
136        Ok(())
137    }
138
139    /// Load `size` bytes from the given offset. Returning a Bytes.
140    #[inline]
141    pub fn load_range(&mut self, offset: usize, size: usize) -> Result<Bytes, VMError> {
142        if size == 0 {
143            return Ok(Bytes::new());
144        }
145
146        let new_size = offset.checked_add(size).ok_or(OutOfBounds)?;
147        self.resize(new_size)?;
148
149        let true_offset = offset.wrapping_add(self.current_base);
150
151        let buf = self.buffer.borrow();
152
153        // SAFETY: resize already makes sure bounds are correct.
154        #[allow(unsafe_code)]
155        unsafe {
156            Ok(Bytes::copy_from_slice(buf.get_unchecked(
157                true_offset..(true_offset.wrapping_add(size)),
158            )))
159        }
160    }
161
162    /// Borrow `size` bytes from the given offset and pass them to `f`, without
163    /// allocating a `Bytes` copy of the range.
164    ///
165    /// `load_range` reads through `self.buffer.borrow()`, whose `Ref` guard cannot
166    /// outlive this call — so a `-> &[u8]` accessor can't be written. Callers that
167    /// only need to read the range (e.g. hashing) take the borrow via this closure
168    /// instead. Semantics match `load_range` exactly, including zero-padding reads
169    /// past the current length (handled by `resize`).
170    #[inline]
171    pub fn with_range<R>(
172        &mut self,
173        offset: usize,
174        size: usize,
175        f: impl FnOnce(&[u8]) -> R,
176    ) -> Result<R, VMError> {
177        if size == 0 {
178            return Ok(f(&[]));
179        }
180
181        let new_size = offset.checked_add(size).ok_or(OutOfBounds)?;
182        self.resize(new_size)?;
183
184        let true_offset = offset.wrapping_add(self.current_base);
185
186        let buf = self.buffer.borrow();
187
188        // SAFETY: resize already makes sure bounds are correct.
189        #[allow(unsafe_code)]
190        let range = unsafe { buf.get_unchecked(true_offset..(true_offset.wrapping_add(size))) };
191        Ok(f(range))
192    }
193
194    /// Load N bytes from the given offset.
195    #[inline(always)]
196    pub fn load_range_const<const N: usize>(&mut self, offset: usize) -> Result<[u8; N], VMError> {
197        let new_size = offset.checked_add(N).ok_or(OutOfBounds)?;
198        self.resize(new_size)?;
199
200        let true_offset = offset.checked_add(self.current_base).ok_or(OutOfBounds)?;
201
202        let buf = self.buffer.borrow();
203        // SAFETY: resize already makes sure bounds are correct.
204        #[allow(unsafe_code)]
205        unsafe {
206            Ok(*buf
207                .get_unchecked(true_offset..(true_offset.wrapping_add(N)))
208                .as_ptr()
209                .cast::<[u8; N]>())
210        }
211    }
212
213    /// Load a word from at the given offset.
214    #[inline(always)]
215    pub fn load_word(&mut self, offset: usize) -> Result<U256, VMError> {
216        let value: [u8; 32] = self.load_range_const(offset)?;
217        Ok(u256_from_big_endian_const(value))
218    }
219
220    /// Stores the given data and data size at the given offset.
221    ///
222    /// Internal use.
223    #[inline(always)]
224    fn store(&self, data: &[u8], at_offset: usize, data_size: usize) -> Result<(), VMError> {
225        if data_size == 0 {
226            return Ok(());
227        }
228
229        let real_offset = self.current_base.wrapping_add(at_offset);
230
231        let mut buffer = self.buffer.borrow_mut();
232
233        let real_data_size = data_size.min(data.len());
234
235        // SAFETY: Used internally, resize always called before this function.
236        #[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
237        #[allow(unsafe_code)]
238        unsafe {
239            std::ptr::copy_nonoverlapping(
240                data.get_unchecked(..real_data_size).as_ptr(),
241                buffer
242                    .get_unchecked_mut(real_offset..(real_offset + real_data_size))
243                    .as_mut_ptr(),
244                real_data_size,
245            );
246        }
247
248        Ok(())
249    }
250
251    /// Stores the given data at the given offset.
252    #[inline(always)]
253    pub fn store_data(&mut self, offset: usize, data: &[u8]) -> Result<(), VMError> {
254        if data.is_empty() {
255            return Ok(());
256        }
257        let new_size = offset.checked_add(data.len()).ok_or(OutOfBounds)?;
258        self.resize(new_size)?;
259        self.store(data, offset, data.len())
260    }
261
262    /// Stores data and zero-pads up to total_size at the given offset.
263    #[inline(always)]
264    pub fn store_data_zero_padded(
265        &mut self,
266        offset: usize,
267        data: &[u8],
268        total_size: usize,
269    ) -> Result<(), VMError> {
270        if total_size == 0 {
271            return Ok(());
272        }
273
274        let new_size = offset.checked_add(total_size).ok_or(OutOfBounds)?;
275        self.resize(new_size)?;
276
277        let copy_size = data.len().min(total_size);
278        if copy_size > 0 {
279            self.store(data, offset, copy_size)?;
280        }
281
282        #[allow(clippy::arithmetic_side_effects)]
283        if copy_size < total_size {
284            // SAFETY: copy_size < total_size and offset + total_size didn't overflow (checked above),
285            // so offset + copy_size cannot overflow.
286            let zero_offset = offset.wrapping_add(copy_size);
287            let zero_size = total_size - copy_size;
288            let real_offset = self.current_base.wrapping_add(zero_offset);
289            let mut buffer = self.buffer.borrow_mut();
290
291            // resize ensures bounds are correct
292            #[expect(unsafe_code)]
293            unsafe {
294                buffer
295                    .get_unchecked_mut(real_offset..real_offset.wrapping_add(zero_size))
296                    .fill(0);
297            }
298        }
299
300        Ok(())
301    }
302
303    /// Stores a word at the given offset, resizing memory if needed.
304    #[inline(always)]
305    pub fn store_word(&mut self, offset: usize, word: U256) -> Result<(), VMError> {
306        let new_size: usize = offset
307            .checked_add(WORD_SIZE_IN_BYTES_USIZE)
308            .ok_or(OutOfBounds)?;
309
310        self.resize(new_size)?;
311        self.store(&u256_to_big_endian(word), offset, WORD_SIZE_IN_BYTES_USIZE)?;
312        Ok(())
313    }
314
315    /// Copies memory within 2 offsets. Like a memmove.
316    ///
317    /// Resizes if needed, because one can copy from "expanded memory", which is initialized with zeroes.
318    pub fn copy_within(
319        &mut self,
320        from_offset: usize,
321        to_offset: usize,
322        size: usize,
323    ) -> Result<(), VMError> {
324        if size == 0 {
325            return Ok(());
326        }
327
328        self.resize(
329            to_offset
330                .max(from_offset)
331                .checked_add(size)
332                .ok_or(InternalError::Overflow)?,
333        )?;
334
335        let true_from_offset = from_offset
336            .checked_add(self.current_base)
337            .ok_or(OutOfBounds)?;
338
339        let true_to_offset = to_offset
340            .checked_add(self.current_base)
341            .ok_or(OutOfBounds)?;
342        let mut buffer = self.buffer.borrow_mut();
343
344        buffer.copy_within(
345            true_from_offset
346                ..(true_from_offset
347                    .checked_add(size)
348                    .ok_or(InternalError::Overflow)?),
349            true_to_offset,
350        );
351
352        Ok(())
353    }
354
355    #[inline(always)]
356    pub fn store_zeros(&mut self, offset: usize, size: usize) -> Result<(), VMError> {
357        if size == 0 {
358            return Ok(());
359        }
360
361        let new_size = offset.checked_add(size).ok_or(OutOfBounds)?;
362        self.resize(new_size)?;
363
364        let real_offset = self.current_base.wrapping_add(offset);
365        let mut buffer = self.buffer.borrow_mut();
366
367        // resize ensures bounds are correct
368        #[expect(unsafe_code)]
369        unsafe {
370            buffer
371                .get_unchecked_mut(real_offset..(real_offset.wrapping_add(size)))
372                .fill(0);
373        }
374
375        Ok(())
376    }
377}
378
379impl Default for Memory {
380    fn default() -> Self {
381        Self::new()
382    }
383}
384
385/// When a memory expansion is triggered, only the additional bytes of memory
386/// must be paid for.
387#[inline]
388pub fn expansion_cost(new_memory_size: usize, current_memory_size: usize) -> Result<u64, VMError> {
389    let cost = if new_memory_size <= current_memory_size {
390        0
391    } else {
392        // We already know new_memory_size > current_memory_size,
393        // and cost(x) > cost(y) where x > y, so cost should not underflow.
394        cost(new_memory_size)?.wrapping_sub(cost(current_memory_size)?)
395    };
396    Ok(cost)
397}
398
399/// The total cost for a given memory size.
400/// Gas cost should always be computed in u64
401#[inline]
402fn cost(memory_size: usize) -> Result<u64, VMError> {
403    let memory_size = u64::try_from(memory_size).map_err(|_| InternalError::TypeConversion)?;
404
405    // memory size measured in 32 byte words
406    let words = memory_size.div_ceil(WORD_SIZE_IN_BYTES_U64);
407
408    // Cost(words) ≈ floor(words^2 / q) + 3 * words
409    // For this to overflow memory size in words should be 2^32, which is impossible.
410    #[expect(clippy::arithmetic_side_effects)]
411    let gas_cost = words * words / MEMORY_EXPANSION_QUOTIENT + 3 * words;
412
413    Ok(gas_cost)
414}
415
416#[inline]
417pub fn calculate_memory_size(offset: usize, size: usize) -> Result<usize, VMError> {
418    if size == 0 {
419        return Ok(0);
420    }
421
422    offset
423        .checked_add(size)
424        .and_then(|sum| sum.checked_next_multiple_of(WORD_SIZE_IN_BYTES_USIZE))
425        .ok_or(OutOfBounds.into())
426}