diff --git a/stylus-sdk/Cargo.toml b/stylus-sdk/Cargo.toml index eecd7ed8..6a0923e3 100644 --- a/stylus-sdk/Cargo.toml +++ b/stylus-sdk/Cargo.toml @@ -17,10 +17,12 @@ derivative.workspace = true hex = { workspace = true, default-features = false, features = ["alloc"] } keccak-const.workspace = true lazy_static.workspace = true + +# export-abi regex = { workspace = true, optional = true } -# data structures -fnv.workspace = true +# storage-cache +fnv = { workspace = true, optional = true } # local deps stylus-proc.workspace = true @@ -30,5 +32,7 @@ paste.workspace = true sha3.workspace = true [features] +default = ["storage-cache"] export-abi = ["debug", "regex", "stylus-proc/export-abi"] debug = [] +storage-cache = ["fnv"] diff --git a/stylus-sdk/src/call/mod.rs b/stylus-sdk/src/call/mod.rs index 6d1d7803..58646044 100644 --- a/stylus-sdk/src/call/mod.rs +++ b/stylus-sdk/src/call/mod.rs @@ -1,12 +1,17 @@ // Copyright 2022-2023, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md -use crate::storage::{StorageCache, TopLevelStorage}; +use crate::storage::TopLevelStorage; use alloy_primitives::{Address, U256}; use core::sync::atomic::{AtomicBool, Ordering}; pub use self::{context::Context, error::Error, raw::RawCall, traits::*}; +pub(crate) use raw::CachePolicy; + +#[cfg(feature = "storage-cache")] +use crate::storage::Storage; + mod context; mod error; mod raw; @@ -45,9 +50,10 @@ pub fn static_call( to: Address, data: &[u8], ) -> Result, Error> { - // flush storage to persist changes, but don't invalidate the cache + #[cfg(feature = "storage-cache")] if reentrancy_enabled() { - StorageCache::flush(); + // flush storage to persist changes, but don't invalidate the cache + Storage::flush(); } unsafe { RawCall::new_static() @@ -59,9 +65,10 @@ pub fn static_call( /// Calls the contract at the given address. pub fn call(context: impl MutatingCallContext, to: Address, data: &[u8]) -> Result, Error> { - // clear the storage to persist changes, invalidating the cache + #[cfg(feature = "storage-cache")] if reentrancy_enabled() { - StorageCache::clear(); + // clear the storage to persist changes, invalidating the cache + Storage::clear(); } unsafe { RawCall::new_with_value(context.value()) diff --git a/stylus-sdk/src/call/raw.rs b/stylus-sdk/src/call/raw.rs index 91865e61..2379b211 100644 --- a/stylus-sdk/src/call/raw.rs +++ b/stylus-sdk/src/call/raw.rs @@ -3,12 +3,13 @@ use crate::{ contract::{read_return_data, RETURN_DATA_LEN}, - hostio, - storage::StorageCache, - tx, ArbResult, + hostio, tx, ArbResult, }; use alloy_primitives::{Address, B256, U256}; +#[cfg(feature = "storage-cache")] +use crate::storage::StorageCache; + /// Mechanism for performing raw calls to other contracts. #[derive(Clone, Default)] #[must_use] @@ -18,6 +19,7 @@ pub struct RawCall { gas: Option, offset: usize, size: Option, + #[allow(unused)] cache_policy: CachePolicy, } @@ -30,8 +32,10 @@ enum CallKind { Static, } -#[derive(Clone, Default, PartialEq, PartialOrd)] -enum CachePolicy { +/// How to manage the storage cache, if enabled. +#[allow(unused)] +#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum CachePolicy { #[default] DoNothing, Flush, @@ -122,14 +126,14 @@ impl RawCall { } /// Write all cached values to persistent storage before the call. + #[cfg(feature = "storage-cache")] pub fn flush_storage_cache(mut self) -> Self { - if self.cache_policy < CachePolicy::Flush { - self.cache_policy = CachePolicy::Flush; - } + self.cache_policy = self.cache_policy.max(CachePolicy::Flush); self } /// Flush and clear the storage cache before the call. + #[cfg(feature = "storage-cache")] pub fn clear_storage_cache(mut self) -> Self { self.cache_policy = CachePolicy::Clear; self @@ -148,6 +152,7 @@ impl RawCall { let gas = self.gas.unwrap_or(u64::MAX); // will be clamped by 63/64 rule let value = B256::from(self.callvalue); let status = unsafe { + #[cfg(feature = "storage-cache")] match self.cache_policy { CachePolicy::Clear => StorageCache::clear(), CachePolicy::Flush => StorageCache::flush(), diff --git a/stylus-sdk/src/deploy/raw.rs b/stylus-sdk/src/deploy/raw.rs index fef0f14d..b43ea626 100644 --- a/stylus-sdk/src/deploy/raw.rs +++ b/stylus-sdk/src/deploy/raw.rs @@ -1,12 +1,15 @@ // Copyright 2023, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md -use alloy_primitives::{Address, B256, U256}; - use crate::{ + call::CachePolicy, contract::{read_return_data, RETURN_DATA_LEN}, hostio, }; +use alloy_primitives::{Address, B256, U256}; + +#[cfg(feature = "storage-cache")] +use crate::storage::StorageCache; /// Mechanism for performing raw deploys of other contracts. #[derive(Clone, Default)] @@ -15,6 +18,8 @@ pub struct RawDeploy { salt: Option, offset: usize, size: Option, + #[allow(unused)] + cache_policy: CachePolicy, } impl RawDeploy { @@ -55,6 +60,20 @@ impl RawDeploy { self.limit_revert_data(0, 0) } + /// Write all cached values to persistent storage before the init code. + #[cfg(feature = "storage-cache")] + pub fn flush_storage_cache(mut self) -> Self { + self.cache_policy = self.cache_policy.max(CachePolicy::Flush); + self + } + + /// Flush and clear the storage cache before the init code. + #[cfg(feature = "storage-cache")] + pub fn clear_storage_cache(mut self) -> Self { + self.cache_policy = CachePolicy::Clear; + self + } + /// Performs a raw deploy of another contract with the given `endowment` and init `code`. /// Returns the address of the newly deployed contract, or the error data in case of failure. /// @@ -67,6 +86,13 @@ impl RawDeploy { /// For extra flexibility, this method does not clear the global storage cache. /// See [`StorageCache::flush`] and [`StorageCache::clear`] for more information. pub unsafe fn deploy(self, code: &[u8], endowment: U256) -> Result> { + #[cfg(feature = "storage-cache")] + match self.cache_policy { + CachePolicy::Clear => StorageCache::clear(), + CachePolicy::Flush => StorageCache::flush(), + CachePolicy::DoNothing => {} + } + let mut contract = Address::default(); let mut revert_data_len = 0; diff --git a/stylus-sdk/src/storage/bytes.rs b/stylus-sdk/src/storage/bytes.rs index 7d67aa65..caafaca2 100644 --- a/stylus-sdk/src/storage/bytes.rs +++ b/stylus-sdk/src/storage/bytes.rs @@ -1,7 +1,7 @@ // Copyright 2022-2023, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md -use super::{Erase, StorageB8, StorageCache, StorageGuard, StorageGuardMut, StorageType}; +use super::{Erase, GlobalStorage, Storage, StorageB8, StorageGuard, StorageGuardMut, StorageType}; use crate::crypto; use alloy_primitives::{U256, U8}; use core::cell::OnceCell; @@ -41,7 +41,7 @@ impl StorageBytes { /// Gets the number of bytes stored. pub fn len(&self) -> usize { - let word = StorageCache::get_word(self.root); + let word = Storage::get_word(self.root); // check if the data is short let slot: &[u8] = word.as_ref(); @@ -70,15 +70,15 @@ impl StorageBytes { // if shrinking, pull data in if (len < 32) && (old > 32) { - let word = StorageCache::get_word(*self.base()); - StorageCache::set_word(self.root, word); + let word = Storage::get_word(*self.base()); + Storage::set_word(self.root, word); return self.write_len(len); } // if growing, push data out - let mut word = StorageCache::get_word(self.root); + let mut word = Storage::get_word(self.root); word[31] = 0; // clear len byte - StorageCache::set_word(*self.base(), word); + Storage::set_word(*self.base(), word); self.write_len(len) } @@ -86,10 +86,10 @@ impl StorageBytes { unsafe fn write_len(&mut self, len: usize) { if len < 32 { // place the len in the last byte of the root with the long bit low - StorageCache::set_uint(self.root, 31, U8::from(len * 2)); + Storage::set_uint(self.root, 31, U8::from(len * 2)); } else { // place the len in the root with the long bit high - StorageCache::set_word(self.root, U256::from(len * 2 + 1).into()) + Storage::set_word(self.root, U256::from(len * 2 + 1).into()) } } @@ -101,7 +101,7 @@ impl StorageBytes { macro_rules! assign { ($slot:expr) => { unsafe { - StorageCache::set_uint($slot, index % 32, value); // pack value + Storage::set_uint($slot, index % 32, value); // pack value self.write_len(index + 1); } }; @@ -114,8 +114,8 @@ impl StorageBytes { // convert to multi-word representation if index == 31 { // copy content over (len byte will be overwritten) - let word = StorageCache::get_word(self.root); - unsafe { StorageCache::set_word(*self.base(), word) }; + let word = Storage::get_word(self.root); + unsafe { Storage::set_word(*self.base(), word) }; } let slot = self.base() + U256::from(index / 32); @@ -135,13 +135,13 @@ impl StorageBytes { let clean = index % 32 == 0; let byte = self.get(index)?; - let clear = |slot| unsafe { StorageCache::clear_word(slot) }; + let clear = |slot| unsafe { Storage::clear_word(slot) }; // convert to single-word representation if len == 32 { // copy content over - let word = StorageCache::get_word(*self.base()); - unsafe { StorageCache::set_word(self.root, word) }; + let word = Storage::get_word(*self.base()); + unsafe { Storage::set_word(self.root, word) }; clear(*self.base()); } @@ -152,7 +152,7 @@ impl StorageBytes { // clear the value if len < 32 { - unsafe { StorageCache::set_byte(self.root, index, 0) }; + unsafe { Storage::set_byte(self.root, index, 0) }; } // set the new length @@ -187,7 +187,7 @@ impl StorageBytes { /// UB if index is out of bounds. pub unsafe fn get_unchecked(&self, index: usize) -> u8 { let (slot, offset) = self.index_slot(index); - unsafe { StorageCache::get_byte(slot, offset.into()) } + unsafe { Storage::get_byte(slot, offset.into()) } } /// Gets the full contents of the collection. @@ -231,11 +231,11 @@ impl Erase for StorageBytes { if len > 31 { while len > 0 { let slot = self.index_slot(len as usize - 1).0; - unsafe { StorageCache::clear_word(slot) }; + unsafe { Storage::clear_word(slot) }; len -= 32; } } - unsafe { StorageCache::clear_word(self.root) }; + unsafe { Storage::clear_word(self.root) }; } } diff --git a/stylus-sdk/src/storage/cache.rs b/stylus-sdk/src/storage/cache.rs index 339d6cd2..63ea2efa 100644 --- a/stylus-sdk/src/storage/cache.rs +++ b/stylus-sdk/src/storage/cache.rs @@ -1,19 +1,18 @@ // Copyright 2022-2023, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md -use super::{load_bytes32, store_bytes32}; -use alloy_primitives::{FixedBytes, Signed, Uint, B256, U256}; -use core::{ - cell::UnsafeCell, - marker::PhantomData, - ops::{Deref, DerefMut}, - ptr, -}; -use derivative::Derivative; +use super::{load_bytes32, store_bytes32, traits::GlobalStorage}; +use alloy_primitives::{B256, U256}; +use core::cell::UnsafeCell; use fnv::FnvHashMap as HashMap; use lazy_static::lazy_static; /// Global cache managing persistent storage operations. +/// +/// This is intended for most use cases. However, one may opt-out +/// of this behavior by turning off default features and not enabling +/// the `storage-cache` feature. Doing so will provide the [`EagerStorage`] type +/// for managing state in the absence of caching. pub struct StorageCache(HashMap); /// Represents the EVM word at a given key. @@ -61,197 +60,20 @@ macro_rules! cache { }; } -impl StorageCache { - /// Retrieves `N ≤ 32` bytes from persistent storage, performing [`SLOAD`]'s only as needed. - /// The bytes are read from slot `key`, starting `offset` bytes from the left. - /// Note that the bytes must exist within a single, 32-byte EVM word. - /// - /// # Safety - /// - /// UB if the read would cross a word boundary. - /// May become safe when Rust stabilizes [`generic_const_exprs`]. - /// - /// [`SLOAD`]: https://www.evm.codes/#54 - /// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560 - pub unsafe fn get(key: U256, offset: usize) -> FixedBytes { - debug_assert!(N + offset <= 32); - let word = Self::get_word(key); - let value = &word[offset..][..N]; - FixedBytes::from_slice(value) - } - - /// Retrieves a [`Uint`] from persistent storage, performing [`SLOAD`]'s only as needed. - /// The integer's bytes are read from slot `key`, starting `offset` bytes from the left. - /// Note that the bytes must exist within a single, 32-byte EVM word. - /// - /// # Safety - /// - /// UB if the read would cross a word boundary. - /// May become safe when Rust stabilizes [`generic_const_exprs`]. - /// - /// [`SLOAD`]: https://www.evm.codes/#54 - /// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560 - pub unsafe fn get_uint(key: U256, offset: usize) -> Uint { - debug_assert!(B / 8 + offset <= 32); - let word = Self::get_word(key); - let value = &word[offset..][..B / 8]; - Uint::try_from_be_slice(value).unwrap() - } - - /// Retrieves a [`u8`] from persistent storage, performing [`SLOAD`]'s only as needed. - /// The byte is read from slot `key`, starting `offset` bytes from the left. - /// - /// # Safety - /// - /// UB if the read is out of bounds. - /// May become safe when Rust stabilizes [`generic_const_exprs`]. - /// - /// [`SLOAD`]: https://www.evm.codes/#54 - /// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560 - pub unsafe fn get_byte(key: U256, offset: usize) -> u8 { - debug_assert!(offset <= 32); - let word = Self::get::<1>(key, offset); - word[0] - } - - /// Retrieves a [`Signed`] from persistent storage, performing [`SLOAD`]'s only as needed. - /// The integer's bytes are read from slot `key`, starting `offset` bytes from the left. - /// Note that the bytes must exist within a single, 32-byte EVM word. - /// - /// # Safety - /// - /// UB if the read would cross a word boundary. - /// May become safe when Rust stabilizes [`generic_const_exprs`]. - /// - /// [`SLOAD`]: https://www.evm.codes/#54 - /// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560 - pub unsafe fn get_signed( - key: U256, - offset: usize, - ) -> Signed { - Signed::from_raw(Self::get_uint(key, offset)) - } - - /// Retrieves a 32-byte EVM word from persistent storage, performing [`SLOAD`]'s only as needed. - /// - /// [`SLOAD`]: https://www.evm.codes/#54 - pub fn get_word(key: U256) -> B256 { +impl GlobalStorage for StorageCache { + fn get_word(key: U256) -> B256 { cache!() .entry(key) .or_insert_with(|| unsafe { StorageWord::new_known(load_bytes32(key)) }) .value } - /// Writes `N ≤ 32` bytes to persistent storage, performing [`SSTORE`]'s only as needed. - /// The bytes are written to slot `key`, starting `offset` bytes from the left. - /// Note that the bytes must be written to a single, 32-byte EVM word. - /// - /// # Safety - /// - /// UB if the write would cross a word boundary. - /// Aliases if called during the lifetime an overlapping accessor. - /// - /// [`SSTORE`]: https://www.evm.codes/#55 - pub unsafe fn set(key: U256, offset: usize, value: FixedBytes) { - debug_assert!(N + offset <= 32); - - if N == 32 { - return Self::set_word(key, FixedBytes::from_slice(value.as_slice())); - } - - let word = cache!() - .entry(key) - .or_insert_with(|| StorageWord::new_known(load_bytes32(key))); - - let dest = word.value[offset..].as_mut_ptr(); - ptr::copy(value.as_ptr(), dest, N) - } - - /// Writes a [`Uint`] to persistent storage, performing [`SSTORE`]'s only as needed. - /// The integer's bytes are written to slot `key`, starting `offset` bytes from the left. - /// Note that the bytes must be written to a single, 32-byte EVM word. - /// - /// # Safety - /// - /// UB if the write would cross a word boundary. - /// Aliases if called during the lifetime an overlapping accessor. - /// - /// [`SSTORE`]: https://www.evm.codes/#55 - pub unsafe fn set_uint( - key: U256, - offset: usize, - value: Uint, - ) { - debug_assert!(B / 8 + offset <= 32); - - if B == 256 { - return Self::set_word(key, FixedBytes::from_slice(&value.to_be_bytes::<32>())); - } - - let cache = &mut cache!(); - let word = cache - .entry(key) - .or_insert_with(|| StorageWord::new_known(load_bytes32(key))); - - let value = value.to_be_bytes_vec(); - let dest = word.value[offset..].as_mut_ptr(); - ptr::copy(value.as_ptr(), dest, B / 8) - } - - /// Writes a [`Signed`] to persistent storage, performing [`SSTORE`]'s only as needed. - /// The bytes are written to slot `key`, starting `offset` bytes from the left. - /// Note that the bytes must be written to a single, 32-byte EVM word. - /// - /// # Safety - /// - /// UB if the write would cross a word boundary. - /// Aliases if called during the lifetime an overlapping accessor. - /// - /// [`SSTORE`]: https://www.evm.codes/#55 - pub unsafe fn set_signed( - key: U256, - offset: usize, - value: Signed, - ) { - Self::set_uint(key, offset, value.into_raw()) - } - - /// Writes a [`u8`] to persistent storage, performing [`SSTORE`]'s only as needed. - /// The byte is written to slot `key`, starting `offset` bytes from the left. - /// - /// # Safety - /// - /// UB if the write is out of bounds. - /// Aliases if called during the lifetime an overlapping accessor. - /// - /// [`SSTORE`]: https://www.evm.codes/#55 - pub unsafe fn set_byte(key: U256, offset: usize, value: u8) { - let fixed = FixedBytes::from_slice(&[value]); - StorageCache::set::<1>(key, offset, fixed) - } - - /// Stores a 32-byte EVM word to persistent storage, performing [`SSTORE`]'s only as needed. - /// - /// # Safety - /// - /// Aliases if called during the lifetime an overlapping accessor. - /// - /// [`SSTORE`]: https://www.evm.codes/#55 - pub unsafe fn set_word(key: U256, value: B256) { + unsafe fn set_word(key: U256, value: B256) { cache!().insert(key, StorageWord::new_unknown(value)); } +} - /// Clears the 32-byte word at the given key, performing [`SSTORE`]'s only as needed. - /// - /// # Safety - /// - /// Aliases if called during the lifetime an overlapping accessor. - /// - /// [`SSTORE`]: https://www.evm.codes/#55 - pub unsafe fn clear_word(key: U256) { - Self::set_word(key, B256::ZERO) - } - +impl StorageCache { /// Write all cached values to persistent storage. /// Note: this operation retains [`SLOAD`] information for optimization purposes. /// If reentrancy is possible, use [`StorageCache::clear`]. @@ -271,167 +93,3 @@ impl StorageCache { cache!().clear(); } } - -/// Accessor trait that lets a type be used in persistent storage. -/// Users can implement this trait to add novel data structures to their contract definitions. -/// The Stylus SDK by default provides only solidity types, which are represented [`the same way`]. -/// -/// [`the same way`]: https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html -pub trait StorageType: Sized { - /// For primative types, this is the type being stored. - /// For collections, this is the [`StorageType`] being collected. - type Wraps<'a>: 'a - where - Self: 'a; - - /// Mutable accessor to the type being stored. - type WrapsMut<'a>: 'a - where - Self: 'a; - - /// The number of bytes in a slot needed to represent the type. Must not exceed 32. - /// For types larger than 32 bytes that are stored inline with a struct's fields, - /// set this to 32 and return the full size in [`StorageType::new`]. - /// - /// For implementing collections, see how Solidity slots are assigned for [`Arrays and Maps`] and their - /// Stylus equivalents [`StorageVec`] and [`StorageMap`]. - /// For multi-word, but still-fixed-size types, see the implementations for structs and [`StorageArray`]. - /// - /// [`Arrays and Maps`]: https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html#mappings-and-dynamic-arrays - const SLOT_BYTES: usize = 32; - - /// The number of words this type must fill. For primitives this is always 0. - /// For complex types requiring more than one inline word, set this to the total size. - const REQUIRED_SLOTS: usize = 0; - - /// Where in persistent storage the type should live. Although useful for framework designers - /// creating new storage types, most user programs shouldn't call this. - /// Note: implementations will have to be `const` once [`generic_const_exprs`] stabilizes. - /// - /// # Safety - /// - /// Aliases storage if two calls to the same slot and offset occur within the same lifetime. - /// - /// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560 - unsafe fn new(slot: U256, offset: u8) -> Self; - - /// Load the wrapped type, consuming the accessor. - /// Note: most types have a `get` and/or `getter`, which don't consume `Self`. - fn load<'s>(self) -> Self::Wraps<'s> - where - Self: 's; - - /// Load the wrapped mutable type, consuming the accessor. - /// Note: most types have a `set` and/or `setter`, which don't consume `Self`. - fn load_mut<'s>(self) -> Self::WrapsMut<'s> - where - Self: 's; -} - -/// Trait for accessors that can be used to completely erase their underlying value. -/// Note that some collections, like [`StorageMap`], don't implement this trait. -pub trait Erase: StorageType { - /// Erase the value from persistent storage. - fn erase(&mut self); -} - -/// Trait for simple accessors that store no more than their wrapped value. -/// The type's representation must be entirely inline, or storage leaks become possible. -/// Note: it is a logic error if erasure does anything more than writing the zero-value. -pub trait SimpleStorageType<'a>: StorageType + Erase + Into> -where - Self: 'a, -{ - /// Write the value to persistent storage. - fn set_by_wrapped(&mut self, value: Self::Wraps<'a>); -} - -/// Trait for top-level storage types, usually implemented by proc macros. -/// Top-level types are special in that their lifetimes track the entirety -/// of all the EVM state-changes throughout a contract invocation. -/// -/// To prevent storage aliasing during reentrancy, you must hold a reference -/// to such a type when making an EVM call. This may change in the future -/// for programs that prevent reentrancy. -/// -/// # Safety -/// -/// The type must be top-level to prevent storage aliasing. -pub unsafe trait TopLevelStorage {} - -/// Binds a storage accessor to a lifetime to prevent aliasing. -/// Because this type doesn't implement `DerefMut`, mutable methods on the accessor aren't available. -/// For a mutable accessor, see [`StorageGuardMut`]. -#[derive(Derivative)] -#[derivative(Debug = "transparent")] -pub struct StorageGuard<'a, T: 'a> { - inner: T, - #[derivative(Debug = "ignore")] - marker: PhantomData<&'a T>, -} - -impl<'a, T: 'a> StorageGuard<'a, T> { - /// Creates a new storage guard around an arbitrary type. - pub fn new(inner: T) -> Self { - Self { - inner, - marker: PhantomData, - } - } - - /// Get the underlying `T` directly, bypassing the borrow checker. - /// - /// # Safety - /// - /// Enables storage aliasing. - pub unsafe fn into_raw(self) -> T { - self.inner - } -} - -impl<'a, T: 'a> Deref for StorageGuard<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -/// Binds a storage accessor to a lifetime to prevent aliasing. -pub struct StorageGuardMut<'a, T: 'a> { - inner: T, - marker: PhantomData<&'a T>, -} - -impl<'a, T: 'a> StorageGuardMut<'a, T> { - /// Creates a new storage guard around an arbitrary type. - pub fn new(inner: T) -> Self { - Self { - inner, - marker: PhantomData, - } - } - - /// Get the underlying `T` directly, bypassing the borrow checker. - /// - /// # Safety - /// - /// Enables storage aliasing. - pub unsafe fn into_raw(self) -> T { - self.inner - } -} - -impl<'a, T: 'a> Deref for StorageGuardMut<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl<'a, T: 'a> DerefMut for StorageGuardMut<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} diff --git a/stylus-sdk/src/storage/eager.rs b/stylus-sdk/src/storage/eager.rs new file mode 100644 index 00000000..72364e38 --- /dev/null +++ b/stylus-sdk/src/storage/eager.rs @@ -0,0 +1,23 @@ +// Copyright 2022-2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md + +use super::{load_bytes32, store_bytes32, traits::GlobalStorage}; +use alloy_primitives::{B256, U256}; + +/// Global accessor to persistent storage that doesn't use caching. +/// +/// To instead use storage-caching optimizations, recompile with the +/// `storage-cache` feature flag, which will provide the [`StorageCache`] type. +/// +/// Note that individual primitive types may still include efficient caching. +pub struct EagerStorage; + +impl GlobalStorage for EagerStorage { + fn get_word(key: U256) -> B256 { + unsafe { load_bytes32(key) } + } + + unsafe fn set_word(key: U256, value: B256) { + store_bytes32(key, value); + } +} diff --git a/stylus-sdk/src/storage/map.rs b/stylus-sdk/src/storage/map.rs index ce077742..76424ba8 100644 --- a/stylus-sdk/src/storage/map.rs +++ b/stylus-sdk/src/storage/map.rs @@ -3,7 +3,7 @@ use crate::crypto; -use super::{cache::Erase, SimpleStorageType, StorageGuard, StorageGuardMut, StorageType}; +use super::{Erase, SimpleStorageType, StorageGuard, StorageGuardMut, StorageType}; use alloy_primitives::{Address, FixedBytes, Signed, Uint, B256, U160, U256}; use core::marker::PhantomData; diff --git a/stylus-sdk/src/storage/mod.rs b/stylus-sdk/src/storage/mod.rs index fd904629..c0230d9b 100644 --- a/stylus-sdk/src/storage/mod.rs +++ b/stylus-sdk/src/storage/mod.rs @@ -7,18 +7,36 @@ use alloy_sol_types::sol_data::{ByteCount, SupportedFixedBytes}; use core::{cell::OnceCell, marker::PhantomData, ops::Deref}; pub use bytes::{StorageBytes, StorageString}; -pub use cache::{ - Erase, SimpleStorageType, StorageCache, StorageGuard, StorageGuardMut, StorageType, +pub use map::StorageMap; +pub use traits::{ + Erase, GlobalStorage, SimpleStorageType, StorageGuard, StorageGuardMut, StorageType, TopLevelStorage, }; -pub use map::StorageMap; pub use vec::StorageVec; +#[cfg(feature = "storage-cache")] +pub use cache::StorageCache; + +#[cfg(not(feature = "storage-cache"))] +pub use eager::EagerStorage; + mod bytes; -mod cache; mod map; +mod traits; mod vec; +#[cfg(feature = "storage-cache")] +mod cache; + +#[cfg(not(feature = "storage-cache"))] +mod eager; + +#[cfg(feature = "storage-cache")] +pub(crate) type Storage = StorageCache; + +#[cfg(not(feature = "storage-cache"))] +pub(crate) type Storage = EagerStorage; + /// Retrieves a 32-byte EVM word from persistent storage directly, bypassing all caches. /// /// # Safety @@ -113,7 +131,7 @@ impl StorageUint { /// Sets the underlying [`Uint`] in persistent storage. pub fn set(&mut self, value: Uint) { overwrite_cell(&mut self.cached, value); - unsafe { StorageCache::set_uint(self.slot, self.offset.into(), value) }; + unsafe { Storage::set_uint(self.slot, self.offset.into(), value) }; } } @@ -158,7 +176,7 @@ impl Deref for StorageUint { fn deref(&self) -> &Self::Target { self.cached - .get_or_init(|| unsafe { StorageCache::get_uint(self.slot, self.offset.into()) }) + .get_or_init(|| unsafe { Storage::get_uint(self.slot, self.offset.into()) }) } } @@ -187,7 +205,7 @@ impl StorageSigned { /// Gets the underlying [`Signed`] in persistent storage. pub fn set(&mut self, value: Signed) { overwrite_cell(&mut self.cached, value); - unsafe { StorageCache::set_signed(self.slot, self.offset.into(), value) }; + unsafe { Storage::set_signed(self.slot, self.offset.into(), value) }; } } @@ -231,7 +249,7 @@ impl Deref for StorageSigned { fn deref(&self) -> &Self::Target { self.cached - .get_or_init(|| unsafe { StorageCache::get_signed(self.slot, self.offset.into()) }) + .get_or_init(|| unsafe { Storage::get_signed(self.slot, self.offset.into()) }) } } @@ -258,7 +276,7 @@ impl StorageFixedBytes { /// Gets the underlying [`FixedBytes`] in persistent storage. pub fn set(&mut self, value: FixedBytes) { overwrite_cell(&mut self.cached, value); - unsafe { StorageCache::set(self.slot, self.offset.into(), value) } + unsafe { Storage::set(self.slot, self.offset.into(), value) } } } @@ -311,7 +329,7 @@ impl Deref for StorageFixedBytes { fn deref(&self) -> &Self::Target { self.cached - .get_or_init(|| unsafe { StorageCache::get(self.slot, self.offset.into()) }) + .get_or_init(|| unsafe { Storage::get(self.slot, self.offset.into()) }) } } @@ -338,7 +356,7 @@ impl StorageBool { /// Gets the underlying [`bool`] in persistent storage. pub fn set(&mut self, value: bool) { overwrite_cell(&mut self.cached, value); - unsafe { StorageCache::set_byte(self.slot, self.offset.into(), value as u8) } + unsafe { Storage::set_byte(self.slot, self.offset.into(), value as u8) } } } @@ -382,7 +400,7 @@ impl Deref for StorageBool { fn deref(&self) -> &Self::Target { self.cached.get_or_init(|| unsafe { - let data = StorageCache::get_byte(self.slot, self.offset.into()); + let data = Storage::get_byte(self.slot, self.offset.into()); data != 0 }) } @@ -411,7 +429,7 @@ impl StorageAddress { /// Gets the underlying [`Address`] in persistent storage. pub fn set(&mut self, value: Address) { overwrite_cell(&mut self.cached, value); - unsafe { StorageCache::set::<20>(self.slot, self.offset.into(), value.into()) } + unsafe { Storage::set::<20>(self.slot, self.offset.into(), value.into()) } } } @@ -454,9 +472,8 @@ impl Deref for StorageAddress { type Target = Address; fn deref(&self) -> &Self::Target { - self.cached.get_or_init(|| unsafe { - StorageCache::get::<20>(self.slot, self.offset.into()).into() - }) + self.cached + .get_or_init(|| unsafe { Storage::get::<20>(self.slot, self.offset.into()).into() }) } } @@ -484,7 +501,7 @@ impl StorageBlockNumber { pub fn set(&mut self, value: BlockNumber) { overwrite_cell(&mut self.cached, value); let value = FixedBytes::from(value.to_be_bytes()); - unsafe { StorageCache::set::<8>(self.slot, self.offset.into(), value) }; + unsafe { Storage::set::<8>(self.slot, self.offset.into(), value) }; } } @@ -528,7 +545,7 @@ impl Deref for StorageBlockNumber { fn deref(&self) -> &Self::Target { self.cached.get_or_init(|| unsafe { - let data = StorageCache::get::<8>(self.slot, self.offset.into()); + let data = Storage::get::<8>(self.slot, self.offset.into()); u64::from_be_bytes(data.0) }) } @@ -556,7 +573,7 @@ impl StorageBlockHash { /// Sets the underlying [`BlockHash`] in persistent storage. pub fn set(&mut self, value: BlockHash) { overwrite_cell(&mut self.cached, value); - unsafe { StorageCache::set_word(self.slot, value) } + unsafe { Storage::set_word(self.slot, value) } } } @@ -594,8 +611,7 @@ impl Deref for StorageBlockHash { type Target = BlockHash; fn deref(&self) -> &Self::Target { - self.cached - .get_or_init(|| StorageCache::get_word(self.slot)) + self.cached.get_or_init(|| Storage::get_word(self.slot)) } } diff --git a/stylus-sdk/src/storage/traits.rs b/stylus-sdk/src/storage/traits.rs new file mode 100644 index 00000000..53936c74 --- /dev/null +++ b/stylus-sdk/src/storage/traits.rs @@ -0,0 +1,364 @@ +// Copyright 2022-2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md + +use alloy_primitives::{FixedBytes, Signed, Uint, B256, U256}; +use core::{ + marker::PhantomData, + ops::{Deref, DerefMut}, + ptr, +}; +use derivative::Derivative; + +/// Accessor trait that lets a type be used in persistent storage. +/// Users can implement this trait to add novel data structures to their contract definitions. +/// The Stylus SDK by default provides only solidity types, which are represented [`the same way`]. +/// +/// [`the same way`]: https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html +pub trait StorageType: Sized { + /// For primative types, this is the type being stored. + /// For collections, this is the [`StorageType`] being collected. + type Wraps<'a>: 'a + where + Self: 'a; + + /// Mutable accessor to the type being stored. + type WrapsMut<'a>: 'a + where + Self: 'a; + + /// The number of bytes in a slot needed to represent the type. Must not exceed 32. + /// For types larger than 32 bytes that are stored inline with a struct's fields, + /// set this to 32 and return the full size in [`StorageType::new`]. + /// + /// For implementing collections, see how Solidity slots are assigned for [`Arrays and Maps`] and their + /// Stylus equivalents [`StorageVec`] and [`StorageMap`]. + /// For multi-word, but still-fixed-size types, see the implementations for structs and [`StorageArray`]. + /// + /// [`Arrays and Maps`]: https://docs.soliditylang.org/en/v0.8.15/internals/layout_in_storage.html#mappings-and-dynamic-arrays + const SLOT_BYTES: usize = 32; + + /// The number of words this type must fill. For primitives this is always 0. + /// For complex types requiring more than one inline word, set this to the total size. + const REQUIRED_SLOTS: usize = 0; + + /// Where in persistent storage the type should live. Although useful for framework designers + /// creating new storage types, most user programs shouldn't call this. + /// Note: implementations will have to be `const` once [`generic_const_exprs`] stabilizes. + /// + /// # Safety + /// + /// Aliases storage if two calls to the same slot and offset occur within the same lifetime. + /// + /// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560 + unsafe fn new(slot: U256, offset: u8) -> Self; + + /// Load the wrapped type, consuming the accessor. + /// Note: most types have a `get` and/or `getter`, which don't consume `Self`. + fn load<'s>(self) -> Self::Wraps<'s> + where + Self: 's; + + /// Load the wrapped mutable type, consuming the accessor. + /// Note: most types have a `set` and/or `setter`, which don't consume `Self`. + fn load_mut<'s>(self) -> Self::WrapsMut<'s> + where + Self: 's; +} + +/// Trait for accessors that can be used to completely erase their underlying value. +/// Note that some collections, like [`StorageMap`], don't implement this trait. +pub trait Erase: StorageType { + /// Erase the value from persistent storage. + fn erase(&mut self); +} + +/// Trait for simple accessors that store no more than their wrapped value. +/// The type's representation must be entirely inline, or storage leaks become possible. +/// Note: it is a logic error if erasure does anything more than writing the zero-value. +pub trait SimpleStorageType<'a>: StorageType + Erase + Into> +where + Self: 'a, +{ + /// Write the value to persistent storage. + fn set_by_wrapped(&mut self, value: Self::Wraps<'a>); +} + +/// Trait for top-level storage types, usually implemented by proc macros. +/// Top-level types are special in that their lifetimes track the entirety +/// of all the EVM state-changes throughout a contract invocation. +/// +/// To prevent storage aliasing during reentrancy, you must hold a reference +/// to such a type when making an EVM call. This may change in the future +/// for programs that prevent reentrancy. +/// +/// # Safety +/// +/// The type must be top-level to prevent storage aliasing. +pub unsafe trait TopLevelStorage {} + +/// Binds a storage accessor to a lifetime to prevent aliasing. +/// Because this type doesn't implement `DerefMut`, mutable methods on the accessor aren't available. +/// For a mutable accessor, see [`StorageGuardMut`]. +#[derive(Derivative)] +#[derivative(Debug = "transparent")] +pub struct StorageGuard<'a, T: 'a> { + inner: T, + #[derivative(Debug = "ignore")] + marker: PhantomData<&'a T>, +} + +impl<'a, T: 'a> StorageGuard<'a, T> { + /// Creates a new storage guard around an arbitrary type. + pub fn new(inner: T) -> Self { + Self { + inner, + marker: PhantomData, + } + } + + /// Get the underlying `T` directly, bypassing the borrow checker. + /// + /// # Safety + /// + /// Enables storage aliasing. + pub unsafe fn into_raw(self) -> T { + self.inner + } +} + +impl<'a, T: 'a> Deref for StorageGuard<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +/// Binds a storage accessor to a lifetime to prevent aliasing. +pub struct StorageGuardMut<'a, T: 'a> { + inner: T, + marker: PhantomData<&'a T>, +} + +impl<'a, T: 'a> StorageGuardMut<'a, T> { + /// Creates a new storage guard around an arbitrary type. + pub fn new(inner: T) -> Self { + Self { + inner, + marker: PhantomData, + } + } + + /// Get the underlying `T` directly, bypassing the borrow checker. + /// + /// # Safety + /// + /// Enables storage aliasing. + pub unsafe fn into_raw(self) -> T { + self.inner + } +} + +impl<'a, T: 'a> Deref for StorageGuardMut<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl<'a, T: 'a> DerefMut for StorageGuardMut<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +/// Trait for managing access to persistent storage. +/// Notable implementations include the [`StorageCache`] and [`EagerStorage`] types. +pub trait GlobalStorage { + /// Retrieves `N ≤ 32` bytes from persistent storage, performing [`SLOAD`]'s only as needed. + /// The bytes are read from slot `key`, starting `offset` bytes from the left. + /// Note that the bytes must exist within a single, 32-byte EVM word. + /// + /// # Safety + /// + /// UB if the read would cross a word boundary. + /// May become safe when Rust stabilizes [`generic_const_exprs`]. + /// + /// [`SLOAD`]: https://www.evm.codes/#54 + /// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560 + unsafe fn get(key: U256, offset: usize) -> FixedBytes { + debug_assert!(N + offset <= 32); + let word = Self::get_word(key); + let value = &word[offset..][..N]; + FixedBytes::from_slice(value) + } + + /// Retrieves a [`Uint`] from persistent storage, performing [`SLOAD`]'s only as needed. + /// The integer's bytes are read from slot `key`, starting `offset` bytes from the left. + /// Note that the bytes must exist within a single, 32-byte EVM word. + /// + /// # Safety + /// + /// UB if the read would cross a word boundary. + /// May become safe when Rust stabilizes [`generic_const_exprs`]. + /// + /// [`SLOAD`]: https://www.evm.codes/#54 + /// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560 + unsafe fn get_uint(key: U256, offset: usize) -> Uint { + debug_assert!(B / 8 + offset <= 32); + let word = Self::get_word(key); + let value = &word[offset..][..B / 8]; + Uint::try_from_be_slice(value).unwrap() + } + + /// Retrieves a [`Signed`] from persistent storage, performing [`SLOAD`]'s only as needed. + /// The integer's bytes are read from slot `key`, starting `offset` bytes from the left. + /// Note that the bytes must exist within a single, 32-byte EVM word. + /// + /// # Safety + /// + /// UB if the read would cross a word boundary. + /// May become safe when Rust stabilizes [`generic_const_exprs`]. + /// + /// [`SLOAD`]: https://www.evm.codes/#54 + /// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560 + unsafe fn get_signed(key: U256, offset: usize) -> Signed { + Signed::from_raw(Self::get_uint(key, offset)) + } + + /// Retrieves a [`u8`] from persistent storage, performing [`SLOAD`]'s only as needed. + /// The byte is read from slot `key`, starting `offset` bytes from the left. + /// + /// # Safety + /// + /// UB if the read is out of bounds. + /// May become safe when Rust stabilizes [`generic_const_exprs`]. + /// + /// [`SLOAD`]: https://www.evm.codes/#54 + /// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560 + unsafe fn get_byte(key: U256, offset: usize) -> u8 { + debug_assert!(offset <= 32); + let word = Self::get::<1>(key, offset); + word[0] + } + + /// Retrieves a [`Signed`] from persistent storage, performing [`SLOAD`]'s only as needed. + /// The integer's bytes are read from slot `key`, starting `offset` bytes from the left. + /// Note that the bytes must exist within a single, 32-byte EVM word. + /// + /// # Safety + /// + /// UB if the read would cross a word boundary. + /// May become safe when Rust stabilizes [`generic_const_exprs`]. + /// + /// [`SLOAD`]: https://www.evm.codes/#54 + /// [`generic_const_exprs`]: https://github.com/rust-lang/rust/issues/76560 + fn get_word(key: U256) -> B256; + + /// Writes `N ≤ 32` bytes to persistent storage, performing [`SSTORE`]'s only as needed. + /// The bytes are written to slot `key`, starting `offset` bytes from the left. + /// Note that the bytes must be written to a single, 32-byte EVM word. + /// + /// # Safety + /// + /// UB if the write would cross a word boundary. + /// Aliases if called during the lifetime an overlapping accessor. + /// + /// [`SSTORE`]: https://www.evm.codes/#55 + unsafe fn set(key: U256, offset: usize, value: FixedBytes) { + debug_assert!(N + offset <= 32); + + if N == 32 { + return Self::set_word(key, FixedBytes::from_slice(value.as_slice())); + } + + let mut word = Self::get_word(key); + + let dest = word[offset..].as_mut_ptr(); + ptr::copy(value.as_ptr(), dest, N); + + Self::set_word(key, word); + } + + /// Writes a [`Uint`] to persistent storage, performing [`SSTORE`]'s only as needed. + /// The integer's bytes are written to slot `key`, starting `offset` bytes from the left. + /// Note that the bytes must be written to a single, 32-byte EVM word. + /// + /// # Safety + /// + /// UB if the write would cross a word boundary. + /// Aliases if called during the lifetime an overlapping accessor. + /// + /// [`SSTORE`]: https://www.evm.codes/#55 + unsafe fn set_uint( + key: U256, + offset: usize, + value: Uint, + ) { + debug_assert!(B / 8 + offset <= 32); + + if B == 256 { + return Self::set_word(key, FixedBytes::from_slice(&value.to_be_bytes::<32>())); + } + + let mut word = Self::get_word(key); + + let value = value.to_be_bytes_vec(); + let dest = word[offset..].as_mut_ptr(); + ptr::copy(value.as_ptr(), dest, B / 8); + Self::set_word(key, word); + } + + /// Writes a [`Signed`] to persistent storage, performing [`SSTORE`]'s only as needed. + /// The bytes are written to slot `key`, starting `offset` bytes from the left. + /// Note that the bytes must be written to a single, 32-byte EVM word. + /// + /// # Safety + /// + /// UB if the write would cross a word boundary. + /// Aliases if called during the lifetime an overlapping accessor. + /// + /// [`SSTORE`]: https://www.evm.codes/#55 + unsafe fn set_signed( + key: U256, + offset: usize, + value: Signed, + ) { + Self::set_uint(key, offset, value.into_raw()) + } + + /// Writes a [`u8`] to persistent storage, performing [`SSTORE`]'s only as needed. + /// The byte is written to slot `key`, starting `offset` bytes from the left. + /// + /// # Safety + /// + /// UB if the write is out of bounds. + /// Aliases if called during the lifetime an overlapping accessor. + /// + /// [`SSTORE`]: https://www.evm.codes/#55 + unsafe fn set_byte(key: U256, offset: usize, value: u8) { + let fixed = FixedBytes::from_slice(&[value]); + Self::set::<1>(key, offset, fixed) + } + + /// Stores a 32-byte EVM word to persistent storage, performing [`SSTORE`]'s only as needed. + /// + /// # Safety + /// + /// Aliases if called during the lifetime an overlapping accessor. + /// + /// [`SSTORE`]: https://www.evm.codes/#55 + unsafe fn set_word(key: U256, value: B256); + + /// Clears the 32-byte word at the given key, performing [`SSTORE`]'s only as needed. + /// + /// # Safety + /// + /// Aliases if called during the lifetime an overlapping accessor. + /// + /// [`SSTORE`]: https://www.evm.codes/#55 + unsafe fn clear_word(key: U256) { + Self::set_word(key, B256::ZERO) + } +} diff --git a/stylus-sdk/src/storage/vec.rs b/stylus-sdk/src/storage/vec.rs index 89b8377c..a5846443 100644 --- a/stylus-sdk/src/storage/vec.rs +++ b/stylus-sdk/src/storage/vec.rs @@ -1,7 +1,9 @@ // Copyright 2023, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md -use super::{Erase, SimpleStorageType, StorageCache, StorageGuard, StorageGuardMut, StorageType}; +use super::{ + Erase, GlobalStorage, SimpleStorageType, Storage, StorageGuard, StorageGuardMut, StorageType, +}; use crate::crypto; use alloy_primitives::U256; use core::{cell::OnceCell, marker::PhantomData}; @@ -43,7 +45,7 @@ impl StorageVec { /// Gets the number of elements stored. pub fn len(&self) -> usize { - let word: U256 = StorageCache::get_word(self.slot).into(); + let word: U256 = Storage::get_word(self.slot).into(); word.try_into().unwrap() } @@ -55,7 +57,7 @@ impl StorageVec { /// or any junk data left over from prior dirty operations. /// Note that [`StorageVec`] has unlimited capacity, so all lengths are valid. pub unsafe fn set_len(&mut self, len: usize) { - StorageCache::set_word(self.slot, U256::from(len).into()) + Storage::set_word(self.slot, U256::from(len).into()) } /// Gets an accessor to the element at a given index, if it exists. @@ -199,7 +201,7 @@ impl<'a, S: SimpleStorageType<'a>> StorageVec { let slot = self.index_slot(index).0; let words = S::REQUIRED_SLOTS.max(1); for i in 0..words { - unsafe { StorageCache::clear_word(slot + U256::from(i)) }; + unsafe { Storage::clear_word(slot + U256::from(i)) }; } } Some(value)