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::{StorageCtx, 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        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!(ctx.is_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!(ctx.is_full(), "Bytes cannot be packed");
126        store_bytes_like(self.as_ref(), storage, slot, ctx)
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!(ctx.is_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!(ctx.is_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!(ctx.is_full(), "String cannot be packed");
151        store_bytes_like(self.as_bytes(), storage, slot, ctx)
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!(ctx.is_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::new();
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/// On T5+ performs tail cleanup when the prior value was long and the new one takes fewer slots.
205#[inline]
206fn store_bytes_like<S: StorageOps>(
207    bytes: &[u8],
208    storage: &mut S,
209    base_slot: U256,
210    ctx: LayoutCtx,
211) -> Result<()> {
212    let new_len = bytes.len();
213    let new_is_long = new_len > 31;
214    let mut data_slot: Option<U256> = None;
215
216    // (T5+) Cleanup stale tail if necessary.
217    if !ctx.skip_tail_cleanup() && StorageCtx.spec().is_t5() {
218        let prev = storage.load(base_slot)?;
219        // Only applicable to long strings, as short ones always get overridden.
220        if is_long_string(prev) {
221            let prev_chunks = calc_chunks(calc_string_length(prev, true)?);
222            let new_chunks = if new_is_long { calc_chunks(new_len) } else { 0 };
223            if prev_chunks > new_chunks {
224                let slot_start = calc_data_slot(base_slot);
225                for i in new_chunks..prev_chunks {
226                    storage.store(slot_start + U256::from(i), U256::ZERO)?;
227                }
228                data_slot = Some(slot_start);
229            }
230        }
231    }
232
233    if !new_is_long {
234        storage.store(base_slot, encode_short_string(bytes))
235    } else {
236        storage.store(base_slot, encode_long_string_length(new_len))?;
237
238        // Store data in chunks at keccak256(base_slot) + i
239        let slot_start = data_slot.unwrap_or_else(|| calc_data_slot(base_slot));
240        let chunks = calc_chunks(new_len);
241        for i in 0..chunks {
242            let slot = slot_start + U256::from(i);
243            let chunk_start = i * 32;
244            let chunk_end = (chunk_start + 32).min(new_len);
245            let chunk = &bytes[chunk_start..chunk_end];
246
247            // Pad chunk to 32 bytes if it's the last chunk
248            let mut chunk_bytes = [0u8; 32];
249            chunk_bytes[..chunk.len()].copy_from_slice(chunk);
250
251            storage.store(slot, U256::from_be_bytes(chunk_bytes))?;
252        }
253
254        Ok(())
255    }
256}
257
258/// Generic delete implementation for byte-like types (String, Bytes) using Solidity's encoding.
259///
260/// Clears both the main slot and any keccak256-addressed data slots for long strings.
261#[inline]
262fn delete_bytes_like<S: StorageOps>(storage: &mut S, base_slot: U256) -> Result<()> {
263    let base_value = storage.load(base_slot)?;
264    let is_long = is_long_string(base_value);
265
266    if is_long {
267        // Long string: need to clear data slots as well
268        let length = calc_string_length(base_value, true)?;
269        let slot_start = calc_data_slot(base_slot);
270        let chunks = calc_chunks(length);
271
272        // Clear all data slots
273        for i in 0..chunks {
274            let slot = slot_start + U256::from(i);
275            storage.store(slot, U256::ZERO)?;
276        }
277    }
278
279    // Clear the main slot
280    storage.store(base_slot, U256::ZERO)
281}
282
283/// Compute the storage slot where long string data begins.
284///
285/// For long strings (≥32 bytes), data is stored starting at `keccak256(base_slot)`.
286#[inline]
287fn calc_data_slot(base_slot: U256) -> U256 {
288    U256::from_be_bytes(keccak256(base_slot.to_be_bytes::<32>()).0)
289}
290
291/// Check if a storage slot value represents a long string.
292///
293/// Solidity string encoding uses bit 0 of the LSB to distinguish:
294/// - Bit 0 = 0: Short string (≤31 bytes)
295/// - Bit 0 = 1: Long string (≥32 bytes)
296#[inline]
297fn is_long_string(slot_value: U256) -> bool {
298    (slot_value.byte(0) & 1) != 0
299}
300
301/// Extract and validate the string length from a storage slot value.
302///
303/// Returns an error if the decoded length overflows `usize` or a short-string length exceeds 31.
304#[inline]
305fn calc_string_length(slot_value: U256, is_long: bool) -> Result<usize> {
306    if is_long {
307        // Long string: slot stores (length * 2 + 1)
308        // Extract length: (value - 1) / 2
309        let length_times_two_plus_one: U256 = slot_value;
310        let length_times_two: U256 = length_times_two_plus_one - U256::ONE;
311        let length_u256: U256 = length_times_two >> 1;
312        if length_u256 > U256::from(u32::MAX) {
313            return Err(TempoPrecompileError::under_overflow());
314        }
315        Ok(length_u256.to::<usize>())
316    } else {
317        // Short string: LSB stores (length * 2)
318        // Extract length: LSB / 2
319        let bytes = slot_value.to_be_bytes::<32>();
320        let length = (bytes[31] / 2) as usize;
321        if length > 31 {
322            // Unreachable unless the state has been tampered
323            return Err(TempoPrecompileError::Fatal(format!(
324                "short string length {length} exceeds maximum of 31 bytes"
325            )));
326        }
327        Ok(length)
328    }
329}
330
331/// Compute the number of 32-byte chunks needed to store a byte string.
332#[inline]
333fn calc_chunks(byte_length: usize) -> usize {
334    byte_length.div_ceil(32)
335}
336
337/// Encode a short string (≤31 bytes) into a U256 for inline storage.
338///
339/// Format: bytes left-aligned, LSB contains (length * 2)
340#[inline]
341fn encode_short_string(bytes: &[u8]) -> U256 {
342    let mut storage_bytes = [0u8; 32];
343    storage_bytes[..bytes.len()].copy_from_slice(bytes);
344    storage_bytes[31] = (bytes.len() * 2) as u8;
345    U256::from_be_bytes(storage_bytes)
346}
347
348/// Encode the length metadata for a long string (≥32 bytes).
349///
350/// Returns `length * 2 + 1` where bit 0 = 1 indicates long string storage.
351#[inline]
352fn encode_long_string_length(byte_length: usize) -> U256 {
353    U256::from(byte_length * 2 + 1)
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::{
360        storage::{Handler, StorageCtx},
361        test_util::setup_storage,
362    };
363    use proptest::prelude::*;
364
365    // Strategy for generating random U256 slot values that won't overflow
366    fn arb_safe_slot() -> impl Strategy<Value = U256> {
367        any::<[u64; 4]>().prop_map(|limbs| {
368            // Ensure we don't overflow by limiting to a reasonable range
369            U256::from_limbs(limbs) % (U256::MAX - U256::from(10000))
370        })
371    }
372
373    // Strategy for short strings (0-31 bytes) - uses inline storage
374    fn arb_short_string() -> impl Strategy<Value = String> {
375        prop_oneof![
376            // Empty string
377            Just(String::new()),
378            // ASCII strings (1-31 bytes)
379            "[a-zA-Z0-9]{1,31}",
380            // Unicode strings (up to 31 bytes)
381            "[\u{0041}-\u{005A}\u{4E00}-\u{4E19}]{1,10}",
382        ]
383    }
384
385    // Strategy for exactly 32-byte strings - boundary between inline and heap storage
386    fn arb_32byte_string() -> impl Strategy<Value = String> {
387        "[a-zA-Z0-9]{32}"
388    }
389
390    // Strategy for long strings (33-100 bytes) - uses heap storage
391    fn arb_long_string() -> impl Strategy<Value = String> {
392        prop_oneof![
393            // ASCII strings (33-100 bytes)
394            "[a-zA-Z0-9]{33,100}",
395            // Unicode strings (>32 bytes)
396            "[\u{0041}-\u{005A}\u{4E00}-\u{4E19}]{11,30}",
397        ]
398    }
399
400    // Strategy for short byte arrays (0-31 bytes) - uses inline storage
401    fn arb_short_bytes() -> impl Strategy<Value = Bytes> {
402        prop::collection::vec(any::<u8>(), 0..=31).prop_map(Bytes::from)
403    }
404
405    // Strategy for exactly 32-byte arrays - boundary between inline and heap storage
406    fn arb_32byte_bytes() -> impl Strategy<Value = Bytes> {
407        prop::collection::vec(any::<u8>(), 32..=32).prop_map(Bytes::from)
408    }
409
410    // Strategy for long byte arrays (33-100 bytes) - uses heap storage
411    fn arb_long_bytes() -> impl Strategy<Value = Bytes> {
412        prop::collection::vec(any::<u8>(), 33..=100).prop_map(Bytes::from)
413    }
414
415    // -- UNIT TESTS FOR HELPER FUNCTIONS (NO STORAGE) ------------------------
416
417    #[test]
418    fn test_calc_data_slot_matches_manual_keccak() {
419        let base_slot = U256::random();
420        let data_slot = calc_data_slot(base_slot);
421
422        // Manual computation
423        let expected = U256::from_be_bytes(keccak256(base_slot.to_be_bytes::<32>()).0);
424
425        assert_eq!(
426            data_slot, expected,
427            "calc_data_slot should match manual keccak256 computation"
428        );
429    }
430
431    #[test]
432    fn test_is_long_string_boundaries() {
433        // Short string (31 bytes): length * 2 = 62 (0x3E), bit 0 = 0
434        let short_31_bytes = encode_short_string(&[b'a'; 31]);
435        assert!(
436            !is_long_string(short_31_bytes),
437            "31-byte string should be short"
438        );
439
440        // Long string (32 bytes): length * 2 + 1 = 65 (0x41), bit 0 = 1
441        let long_32_bytes = encode_long_string_length(32);
442        assert!(
443            is_long_string(long_32_bytes),
444            "32-byte string should be long"
445        );
446
447        // Edge case: empty string
448        let empty = encode_short_string(&[]);
449        assert!(!is_long_string(empty), "Empty string should be short");
450
451        // Edge case: 1-byte string
452        let one_byte = encode_short_string(b"x");
453        assert!(!is_long_string(one_byte), "1-byte string should be short");
454    }
455
456    #[test]
457    fn test_calc_string_length_short() {
458        // Test short strings with various lengths
459        for len in 0..=31 {
460            let bytes = vec![b'a'; len];
461            let encoded = encode_short_string(&bytes);
462            let decoded_len = calc_string_length(encoded, false);
463            assert_eq!(
464                decoded_len,
465                Ok(len),
466                "Short string length mismatch for {len} bytes"
467            );
468        }
469    }
470
471    #[test]
472    fn test_calc_string_length_long() {
473        // Test long strings with various lengths
474        for len in [32, 33, 63, 64, 65, 100, 1000, 10000] {
475            let encoded = encode_long_string_length(len);
476            let decoded_len = calc_string_length(encoded, true);
477            assert_eq!(
478                decoded_len,
479                Ok(len),
480                "Long string length mismatch for {len} bytes"
481            );
482        }
483    }
484
485    #[test]
486    fn test_calc_chunks_boundaries() {
487        assert_eq!(calc_chunks(0), 0, "0 bytes should require 0 chunks");
488        assert_eq!(calc_chunks(1), 1, "1 byte should require 1 chunk");
489        assert_eq!(calc_chunks(31), 1, "31 bytes should require 1 chunk");
490        assert_eq!(calc_chunks(32), 1, "32 bytes should require 1 chunk");
491        assert_eq!(calc_chunks(33), 2, "33 bytes should require 2 chunks");
492        assert_eq!(calc_chunks(64), 2, "64 bytes should require 2 chunks");
493        assert_eq!(calc_chunks(65), 3, "65 bytes should require 3 chunks");
494        assert_eq!(calc_chunks(100), 4, "100 bytes should require 4 chunks");
495    }
496
497    #[test]
498    fn test_encode_short_string_format() {
499        let test_str = b"Hello";
500        let encoded = encode_short_string(test_str);
501        let bytes = encoded.to_be_bytes::<32>();
502
503        // Verify data is left-aligned
504        assert_eq!(&bytes[..5], test_str, "Data should be left-aligned");
505
506        // Verify padding is zero
507        assert_eq!(&bytes[5..31], &[0u8; 26], "Padding should be zero");
508
509        // Verify LSB contains length * 2
510        assert_eq!(
511            bytes[31],
512            (test_str.len() * 2) as u8,
513            "LSB should be length * 2"
514        );
515
516        // Verify bit 0 is 0 (short string marker)
517        assert_eq!(bytes[31] & 1, 0, "Bit 0 should be 0 for short strings");
518    }
519
520    #[test]
521    fn test_encode_short_string_empty() {
522        let encoded = encode_short_string(&[]);
523        let bytes = encoded.to_be_bytes::<32>();
524
525        // All bytes should be zero for empty string
526        assert_eq!(bytes, [0u8; 32], "Empty string should encode to all zeros");
527    }
528
529    #[test]
530    fn test_encode_long_string_length_formula() {
531        for len in [32, 33, 100, 1000, 10000] {
532            let encoded = encode_long_string_length(len);
533            let expected = U256::from(len * 2 + 1);
534            assert_eq!(
535                encoded, expected,
536                "Long string length encoding mismatch for {len} bytes"
537            );
538
539            // Verify bit 0 is 1 (long string marker)
540            assert_eq!(encoded.byte(0) & 1, 1, "Bit 0 should be 1 for long strings");
541        }
542    }
543
544    #[test]
545    fn test_encode_decode_roundtrip() {
546        // Short strings roundtrip
547        for len in [0, 1, 15, 30, 31] {
548            let bytes = vec![b'x'; len];
549            let encoded = encode_short_string(&bytes);
550            let decoded_len = calc_string_length(encoded, false);
551            assert_eq!(
552                decoded_len,
553                Ok(len),
554                "Short string roundtrip failed for {len} bytes"
555            );
556        }
557
558        // Long strings roundtrip
559        for len in [32, 33, 64, 100] {
560            let encoded = encode_long_string_length(len);
561            let decoded_len = calc_string_length(encoded, true);
562            assert_eq!(
563                decoded_len,
564                Ok(len),
565                "Long string roundtrip failed for {len} bytes"
566            );
567        }
568    }
569
570    // -- TAMPERED STATE TESTS --------------------------------------------------
571
572    #[test]
573    fn test_calc_string_length_tampered() {
574        // -- long-string overflow -----------------------------------------------
575
576        // PoC value: decoded length = 0x0004000000000000, exceeds u32::MAX
577        let malicious_slot = U256::from(0x0008000000000001u64);
578        assert!(is_long_string(malicious_slot));
579        assert_eq!(
580            calc_string_length(malicious_slot, true),
581            Err(TempoPrecompileError::under_overflow())
582        );
583
584        // Boundary: u32::MAX is accepted
585        let at_max = U256::from(u64::from(u32::MAX) * 2 + 1);
586        assert_eq!(calc_string_length(at_max, true), Ok(u32::MAX as usize));
587
588        // Boundary: u32::MAX + 1 is rejected
589        let above_max = U256::from((u64::from(u32::MAX) + 1) * 2 + 1);
590        assert_eq!(
591            calc_string_length(above_max, true),
592            Err(TempoPrecompileError::under_overflow())
593        );
594
595        // -- short-string tamper ------------------------------------------------
596
597        // Valid boundary: 31 bytes → LSB = 62 (0x3E), must be accepted
598        let max_short = U256::from(31u64 * 2);
599        assert!(!is_long_string(max_short));
600        assert_eq!(calc_string_length(max_short, false), Ok(31));
601
602        // Tampered: LSB = 0xFE → decoded length = 127, must be rejected
603        let malicious_short = U256::from(0xFEu64);
604        assert!(!is_long_string(malicious_short));
605        assert!(calc_string_length(malicious_short, false).is_err());
606
607        // Boundary: 32 bytes → LSB = 64 (0x40), must be rejected
608        let above_short = U256::from(32u64 * 2);
609        assert!(calc_string_length(above_short, false).is_err());
610    }
611
612    // -- PROPERTY TESTS FOR STORAGE INTERACTION -------------------------------
613
614    proptest! {
615        #![proptest_config(ProptestConfig::with_cases(500))]
616
617        #[test]
618        fn test_short_strings(s in arb_short_string(), base_slot in arb_safe_slot()) {
619            let (mut storage, address) = setup_storage();
620            StorageCtx::enter(&mut storage, || {
621                let mut slot = BytesLikeHandler::<String>::new(base_slot, address);
622
623                // Verify store → load roundtrip
624                slot.write(s.clone()).unwrap();
625                let loaded = slot.read().unwrap();
626                prop_assert_eq!(&s, &loaded, "Short string roundtrip failed");
627
628                // Verify delete works
629                slot.delete().unwrap();
630                let after_delete = slot.read().unwrap();
631                prop_assert_eq!(after_delete, String::new(), "Short string not empty after delete");
632
633                Ok(())
634            }).unwrap();
635        }
636
637        #[test]
638        #[allow(clippy::redundant_clone)]
639        fn test_32byte_strings(s in arb_32byte_string(), base_slot in arb_safe_slot()) {
640            let (mut storage, address) = setup_storage();
641            StorageCtx::enter(&mut storage, || {
642                // Verify 32-byte boundary string is stored correctly
643                prop_assert_eq!(s.len(), 32, "Generated string should be exactly 32 bytes");
644
645                let mut slot = BytesLikeHandler::<String>::new(base_slot, address);
646
647                // Verify store → load roundtrip
648                slot.write(s.clone()).unwrap();
649                let loaded = slot.read().unwrap();
650                prop_assert_eq!(s.clone(), loaded, "32-byte string roundtrip failed");
651
652                // Verify delete works
653                slot.delete().unwrap();
654                let after_delete = slot.read().unwrap();
655                prop_assert_eq!(after_delete, String::new(), "32-byte string not empty after delete");
656
657                Ok(())
658            }).unwrap();
659        }
660
661        #[test]
662        fn test_long_strings(s in arb_long_string(), base_slot in arb_safe_slot()) {
663            let (mut storage, address) = setup_storage();
664            StorageCtx::enter(&mut storage, || {
665                let mut slot = BytesLikeHandler::<String>::new(base_slot, address);
666
667                // Verify store → load roundtrip
668                slot.write(s.clone()).unwrap();
669                let loaded = slot.read().unwrap();
670                prop_assert_eq!(&s, &loaded, "Long string roundtrip failed for length: {}", s.len());
671
672                // Calculate how many data slots were used
673                let chunks = calc_chunks(s.len());
674
675                // Verify delete works (clears both main slot and keccak256-addressed data)
676                slot.delete().unwrap();
677                let after_delete = slot.read().unwrap();
678                prop_assert_eq!(after_delete, String::new(), "Long string not empty after delete");
679
680                // Verify all keccak256-addressed data slots are actually zero
681                let data_slot_start = calc_data_slot(base_slot);
682                for i in 0..chunks {
683                    let slot = Slot::<U256>::new_at_offset(data_slot_start, i, address);
684                    let value = slot.read().unwrap();
685                    prop_assert_eq!(value, U256::ZERO, "Data slot not cleared after delete");
686                }
687
688                Ok(())
689            }).unwrap();
690        }
691
692        #[test]
693        fn test_short_bytes(b in arb_short_bytes(), base_slot in arb_safe_slot()) {
694            let (mut storage, address) = setup_storage();
695            StorageCtx::enter(&mut storage, || {
696                let mut slot = BytesLikeHandler::<Bytes>::new(base_slot, address);
697
698                // Verify store → load roundtrip
699                slot.write(b.clone()).unwrap();
700                let loaded = slot.read().unwrap();
701                prop_assert_eq!(&b, &loaded, "Short bytes roundtrip failed for length: {}", b.len());
702
703                // Verify delete works
704                slot.delete().unwrap();
705                let after_delete = slot.read().unwrap();
706                prop_assert_eq!(after_delete, Bytes::new(), "Short bytes not empty after delete");
707
708                Ok(())
709            }).unwrap();
710        }
711
712        #[test]
713        fn test_32byte_bytes(b in arb_32byte_bytes(), base_slot in arb_safe_slot()) {
714            let (mut storage, address) = setup_storage();
715            StorageCtx::enter(&mut storage, || {
716                // Verify 32-byte boundary bytes is stored correctly
717                prop_assert_eq!(b.len(), 32, "Generated bytes should be exactly 32 bytes");
718
719                let mut slot = BytesLikeHandler::<Bytes>::new(base_slot, address);
720
721                // Verify store → load roundtrip
722                slot.write(b.clone()).unwrap();
723                let loaded = slot.read().unwrap();
724                prop_assert_eq!(&b, &loaded, "32-byte bytes roundtrip failed");
725
726                // Verify delete works
727                slot.delete().unwrap();
728                let after_delete = slot.read().unwrap();
729                prop_assert_eq!(after_delete, Bytes::new(), "32-byte bytes not empty after delete");
730
731                Ok(())
732            }).unwrap();
733        }
734
735        #[test]
736        fn test_long_bytes(b in arb_long_bytes(), base_slot in arb_safe_slot()) {
737            let (mut storage, address) = setup_storage();
738            StorageCtx::enter(&mut storage, || {
739                let mut slot = BytesLikeHandler::<Bytes>::new(base_slot, address);
740
741                // Verify store → load roundtrip
742                slot.write(b.clone()).unwrap();
743                let loaded = slot.read().unwrap();
744                prop_assert_eq!(&b, &loaded, "Long bytes roundtrip failed for length: {}", b.len());
745
746                // Calculate how many data slots were used
747                let chunks = calc_chunks(b.len());
748
749                // Verify delete works (clears both main slot and keccak256-addressed data)
750                slot.delete().unwrap();
751                let after_delete = slot.read().unwrap();
752                prop_assert_eq!(after_delete, Bytes::new(), "Long bytes not empty after delete");
753
754                // Verify all keccak256-addressed data slots are actually zero
755                let data_slot_start = calc_data_slot(base_slot);
756                for i in 0..chunks {
757                    let slot = Slot::<U256>::new_at_offset(data_slot_start, i, address);
758                    let value = slot.read().unwrap();
759                    prop_assert_eq!(value, U256::ZERO, "Data slot not cleared after delete");
760                }
761
762                Ok(())
763            }).unwrap();
764        }
765
766        #[test]
767        fn test_string_len(s in prop_oneof![arb_short_string(), arb_long_string()], base_slot in arb_safe_slot()) {
768            let (mut storage, address) = setup_storage();
769            StorageCtx::enter(&mut storage, || {
770                let mut slot = BytesLikeHandler::<String>::new(base_slot, address);
771
772                // Verify empty handler returns 0
773                prop_assert_eq!(slot.len().unwrap(), 0, "Empty string should have len 0");
774                prop_assert!(slot.is_empty().unwrap(), "Empty string should be empty");
775
776                // Write string and verify len matches
777                slot.write(s.clone()).unwrap();
778                prop_assert_eq!(slot.len().unwrap(), s.len(), "len() should match string byte length");
779                prop_assert_eq!(slot.is_empty().unwrap(), s.is_empty(), "is_empty() should match");
780
781                // After delete, len should be 0 again
782                slot.delete().unwrap();
783                prop_assert_eq!(slot.len().unwrap(), 0, "Deleted string should have len 0");
784
785                Ok(())
786            }).unwrap();
787        }
788    }
789}