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