Skip to main content

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