diff --git a/Cargo.lock b/Cargo.lock index 6d6fa839..56340f99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,12 +206,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "generic-array" version = "0.14.7" @@ -521,7 +515,6 @@ dependencies = [ "alloy-sol-types", "cfg-if 1.0.0", "derivative", - "fnv", "hex", "keccak-const", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index b38370b5..daa6dd5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,6 @@ keccak-const = "0.2.0" lazy_static = "1.4.0" sha3 = "0.10.8" -# data structures -fnv = "1.0.7" - # proc macros syn = { version = "1.0", features = ["full"] } paste = "1.0.14" diff --git a/stylus-proc/Cargo.toml b/stylus-proc/Cargo.toml index fc85630a..c5a5b432 100644 --- a/stylus-proc/Cargo.toml +++ b/stylus-proc/Cargo.toml @@ -25,11 +25,10 @@ quote.workspace = true [features] export-abi = [] -storage-cache = [] reentrant = [] [package.metadata.docs.rs] -features = ["export-abi", "storage-cache"] +features = ["export-abi"] [lib] proc-macro = true diff --git a/stylus-proc/src/lib.rs b/stylus-proc/src/lib.rs index 471b0971..12af7a71 100644 --- a/stylus-proc/src/lib.rs +++ b/stylus-proc/src/lib.rs @@ -148,8 +148,8 @@ pub fn sol_storage(input: TokenStream) -> TokenStream { /// # Reentrant calls /// /// Contracts that opt into reentrancy via the `reentrant` feature flag require extra care. -/// When the `storage-cache` feature is enabled, cross-contract calls must [`flush`] or [`clear`] -/// the [`StorageCache`] to safeguard state. This happens automatically via the type system. +/// When enabled, cross-contract calls must [`flush`] or [`clear`] the [`StorageCache`] to safeguard state. +/// This happens automatically via the type system. /// /// ```ignore /// sol_interface! { diff --git a/stylus-proc/src/methods/entrypoint.rs b/stylus-proc/src/methods/entrypoint.rs index 98f39042..2dee2772 100644 --- a/stylus-proc/src/methods/entrypoint.rs +++ b/stylus-proc/src/methods/entrypoint.rs @@ -72,15 +72,6 @@ pub fn entrypoint(attr: TokenStream, input: TokenStream) -> TokenStream { } } - // flush the cache before program exit - cfg_if! { - if #[cfg(feature = "storage-cache")] { - let flush_cache = quote! { stylus_sdk::storage::StorageCache::flush(); }; - } else { - let flush_cache = quote! {}; - } - } - output.extend(quote! { #[no_mangle] pub unsafe fn mark_used() { @@ -97,7 +88,7 @@ pub fn entrypoint(attr: TokenStream, input: TokenStream) -> TokenStream { Ok(data) => (data, 0), Err(data) => (data, 1), }; - #flush_cache + unsafe { stylus_sdk::storage::StorageCache::flush() }; stylus_sdk::contract::output(&data); status } diff --git a/stylus-sdk/Cargo.toml b/stylus-sdk/Cargo.toml index 007a2b60..430b1eeb 100644 --- a/stylus-sdk/Cargo.toml +++ b/stylus-sdk/Cargo.toml @@ -22,9 +22,6 @@ lazy_static.workspace = true # export-abi regex = { workspace = true, optional = true } -# storage-cache -fnv = { workspace = true, optional = true } - # local deps stylus-proc.workspace = true @@ -36,10 +33,9 @@ sha3.workspace = true features = ["default", "docs", "debug", "export-abi"] [features] -default = ["storage-cache"] +default = [] export-abi = ["debug", "regex", "stylus-proc/export-abi"] debug = [] docs = [] hostio = [] -storage-cache = ["fnv", "stylus-proc/storage-cache"] reentrant = ["stylus-proc/reentrant"] diff --git a/stylus-sdk/src/call/context.rs b/stylus-sdk/src/call/context.rs index 5d968e3b..e40a7f4f 100644 --- a/stylus-sdk/src/call/context.rs +++ b/stylus-sdk/src/call/context.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2023, Offchain Labs, Inc. +// Copyright 2022-2024, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md use crate::storage::TopLevelStorage; @@ -52,9 +52,6 @@ where /// } /// ``` /// - /// Projects that opt out of the [`StorageCache`] by disabling the `storage-cache` feature - /// may ignore this method. - /// /// [`StorageCache`]: crate::storage::StorageCache /// [`flush`]: crate::storage::StorageCache::flush /// [`clear`]: crate::storage::StorageCache::clear @@ -133,7 +130,7 @@ where impl NonPayableCallContext for &mut T where T: TopLevelStorage {} cfg_if! { - if #[cfg(all(feature = "storage-cache", feature = "reentrant"))] { + if #[cfg(feature = "reentrant")] { // The following impls safeguard state during reentrancy scenarios impl StaticCallContext for Call<&S, false> {} @@ -165,7 +162,7 @@ cfg_if! { } cfg_if! { - if #[cfg(any(all(feature = "storage-cache", feature = "reentrant"), feature = "docs"))] { + if #[cfg(any(feature = "reentrant", feature = "docs"))] { impl Default for Call<(), false> { fn default() -> Self { Self::new() diff --git a/stylus-sdk/src/call/mod.rs b/stylus-sdk/src/call/mod.rs index 9fa3df02..5c412e22 100644 --- a/stylus-sdk/src/call/mod.rs +++ b/stylus-sdk/src/call/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2023, Offchain Labs, Inc. +// Copyright 2022-2024, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md //! Call other contracts. @@ -18,7 +18,7 @@ pub use self::{context::Call, error::Error, raw::RawCall, traits::*, transfer::t pub(crate) use raw::CachePolicy; -#[cfg(all(feature = "storage-cache", feature = "reentrant"))] +#[cfg(feature = "reentrant")] use crate::storage::Storage; mod context; @@ -29,12 +29,12 @@ mod transfer; macro_rules! unsafe_reentrant { ($block:block) => { - #[cfg(all(feature = "storage-cache", feature = "reentrant"))] + #[cfg(feature = "reentrant")] unsafe { $block } - #[cfg(not(all(feature = "storage-cache", feature = "reentrant")))] + #[cfg(not(feature = "reentrant"))] $block }; } @@ -45,7 +45,7 @@ pub fn static_call( to: Address, data: &[u8], ) -> Result, Error> { - #[cfg(all(feature = "storage-cache", feature = "reentrant"))] + #[cfg(feature = "reentrant")] Storage::flush(); // flush storage to persist changes, but don't invalidate the cache unsafe_reentrant! {{ @@ -68,7 +68,7 @@ pub unsafe fn delegate_call( to: Address, data: &[u8], ) -> Result, Error> { - #[cfg(all(feature = "storage-cache", feature = "reentrant"))] + #[cfg(feature = "reentrant")] Storage::clear(); // clear the storage to persist changes, invalidating the cache RawCall::new_delegate() @@ -79,7 +79,7 @@ pub unsafe fn delegate_call( /// Calls the contract at the given address. pub fn call(context: impl MutatingCallContext, to: Address, data: &[u8]) -> Result, Error> { - #[cfg(all(feature = "storage-cache", feature = "reentrant"))] + #[cfg(feature = "reentrant")] Storage::clear(); // clear the storage to persist changes, invalidating the cache unsafe_reentrant! {{ diff --git a/stylus-sdk/src/call/raw.rs b/stylus-sdk/src/call/raw.rs index 91655c64..4e6c7a0f 100644 --- a/stylus-sdk/src/call/raw.rs +++ b/stylus-sdk/src/call/raw.rs @@ -8,13 +8,13 @@ use crate::{ use alloy_primitives::{Address, B256, U256}; use cfg_if::cfg_if; -#[cfg(all(feature = "storage-cache", feature = "reentrant"))] +#[cfg(feature = "reentrant")] use crate::storage::StorageCache; macro_rules! unsafe_reentrant { ($(#[$meta:meta])* pub fn $name:ident $($rest:tt)*) => { cfg_if! { - if #[cfg(all(feature = "storage-cache", feature = "reentrant"))] { + if #[cfg(feature = "reentrant")] { $(#[$meta])* pub unsafe fn $name $($rest)* } else { @@ -161,20 +161,14 @@ impl RawCall { } /// Write all cached values to persistent storage before the call. - #[cfg(any( - all(feature = "storage-cache", feature = "reentrant"), - feature = "docs" - ))] + #[cfg(any(feature = "reentrant", feature = "docs"))] 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 call. - #[cfg(any( - all(feature = "storage-cache", feature = "reentrant"), - feature = "docs" - ))] + #[cfg(any(feature = "reentrant", feature = "docs"))] pub fn clear_storage_cache(mut self) -> Self { self.cache_policy = CachePolicy::Clear; self @@ -185,7 +179,7 @@ impl RawCall { /// /// # Safety /// - /// This function becomes `unsafe` when the `reentrant` and `storage-cache` features are enabled. + /// This function becomes `unsafe` when the `reentrant` feature is enabled. /// That's because raw calls might alias storage if used in the middle of a storage ref's lifetime. /// /// For extra flexibility, this method does not clear the global storage cache by default. @@ -198,7 +192,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(all(feature = "storage-cache", feature = "reentrant"))] + #[cfg(feature = "reentrant")] match self.cache_policy { CachePolicy::Clear => StorageCache::clear(), CachePolicy::Flush => StorageCache::flush(), diff --git a/stylus-sdk/src/call/transfer.rs b/stylus-sdk/src/call/transfer.rs index 037bf4b7..e42bf4c4 100644 --- a/stylus-sdk/src/call/transfer.rs +++ b/stylus-sdk/src/call/transfer.rs @@ -1,14 +1,14 @@ -// Copyright 2022-2023, Offchain Labs, Inc. +// Copyright 2022-2024, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md use crate::call::RawCall; use alloc::vec::Vec; use alloy_primitives::{Address, U256}; -#[cfg(all(feature = "storage-cache", feature = "reentrant"))] +#[cfg(feature = "reentrant")] use crate::storage::TopLevelStorage; -#[cfg(all(feature = "storage-cache", feature = "reentrant"))] +#[cfg(feature = "reentrant")] use crate::storage::Storage; /// Transfers an amount of ETH in wei to the given account. @@ -18,7 +18,7 @@ use crate::storage::Storage; /// If this is not desired, the [`call`](super::call) function may be used directly. /// /// [`call`]: super::call -#[cfg(all(feature = "storage-cache", feature = "reentrant"))] +#[cfg(feature = "reentrant")] pub fn transfer_eth( _storage: &mut impl TopLevelStorage, to: Address, @@ -43,7 +43,7 @@ pub fn transfer_eth( /// transfer_eth(recipient, value)?; // these two are equivalent /// call(Call::new().value(value), recipient, &[])?; // these two are equivalent /// ``` -#[cfg(not(all(feature = "storage-cache", feature = "reentrant")))] +#[cfg(not(feature = "reentrant"))] pub fn transfer_eth(to: Address, amount: U256) -> Result<(), Vec> { RawCall::new_with_value(amount) .skip_return_data() diff --git a/stylus-sdk/src/deploy/raw.rs b/stylus-sdk/src/deploy/raw.rs index 74ea6805..a92510d1 100644 --- a/stylus-sdk/src/deploy/raw.rs +++ b/stylus-sdk/src/deploy/raw.rs @@ -9,7 +9,7 @@ use crate::{ use alloc::vec::Vec; use alloy_primitives::{Address, B256, U256}; -#[cfg(all(feature = "storage-cache", feature = "reentrant"))] +#[cfg(feature = "reentrant")] use crate::storage::StorageCache; /// Mechanism for performing raw deploys of other contracts. @@ -62,14 +62,14 @@ impl RawDeploy { } /// Write all cached values to persistent storage before the init code. - #[cfg(feature = "storage-cache")] + #[cfg(feature = "reentrant")] 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")] + #[cfg(feature = "reentrant")] pub fn clear_storage_cache(mut self) -> Self { self.cache_policy = CachePolicy::Clear; self @@ -90,7 +90,7 @@ impl RawDeploy { /// [flush]: crate::storage::StorageCache::flush /// [clear]: crate::storage::StorageCache::clear pub unsafe fn deploy(self, code: &[u8], endowment: U256) -> Result> { - #[cfg(all(feature = "storage-cache", feature = "reentrant"))] + #[cfg(feature = "reentrant")] match self.cache_policy { CachePolicy::Clear => StorageCache::clear(), CachePolicy::Flush => StorageCache::flush(), diff --git a/stylus-sdk/src/hostio.rs b/stylus-sdk/src/hostio.rs index 82792bff..c7b378db 100644 --- a/stylus-sdk/src/hostio.rs +++ b/stylus-sdk/src/hostio.rs @@ -55,16 +55,27 @@ extern "C" { /// value stored in the EVM state trie at offset `key`, which will be `0` when not previously /// set. The semantics, then, are equivalent to that of the EVM's [`SLOAD`] opcode. /// + /// Note: the Stylus VM implements storage caching. This means that repeated calls to the same key + /// will cost less than in the EVM. + /// /// [`SLOAD`]: https://www.evm.codes/#54 pub fn storage_load_bytes32(key: *const u8, dest: *mut u8); - /// Stores a 32-byte value to permanent storage. Stylus's storage format is identical to that - /// of the EVM. This means that, under the hood, this hostio is storing a 32-byte value into - /// the EVM state trie at offset `key`. Furthermore, refunds are tabulated exactly as in the - /// EVM. The semantics, then, are equivalent to that of the EVM's [`SSTORE`] opcode. + /// Writes a 32-byte value to the permanent storage cache. Stylus's storage format is identical to that + /// of the EVM. This means that, under the hood, this hostio represents storing a 32-byte value into + /// the EVM state trie at offset `key`. Refunds are tabulated exactly as in the EVM. The semantics, then, + /// are equivalent to that of the EVM's [`SSTORE`] opcode. + /// + /// Note: because the value is cached, one must call `storage_flush_cache` to persist it. + /// + /// [`SSTORE`]: https://www.evm.codes/#55 + pub fn storage_cache_bytes32(key: *const u8, value: *const u8); + + /// Persists any dirty values in the storage cache to the EVM state trie, dropping the cache entirely if requested. + /// Analogous to repeated invocations of [`SSTORE`]. /// /// [`SSTORE`]: https://www.evm.codes/#55 - pub fn storage_store_bytes32(key: *const u8, value: *const u8); + pub fn storage_flush_cache(clear: bool); /// Gets the basefee of the current block. The semantics are equivalent to that of the EVM's /// [`BASEFEE`] opcode. diff --git a/stylus-sdk/src/storage/cache.rs b/stylus-sdk/src/storage/cache.rs deleted file mode 100644 index 17770369..00000000 --- a/stylus-sdk/src/storage/cache.rs +++ /dev/null @@ -1,97 +0,0 @@ -// 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}; -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. -/// -/// [`EagerStorage`]: super::EagerStorage -pub struct StorageCache(HashMap); - -/// Represents the EVM word at a given key. -pub struct StorageWord { - /// The current value of the slot. - value: B256, - /// The value in the EVM state trie, if known. - known: Option, -} - -impl StorageWord { - /// Creates a new slot from a known value in the EVM state trie. - fn new_known(known: B256) -> Self { - Self { - value: known, - known: Some(known), - } - } - - /// Creates a new slot without knowing the underlying value in the EVM state trie. - fn new_unknown(value: B256) -> Self { - Self { value, known: None } - } - - /// Whether a slot should be written to disk. - fn dirty(&self) -> bool { - Some(self.value) != self.known - } -} - -/// Forces a type to implement [`Sync`]. -struct ForceSync(T); - -unsafe impl Sync for ForceSync {} - -lazy_static! { - /// Global cache managing persistent storage operations. - static ref CACHE: ForceSync> = ForceSync(UnsafeCell::new(StorageCache(HashMap::default()))); -} - -/// Mutably accesses the global cache's hashmap -macro_rules! cache { - () => { - unsafe { &mut (*CACHE.0.get()).0 } - }; -} - -impl GlobalStorage for StorageCache { - fn get_word(key: U256) -> B256 { - cache!() - .entry(key) - .or_insert_with(|| unsafe { StorageWord::new_known(load_bytes32(key)) }) - .value - } - - unsafe fn set_word(key: U256, value: B256) { - cache!().insert(key, StorageWord::new_unknown(value)); - } -} - -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`]. - /// - /// [`SLOAD`]: https://www.evm.codes/#54 - pub fn flush() { - for (key, entry) in cache!() { - if entry.dirty() { - unsafe { store_bytes32(*key, entry.value) }; - } - } - } - - /// Flush and clear the storage cache. - pub fn clear() { - StorageCache::flush(); - cache!().clear(); - } -} diff --git a/stylus-sdk/src/storage/eager.rs b/stylus-sdk/src/storage/eager.rs deleted file mode 100644 index 9e8d335e..00000000 --- a/stylus-sdk/src/storage/eager.rs +++ /dev/null @@ -1,25 +0,0 @@ -// 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. -/// -/// [`StorageCache`]: super::StorageCache -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/mod.rs b/stylus-sdk/src/storage/mod.rs index 80853ba8..d79a16a4 100644 --- a/stylus-sdk/src/storage/mod.rs +++ b/stylus-sdk/src/storage/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2023, Offchain Labs, Inc. +// Copyright 2023-2024, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/stylus/licenses/COPYRIGHT.md //! Solidity compatible storage types and persistent storage access. @@ -25,7 +25,6 @@ use crate::hostio; use alloy_primitives::{Address, BlockHash, BlockNumber, FixedBytes, Signed, Uint, B256, U256}; use alloy_sol_types::sol_data::{ByteCount, SupportedFixedBytes}; -use cfg_if::cfg_if; use core::{cell::OnceCell, marker::PhantomData, ops::Deref}; pub use array::StorageArray; @@ -43,43 +42,45 @@ mod map; mod traits; mod vec; -cfg_if! { - if #[cfg(any(not(feature = "storage-cache"), feature = "docs"))] { - mod eager; - pub use eager::EagerStorage; - } -} +pub(crate) type Storage = StorageCache; -cfg_if! { - if #[cfg(feature = "storage-cache")] { - mod cache; +/// Global accessor to persistent storage that relies on VM-level caching. +/// +/// [`LocalStorageCache`]: super::LocalStorageCache +pub struct StorageCache; - pub use cache::StorageCache; +impl GlobalStorage for StorageCache { + /// Retrieves a 32-byte EVM word from persistent storage. + fn get_word(key: U256) -> B256 { + let mut data = B256::ZERO; + unsafe { hostio::storage_load_bytes32(B256::from(key).as_ptr(), data.as_mut_ptr()) }; + data + } - pub(crate) type Storage = StorageCache; - } else { - pub(crate) type Storage = EagerStorage; + /// Stores a 32-byte EVM word to persistent storage. + /// + /// # Safety + /// + /// May alias storage. + unsafe fn set_word(key: U256, value: B256) { + hostio::storage_cache_bytes32(B256::from(key).as_ptr(), value.as_ptr()) } } -/// Retrieves a 32-byte EVM word from persistent storage directly, bypassing all caches. -/// -/// # Safety -/// -/// May alias storage. -pub unsafe fn load_bytes32(key: U256) -> B256 { - let mut data = B256::ZERO; - unsafe { hostio::storage_load_bytes32(B256::from(key).as_ptr(), data.as_mut_ptr()) }; - data -} +impl StorageCache { + /// Flushes the VM cache, persisting all values to the EVM state trie. + /// Note: this is used at the end of the [`entrypoint`] macro and is not typically called by user code. + /// + /// [`entrypoint`]: macro@stylus_proc::entrypoint + pub fn flush() { + unsafe { hostio::storage_flush_cache(false) } + } -/// Stores a 32-byte EVM word to persistent storage directly, bypassing all caches. -/// -/// # Safety -/// -/// May alias storage. -pub unsafe fn store_bytes32(key: U256, data: B256) { - unsafe { hostio::storage_store_bytes32(B256::from(key).as_ptr(), data.as_ptr()) }; + /// Flushes and clears the VM cache, persisting all values to the EVM state trie. + /// This is useful in cases of reentrancy to ensure cached values from one call context show up in another. + pub fn clear() { + unsafe { hostio::storage_flush_cache(true) } + } } /// Overwrites the value in a cell.