Skip to main content

tempo_precompiles/storage/types/
bytes_like.rs

1//! Bytes-like (`Bytes`, `String`) implementation for the storage traits.
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 crate::{
15    error::{Result, TempoPrecompileError},
16    storage::{StorageOps, types::*},
17};
18use alloy::primitives::{Address, Bytes, U256, keccak256};
19use std::marker::PhantomData;
20
21impl StorableType for Bytes {
22    const LAYOUT: Layout = Layout::Slots(1);
23    const IS_DYNAMIC: bool = true;
24    type Handler = BytesLikeHandler<Self>;
25
26    fn handle(slot: U256, _ctx: LayoutCtx, address: Address) -> Self::Handler {
27        BytesLikeHandler::new(slot, address)
28    }
29}
30
31impl StorableType for String {
32    const LAYOUT: Layout = Layout::Slots(1);
33    const IS_DYNAMIC: bool = true;
34    type Handler = BytesLikeHandler<Self>;
35
36    fn handle(slot: U256, _ctx: LayoutCtx, address: Address) -> Self::Handler {
37        BytesLikeHandler::new(slot, address)
38    }
39}
40
41// -- BYTES-LIKE HANDLER -------------------------------------------------------
42
43/// Handler for bytes-like types (`Bytes`, `String`) that provides efficient length queries.
44#[derive(Debug, Clone)]
45pub struct BytesLikeHandler<T> {
46    base_slot: U256,
47    address: Address,
48    _ty: PhantomData<T>,
49}
50
51impl<T: Storable> BytesLikeHandler<T> {
52    /// Creates a new handler for the bytes-like value at the given base slot.
53    #[inline]
54    pub fn new(base_slot: U256, address: Address) -> Self {
55        Self {
56            base_slot,
57            address,
58            _ty: PhantomData,
59        }
60    }
61
62    #[inline]
63    fn as_slot(&self) -> Slot<T> {
64        Slot::new(self.base_slot, self.address)
65    }
66
67    /// Returns the byte length without loading all data (only reads base slot).
68    #[inline]
69    pub fn len(&self) -> Result<usize> {
70        let base_value = Slot::<U256>::new(self.base_slot, self.address).read()?;
71        let is_long = is_long_string(base_value);
72        Ok(calc_string_length(base_value, is_long))
73    }
74
75    /// Returns whether the stored value is empty.
76    #[inline]
77    pub fn is_empty(&self) -> Result<bool> {
78        Ok(self.len()? == 0)
79    }
80}
81
82impl<T: Storable> Handler<T> for BytesLikeHandler<T> {
83    #[inline]
84    fn read(&self) -> Result<T> {
85        self.as_slot().read()
86    }
87
88    #[inline]
89    fn write(&mut self, value: T) -> Result<()> {
90        self.as_slot().write(value)
91    }
92
93    #[inline]
94    fn delete(&mut self) -> Result<()> {
95        self.as_slot().delete()
96    }
97
98    #[inline]
99    fn t_read(&self) -> Result<T> {
100        self.as_slot().t_read()
101    }
102
103    #[inline]
104    fn t_write(&mut self, value: T) -> Result<()> {
105        self.as_slot().t_write(value)
106    }
107
108    #[inline]
109    fn t_delete(&mut self) -> Result<()> {
110        self.as_slot().t_delete()
111    }
112}
113
114// -- STORABLE OPS IMPLEMENTATIONS ---------------------------------------------
115
116impl Storable for Bytes {
117    #[inline]
118    fn load<S: StorageOps>(storage: &S, slot: U256, ctx: LayoutCtx) -> Result<Self> {
119        debug_assert_eq!(ctx, LayoutCtx::FULL, "Bytes cannot be packed");
120        load_bytes_like(storage, slot, |data| Ok(Self::from(data)))
121    }
122
123    #[inline]
124    fn store<S: StorageOps>(&self, storage: &mut S, slot: U256, ctx: LayoutCtx) -> Result<()> {
125        debug_assert_eq!(ctx, LayoutCtx::FULL, "Bytes cannot be packed");
126        store_bytes_like(self.as_ref(), storage, slot)
127    }
128
129    /// Custom delete for bytes-like types: clears keccak256-addressed data slots for long values.
130    #[inline]
131    fn delete<S: StorageOps>(storage: &mut S, slot: U256, ctx: LayoutCtx) -> Result<()> {
132        debug_assert_eq!(ctx, LayoutCtx::FULL, "Bytes cannot be packed");
133        delete_bytes_like(storage, slot)
134    }
135}
136
137impl Storable for String {
138    #[inline]
139    fn load<S: StorageOps>(storage: &S, slot: U256, ctx: LayoutCtx) -> Result<Self> {
140        debug_assert_eq!(ctx, LayoutCtx::FULL, "String cannot be packed");
141        load_bytes_like(storage, slot, |data| {
142            Self::from_utf8(data).map_err(|e| {
143                TempoPrecompileError::Fatal(format!("Invalid UTF-8 in stored string: {e}"))
144            })
145        })
146    }
147
148    #[inline]
149    fn store<S: StorageOps>(&self, storage: &mut S, slot: U256, ctx: LayoutCtx) -> Result<()> {
150        debug_assert_eq!(ctx, LayoutCtx::FULL, "String cannot be packed");
151        store_bytes_like(self.as_bytes(), storage, slot)
152    }
153
154    /// Custom delete for bytes-like types: clears keccak256-addressed data slots for long values.
155    #[inline]
156    fn delete<S: StorageOps>(storage: &mut S, slot: U256, ctx: LayoutCtx) -> Result<()> {
157        debug_assert_eq!(ctx, LayoutCtx::FULL, "String cannot be packed");
158        delete_bytes_like(storage, slot)
159    }
160}
161
162// -- HELPER FUNCTIONS ---------------------------------------------------------
163
164/// Generic load implementation for string-like types (String, Bytes) using Solidity's encoding.
165#[inline]
166fn load_bytes_like<T, S, F>(storage: &S, base_slot: U256, into: F) -> Result<T>
167where
168    S: StorageOps,
169    F: FnOnce(Vec<u8>) -> Result<T>,
170{
171    let base_value = storage.load(base_slot)?;
172    let is_long = is_long_string(base_value);
173    let length = calc_string_length(base_value, is_long);
174
175    if is_long {
176        // Long string: read data from keccak256(base_slot) + i
177        let slot_start = calc_data_slot(base_slot);
178        let chunks = calc_chunks(length);
179        let mut data = Vec::with_capacity(length);
180
181        for i in 0..chunks {
182            let slot = slot_start + U256::from(i);
183            let chunk_value = storage.load(slot)?;
184            let chunk_bytes = chunk_value.to_be_bytes::<32>();
185
186            // For the last chunk, only take the remaining bytes
187            let bytes_to_take = if i == chunks - 1 {
188                length - (i * 32)
189            } else {
190                32
191            };
192            data.extend_from_slice(&chunk_bytes[..bytes_to_take]);
193        }
194
195        into(data)
196    } else {
197        // Short string: data is inline in the main slot
198        let bytes = base_value.to_be_bytes::<32>();
199        into(bytes[..length].to_vec())
200    }
201}
202
203/// Generic store implementation for byte-like types (String, Bytes) using Solidity's encoding.
204#[inline]
205fn store_bytes_like<S: StorageOps>(bytes: &[u8], storage: &mut S, base_slot: U256) -> Result<()> {
206    let length = bytes.len();
207
208    if length <= 31 {
209        storage.store(base_slot, encode_short_string(bytes))
210    } else {
211        storage.store(base_slot, encode_long_string_length(length))?;
212
213        // Store data in chunks at keccak256(base_slot) + i
214        let slot_start = calc_data_slot(base_slot);
215        let chunks = calc_chunks(length);
216
217        for i in 0..chunks {
218            let slot = slot_start + U256::from(i);
219            let chunk_start = i * 32;
220            let chunk_end = (chunk_start + 32).min(length);
221            let chunk = &bytes[chunk_start..chunk_end];
222
223            // Pad chunk to 32 bytes if it's the last chunk
224            let mut chunk_bytes = [0u8; 32];
225            chunk_bytes[..chunk.len()].copy_from_slice(chunk);
226
227            storage.store(slot, U256::from_be_bytes(chunk_bytes))?;
228        }
229
230        Ok(())
231    }
232}
233
234/// Generic delete implementation for byte-like types (String, Bytes) using Solidity's encoding.
235///
236/// Clears both the main slot and any keccak256-addressed data slots for long strings.
237#[inline]
238fn delete_bytes_like<S: StorageOps>(storage: &mut S, base_slot: U256) -> Result<()> {
239    let base_value = storage.load(base_slot)?;
240    let is_long = is_long_string(base_value);
241
242    if is_long {
243        // Long string: need to clear data slots as well
244        let length = calc_string_length(base_value, true);
245        let slot_start = calc_data_slot(base_slot);
246        let chunks = calc_chunks(length);
247
248        // Clear all data slots
249        for i in 0..chunks {
250            let slot = slot_start + U256::from(i);
251            storage.store(slot, U256::ZERO)?;
252        }
253    }
254
255    // Clear the main slot
256    storage.store(base_slot, U256::ZERO)
257}
258
259/// Compute the storage slot where long string data begins.
260///
261/// For long strings (≥32 bytes), data is stored starting at `keccak256(base_slot)`.
262#[inline]
263fn calc_data_slot(base_slot: U256) -> U256 {
264    U256::from_be_bytes(keccak256(base_slot.to_be_bytes::<32>()).0)
265}
266
267/// Check if a storage slot value represents a long string.
268///
269/// Solidity string encoding uses bit 0 of the LSB to distinguish:
270/// - Bit 0 = 0: Short string (≤31 bytes)
271/// - Bit 0 = 1: Long string (≥32 bytes)
272#[inline]
273fn is_long_string(slot_value: U256) -> bool {
274    (slot_value.byte(0) & 1) != 0
275}
276
277/// Extract the string length from a storage slot value.
278#[inline]
279fn calc_string_length(slot_value: U256, is_long: bool) -> usize {
280    if is_long {
281        // Long string: slot stores (length * 2 + 1)
282        // Extract length: (value - 1) / 2
283        let length_times_two_plus_one: U256 = slot_value;
284        let length_times_two: U256 = length_times_two_plus_one - U256::ONE;
285        let length_u256: U256 = length_times_two >> 1;
286        length_u256.to::<usize>()
287    } else {
288        // Short string: LSB stores (length * 2)
289        // Extract length: LSB / 2
290        let bytes = slot_value.to_be_bytes::<32>();
291        (bytes[31] / 2) as usize
292    }
293}
294
295/// Compute the number of 32-byte chunks needed to store a byte string.
296#[inline]
297fn calc_chunks(byte_length: usize) -> usize {
298    byte_length.div_ceil(32)
299}
300
301/// Encode a short string (≤31 bytes) into a U256 for inline storage.
302///
303/// Format: bytes left-aligned, LSB contains (length * 2)
304#[inline]
305fn encode_short_string(bytes: &[u8]) -> U256 {
306    let mut storage_bytes = [0u8; 32];
307    storage_bytes[..bytes.len()].copy_from_slice(bytes);
308    storage_bytes[31] = (bytes.len() * 2) as u8;
309    U256::from_be_bytes(storage_bytes)
310}
311
312/// Encode the length metadata for a long string (≥32 bytes).
313///
314/// Returns `length * 2 + 1` where bit 0 = 1 indicates long string storage.
315#[inline]
316fn encode_long_string_length(byte_length: usize) -> U256 {
317    U256::from(byte_length * 2 + 1)
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use crate::{
324        storage::{Handler, StorageCtx},
325        test_util::setup_storage,
326    };
327    use proptest::prelude::*;
328
329    // Strategy for generating random U256 slot values that won't overflow
330    fn arb_safe_slot() -> impl Strategy<Value = U256> {
331        any::<[u64; 4]>().prop_map(|limbs| {
332            // Ensure we don't overflow by limiting to a reasonable range
333            U256::from_limbs(limbs) % (U256::MAX - U256::from(10000))
334        })
335    }
336
337    // Strategy for short strings (0-31 bytes) - uses inline storage
338    fn arb_short_string() -> impl Strategy<Value = String> {
339        prop_oneof![
340            // Empty string
341            Just(String::new()),
342            // ASCII strings (1-31 bytes)
343            "[a-zA-Z0-9]{1,31}",
344            // Unicode strings (up to 31 bytes)
345            "[\u{0041}-\u{005A}\u{4E00}-\u{4E19}]{1,10}",
346        ]
347    }
348
349    // Strategy for exactly 32-byte strings - boundary between inline and heap storage
350    fn arb_32byte_string() -> impl Strategy<Value = String> {
351        "[a-zA-Z0-9]{32}"
352    }
353
354    // Strategy for long strings (33-100 bytes) - uses heap storage
355    fn arb_long_string() -> impl Strategy<Value = String> {
356        prop_oneof![
357            // ASCII strings (33-100 bytes)
358            "[a-zA-Z0-9]{33,100}",
359            // Unicode strings (>32 bytes)
360            "[\u{0041}-\u{005A}\u{4E00}-\u{4E19}]{11,30}",
361        ]
362    }
363
364    // Strategy for short byte arrays (0-31 bytes) - uses inline storage
365    fn arb_short_bytes() -> impl Strategy<Value = Bytes> {
366        prop::collection::vec(any::<u8>(), 0..=31).prop_map(Bytes::from)
367    }
368
369    // Strategy for exactly 32-byte arrays - boundary between inline and heap storage
370    fn arb_32byte_bytes() -> impl Strategy<Value = Bytes> {
371        prop::collection::vec(any::<u8>(), 32..=32).prop_map(Bytes::from)
372    }
373
374    // Strategy for long byte arrays (33-100 bytes) - uses heap storage
375    fn arb_long_bytes() -> impl Strategy<Value = Bytes> {
376        prop::collection::vec(any::<u8>(), 33..=100).prop_map(Bytes::from)
377    }
378
379    // -- UNIT TESTS FOR HELPER FUNCTIONS (NO STORAGE) ------------------------
380
381    #[test]
382    fn test_calc_data_slot_matches_manual_keccak() {
383        let base_slot = U256::random();
384        let data_slot = calc_data_slot(base_slot);
385
386        // Manual computation
387        let expected = U256::from_be_bytes(keccak256(base_slot.to_be_bytes::<32>()).0);
388
389        assert_eq!(
390            data_slot, expected,
391            "calc_data_slot should match manual keccak256 computation"
392        );
393    }
394
395    #[test]
396    fn test_is_long_string_boundaries() {
397        // Short string (31 bytes): length * 2 = 62 (0x3E), bit 0 = 0
398        let short_31_bytes = encode_short_string(&[b'a'; 31]);
399        assert!(
400            !is_long_string(short_31_bytes),
401            "31-byte string should be short"
402        );
403
404        // Long string (32 bytes): length * 2 + 1 = 65 (0x41), bit 0 = 1
405        let long_32_bytes = encode_long_string_length(32);
406        assert!(
407            is_long_string(long_32_bytes),
408            "32-byte string should be long"
409        );
410
411        // Edge case: empty string
412        let empty = encode_short_string(&[]);
413        assert!(!is_long_string(empty), "Empty string should be short");
414
415        // Edge case: 1-byte string
416        let one_byte = encode_short_string(b"x");
417        assert!(!is_long_string(one_byte), "1-byte string should be short");
418    }
419
420    #[test]
421    fn test_calc_string_length_short() {
422        // Test short strings with various lengths
423        for len in 0..=31 {
424            let bytes = vec![b'a'; len];
425            let encoded = encode_short_string(&bytes);
426            let decoded_len = calc_string_length(encoded, false);
427            assert_eq!(
428                decoded_len, len,
429                "Short string length mismatch for {len} bytes"
430            );
431        }
432    }
433
434    #[test]
435    fn test_calc_string_length_long() {
436        // Test long strings with various lengths
437        for len in [32, 33, 63, 64, 65, 100, 1000, 10000] {
438            let encoded = encode_long_string_length(len);
439            let decoded_len = calc_string_length(encoded, true);
440            assert_eq!(
441                decoded_len, len,
442                "Long string length mismatch for {len} bytes"
443            );
444        }
445    }
446
447    #[test]
448    fn test_calc_chunks_boundaries() {
449        assert_eq!(calc_chunks(0), 0, "0 bytes should require 0 chunks");
450        assert_eq!(calc_chunks(1), 1, "1 byte should require 1 chunk");
451        assert_eq!(calc_chunks(31), 1, "31 bytes should require 1 chunk");
452        assert_eq!(calc_chunks(32), 1, "32 bytes should require 1 chunk");
453        assert_eq!(calc_chunks(33), 2, "33 bytes should require 2 chunks");
454        assert_eq!(calc_chunks(64), 2, "64 bytes should require 2 chunks");
455        assert_eq!(calc_chunks(65), 3, "65 bytes should require 3 chunks");
456        assert_eq!(calc_chunks(100), 4, "100 bytes should require 4 chunks");
457    }
458
459    #[test]
460    fn test_encode_short_string_format() {
461        let test_str = b"Hello";
462        let encoded = encode_short_string(test_str);
463        let bytes = encoded.to_be_bytes::<32>();
464
465        // Verify data is left-aligned
466        assert_eq!(&bytes[..5], test_str, "Data should be left-aligned");
467
468        // Verify padding is zero
469        assert_eq!(&bytes[5..31], &[0u8; 26], "Padding should be zero");
470
471        // Verify LSB contains length * 2
472        assert_eq!(
473            bytes[31],
474            (test_str.len() * 2) as u8,
475            "LSB should be length * 2"
476        );
477
478        // Verify bit 0 is 0 (short string marker)
479        assert_eq!(bytes[31] & 1, 0, "Bit 0 should be 0 for short strings");
480    }
481
482    #[test]
483    fn test_encode_short_string_empty() {
484        let encoded = encode_short_string(&[]);
485        let bytes = encoded.to_be_bytes::<32>();
486
487        // All bytes should be zero for empty string
488        assert_eq!(bytes, [0u8; 32], "Empty string should encode to all zeros");
489    }
490
491    #[test]
492    fn test_encode_long_string_length_formula() {
493        for len in [32, 33, 100, 1000, 10000] {
494            let encoded = encode_long_string_length(len);
495            let expected = U256::from(len * 2 + 1);
496            assert_eq!(
497                encoded, expected,
498                "Long string length encoding mismatch for {len} bytes"
499            );
500
501            // Verify bit 0 is 1 (long string marker)
502            assert_eq!(encoded.byte(0) & 1, 1, "Bit 0 should be 1 for long strings");
503        }
504    }
505
506    #[test]
507    fn test_encode_decode_roundtrip() {
508        // Short strings roundtrip
509        for len in [0, 1, 15, 30, 31] {
510            let bytes = vec![b'x'; len];
511            let encoded = encode_short_string(&bytes);
512            let decoded_len = calc_string_length(encoded, false);
513            assert_eq!(
514                decoded_len, len,
515                "Short string roundtrip failed for {len} bytes"
516            );
517        }
518
519        // Long strings roundtrip
520        for len in [32, 33, 64, 100] {
521            let encoded = encode_long_string_length(len);
522            let decoded_len = calc_string_length(encoded, true);
523            assert_eq!(
524                decoded_len, len,
525                "Long string roundtrip failed for {len} bytes"
526            );
527        }
528    }
529
530    // -- PROPERTY TESTS FOR STORAGE INTERACTION -------------------------------
531
532    proptest! {
533        #![proptest_config(ProptestConfig::with_cases(500))]
534
535        #[test]
536        fn test_short_strings(s in arb_short_string(), base_slot in arb_safe_slot()) {
537            let (mut storage, address) = setup_storage();
538            StorageCtx::enter(&mut storage, || {
539                let mut slot = BytesLikeHandler::<String>::new(base_slot, address);
540
541                // Verify store → load roundtrip
542                slot.write(s.clone()).unwrap();
543                let loaded = slot.read().unwrap();
544                prop_assert_eq!(&s, &loaded, "Short string roundtrip failed");
545
546                // Verify delete works
547                slot.delete().unwrap();
548                let after_delete = slot.read().unwrap();
549                prop_assert_eq!(after_delete, String::new(), "Short string not empty after delete");
550
551                Ok(())
552            }).unwrap();
553        }
554
555        #[test]
556        #[allow(clippy::redundant_clone)]
557        fn test_32byte_strings(s in arb_32byte_string(), base_slot in arb_safe_slot()) {
558            let (mut storage, address) = setup_storage();
559            StorageCtx::enter(&mut storage, || {
560                // Verify 32-byte boundary string is stored correctly
561                prop_assert_eq!(s.len(), 32, "Generated string should be exactly 32 bytes");
562
563                let mut slot = BytesLikeHandler::<String>::new(base_slot, address);
564
565                // Verify store → load roundtrip
566                slot.write(s.clone()).unwrap();
567                let loaded = slot.read().unwrap();
568                prop_assert_eq!(s.clone(), loaded, "32-byte string roundtrip failed");
569
570                // Verify delete works
571                slot.delete().unwrap();
572                let after_delete = slot.read().unwrap();
573                prop_assert_eq!(after_delete, String::new(), "32-byte string not empty after delete");
574
575                Ok(())
576            }).unwrap();
577        }
578
579        #[test]
580        fn test_long_strings(s in arb_long_string(), base_slot in arb_safe_slot()) {
581            let (mut storage, address) = setup_storage();
582            StorageCtx::enter(&mut storage, || {
583                let mut slot = BytesLikeHandler::<String>::new(base_slot, address);
584
585                // Verify store → load roundtrip
586                slot.write(s.clone()).unwrap();
587                let loaded = slot.read().unwrap();
588                prop_assert_eq!(&s, &loaded, "Long string roundtrip failed for length: {}", s.len());
589
590                // Calculate how many data slots were used
591                let chunks = calc_chunks(s.len());
592
593                // Verify delete works (clears both main slot and keccak256-addressed data)
594                slot.delete().unwrap();
595                let after_delete = slot.read().unwrap();
596                prop_assert_eq!(after_delete, String::new(), "Long string not empty after delete");
597
598                // Verify all keccak256-addressed data slots are actually zero
599                let data_slot_start = calc_data_slot(base_slot);
600                for i in 0..chunks {
601                    let slot = Slot::<U256>::new_at_offset(data_slot_start, i, address);
602                    let value = slot.read().unwrap();
603                    prop_assert_eq!(value, U256::ZERO, "Data slot not cleared after delete");
604                }
605
606                Ok(())
607            }).unwrap();
608        }
609
610        #[test]
611        fn test_short_bytes(b in arb_short_bytes(), base_slot in arb_safe_slot()) {
612            let (mut storage, address) = setup_storage();
613            StorageCtx::enter(&mut storage, || {
614                let mut slot = BytesLikeHandler::<Bytes>::new(base_slot, address);
615
616                // Verify store → load roundtrip
617                slot.write(b.clone()).unwrap();
618                let loaded = slot.read().unwrap();
619                prop_assert_eq!(&b, &loaded, "Short bytes roundtrip failed for length: {}", b.len());
620
621                // Verify delete works
622                slot.delete().unwrap();
623                let after_delete = slot.read().unwrap();
624                prop_assert_eq!(after_delete, Bytes::new(), "Short bytes not empty after delete");
625
626                Ok(())
627            }).unwrap();
628        }
629
630        #[test]
631        fn test_32byte_bytes(b in arb_32byte_bytes(), base_slot in arb_safe_slot()) {
632            let (mut storage, address) = setup_storage();
633            StorageCtx::enter(&mut storage, || {
634                // Verify 32-byte boundary bytes is stored correctly
635                prop_assert_eq!(b.len(), 32, "Generated bytes should be exactly 32 bytes");
636
637                let mut slot = BytesLikeHandler::<Bytes>::new(base_slot, address);
638
639                // Verify store → load roundtrip
640                slot.write(b.clone()).unwrap();
641                let loaded = slot.read().unwrap();
642                prop_assert_eq!(&b, &loaded, "32-byte bytes roundtrip failed");
643
644                // Verify delete works
645                slot.delete().unwrap();
646                let after_delete = slot.read().unwrap();
647                prop_assert_eq!(after_delete, Bytes::new(), "32-byte bytes not empty after delete");
648
649                Ok(())
650            }).unwrap();
651        }
652
653        #[test]
654        fn test_long_bytes(b in arb_long_bytes(), base_slot in arb_safe_slot()) {
655            let (mut storage, address) = setup_storage();
656            StorageCtx::enter(&mut storage, || {
657                let mut slot = BytesLikeHandler::<Bytes>::new(base_slot, address);
658
659                // Verify store → load roundtrip
660                slot.write(b.clone()).unwrap();
661                let loaded = slot.read().unwrap();
662                prop_assert_eq!(&b, &loaded, "Long bytes roundtrip failed for length: {}", b.len());
663
664                // Calculate how many data slots were used
665                let chunks = calc_chunks(b.len());
666
667                // Verify delete works (clears both main slot and keccak256-addressed data)
668                slot.delete().unwrap();
669                let after_delete = slot.read().unwrap();
670                prop_assert_eq!(after_delete, Bytes::new(), "Long bytes not empty after delete");
671
672                // Verify all keccak256-addressed data slots are actually zero
673                let data_slot_start = calc_data_slot(base_slot);
674                for i in 0..chunks {
675                    let slot = Slot::<U256>::new_at_offset(data_slot_start, i, address);
676                    let value = slot.read().unwrap();
677                    prop_assert_eq!(value, U256::ZERO, "Data slot not cleared after delete");
678                }
679
680                Ok(())
681            }).unwrap();
682        }
683
684        #[test]
685        fn test_string_len(s in prop_oneof![arb_short_string(), arb_long_string()], base_slot in arb_safe_slot()) {
686            let (mut storage, address) = setup_storage();
687            StorageCtx::enter(&mut storage, || {
688                let mut slot = BytesLikeHandler::<String>::new(base_slot, address);
689
690                // Verify empty handler returns 0
691                prop_assert_eq!(slot.len().unwrap(), 0, "Empty string should have len 0");
692                prop_assert!(slot.is_empty().unwrap(), "Empty string should be empty");
693
694                // Write string and verify len matches
695                slot.write(s.clone()).unwrap();
696                prop_assert_eq!(slot.len().unwrap(), s.len(), "len() should match string byte length");
697                prop_assert_eq!(slot.is_empty().unwrap(), s.is_empty(), "is_empty() should match");
698
699                // After delete, len should be 0 again
700                slot.delete().unwrap();
701                prop_assert_eq!(slot.len().unwrap(), 0, "Deleted string should have len 0");
702
703                Ok(())
704            }).unwrap();
705        }
706    }
707}