Skip to main content

tempo_precompiles/storage/types/
mod.rs

1//! Storable type system for EVM storage.
2//!
3//! Defines the core traits ([`StorableType`], [`Storable`], [`FromWord`], [`Packable`])
4//! and types ([`Slot`], [`Mapping`], [`Set`], [`vec::VecHandler`], [`array::ArrayHandler`]) that
5//! enable type-safe access to EVM storage slots with automatic packing.
6
7mod slot;
8pub use slot::*;
9
10pub mod mapping;
11pub use mapping::*;
12
13pub mod array;
14pub mod set;
15pub mod vec;
16pub use set::{Set, SetHandler};
17
18pub mod bytes_like;
19mod primitives;
20
21use crate::{
22    error::Result,
23    storage::{StorageOps, packing},
24};
25use alloy::primitives::{Address, U256, keccak256};
26use std::{cell::RefCell, collections::HashMap, hash::Hash};
27
28/// Describes how a type is laid out in EVM storage.
29///
30/// This determines whether a type can be packed with other fields
31/// and how many storage slots it occupies.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Layout {
34    /// Single slot, N bytes (1-32). Can be packed with other fields if N < 32.
35    ///
36    /// Used for primitive types like integers, booleans, and addresses.
37    Bytes(usize),
38
39    /// Occupies N full slots (each 32 bytes). Cannot be packed.
40    ///
41    /// Used for structs, fixed-size arrays, and dynamic types.
42    Slots(usize),
43}
44
45impl Layout {
46    /// Returns true if this field can be packed with adjacent fields.
47    pub const fn is_packable(&self) -> bool {
48        match self {
49            Self::Bytes(n) => *n < 32,
50            Self::Slots(_) => false,
51        }
52    }
53
54    /// Returns the number of storage slots this type occupies.
55    pub const fn slots(&self) -> usize {
56        match self {
57            Self::Bytes(_) => 1,
58            Self::Slots(n) => *n,
59        }
60    }
61
62    /// Returns the number of bytes this type occupies. For `Bytes(n)`, returns n.
63    /// For `Slots(n)`, returns n * 32 (each slot is 32 bytes).
64    pub const fn bytes(&self) -> usize {
65        match self {
66            Self::Bytes(n) => *n,
67            Self::Slots(n) => {
68                // Compute n * 32 using repeated addition for const compatibility
69                let (mut i, mut result) = (0, 0);
70                while i < *n {
71                    result += 32;
72                    i += 1;
73                }
74                result
75            }
76        }
77    }
78}
79
80/// Describes the context in which a storable value is being loaded or stored.
81///
82/// Determines whether the value occupies an entire storage slot or is packed
83/// with other values at a specific byte offset within a slot.
84///
85/// **NOTE:** This type is not an enum to minimize its memory size, but its
86/// implementation is equivalent to:
87/// ```rs
88/// enum LayoutCtx {
89///    Full,
90///    Packed(usize)
91/// }
92/// ```
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94#[repr(transparent)]
95pub struct LayoutCtx(usize);
96
97impl LayoutCtx {
98    /// Load/store the entire value at a given slot.
99    ///
100    /// For writes, this directly overwrites the entire slot without needing SLOAD.
101    /// All storable types support this context.
102    pub const FULL: Self = Self(usize::MAX);
103
104    /// Load/store a packed primitive at the given byte offset within a slot.
105    ///
106    /// For writes, this requires a read-modify-write: SLOAD the current slot value,
107    /// modify the bytes at the offset, then SSTORE back. This preserves other
108    /// packed fields in the same slot.
109    ///
110    /// Only primitive types with `Layout::Bytes(n)` where `n < 32` support this context.
111    pub const fn packed(offset: usize) -> Self {
112        debug_assert!(offset < 32);
113        Self(offset)
114    }
115
116    /// Get the packed offset, returns `None` for `Full`
117    #[inline]
118    pub const fn packed_offset(&self) -> Option<usize> {
119        if self.0 == usize::MAX {
120            None
121        } else {
122            Some(self.0)
123        }
124    }
125}
126
127/// Helper trait to access storage layout information without requiring const generic parameter.
128///
129/// This trait provides compile-time layout information (slot count, byte size, packability)
130/// and a factory method for creating handlers. It enables the derive macro to compute
131/// struct layouts before the final slot count is known.
132///
133/// **NOTE:** Don't need to implement the trait manually. Use `#[derive(Storable)]` instead.
134pub trait StorableType {
135    /// Describes how this type is laid out in storage.
136    ///
137    /// - Primitives use `Layout::Bytes(N)` where N is their size
138    /// - Dynamic types (String, Bytes, Vec) use `Layout::Slots(1)`
139    /// - Structs and arrays use `Layout::Slots(N)` where N is the slot count
140    const LAYOUT: Layout;
141
142    /// Number of storage slots this type takes.
143    const SLOTS: usize = Self::LAYOUT.slots();
144
145    /// Number of bytes this type takes.
146    const BYTES: usize = Self::LAYOUT.bytes();
147
148    /// Whether this type can be packed with adjacent fields.
149    const IS_PACKABLE: bool = Self::LAYOUT.is_packable();
150
151    /// Whether this type stores it's data in its base slot or not.
152    ///
153    /// Dynamic types (`Bytes`, `String`, `Vec`) store data at keccak256-addressed
154    /// slots and need special cleanup. Non-dynamic types just zero their slots.
155    const IS_DYNAMIC: bool = false;
156
157    /// The handler type that provides storage access for this type.
158    ///
159    /// For primitives, this is `Slot<Self>`.
160    /// For mappings, this is `Self` (mappings are their own handlers).
161    /// For user-defined structs, this is a generated handler type (e.g., `MyStructHandler`).
162    type Handler;
163
164    /// Creates a handler for this type at the given storage location.
165    fn handle(slot: U256, ctx: LayoutCtx, address: Address) -> Self::Handler;
166}
167
168/// Abstracts reading, writing, and deleting values for [`Storable`] types.
169pub trait Handler<T: Storable> {
170    /// Reads the value from storage.
171    fn read(&self) -> Result<T>;
172
173    /// Writes the value to storage.
174    fn write(&mut self, value: T) -> Result<()>;
175
176    /// Deletes the value from storage (sets to zero).
177    fn delete(&mut self) -> Result<()>;
178
179    /// Reads the value from storage.
180    fn t_read(&self) -> Result<T>;
181
182    /// Writes the value to storage.
183    fn t_write(&mut self, value: T) -> Result<()>;
184
185    /// Deletes the value from storage (sets to zero).
186    fn t_delete(&mut self) -> Result<()>;
187}
188
189/// High-level storage operations for storable types.
190///
191/// This trait provides storage I/O operations: load, store, delete.
192/// Types implement their own logic for handling packed vs full-slot contexts.
193pub trait Storable: StorableType + Sized {
194    /// Load this type from storage at the given slot.
195    fn load<S: StorageOps>(storage: &S, slot: U256, ctx: LayoutCtx) -> Result<Self>;
196
197    /// Store this type to storage at the given slot.
198    fn store<S: StorageOps>(&self, storage: &mut S, slot: U256, ctx: LayoutCtx) -> Result<()>;
199
200    /// Delete this type from storage (set to zero).
201    ///
202    /// Default implementation handles both full-slot and packed contexts:
203    /// - `LayoutCtx::FULL`: Writes zero to all `Self::SLOTS` consecutive slots
204    /// - `LayoutCtx::packed(offset)`: Clears only the bytes at the offset (read-modify-write)
205    fn delete<S: StorageOps>(storage: &mut S, slot: U256, ctx: LayoutCtx) -> Result<()> {
206        match ctx.packed_offset() {
207            None => {
208                for offset in 0..Self::SLOTS {
209                    storage.store(slot + U256::from(offset), U256::ZERO)?;
210                }
211                Ok(())
212            }
213            Some(offset) => {
214                // For packed context, we need to preserve other fields in the slot
215                let bytes = Self::BYTES;
216                let current = storage.load(slot)?;
217                let cleared = crate::storage::packing::delete_from_word(current, offset, bytes)?;
218                storage.store(slot, cleared)
219            }
220        }
221    }
222}
223
224/// Private module to seal the `Packable` trait.
225#[allow(unnameable_types)]
226pub(in crate::storage::types) mod sealed {
227    /// Marker trait to prevent external implementations of `Packable`.
228    pub trait OnlyPrimitives {}
229}
230
231/// Trait for types that can be packed into EVM storage slots.
232///
233/// This trait is **sealed** - it can only be implemented within this crate
234/// for primitive types that fit in a single U256 word.
235///
236/// # Usage
237///
238/// `Packable` is used by the storage packing system to efficiently pack multiple
239/// small values into a single 32-byte storage slot.
240///
241/// # Warning
242///
243/// `IS_PACKABLE` must be true for the implementing type (enforced at compile time)
244pub trait Packable: FromWord + StorableType {}
245
246/// Trait for primitive types that fit into a single EVM storage slot.
247///
248/// Implementations must produce right-aligned U256 values (data in low bytes)
249/// to match EVM storage slot layout expectations.
250///
251/// # Warning
252///
253/// Round-trip conversions must preserve data: `from_word(to_word(x)) == x`
254pub trait FromWord: sealed::OnlyPrimitives {
255    /// Encode this type to a single U256 word.
256    fn to_word(&self) -> U256;
257
258    /// Decode this type from a single U256 word.
259    fn from_word(word: U256) -> Result<Self>
260    where
261        Self: Sized;
262}
263
264/// Blanket implementation of `Storable` for all `Packable` types.
265///
266/// This provides a unified load/store implementation for all primitive types,
267/// handling both full-slot and packed contexts automatically.
268impl<T: Packable> Storable for T {
269    #[inline]
270    fn load<S: StorageOps>(storage: &S, slot: U256, ctx: LayoutCtx) -> Result<Self> {
271        const { assert!(T::IS_PACKABLE, "Packable requires IS_PACKABLE to be true") };
272
273        match ctx.packed_offset() {
274            None => storage.load(slot).and_then(Self::from_word),
275            Some(offset) => {
276                let slot_value = storage.load(slot)?;
277                packing::extract_from_word(slot_value, offset, Self::BYTES)
278            }
279        }
280    }
281
282    #[inline]
283    fn store<S: StorageOps>(&self, storage: &mut S, slot: U256, ctx: LayoutCtx) -> Result<()> {
284        const { assert!(T::IS_PACKABLE, "Packable requires IS_PACKABLE to be true") };
285
286        match ctx.packed_offset() {
287            None => storage.store(slot, self.to_word()),
288            Some(offset) => {
289                let current = storage.load(slot)?;
290                let updated = packing::insert_into_word(current, self, offset, Self::BYTES)?;
291                storage.store(slot, updated)
292            }
293        }
294    }
295}
296
297/// Trait for types that can be used as storage mapping keys.
298///
299/// Keys are hashed using keccak256 along with the mapping's base slot
300/// to determine the final storage location. This trait provides the
301/// byte representation used in that hash.
302///
303/// # Sealed to single-word primitives
304///
305/// Only types that implement `sealed::OnlyPrimitives` (single-word types ≤32 bytes)
306/// can be mapping keys. This prevents arrays, structs, and dynamic types from being
307/// used as keys — matching Solidity's restriction to value types.
308///
309/// # Encoding
310///
311/// Mapping slots are computed as `keccak256(bytes32(key) | bytes32(slot))`, where the
312/// key's raw bytes are left-padded to 32 bytes and the slot is appended in big-endian.
313///
314/// This differs from Solidity's `keccak256(abi.encode(key, slot))`, where signed integers
315/// are sign-extended and `bytesN` (N < 32) are right-padded. Per-type equivalence:
316///
317/// - **Unsigned integers, `Address`, `bytes32`**: identical — both zero-left-pad.
318/// - **Signed integers**: diverges — Solidity sign-extends negative values to 32 bytes,
319///   we zero-left-pad the two's complement representation.
320/// - **`bytesN` (N < 32)**: diverges — Solidity right-pads, we left-pad.
321///
322/// This is **not** a soundness issue — there are no slot collision risks — but off-chain
323/// tools that reconstruct storage slots using Solidity's `abi.encode` rules will compute
324/// different locations for the divergent types. View functions should be used instead.
325pub trait StorageKey: sealed::OnlyPrimitives {
326    /// Returns key bytes for storage slot computation.
327    fn as_storage_bytes(&self) -> impl AsRef<[u8]>;
328
329    /// Compute storage slot for a mapping with this key.
330    ///
331    /// Left-pads the key to 32 bytes, concatenates with the slot, and hashes.
332    fn mapping_slot(&self, slot: U256) -> U256 {
333        let key_bytes = self.as_storage_bytes();
334        let key_bytes = key_bytes.as_ref();
335        debug_assert!(key_bytes.len() <= 32);
336
337        let mut buf = [0u8; 64];
338        buf[32 - key_bytes.len()..32].copy_from_slice(key_bytes);
339        buf[32..].copy_from_slice(&slot.to_be_bytes::<32>());
340
341        U256::from_be_bytes(keccak256(buf).0)
342    }
343}
344
345/// Cache for computed handlers with stable references.
346///
347/// Enables `Index` implementations on handlers by storing child handlers and
348/// returning references that remain valid across insertions.
349///
350/// Uses `RefCell` for interior mutability with runtime borrow checking.
351/// Re-entrant access will panic rather than cause undefined behavior.
352#[derive(Debug, Default)]
353pub(super) struct HandlerCache<K, H> {
354    inner: RefCell<HashMap<K, Box<H>>>,
355}
356
357impl<K, H> HandlerCache<K, H> {
358    /// Creates a new empty handler cache.
359    #[inline]
360    pub(super) fn new() -> Self {
361        Self {
362            inner: RefCell::new(HashMap::new()),
363        }
364    }
365}
366
367impl<K, H> Clone for HandlerCache<K, H> {
368    /// Creates a new empty cache (cached handlers are not cloned).
369    fn clone(&self) -> Self {
370        Self::new()
371    }
372}
373
374impl<K: Hash + Eq + Clone, H> HandlerCache<K, H> {
375    /// Returns a reference to a lazily initialized handler for the given key.
376    #[inline]
377    pub(super) fn get_or_insert(&self, key: &K, f: impl FnOnce() -> H) -> &H {
378        let mut cache = self.inner.borrow_mut();
379        // Lookup first to avoid cloning on cache hit
380        if let Some(boxed) = cache.get(key) {
381            // SAFETY: Box provides stable heap address. Cache is append-only.
382            return unsafe { &*(boxed.as_ref() as *const H) };
383        }
384        let boxed = cache.entry(key.clone()).or_insert_with(|| Box::new(f()));
385        // SAFETY: Box provides stable heap address. Cache is append-only.
386        unsafe { &*(boxed.as_ref() as *const H) }
387    }
388
389    /// Returns a mutable reference to a lazily initialized handler for the given key.
390    #[inline]
391    pub(super) fn get_or_insert_mut(&mut self, key: &K, f: impl FnOnce() -> H) -> &mut H {
392        let mut cache = self.inner.borrow_mut();
393        // Lookup first to avoid cloning on cache hit
394        if let Some(boxed) = cache.get_mut(key) {
395            // SAFETY: Box provides stable heap address. Cache is append-only. `&mut self` ensures exclusive access.
396            return unsafe { &mut *(boxed.as_mut() as *mut H) };
397        }
398        let boxed = cache.entry(key.clone()).or_insert_with(|| Box::new(f()));
399        // SAFETY: Box provides stable heap address. Cache is append-only. `&mut self` ensures exclusive access.
400        unsafe { &mut *(boxed.as_mut() as *mut H) }
401    }
402}