tempo_precompiles/storage/types/
bytes_like.rs1use 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#[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 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 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 let bytes = base_value.to_be_bytes::<32>();
134 into(bytes[..length].to_vec())
135 }
136}
137
138#[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 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 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#[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 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 for i in 0..chunks {
185 let slot = slot_start + U256::from(i);
186 storage.sstore(slot, U256::ZERO)?;
187 }
188 }
189
190 storage.sstore(base_slot, U256::ZERO)
192}
193
194#[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 Ok([encode_long_string_length(length)])
204 }
205}
206
207#[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 Err(TempoPrecompileError::Fatal(
220 "Cannot reconstruct long string from single word. Use load() instead.".into(),
221 ))
222 } else {
223 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#[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#[inline]
244fn is_long_string(slot_value: U256) -> bool {
245 (slot_value.byte(0) & 1) != 0
246}
247
248#[inline]
250fn calc_string_length(slot_value: U256, is_long: bool) -> usize {
251 if is_long {
252 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 let bytes = slot_value.to_be_bytes::<32>();
262 (bytes[31] / 2) as usize
263 }
264}
265
266#[inline]
268fn calc_chunks(byte_length: usize) -> usize {
269 byte_length.div_ceil(32)
270}
271
272#[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#[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 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 fn setup_test_contract() -> TestContract {
318 TestContract {
319 address: Address::random(),
320 storage: HashMapStorageProvider::new(1),
321 }
322 }
323
324 fn arb_safe_slot() -> impl Strategy<Value = U256> {
326 any::<[u64; 4]>().prop_map(|limbs| {
327 U256::from_limbs(limbs) % (U256::MAX - U256::from(10000))
329 })
330 }
331
332 fn arb_short_string() -> impl Strategy<Value = String> {
334 prop_oneof![
335 Just(String::new()),
337 "[a-zA-Z0-9]{1,31}",
339 "[\u{0041}-\u{005A}\u{4E00}-\u{4E19}]{1,10}",
341 ]
342 }
343
344 fn arb_32byte_string() -> impl Strategy<Value = String> {
346 "[a-zA-Z0-9]{32}"
347 }
348
349 fn arb_long_string() -> impl Strategy<Value = String> {
351 prop_oneof![
352 "[a-zA-Z0-9]{33,100}",
354 "[\u{0041}-\u{005A}\u{4E00}-\u{4E19}]{11,30}",
356 ]
357 }
358
359 fn arb_short_bytes() -> impl Strategy<Value = Bytes> {
361 prop::collection::vec(any::<u8>(), 0..=31).prop_map(Bytes::from)
362 }
363
364 fn arb_32byte_bytes() -> impl Strategy<Value = Bytes> {
366 prop::collection::vec(any::<u8>(), 32..=32).prop_map(Bytes::from)
367 }
368
369 fn arb_long_bytes() -> impl Strategy<Value = Bytes> {
371 prop::collection::vec(any::<u8>(), 33..=100).prop_map(Bytes::from)
372 }
373
374 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 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 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 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 assert_eq!(s.len(), 32, "Generated string should be exactly 32 bytes");
405
406 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 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 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 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 let chunks = calc_chunks(s.len());
434
435 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 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 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 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 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 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 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 assert_eq!(b.len(), 32, "Generated bytes should be exactly 32 bytes");
488
489 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 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 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 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 let chunks = calc_chunks(b.len());
517
518 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 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 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 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}