tempo_precompiles/storage/types/
array.rs

1//! Fixed-size array handler for the storage traits.
2//!
3//! # Storage Layout
4//!
5//! Fixed-size arrays `[T; N]` use Solidity-compatible array storage:
6//! - **Base slot**: Arrays start directly at `base_slot` (not at keccak256)
7//! - **Data slots**: Elements are stored sequentially, either packed or unpacked
8//!
9//! ## Packing Strategy
10//!
11//! - **Packed**: When `T::BYTES <= 16`, multiple elements fit in one slot
12//! - **Unpacked**: When `T::BYTES > 16` or doesn't divide 32, each element uses full slot(s)
13
14use alloy::primitives::{Address, U256};
15use std::marker::PhantomData;
16use tempo_precompiles_macros;
17
18use crate::{
19    error::Result,
20    storage::{Handler, LayoutCtx, Storable, StorableType, packing, types::Slot},
21};
22
23// fixed-size arrays: [T; N] for primitive types T and sizes 1-32
24tempo_precompiles_macros::storable_arrays!();
25// nested arrays: [[T; M]; N] for small primitive types
26tempo_precompiles_macros::storable_nested_arrays!();
27
28/// Type-safe handler for accessing fixed-size arrays `[T; N]` in storage.
29///
30/// Unlike `VecHandler`, arrays have a fixed compile-time size and store elements
31/// directly at the base slot (not at `keccak256(base_slot)`).
32///
33/// # Element Access
34///
35/// Use `at(index)` to get a `Slot<T>` for individual element operations:
36/// - For packed elements (T::BYTES ≤ 16): returns a packed `Slot<T>` with byte offsets
37/// - For unpacked elements: returns a full `Slot<T>` for the element's dedicated slot
38/// - Returns `None` if index is out of bounds
39///
40/// # Example
41///
42/// ```ignore
43/// let handler = <[u8; 32] as StorableType>::handle(base_slot, LayoutCtx::FULL);
44///
45/// // Full array operations
46/// let array = handler.read()?;
47/// handler.write([1; 32])?;
48///
49/// // Individual element operations
50/// if let Some(slot) = handler.at(0) {
51///     let elem = slot.read()?;
52///     slot.write(42)?;
53/// }
54/// ```
55pub struct ArrayHandler<T, const N: usize>
56where
57    T: StorableType,
58{
59    base_slot: U256,
60    address: Address,
61    _phantom: PhantomData<T>,
62}
63
64impl<T, const N: usize> ArrayHandler<T, N>
65where
66    T: StorableType,
67{
68    /// Creates a new handler for the array at the given base slot and address.
69    #[inline]
70    pub fn new(base_slot: U256, address: Address) -> Self {
71        Self {
72            base_slot,
73            address,
74            _phantom: PhantomData,
75        }
76    }
77
78    /// Returns a `Slot` accessor for full-array operations.
79    #[inline]
80    fn as_slot(&self) -> Slot<[T; N]> {
81        Slot::new(self.base_slot, self.address)
82    }
83
84    /// Returns the base storage slot where this array's data is stored.
85    ///
86    /// Single-slot arrays pack all fields into this slot.
87    /// Multi-slot arrays use consecutive slots starting from this base.
88    #[inline]
89    pub fn base_slot(&self) -> ::alloy::primitives::U256 {
90        self.base_slot
91    }
92
93    /// Returns the array size (known at compile time).
94    #[inline]
95    pub const fn len(&self) -> usize {
96        N
97    }
98
99    /// Returns whether the array is empty (always false for N > 0).
100    #[inline]
101    pub const fn is_empty(&self) -> bool {
102        N == 0
103    }
104
105    /// Returns a `Slot<T>` accessor for the element at the given index.
106    ///
107    /// The returned `Slot` automatically handles packing based on `T::BYTES`.
108    ///
109    /// Returns `None` if the index is out of bounds (>= N).
110    #[inline]
111    pub fn at(&self, index: usize) -> Option<T::Handler>
112    where
113        T: StorableType,
114    {
115        if index >= N {
116            return None;
117        }
118
119        // Pack small elements into shared slots, use T::SLOTS for multi-slot types
120        let (base_slot, layout_ctx) = if T::BYTES <= 16 {
121            let location = packing::calc_element_loc(index, T::BYTES);
122            (
123                self.base_slot + U256::from(location.offset_slots),
124                LayoutCtx::packed(location.offset_bytes),
125            )
126        } else {
127            (
128                self.base_slot + U256::from(index * T::SLOTS),
129                LayoutCtx::FULL,
130            )
131        };
132
133        Some(T::handle(base_slot, layout_ctx, self.address))
134    }
135}
136
137impl<T, const N: usize> Handler<[T; N]> for ArrayHandler<T, N>
138where
139    T: StorableType,
140    [T; N]: Storable,
141{
142    /// Reads the entire array from storage.
143    #[inline]
144    fn read(&self) -> Result<[T; N]> {
145        self.as_slot().read()
146    }
147
148    /// Writes the entire array to storage.
149    #[inline]
150    fn write(&mut self, value: [T; N]) -> Result<()> {
151        self.as_slot().write(value)
152    }
153
154    /// Deletes the entire array from storage (clears all elements).
155    #[inline]
156    fn delete(&mut self) -> Result<()> {
157        self.as_slot().delete()
158    }
159
160    /// Reads the entire array from transient storage.
161    #[inline]
162    fn t_read(&self) -> Result<[T; N]> {
163        self.as_slot().t_read()
164    }
165
166    /// Writes the entire array to transient storage.
167    #[inline]
168    fn t_write(&mut self, value: [T; N]) -> Result<()> {
169        self.as_slot().t_write(value)
170    }
171
172    /// Deletes the entire array from transient storage (clears all elements).
173    #[inline]
174    fn t_delete(&mut self) -> Result<()> {
175        self.as_slot().t_delete()
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::{
183        storage::{Layout, LayoutCtx, PrecompileStorageProvider, StorageCtx},
184        test_util::setup_storage,
185    };
186    use proptest::prelude::*;
187
188    // Strategy for generating random U256 slot values that won't overflow
189    fn arb_safe_slot() -> impl Strategy<Value = U256> {
190        any::<[u64; 4]>().prop_map(|limbs| {
191            // Ensure we don't overflow by limiting to a reasonable range
192            U256::from_limbs(limbs) % (U256::MAX - U256::from(10000))
193        })
194    }
195
196    #[test]
197    fn test_array_u8_32_single_slot() {
198        let (mut storage, address) = setup_storage();
199        let base_slot = U256::ZERO;
200
201        // [u8; 32] should pack into exactly 1 slot
202        let data: [u8; 32] = [
203            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
204            25, 26, 27, 28, 29, 30, 31, 32,
205        ];
206
207        // Verify LAYOUT
208        assert_eq!(<[u8; 32] as StorableType>::LAYOUT, Layout::Slots(1));
209
210        // Store and load
211        StorageCtx::enter(&mut storage, || {
212            let mut slot = <[u8; 32]>::handle(base_slot, LayoutCtx::FULL, address);
213            slot.write(data).unwrap();
214            let loaded = slot.read().unwrap();
215            assert_eq!(loaded, data, "[u8; 32] roundtrip failed");
216
217            // Verify delete
218            slot.delete().unwrap();
219        });
220        let slot_value = storage.sload(address, base_slot).unwrap();
221        assert_eq!(slot_value, U256::ZERO, "Slot not cleared after delete");
222    }
223
224    #[test]
225    fn test_array_u64_5_multi_slot() {
226        let (mut storage, address) = setup_storage();
227        let base_slot = U256::from(100);
228
229        // [u64; 5] should require 2 slots (5 * 8 = 40 bytes > 32)
230        let data: [u64; 5] = [1, 2, 3, 4, 5];
231
232        // Verify slot count
233        assert_eq!(<[u64; 5] as StorableType>::LAYOUT, Layout::Slots(2));
234
235        // Store and load
236        StorageCtx::enter(&mut storage, || {
237            let mut slot = <[u64; 5]>::handle(base_slot, LayoutCtx::FULL, address);
238            slot.write(data).unwrap();
239            let loaded = slot.read().unwrap();
240            assert_eq!(loaded, data, "[u64; 5] roundtrip failed");
241        });
242
243        // Verify both slots are used
244        let slot0 = storage.sload(address, base_slot).unwrap();
245        let slot1 = storage.sload(address, base_slot + U256::ONE).unwrap();
246        assert_ne!(slot0, U256::ZERO, "Slot 0 should be non-zero");
247        assert_ne!(slot1, U256::ZERO, "Slot 1 should be non-zero");
248
249        // Verify delete clears both slots
250        StorageCtx::enter(&mut storage, || {
251            let mut slot = <[u64; 5]>::handle(base_slot, LayoutCtx::FULL, address);
252            slot.delete().unwrap();
253        });
254        let slot0_after = storage.sload(address, base_slot).unwrap();
255        let slot1_after = storage.sload(address, base_slot + U256::ONE).unwrap();
256        assert_eq!(slot0_after, U256::ZERO, "Slot 0 not cleared");
257        assert_eq!(slot1_after, U256::ZERO, "Slot 1 not cleared");
258    }
259
260    #[test]
261    fn test_array_u16_packing() {
262        let (mut storage, address) = setup_storage();
263        let base_slot = U256::from(200);
264
265        // [u16; 16] should pack into exactly 1 slot (16 * 2 = 32 bytes)
266        let data: [u16; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
267
268        // Verify slot count
269        assert_eq!(<[u16; 16] as StorableType>::LAYOUT, Layout::Slots(1));
270
271        // Store and load
272        StorageCtx::enter(&mut storage, || {
273            let mut slot = <[u16; 16]>::handle(base_slot, LayoutCtx::FULL, address);
274            slot.write(data).unwrap();
275            let loaded = slot.read().unwrap();
276            assert_eq!(loaded, data, "[u16; 16] roundtrip failed");
277        });
278    }
279
280    #[test]
281    fn test_array_u256_no_packing() {
282        let (mut storage, address) = setup_storage();
283        let base_slot = U256::from(300);
284
285        // [U256; 3] should use 3 slots (no packing for 32-byte types)
286        let data: [U256; 3] = [U256::from(12345), U256::from(67890), U256::from(111111)];
287
288        // Verify slot count
289        assert_eq!(<[U256; 3] as StorableType>::LAYOUT, Layout::Slots(3));
290
291        // Store and load
292        StorageCtx::enter(&mut storage, || {
293            let mut slot = <[U256; 3]>::handle(base_slot, LayoutCtx::FULL, address);
294            slot.write(data).unwrap();
295            let loaded = slot.read().unwrap();
296            assert_eq!(loaded, data, "[U256; 3] roundtrip failed");
297        });
298
299        // Verify each element is in its own slot
300        for (i, expected_value) in data.iter().enumerate() {
301            let slot_value = storage.sload(address, base_slot + U256::from(i)).unwrap();
302            assert_eq!(slot_value, *expected_value, "Slot {i} mismatch");
303        }
304    }
305
306    #[test]
307    fn test_array_address_no_packing() {
308        let (mut storage, address) = setup_storage();
309        let base_slot = U256::from(400);
310
311        // [Address; 3] should use 3 slots (20 bytes doesn't divide 32 evenly)
312        let data: [Address; 3] = [
313            Address::repeat_byte(0x11),
314            Address::repeat_byte(0x22),
315            Address::repeat_byte(0x33),
316        ];
317
318        // Verify slot count
319        assert_eq!(<[Address; 3] as StorableType>::LAYOUT, Layout::Slots(3));
320
321        // Store and load
322        StorageCtx::enter(&mut storage, || {
323            let mut slot = <[Address; 3]>::handle(base_slot, LayoutCtx::FULL, address);
324            slot.write(data).unwrap();
325            let loaded = slot.read().unwrap();
326            assert_eq!(loaded, data, "[Address; 3] roundtrip failed");
327        });
328    }
329
330    #[test]
331    fn test_array_empty_single_element() {
332        let (mut storage, address) = setup_storage();
333        let base_slot = U256::from(500);
334
335        // [u8; 1] should use 1 slot
336        let data: [u8; 1] = [42];
337
338        // Verify slot count
339        assert_eq!(<[u8; 1] as StorableType>::LAYOUT, Layout::Slots(1));
340
341        // Store and load
342        StorageCtx::enter(&mut storage, || {
343            let mut slot = <[u8; 1]>::handle(base_slot, LayoutCtx::FULL, address);
344            slot.write(data).unwrap();
345            let loaded = slot.read().unwrap();
346            assert_eq!(loaded, data, "[u8; 1] roundtrip failed");
347        });
348    }
349
350    #[test]
351    fn test_nested_array_u8_4x8() {
352        let (mut storage, address) = setup_storage();
353        let base_slot = U256::from(600);
354
355        // [[u8; 4]; 8] uses 8 slots (one per inner array)
356        // Each inner [u8; 4] gets a full 32-byte slot, even though it only uses 4 bytes
357        // This follows EVM's rule: nested arrays don't pack tightly across boundaries
358        let data: [[u8; 4]; 8] = [
359            [1, 2, 3, 4],
360            [5, 6, 7, 8],
361            [9, 10, 11, 12],
362            [13, 14, 15, 16],
363            [17, 18, 19, 20],
364            [21, 22, 23, 24],
365            [25, 26, 27, 28],
366            [29, 30, 31, 32],
367        ];
368
369        // Verify LAYOUT: 8 slots (one per inner array)
370        assert_eq!(<[[u8; 4]; 8] as StorableType>::LAYOUT, Layout::Slots(8));
371
372        // Store and load
373        StorageCtx::enter(&mut storage, || {
374            let mut slot = <[[u8; 4]; 8]>::handle(base_slot, LayoutCtx::FULL, address);
375            slot.write(data).unwrap();
376            let loaded = slot.read().unwrap();
377            assert_eq!(loaded, data, "[[u8; 4]; 8] roundtrip failed");
378
379            // Verify delete clears all 8 slots
380            slot.delete().unwrap();
381        });
382        for i in 0..8 {
383            let slot_value = storage.sload(address, base_slot + U256::from(i)).unwrap();
384            assert_eq!(slot_value, U256::ZERO, "Slot {i} not cleared after delete");
385        }
386    }
387
388    #[test]
389    fn test_nested_array_u16_2x8() {
390        let (mut storage, address) = setup_storage();
391        let base_slot = U256::from(700);
392
393        // [[u16; 2]; 8] uses 8 slots (one per inner array)
394        // Each inner [u16; 2] gets a full 32-byte slot, even though it only uses 4 bytes
395        // Compare: flat [u16; 16] would pack into 1 slot (16 × 2 = 32 bytes)
396        // But nested arrays don't pack across boundaries in EVM
397        let data: [[u16; 2]; 8] = [
398            [100, 101],
399            [200, 201],
400            [300, 301],
401            [400, 401],
402            [500, 501],
403            [600, 601],
404            [700, 701],
405            [800, 801],
406        ];
407
408        // Verify LAYOUT: 8 slots (one per inner array)
409        assert_eq!(<[[u16; 2]; 8] as StorableType>::LAYOUT, Layout::Slots(8));
410
411        // Store and load
412        StorageCtx::enter(&mut storage, || {
413            let mut slot = <[[u16; 2]; 8]>::handle(base_slot, LayoutCtx::FULL, address);
414            slot.write(data).unwrap();
415            let loaded = slot.read().unwrap();
416            assert_eq!(loaded, data, "[[u16; 2]; 8] roundtrip failed");
417
418            // Verify delete clears all 8 slots
419            slot.delete().unwrap();
420        });
421        for i in 0..8 {
422            let slot_value = storage.sload(address, base_slot + U256::from(i)).unwrap();
423            assert_eq!(slot_value, U256::ZERO, "Slot {i} not cleared after delete");
424        }
425    }
426
427    proptest! {
428        #![proptest_config(ProptestConfig::with_cases(500))]
429
430        #[test]
431        fn test_array_u8_32(
432            data in prop::array::uniform32(any::<u8>()),
433            base_slot in arb_safe_slot()
434        ) {
435            let (mut storage, address) = setup_storage();
436
437            // Store and load
438            StorageCtx::enter(&mut storage, || {
439                let mut slot = <[u8; 32]>::handle(base_slot, LayoutCtx::FULL, address);
440                slot.write(data).unwrap();
441                let loaded = slot.read().unwrap();
442                prop_assert_eq!(&loaded, &data, "[u8; 32] roundtrip failed");
443
444                // Delete
445                slot.delete().unwrap();
446                Ok(())
447            })?;
448            let slot_value = storage.sload(address, base_slot).unwrap();
449            prop_assert_eq!(slot_value, U256::ZERO, "Slot not cleared after delete");
450        }
451
452        #[test]
453        fn test_array_u16_16(
454            data in prop::array::uniform16(any::<u16>()),
455            base_slot in arb_safe_slot()
456        ) {
457            let (mut storage, address) = setup_storage();
458
459            // Store and load
460            StorageCtx::enter(&mut storage, || {
461                let mut slot = <[u16; 16]>::handle(base_slot, LayoutCtx::FULL, address);
462                slot.write(data).unwrap();
463                let loaded = slot.read().unwrap();
464                prop_assert_eq!(&loaded, &data, "[u16; 16] roundtrip failed");
465                Ok(())
466            })?;
467        }
468
469        #[test]
470        fn test_array_u256_5(
471            data in prop::array::uniform5(any::<u64>()).prop_map(|arr| arr.map(U256::from)),
472            base_slot in arb_safe_slot()
473        ) {
474            let (mut storage, address) = setup_storage();
475
476            // Store and load
477            StorageCtx::enter(&mut storage, || {
478                let mut slot = <[U256; 5]>::handle(base_slot, LayoutCtx::FULL, address);
479                slot.write(data).unwrap();
480                let loaded = slot.read().unwrap();
481                prop_assert_eq!(&loaded, &data, "[U256; 5] roundtrip failed");
482                Ok(())
483            })?;
484
485            // Verify each element is in its own slot
486            for (i, expected_value) in data.iter().enumerate() {
487                let slot_value = storage.sload(address, base_slot + U256::from(i)).unwrap();
488                prop_assert_eq!(slot_value, *expected_value, "Slot {} mismatch", i);
489            }
490
491            // Delete
492            StorageCtx::enter(&mut storage, || {
493                let mut slot = <[U256; 5]>::handle(base_slot, LayoutCtx::FULL, address);
494                slot.delete().unwrap();
495                Ok::<(), proptest::test_runner::TestCaseError>(())
496            })?;
497            for i in 0..5 {
498                let slot_value = storage.sload(address, base_slot + U256::from(i)).unwrap();
499                prop_assert_eq!(slot_value, U256::ZERO, "Slot {} not cleared", i);
500            }
501        }
502    }
503}