From f03cab8e11bca5ad02196d0262f420a3f7686f19 Mon Sep 17 00:00:00 2001 From: clearloop Date: Tue, 24 Sep 2024 17:02:20 +0800 Subject: [PATCH] feat(zink): introduce storage mapping (#233) * feat(zink): introduce storage registry * docs(storage): design of storage * feat(zink): introduce mapping interface * feat(zink): expand module storage * feat(zink): mainly use trait for kv storage * feat(storage): introduce mapping interface * refactor(zink): declare storage on exposed struct * feat(zink): proc-macro for mapping storage * feat(zink): add tests of mapping * feat(zink): mapping tests in bytecode level * feat(zink): complete the test of mappings * feat(zink): clean unused work * feat(zink): bump version to 0.1.11 --- Cargo.lock | 21 +++++---- Cargo.toml | 25 +++++----- abi/src/selector.rs | 4 +- codegen/src/asm.rs | 10 ++-- codegen/src/wasm/host.rs | 9 ++-- docs/SUMMARY.md | 7 +-- docs/compiler/storage.md | 28 +++++++++++ evm/abi/src/abi.rs | 10 ++-- evm/opcodes/Cargo.toml | 2 +- evm/opcodes/src/shanghai.rs | 2 +- examples/addition.rs | 2 +- examples/constructor.rs | 4 +- examples/mapping.rs | 60 +++++++++++++++++++++++ examples/storage.rs | 30 ++---------- tests/storage.rs | 75 ++++++++++++++++++++++++++++- zink/codegen/src/lib.rs | 39 +++++---------- zink/codegen/src/storage.rs | 94 ++++++++++++++++++++++++------------- zink/src/asm.rs | 2 +- zink/src/ffi/evm.rs | 12 +++++ zink/src/lib.rs | 6 ++- zink/src/primitives.rs | 10 ++++ zink/src/storage/array.rs | 19 ++++++++ zink/src/storage/kv.rs | 23 +++++++++ zink/src/storage/mapping.rs | 71 +++++++++++++++++++++++++++- zink/src/storage/mod.rs | 33 +++++++++---- zint/src/contract.rs | 14 +++++- zint/src/evm.rs | 32 +++++++++++-- 27 files changed, 496 insertions(+), 148 deletions(-) create mode 100644 docs/compiler/storage.md create mode 100644 examples/mapping.rs create mode 100644 zink/src/primitives.rs create mode 100644 zink/src/storage/array.rs create mode 100644 zink/src/storage/kv.rs diff --git a/Cargo.lock b/Cargo.lock index a3b63b52d..b631c9a12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -808,7 +808,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "elko" -version = "0.1.10" +version = "0.1.11" dependencies = [ "anyhow", "cargo_metadata", @@ -889,7 +889,7 @@ checksum = "1b241177c7107d9829286c2ffdc5eee98d992d6356f3515e7f412f988b1a72fd" [[package]] name = "evm-opcodes" -version = "0.0.3" +version = "0.0.4" dependencies = [ "paste", ] @@ -2552,7 +2552,7 @@ dependencies = [ [[package]] name = "zabi" -version = "0.1.10" +version = "0.1.11" dependencies = [ "hex", "postcard", @@ -2606,7 +2606,7 @@ dependencies = [ [[package]] name = "zingen" -version = "0.1.10" +version = "0.1.11" dependencies = [ "anyhow", "evm-opcodes", @@ -2622,10 +2622,13 @@ dependencies = [ [[package]] name = "zink" -version = "0.1.10" +version = "0.1.11" dependencies = [ "anyhow", + "evm-opcodes", + "hex", "paste", + "tracing", "zink-codegen", "zinkc-filetests", "zint", @@ -2633,7 +2636,7 @@ dependencies = [ [[package]] name = "zink-codegen" -version = "0.1.10" +version = "0.1.11" dependencies = [ "proc-macro2", "quote", @@ -2643,7 +2646,7 @@ dependencies = [ [[package]] name = "zinkc" -version = "0.1.10" +version = "0.1.11" dependencies = [ "anyhow", "ccli", @@ -2664,7 +2667,7 @@ dependencies = [ [[package]] name = "zinkc-filetests" -version = "0.1.10" +version = "0.1.11" dependencies = [ "anyhow", "cargo_metadata", @@ -2679,7 +2682,7 @@ dependencies = [ [[package]] name = "zint" -version = "0.1.10" +version = "0.1.11" dependencies = [ "anyhow", "cargo_metadata", diff --git a/Cargo.toml b/Cargo.toml index 73f42744d..d422cbb43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.1.10" +version = "0.1.11" authors = ["clearloop"] edition = "2021" license = "GPL-3.0-only" @@ -43,7 +43,7 @@ serde_json = "1.0.113" smallvec = "1.13.1" syn = { version = "2.0.48", features = [ "full" ] } thiserror = "1.0.56" -tiny-keccak = "2.0.2" +tiny-keccak = { version = "2.0.2", features = ["keccak"], default-features = false } toml = "0.8.9" tracing = "0.1.40" tracing-subscriber = "0.3.18" @@ -52,18 +52,18 @@ wasmparser = "0.121.0" wat = "1.0.85" ## EVM packages -opcodes = { package = "evm-opcodes", path = "evm/opcodes", version = "=0.0.3", features = [ "data" ] } +opcodes = { package = "evm-opcodes", path = "evm/opcodes", version = "=0.0.4", features = [ "data" ] } sol-abi = { path = "evm/abi", version = "=0.0.1" } ## Zink packages -elko = { path = "elko", version = "0.1.10" } -filetests = { package = "zinkc-filetests", path = "compiler/filetests", version = "0.1.10" } -zabi = { path = "abi", version = "0.1.10" } -zingen = { path = "codegen", version = "0.1.10" } -zink = { path = ".", version = "0.1.10" } -zink-codegen = { path = "zink/codegen", version = "0.1.10" } -zinkc = { path = "compiler", version = "0.1.10" } -zint = { path = "zint", version = "0.1.10" } +elko = { path = "elko", version = "0.1.11" } +filetests = { package = "zinkc-filetests", path = "compiler/filetests", version = "0.1.11" } +zabi = { path = "abi", version = "0.1.11" } +zingen = { path = "codegen", version = "0.1.11" } +zink = { path = ".", version = "0.1.11" } +zink-codegen = { path = "zink/codegen", version = "0.1.11" } +zinkc = { path = "compiler", version = "0.1.11" } +zint = { path = "zint", version = "0.1.11" } [workspace.metadata.conta] packages = [ @@ -102,4 +102,7 @@ zink-codegen.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] anyhow.workspace = true filetests.workspace = true +opcodes = { workspace = true, features = ["data"] } +tracing.workspace = true zint.workspace = true +hex.workspace = true diff --git a/abi/src/selector.rs b/abi/src/selector.rs index 7f5e15f34..a41a9a483 100644 --- a/abi/src/selector.rs +++ b/abi/src/selector.rs @@ -2,11 +2,11 @@ #![cfg(feature = "selector")] use crate::Abi; -use tiny_keccak::{Hasher, Sha3}; +use tiny_keccak::{Hasher, Keccak}; /// Generate a keccak hash of the input (sha3) pub fn keccak256(input: &[u8]) -> [u8; 32] { - let mut hasher = Sha3::v256(); + let mut hasher = Keccak::v256(); let mut output = [0; 32]; hasher.update(input); hasher.finalize(&mut output); diff --git a/codegen/src/asm.rs b/codegen/src/asm.rs index 028b0b045..bb57f4991 100644 --- a/codegen/src/asm.rs +++ b/codegen/src/asm.rs @@ -46,11 +46,11 @@ impl Assembler { return Ok(()); } - tracing::trace!( - "increment stack pointer {}({items}) -> {}", - self.sp, - self.sp + items - ); + // tracing::trace!( + // "increment stack pointer {}({items}) -> {}", + // self.sp, + // self.sp + items + // ); self.sp += items; // TODO: fix this limitation: should be 1024. (#127) diff --git a/codegen/src/wasm/host.rs b/codegen/src/wasm/host.rs index 8ab2e1f2d..8ee67c9c8 100644 --- a/codegen/src/wasm/host.rs +++ b/codegen/src/wasm/host.rs @@ -48,11 +48,10 @@ impl TryFrom<(&str, &str)> for HostFunc { Ok(Self::NoOp) } } - ("evm", name) => { - Ok(Self::Evm(OpCode::from_str(name).map_err(|_| { - Error::HostFuncNotFound(module.into(), name.into()) - })?)) - } + ("evm", name) => Ok(Self::Evm(OpCode::from_str(name).map_err(|_| { + tracing::error!("Failed to load host function: {:?}", import); + Error::HostFuncNotFound(module.into(), name.into()) + })?)), ("zinkc", "emit_abi") => Ok(Self::EmitABI), _ => { tracing::warn!("Failed to load host function: {:?}", import); diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 4a37b203d..a83f17bd5 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -10,9 +10,6 @@ - [Log](./examples/log.md) - [Select](./examples/select.md) - [Storage](./examples/storage.md) -- [Command Line Tool](./cli/README.md) - - [elko](./cli/elko.md) - - [zinkc](./cli/zinkc.md) - [Styles](./styles/README.md) - [Compiler](./compiler/README.md) - [Arithmetic](./compiler/arithmetic.md) @@ -20,6 +17,7 @@ - [Control Flow](./compiler/control-flow.md) - [Locals](./compiler/locals.md) - [Recursion](./compiler/recursion.md) + - [Storage](./compiler/storage.md) - [Stability](./stability/README.md) - [v0.1.0](./stability/v0.1.0.md) - [Security](./security.md) @@ -27,6 +25,9 @@ - [Fibonacci](./benchmarks/fibonacci.md) - [Log](./benchmarks/log.md) - [Storage](./benchmarks/storage.md) +- [Command Line Tool](./cli/README.md) + - [elko](./cli/elko.md) + - [zinkc](./cli/zinkc.md) - [Contributing](./contributing/README.md) - [Architecture](./contributing/architecture.md) - [Building](./contributing/building.md) diff --git a/docs/compiler/storage.md b/docs/compiler/storage.md new file mode 100644 index 000000000..e316a91e8 --- /dev/null +++ b/docs/compiler/storage.md @@ -0,0 +1,28 @@ +# Storage + +The storage keys in Zink is slot based, for example, the first detected +storage in compilation will be using `0` as storage key. + +```solidity +// Loading storage at 0 +PUSH0 +SLOAD + +// Loading storage at 1 +PUSH1 0x01 +SLOAD +``` + +## Key-Value + +As mentioned above, all key-value pairs follows using number as storage key, however, the value +will be limited with 32 bytes, dynamic value like string is currently not supported. + +## Mapping + +Mapping keys are generated via `keccak256(slot, key)` + +## Array + +Similar to mappings, but the keys will be using `u32` / `u64` for indexing due to the optimization +on the wasm side in the zink compiler, which means, the max size of an array is `max(u64)`. diff --git a/evm/abi/src/abi.rs b/evm/abi/src/abi.rs index f62a075d1..c4a91f1ef 100644 --- a/evm/abi/src/abi.rs +++ b/evm/abi/src/abi.rs @@ -3,6 +3,9 @@ use crate::Arg; use core::{convert::Infallible, fmt, str::FromStr}; +#[cfg(feature = "syn")] +use quote::ToTokens; + #[cfg(not(feature = "std"))] use crate::std::{String, ToString, Vec}; @@ -28,9 +31,9 @@ impl From<&syn::Signature> for Abi { .inputs .iter() .filter_map(|arg| { - if let syn::FnArg::Typed(syn::PatType { ty, .. }) = arg { + if let syn::FnArg::Typed(syn::PatType { pat, ty, .. }) = arg { Some(Arg { - name: sig.ident.to_string(), + name: pat.to_token_stream().to_string(), ty: crate::Param::from(ty), }) } else { @@ -41,7 +44,8 @@ impl From<&syn::Signature> for Abi { let outputs = if let syn::ReturnType::Type(_, ty) = &sig.output { vec![Arg { - name: sig.ident.to_string(), + // TODO: how to name the output? + name: "output".into(), ty: crate::Param::from(ty), }] } else { diff --git a/evm/opcodes/Cargo.toml b/evm/opcodes/Cargo.toml index f32e739f6..367692ac2 100644 --- a/evm/opcodes/Cargo.toml +++ b/evm/opcodes/Cargo.toml @@ -2,7 +2,7 @@ name = "evm-opcodes" description = "Rust implementation of EVM opcode" documentation = "https://docs.rs/evm-opcodes" -version = "0.0.3" +version = "0.0.4" authors.workspace = true edition.workspace = true homepage.workspace = true diff --git a/evm/opcodes/src/shanghai.rs b/evm/opcodes/src/shanghai.rs index 01428ba0c..5c3cf8986 100644 --- a/evm/opcodes/src/shanghai.rs +++ b/evm/opcodes/src/shanghai.rs @@ -30,7 +30,7 @@ opcodes! { (0x1b, SHL, 3, 2, 1, "Left shift operation", Constantinople, ComparisonBitwiseLogic), (0x1c, SHR, 3, 2, 1, "Logical right shift operation", Constantinople, ComparisonBitwiseLogic), (0x1d, SAR, 3, 2, 1, "Arithmetic (signed) right shift operation", Constantinople, ComparisonBitwiseLogic), - (0x20, SHA3, 30, 2, 1, "Compute Keccak-256 hash.", Frontier, StopArithmetic), + (0x20, KECCAK256, 30, 2, 1, "Compute Keccak-256 hash.", Frontier, StopArithmetic), (0x30, ADDRESS, 2, 0, 1, "Get address of currently executing account.", Frontier, EnvironmentalInformation), (0x31, BALANCE, 20, 1, 1, "Get balance of the given account.", Frontier, EnvironmentalInformation), (0x32, ORIGIN, 2, 0, 1, "Get execution origination address.", Frontier, EnvironmentalInformation), diff --git a/examples/addition.rs b/examples/addition.rs index ee618a2fe..b64c2c597 100644 --- a/examples/addition.rs +++ b/examples/addition.rs @@ -22,7 +22,7 @@ fn test() -> anyhow::Result<()> { &1u64.to_bytes32(), &2u64.to_bytes32(), ])?; - assert_eq!(info.ret, 3u64.to_bytes32()); + assert_eq!(info.ret, 3u64.to_bytes32()); Ok(()) } diff --git a/examples/constructor.rs b/examples/constructor.rs index 72f02f98e..6a314d3c4 100644 --- a/examples/constructor.rs +++ b/examples/constructor.rs @@ -13,8 +13,8 @@ use zink::Storage; /// Storage key is taken based on macro order /// (e.g this macro is first and only in this project, /// so it will take 0x0 contract storage key) -#[zink::storage] -pub type Counter = i32; +#[zink::storage(i32)] +pub struct Counter; /// Get value from the storage. #[zink::external] diff --git a/examples/mapping.rs b/examples/mapping.rs new file mode 100644 index 000000000..9efac021c --- /dev/null +++ b/examples/mapping.rs @@ -0,0 +1,60 @@ +//! Storage example. +#![cfg_attr(target_arch = "wasm32", no_std)] +#![cfg_attr(target_arch = "wasm32", no_main)] + +extern crate zink; + +use zink::Mapping as _; + +/// Counter with value type `i32` +#[zink::storage(i32 => i32)] +pub struct Mapping; + +/// Set the mapping +#[zink::external] +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<()> { + use zint::{Bytes32, Contract}; + + let mut contract = Contract::search("mapping")?.compile()?; + let mut evm = contract.deploy()?.commit(true); + + let key = 0x00; + let value: i32 = 0x42; + + // set value to storage + let calldata = contract.encode(&[ + b"mset(int32,int32)".to_vec(), + value.to_bytes32().to_vec(), + key.to_bytes32().to_vec(), + ])?; + 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!( + evm.storage(contract.address, storage_key)?, + value.to_bytes32(), + ); + + // get value from storage + let calldata = contract.encode(&[b"mget(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 b477db0e4..65b3364e7 100644 --- a/examples/storage.rs +++ b/examples/storage.rs @@ -6,22 +6,9 @@ extern crate zink; use zink::Storage; -/// It gets expanded to 'Counter' struct -/// that implements zink::Storage trait -/// (::set and ::get) -/// -/// Storage key is taken based on macro order -/// (e.g this macro is first and only in this project, -/// so it will take 0x0 contract storage key) -#[zink::storage] -pub type Counter = i32; - -/// Set value to the storage and get it. -#[zink::external] -pub fn set_and_get(value: i32) -> i32 { - Counter::set(value); - Counter::get() -} +/// Counter with value type `i32` +#[zink::storage(i32)] +pub struct Counter; /// set value to the storage. #[zink::external] @@ -39,7 +26,7 @@ pub fn get() -> i32 { fn main() {} #[test] -fn selector() -> anyhow::Result<()> { +fn value() -> anyhow::Result<()> { use zint::{Bytes32, Contract, U256}; let mut contract = Contract::search("storage")?.compile()?; @@ -57,14 +44,5 @@ fn selector() -> anyhow::Result<()> { assert_eq!(info.ret, 0.to_bytes32()); } - { - let key = 0; - let value = 42; - let info = - contract.execute(&[b"set_and_get(int32)".to_vec(), value.to_bytes32().to_vec()])?; - assert_eq!(info.ret, value.to_bytes32()); - assert_eq!(info.storage.get(&U256::from(key)), Some(&U256::from(value))); - } - Ok(()) } diff --git a/tests/storage.rs b/tests/storage.rs index 78e10ffb4..30734eea1 100644 --- a/tests/storage.rs +++ b/tests/storage.rs @@ -3,7 +3,7 @@ use anyhow::Result; use filetests::Test; -use zint::{Bytes32, Contract, U256}; +use zint::{keccak256, Bytes32, Contract, EVM, U256}; #[test] fn store() -> Result<()> { @@ -39,3 +39,76 @@ fn basic() -> Result<()> { Ok(()) } + +#[test] +fn mapping() -> Result<()> { + use opcodes::ShangHai; + use zint::Bytes32; + + zint::setup_logger(); + + let hashing: Vec = vec![ + // storage value + ShangHai::PUSH1, + ShangHai::Data(0x42), + // Load storage slot + // + // write index to memory + ShangHai::PUSH0, + ShangHai::PUSH0, + ShangHai::MSTORE8, + // write key to memory + ShangHai::PUSH0, + ShangHai::PUSH1, + ShangHai::Data(0x01), + ShangHai::MSTORE, + // hash key + ShangHai::PUSH1, + ShangHai::Data(0x20), + ShangHai::PUSH0, + ShangHai::KECCAK256, + // write storage + ShangHai::SSTORE, + // Load storage slot + // + // write index to memory + ShangHai::PUSH0, + ShangHai::PUSH0, + ShangHai::MSTORE8, + // write key to memory + ShangHai::PUSH0, + ShangHai::PUSH1, + ShangHai::Data(0x01), + ShangHai::MSTORE, + // hash key + ShangHai::PUSH1, + ShangHai::Data(0x20), + ShangHai::PUSH0, + ShangHai::KECCAK256, + // load storage to stack + ShangHai::SLOAD, + // write storage to memory + ShangHai::PUSH0, + ShangHai::MSTORE, + // return + ShangHai::PUSH1, + ShangHai::Data(0x20), + ShangHai::PUSH0, + ShangHai::RETURN, + ] + .into_iter() + .map(Into::into) + .collect(); + + let info = EVM::interp(&hashing, &[])?; + tracing::debug!("bytecode: {}", hex::encode(&hashing)); + + let key = keccak256(&[0; 0x20]); + assert_eq!( + info.storage.get(&U256::from_be_bytes(key)), + Some(&U256::from_be_bytes(0x42.to_bytes32())), + "{info:#?}" + ); + assert_eq!(0x42.to_bytes32().to_vec(), info.ret); + Ok(()) +} diff --git a/zink/codegen/src/lib.rs b/zink/codegen/src/lib.rs index 668c72108..7a6f319c6 100644 --- a/zink/codegen/src/lib.rs +++ b/zink/codegen/src/lib.rs @@ -1,7 +1,7 @@ //! Code generation library for the zink API use proc_macro::TokenStream; -use syn::{parse_macro_input, DeriveInput, ItemFn, ItemType}; +use syn::{parse_macro_input, DeriveInput, ItemFn, ItemStruct}; mod event; mod selector; @@ -37,38 +37,21 @@ pub fn event(input: TokenStream) -> TokenStream { event::parse(input) } -/// Order-based storage macro. -/// Currently only i32 is supported +/// Declare on-chain storage /// /// ```ignore -/// use zink::storage; +/// /// storage value +/// #[zink::storage(i32)] +/// pub struct Counter; /// -/// #[storage] -/// pub type Counter = i32; -/// ``` -/// -/// will generate: -/// -/// ```ignore -/// struct Counter; -/// -/// impl zink::Storage for Counter { -/// // if this macro were the second one in the project, this key would be 1i32 -/// const STORAGE_KEY: i32 = 0i32; -/// -/// fn get() -> i32 { -/// zink::ffi::evm::sload(Self::STORAGE_KEY) -/// } -/// -/// fn set(value: i32) { -/// zink::ffi::evm::sstore(Self::STORAGE_KEY, value); -/// } -/// } +/// /// storage mapping +/// #[zink::storage(i32 => i32)] +/// pub struct Mapping; /// ``` #[proc_macro_attribute] -pub fn storage(_args: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemType); - storage::parse(input).into() +pub fn storage(attr: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + storage::parse(attr.into(), input) } /// Mark the function as an external entry point. diff --git a/zink/codegen/src/storage.rs b/zink/codegen/src/storage.rs index 43549287d..f549ad81e 100644 --- a/zink/codegen/src/storage.rs +++ b/zink/codegen/src/storage.rs @@ -1,11 +1,13 @@ extern crate proc_macro; -use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; -use std::sync::atomic::{AtomicI32, Ordering::Relaxed}; -use syn::ItemType; +use proc_macro2::{TokenStream, TokenTree}; +use quote::quote; +use std::{cell::RefCell, collections::HashSet}; +use syn::ItemStruct; -static IOTA: AtomicI32 = AtomicI32::new(0); +thread_local! { + static STORAGE_REGISTRY: RefCell> = RefCell::new(HashSet::new()); +} /// Parse storage attribute. /// @@ -15,37 +17,65 @@ static IOTA: AtomicI32 = AtomicI32::new(0); /// /// For the cases in EVM, it doesn't matter it returns pointer /// since the value will be left on stack anyway. -pub fn parse(input: ItemType) -> TokenStream { - let name = input.ident; - let ty = input.ty.to_token_stream(); +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() +} + +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 zink::storage::Storage for #name { + type Value = #ty; + const STORAGE_SLOT: i32 = #slot; + } + }; + + expanded +} + +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"); + } + } - // Temporary solution, we'll switch to 32 byte storage keys later - let key = IOTA.fetch_add(1, Relaxed); + let key = &ty[0]; + let value = &ty[3]; + let name = is.ident.clone(); + let slot = storage_slot(name.to_string()); let expanded = quote! { - #[doc = concat!(" Storage ", stringify!($variable_name))] - struct #name; - - impl zink::Storage<#ty> for #name { - const STORAGE_KEY: i32 = #key; - - fn get() -> #ty { - zink::Asm::push(Self::STORAGE_KEY); - unsafe { - paste::paste! { - zink::ffi::asm::[< sload_ #ty >]() - } - } - } - - fn set(value: #ty) { - zink::Asm::push(value); - zink::Asm::push(Self::STORAGE_KEY); - unsafe { - zink::ffi::evm::sstore(); - } - } + #is + + impl zink::storage::Mapping for #name { + const STORAGE_SLOT: i32 = #slot; + + type Key = #key; + type Value = #value; } }; expanded } + +fn storage_slot(name: String) -> i32 { + STORAGE_REGISTRY.with_borrow_mut(|r| { + let key = r.len(); + if !r.insert(name.clone()) { + panic!("Storage {name} has already been declared"); + } + + key + }) as i32 +} diff --git a/zink/src/asm.rs b/zink/src/asm.rs index b26b94bdf..a2a8f524f 100644 --- a/zink/src/asm.rs +++ b/zink/src/asm.rs @@ -4,7 +4,7 @@ use crate::ffi; use paste::paste; /// Types implemented this trait are able to be pushed on stack. -pub trait Asm { +pub trait Asm: Copy { /// Push self on the stack. fn push(self); } diff --git a/zink/src/ffi/evm.rs b/zink/src/ffi/evm.rs index ffa2ffcc9..398ec523c 100644 --- a/zink/src/ffi/evm.rs +++ b/zink/src/ffi/evm.rs @@ -108,6 +108,18 @@ extern "C" { /// Load a value from the storage pub fn sload(); + /// Save word to memory + pub fn mstore(); + + /// Save byte to memory + pub fn mstore8(); + + /// Load word from memory + pub fn mload(); + + /// Compute Keccak-256 hash + pub fn keccak256(); + /// Append log record with no topics pub fn log0(name: &'static [u8]); diff --git a/zink/src/lib.rs b/zink/src/lib.rs index 00a751054..84bb3adac 100644 --- a/zink/src/lib.rs +++ b/zink/src/lib.rs @@ -5,9 +5,11 @@ mod asm; mod event; pub mod ffi; -mod storage; +pub mod primitives; +pub mod storage; -pub use self::{asm::Asm, event::Event, storage::Storage}; +pub use self::{asm::Asm, event::Event}; +pub use storage::{Mapping, Storage}; pub use zink_codegen::{external, storage, Event}; // Panic hook implementation diff --git a/zink/src/primitives.rs b/zink/src/primitives.rs new file mode 100644 index 000000000..a09b51017 --- /dev/null +++ b/zink/src/primitives.rs @@ -0,0 +1,10 @@ +//! Zink primitive types + +/// Fixed-sized 32 bytes array +pub type Bytes32 = [u8; 32]; + +/// Account address +pub type Address = Bytes32; + +/// Fixed-sized 32 bytes array +pub type Hash = Bytes32; diff --git a/zink/src/storage/array.rs b/zink/src/storage/array.rs new file mode 100644 index 000000000..239e4b40f --- /dev/null +++ b/zink/src/storage/array.rs @@ -0,0 +1,19 @@ +//! 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/kv.rs b/zink/src/storage/kv.rs new file mode 100644 index 000000000..8338084b0 --- /dev/null +++ b/zink/src/storage/kv.rs @@ -0,0 +1,23 @@ +//! Key-Value storage +use crate::{ffi, storage::StorageValue, Asm}; + +/// Storage trait. Currently not for public use +pub trait Storage { + const STORAGE_SLOT: i32; + type Value: StorageValue + Asm; + + /// Get value from storage. + fn get() -> Self::Value { + Asm::push(Self::STORAGE_SLOT); + Self::Value::sload() + } + + /// Set value to storage. + fn set(value: Self::Value) { + value.push(); + Asm::push(Self::STORAGE_SLOT); + unsafe { + ffi::evm::sstore(); + } + } +} diff --git a/zink/src/storage/mapping.rs b/zink/src/storage/mapping.rs index b6113ccca..5c2200187 100644 --- a/zink/src/storage/mapping.rs +++ b/zink/src/storage/mapping.rs @@ -1 +1,70 @@ -//! Zink storage mapping implementation. +//! Storage Mapping + +use crate::{ffi, storage::StorageValue, Asm}; + +/// Storage mapping interface +pub trait Mapping { + const STORAGE_SLOT: i32; + + type Key: MappingKey; + type Value: StorageValue; + + /// Get value from storage key. + fn get(key: Self::Key) -> Self::Value { + key.load(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); + 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(); + } + } +} + +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 c8f034449..b2363bb4f 100644 --- a/zink/src/storage/mod.rs +++ b/zink/src/storage/mod.rs @@ -1,16 +1,33 @@ //! Zink storage implementation. -use crate::Asm; +use crate::{ffi, Asm}; +pub use { + array::StorageArray, + kv::Storage, + mapping::{Mapping, MappingKey}, +}; +mod array; +mod kv; mod mapping; -/// Storage trait. Currently not for public use -pub trait Storage { - const STORAGE_KEY: i32; +/// Interface for the value of kv based storage +pub trait StorageValue: Asm { + /// Load from storage + fn sload() -> Self; +} + +impl StorageValue for i32 { + fn sload() -> Self { + unsafe { ffi::asm::sload_i32() } + } +} - /// Get value from storage. - fn get() -> T; +/// Sub index of storage +pub trait StorageIndex { + /// Increment the index + fn increment(); - /// Set value to storage. - fn set(value: T); + /// Load index to stack + fn load(); } diff --git a/zint/src/contract.rs b/zint/src/contract.rs index 5e8de2c53..b53be3d03 100644 --- a/zint/src/contract.rs +++ b/zint/src/contract.rs @@ -16,6 +16,8 @@ pub struct Contract { pub wasm: Vec, /// Bytecode constructor pub constructor: Constructor, + /// Address in evm + pub address: [u8; 20], } impl From for Contract @@ -41,7 +43,6 @@ impl Contract { .finish(self.artifact.runtime_bytecode.clone().into()) .map(|v| v.to_vec())?; - tracing::debug!("bytecode: {}", hex::encode(&bytecode)); Ok(bytecode) } @@ -59,9 +60,19 @@ impl Contract { self.artifact = compiler.compile(&self.wasm)?; tracing::debug!("abi: {:#}", self.json_abi()?); + tracing::debug!("bytecode: {}", hex::encode(&self.artifact.runtime_bytecode)); Ok(self) } + /// Deploy self to evm + pub fn deploy<'e>(&mut self) -> Result> { + let mut evm = EVM::default(); + let info = evm.deploy(&self.bytecode()?)?; + + self.address.copy_from_slice(&info.address); + Ok(evm) + } + /// Load zink contract defined in the current /// package. /// @@ -91,6 +102,7 @@ impl Contract { calldata.extend_from_slice(&input.to_bytes32()); } + tracing::debug!("calldata: {}", hex::encode(&calldata)); Ok(calldata) } diff --git a/zint/src/evm.rs b/zint/src/evm.rs index c7da8b7cd..9d82c6f47 100644 --- a/zint/src/evm.rs +++ b/zint/src/evm.rs @@ -7,7 +7,7 @@ use revm::{ AccountInfo, Bytecode, Bytes, ExecutionResult, HaltReason, Log, Output, ResultAndState, SuccessReason, TransactTo, TxKind, U256, }, - Evm as Revm, InMemoryDB, + Database, Evm as Revm, InMemoryDB, }; use std::collections::HashMap; @@ -23,6 +23,8 @@ pub const CONTRACT: [u8; 20] = [1; 20]; /// Wrapper of full REVM pub struct EVM<'e> { inner: Revm<'e, (), InMemoryDB>, + /// If commit changes + commit: bool, } impl<'e> Default for EVM<'e> { @@ -31,8 +33,10 @@ impl<'e> Default for EVM<'e> { db.insert_account_info(ALICE.into(), AccountInfo::from_balance(U256::MAX)); let evm = Revm::<'e, (), EmptyDB>::builder().with_db(db).build(); - - Self { inner: evm } + Self { + inner: evm, + commit: false, + } } } @@ -45,13 +49,31 @@ impl<'e> EVM<'e> { .call(CONTRACT) } + /// Get storage from address and storage index + pub fn storage(&mut self, address: [u8; 20], key: [u8; 32]) -> Result<[u8; 32]> { + let db = self.inner.db_mut(); + Ok(db + .storage(address.into(), U256::from_be_bytes(key))? + .to_be_bytes()) + } + + /// If commit changes + pub fn commit(mut self, flag: bool) -> Self { + self.commit = flag; + self + } + /// Send transaction to the provided address. pub fn call(&mut self, to: [u8; 20]) -> Result { let to = TransactTo::Call(to.into()); self.inner.tx_mut().gas_limit = GAS_LIMIT; self.inner.tx_mut().transact_to = to; - let result = self.inner.transact().map_err(|e| anyhow!(e))?; - (result, to).try_into() + if self.commit { + self.inner.transact_commit()?.try_into() + } else { + let result = self.inner.transact().map_err(|e| anyhow!(e))?; + (result, to).try_into() + } } /// Interpret runtime bytecode with provided arguments