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}