tempo_precompiles/storage/types/mod.rs
1mod slot;
2pub use slot::*;
3
4pub mod mapping;
5pub use mapping::*;
6
7mod bytes_like;
8mod primitives;
9pub mod vec;
10
11use crate::{error::Result, storage::StorageOps};
12use alloy::primitives::U256;
13
14/// Describes how a type is laid out in EVM storage.
15///
16/// This determines whether a type can be packed with other fields
17/// and how many storage slots it occupies.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Layout {
20 /// Single slot, N bytes (1-32). Can be packed with other fields if N < 32.
21 ///
22 /// Used for primitive types like integers, booleans, and addresses.
23 Bytes(usize),
24
25 /// Occupies N full slots (each 32 bytes). Cannot be packed.
26 ///
27 /// Used for structs, fixed-size arrays, and dynamic types.
28 Slots(usize),
29}
30
31impl Layout {
32 /// Returns true if this field can be packed with adjacent fields.
33 pub const fn is_packable(&self) -> bool {
34 match self {
35 // TODO(rusowsky): use `Self::Bytes(n) => *n < 32` to reduce gas usage.
36 // Note that this requires a hardfork and must be properly coordinated.
37 Self::Bytes(_) => true,
38 Self::Slots(_) => false,
39 }
40 }
41
42 /// Returns the number of storage slots this type occupies.
43 pub const fn slots(&self) -> usize {
44 match self {
45 Self::Bytes(_) => 1,
46 Self::Slots(n) => *n,
47 }
48 }
49
50 /// Returns the number of bytes this type occupies.
51 ///
52 /// For `Bytes(n)`, returns n.
53 /// For `Slots(n)`, returns n * 32 (each slot is 32 bytes).
54 pub const fn bytes(&self) -> usize {
55 match self {
56 Self::Bytes(n) => *n,
57 Self::Slots(n) => {
58 // Compute n * 32 using repeated addition for const compatibility
59 let (mut i, mut result) = (0, 0);
60 while i < *n {
61 result += 32;
62 i += 1;
63 }
64 result
65 }
66 }
67 }
68}
69
70/// Describes the context in which a `Storable` value is being loaded or stored.
71///
72/// Determines whether the value occupies an entire storage slot or is packed
73/// with other values at a specific byte offset within a slot.
74///
75/// **NOTE:** This type is not an enum to minimize its memory size, but its
76/// implementation is equivalent to:
77/// ```rs
78/// enum LayoutCtx {
79/// Full,
80/// Packed(usize)
81/// }
82/// ```
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[repr(transparent)]
85pub struct LayoutCtx(usize);
86
87impl LayoutCtx {
88 /// Load/store the entire value at `base_slot`.
89 ///
90 /// For writes, this directly overwrites the entire slot without needing SLOAD.
91 /// All `Storable` types support this context.
92 pub const FULL: Self = Self(usize::MAX);
93
94 /// Load/store a packed primitive at the given byte offset within a slot.
95 ///
96 /// For writes, this requires a read-modify-write: SLOAD the current slot value,
97 /// modify the bytes at the offset, then SSTORE back. This preserves other
98 /// packed fields in the same slot.
99 ///
100 /// Only primitive types with `Layout::Bytes(n)` where `n < 32` support this context.
101 pub const fn packed(offset: usize) -> Self {
102 debug_assert!(offset < 32);
103 Self(offset)
104 }
105
106 /// Get the packed offset, returns `None` for `Full`
107 #[inline]
108 pub const fn packed_offset(&self) -> Option<usize> {
109 if self.0 == usize::MAX {
110 None
111 } else {
112 Some(self.0)
113 }
114 }
115}
116
117/// Helper trait to access storage layout information without requiring const generic parameter.
118///
119/// This trait exists to allow the derive macro to query the layout and size of field types
120/// during layout computation, before the slot count is known.
121pub trait StorableType {
122 /// Describes how this type is laid out in storage.
123 ///
124 /// - Primitives use `Layout::Bytes(N)` where N is their size
125 /// - Dynamic types (String, Bytes, Vec) use `Layout::Slots(1)`
126 /// - Structs and arrays use `Layout::Slots(N)` where N is the slot count
127 const LAYOUT: Layout;
128
129 /// Number of storage slots this type takes.
130 const SLOTS: usize = Self::LAYOUT.slots();
131
132 /// Number of bytes this type takes.
133 const BYTES: usize = Self::LAYOUT.bytes();
134
135 /// Whether this type can be packed with adjacent fields.
136 const IS_PACKABLE: bool = Self::LAYOUT.is_packable();
137}
138
139/// Trait for types that can be stored/loaded from EVM storage.
140///
141/// This trait provides a flexible abstraction for reading and writing Rust types
142/// to EVM storage. Types can occupy one or more consecutive storage slots, enabling
143/// support for both simple values (Address, U256, bool) and complex multi-slot types
144/// (structs, fixed arrays).
145///
146/// # Type Parameter
147///
148/// - `SLOTS`: The number of consecutive storage slots this type occupies.
149/// For single-word types (Address, U256, bool), this is `1`.
150/// For fixed-size arrays, this equals the number of elements.
151/// For user-defined structs, this a number between `1` and the number of fields, which depends on slot packing.
152///
153/// # Storage Layout
154///
155/// For a type with `SLOTS = 3` starting at `base_slot`:
156/// - Slot 0: `base_slot + 0`
157/// - Slot 1: `base_slot + 1`
158/// - Slot 2: `base_slot + 2`
159///
160/// # Safety
161///
162/// Implementations must ensure that:
163/// - Round-trip conversions preserve data: `load(store(x)) == Ok(x)`
164/// - `SLOTS` accurately reflects the number of slots used
165/// - `store` and `load` access exactly `SLOTS` consecutive slots
166/// - `to_evm_words` and `from_evm_words` produce/consume exactly `SLOTS` words
167pub trait Storable<const SLOTS: usize>: Sized + StorableType {
168 /// Load this type from storage starting at the given base slot.
169 ///
170 /// Reads `SLOTS` consecutive slots starting from `base_slot`.
171 ///
172 /// # Context
173 ///
174 /// - `LayoutCtx::FULL`: Load the entire value from `base_slot` (and subsequent slots if multi-slot)
175 /// - `LayoutCtx::packed(offset)`: Load a packed primitive from byte `offset` within `base_slot`
176 ///
177 /// # Errors
178 ///
179 /// Returns an error if:
180 /// - Storage read fails
181 /// - Data cannot be decoded into this type
182 /// - Context is invalid for this type (e.g., `Packed` for a multi-slot type)
183 fn load<S: StorageOps>(storage: &mut S, base_slot: U256, ctx: LayoutCtx) -> Result<Self>;
184
185 /// Store this type to storage starting at the given base slot.
186 ///
187 /// Writes `SLOTS` consecutive slots starting from `base_slot`.
188 ///
189 /// # Context
190 ///
191 /// - `LayoutCtx::FULL`: Write the entire value to `base_slot` (overwrites full slot)
192 /// - `LayoutCtx::packed(offset)`: Write a packed primitive at byte `offset` (read-modify-write)
193 ///
194 /// # Errors
195 ///
196 /// Returns an error if:
197 /// - Storage write fails
198 /// - Context is invalid for this type (e.g., `Packed` for a multi-slot type)
199 fn store<S: StorageOps>(&self, storage: &mut S, base_slot: U256, ctx: LayoutCtx) -> Result<()>;
200
201 /// Delete this type from storage (set all slots to zero).
202 ///
203 /// Sets `SLOTS` consecutive slots to zero, starting from `base_slot`.
204 ///
205 /// # Context
206 ///
207 /// - `LayoutCtx::FULL`: Clear entire slot(s) by writing zero
208 /// - `LayoutCtx::packed(offset)`: Clear only the bytes at the offset (read-modify-write)
209 ///
210 /// The default implementation handles both contexts appropriately.
211 ///
212 /// # Errors
213 ///
214 /// Returns an error if:
215 /// - Storage write fails
216 /// - Context is invalid for this type
217 fn delete<S: StorageOps>(storage: &mut S, base_slot: U256, ctx: LayoutCtx) -> Result<()> {
218 match ctx.packed_offset() {
219 None => {
220 for offset in 0..SLOTS {
221 storage.sstore(base_slot + U256::from(offset), U256::ZERO)?;
222 }
223 Ok(())
224 }
225 Some(offset) => {
226 // For packed context, we need to preserve other fields in the slot
227 let bytes = Self::BYTES;
228 let current = storage.sload(base_slot)?;
229 let cleared = crate::storage::packing::zero_packed_value(current, offset, bytes)?;
230 storage.sstore(base_slot, cleared)
231 }
232 }
233 }
234
235 /// Encode this type to an array of U256 words.
236 ///
237 /// Returns exactly `SLOTS` words, where each word represents one storage slot.
238 /// For single-slot types (`SLOTS = 1`), returns a single-element array.
239 /// For multi-slot types, each array element corresponds to one slot's data.
240 ///
241 /// # Packed Storage
242 ///
243 /// When multiple small fields are packed into a single slot, they are
244 /// positioned and combined into a single U256 word according to their
245 /// byte offsets. The derive macro handles this automatically.
246 fn to_evm_words(&self) -> Result<[U256; SLOTS]>;
247
248 /// Decode this type from an array of U256 words.
249 ///
250 /// Accepts exactly `N` words, where each word represents one storage slot.
251 /// Constructs the complete type from all provided words.
252 ///
253 /// # Packed Storage
254 ///
255 /// When multiple small fields are packed into a single slot, they are
256 /// extracted from the appropriate word using bit shifts and masks.
257 /// The derive macro handles this automatically.
258 fn from_evm_words(words: [U256; SLOTS]) -> Result<Self>;
259
260 /// Test helper to ensure `LAYOUT` and `SLOTS` are in sync.
261 fn validate_layout() {
262 debug_assert_eq!(<Self as StorableType>::SLOTS, SLOTS)
263 }
264}
265
266/// Trait for types that can be used as storage mapping keys.
267///
268/// Keys are hashed using keccak256 along with the mapping's base slot
269/// to determine the final storage location. This trait provides the
270/// byte representation used in that hash.
271pub trait StorageKey {
272 fn as_storage_bytes(&self) -> impl AsRef<[u8]>;
273}