Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions crates/precompiles-macros/src/storable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ fn derive_struct_impl(input: &DeriveInput, data_struct: &DataStruct) -> syn::Res
ctx: crate::storage::LayoutCtx
) -> crate::error::Result<Self> {
use crate::storage::Storable;
debug_assert_eq!(ctx, crate::storage::LayoutCtx::FULL, "Struct types can only be loaded with LayoutCtx::FULL");
debug_assert!(ctx.is_full(), "Struct types can only be loaded with a full-slot LayoutCtx (FULL or INIT)");

#load_impl

Expand All @@ -144,7 +144,7 @@ fn derive_struct_impl(input: &DeriveInput, data_struct: &DataStruct) -> syn::Res
ctx: crate::storage::LayoutCtx
) -> crate::error::Result<()> {
use crate::storage::Storable;
debug_assert_eq!(ctx, crate::storage::LayoutCtx::FULL, "Struct types can only be stored with LayoutCtx::FULL");
debug_assert!(ctx.is_full(), "Struct types can only be stored with a full-slot LayoutCtx (FULL or INIT)");

#store_impl

Expand All @@ -157,7 +157,7 @@ fn derive_struct_impl(input: &DeriveInput, data_struct: &DataStruct) -> syn::Res
ctx: crate::storage::LayoutCtx
) -> crate::error::Result<()> {
use crate::storage::Storable;
debug_assert_eq!(ctx, crate::storage::LayoutCtx::FULL, "Struct types can only be deleted with LayoutCtx::FULL");
debug_assert!(ctx.is_full(), "Struct types can only be deleted with a full-slot LayoutCtx (FULL or INIT)");

#delete_impl

Expand Down Expand Up @@ -547,7 +547,11 @@ fn gen_store_impl(fields: &[(&Ident, &Type)], packing: &Ident) -> TokenStream {
storage.store(base_slot + ::alloy::primitives::U256::from(offset), pending_val)?;
pending_offset = None;
}
<#ty as crate::storage::Storable>::store(&self.#name, storage, #slot_addr, crate::storage::LayoutCtx::FULL)?;
// Dynamic fields need INIT to skip stale-tail cleanup on virgin storage.
// Static fields ignore INIT, so we always use FULL for them.
let ctx = if <#ty as crate::storage::StorableType>::IS_DYNAMIC { ctx } else { crate::storage::LayoutCtx::FULL };
debug_assert!(ctx.is_full(), "Struct types can only use full-slot LayoutCtx (FULL or INIT)");
<#ty as crate::storage::Storable>::store(&self.#name, storage, #slot_addr, ctx)?;
}

// Store if this is the last field in the current slot group
Expand Down Expand Up @@ -579,7 +583,11 @@ fn gen_store_impl(fields: &[(&Ident, &Type)], packing: &Ident) -> TokenStream {
pending_offset = None;
}
} else {
<#ty as crate::storage::Storable>::store(&self.#name, storage, #slot_addr, crate::storage::LayoutCtx::FULL)?;
// Dynamic fields need INIT to skip stale-tail cleanup on virgin storage.
// Static fields ignore INIT, so we always use FULL for them.
let ctx = if <#ty as crate::storage::StorableType>::IS_DYNAMIC { ctx } else { crate::storage::LayoutCtx::FULL };
debug_assert!(ctx.is_full(), "Struct types can only use full-slot LayoutCtx (FULL or INIT)");
<#ty as crate::storage::Storable>::store(&self.#name, storage, #slot_addr, ctx)?;
}
}}
}
Expand Down
59 changes: 42 additions & 17 deletions crates/precompiles/src/storage/types/bytes_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

use crate::{
error::{Result, TempoPrecompileError},
storage::{StorageOps, types::*},
storage::{StorageCtx, StorageOps, types::*},
};
use alloy::primitives::{Address, Bytes, U256, keccak256};
use std::marker::PhantomData;
Expand Down Expand Up @@ -116,28 +116,28 @@ impl<T: Storable> Handler<T> for BytesLikeHandler<T> {
impl Storable for Bytes {
#[inline]
fn load<S: StorageOps>(storage: &S, slot: U256, ctx: LayoutCtx) -> Result<Self> {
debug_assert_eq!(ctx, LayoutCtx::FULL, "Bytes cannot be packed");
debug_assert!(ctx.is_full(), "Bytes cannot be packed");
load_bytes_like(storage, slot, |data| Ok(Self::from(data)))
}

#[inline]
fn store<S: StorageOps>(&self, storage: &mut S, slot: U256, ctx: LayoutCtx) -> Result<()> {
debug_assert_eq!(ctx, LayoutCtx::FULL, "Bytes cannot be packed");
store_bytes_like(self.as_ref(), storage, slot)
debug_assert!(ctx.is_full(), "Bytes cannot be packed");
store_bytes_like(self.as_ref(), storage, slot, ctx)
}

/// Custom delete for bytes-like types: clears keccak256-addressed data slots for long values.
#[inline]
fn delete<S: StorageOps>(storage: &mut S, slot: U256, ctx: LayoutCtx) -> Result<()> {
debug_assert_eq!(ctx, LayoutCtx::FULL, "Bytes cannot be packed");
debug_assert!(ctx.is_full(), "Bytes cannot be packed");
delete_bytes_like(storage, slot)
}
}

impl Storable for String {
#[inline]
fn load<S: StorageOps>(storage: &S, slot: U256, ctx: LayoutCtx) -> Result<Self> {
debug_assert_eq!(ctx, LayoutCtx::FULL, "String cannot be packed");
debug_assert!(ctx.is_full(), "String cannot be packed");
load_bytes_like(storage, slot, |data| {
Self::from_utf8(data).map_err(|e| {
TempoPrecompileError::Fatal(format!("Invalid UTF-8 in stored string: {e}"))
Expand All @@ -147,14 +147,14 @@ impl Storable for String {

#[inline]
fn store<S: StorageOps>(&self, storage: &mut S, slot: U256, ctx: LayoutCtx) -> Result<()> {
debug_assert_eq!(ctx, LayoutCtx::FULL, "String cannot be packed");
store_bytes_like(self.as_bytes(), storage, slot)
debug_assert!(ctx.is_full(), "String cannot be packed");
store_bytes_like(self.as_bytes(), storage, slot, ctx)
}

/// Custom delete for bytes-like types: clears keccak256-addressed data slots for long values.
#[inline]
fn delete<S: StorageOps>(storage: &mut S, slot: U256, ctx: LayoutCtx) -> Result<()> {
debug_assert_eq!(ctx, LayoutCtx::FULL, "String cannot be packed");
debug_assert!(ctx.is_full(), "String cannot be packed");
delete_bytes_like(storage, slot)
}
}
Expand Down Expand Up @@ -201,22 +201,47 @@ where
}

/// Generic store implementation for byte-like types (String, Bytes) using Solidity's encoding.
/// On T5+ performs tail cleanup when the prior value was long and the new one takes fewer slots.
#[inline]
fn store_bytes_like<S: StorageOps>(bytes: &[u8], storage: &mut S, base_slot: U256) -> Result<()> {
let length = bytes.len();
if length <= 31 {
fn store_bytes_like<S: StorageOps>(
bytes: &[u8],
storage: &mut S,
base_slot: U256,
ctx: LayoutCtx,
) -> Result<()> {
let new_len = bytes.len();
let new_is_long = new_len > 31;
let mut data_slot: Option<U256> = None;

// (T5+) Cleanup stale tail if necessary.
if !ctx.skip_tail_cleanup() && StorageCtx.spec().is_t5() {
Comment thread
0xrusowsky marked this conversation as resolved.
let prev = storage.load(base_slot)?;
// Only applicable to long strings, as short ones always get overridden.
if is_long_string(prev) {
let prev_chunks = calc_chunks(calc_string_length(prev, true)?);
let new_chunks = if new_is_long { calc_chunks(new_len) } else { 0 };
if prev_chunks > new_chunks {
let slot_start = calc_data_slot(base_slot);
for i in new_chunks..prev_chunks {
storage.store(slot_start + U256::from(i), U256::ZERO)?;
}
data_slot = Some(slot_start);
}
}
}

if !new_is_long {
storage.store(base_slot, encode_short_string(bytes))
} else {
storage.store(base_slot, encode_long_string_length(length))?;
storage.store(base_slot, encode_long_string_length(new_len))?;

// Store data in chunks at keccak256(base_slot) + i
let slot_start = calc_data_slot(base_slot);
let chunks = calc_chunks(length);

let slot_start = data_slot.unwrap_or_else(|| calc_data_slot(base_slot));
let chunks = calc_chunks(new_len);
for i in 0..chunks {
let slot = slot_start + U256::from(i);
let chunk_start = i * 32;
let chunk_end = (chunk_start + 32).min(length);
let chunk_end = (chunk_start + 32).min(new_len);
let chunk = &bytes[chunk_start..chunk_end];

// Pad chunk to 32 bytes if it's the last chunk
Expand Down
36 changes: 31 additions & 5 deletions crates/precompiles/src/storage/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ impl Layout {
/// ```rs
/// enum LayoutCtx {
/// Full,
/// Packed(usize)
/// Init,
/// Packed(usize),
/// }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand All @@ -97,10 +98,21 @@ pub struct LayoutCtx(usize);
impl LayoutCtx {
/// Load/store the entire value at a given slot.
///
/// For writes, this directly overwrites the entire slot without needing SLOAD.
/// All storable types support this context.
/// For writes, this signals that the value occupies full slot(s), and that the
/// implementation must clear potential stale tail data:
///
/// - Static types overwrite the entire slot without needing an SLOAD.
/// - Dynamic types read the prior length (1 extra SLOAD) and zero any stale tail slots.
pub const FULL: Self = Self(usize::MAX);

/// Like `Full`, but the asserts the destination is virgin (zero-filled).
///
/// - Static types behave identically to `Full`.
/// - Dynamic types skip reading the prior length and clearing stale tail slots.
///
/// Used by hot paths that know by construction the target is empty.
pub const INIT: Self = Self(usize::MAX - 1);

/// Load/store a packed primitive at the given byte offset within a slot.
///
/// For writes, this requires a read-modify-write: SLOAD the current slot value,
Expand All @@ -114,15 +126,29 @@ impl LayoutCtx {
Self(offset)
}

/// Get the packed offset, returns `None` for `Full`
/// Get the packed offset, returns `None` for `FULL` and `INIT`
#[inline]
pub const fn packed_offset(&self) -> Option<usize> {
if self.0 == usize::MAX {
if self.0 >= usize::MAX - 1 {
None
} else {
Some(self.0)
}
}

/// Returns `true` if this context signals the tail doesn't need to be cleared.
///
/// Used by dynamic type's `Storable::store` to skip the extra SLOAD to check stale tails.
#[inline]
pub const fn skip_tail_cleanup(&self) -> bool {
self.0 == usize::MAX - 1
}

/// Returns true if this context is a full-slot context (`FULL` or `INIT`).
#[inline]
pub const fn is_full(&self) -> bool {
self.0 >= usize::MAX - 1
}
}

/// Helper trait to access storage layout information without requiring const generic parameter.
Expand Down
Loading
Loading