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}