diff --git a/Cargo.lock b/Cargo.lock index b631c9a12..57cd80fb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2638,6 +2638,7 @@ dependencies = [ name = "zink-codegen" version = "0.1.11" dependencies = [ + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.77", diff --git a/Cargo.toml b/Cargo.toml index d422cbb43..ec64dfae3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ cargo_metadata = "0.18.1" ccli = "0.0.1" colored = "2.1.0" etc = "0.1.19" +heck = "0.5.0" hex = "0.4.3" indexmap = "2.2.2" paste = "1.0.14" @@ -41,7 +42,7 @@ semver = "1.0.21" serde = { version = "1.0.196", default-features = false } serde_json = "1.0.113" smallvec = "1.13.1" -syn = { version = "2.0.48", features = [ "full" ] } +syn = { version = "2.0.77", features = [ "full" ] } thiserror = "1.0.56" tiny-keccak = { version = "2.0.2", features = ["keccak"], default-features = false } toml = "0.8.9" diff --git a/examples/constructor.rs b/examples/constructor.rs index 6a314d3c4..8f289ea92 100644 --- a/examples/constructor.rs +++ b/examples/constructor.rs @@ -16,12 +16,6 @@ use zink::Storage; #[zink::storage(i32)] pub struct Counter; -/// Get value from the storage. -#[zink::external] -pub fn get() -> i32 { - Counter::get() -} - #[cfg(not(target_arch = "wasm32"))] fn main() {} @@ -61,7 +55,7 @@ fn init_storage() -> anyhow::Result<()> { .bytecode()?, )?; info = evm - .calldata(&contract.encode(&["get()"])?) + .calldata(&contract.encode(&["counter()"])?) .call(info.address)?; assert_eq!(info.ret, value.to_bytes32()); diff --git a/examples/dkmapping.rs b/examples/dkmapping.rs new file mode 100644 index 000000000..bf0f3ec8b --- /dev/null +++ b/examples/dkmapping.rs @@ -0,0 +1,59 @@ +//! Storage example. +#![cfg_attr(target_arch = "wasm32", no_std)] +#![cfg_attr(target_arch = "wasm32", no_main)] + +extern crate zink; + +use zink::DoubleKeyMapping as _; + +/// Counter with value type `i32` +#[zink::storage(i32, i32, i32)] +pub struct DoubleKeyMapping; + +/// Set the mapping +#[zink::external] +pub fn mset(key1: i32, key2: i32, value: i32) { + DoubleKeyMapping::set(key1, key2, value); +} + +#[cfg(not(target_arch = "wasm32"))] +fn main() {} + +#[test] +fn storage_double_key_mapping() -> anyhow::Result<()> { + use zint::{Bytes32, Contract}; + + let mut contract = Contract::search("dkmapping")?.compile()?; + let mut evm = contract.deploy()?.commit(true); + + let key1 = 0x00; + let key2 = 0x01; + let value: i32 = 0x42; + + // set value to storage + let calldata = contract.encode(&[ + b"mset(int32,int32,int32)".to_vec(), + value.to_bytes32().to_vec(), + key2.to_bytes32().to_vec(), + key1.to_bytes32().to_vec(), + ])?; + let info = evm.calldata(&calldata).call(contract.address)?; + assert!(info.ret.is_empty()); + + // verify result with database + let storage_key = zint::keccak256(&[[0; 32], [0; 32], 1.to_bytes32()].concat()); + assert_eq!( + evm.storage(contract.address, storage_key)?, + value.to_bytes32(), + ); + + // get value from storage + let calldata = contract.encode(&[ + b"double_key_mapping(int32,int32)".to_vec(), + key2.to_bytes32().to_vec(), + key1.to_bytes32().to_vec(), + ])?; + let info = evm.calldata(&calldata).call(contract.address)?; + assert_eq!(info.ret, value.to_bytes32(), "{info:#?}",); + Ok(()) +} diff --git a/examples/mapping.rs b/examples/mapping.rs index 9efac021c..d34bc184b 100644 --- a/examples/mapping.rs +++ b/examples/mapping.rs @@ -7,7 +7,7 @@ extern crate zink; use zink::Mapping as _; /// Counter with value type `i32` -#[zink::storage(i32 => i32)] +#[zink::storage(i32, i32)] pub struct Mapping; /// Set the mapping @@ -16,17 +16,11 @@ pub fn mset(key: i32, value: i32) { Mapping::set(key, value); } -/// Get from ampping -#[zink::external] -pub fn mget(key: i32) -> i32 { - Mapping::get(key) -} - #[cfg(not(target_arch = "wasm32"))] fn main() {} #[test] -fn mapping() -> anyhow::Result<()> { +fn storage_mapping() -> anyhow::Result<()> { use zint::{Bytes32, Contract}; let mut contract = Contract::search("mapping")?.compile()?; @@ -44,7 +38,6 @@ fn mapping() -> anyhow::Result<()> { let info = evm.calldata(&calldata).call(contract.address)?; assert!(info.ret.is_empty()); - tracing::debug!("{info:?}"); // verify result with database let storage_key = zint::keccak256(&[0; 0x40]); assert_eq!( @@ -53,7 +46,7 @@ fn mapping() -> anyhow::Result<()> { ); // get value from storage - let calldata = contract.encode(&[b"mget(int32)".to_vec(), key.to_bytes32().to_vec()])?; + let calldata = contract.encode(&[b"mapping(int32)".to_vec(), key.to_bytes32().to_vec()])?; let info = evm.calldata(&calldata).call(contract.address)?; assert_eq!(info.ret, value.to_bytes32(), "{info:#?}",); Ok(()) diff --git a/examples/storage.rs b/examples/storage.rs index 65b3364e7..40d79b1ac 100644 --- a/examples/storage.rs +++ b/examples/storage.rs @@ -16,12 +16,6 @@ pub fn set(value: i32) { Counter::set(value); } -/// Get value from the storage. -#[zink::external] -pub fn get() -> i32 { - Counter::get() -} - #[cfg(not(target_arch = "wasm32"))] fn main() {} @@ -40,7 +34,7 @@ fn value() -> anyhow::Result<()> { } { - let info = contract.execute(&["get()"])?; + let info = contract.execute(&["counter()"])?; assert_eq!(info.ret, 0.to_bytes32()); } diff --git a/zink/codegen/Cargo.toml b/zink/codegen/Cargo.toml index 6e05dc850..85a95d57b 100644 --- a/zink/codegen/Cargo.toml +++ b/zink/codegen/Cargo.toml @@ -13,6 +13,7 @@ repository.workspace = true proc-macro = true [dependencies] +heck.workspace = true proc-macro2.workspace = true quote.workspace = true syn.workspace = true diff --git a/zink/codegen/src/lib.rs b/zink/codegen/src/lib.rs index 7a6f319c6..b2edf79c1 100644 --- a/zink/codegen/src/lib.rs +++ b/zink/codegen/src/lib.rs @@ -1,7 +1,8 @@ //! Code generation library for the zink API +#![allow(unused)] use proc_macro::TokenStream; -use syn::{parse_macro_input, DeriveInput, ItemFn, ItemStruct}; +use syn::{parse_macro_input, Attribute, DeriveInput, ItemFn, ItemStruct}; mod event; mod selector; @@ -45,13 +46,14 @@ pub fn event(input: TokenStream) -> TokenStream { /// pub struct Counter; /// /// /// storage mapping -/// #[zink::storage(i32 => i32)] +/// #[zink::storage(i32, i32)] /// pub struct Mapping; /// ``` #[proc_macro_attribute] pub fn storage(attr: TokenStream, input: TokenStream) -> TokenStream { + let ty = storage::StorageType::from(attr); let input = parse_macro_input!(input as ItemStruct); - storage::parse(attr.into(), input) + storage::Storage::parse(ty, input) } /// Mark the function as an external entry point. diff --git a/zink/codegen/src/storage.rs b/zink/codegen/src/storage.rs index f549ad81e..56b943b4b 100644 --- a/zink/codegen/src/storage.rs +++ b/zink/codegen/src/storage.rs @@ -1,72 +1,220 @@ extern crate proc_macro; -use proc_macro2::{TokenStream, TokenTree}; +use heck::AsSnakeCase; +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenTree}; use quote::quote; use std::{cell::RefCell, collections::HashSet}; -use syn::ItemStruct; +use syn::{ + meta::{self, ParseNestedMeta}, + parse::{Parse, ParseStream, Result}, + parse_quote, Attribute, Ident, ItemFn, ItemStruct, Visibility, +}; thread_local! { static STORAGE_REGISTRY: RefCell> = RefCell::new(HashSet::new()); } -/// Parse storage attribute. -/// -/// Method `get` unwraps the ptr as the original type, mainly -/// mainly for passing the compilation checks at the moment, -/// and it works for WASM in real cases as well. -/// -/// For the cases in EVM, it doesn't matter it returns pointer -/// since the value will be left on stack anyway. -pub fn parse(attr: TokenStream, input: ItemStruct) -> proc_macro::TokenStream { - let tree: Vec<_> = attr.into_iter().collect(); - match tree.len() { - 1 => storage_value(input, tree[0].clone()), - 4 => storage_mapping(input, tree), - _ => panic!("Invalid storage attributes"), - } - .into() +/// Storage attributes parser +pub struct Storage { + /// kind of the storage + ty: StorageType, + /// The source and the target storage struct + target: ItemStruct, + /// Getter function of storage + getter: Option, } -fn storage_value(is: ItemStruct, ty: TokenTree) -> TokenStream { - let name = is.ident.clone(); - let slot = storage_slot(name.to_string()); - let expanded = quote! { - #is +impl Storage { + /// Parse from proc_macro attribute + pub fn parse(ty: StorageType, target: ItemStruct) -> TokenStream { + let storage = Self::from((ty, target)); + storage.expand() + } - impl zink::storage::Storage for #name { - type Value = #ty; - const STORAGE_SLOT: i32 = #slot; + fn expand(mut self) -> TokenStream { + match &self.ty { + StorageType::Value(value) => self.expand_value(value.clone()), + StorageType::Mapping { key, value } => self.expand_mapping(key.clone(), value.clone()), + StorageType::DoubleKeyMapping { key1, key2, value } => { + self.expand_dk_mapping(key1.clone(), key2.clone(), value.clone()) + } + StorageType::Invalid => panic!("Invalid storage type"), } - }; + } - expanded -} + fn expand_value(&mut self, value: Ident) -> TokenStream { + let is = &self.target; + let name = self.target.ident.clone(); + let slot = storage_slot(name.to_string()); + let mut expanded = quote! { + #is + + impl zink::storage::Storage for #name { + type Value = #value; + const STORAGE_SLOT: i32 = #slot; + } + }; -fn storage_mapping(is: ItemStruct, ty: Vec) -> TokenStream { - // TODO: better message for this panicking - { - let conv = ty[1].to_string() + &ty[2].to_string(); - if &conv != "=>" { - panic!("Invalid mapping storage symbol"); + if let Some(getter) = self.getter() { + // TODO: generate docs from the storage doc + let gs: proc_macro2::TokenStream = parse_quote! { + #[allow(missing_docs)] + #[zink::external] + pub fn #getter() -> #value { + #name::get() + } + }; + expanded.extend(gs); } + + expanded.into() } - let key = &ty[0]; - let value = &ty[3]; - let name = is.ident.clone(); - let slot = storage_slot(name.to_string()); - let expanded = quote! { - #is + fn expand_mapping(&mut self, key: Ident, value: Ident) -> TokenStream { + let is = &self.target; + let name = self.target.ident.clone(); + let slot = storage_slot(name.to_string()); + let mut expanded = quote! { + #is + + impl zink::storage::Mapping for #name { + const STORAGE_SLOT: i32 = #slot; - impl zink::storage::Mapping for #name { - const STORAGE_SLOT: i32 = #slot; + type Key = #key; + type Value = #value; + } + }; - type Key = #key; - type Value = #value; + if let Some(getter) = self.getter() { + // TODO: generate docs from the storage doc + let gs: proc_macro2::TokenStream = parse_quote! { + #[allow(missing_docs)] + #[zink::external] + pub fn #getter(key: #key) -> #value { + #name::get(key) + } + }; + expanded.extend(gs); } - }; - expanded + expanded.into() + } + + fn expand_dk_mapping(&mut self, key1: Ident, key2: Ident, value: Ident) -> TokenStream { + let is = &self.target; + let name = self.target.ident.clone(); + let slot = storage_slot(name.to_string()); + let mut expanded = quote! { + #is + + impl zink::DoubleKeyMapping for #name { + const STORAGE_SLOT: i32 = #slot; + + type Key1 = #key1; + type Key2 = #key2; + type Value = #value; + } + }; + + if let Some(getter) = self.getter() { + // TODO: generate docs from the storage doc + let gs: proc_macro2::TokenStream = parse_quote! { + #[allow(missing_docs)] + #[zink::external] + pub fn #getter(key1: #key1, key2: #key2) -> #value { + #name::get(key1, key2) + } + }; + expanded.extend(gs); + } + + expanded.into() + } + + /// Get the getter of this storage + fn getter(&mut self) -> Option { + let mut getter = if matches!(self.target.vis, Visibility::Public(_)) { + let fname = Ident::new( + &AsSnakeCase(self.target.ident.to_string()).to_string(), + Span::call_site(), + ); + Some(fname) + } else { + None + }; + + self.getter.take().or(getter) + } +} + +impl From<(StorageType, ItemStruct)> for Storage { + fn from(patts: (StorageType, ItemStruct)) -> Self { + let mut this = Self { + ty: patts.0, + target: patts.1, + getter: None, + }; + + let mut attrs: Vec = Default::default(); + for attr in this.target.attrs.iter().cloned() { + if !attr.path().is_ident("getter") { + attrs.push(attr); + continue; + } + + let Ok(list) = attr.meta.require_list().clone() else { + panic!("Invali getter arguments"); + }; + + let Some(TokenTree::Ident(getter)) = list.tokens.clone().into_iter().nth(0) else { + panic!("Invalid getter function name"); + }; + + this.getter = Some(getter); + } + + this.target.attrs = attrs; + this + } +} + +/// Zink storage type parser +#[derive(Default, Debug)] +pub enum StorageType { + /// Single value storage + Value(Ident), + /// Mapping storage + Mapping { key: Ident, value: Ident }, + /// Double key mapping storage + DoubleKeyMapping { + key1: Ident, + key2: Ident, + value: Ident, + }, + /// Invalid storage type + #[default] + Invalid, +} + +impl From for StorageType { + fn from(input: TokenStream) -> Self { + let tokens = input.to_string(); + let types: Vec<_> = tokens.split(',').collect(); + match types.len() { + 1 => StorageType::Value(Ident::new(types[0].trim(), Span::call_site())), + 2 => StorageType::Mapping { + key: Ident::new(types[0].trim(), Span::call_site()), + value: Ident::new(types[1].trim(), Span::call_site()), + }, + 3 => StorageType::DoubleKeyMapping { + key1: Ident::new(types[0].trim(), Span::call_site()), + key2: Ident::new(types[1].trim(), Span::call_site()), + value: Ident::new(types[2].trim(), Span::call_site()), + }, + _ => panic!("Invalid storage attributes"), + } + } } fn storage_slot(name: String) -> i32 { diff --git a/zink/src/lib.rs b/zink/src/lib.rs index 84bb3adac..b2aa84dd2 100644 --- a/zink/src/lib.rs +++ b/zink/src/lib.rs @@ -9,7 +9,7 @@ pub mod primitives; pub mod storage; pub use self::{asm::Asm, event::Event}; -pub use storage::{Mapping, Storage}; +pub use storage::{DoubleKeyMapping, Mapping, Storage}; pub use zink_codegen::{external, storage, Event}; // Panic hook implementation diff --git a/zink/src/storage/array.rs b/zink/src/storage/array.rs deleted file mode 100644 index 239e4b40f..000000000 --- a/zink/src/storage/array.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Storage Array - -/// Storage array interface -pub trait StorageArray { - type Index; - const STORAGE_INDEX: i32; - - /// Get value from storage key. - fn get(index: &Self::Index) -> Value; - - /// Set value to index - fn set(index: &Self::Index, value: Value); - - /// Set value to index - fn push(value: Value); - - /// Size of array - fn size() -> Self::Index; -} diff --git a/zink/src/storage/dkmapping.rs b/zink/src/storage/dkmapping.rs new file mode 100644 index 000000000..37973b6d3 --- /dev/null +++ b/zink/src/storage/dkmapping.rs @@ -0,0 +1,52 @@ +//! Double key mapping + +use crate::{ffi, storage::StorageValue, Asm}; + +/// Storage mapping interface +pub trait DoubleKeyMapping { + const STORAGE_SLOT: i32; + + type Key1: Asm; + type Key2: Asm; + type Value: StorageValue; + + /// Get value from storage key. + fn get(key1: Self::Key1, key2: Self::Key2) -> Self::Value { + load_double_key(key1, key2, Self::STORAGE_SLOT); + Self::Value::sload() + } + + /// Set key and value + fn set(key1: Self::Key1, key2: Self::Key2, value: Self::Value) { + value.push(); + load_double_key(key1, key2, Self::STORAGE_SLOT); + unsafe { + ffi::evm::sstore(); + } + } +} + +/// Load storage key to stack +fn load_double_key(key1: impl Asm, key2: impl Asm, index: i32) { + unsafe { + // write index to memory + index.push(); + ffi::evm::push0(); + ffi::evm::mstore8(); + + // write key to memory + key1.push(); + ffi::asm::push_u8(0x20); + ffi::evm::mstore(); + + // write key to memory + key2.push(); + ffi::asm::push_u8(0x40); + ffi::evm::mstore(); + + // hash key + ffi::asm::push_u8(0x60); + ffi::evm::push0(); + ffi::evm::keccak256(); + } +} diff --git a/zink/src/storage/mapping.rs b/zink/src/storage/mapping.rs index 5c2200187..8c79d9c6e 100644 --- a/zink/src/storage/mapping.rs +++ b/zink/src/storage/mapping.rs @@ -6,65 +6,41 @@ use crate::{ffi, storage::StorageValue, Asm}; pub trait Mapping { const STORAGE_SLOT: i32; - type Key: MappingKey; + type Key: Asm; type Value: StorageValue; /// Get value from storage key. fn get(key: Self::Key) -> Self::Value { - key.load(Self::STORAGE_SLOT); + load_key(key, Self::STORAGE_SLOT); Self::Value::sload() } /// Set key and value fn set(key: Self::Key, value: Self::Value) { value.push(); - key.load(Self::STORAGE_SLOT); + load_key(key, Self::STORAGE_SLOT); unsafe { ffi::evm::sstore(); } } } -/// Interface for the key of mappings -pub trait MappingKey: Asm { - /// Load storage key to stack - fn load(self, index: i32) { - unsafe { - // write index to memory - index.push(); - ffi::evm::push0(); - ffi::evm::mstore8(); - - // write key to memory - self.push(); - ffi::asm::push_u8(0x01); - ffi::evm::mstore(); - - // hash key - ffi::asm::push_u8(0x40); - ffi::evm::push0(); - ffi::evm::keccak256(); - } +/// Load storage key to stack +fn load_key(key: impl Asm, index: i32) { + unsafe { + // write index to memory + index.push(); + ffi::evm::push0(); + ffi::evm::mstore8(); + + // write key to memory + key.push(); + ffi::asm::push_u8(0x20); + ffi::evm::mstore(); + + // hash key + ffi::asm::push_u8(0x40); + ffi::evm::push0(); + ffi::evm::keccak256(); } } - -macro_rules! impl_mapping_key { - (($ty:ident, $size:expr)) => { - impl MappingKey for $ty {} - }; - ($len:expr) => { - impl MappingKey for [u8; $len] {} - }; - ($($ty:tt),+) => { - $(impl_mapping_key!($ty);)+ - }; -} - -impl_mapping_key! { - (i8, 1), (u8, 1), - (i16, 2), (u16, 2), - (i32, 4), (u32, 4), - (i64, 4), (u64, 4), - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, - 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 -} diff --git a/zink/src/storage/mod.rs b/zink/src/storage/mod.rs index b2363bb4f..8eaef9326 100644 --- a/zink/src/storage/mod.rs +++ b/zink/src/storage/mod.rs @@ -1,15 +1,11 @@ //! Zink storage implementation. use crate::{ffi, Asm}; -pub use { - array::StorageArray, - kv::Storage, - mapping::{Mapping, MappingKey}, -}; +pub use {dkmapping::DoubleKeyMapping, mapping::Mapping, value::Storage}; -mod array; -mod kv; +mod dkmapping; mod mapping; +mod value; /// Interface for the value of kv based storage pub trait StorageValue: Asm { @@ -22,12 +18,3 @@ impl StorageValue for i32 { unsafe { ffi::asm::sload_i32() } } } - -/// Sub index of storage -pub trait StorageIndex { - /// Increment the index - fn increment(); - - /// Load index to stack - fn load(); -} diff --git a/zink/src/storage/kv.rs b/zink/src/storage/value.rs similarity index 100% rename from zink/src/storage/kv.rs rename to zink/src/storage/value.rs