diff --git a/Cargo.lock b/Cargo.lock index 418fc24..1d8f9b3 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" @@ -308,7 +302,7 @@ checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" [[package]] name = "mini-alloc" -version = "0.4.3" +version = "0.5.0" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-test", @@ -498,7 +492,7 @@ dependencies = [ [[package]] name = "stylus-proc" -version = "0.4.3" +version = "0.5.0" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -515,13 +509,12 @@ dependencies = [ [[package]] name = "stylus-sdk" -version = "0.4.3" +version = "0.5.0" dependencies = [ "alloy-primitives", "alloy-sol-types", "cfg-if 1.0.0", "derivative", - "fnv", "hex", "keccak-const", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index d56cda7..fa139e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["stylus-sdk", "stylus-proc", "mini-alloc"] resolver = "2" [workspace.package] -version = "0.4.3" +version = "0.5.0" edition = "2021" authors = ["Offchain Labs"] license = "MIT OR Apache-2.0" @@ -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" @@ -35,4 +32,4 @@ convert_case = "0.6.0" # members stylus-sdk = { path = "stylus-sdk" } -stylus-proc = { path = "stylus-proc", version = "0.4.1" } +stylus-proc = { path = "stylus-proc", version = "0.5.0" } diff --git a/stylus-proc/Cargo.toml b/stylus-proc/Cargo.toml index fc85630..c5a5b43 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 471b097..12af7a7 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 98f3904..e7d8c52 100644 --- a/stylus-proc/src/methods/entrypoint.rs +++ b/stylus-proc/src/methods/entrypoint.rs @@ -47,8 +47,8 @@ pub fn entrypoint(attr: TokenStream, input: TokenStream) -> TokenStream { if cfg!(feature = "export-abi") { output.extend(quote! { - pub fn main() { - stylus_sdk::abi::export::print_abi::<#name>(); + pub fn print_abi(license: &str, pragma: &str) { + stylus_sdk::abi::export::print_abi::<#name>(license, pragma); } }); } @@ -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-proc/src/methods/external.rs b/stylus-proc/src/methods/external.rs index 0956b35..9ec4e98 100644 --- a/stylus-proc/src/methods/external.rs +++ b/stylus-proc/src/methods/external.rs @@ -5,13 +5,14 @@ use crate::types::{self, Purity}; use convert_case::{Case, Casing}; use proc_macro::TokenStream; use proc_macro2::Ident; -use quote::quote; +use quote::{quote, quote_spanned}; use std::{mem, str::FromStr}; use syn::{ parenthesized, parse::{Parse, ParseStream}, parse_macro_input, punctuated::Punctuated, + spanned::Spanned, FnArg, ImplItem, Index, ItemImpl, Lit, LitStr, Pat, PatType, Result, ReturnType, Token, Type, }; @@ -128,7 +129,14 @@ pub fn external(_attr: TokenStream, input: TokenStream) -> TokenStream { }; // get the solidity args - let expand_args = (0..args.len()).map(Index::from).map(|i| quote! { args.#i }); + let mut expand_args = vec![]; + for (index, (_, ty)) in args.iter().enumerate() { + let index = Index { + index: index as u32, + span: ty.span(), + }; + expand_args.push(quote! { args.#index }); + } // calculate selector let constant = Ident::new(&format!("SELECTOR_{name}"), name.span()); @@ -136,19 +144,28 @@ pub fn external(_attr: TokenStream, input: TokenStream) -> TokenStream { let selector = match override_id { Some(id) => quote! { #id }, - None => quote! { u32::from_be_bytes(function_selector!(#sol_name #(, #arg_types)*)) }, + None => quote! { u32::from_be_bytes(function_selector!(#sol_name #(, #arg_types )*)) }, }; selectors.extend(quote! { #[allow(non_upper_case_globals)] const #constant: u32 = #selector; }); + let in_span = method.sig.inputs.span(); + let decode_inputs = quote_spanned! { in_span => <(#( #arg_types, )*) as AbiType>::SolType }; + + let ret_span = match &method.sig.output { + x @ ReturnType::Default => x.span(), + ReturnType::Type(_, ty) => ty.span(), // right of arrow + }; + let encode_result = quote_spanned! { ret_span => EncodableReturnType::encode(result) }; + // match against the selector match_selectors.extend(quote! { #[allow(non_upper_case_globals)] #constant => { #deny_value - let args = match <<( #( #arg_types, )* ) as AbiType>::SolType as SolType>::decode(input, true) { + let args = match <#decode_inputs as SolType>::decode(input, true) { Ok(args) => args, Err(err) => { internal::failed_to_decode_arguments(err); @@ -156,7 +173,7 @@ pub fn external(_attr: TokenStream, input: TokenStream) -> TokenStream { } }; let result = Self::#name(#storage #(#expand_args, )* ); - Some(EncodableReturnType::encode(result)) + Some(#encode_result) } }); diff --git a/stylus-sdk/Cargo.toml b/stylus-sdk/Cargo.toml index 007a2b6..c642065 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"] -export-abi = ["debug", "regex", "stylus-proc/export-abi"] +default = [] +export-abi = ["debug", "regex", "stylus-proc/export-abi", "alloy-primitives/tiny-keccak"] debug = [] docs = [] hostio = [] -storage-cache = ["fnv", "stylus-proc/storage-cache"] reentrant = ["stylus-proc/reentrant"] diff --git a/stylus-sdk/src/abi/export/mod.rs b/stylus-sdk/src/abi/export/mod.rs index 535291c..8553c1a 100644 --- a/stylus-sdk/src/abi/export/mod.rs +++ b/stylus-sdk/src/abi/export/mod.rs @@ -37,12 +37,15 @@ impl fmt::Display for AbiPrinter { } /// Prints the full contract ABI to standard out -pub fn print_abi() { +pub fn print_abi(license: &str, pragma: &str) { println!("/**"); println!(" * This file was automatically generated by Stylus and represents a Rust program."); println!(" * For more information, please see [The Stylus SDK](https://github.com/OffchainLabs/stylus-sdk-rs)."); println!(" */"); println!(); + println!("// SPDX-License-Identifier: {license}"); + println!("{pragma}"); + println!(); print!("{}", AbiPrinter::(PhantomData)); } diff --git a/stylus-sdk/src/call/context.rs b/stylus-sdk/src/call/context.rs index 5d968e3..e40a7f4 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 8e9fbbb..b2028c5 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. @@ -21,7 +21,7 @@ pub use self::{ pub(crate) use raw::CachePolicy; -#[cfg(all(feature = "storage-cache", feature = "reentrant"))] +#[cfg(feature = "reentrant")] use crate::storage::Storage; mod context; @@ -32,12 +32,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 }; } @@ -48,7 +48,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! {{ @@ -71,7 +71,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() @@ -82,7 +82,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 91655c6..4e6c7a0 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 037bf4b..e42bf4c 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 74ea680..a92510d 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/evm.rs b/stylus-sdk/src/evm.rs index f2b15fb..96e9ae4 100644 --- a/stylus-sdk/src/evm.rs +++ b/stylus-sdk/src/evm.rs @@ -54,7 +54,7 @@ pub fn log(event: T) { /// This function exists to force the compiler to import this symbol. /// Calling it will unproductively consume gas. pub fn pay_for_memory_grow(pages: u16) { - unsafe { hostio::memory_grow(pages) } + unsafe { hostio::pay_for_memory_grow(pages) } } wrap_hostio!( diff --git a/stylus-sdk/src/hostio.rs b/stylus-sdk/src/hostio.rs index c115630..36ab02c 100644 --- a/stylus-sdk/src/hostio.rs +++ b/stylus-sdk/src/hostio.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 //! Raw host I/Os for low-level access to the Stylus runtime. @@ -20,14 +20,69 @@ //! assert_eq!(sender, msg::sender()); //! ``` -#[link(wasm_import_module = "vm_hooks")] -extern "C" { +use cfg_if::cfg_if; + +macro_rules! vm_hooks { + ( + $(#[$block_meta:meta])* // macros & docstrings to apply to all funcs + module($link:literal, $stub:ident); // configures the wasm_import_module to link + + // all the function declarations + $($(#[$meta:meta])* $vis:vis fn $func:ident ($($arg:ident : $arg_type:ty),* ) $(-> $return_type:ty)?);* + ) => { + cfg_if! { + if #[cfg(feature = "export-abi")] { + // Generate a stub for each function. + // We use a module for the block macros & docstrings. + $(#[$block_meta])* + mod $stub { + $( + $(#[$meta])* + #[allow(unused_variables, clippy::missing_safety_doc)] + $vis unsafe fn $func($($arg : $arg_type),*) $(-> $return_type)? { + unimplemented!() + } + )* + } + pub use $stub::*; + } else { + // Generate a wasm import for each function. + $(#[$block_meta])* + #[link(wasm_import_module = $link)] + extern "C" { + $( + $(#[$meta])* + $vis fn $func($($arg : $arg_type),*) $(-> $return_type)?; + )* + } + } + } + }; +} + +vm_hooks! { + module("vm_hooks", vm_hooks); + /// Gets the ETH balance in wei of the account at the given address. /// The semantics are equivalent to that of the EVM's [`BALANCE`] opcode. /// /// [`BALANCE`]: https://www.evm.codes/#31 pub fn account_balance(address: *const u8, dest: *mut u8); + /// Gets a subset of the code from the account at the given address. The semantics are identical to that + /// of the EVM's [`EXT_CODE_COPY`] opcode, aside from one small detail: the write to the buffer `dest` will + /// stop after the last byte is written. This is unlike the EVM, which right pads with zeros in this scenario. + /// The return value is the number of bytes written, which allows the caller to detect if this has occured. + /// + /// [`EXT_CODE_COPY`]: https://www.evm.codes/#3C + pub fn account_code(address: *const u8, offset: usize, size: usize, dest: *mut u8) -> usize; + + /// Gets the size of the code in bytes at the given address. The semantics are equivalent + /// to that of the EVM's [`EXT_CODESIZE`]. + /// + /// [`EXT_CODESIZE`]: https://www.evm.codes/#3B + pub fn account_code_size(address: *const u8) -> usize; + /// Gets the code hash of the account at the given address. The semantics are equivalent /// to that of the EVM's [`EXT_CODEHASH`] opcode. Note that the code hash of an account without /// code will be the empty hash @@ -41,16 +96,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. @@ -112,7 +178,7 @@ extern "C" { calldata_len: usize, value: *const u8, gas: u64, - return_data_len: *mut usize, + return_data_len: *mut usize ) -> u8; /// Gets the address of the current program. The semantics are equivalent to that of the EVM's @@ -141,7 +207,7 @@ extern "C" { code_len: usize, endowment: *const u8, contract: *mut u8, - revert_data_len: *mut usize, + revert_data_len: *mut usize ); /// Deploys a new contract using the init code provided, which the EVM executes to construct @@ -165,7 +231,7 @@ extern "C" { endowment: *const u8, salt: *const u8, contract: *mut u8, - revert_data_len: *mut usize, + revert_data_len: *mut usize ); /// Delegate calls the contract at the given address, with the option to limit the amount of @@ -187,7 +253,7 @@ extern "C" { calldata: *const u8, calldata_len: usize, gas: u64, - return_data_len: *mut usize, + return_data_len: *mut usize ) -> u8; /// Emits an EVM log with the given number of topics and data, the first bytes of which should @@ -220,8 +286,7 @@ extern "C" { /// program's memory grows. Otherwise compilation through the `ArbWasm` precompile will revert. /// Internally the Stylus VM forces calls to this hostio whenever new WASM pages are allocated. /// Calls made voluntarily will unproductively consume gas. - #[allow(dead_code)] - pub fn memory_grow(pages: u16); + pub fn pay_for_memory_grow(pages: u16); /// Whether the current call is reentrant. pub fn msg_reentrant() -> bool; @@ -298,7 +363,7 @@ extern "C" { calldata: *const u8, calldata_len: usize, gas: u64, - return_data_len: *mut usize, + return_data_len: *mut usize ) -> u8; /// Gets the gas price in wei per gas, which on Arbitrum chains equals the basefee. The @@ -317,12 +382,13 @@ extern "C" { /// EVM's [`ORIGIN`] opcode. /// /// [`ORIGIN`]: https://www.evm.codes/#32 - pub fn tx_origin(origin: *mut u8); + pub fn tx_origin(origin: *mut u8) } -#[allow(dead_code)] -#[link(wasm_import_module = "console")] -extern "C" { +vm_hooks! { + #[allow(dead_code)] + module("console", console); + /// Prints a 32-bit floating point number to the console. Only available in debug mode with /// floating point enabled. pub fn log_f32(value: f32); @@ -340,7 +406,7 @@ extern "C" { pub fn log_i64(value: i64); /// Prints a UTF-8 encoded string to the console. Only available in debug mode. - pub fn log_txt(text: *const u8, len: usize); + pub fn log_txt(text: *const u8, len: usize) } macro_rules! wrap_hostio { diff --git a/stylus-sdk/src/storage/cache.rs b/stylus-sdk/src/storage/cache.rs deleted file mode 100644 index 1777036..0000000 --- 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 9e8d335..0000000 --- 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 80853ba..d79a16a 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. diff --git a/stylus-sdk/src/types.rs b/stylus-sdk/src/types.rs index 2c65cb5..1847430 100644 --- a/stylus-sdk/src/types.rs +++ b/stylus-sdk/src/types.rs @@ -14,6 +14,7 @@ //! ``` use crate::hostio; +use alloc::vec::Vec; use alloy_primitives::{b256, Address, B256, U256}; /// Trait that allows the [`Address`] type to inspect the corresponding account's balance and codehash. @@ -21,10 +22,21 @@ pub trait AddressVM { /// The balance in wei of the account. fn balance(&self) -> U256; + /// Gets the code at the given address. The semantics are equivalent to that of the EVM's [`EXT_CODESIZE`]. + /// + /// [`EXT_CODE_COPY`]: https://www.evm.codes/#3C + fn code(&self) -> Vec; + + /// Gets the size of the code in bytes at the given address. The semantics are equivalent + /// to that of the EVM's [`EXT_CODESIZE`]. + /// + /// [`EXT_CODESIZE`]: https://www.evm.codes/#3B + fn code_size(&self) -> usize; + /// The codehash of the contract or [`EOA`] at the given address. /// /// [`EOA`]: https://ethereum.org/en/developers/docs/accounts/#types-of-account - fn codehash(&self) -> B256; + fn code_hash(&self) -> B256; /// Determines if an account has code. Note that this is insufficient to determine if an address is an /// [`EOA`]. During contract deployment, an account only gets its code at the very end, meaning that @@ -41,14 +53,28 @@ impl AddressVM for Address { U256::from_be_bytes(data) } - fn codehash(&self) -> B256 { + fn code(&self) -> Vec { + let size = self.code_size(); + let mut dest = Vec::with_capacity(size); + unsafe { + hostio::account_code(self.as_ptr(), 0, size, dest.as_mut_ptr()); + dest.set_len(size); + dest + } + } + + fn code_size(&self) -> usize { + unsafe { hostio::account_code_size(self.as_ptr()) } + } + + fn code_hash(&self) -> B256 { let mut data = [0; 32]; unsafe { hostio::account_codehash(self.as_ptr(), data.as_mut_ptr()) }; data.into() } fn has_code(&self) -> bool { - let hash = self.codehash(); + let hash = self.code_hash(); !hash.is_zero() && hash != b256!("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") }