tempo_precompiles/storage/types/
bytes_like.rs

1//! Bytes-like (`Bytes`, `String`) implementation for the `Storable` trait.
2//!
3//! # Storage Layout
4//!
5//! Bytes-like types use Solidity-compatible:
6//! **Short strings (≤31 bytes)** are stored inline in a single slot:
7//! - Bytes 0..len: UTF-8 string data (left-aligned)
8//! - Byte 31 (LSB): length * 2 (bit 0 = 0 indicates short string)
9//!
10//! **Long strings (≥32 bytes)** use keccak256-based storage:
11//! - Base slot: stores `length * 2 + 1` (bit 0 = 1 indicates long string)
12//! - Data slots: stored at `keccak256(main_slot) + i` for each 32-byte chunk
13
14use alloy::primitives::{Bytes, U256, keccak256};
15
16use crate::{
17    error::{Result, TempoPrecompileError},
18    storage::{StorageOps, types::*},
19};
20
21impl StorableType for Bytes {
22    const LAYOUT: Layout = Layout::Slots(1);
23}
24
25impl Storable<1> for Bytes {
26    #[inline]
27    fn load<S: StorageOps>(storage: &mut S, base_slot: U256, ctx: LayoutCtx) -> Result<Self> {
28        debug_assert_eq!(ctx, LayoutCtx::FULL, "Bytes cannot be packed");
29        load_bytes_like(storage, base_slot, |data| Ok(Self::from(data)))
30    }
31
32    #[inline]
33    fn store<S: StorageOps>(&self, storage: &mut S, base_slot: U256, ctx: LayoutCtx) -> Result<()> {
34        debug_assert_eq!(ctx, LayoutCtx::FULL, "Bytes cannot be packed");
35        store_bytes_like(self.as_ref(), storage, base_slot)
36    }
37
38    #[inline]
39    fn delete<S: StorageOps>(storage: &mut S, base_slot: U256, ctx: LayoutCtx) -> Result<()> {
40        debug_assert_eq!(ctx, LayoutCtx::FULL, "Bytes cannot be packed");
41        delete_bytes_like(storage, base_slot)
42    }
43
44    #[inline]
45    fn to_evm_words(&self) -> Result<[U256; 1]> {
46        to_evm_words_bytes_like(self.as_ref())
47    }
48
49    #[inline]
50    fn from_evm_words(words: [U256; 1]) -> Result<Self> {
51        from_evm_words_bytes_like(words, |data| Ok(Self::from(data)))
52    }
53}
54
55impl StorableType for String {
56    const LAYOUT: Layout = Layout::Slots(1);
57}
58
59impl Storable<1> for String {
60    #[inline]
61    fn load<S: StorageOps>(storage: &mut S, base_slot: U256, ctx: LayoutCtx) -> Result<Self> {
62        debug_assert_eq!(ctx, LayoutCtx::FULL, "String cannot be packed");
63        load_bytes_like(storage, base_slot, |data| {
64            Self::from_utf8(data).map_err(|e| {
65                TempoPrecompileError::Fatal(format!("Invalid UTF-8 in stored string: {e}"))
66            })
67        })
68    }
69
70    #[inline]
71    fn store<S: StorageOps>(&self, storage: &mut S, base_slot: U256, ctx: LayoutCtx) -> Result<()> {
72        debug_assert_eq!(ctx, LayoutCtx::FULL, "String cannot be packed");
73        store_bytes_like(self.as_bytes(), storage, base_slot)
74    }
75
76    #[inline]
77    fn delete<S: StorageOps>(storage: &mut S, base_slot: U256, ctx: LayoutCtx) -> Result<()> {
78        debug_assert_eq!(ctx, LayoutCtx::FULL, "String cannot be packed");
79        delete_bytes_like(storage, base_slot)
80    }
81
82    #[inline]
83    fn to_evm_words(&self) -> Result<[U256; 1]> {
84        to_evm_words_bytes_like(self.as_bytes())
85    }
86
87    #[inline]
88    fn from_evm_words(words: [U256; 1]) -> Result<Self> {
89        from_evm_words_bytes_like(words, |data| {
90            Self::from_utf8(data).map_err(|e| {
91                TempoPrecompileError::Fatal(format!("Invalid UTF-8 in stored string: {e}"))
92            })
93        })
94    }
95}
96
97// -- HELPER FUNCTIONS ---------------------------------------------------------
98
99/// Generic load implementation for string-like types (String, Bytes) using Solidity's encoding.
100#[inline]
101fn load_bytes_like<T, S, F>(storage: &mut S, base_slot: U256, into: F) -> Result<T>
102where
103    S: StorageOps,
104    F: FnOnce(Vec<u8>) -> Result<T>,
105{
106    let base_value = storage.sload(base_slot)?;
107    let is_long = is_long_string(base_value);
108    let length = calc_string_length(base_value, is_long);
109
110    if is_long {
111        // Long string: read data from keccak256(base_slot) + i
112        let slot_start = calc_data_slot(base_slot);
113        let chunks = calc_chunks(length);
114        let mut data = Vec::with_capacity(length);
115
116        for i in 0..chunks {
117            let slot = slot_start + U256::from(i);
118            let chunk_value = storage.sload(slot)?;
119            let chunk_bytes = chunk_value.to_be_bytes::<32>();
120
121            // For the last chunk, only take the remaining bytes
122            let bytes_to_take = if i == chunks - 1 {
123                length - (i * 32)
124            } else {
125                32
126            };
127            data.extend_from_slice(&chunk_bytes[..bytes_to_take]);
128        }
129
130        into(data)
131    } else {
132        // Short string: data is inline in the main slot
133        let bytes = base_value.to_be_bytes::<32>();
134        into(bytes[..length].to_vec())
135    }
136}
137
138/// Generic store implementation for byte-like types (String, Bytes) using Solidity's encoding.
139#[inline]
140fn store_bytes_like<S: StorageOps>(bytes: &[u8], storage: &mut S, base_slot: U256) -> Result<()> {
141    let length = bytes.len();
142
143    if length <= 31 {
144        storage.sstore(base_slot, encode_short_string(bytes))
145    } else {
146        storage.sstore(base_slot, encode_long_string_length(length))?;
147
148        // Store data in chunks at keccak256(base_slot) + i
149        let slot_start = calc_data_slot(base_slot);
150        let chunks = calc_chunks(length);
151
152        for i in 0..chunks {
153            let slot = slot_start + U256::from(i);
154            let chunk_start = i * 32;
155            let chunk_end = (chunk_start + 32).min(length);
156            let chunk = &bytes[chunk_start..chunk_end];
157
158            // Pad chunk to 32 bytes if it's the last chunk
159            let mut chunk_bytes = [0u8; 32];
160            chunk_bytes[..chunk.len()].copy_from_slice(chunk);
161
162            storage.sstore(slot, U256::from_be_bytes(chunk_bytes))?;
163        }
164
165        Ok(())
166    }
167}
168
169/// Generic delete implementation for byte-like types (String, Bytes) using Solidity's encoding.
170///
171/// Clears both the main slot and any keccak256-addressed data slots for long strings.
172#[inline]
173fn delete_bytes_like<S: StorageOps>(storage: &mut S, base_slot: U256) -> Result<()> {
174    let base_value = storage.sload(base_slot)?;
175    let is_long = is_long_string(base_value);
176
177    if is_long {
178        // Long string: need to clear data slots as well
179        let length = calc_string_length(base_value, true);
180        let slot_start = calc_data_slot(base_slot);
181        let chunks = calc_chunks(length);
182
183        // Clear all data slots
184        for i in 0..chunks {
185            let slot = slot_start + U256::from(i);
186            storage.sstore(slot, U256::ZERO)?;
187        }
188    }
189
190    // Clear the main slot
191    storage.sstore(base_slot, U256::ZERO)
192}
193
194/// Returns the encoded length for long strings or the inline data for short strings.
195#[inline]
196fn to_evm_words_bytes_like(bytes: &[u8]) -> Result<[U256; 1]> {
197    let length = bytes.len();
198
199    if length <= 31 {
200        Ok([encode_short_string(bytes)])
201    } else {
202        // Note: actual string data is in keccak256-addressed slots (not included here)
203        Ok([encode_long_string_length(length)])
204    }
205}
206
207/// The converter function transforms raw bytes into the target type.
208/// Returns an error for long strings, which require storage access to reconstruct.
209#[inline]
210fn from_evm_words_bytes_like<T, F>(words: [U256; 1], into: F) -> Result<T>
211where
212    F: FnOnce(Vec<u8>) -> Result<T>,
213{
214    let slot_value = words[0];
215    let is_long = is_long_string(slot_value);
216
217    if is_long {
218        // Long string: cannot reconstruct without storage access to keccak256-addressed data
219        Err(TempoPrecompileError::Fatal(
220            "Cannot reconstruct long string from single word. Use load() instead.".into(),
221        ))
222    } else {
223        // Short string: data is inline in the word
224        let length = calc_string_length(slot_value, false);
225        let bytes = slot_value.to_be_bytes::<32>();
226        into(bytes[..length].to_vec())
227    }
228}
229
230/// Compute the storage slot where long string data begins.
231///
232/// For long strings (≥32 bytes), data is stored starting at `keccak256(base_slot)`.
233#[inline]
234fn calc_data_slot(base_slot: U256) -> U256 {
235    U256::from_be_bytes(keccak256(base_slot.to_be_bytes::<32>()).0)
236}
237
238/// Check if a storage slot value represents a long string.
239///
240/// Solidity string encoding uses bit 0 of the LSB to distinguish:
241/// - Bit 0 = 0: Short string (≤31 bytes)
242/// - Bit 0 = 1: Long string (≥32 bytes)
243#[inline]
244fn is_long_string(slot_value: U256) -> bool {
245    (slot_value.byte(0) & 1) != 0
246}
247
248/// Extract the string length from a storage slot value.
249#[inline]
250fn calc_string_length(slot_value: U256, is_long: bool) -> usize {
251    if is_long {
252        // Long string: slot stores (length * 2 + 1)
253        // Extract length: (value - 1) / 2
254        let length_times_two_plus_one: U256 = slot_value;
255        let length_times_two: U256 = length_times_two_plus_one - U256::ONE;
256        let length_u256: U256 = length_times_two >> 1;
257        length_u256.to::<usize>()
258    } else {
259        // Short string: LSB stores (length * 2)
260        // Extract length: LSB / 2
261        let bytes = slot_value.to_be_bytes::<32>();
262        (bytes[31] / 2) as usize
263    }
264}
265
266/// Compute the number of 32-byte chunks needed to store a byte string.
267#[inline]
268fn calc_chunks(byte_length: usize) -> usize {
269    byte_length.div_ceil(32)
270}
271
272/// Encode a short string (≤31 bytes) into a U256 for inline storage.
273///
274/// Format: bytes left-aligned, LSB contains (length * 2)
275#[inline]
276fn encode_short_string(bytes: &[u8]) -> U256 {
277    let mut storage_bytes = [0u8; 32];
278    storage_bytes[..bytes.len()].copy_from_slice(bytes);
279    storage_bytes[31] = (bytes.len() * 2) as u8;
280    U256::from_be_bytes(storage_bytes)
281}
282
283/// Encode the length metadata for a long string (≥32 bytes).
284///
285/// Returns `length * 2 + 1` where bit 0 = 1 indicates long string storage.
286#[inline]
287fn encode_long_string_length(byte_length: usize) -> U256 {
288    U256::from(byte_length * 2 + 1)
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use crate::storage::{PrecompileStorageProvider, StorageOps, hashmap::HashMapStorageProvider};
295    use alloy::primitives::Address;
296    use proptest::prelude::*;
297
298    // -- TEST HELPERS -------------------------------------------------------------
299
300    // Test helper that owns storage and implements StorageOps
301    struct TestContract {
302        address: Address,
303        storage: HashMapStorageProvider,
304    }
305
306    impl StorageOps for TestContract {
307        fn sstore(&mut self, slot: U256, value: U256) -> Result<()> {
308            self.storage.sstore(self.address, slot, value)
309        }
310
311        fn sload(&mut self, slot: U256) -> Result<U256> {
312            self.storage.sload(self.address, slot)
313        }
314    }
315
316    /// Helper to create a test contract with fresh storage.
317    fn setup_test_contract() -> TestContract {
318        TestContract {
319            address: Address::random(),
320            storage: HashMapStorageProvider::new(1),
321        }
322    }
323
324    // Strategy for generating random U256 slot values that won't overflow
325    fn arb_safe_slot() -> impl Strategy<Value = U256> {
326        any::<[u64; 4]>().prop_map(|limbs| {
327            // Ensure we don't overflow by limiting to a reasonable range
328            U256::from_limbs(limbs) % (U256::MAX - U256::from(10000))
329        })
330    }
331
332    // Strategy for short strings (0-31 bytes) - uses inline storage
333    fn arb_short_string() -> impl Strategy<Value = String> {
334        prop_oneof![
335            // Empty string
336            Just(String::new()),
337            // ASCII strings (1-31 bytes)
338            "[a-zA-Z0-9]{1,31}",
339            // Unicode strings (up to 31 bytes)
340            "[\u{0041}-\u{005A}\u{4E00}-\u{4E19}]{1,10}",
341        ]
342    }
343
344    // Strategy for exactly 32-byte strings - boundary between inline and heap storage
345    fn arb_32byte_string() -> impl Strategy<Value = String> {
346        "[a-zA-Z0-9]{32}"
347    }
348
349    // Strategy for long strings (33-100 bytes) - uses heap storage
350    fn arb_long_string() -> impl Strategy<Value = String> {
351        prop_oneof![
352            // ASCII strings (33-100 bytes)
353            "[a-zA-Z0-9]{33,100}",
354            // Unicode strings (>32 bytes)
355            "[\u{0041}-\u{005A}\u{4E00}-\u{4E19}]{11,30}",
356        ]
357    }
358
359    // Strategy for short byte arrays (0-31 bytes) - uses inline storage
360    fn arb_short_bytes() -> impl Strategy<Value = Bytes> {
361        prop::collection::vec(any::<u8>(), 0..=31).prop_map(Bytes::from)
362    }
363
364    // Strategy for exactly 32-byte arrays - boundary between inline and heap storage
365    fn arb_32byte_bytes() -> impl Strategy<Value = Bytes> {
366        prop::collection::vec(any::<u8>(), 32..=32).prop_map(Bytes::from)
367    }
368
369    // Strategy for long byte arrays (33-100 bytes) - uses heap storage
370    fn arb_long_bytes() -> impl Strategy<Value = Bytes> {
371        prop::collection::vec(any::<u8>(), 33..=100).prop_map(Bytes::from)
372    }
373
374    // -- STORAGE TESTS --------------------------------------------------------
375
376    proptest! {
377        #![proptest_config(ProptestConfig::with_cases(500))]
378
379        #[test]
380        fn test_short_strings(s in arb_short_string(), base_slot in arb_safe_slot()) {
381            let mut contract = setup_test_contract();
382
383            // Verify store → load roundtrip
384            s.store(&mut contract, base_slot, LayoutCtx::FULL)?;
385            let loaded = String::load(&mut contract, base_slot, LayoutCtx::FULL)?;
386            assert_eq!(s, loaded, "Short string roundtrip failed for: {s:?}");
387
388            // Verify delete works
389            String::delete(&mut contract, base_slot, LayoutCtx::FULL)?;
390            let after_delete = String::load(&mut contract, base_slot, LayoutCtx::FULL)?;
391            assert_eq!(after_delete, String::new(), "Short string not empty after delete");
392
393            // EVM words roundtrip (only works for short strings ≤31 bytes)
394            let words = s.to_evm_words()?;
395            let recovered = String::from_evm_words(words)?;
396            assert_eq!(s, recovered, "Short string EVM words roundtrip failed");
397        }
398
399        #[test]
400        fn test_32byte_strings(s in arb_32byte_string(), base_slot in arb_safe_slot()) {
401            let mut contract = setup_test_contract();
402
403            // Verify 32-byte boundary string is stored correctly
404            assert_eq!(s.len(), 32, "Generated string should be exactly 32 bytes");
405
406            // Verify store → load roundtrip
407            s.store(&mut contract, base_slot, LayoutCtx::FULL)?;
408            let loaded = String::load(&mut contract, base_slot, LayoutCtx::FULL)?;
409            assert_eq!(s, loaded, "32-byte string roundtrip failed");
410
411            // Verify delete works
412            String::delete(&mut contract, base_slot, LayoutCtx::FULL)?;
413            let after_delete = String::load(&mut contract, base_slot, LayoutCtx::FULL)?;
414            assert_eq!(after_delete, String::new(), "32-byte string not empty after delete");
415
416            // Note: 32-byte strings use long storage format and cannot be
417            // reconstructed from a single word without storage access
418            let words = s.to_evm_words()?;
419            let result = String::from_evm_words(words);
420            assert!(result.is_err(), "32-byte string should not be reconstructable from single word");
421        }
422
423        #[test]
424        fn test_long_strings(s in arb_long_string(), base_slot in arb_safe_slot()) {
425            let mut contract = setup_test_contract();
426
427            // Verify store → load roundtrip
428            s.store(&mut contract, base_slot, LayoutCtx::FULL)?;
429            let loaded = String::load(&mut contract, base_slot, LayoutCtx::FULL)?;
430            assert_eq!(s, loaded, "Long string roundtrip failed for length: {}", s.len());
431
432            // Calculate how many data slots were used
433            let chunks = calc_chunks(s.len());
434
435            // Verify delete works (clears both main slot and keccak256-addressed data)
436            String::delete(&mut contract, base_slot, LayoutCtx::FULL)?;
437            let after_delete = String::load(&mut contract, base_slot, LayoutCtx::FULL)?;
438            assert_eq!(after_delete, String::new(), "Long string not empty after delete");
439
440            // Verify all keccak256-addressed data slots are actually zero
441            let data_slot_start = calc_data_slot(base_slot);
442            for i in 0..chunks {
443                let slot = data_slot_start + U256::from(i);
444                let value = contract.sload(slot)?;
445                assert_eq!(value, U256::ZERO, "Data slot {i} not cleared after delete");
446            }
447
448            // Verify that strings >= 32 bytes cannot be reconstructed from single word
449            // Note: arb_long_string() may occasionally generate strings < 32 bytes due to Unicode
450            if s.len() >= 32 {
451                let words = s.to_evm_words()?;
452                let result = String::from_evm_words(words);
453                assert!(result.is_err(), "Long string (>= 32 bytes) should not be reconstructable from single word");
454            } else {
455                // For strings < 32 bytes, verify roundtrip works
456                let words = s.to_evm_words()?;
457                let recovered = String::from_evm_words(words)?;
458                assert_eq!(s, recovered, "String < 32 bytes EVM words roundtrip failed");
459            }
460        }
461
462        #[test]
463        fn test_short_bytes(b in arb_short_bytes(), base_slot in arb_safe_slot()) {
464            let mut contract = setup_test_contract();
465
466            // Verify store → load roundtrip
467            b.store(&mut contract, base_slot, LayoutCtx::FULL)?;
468            let loaded = Bytes::load(&mut contract, base_slot, LayoutCtx::FULL)?;
469            assert_eq!(b, loaded, "Short bytes roundtrip failed for length: {}", b.len());
470
471            // Verify delete works
472            Bytes::delete(&mut contract, base_slot, LayoutCtx::FULL)?;
473            let after_delete = Bytes::load(&mut contract, base_slot, LayoutCtx::FULL)?;
474            assert_eq!(after_delete, Bytes::new(), "Short bytes not empty after delete");
475
476            // EVM words roundtrip (only works for short bytes ≤31 bytes)
477            let words = b.to_evm_words()?;
478            let recovered = Bytes::from_evm_words(words)?;
479            assert_eq!(b, recovered, "Short bytes EVM words roundtrip failed");
480        }
481
482        #[test]
483        fn test_32byte_bytes(b in arb_32byte_bytes(), base_slot in arb_safe_slot()) {
484            let mut contract = setup_test_contract();
485
486            // Verify 32-byte boundary bytes is stored correctly
487            assert_eq!(b.len(), 32, "Generated bytes should be exactly 32 bytes");
488
489            // Verify store → load roundtrip
490            b.store(&mut contract, base_slot, LayoutCtx::FULL)?;
491            let loaded = Bytes::load(&mut contract, base_slot, LayoutCtx::FULL)?;
492            assert_eq!(b, loaded, "32-byte bytes roundtrip failed");
493
494            // Verify delete works
495            Bytes::delete(&mut contract, base_slot, LayoutCtx::FULL)?;
496            let after_delete = Bytes::load(&mut contract, base_slot, LayoutCtx::FULL)?;
497            assert_eq!(after_delete, Bytes::new(), "32-byte bytes not empty after delete");
498
499            // Note: 32-byte Bytes use long storage format and cannot be
500            // reconstructed from a single word without storage access
501            let words = b.to_evm_words()?;
502            let result = Bytes::from_evm_words(words);
503            assert!(result.is_err(), "32-byte Bytes should not be reconstructable from single word");
504        }
505
506        #[test]
507        fn test_long_bytes(b in arb_long_bytes(), base_slot in arb_safe_slot()) {
508            let mut contract = setup_test_contract();
509
510            // Verify store → load roundtrip
511            b.store(&mut contract, base_slot, LayoutCtx::FULL)?;
512            let loaded = Bytes::load(&mut contract, base_slot, LayoutCtx::FULL)?;
513            assert_eq!(b, loaded, "Long bytes roundtrip failed for length: {}", b.len());
514
515            // Calculate how many data slots were used
516            let chunks = calc_chunks(b.len());
517
518            // Verify delete works (clears both main slot and keccak256-addressed data)
519            Bytes::delete(&mut contract, base_slot, LayoutCtx::FULL)?;
520            let after_delete = Bytes::load(&mut contract, base_slot, LayoutCtx::FULL)?;
521            assert_eq!(after_delete, Bytes::new(), "Long bytes not empty after delete");
522
523            // Verify all keccak256-addressed data slots are actually zero
524            let data_slot_start = calc_data_slot(base_slot);
525            for i in 0..chunks {
526                let slot = data_slot_start + U256::from(i);
527                let value = contract.sload(slot)?;
528                assert_eq!(value, U256::ZERO, "Data slot {i} not cleared after delete");
529            }
530
531            // Verify that bytes >= 32 bytes cannot be reconstructed from single word
532            if b.len() >= 32 {
533                let words = b.to_evm_words()?;
534                let result = Bytes::from_evm_words(words);
535                assert!(result.is_err(), "Long bytes (>= 32 bytes) should not be reconstructable from single word");
536            } else {
537                // For bytes < 32 bytes, verify roundtrip works
538                let words = b.to_evm_words()?;
539                let recovered = Bytes::from_evm_words(words)?;
540                assert_eq!(b, recovered, "Bytes < 32 bytes EVM words roundtrip failed");
541            }
542        }
543    }
544}