diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 25f382366..de6e0fc33 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,5 +3,29 @@ Resolves # ### Changes - [x] step 1 -- [ ] step 2 - [ ] ... + + + + diff --git a/Cargo.lock b/Cargo.lock index 8e63714fe..dcb8df2f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -943,6 +943,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09904adae26440d46daeacb5ed7922fee69d43ebf84d31e078cd49652cecd718" + [[package]] name = "fnv" version = "1.0.7" @@ -2646,6 +2652,7 @@ version = "0.1.11" dependencies = [ "anyhow", "evm-opcodes", + "fmt", "hex", "paste", "tiny-keccak", diff --git a/Cargo.toml b/Cargo.toml index 3415610a8..807a43dcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,18 @@ [profile] -dev = { panic = "abort"} +dev = { panic = "abort" } release = { panic = "unwind" } [workspace] members = [ - "abi", "codegen", "compiler", "compiler/filetests", - "elko", "evm/opcodes", "evm/abi", + "zink/abi", "zink/codegen", - "zint", + "zink/elko", + "zink/zint", ] resolver = "2" @@ -42,9 +42,11 @@ 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.77", features = [ "full" ] } +syn = { version = "2.0.77", features = ["full"] } thiserror = "1.0.56" -tiny-keccak = { version = "2.0.2", features = ["keccak"], default-features = false } +tiny-keccak = { version = "2.0.2", features = [ + "keccak", +], default-features = false } toml = "0.8.9" tracing = "0.1.40" tracing-subscriber = "0.3.18" @@ -53,18 +55,20 @@ wasmparser = "0.121.0" wat = "1.0.85" ## EVM packages -opcodes = { package = "evm-opcodes", path = "evm/opcodes", version = "=0.0.4", 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.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" } +zabi = { path = "zink/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" } +zint = { path = "zink/zint", version = "0.1.11" } [workspace.metadata.conta] packages = [ @@ -75,7 +79,7 @@ packages = [ "zint", "zink-codegen", "zink", - "elko" + "elko", ] # Zink Programming Language @@ -97,6 +101,7 @@ readme = "zink/README.md" path = "zink/src/lib.rs" [dependencies] +fmt = "0.1.0" paste.workspace = true zink-codegen.workspace = true diff --git a/README.md b/README.md index e876791da..be7b0b2d5 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,69 @@ -# The Zink Project +# The Zink Language > [!CAUTION] > -> This project is still under active development, plz DO NOT use it in production. +> This project is still under active development, please DO NOT use it in production. [![zink][version-badge]][version-link] [![ci][ci-badge]][ci-link] [![telegram][telegram-badge]][telegram-group] -[The Zink project][book] mainly provides a singlepass compiler `zinkc` which compiles -WASM to EVM bytecode, the source code of your smart contracts could be any language you like! +Welcome to the Zink Language! [Bounty issues](https://zink-lang.org/budgets) are now available, join the development of Zink by reading the [book](https://zink-lang.org/). -```mermaid -flowchart LR - R{{Rust}} --> W(WebAssembly) - O[...] --> W - W --> Z{Zink Compiler} - Z --> V[(EVM)] -``` +```rust +//! ERC20 Example (WIP) +#[zink::contract] +pub struct ERC20; + +#[zink::calls] +impl ERC20 { + /// VMs that zink supports + pub fn support() -> [zink::String; 4] { + ["EVM", "WASM", "RISC-V", "...OTHER_VMS"] + } +} -Here we highly recommend you to choose `rust` as the language of your smart contracts -which will unlock all of the following features: +#[zink::interface] +impl ERC20 for ERC20 { + fn name() -> zink::String { + "Zink Language".to_string() + } +} +``` -- **Safe**: `rustc` is watching you! Furthermore, after compiling your rust code to WASM, - `zinkc` will precompute all of the stack and memory usage in your contracts to ensure they - are safe in EVM bytecode as well! +- **Safe**: `rustc` monitors your code! -- **High Performance**: The optimizations are provided by the three of `rustc`, `wasm-opt` - and `zinkc`, your contracts will have the smallest size with **strong performance** in EVM - bytecode at the end! +- **Efficient**: Efficient EVM bytecode from `rustc`, `wasm-opt`, and `zinkc`. -- **Compatible**: All of the `no_std` libraries in rust are your libraries, you can use your - solidity contracts as part of your zink contracts and your zink contracts as part of your - solidity contracts :) +- **Modular**: Upload and download your contract components via `crates.io`. -- **Easy Debugging**: Developing your smart contracts with only one programming language! - zink will provide everything you need for developing your contracts officially based on the - stable projects in rust like the `foundry` tools. +- **Rusty**: All of the rust tools are available for your contracts! Run `cargo install zinkup` to install the toolchain! -## Fibonacci Example - -| fib(n) | Zink | Solidity@0.8.21 | -| ------ | ---- | --------------- | -| 0 | 110 | 614 | -| 1 | 110 | 614 | -| 2 | 262 | 1322 | -| 3 | 414 | 2030 | -| 4 | 718 | 3446 | -| 5 | 1174 | 5570 | - -```rust -/// Calculates the nth fibonacci number using recursion. -#[no_mangle] -pub extern "C" fn recursion(n: usize) -> usize { - if n < 2 { - n - } else { - recursion(n - 1) + recursion(n - 2) - } -} -``` - -As an example for the benchmark, calculating fibonacci sequence with recursion, missed -vyper because it doesn't support recursion...Zink is 5x fast on this, but it is mainly -caused by our current implementation is not completed yet ( missing logic to adapt to more -situations ), let's stay tuned for `v0.3.0`. +## Testing & Development -## Donation +| Command | Description | +| ---------- | ---------------------- | +| `cargo cc` | Clippy all packages | +| `cargo tt` | Run all tests | +| `cargo be` | Build all examples | +| `cargo te` | Run tests for examples | -After completing the ERC20 implementation, Zink will focus on MEV logic since everything could -be even more compact and realistic from this dark forest. +We're using `cargo-nextest` for testing, the commands above are described in [.cargo/config.toml](.cargo/config.toml). -Zink is now moving forward without any grants or backups, if you like this dreaming project, -please feel free to reach out, would be appreciated for any opportunities ^ ^ +## Special Thanks -- ETH: `0xf0306047Fa598fe95502f466aeb49b68dd94365B` -- SOL: `AZGXAerErfwVzJkiSR8moVPZxe1nEhvjdkvxQ7qR6Yst` +- [MegaETH](https://github.com/megaeth-labs) for the funding and trust! +- [revm](https://github.com/bluealloy/revm) for the EVM in rust! ## LICENSE GPL-3.0-only -[book]: https://docs.zink-lang.org/ +[book]: https://zink-lang.org/ [telegram-badge]: https://img.shields.io/endpoint?label=chat&style=flat&url=https%3A%2F%2Fmogyo.ro%2Fquart-apis%2Ftgmembercount%3Fchat_id%3Dzinklang [telegram-group]: https://t.me/zinklang [version-badge]: https://img.shields.io/crates/v/zinkc diff --git a/codegen/src/asm.rs b/codegen/src/asm.rs index 088d8e6a9..8ab558e43 100644 --- a/codegen/src/asm.rs +++ b/codegen/src/asm.rs @@ -3,7 +3,9 @@ //! TODO: refactor this module with Result as outputs. (issue-21) use crate::{Buffer, Error, Result}; -use opcodes::{for_each_shanghai_operator, OpCode as _, ShangHai as OpCode}; +use opcodes::{for_each_cancun_operator, Cancun as OpCode, OpCode as _}; + +const MAX_STACK_SIZE: u16 = 1024; /// Low level assembler implementation for EVM. #[derive(Default, Clone, Debug)] @@ -18,8 +20,8 @@ pub struct Assembler { gas: u128, /// Memory pointer for byte offset. pub mp: usize, - /// Stack pointer, maximum 1024 items. - pub sp: u8, + /// Stack pointer, maximum `MAX_STACK_SIZE` items. + pub sp: u16, } impl Assembler { @@ -41,7 +43,7 @@ impl Assembler { } /// Increment stack pointer - pub fn increment_sp(&mut self, items: u8) -> Result<()> { + pub fn increment_sp(&mut self, items: u16) -> Result<()> { if items == 0 { return Ok(()); } @@ -51,18 +53,20 @@ impl Assembler { self.sp, self.sp + items ); - self.sp += items; + self.sp = self + .sp + .checked_add(items) + .ok_or(Error::StackOverflow(self.sp, items))?; - // TODO: fix this limitation: should be 1024. (#127) - if self.sp > 254 { - return Err(Error::StackOverflow(self.sp)); + if self.sp > MAX_STACK_SIZE { + return Err(Error::StackOverflow(self.sp, items)); } Ok(()) } /// Decrement stack pointer - pub fn decrement_sp(&mut self, items: u8) -> Result<()> { + pub fn decrement_sp(&mut self, items: u16) -> Result<()> { if items == 0 { return Ok(()); } @@ -118,10 +122,10 @@ impl Assembler { /// the stack usages. pub fn emit_op(&mut self, opcode: OpCode) -> Result<()> { tracing::trace!("emit opcode: {:?}", opcode); - self.decrement_sp(opcode.stack_in() as u8)?; + self.decrement_sp(opcode.stack_in())?; self.emit(opcode.into()); self.increment_gas(opcode.gas().into()); - self.increment_sp(opcode.stack_out() as u8)?; + self.increment_sp(opcode.stack_out())?; Ok(()) } @@ -141,5 +145,5 @@ macro_rules! impl_opcodes { /// Basic instruction implementations impl Assembler { - for_each_shanghai_operator!(impl_opcodes); + for_each_cancun_operator!(impl_opcodes); } diff --git a/codegen/src/codegen/dispatcher.rs b/codegen/src/codegen/dispatcher.rs index ceae36c57..628bfcd51 100644 --- a/codegen/src/codegen/dispatcher.rs +++ b/codegen/src/codegen/dispatcher.rs @@ -75,7 +75,7 @@ impl Dispatcher { self.asm.increment_sp(1)?; // Prepare the `PC` of the callee function. - self.table.call(self.asm.pc_offset(), func); + self.table.call(self.asm.pc(), func); if last { self.asm._swap1()?; diff --git a/codegen/src/codegen/function.rs b/codegen/src/codegen/function.rs index 380983fab..3b5596adb 100644 --- a/codegen/src/codegen/function.rs +++ b/codegen/src/codegen/function.rs @@ -137,7 +137,7 @@ impl Function { /// Finish code generation. pub fn finish(self, jump_table: &mut JumpTable, pc: u16) -> Result { let sp = self.masm.sp(); - if !self.is_main && self.abi.is_none() && self.masm.sp() != self.ty.results().len() as u8 { + if !self.is_main && self.abi.is_none() && self.masm.sp() != self.ty.results().len() as u16 { return Err(Error::StackNotBalanced(sp)); } diff --git a/codegen/src/control.rs b/codegen/src/control.rs index 418c829f4..61d342431 100644 --- a/codegen/src/control.rs +++ b/codegen/src/control.rs @@ -38,7 +38,7 @@ pub struct ControlStackFrame { result: BlockType, /// Original stack pointer. - pub original_sp: u8, + pub original_sp: u16, } impl ControlStackFrame { @@ -46,7 +46,7 @@ impl ControlStackFrame { pub fn new( ty: ControlStackFrameType, original_pc_offset: u16, - original_sp: u8, + original_sp: u16, result: BlockType, ) -> Self { Self { diff --git a/codegen/src/jump/mod.rs b/codegen/src/jump/mod.rs index bf7a83f05..bf5c38026 100644 --- a/codegen/src/jump/mod.rs +++ b/codegen/src/jump/mod.rs @@ -16,8 +16,6 @@ mod target; /// Represents the different types of jumps in the program. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Jump { - /// Offset to the program counter. - Offset(u16), /// Jump to a specific label, which corresponds to the original program counter. Label(u16), /// Jump to a function identified by its index. @@ -29,7 +27,6 @@ pub enum Jump { impl Display for Jump { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Jump::Offset(offset) => write!(f, "Offset(0x{offset:x})"), Jump::Label(offset) => write!(f, "Label(0x{offset:x})"), Jump::Func(index) => write!(f, "Func({index})"), Jump::ExtFunc(_) => write!(f, "ExtFunc"), @@ -43,14 +40,9 @@ impl Jump { matches!(self, Jump::Label { .. }) } - /// Checks if the target is a fixed offset of the program counter. - pub fn is_offset(&self) -> bool { - matches!(self, Jump::Offset(_)) - } - /// Checks if the target is a function call. pub fn is_call(&self) -> bool { - !self.is_label() && !self.is_offset() + !self.is_label() } } @@ -92,7 +84,6 @@ mod tests { // Register jumps with known offsets and labels table.register(0x10, Jump::Label(0x20)); // Jump to label at 0x20 - table.register(0x20, Jump::Offset(0x10)); // Offset jump forward by 0x10 table.register(0x30, Jump::Label(0x40)); // Jump to label at 0x40 assert_target_shift_vs_relocation(table) @@ -105,37 +96,144 @@ mod tests { // Simulate multiple functions calling _approve table.register(0x10, Jump::Label(0x100)); // approve() -> _approve table.register(0x20, Jump::Label(0x100)); // spend_allowance() -> _approve - table.register(0x60, Jump::Offset(0x30)); // _approve implementation assert_target_shift_vs_relocation(table) } #[test] - fn test_nested_internal_calls() -> anyhow::Result<()> { + fn test_nested_function_calls() -> anyhow::Result<()> { let mut table = JumpTable::default(); - // Simulate transfer_from calling both _spend_allowance and _transfer - table.register(0x10, Jump::Label(0x100)); // transfer_from -> _spend_allowance - table.register(0x100, Jump::Label(0x200)); // _spend_allowance -> _approve - table.register(0x20, Jump::Label(0x300)); // transfer_from -> _transfer - table.register(0x300, Jump::Label(0x400)); // _transfer -> _update + // Simulate ERC20's approve -> _approve call chain + table.register(0x100, Jump::Label(0x200)); // approve entry + table.register(0x110, Jump::Label(0x300)); // approve -> _approve + table.register(0x200, Jump::Label(0x400)); // _approve entry - assert_target_shift_vs_relocation(table) + let mut buffer = smallvec![0; table.max_target() as usize]; + table.relocate(&mut buffer)?; + + // Check if all jumps use correct PUSH instructions + assert_eq!(buffer[0x100], 0x61); // PUSH2 + assert_eq!(buffer[0x113], 0x61); // PUSH2 + assert_eq!(buffer[0x206], 0x61); // PUSH2 + + Ok(()) } - // FIXME: refactor offset handling (#280) - #[ignore] #[test] - fn test_conditional_jumps() -> anyhow::Result<()> { + fn test_label_call_interaction() -> anyhow::Result<()> { + init_tracing(); let mut table = JumpTable::default(); - // Simulate the conditional logic in _spend_allowance - table.register(0x10, Jump::Label(0x100)); // Entry point - table.register(0x20, Jump::Label(0x200)); // If branch - table.register(0x30, Jump::Label(0x300)); // Else branch - table.register(0x100, Jump::Offset(0x50)); // Condition check - table.register(0x200, Jump::Label(0x400)); // Call to _approve + table.func.insert(1, 0x317); + table.label(0x10, 0x12); + table.call(0x11, 1); - assert_target_shift_vs_relocation(table) + let mut buffer = smallvec![0; table.max_target() as usize]; + table.relocate(&mut buffer)?; + + assert_eq!(buffer[0x11], 0x17, "{buffer:?}"); + assert_eq!(buffer[0x14], 0x03, "{buffer:?}"); + assert_eq!(buffer[0x15], 0x1c, "{buffer:?}"); + Ok(()) + } + + #[test] + fn test_large_target_offset_calculation() -> anyhow::Result<()> { + let mut table = JumpTable::default(); + + // Register a jump with target < 0xff + table.register(0x10, Jump::Label(0x80)); + + // Register a jump with target > 0xff + table.register(0x20, Jump::Label(0x100)); + + // Register a jump with target > 0xfff + table.register(0x30, Jump::Label(0x1000)); + + let mut buffer = smallvec![0; table.max_target() as usize]; + table.relocate(&mut buffer)?; + + // Check if offsets are correctly calculated + // For target 0x80: PUSH1 (1 byte) + target (1 byte) + // For target 0x100: PUSH2 (1 byte) + target (2 bytes) + // For target 0x1000: PUSH2 (1 byte) + target (2 bytes) + assert_eq!(buffer[0x11], 0x88); // Small target + assert_eq!(buffer[0x23], 0x01); // First byte of large target + assert_eq!(buffer[0x24], 0x08); // Second byte of large target + assert_eq!(buffer[0x36], 0x10); // First byte of large target + assert_eq!(buffer[0x37], 0x08); // Second byte of large target + + Ok(()) + } + + #[test] + fn test_sequential_large_jumps() -> anyhow::Result<()> { + let mut table = JumpTable::default(); + + // Register multiple sequential jumps with increasing targets + // This mirrors the ERC20 pattern where we have many functions + for i in 0..20 { + let target = 0x100 + (i * 0x20); + table.register(0x10 + i, Jump::Label(target)); + } + + let mut buffer = smallvec![0; table.max_target() as usize]; + table.relocate(&mut buffer)?; + + // Check first jump (should use PUSH2) + assert_eq!(buffer[0x10], 0x61); // PUSH2 + assert_eq!(buffer[0x11], 0x01); // First byte + assert_eq!(buffer[0x12], 0x3c); // Second byte + assert_eq!(0x013c, 0x100 + 20 * 3); + + // Check last jump (should still use PUSH2 but with adjusted offset) + let last_idx = 0x10 + 19 + 19 * 3; + assert_eq!(buffer[last_idx], 0x61); // PUSH2 + assert_eq!(buffer[last_idx + 1], 0x03); // First byte should be larger + assert_eq!(buffer[last_idx + 2], 0x9c); // Second byte accounts for all previous jumps + assert_eq!(0x039c, 0x100 + 0x20 * 19 + 20 * 3); + + Ok(()) + } + + #[test] + fn test_dispatcher_jump_targets() -> anyhow::Result<()> { + let mut table = JumpTable::default(); + let selectors = 5; + + // Register jumps for each selector check + for i in 0..selectors { + let i = i as u16; + let check_pc = 0x10 + i * 0x20; + let target_pc = 0x100 + i * 0x40; + + // Register both the comparison jump and function jump + table.register(check_pc, Jump::Label(check_pc + 0x10)); + table.register(check_pc + 0x10, Jump::Label(target_pc)); + } + + let mut buffer = smallvec![0; table.max_target() as usize]; + table.relocate(&mut buffer)?; + + // Verify each selector's jump chain + let mut total_offset = 0; + for i in 0..selectors { + let check_pc = 0x10 + i * 0x20 + total_offset; + let check_pc_offset = if check_pc + 0x10 > 0xff { 3 } else { 2 }; + + let func_pc = check_pc + 0x10 + check_pc_offset; + + let check_jump = buffer[check_pc]; + let func_jump = buffer[func_pc]; + + assert_eq!(check_jump, if func_pc > 0xff { 0x61 } else { 0x60 }); + assert_eq!(func_jump, 0x61); + + // Update total offset for next iteration + total_offset += check_pc_offset + 3; + } + + Ok(()) } } diff --git a/codegen/src/jump/relocate.rs b/codegen/src/jump/relocate.rs index 9e2afff3f..bdc92148a 100644 --- a/codegen/src/jump/relocate.rs +++ b/codegen/src/jump/relocate.rs @@ -36,12 +36,7 @@ impl JumpTable { pc, self.code.offset() ); - let mut target = self.target(&jump)?; - - // If the jump is an offset, adjust the target accordingly. - if jump.is_offset() { - self.update_offset_target(pc, &mut target)?; - } + let target = self.target(&jump)?; tracing::debug!( "relocate: pc=0x{:x}, jump={:?}, target=0x{:x}", @@ -59,31 +54,6 @@ impl JumpTable { buffer.extend_from_slice(&self.code.finish()); Ok(()) } - - /// Relocate the target of an offset jump. - /// - /// This function adjusts the target program counter for jumps that are - /// represented as offsets. It modifies the target based on the original - /// program counter and checks for specific conditions that may require - /// further adjustments. - fn update_offset_target(&self, pc: u16, target: &mut u16) -> Result<()> { - // NOTE: If the target is offset, the return data is the offset instead of the PC. - *target += pc; - - // Check if the original program counter of the offset is greater than 0xff. - if *target > 0xff { - *target += 1; - } - - // Check if the offset of the embedded call is greater than 0xff. - if let Some((_, next_target)) = self.jump.first_key_value() { - if next_target.is_call() && self.target(next_target)? > 0xff { - *target += 1 - } - } - - Ok(()) - } } /// Relocate program counter to buffer. diff --git a/codegen/src/jump/table.rs b/codegen/src/jump/table.rs index f52680e26..6c466527c 100644 --- a/codegen/src/jump/table.rs +++ b/codegen/src/jump/table.rs @@ -53,11 +53,6 @@ impl JumpTable { self.jump.insert(pc, Jump::Label(label)); } - /// Registers a label at a specific program counter offset. - pub fn offset(&mut self, pc: u16, offset: u16) { - self.jump.insert(pc, Jump::Offset(offset)); - } - /// Merges another jump table into this one. /// /// This function updates the program counters of the target jump table and @@ -108,31 +103,11 @@ fn test_multiple_jumps_same_target() -> anyhow::Result<()> { // Setup multiple jumps to same target table.register(0x10, Jump::Label(0x100)); table.register(0x20, Jump::Label(0x100)); - table.register(0x30, Jump::Offset(0x10)); - table.shift_targets()?; // Verify each jump's final target - assert_eq!(table.target(table.jump.get(&0x10).unwrap())?, 0x108); - assert_eq!(table.target(table.jump.get(&0x20).unwrap())?, 0x108); - assert_eq!(table.target(table.jump.get(&0x30).unwrap())?, 0x10); - Ok(()) -} - -#[test] -fn test_multiple_jumps_with_backwards() -> anyhow::Result<()> { - let mut table = JumpTable::default(); - - // Simulate multiple functions calling _approve - table.register(0x10, Jump::Label(0x100)); // approve() -> _approve - table.register(0x20, Jump::Label(0x100)); // spend_allowance() -> _approve - table.register(0x100, Jump::Offset(0x30)); // _approve implementation - - table.shift_targets()?; - assert_eq!(table.target(table.jump.get(&0x10).unwrap())?, 0x106); assert_eq!(table.target(table.jump.get(&0x20).unwrap())?, 0x106); - assert_eq!(table.target(table.jump.get(&0x100).unwrap())?, 0x30); Ok(()) } @@ -154,25 +129,6 @@ fn test_nested_jumps() -> anyhow::Result<()> { Ok(()) } -#[test] -fn test_offset_label_interaction() -> anyhow::Result<()> { - let mut table = JumpTable::default(); - - // Create offset and label jumps targeting same area - table.register(0x10, Jump::Offset(0x50)); // Offset jump forward - table.register(0x20, Jump::Label(0x60)); // Label jump to area after offset - table.register(0x30, Jump::Label(0x50)); // Label jump to offset target - - table.shift_targets()?; - - // Verify jumps are processed correctly - assert_eq!(table.target(table.jump.get(&0x10).unwrap())?, 0x50); - assert_eq!(table.target(table.jump.get(&0x20).unwrap())?, 0x66); - assert_eq!(table.target(table.jump.get(&0x30).unwrap())?, 0x56); - - Ok(()) -} - #[test] fn test_sequential_jumps() -> anyhow::Result<()> { let mut table = JumpTable::default(); @@ -204,3 +160,65 @@ fn test_jump_backwards() -> anyhow::Result<()> { assert_eq!(table.target(table.jump.get(&0x30).unwrap())?, 0x22); Ok(()) } + +#[test] +fn test_jump_table_state_consistency() -> anyhow::Result<()> { + let mut table = JumpTable::default(); + + // Register a sequence of jumps that mirror ERC20's pattern + table.register(0x10, Jump::Label(0x100)); // First jump + table.register(0x20, Jump::Label(0x100)); // Second jump to same target + + // Record state before and after each operation + let initial_state = table.jump.clone(); + table.shift_targets()?; + let shifted_state = table.jump.clone(); + + // Verify jump table consistency + assert_eq!(table.jump.len(), initial_state.len()); + assert!(shifted_state.values().all(|j| matches!(j, Jump::Label(_)))); + Ok(()) +} + +#[test] +fn test_jump_target_ordering() -> anyhow::Result<()> { + let mut table = JumpTable::default(); + + // Register jumps in reverse order + table.register(0x30, Jump::Label(0x100)); + table.register(0x20, Jump::Label(0x100)); + table.register(0x10, Jump::Label(0x100)); + + // Track all target shifts + let mut shifts = Vec::new(); + let cloned = table.clone(); + let original_targets: Vec<_> = cloned.jump.values().collect(); + + table.shift_targets()?; + + // Verify target consistency + for (orig, shifted) in original_targets.iter().zip(table.jump.values()) { + shifts.push((orig, shifted)); + } + + Ok(()) +} + +#[test] +fn test_mixed_jump_types() -> anyhow::Result<()> { + let mut table = JumpTable::default(); + + // Mix function calls and labels like in ERC20 + table.func.insert(1, 0x100); + table.call(0x10, 1); // Function call + table.register(0x20, Jump::Label(0x100)); // Label jump to same target + + let before_shift = table.jump.clone(); + table.shift_targets()?; + let after_shift = table.jump.clone(); + + // Compare states + assert_eq!(before_shift.len(), after_shift.len()); + + Ok(()) +} diff --git a/codegen/src/jump/target.rs b/codegen/src/jump/target.rs index 7770d76a3..f810f490d 100644 --- a/codegen/src/jump/target.rs +++ b/codegen/src/jump/target.rs @@ -15,7 +15,6 @@ impl JumpTable { /// (offset, label, function, or external function). pub fn target(&self, jump: &Jump) -> Result { match jump { - Jump::Offset(offset) => Ok(*offset), Jump::Label(label) => Ok(*label), Jump::Func(func) => Ok(*self.func.get(func).ok_or(Error::FuncNotFound(*func))?), Jump::ExtFunc(ext) => Ok(self.code.offset_of(ext).ok_or(Error::ExtFuncNotFound)?), @@ -28,26 +27,40 @@ impl JumpTable { /// counter and the target, adjusting for any offsets. pub fn shift_targets(&mut self) -> Result<()> { let mut total_offset = 0; - for (original_pc, jump) in self.jump.clone().iter() { - tracing::debug!("shift targets for {jump} <- (0x{original_pc:x})"); - let pc = original_pc + total_offset; + let mut target_sizes = Vec::new(); + let jumps = self.jump.clone(); - // Determine the size of the target PC based on its value. - let target = self.target(jump)? + total_offset; + // First pass: calculate all target sizes and accumulate offsets + for (original_pc, jump) in jumps.iter() { + let pc = original_pc + total_offset; + let raw_target = self.target(jump)?; - /* if jump.is_offset() { - target += original_pc; - } */ + // Calculate the absolute target including future offsets + let target = if raw_target > *original_pc { + raw_target + total_offset + } else { + raw_target + }; - let offset = if target > 0xff { - 3 // Requires 3 bytes for processing the JUMP target offset + // Calculate instruction size based on absolute target value + let instr_size = if target > 0xff { + 3 // PUSH2 + 2 bytes } else { - 2 // Requires 2 bytes + 2 // PUSH1 + 1 byte }; - self.shift_target(pc, offset)?; - total_offset += offset; + target_sizes.push((pc, instr_size)); + total_offset += instr_size; } + + // Second pass: apply shifts with accumulated offsets + total_offset = 0; + for (pc, size) in target_sizes { + tracing::debug!("shift target at pc=0x{pc:x} with size={size}"); + self.shift_target(pc, size)?; + total_offset += size; + } + Ok(()) } @@ -56,22 +69,19 @@ impl JumpTable { /// This function handles the shifting of the code section, label targets, and /// function targets. pub fn shift_target(&mut self, ptr: u16, offset: u16) -> Result<()> { + // First shift the code section self.code.shift(offset); + + // Only shift targets that are after ptr self.shift_label_target(ptr, offset)?; self.shift_func_target(ptr, offset) } /// Shifts the program counter for functions. pub fn shift_func_target(&mut self, ptr: u16, offset: u16) -> Result<()> { - if self.func.is_empty() { - tracing::trace!("No functions to shift."); - return Ok(()); - } - self.func.iter_mut().try_for_each(|(index, target)| { - let next_target = *target + offset; - if *target > ptr { + let next_target = *target + offset; tracing::trace!( "shift Func({index}) target with offset={offset}: 0x{target:x}(0x{ptr:x}) -> 0x{:x}", next_target @@ -86,26 +96,22 @@ impl JumpTable { /// Shifts the program counter for labels. pub fn shift_label_target(&mut self, ptr: u16, offset: u16) -> Result<()> { - if self.jump.is_empty() { - tracing::trace!("No labels to shift."); - return Ok(()); - } + for (_, jump) in self.jump.iter_mut() { + let Jump::Label(target) = jump else { + continue; + }; - self.jump.iter_mut().try_for_each(|(pc, jump)| { - if let Jump::Label(target) = jump { + // Only shift targets that come after ptr AND + // only if the jump instruction itself comes after ptr + if *target > ptr { let next_target = *target + offset; - - if *target > ptr { - tracing::trace!( - "shift Label(0x{pc:x}) target with offset={offset}: 0x{target:x}(0x{ptr:x}) -> 0x{:x}", - next_target, - ); - - *target = next_target; - } + tracing::trace!( + "shift Label target with offset={offset}: 0x{target:x}(0x{ptr:x}) -> 0x{:x}", + next_target, + ); + *target = next_target; } - - Ok(()) - }) + } + Ok(()) } } diff --git a/codegen/src/masm/cmp.rs b/codegen/src/masm/cmp.rs index a2a148049..a2357c0fb 100644 --- a/codegen/src/masm/cmp.rs +++ b/codegen/src/masm/cmp.rs @@ -1,7 +1,7 @@ // Comparison Instructions use crate::{MacroAssembler, Result}; -use opcodes::ShangHai as OpCode; +use opcodes::Cancun as OpCode; impl MacroAssembler { /// Greater than or equal comparison. diff --git a/codegen/src/masm/mod.rs b/codegen/src/masm/mod.rs index b8a1577e8..0978a3ef1 100644 --- a/codegen/src/masm/mod.rs +++ b/codegen/src/masm/mod.rs @@ -80,10 +80,19 @@ impl MacroAssembler { } /// Get the current program counter offset. - pub fn pc_offset(&self) -> u16 { + pub fn pc(&self) -> u16 { self.asm.buffer().len() as u16 } + /// Get the current program counter offset. + pub fn pc_offset(&self) -> u16 { + if self.pc() > 0xff { + 3 + } else { + 2 + } + } + /// Place n bytes on stack. pub fn push(&mut self, bytes: &[u8]) -> Result<()> { tracing::trace!("push bytes: 0x{}", hex::encode(bytes)); @@ -130,7 +139,7 @@ impl MacroAssembler { 30 => self.asm._push30(), 31 => self.asm._push31(), 32 => self.asm._push32(), - _ => return Err(Error::StackIndexOutOfRange(len as u8)), + _ => return Err(Error::StackIndexOutOfRange(len as u16)), }?; self.asm.emitn(bytes); @@ -146,12 +155,12 @@ impl MacroAssembler { } /// Get the stack pointer. - pub fn sp(&self) -> u8 { + pub fn sp(&self) -> u16 { self.asm.sp } /// Swap memory by target index. - pub fn swap(&mut self, index: u8) -> Result<()> { + pub fn swap(&mut self, index: u16) -> Result<()> { tracing::trace!("swap index: {}", index); match index { 0 => Ok(()), @@ -176,7 +185,7 @@ impl MacroAssembler { } /// Duplicate stack item by target index. - pub fn dup(&mut self, index: u8) -> Result<()> { + pub fn dup(&mut self, index: u16) -> Result<()> { tracing::trace!("dup index: {}", index); match index { 0 => Ok(()), @@ -203,7 +212,7 @@ impl MacroAssembler { /// Shift the program counter to the bottom or the top of the /// parameters. This is used by the callee function for jumping /// back to the caller function. - pub fn shift_stack(&mut self, count: u8, from_top: bool) -> Result<()> { + pub fn shift_stack(&mut self, count: u16, from_top: bool) -> Result<()> { let mut swaps = 0; if from_top { diff --git a/codegen/src/masm/ret.rs b/codegen/src/masm/ret.rs index 84ef44ab2..ec5b65a04 100644 --- a/codegen/src/masm/ret.rs +++ b/codegen/src/masm/ret.rs @@ -31,7 +31,7 @@ impl MacroAssembler { /// Handle the return of a call. pub fn call_return(&mut self, results: &[ValType]) -> Result<()> { - let len = results.len() as u8; + let len = results.len() as u16; tracing::trace!("cleaning frame stack, target: {}", len + 1); // TODO: clean stacks via the count of nested control stacks. diff --git a/codegen/src/result.rs b/codegen/src/result.rs index 1889e1bbd..12816f3b4 100644 --- a/codegen/src/result.rs +++ b/codegen/src/result.rs @@ -86,16 +86,16 @@ pub enum Error { SelectorNotFound, /// Failed to index data on stack. #[error("Stack index is out of range {0}, max is 255 (0x400)")] - StackIndexOutOfRange(u8), + StackIndexOutOfRange(u16), /// Failed to increment stack pointer. - #[error("Stack overflow, max is 1024 stack items, got {0}")] - StackOverflow(u8), + #[error("Stack overflow, max is 1024 stack items, but add {1} to {0}")] + StackOverflow(u16, u16), /// Failed to decrement stack pointer. #[error("Stack underflow, current stack items {0}, expect at least {1}")] - StackUnderflow(u8, u8), + StackUnderflow(u16, u16), /// Failed to pop stack. #[error("Stack not balanced, current stack items {0}")] - StackNotBalanced(u8), + StackNotBalanced(u16), /// Failed to queue host functions. #[error("Unsupported host function {0:?}")] UnsupportedHostFunc(crate::wasm::HostFunc), diff --git a/codegen/src/visitor/call.rs b/codegen/src/visitor/call.rs index c28e99e26..cdf9066af 100644 --- a/codegen/src/visitor/call.rs +++ b/codegen/src/visitor/call.rs @@ -10,7 +10,7 @@ use crate::{ Error, Function, Result, }; use anyhow::anyhow; -use opcodes::ShangHai as OpCode; +use opcodes::Cancun as OpCode; impl Function { /// The call indirect instruction calls a function indirectly @@ -67,26 +67,10 @@ impl Function { let reserved = self.env.slots.get(&index).unwrap_or(&0); let (params, results) = self.env.funcs.get(&index).unwrap_or(&(0, 0)); - // Prepare the stack structure for the function call. - // The stack will be structured as follows: - // [ .., - // , // The current program counter - // params[SWAP], // Swap the parameters for the call - // params[PUSH, SLOT, MSTORE], // Push parameters to the stack - // {(PUSH, PC), JUMP, JUMPDEST} // Prepare for the jump to the callee - // ] - let base_offset = 5 + ((params + reserved) * 0x20).saturating_sub(0xff) / 0x20; - - // Move the PC before the parameters in the stack. - self.table.offset( - self.masm.pc_offset(), - base_offset as u16 + 4 * (*params as u16), - ); + // TODO This is a temporary fix to avoid stack underflow. + // We need to find a more elegant solution for this. self.masm.increment_sp(1)?; - // Adjust the stack to place the PC before the parameters. - self.masm.shift_stack(*params as u8, true)?; - // Store parameters in memory and register the call index in the jump table. for i in (0..*params).rev() { tracing::trace!("Storing local at {} for function {index}", i + reserved); @@ -94,15 +78,18 @@ impl Function { self.masm._mstore()?; } - // Register the call index in the jump table. - self.table.call(self.masm.pc_offset(), index); + // Register the label to jump back. + let return_pc = self.masm.pc() + 2; + self.table.label(self.masm.pc(), return_pc); + self.masm._jumpdest()?; // TODO: support same pc different label - // Jump to the callee function. + // Register the call index in the jump table. + self.table.call(self.masm.pc(), index); // [PUSHN, CALL_PC] self.masm._jump()?; - self.masm._jumpdest()?; // Adjust the stack pointer for the results. - self.masm.increment_sp(*results as u8)?; + self.masm._jumpdest()?; + self.masm.increment_sp(*results as u16)?; Ok(()) } diff --git a/codegen/src/visitor/control.rs b/codegen/src/visitor/control.rs index f151f1533..f278b401b 100644 --- a/codegen/src/visitor/control.rs +++ b/codegen/src/visitor/control.rs @@ -15,7 +15,7 @@ impl Function { // push an `If` frame to the control stack let frame = ControlStackFrame::new( ControlStackFrameType::If(false), - self.masm.pc_offset(), + self.masm.pc(), self.masm.sp(), blockty, ); @@ -35,7 +35,7 @@ impl Function { pub fn _block(&mut self, blockty: BlockType) -> Result<()> { let frame = ControlStackFrame::new( ControlStackFrameType::Block, - self.masm.pc_offset(), + self.masm.pc(), self.masm.sp(), blockty, ); @@ -50,7 +50,7 @@ impl Function { pub fn _loop(&mut self, blockty: BlockType) -> Result<()> { let frame = ControlStackFrame::new( ControlStackFrameType::Loop, - self.masm.pc_offset(), + self.masm.pc(), self.masm.sp(), blockty, ); @@ -68,7 +68,7 @@ impl Function { // push an `Else` frame to the control stack. let frame = ControlStackFrame::new( ControlStackFrameType::Else, - self.masm.pc_offset(), + self.masm.pc(), self.masm.sp(), last_frame.result(), ); @@ -78,7 +78,7 @@ impl Function { // mark else as the jump destination of the if block. self.table - .label(last_frame.original_pc_offset, self.masm.pc_offset()); + .label(last_frame.original_pc_offset, self.masm.pc()); self.masm._jumpdest()?; Ok(()) @@ -92,7 +92,7 @@ impl Function { tracing::trace!("select"); self.masm._iszero()?; self.masm.increment_sp(1)?; - self.table.offset(self.masm.pc_offset(), 4); + self.table.label(self.masm.pc(), self.masm.pc() + 2); self.masm._jumpi()?; self.masm._drop()?; self.masm._jumpdest()?; @@ -113,10 +113,17 @@ impl Function { /// Conditional branch to a given label in an enclosing construct. pub fn _br_if(&mut self, depth: u32) -> Result<()> { let label = self.control.label_from_depth(depth)?; - self.table.label(self.masm.pc_offset(), label); - self.masm.asm.increment_sp(1)?; + + // Register the jump target for breaking out + self.table.label(self.masm.pc(), label); + self.masm.increment_sp(1)?; + + // JUMPI will check condition and jump if true self.masm._jumpi()?; + // If we don't jump, we continue in the current frame + // No need for explicit ISZERO since JUMPI handles the condition + Ok(()) } @@ -172,13 +179,12 @@ impl Function { ControlStackFrameType::Block => self.masm._jumpdest(), ControlStackFrameType::Loop => Ok(()), _ => { - self.table - .label(frame.original_pc_offset, self.masm.pc_offset()); + self.table.label(frame.original_pc_offset, self.masm.pc()); // TODO: Check the stack output and make decisions // how to handle the results. - // Emit JUMPDEST after at the end of the control flow. + // Emit JUMPDEST at the end of the control flow. self.masm._jumpdest() } } diff --git a/codegen/src/visitor/mod.rs b/codegen/src/visitor/mod.rs index 6b95a1de5..41a3cf06f 100644 --- a/codegen/src/visitor/mod.rs +++ b/codegen/src/visitor/mod.rs @@ -188,7 +188,7 @@ macro_rules! map_wasm_operators { }; } -impl<'a> VisitOperator<'a> for Function { +impl VisitOperator<'_> for Function { type Output = Result<()>; for_each_operator!(impl_visit_operator); diff --git a/codegen/src/wasm/func.rs b/codegen/src/wasm/func.rs index 12424e08b..131a844f3 100644 --- a/codegen/src/wasm/func.rs +++ b/codegen/src/wasm/func.rs @@ -81,7 +81,7 @@ impl<'f> Deref for Functions<'f> { } } -impl<'f> DerefMut for Functions<'f> { +impl DerefMut for Functions<'_> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } diff --git a/codegen/src/wasm/host.rs b/codegen/src/wasm/host.rs index 2fce8a2d4..454d7b355 100644 --- a/codegen/src/wasm/host.rs +++ b/codegen/src/wasm/host.rs @@ -3,7 +3,7 @@ use crate::{Error, Result}; use anyhow::anyhow; use core::str::FromStr; -use opcodes::{OpCode as _, ShangHai as OpCode}; +use opcodes::{Cancun as OpCode, OpCode as _}; /// EVM built-in function. #[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)] @@ -16,8 +16,6 @@ pub enum HostFunc { // /// Emit ABI to the compiler. EmitABI, - /// check equal of two addresses - AddressEq, /// Push u256 max to stack U256MAX, /// Revert messages with length of slots @@ -50,30 +48,42 @@ impl TryFrom<(&str, &str)> for HostFunc { fn try_from(import: (&str, &str)) -> Result { let (module, name) = import; match import { - ("asm", name) => { - if name.starts_with("sload") { - Ok(Self::Evm(OpCode::SLOAD)) - } else if name.starts_with("revert") { - let count = name.trim_start_matches("revert"); - - // TODO: use anyhow instead of Error - Ok(Self::Revert(count.parse().map_err(|e| anyhow!("{e}"))?)) - } else { - Ok(Self::NoOp) - } - } + ("zinkc", name) => match name { + "emit_abi" => Ok(Self::EmitABI), + "u256_add" => Ok(Self::Evm(OpCode::ADD)), + "u256_sub" => Ok(Self::Evm(OpCode::SUB)), + "u256_lt" => Ok(Self::Evm(OpCode::LT)), + "u256_max" => Ok(Self::U256MAX), + "u256_addmod" => Ok(Self::Evm(OpCode::ADDMOD)), + "u256_mulmod" => Ok(Self::Evm(OpCode::MULMOD)), + "label_reserve_mem_32" => Ok(Self::Label(CompilerLabel::ReserveMemory32)), + "label_reserve_mem_64" => Ok(Self::Label(CompilerLabel::ReserveMemory64)), + _ => 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), - ("zinkc", "address_eq") => Ok(Self::Evm(OpCode::EQ)), - ("zinkc", "u256_add") => Ok(Self::Evm(OpCode::ADD)), - ("zinkc", "u256_sub") => Ok(Self::Evm(OpCode::SUB)), - ("zinkc", "u256_lt") => Ok(Self::Evm(OpCode::LT)), - ("zinkc", "u256_max") => Ok(Self::U256MAX), - ("zinkc", "label_reserve_mem_32") => Ok(Self::Label(CompilerLabel::ReserveMemory32)), - ("zinkc", "label_reserve_mem_64") => Ok(Self::Label(CompilerLabel::ReserveMemory64)), + ("asm", name) => match name { + n if n.starts_with("sload") => Ok(Self::Evm(OpCode::SLOAD)), + n if n.starts_with("tload") => Ok(Self::Evm(OpCode::TLOAD)), + n if n.starts_with("revert") => { + let count = n.trim_start_matches("revert"); + Ok(Self::Revert(count.parse().map_err(|e| anyhow!("{e}"))?)) + } + n if n.starts_with("mulmod") => Ok(Self::Evm(OpCode::MULMOD)), + n if n.starts_with("addmod") => Ok(Self::Evm(OpCode::ADDMOD)), + _ => Ok(Self::NoOp), + }, + ("bytes", instr) => match instr { + push if push.starts_with("push_bytes") => Ok(Self::NoOp), + sload if sload.starts_with("sload_bytes") => Ok(Self::Evm(OpCode::SLOAD)), + eq if eq.ends_with("_eq") => Ok(Self::Evm(OpCode::EQ)), + _ => { + tracing::warn!("Failed to load host function: {import:?} from module bytes"); + Err(Error::HostFuncNotFound(module.into(), name.into())) + } + }, _ => { tracing::warn!("Failed to load host function: {:?}", import); Err(Error::HostFuncNotFound(module.into(), name.into())) @@ -88,3 +98,22 @@ pub enum CompilerLabel { ReserveMemory32, ReserveMemory64, } + +#[cfg(test)] +mod tests { + use anyhow::Ok; + + use super::*; + + #[test] + fn test_addmod_mulmod_host_functions() -> anyhow::Result<()> { + let addmod_func = HostFunc::try_from(("zinkc", "u256_addmod"))?; + + assert_eq!(addmod_func, HostFunc::Evm(OpCode::ADDMOD)); + + // Test MULMOD host function conversion + let mulmod_func = HostFunc::try_from(("zinkc", "u256_mulmod")); + assert!(mulmod_func.is_ok()); + Ok(()) + } +} diff --git a/compiler/filetests/wat/br_if/as_block_last.wat b/compiler/filetests/wat/br_if/as_block_last.wat index 5d5130a8e..e6163dabc 100644 --- a/compiler/filetests/wat/br_if/as_block_last.wat +++ b/compiler/filetests/wat/br_if/as_block_last.wat @@ -6,7 +6,7 @@ ) (func $internal (param i32) (block - (call $dummy) + (call $dummy) (call $dummy) (br_if 0 (local.get 0)) ) diff --git a/compiler/src/compiler.rs b/compiler/src/compiler.rs index 67cc50212..f95c04f0b 100644 --- a/compiler/src/compiler.rs +++ b/compiler/src/compiler.rs @@ -57,6 +57,7 @@ impl Compiler { .. } = self; + tracing::debug!("code length: {}", buffer.len()); Ok(Artifact { abi, config, diff --git a/docs/README.md b/docs/README.md index 0da08496d..d06d9d883 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,47 +1,10 @@ # The Zink Book +This is the book for the Zink Language, you can read it online at [docs.zink-lang.org](https://zink-lang.org/). -## Validation - -e.g. How Zink Compiler helps you writing your EVM smart contracts ;) - - -#### `0x35` - CALLDATALOAD - -1. validate the function signatures -2. validate the stack usages - - -## Optimizations - - -#### StackCompressor - -The max limit of the defined local variables is 16 due to there is a hard limit -of 16 slots for reaching down the expression stack of EVM. - - -## Function Calls - -### Calling Convention - -There we two ways to handle the calling convention, for storing PC - -1. Store the PC - -few arguments -> store the PC on stack. -lots of arguments -> store the PC in reserved memory. - - -2. Retrieve the PC - -stack -> swap the parameters and the PC -memory -> read from reserved memory - - -3. Jump back to the caller - -stack -> swap the results and the PC - -> dup the PC and pop in caller function -memory -> load PC from memory +## Contributing +``` +cargo install mdbook +mdbook serve +``` diff --git a/docs/book.toml b/docs/book.toml index d00cff8dc..a1080729b 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -1,5 +1,5 @@ [book] -title = "The Zink Project" +title = "The Zink Language" authors = ["clearloop"] multilingual = false src = "." @@ -9,7 +9,7 @@ edition = "2021" [output.html] cname = "docs.zink-lang.org" -git-repository-url = "https://github.com/clearloop/zink" +git-repository-url = "https://github.com/zink-lang/zink" git-repository-icon = "fa-github" default-theme = "dark" preferred-dark-theme = "navy" @@ -17,11 +17,11 @@ curly-quotes = true mathjax-support = false # If editable -# edit-url-template = "https://github.com/clearloop/zink/edit/main/docs/{path}" +edit-url-template = "https://github.com/zink-lang/zink/edit/main/docs/{path}" # If enable folding chapters -# [output.html.fold] -# enable = true +[output.html.fold] +enable = true # level = 1 [output.html.playground] diff --git a/docs/introduction.md b/docs/introduction.md index 383ea68da..45e7d24c0 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -11,6 +11,7 @@ contracts on Ethereum. This guide is intended to serve a number of purposes and within you'll find: +- [The Bounty issues][bounty] - [The rustdocs of the zink project][rustdocs] - [The design of the zink compiler][compiler] - [The rust guide of zink projects][styles] @@ -23,3 +24,4 @@ This guide is intended to serve a number of purposes and within you'll find: [zinkc]: https://github.com/clearloop/zink/tree/main/compiler [rustdocs]: https://docs.zink-lang.org/rustdocs [source]: https://github.com/clearloop/zink/tree/main/docs +[bounty]: https://zink-lang.org/budgets diff --git a/evm/abi/src/arg.rs b/evm/abi/src/arg.rs index d7700166e..01f9617e8 100644 --- a/evm/abi/src/arg.rs +++ b/evm/abi/src/arg.rs @@ -67,8 +67,9 @@ impl From<&str> for Param { "U256" | "u256" | "uint256" => Param::UInt256, "bool" => Param::Bool, "address" | "Address" => Param::Address, - "Bytes" | "Vec" => Param::Bytes, "String" | "String32" => Param::String, + "Vec" => Param::Bytes, + bytes if bytes.starts_with("Bytes") => Param::Bytes, _ => Param::Unknown(s.to_string()), } } diff --git a/evm/opcodes/src/cancun.rs b/evm/opcodes/src/cancun.rs new file mode 100644 index 000000000..43a078cf6 --- /dev/null +++ b/evm/opcodes/src/cancun.rs @@ -0,0 +1,156 @@ +//! Instructions for Cancun. + +use crate::{opcodes, Group, OpCode, Upgrade}; + +opcodes! { + Cancun, + (0x00, STOP, 0, 0, 0, "Halts execution.", Frontier, StopArithmetic), + (0x01, ADD, 3, 2, 1, "Addition operation.", Frontier, StopArithmetic), + (0x02, MUL, 5, 2, 1, "Multiplication operation.", Frontier, StopArithmetic), + (0x03, SUB, 3, 2, 1, "Subtraction operation.", Frontier, StopArithmetic), + (0x04, DIV, 5, 2, 1, "Integer division operation.", Frontier, StopArithmetic), + (0x05, SDIV, 5, 2, 1, "Signed integer division operation (truncated).", Frontier, StopArithmetic), + (0x06, MOD, 5, 2, 1, "Modulo remainder operation.", Frontier, StopArithmetic), + (0x07, SMOD, 5, 2, 1, "Signed modulo remainder operation.", Frontier, StopArithmetic), + (0x08, ADDMOD, 8, 3, 1, "Modulo addition operation.", Frontier, StopArithmetic), + (0x09, MULMOD, 8, 3, 1, "Modulo multiplication operation.", Frontier, StopArithmetic), + (0x0a, EXP, 10, 2, 1, "Exponential operation.", Frontier, StopArithmetic), + (0x0b, SIGNEXTEND, 5, 2, 1, "Extend length of two's complement signed integer.", Frontier, StopArithmetic), + (0x10, LT, 3, 2, 1, "Less-than comparison.", Frontier, ComparisonBitwiseLogic), + (0x11, GT, 3, 2, 1, "Greater-than comparison.", Frontier, ComparisonBitwiseLogic), + (0x12, SLT, 3, 2, 1, "Signed less-than comparison.", Frontier, ComparisonBitwiseLogic), + (0x13, SGT, 3, 2, 1, "Signed greater-than comparison.", Frontier, ComparisonBitwiseLogic), + (0x14, EQ, 3, 2, 1, "Equality comparison.", Frontier, ComparisonBitwiseLogic), + (0x15, ISZERO, 3, 1, 1, "Simple not operator.", Frontier, ComparisonBitwiseLogic), + (0x16, AND, 3, 2, 1, "Bitwise AND operation.", Frontier, ComparisonBitwiseLogic), + (0x17, OR, 3, 2, 1, "Bitwise OR operation.", Frontier, ComparisonBitwiseLogic), + (0x18, XOR, 3, 2, 1, "Bitwise XOR operation.", Frontier, ComparisonBitwiseLogic), + (0x19, NOT, 3, 1, 1, "Bitwise NOT operation.", Frontier, ComparisonBitwiseLogic), + (0x1a, BYTE, 3, 2, 1, "Retrieve single byte from word.", Frontier, ComparisonBitwiseLogic), + (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, 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), + (0x33, CALLER, 2, 0, 1, "Get caller address.", Frontier, EnvironmentalInformation), + (0x34, CALLVALUE, 2, 0, 1, "Get deposited value by the instruction/transaction responsible for this execution.", Frontier, EnvironmentalInformation), + (0x35, CALLDATALOAD, 3, 1, 1, "Get input data of current environment.", Frontier, EnvironmentalInformation), + (0x36, CALLDATASIZE, 2, 0, 1, "Get size of input data in current environment.", Frontier, EnvironmentalInformation), + (0x37, CALLDATACOPY, 3, 3, 0, "Copy input data in current environment to memory.", Frontier, EnvironmentalInformation), + (0x38, CODESIZE, 2, 0, 1, "Get size of code running in current environment.", Frontier, EnvironmentalInformation), + (0x39, CODECOPY, 3, 3, 0, "Copy code running in current environment to memory.", Frontier, EnvironmentalInformation), + (0x3a, GASPRICE, 2, 0, 1, "Get price of gas in current environment", Frontier, EnvironmentalInformation), + (0x3b, EXTCODESIZE, 20, 1, 1, "Get size of an account's code.", Frontier, EnvironmentalInformation), + (0x3c, EXTCODECOPY, 20, 4, 0, "Copy an account's code to memory.", Frontier, EnvironmentalInformation), + (0x3d, RETURNDATASIZE, 2, 0, 1, "Get size of output data from the previous call from the current environment.", Byzantium, EnvironmentalInformation), + (0x3e, RETURNDATACOPY, 3, 3, 0, "Copy output data from the previous call to memory.", Byzantium, EnvironmentalInformation), + (0x3f, EXTCODEHASH, 100, 1, 1, "Get hash of an account’s code.", Cancun, EnvironmentalInformation), + (0x40, BLOCKHASH, 20, 1, 1, "Get the hash of one of the 256 most recent complete blocks.", Cancun, EnvironmentalInformation), + (0x41, COINBASE, 2, 0, 1, "Get the block's beneficiary address.", Frontier, BlockInformation), + (0x42, TIMESTAMP, 2, 0, 1, "Get the block's timestamp.", Frontier, BlockInformation), + (0x43, NUMBER, 2, 0, 1, "Get the block's number.", Frontier, BlockInformation), + (0x44, DIFFICULTY, 2, 0, 1, "Get the block's difficulty.", Frontier, BlockInformation), + (0x45, GASLIMIT, 2, 0, 1, "Get the block's gas limit.", Frontier, BlockInformation), + (0x46, CHAINID, 2, 0, 1, "Get the chain ID.", Istanbul, BlockInformation), + (0x47, SELFBALANCE, 5, 0, 1, "Get balance of currently executing account.", Istanbul, BlockInformation), + (0x48, BASEFEE, 2, 0, 1, "Get the base fee.", London, BlockInformation), + (0x49, BLOBHASH, 3, 1, 1, "Get versioned hash at index.", Cancun, BlockInformation), + (0x4a, BLOBBASEFEE, 2, 0, 1, "Get the current blob base fee.", Cancun, BlockInformation), + (0x50, POP, 2, 1, 0, "Remove item from stack.", Frontier, StackMemoryStorageFlow), + (0x51, MLOAD, 3, 1, 1, "Load word from memory.", Frontier, StackMemoryStorageFlow), + (0x52, MSTORE, 3, 2, 0, "Save word to memory.", Frontier, StackMemoryStorageFlow), + (0x53, MSTORE8, 3, 2, 0, "Save byte to memory.", Frontier, StackMemoryStorageFlow), + (0x54, SLOAD, 50, 1, 1, "Load word from storage.", Frontier, StackMemoryStorageFlow), + (0x55, SSTORE, 0, 2, 0, "Save word to storage.", Frontier, StackMemoryStorageFlow), + (0x56, JUMP, 8, 1, 0, "Alter the program counter.", Frontier, StackMemoryStorageFlow), + (0x57, JUMPI, 10, 2, 0, "Conditionally alter the program counter.", Frontier, StackMemoryStorageFlow), + (0x58, PC, 2, 0, 1, "Get the value of the program counter prior to the increment.", Frontier, StackMemoryStorageFlow), + (0x59, MSIZE, 2, 0, 1, "Get the size of active memory in bytes.", Frontier, StackMemoryStorageFlow), + (0x5a, GAS, 2, 0, 1, "Get the amount of available gas.", Frontier, StackMemoryStorageFlow), + (0x5b, JUMPDEST, 1, 0, 0, "Mark a valid destination for jumps.", Frontier, StackMemoryStorageFlow), + (0x5c, TLOAD, 50, 1, 1, "Load word from transient storage", Cancun, StackMemoryStorageFlow), + (0x5d, TSTORE, 0, 2, 0, "Save word to transient storage.", Cancun, StackMemoryStorageFlow), + (0x5e, MCOPY, 3,3,0, "copy memory areas", Cancun,StackMemoryStorageFlow), + (0x5f, PUSH0, 2, 0, 1, "Place 0 byte item on stack.", Shanghai, Push), + (0x60, PUSH1, 3, 0, 1, "Place 1 byte item on stack.", Frontier, Push), + (0x61, PUSH2, 3, 0, 1, "Place 2-byte item on stack.", Frontier, Push), + (0x62, PUSH3, 3, 0, 1, "Place 3-byte item on stack.", Frontier, Push), + (0x63, PUSH4, 3, 0, 1, "Place 4-byte item on stack.", Frontier, Push), + (0x64, PUSH5, 3, 0, 1, "Place 5-byte item on stack.", Frontier, Push), + (0x65, PUSH6, 3, 0, 1, "Place 6-byte item on stack.", Frontier, Push), + (0x66, PUSH7, 3, 0, 1, "Place 7-byte item on stack.", Frontier, Push), + (0x67, PUSH8, 3, 0, 1, "Place 8-byte item on stack.", Frontier, Push), + (0x68, PUSH9, 3, 0, 1, "Place 9-byte item on stack.", Frontier, Push), + (0x69, PUSH10, 3, 0, 1, "Place 10-byte item on stack.", Frontier, Push), + (0x6a, PUSH11, 3, 0, 1, "Place 11-byte item on stack.", Frontier, Push), + (0x6b, PUSH12, 3, 0, 1, "Place 12-byte item on stack.", Frontier, Push), + (0x6c, PUSH13, 3, 0, 1, "Place 13-byte item on stack.", Frontier, Push), + (0x6d, PUSH14, 3, 0, 1, "Place 14-byte item on stack.", Frontier, Push), + (0x6e, PUSH15, 3, 0, 1, "Place 15-byte item on stack.", Frontier, Push), + (0x6f, PUSH16, 3, 0, 1, "Place 16-byte item on stack.", Frontier, Push), + (0x70, PUSH17, 3, 0, 1, "Place 17-byte item on stack.", Frontier, Push), + (0x71, PUSH18, 3, 0, 1, "Place 18-byte item on stack.", Frontier, Push), + (0x72, PUSH19, 3, 0, 1, "Place 19-byte item on stack.", Frontier, Push), + (0x73, PUSH20, 3, 0, 1, "Place 20-byte item on stack.", Frontier, Push), + (0x74, PUSH21, 3, 0, 1, "Place 21-byte item on stack.", Frontier, Push), + (0x75, PUSH22, 3, 0, 1, "Place 22-byte item on stack.", Frontier, Push), + (0x76, PUSH23, 3, 0, 1, "Place 23-byte item on stack.", Frontier, Push), + (0x77, PUSH24, 3, 0, 1, "Place 24-byte item on stack.", Frontier, Push), + (0x78, PUSH25, 3, 0, 1, "Place 25-byte item on stack.", Frontier, Push), + (0x79, PUSH26, 3, 0, 1, "Place 26-byte item on stack.", Frontier, Push), + (0x7a, PUSH27, 3, 0, 1, "Place 27-byte item on stack.", Frontier, Push), + (0x7b, PUSH28, 3, 0, 1, "Place 28-byte item on stack.", Frontier, Push), + (0x7c, PUSH29, 3, 0, 1, "Place 29-byte item on stack.", Frontier, Push), + (0x7d, PUSH30, 3, 0, 1, "Place 30-byte item on stack.", Frontier, Push), + (0x7e, PUSH31, 3, 0, 1, "Place 31-byte item on stack.", Frontier, Push), + (0x7f, PUSH32, 3, 0, 1, "Place 32-byte (full word) item on stack.", Frontier, Push), + (0x80, DUP1, 3, 1, 2, "Duplicate 1st stack item.", Frontier, Duplication), + (0x81, DUP2, 3, 2, 3, "Duplicate 2nd stack item.", Frontier, Duplication), + (0x82, DUP3, 3, 3, 4, "Duplicate 3rd stack item.", Frontier, Duplication), + (0x83, DUP4, 3, 4, 5, "Duplicate 4th stack item.", Frontier, Duplication), + (0x84, DUP5, 3, 5, 6, "Duplicate 5th stack item.", Frontier, Duplication), + (0x85, DUP6, 3, 6, 7, "Duplicate 6th stack item.", Frontier, Duplication), + (0x86, DUP7, 3, 7, 8, "Duplicate 7th stack item.", Frontier, Duplication), + (0x87, DUP8, 3, 8, 9, "Duplicate 8th stack item.", Frontier, Duplication), + (0x88, DUP9, 3, 9, 10, "Duplicate 9th stack item.", Frontier, Duplication), + (0x89, DUP10, 3, 10, 11, "Duplicate 10th stack item.", Frontier, Duplication), + (0x8a, DUP11, 3, 11, 12, "Duplicate 11th stack item.", Frontier, Duplication), + (0x8b, DUP12, 3, 12, 13, "Duplicate 12th stack item.", Frontier, Duplication), + (0x8c, DUP13, 3, 13, 14, "Duplicate 13th stack item.", Frontier, Duplication), + (0x8d, DUP14, 3, 14, 15, "Duplicate 14th stack item.", Frontier, Duplication), + (0x8e, DUP15, 3, 15, 16, "Duplicate 15th stack item.", Frontier, Duplication), + (0x8f, DUP16, 3, 16, 17, "Duplicate 16th stack item.", Frontier, Duplication), + (0x90, SWAP1, 3, 2, 2, "Exchange 1st and 2nd stack items.", Frontier, Exchange), + (0x91, SWAP2, 3, 3, 3, "Exchange 1st and 3rd stack items.", Frontier, Exchange), + (0x92, SWAP3, 3, 4, 4, "Exchange 1st and 4th stack items.", Frontier, Exchange), + (0x93, SWAP4, 3, 5, 5, "Exchange 1st and 5th stack items.", Frontier, Exchange), + (0x94, SWAP5, 3, 6, 6, "Exchange 1st and 6th stack items.", Frontier, Exchange), + (0x95, SWAP6, 3, 7, 7, "Exchange 1st and 7th stack items.", Frontier, Exchange), + (0x96, SWAP7, 3, 8, 8, "Exchange 1st and 8th stack items.", Frontier, Exchange), + (0x97, SWAP8, 3, 9, 9, "Exchange 1st and 9th stack items.", Frontier, Exchange), + (0x98, SWAP9, 3, 10, 10, "Exchange 1st and 10th stack items.", Frontier, Exchange), + (0x99, SWAP10, 3, 11, 11, "Exchange 1st and 11th stack items.", Frontier, Exchange), + (0x9a, SWAP11, 3, 12, 12, "Exchange 1st and 12th stack items.", Frontier, Exchange), + (0x9b, SWAP12, 3, 13, 13, "Exchange 1st and 13th stack items.", Frontier, Exchange), + (0x9c, SWAP13, 3, 14, 14, "Exchange 1st and 14th stack items.", Frontier, Exchange), + (0x9d, SWAP14, 3, 15, 15, "Exchange 1st and 15th stack items.", Frontier, Exchange), + (0x9e, SWAP15, 3, 16, 16, "Exchange 1st and 16th stack items.", Frontier, Exchange), + (0x9f, SWAP16, 3, 17, 17, "Exchange 1st and 17th stack items.", Frontier, Exchange), + (0xa0, LOG0, 375, 2, 0, "Append log record with no topics.", Frontier, Logging), + (0xa1, LOG1, 750, 3, 0, "Append log record with one topic.", Frontier, Logging), + (0xa2, LOG2, 1125, 4, 0, "Append log record with two topics.", Frontier, Logging), + (0xa3, LOG3, 1500, 5, 0, "Append log record with three topics.", Frontier, Logging), + (0xa4, LOG4, 1875, 6, 0, "Append log record with four topics.", Frontier, Logging), + (0xf0, CREATE, 32000, 3, 1, "Create a new account with associated code.", Frontier, System), + (0xf1, CALL, 100, 7, 1, "Message-call into an account.", Frontier, System), + (0xf2, CALLCODE, 100, 7, 1, "Message-call into this account with alternative account's code.", Frontier, System), + (0xf3, RETURN, 0, 2, 0, "Halt execution returning output data.", Frontier, System), + (0xf4, DELEGATECALL, 40, 6, 1, "Message-call with an alternative account's code, persisting the current context.", Frontier, System), + (0xf5, CREATE2, 32000, 4, 1, "Create a new account with associated code at a specified address.", Constantinople, System), + (0xfa, STATICCALL, 40, 6, 1, "Static message-call into an account.", Byzantium, System), + (0xfd, REVERT, 0, 2, 0, "Stop execution and revert state changes, without consuming all gas and providing a reason.", Byzantium, System), + (0xfe, INVALID, 0, 0, 0, "Designated invalid instruction.", Frontier, System), + (0xff, SELFDESTRUCT, 5000, 1, 0, "Halt execution and register account for later deletion.", Frontier, System) +} diff --git a/evm/opcodes/src/lib.rs b/evm/opcodes/src/lib.rs index d9f531a94..f8279d75f 100644 --- a/evm/opcodes/src/lib.rs +++ b/evm/opcodes/src/lib.rs @@ -1,8 +1,10 @@ //! Ethereum virtual machine opcode #![deny(missing_docs)] +mod cancun; mod shanghai; +pub use cancun::Cancun; pub use shanghai::ShangHai; /// Ethereum virtual machine opcode generator. @@ -185,6 +187,8 @@ pub enum Upgrade { London, /// Shanghai Shanghai, + /// Cancun + Cancun, } /// Ethereum virtual machine opcode. diff --git a/examples/addmod.rs b/examples/addmod.rs new file mode 100644 index 000000000..3235b3df5 --- /dev/null +++ b/examples/addmod.rs @@ -0,0 +1,87 @@ +//! Addmod example for i64, i32, u64, u32. +#![cfg_attr(target_arch = "wasm32", no_std)] +#![cfg_attr(target_arch = "wasm32", no_main)] + +extern crate zink; +use zink::primitives::{numeric::Numeric, U256}; + +#[zink::external] +pub fn addmod_i32(a: i32, b: i32, n: i32) -> i32 { + a.addmod(b, n) +} + +#[zink::external] +pub fn addmod_i64(a: i64, b: i64, n: i64) -> i64 { + a.addmod(b, n) +} + +#[zink::external] +pub fn addmod_u32(a: u32, b: u32, n: u32) -> u32 { + a.addmod(b, n) +} + +#[zink::external] +pub fn addmod_u64(a: u64, b: u64, n: u64) -> u64 { + a.addmod(b, n) +} + +#[zink::external] +pub fn addmod_U256(a: U256, b: U256, n: U256) -> U256 { + a.addmod(b, n) +} + +#[cfg(not(target_arch = "wasm32"))] +fn main() {} + +#[test] +fn test() -> anyhow::Result<()> { + use zint::{Bytes32 as _, Contract}; + + // Test for i32 + let mut contract = Contract::search("addmod")?.compile()?; + + let info_i32 = contract.execute([ + "addmod_i32(int32,int32,int32)".as_bytes(), + &3i32.to_bytes32(), + &5i32.to_bytes32(), + &7i32.to_bytes32(), + ])?; + assert_eq!(info_i32.ret, 1i32.to_bytes32()); + + // Test for i64 + let info_i64 = contract.execute([ + "addmod_i64(int64,int64,int64)".as_bytes(), + &3i64.to_bytes32(), + &5i64.to_bytes32(), + &7i64.to_bytes32(), + ])?; + assert_eq!(info_i64.ret, 1i64.to_bytes32()); + + let info_u32 = contract.execute([ + "addmod_u32(uint32,uint32,uint32)".as_bytes(), + &3u32.to_bytes32(), + &5u32.to_bytes32(), + &7u32.to_bytes32(), + ])?; + assert_eq!(info_u32.ret, 1u32.to_bytes32()); + + // Test for u64 + let info_u64 = contract.execute([ + "addmod_u64(uint64,uint64,uint64)".as_bytes(), + &3u64.to_bytes32(), + &5u64.to_bytes32(), + &7u64.to_bytes32(), + ])?; + assert_eq!(info_u64.ret, 1u64.to_bytes32()); + + //Test for U256 + let info_u256 = contract.execute([ + "addmod_U256(uint256,uint256,uint256)".as_bytes(), + &3i32.to_bytes32(), + &5i32.to_bytes32(), + &7i32.to_bytes32(), + ])?; + assert_eq!(info_u256.ret, 1i32.to_bytes32()); + + Ok(()) +} diff --git a/examples/approval.rs b/examples/approval.rs index 8965d7e6a..b033bb229 100644 --- a/examples/approval.rs +++ b/examples/approval.rs @@ -79,7 +79,7 @@ fn test_approval() -> anyhow::Result<()> { let allowance = evm.storage( address, - Allowance::storage_key(Address(evm.caller), Address(spender)), + Allowance::storage_key(Address::from(evm.caller), Address::from(spender)), )?; assert_eq!(value.to_bytes32(), allowance); @@ -105,7 +105,7 @@ fn test_approval() -> anyhow::Result<()> { assert_eq!(info.ret, true.to_bytes32()); let allowance = evm.storage( address, - Allowance::storage_key(Address(evm.caller), Address(spender)), + Allowance::storage_key(Address::from(evm.caller), Address::from(spender)), )?; assert_eq!(half_value.to_bytes32(), allowance); Ok(()) diff --git a/examples/br_balance.rs b/examples/br_balance.rs new file mode 100644 index 000000000..33295797a --- /dev/null +++ b/examples/br_balance.rs @@ -0,0 +1,60 @@ +#![cfg_attr(target_arch = "wasm32", no_std)] +#![cfg_attr(target_arch = "wasm32", no_main)] + +extern crate zink; +use zink::{storage, Storage}; + +#[storage(i32)] +struct Balance; + +#[zink::external] +fn check_and_update(value: i32) -> bool { + let current = Balance::get(); + + // This mimics the ERC20 balance check + if current < value { + zink::revert!("Not enough balance"); + // TODO: #287 + // return false; + } + + Balance::set(current - value); + return true; +} + +// TODO: identify if the problem is caused by control flow of incorrect opcode mapping. (issue #287) +#[test] +fn test_balance_check() -> anyhow::Result<()> { + use zint::{Bytes32, Contract, EVM}; + + let mut evm = EVM::default().commit(true); + let mut contract = Contract::search("br_balance")?.compile()?; + + // Initialize with balance of 42 + let info = evm.deploy( + &contract + .construct( + [( + Balance::STORAGE_KEY.to_bytes32().into(), + vec![42].try_into()?, + )] + .into_iter() + .collect(), + )? + .bytecode()?, + )?; + + // Try to transfer 21 (should succeed) + let info = evm + .calldata(&contract.encode(&[ + b"check_and_update(int32)".to_vec(), + 21i32.to_bytes32().to_vec(), + ])?) + .call(info.address)?; + assert_eq!(info.ret, true.to_bytes32(), "{info:?}"); + + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn main() {} diff --git a/examples/bytes.rs b/examples/bytes.rs new file mode 100644 index 000000000..69ec6084a --- /dev/null +++ b/examples/bytes.rs @@ -0,0 +1,40 @@ +//! Storage example. +#![cfg_attr(target_arch = "wasm32", no_std)] +#![cfg_attr(target_arch = "wasm32", no_main)] + +extern crate zink; + +use zink::{primitives::Bytes16, Storage}; + +/// Counter with value type `Bytes16` +#[zink::storage(Bytes16)] +pub struct Bytes; + +/// set value to the storage. +#[zink::external] +pub fn set(value: Bytes16) { + Bytes::set(value); +} + +#[cfg(not(target_arch = "wasm32"))] +fn main() {} + +#[test] +fn value() -> anyhow::Result<()> { + use zink::Asm; + use zint::Contract; + + let mut contract = Contract::search("bytes")?.compile()?; + let new_storage = [8u8; 16]; + let mut evm = contract.deploy()?.commit(true); + + evm.calldata(&contract.encode(&[b"set(bytes)".to_vec(), new_storage.to_vec()])?) + .call(contract.address)?; + + assert_eq!( + evm.storage(contract.address, [0; 32])?, + Bytes16(new_storage).bytes32(), + ); + + Ok(()) +} diff --git a/examples/erc20.rs b/examples/erc20.rs index f24134761..3b21fcaf3 100644 --- a/examples/erc20.rs +++ b/examples/erc20.rs @@ -78,6 +78,7 @@ fn _update(from: Address, to: Address, value: U256) { TotalSupply::set(TotalSupply::get().add(value)); } else { let from_balance = Balances::get(from); + if from_balance.lt(value) { zink::revert!("Insufficient balance"); } @@ -143,6 +144,7 @@ fn deploy() -> anyhow::Result<()> { use zint::{Bytes32, Contract, EVM}; let caller_bytes = hex::decode("be862ad9abfe6f22bcb087716c7d89a26051f74c")?; + let spender = [42; 20]; let mut caller = [0; 20]; caller.copy_from_slice(&caller_bytes); @@ -150,6 +152,8 @@ fn deploy() -> anyhow::Result<()> { let mut contract = Contract::search("erc20")?.compile()?; let name = "The Zink Language"; let symbol = "zink"; + let value = 42; + //let half_value = 21; // 1. deploy let info = evm.deploy( @@ -165,6 +169,10 @@ fn deploy() -> anyhow::Result<()> { TotalSupply::STORAGE_KEY.to_bytes32().into(), vec![42].try_into()?, ), + ( + Balances::storage_key(Address::from(caller)).into(), + vec![42].try_into()?, + ), ] .into_iter() .collect(), @@ -173,58 +181,90 @@ fn deploy() -> anyhow::Result<()> { )?; let address = info.address; - // 2. get name - let info = evm - .calldata(&contract.encode(&[b"name()".to_vec()])?) - .call(address)?; - assert_eq!(info.ret, name.to_bytes32()); - - // 3. get symbol - let info = evm - .calldata(&contract.encode(&[b"symbol()".to_vec()])?) - .call(address)?; - assert_eq!(info.ret, symbol.to_bytes32()); - - // 4. get total supply - let info = evm - .calldata(&contract.encode(&[b"total_supply()".to_vec()])?) - .call(address)?; - assert_eq!(info.ret, 42u64.to_bytes32()); - - // 5. check decimals - let info = evm - .calldata(&contract.encode(&[b"decimals()".to_vec()])?) - .call(address)?; - assert_eq!(info.ret, 8u64.to_bytes32()); - - // TODO: refactor offset handling (#280) - // // 6. check approval - // let value = 42; - // let spender = [42; 20]; - // let info = evm - // .calldata(&contract.encode(&[ - // b"approve(address,uint256)".to_vec(), - // spender.to_bytes32().to_vec(), - // value.to_bytes32().to_vec(), - // ])?) - // .call(address)?; - // assert_eq!(info.ret, true.to_bytes32(), "{info:?}"); - // - // let allowance = evm.storage( - // address, - // Allowance::storage_key(Address(evm.caller), Address(spender)), - // )?; - // assert_eq!(value.to_bytes32(), allowance); - // - // // 7. check approval results - // let info = evm - // .calldata(&contract.encode(&[ - // b"allowance(address,address)".to_vec(), - // evm.caller.to_bytes32().to_vec(), - // spender.to_bytes32().to_vec(), - // ])?) - // .call(address)?; - // assert_eq!(info.ret, allowance); + // 2. get static data + { + // 2.1. get name + let info = evm + .calldata(&contract.encode(&[b"name()".to_vec()])?) + .call(address)?; + assert_eq!(info.ret, name.to_bytes32(), "{info:?}"); + + // 2.2. get symbol + let info = evm + .calldata(&contract.encode(&[b"symbol()".to_vec()])?) + .call(address)?; + assert_eq!(info.ret, symbol.to_bytes32(), "{info:?}"); + + // 2.3. get total supply + let info = evm + .calldata(&contract.encode(&[b"total_supply()".to_vec()])?) + .call(address)?; + assert_eq!(info.ret, 42u64.to_bytes32(), "{info:?}"); + + // 2.4. check decimals + let info = evm + .calldata(&contract.encode(&[b"decimals()".to_vec()])?) + .call(address)?; + assert_eq!(info.ret, 8u64.to_bytes32(), "{info:?}"); + + // 2.5. check balance of the caller + let balance = evm.storage(address, Balances::storage_key(Address::from(caller)))?; + assert_eq!(value.to_bytes32(), balance); + } + + // 3. check approval + { + // 3.1. approve + let info = evm + .calldata(&contract.encode(&[ + b"approve(address,uint256)".to_vec(), + spender.to_bytes32().to_vec(), + value.to_bytes32().to_vec(), + ])?) + .call(address)?; + assert_eq!(info.ret, true.to_bytes32(), "{info:?}"); + + let allowance = evm.storage( + address, + Allowance::storage_key(Address::from(evm.caller), Address::from(spender)), + )?; + assert_eq!(value.to_bytes32(), allowance); + + // 3.2. check approval results + let info = evm + .calldata(&contract.encode(&[ + b"allowance(address,address)".to_vec(), + evm.caller.to_bytes32().to_vec(), + spender.to_bytes32().to_vec(), + ])?) + .call(address)?; + assert_eq!(info.ret, allowance); + } + + // 4. check transfer + { + // 4.1. verify balance of the caller + let info = evm + .calldata(&contract.encode(&[ + b"balances(address)".to_vec(), + evm.caller.to_bytes32().to_vec(), + ])?) + .call(address)?; + assert_eq!(info.ret, value.to_bytes32(), "{info:?}"); + + // TODO: see br_balance.rs (#287) + // 4.2. check transfer + /* evm = evm.commit(false); + let info = evm + .calldata(&contract.encode(&[ + b"transfer(address,uint256)".to_vec(), + spender.to_bytes32().to_vec(), + half_value.to_bytes32().to_vec(), + ])?) + .call(address)?; + println!("{info:?}"); + assert_eq!(info.ret, true.to_bytes32(), "{info:?}"); */ + } Ok(()) } diff --git a/examples/revert.rs b/examples/revert.rs index d0ae2d7cf..620e1197f 100644 --- a/examples/revert.rs +++ b/examples/revert.rs @@ -9,10 +9,16 @@ fn main() {} /// check if the passing address is owner #[zink::external] -pub fn run_revert() { +pub fn revert() { zink::revert!("revert works") } +/// check if the passing address is owner +#[zink::external] +pub fn assert() { + zink::assert!(false, "assert works"); +} + #[test] fn test_revert() -> anyhow::Result<()> { use zint::Contract; @@ -20,5 +26,8 @@ fn test_revert() -> anyhow::Result<()> { let info = contract.execute(["revert()".as_bytes()])?; assert_eq!(info.revert, Some("revert works".into())); + + let info = contract.execute(["assert()".as_bytes()])?; + assert_eq!(info.revert, Some("assert works".into())); Ok(()) } diff --git a/examples/transient_storage.rs b/examples/transient_storage.rs new file mode 100644 index 000000000..4dc48ab2b --- /dev/null +++ b/examples/transient_storage.rs @@ -0,0 +1,39 @@ +//! Transient Storage example. +#![cfg_attr(target_arch = "wasm32", no_std)] +#![cfg_attr(target_arch = "wasm32", no_main)] + +extern crate zink; + +use zink::storage::TransientStorage; +/// Temporary counter with value type `i32` that resets after each transaction +#[zink::transient_storage(i32)] +pub struct TempCounter; + +/// Set and get value via the transient storage. +#[zink::external] +pub fn set_and_get_temp(value: i32) -> i32 { + TempCounter::set(value); + TempCounter::get() +} + +#[cfg(not(target_arch = "wasm32"))] +fn main() {} + +#[test] +fn transient_value() -> anyhow::Result<()> { + use zint::{Bytes32, Contract}; + + let mut contract = Contract::search("transient_storage")?.compile()?; + let value: i32 = 42; + + { + let info = contract.execute(&[ + b"set_and_get_temp(int32)".to_vec(), + value.to_bytes32().to_vec(), + ])?; + assert!(!info.ret.is_empty()); + assert_eq!(info.ret.to_bytes32(), value.to_bytes32()); + } + + Ok(()) +} diff --git a/tests/br_if.rs b/tests/br_if.rs index ff83dcda3..51714e71d 100644 --- a/tests/br_if.rs +++ b/tests/br_if.rs @@ -1,7 +1,7 @@ //! br_if tests for the zink compiler. use anyhow::Result; use filetests::Test; -use zint::Contract; +use zint::{Contract, HaltReason}; #[test] fn as_block_last() -> Result<()> { @@ -12,7 +12,7 @@ fn as_block_last() -> Result<()> { assert!(info.ret.is_empty()); let info = contract.execute(&[42])?; - assert!(info.halt.is_none()); + assert!(matches!(info.halt, Some(HaltReason::OutOfGas(_)))); assert!(info.ret.is_empty()); Ok(()) diff --git a/abi/Cargo.toml b/zink/abi/Cargo.toml similarity index 100% rename from abi/Cargo.toml rename to zink/abi/Cargo.toml diff --git a/abi/README.md b/zink/abi/README.md similarity index 100% rename from abi/README.md rename to zink/abi/README.md diff --git a/abi/src/abi.rs b/zink/abi/src/abi.rs similarity index 100% rename from abi/src/abi.rs rename to zink/abi/src/abi.rs diff --git a/abi/src/lib.rs b/zink/abi/src/lib.rs similarity index 100% rename from abi/src/lib.rs rename to zink/abi/src/lib.rs diff --git a/abi/src/result.rs b/zink/abi/src/result.rs similarity index 100% rename from abi/src/result.rs rename to zink/abi/src/result.rs diff --git a/abi/src/selector.rs b/zink/abi/src/selector.rs similarity index 100% rename from abi/src/selector.rs rename to zink/abi/src/selector.rs diff --git a/zink/codegen/src/lib.rs b/zink/codegen/src/lib.rs index 38b8e877d..776ddaa54 100644 --- a/zink/codegen/src/lib.rs +++ b/zink/codegen/src/lib.rs @@ -4,7 +4,9 @@ extern crate proc_macro; use proc_macro::TokenStream; -use syn::{parse_macro_input, Attribute, DeriveInput, ItemFn, ItemStruct, LitStr}; +use proc_macro2::Span; +use quote::ToTokens; +use syn::{parse_macro_input, Attribute, DeriveInput, Expr, ItemFn, ItemStruct, LitStr}; mod event; mod revert; @@ -21,6 +23,16 @@ pub fn revert(input: TokenStream) -> TokenStream { revert::parse(input) } +/// Check and expression and revert with the input message +/// +/// This is similar with the builtin `assert!` in rust, but the revert +/// message only support raw string. +#[proc_macro] +pub fn assert(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as revert::AssertInput); + revert::parse_assert(input) +} + /// Event logging interface /// /// ```ignore @@ -69,6 +81,24 @@ pub fn storage(attr: TokenStream, input: TokenStream) -> TokenStream { storage::Storage::parse(ty, input) } +/// Declare transient storage (cleared after each transaction) +/// +/// ```ignore +/// /// transient storage value +/// #[zink::transient_storage(i32)] +/// pub struct TempCounter; +/// +/// /// transient storage mapping +/// #[zink::transient_storage(i32, i32)] +/// pub struct TempMapping; +/// ``` +#[proc_macro_attribute] +pub fn transient_storage(attr: TokenStream, input: TokenStream) -> TokenStream { + let ty = storage::StorageType::from(attr); + let input = parse_macro_input!(input as ItemStruct); + storage::Storage::parse_transient(ty, input) +} + /// Mark the function as an external entry point. #[proc_macro_attribute] pub fn external(_args: TokenStream, input: TokenStream) -> TokenStream { diff --git a/zink/codegen/src/revert.rs b/zink/codegen/src/revert.rs index e03076cdb..e8fc1305c 100644 --- a/zink/codegen/src/revert.rs +++ b/zink/codegen/src/revert.rs @@ -3,7 +3,10 @@ use proc_macro::TokenStream; use proc_macro2::{Literal, Span}; use quote::{quote, ToTokens}; -use syn::{Ident, LitStr}; +use syn::{ + parse::{Parse, ParseStream}, + parse2, Expr, Ident, LitStr, Token, +}; /// Revert with message pub fn parse(input: LitStr) -> TokenStream { @@ -34,3 +37,41 @@ pub fn parse(input: LitStr) -> TokenStream { } .into() } + +/// Parse assert macro +pub fn parse_assert(input: AssertInput) -> TokenStream { + let cond = input.cond; + let revert: Expr = syn::parse2( + parse( + input + .message + .unwrap_or(LitStr::new("unknown error", Span::call_site())), + ) + .into(), + ) + .expect("Invalid revert message"); + + quote! { + if !#cond { + #revert + } + } + .into() +} + +/// Assert input +pub struct AssertInput { + pub cond: Expr, + pub comma: Token![,], + pub message: Option, +} + +impl Parse for AssertInput { + fn parse(input: ParseStream) -> syn::Result { + Ok(AssertInput { + cond: input.parse()?, + comma: input.parse()?, + message: input.parse()?, + }) + } +} diff --git a/zink/codegen/src/storage.rs b/zink/codegen/src/storage.rs index 58b33a1ac..7f73ca863 100644 --- a/zink/codegen/src/storage.rs +++ b/zink/codegen/src/storage.rs @@ -12,10 +12,20 @@ use syn::{ thread_local! { static STORAGE_REGISTRY: RefCell> = RefCell::new(HashSet::new()); + static TRANSIENT_STORAGE_REGISTRY: RefCell> = RefCell::new(HashSet::new()); +} + +/// Storage type (persistent or transient) +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum StorageKind { + Persistent, + Transient, } /// Storage attributes parser pub struct Storage { + /// Storage kind (persistent or transient) + kind: StorageKind, /// kind of the storage ty: StorageType, /// The source and the target storage struct @@ -25,12 +35,48 @@ pub struct Storage { } impl Storage { - /// Parse from proc_macro attribute + /// Parse from proc_macro attribute for persistent storage pub fn parse(ty: StorageType, target: ItemStruct) -> TokenStream { - let storage = Self::from((ty, target)); + let storage = Self::from_parts(StorageKind::Persistent, ty, target); + storage.expand() + } + + /// Parse from proc_macro attribute for transient storage + pub fn parse_transient(ty: StorageType, target: ItemStruct) -> TokenStream { + let storage = Self::from_parts(StorageKind::Transient, ty, target); storage.expand() } + fn from_parts(kind: StorageKind, ty: StorageType, target: ItemStruct) -> Self { + let mut this = Self { + kind, + ty, + target, + 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!("Invalid 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 + } + fn expand(mut self) -> TokenStream { match &self.ty { StorageType::Value(value) => self.expand_value(value.clone()), @@ -45,14 +91,19 @@ impl Storage { 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 slot = self.get_storage_slot(name.to_string()); let key = slot.to_bytes32(); let keyl = Literal::byte_string(&key); + let trait_path = match self.kind { + StorageKind::Persistent => quote!(zink::storage::Storage), + StorageKind::Transient => quote!(zink::storage::TransientStorage), + }; + let mut expanded = quote! { #is - impl zink::storage::Storage for #name { + impl #trait_path for #name { #[cfg(not(target_family = "wasm"))] const STORAGE_KEY: [u8; 32] = *#keyl; const STORAGE_SLOT: i32 = #slot; @@ -62,7 +113,6 @@ impl Storage { }; 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] @@ -79,12 +129,17 @@ impl Storage { 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 slot = self.get_storage_slot(name.to_string()); + + let trait_path = match self.kind { + StorageKind::Persistent => quote!(zink::storage::Mapping), + StorageKind::Transient => quote!(zink::transient_storage::TransientMapping), + }; let mut expanded = quote! { #is - impl zink::storage::Mapping for #name { + impl #trait_path for #name { const STORAGE_SLOT: i32 = #slot; type Key = #key; @@ -103,7 +158,6 @@ impl Storage { }; 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] @@ -120,12 +174,17 @@ impl Storage { 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 slot = self.get_storage_slot(name.to_string()); + + let trait_path = match self.kind { + StorageKind::Persistent => quote!(zink::storage::DoubleKeyMapping), + StorageKind::Transient => quote!(zink::transient_storage::DoubleKeyTransientMapping), + }; let mut expanded = quote! { #is - impl zink::DoubleKeyMapping for #name { + impl #trait_path for #name { const STORAGE_SLOT: i32 = #slot; type Key1 = #key1; @@ -148,7 +207,6 @@ impl Storage { }; 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] @@ -162,6 +220,25 @@ impl Storage { expanded.into() } + fn get_storage_slot(&self, name: String) -> i32 { + match self.kind { + StorageKind::Persistent => 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, + StorageKind::Transient => TRANSIENT_STORAGE_REGISTRY.with_borrow_mut(|r| { + let key = r.len(); + if !r.insert(name.clone()) { + panic!("Transient storage {name} has already been declared"); + } + key + }) as i32, + } + } + /// Get the getter of this storage fn getter(&mut self) -> Option { let mut getter = if matches!(self.target.vis, Visibility::Public(_)) { @@ -178,37 +255,6 @@ impl Storage { } } -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 { @@ -246,14 +292,3 @@ impl From for StorageType { } } } - -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/elko/Cargo.toml b/zink/elko/Cargo.toml similarity index 100% rename from elko/Cargo.toml rename to zink/elko/Cargo.toml diff --git a/elko/README.md b/zink/elko/README.md similarity index 100% rename from elko/README.md rename to zink/elko/README.md diff --git a/elko/src/bin/elko.rs b/zink/elko/src/bin/elko.rs similarity index 100% rename from elko/src/bin/elko.rs rename to zink/elko/src/bin/elko.rs diff --git a/elko/src/build.rs b/zink/elko/src/build.rs similarity index 100% rename from elko/src/build.rs rename to zink/elko/src/build.rs diff --git a/elko/src/lib.rs b/zink/elko/src/lib.rs similarity index 100% rename from elko/src/lib.rs rename to zink/elko/src/lib.rs diff --git a/elko/src/new.rs b/zink/elko/src/new.rs similarity index 100% rename from elko/src/new.rs rename to zink/elko/src/new.rs diff --git a/elko/src/utils/manifest.rs b/zink/elko/src/utils/manifest.rs similarity index 100% rename from elko/src/utils/manifest.rs rename to zink/elko/src/utils/manifest.rs diff --git a/elko/src/utils/mod.rs b/zink/elko/src/utils/mod.rs similarity index 100% rename from elko/src/utils/mod.rs rename to zink/elko/src/utils/mod.rs diff --git a/elko/src/utils/result.rs b/zink/elko/src/utils/result.rs similarity index 100% rename from elko/src/utils/result.rs rename to zink/elko/src/utils/result.rs diff --git a/elko/src/utils/wasm.rs b/zink/elko/src/utils/wasm.rs similarity index 100% rename from elko/src/utils/wasm.rs rename to zink/elko/src/utils/wasm.rs diff --git a/zink/src/ffi/asm.rs b/zink/src/ffi/asm.rs index b308531a0..16870b707 100644 --- a/zink/src/ffi/asm.rs +++ b/zink/src/ffi/asm.rs @@ -1,7 +1,5 @@ //! Assembly FFI. -use crate::primitives::{Address, U256}; - #[link(wasm_import_module = "asm")] #[allow(improper_ctypes)] extern "C" { @@ -29,11 +27,45 @@ extern "C" { /// Push a 64-bit unsigned integer to the stack. pub fn push_u64(val: u64); - /// Push address to stack - pub fn push_address(address: Address); - - /// Push u256 to stack - pub fn push_u256(u256: U256); + /// Emit opcode ADDMOD + pub fn addmod_i8(a: i8, b: i8, n: i8) -> i8; + /// Emit opcode ADDMOD + pub fn mulmod_i8(a: i8, b: i8, n: i8) -> i8; + + /// Emit opcode ADDMOD + pub fn addmod_i16(a: i16, b: i16, n: i16) -> i16; + /// Emit opcode ADDMOD + pub fn mulmod_i16(a: i16, b: i16, n: i16) -> i16; + + /// Emit opcode ADDMOD + pub fn addmod_i32(a: i32, b: i32, n: i32) -> i32; + /// Emit opcode ADDMOD + pub fn mulmod_i32(a: i32, b: i32, n: i32) -> i32; + + /// Emit opcode ADDMOD + pub fn addmod_i64(a: i64, b: i64, n: i64) -> i64; + /// Emit opcode ADDMOD + pub fn mulmod_i64(a: i64, b: i64, n: i64) -> i64; + + /// Emit opcode ADDMOD + pub fn addmod_u8(a: u8, b: u8, n: u8) -> u8; + /// Emit opcode ADDMOD + pub fn mulmod_u8(a: u8, b: u8, n: u8) -> u8; + + /// Emit opcode ADDMOD + pub fn addmod_u16(a: u16, b: u16, n: u16) -> u16; + /// Emit opcode ADDMOD + pub fn mulmod_u16(a: u16, b: u16, n: u16) -> u16; + + /// Emit opcode ADDMOD + pub fn addmod_u32(a: u32, b: u32, n: u32) -> u32; + /// Emit opcode ADDMOD + pub fn mulmod_u32(a: u32, b: u32, n: u32) -> u32; + + /// Emit opcode ADDMOD + pub fn addmod_u64(a: u64, b: u64, n: u64) -> u64; + /// Emit opcode ADDMOD + pub fn mulmod_u64(a: u64, b: u64, n: u64) -> u64; /// Revert with message in 32 bytes pub fn revert1(message: &'static str); @@ -71,9 +103,51 @@ extern "C" { /// Load a 64-bit unsigned integer from the storage. pub fn sload_u64() -> u64; - /// Load address from storage - pub fn sload_address() -> Address; + /// Load a 8-bit signed integer from the transient storage. + pub fn tload_i8() -> i8; + + /// Load a 8-bit unsigned integer from the transient storage. + pub fn tload_u8() -> u8; + + /// Load a 16-bit signed integer from the transient storage. + pub fn tload_i16() -> i16; + + /// Load a 16-bit unsigned integer from the transient storage. + pub fn tload_u16() -> u16; + + /// Load a 32-bit signed integer from the transient storage. + pub fn tload_i32() -> i32; + + /// Load a 32-bit unsigned integer from the transient storage. + pub fn tload_u32() -> u32; + + /// Load a 64-bit signed integer from the transient storage. + pub fn tload_i64() -> i64; + + /// Load a 64-bit unsigned integer from the transient storage. + pub fn tload_u64() -> u64; + + /// Store a 8-bit signed integer to the transient storage. + pub fn tstore_i8(val: i8); + + /// Store a 8-bit unsigned integer to the transient storage. + pub fn tstore_u8(val: u8); + + /// Store a 16-bit signed integer to the transient storage. + pub fn tstore_i16(val: i16); + + /// Store a 16-bit unsigned integer to the transient storage. + pub fn tstore_u16(val: u16); + + /// Store a 32-bit signed integer to the transient storage. + pub fn tstore_i32(val: i32); + + /// Store a 32-bit unsigned integer to the transient storage. + pub fn tstore_u32(val: u32); + + /// Store a 64-bit signed integer to the transient storage. + pub fn tstore_i64(val: i64); - /// Load address from storage - pub fn sload_u256() -> U256; + /// Store a 64-bit unsigned integer to the transient storage. + pub fn tstore_u64(val: u64); } diff --git a/zink/src/ffi/bytes.rs b/zink/src/ffi/bytes.rs new file mode 100644 index 000000000..83dbb3eb4 --- /dev/null +++ b/zink/src/ffi/bytes.rs @@ -0,0 +1,32 @@ +//! Bytes based instructions + +use crate::primitives::*; + +macro_rules! impl_bytes { + ($($count:expr),*) => { + #[link(wasm_import_module = "bytes")] + #[allow(improper_ctypes)] + extern "C" { + paste::paste! { + $( + #[doc = concat!("Push ", stringify!($count), " bytes to stack")] + pub fn [< push_bytes $count >] (bytes: [< Bytes $count >]); + + #[doc = concat!("Load ", stringify!($count), " bytes from storage")] + pub fn [< sload_bytes $count >] () -> [< Bytes $count >]; + + #[doc = concat!("TLoad ", stringify!($count), " bytes from transient storage")] + pub fn [< tload_bytes $count >] () -> [< Bytes $count >]; + + #[doc = concat!("Check equal for bytes", stringify!($count))] + pub fn [< bytes $count _eq >] (this: [< Bytes $count >], other: [< Bytes $count >]) -> bool; + )* + } + } + }; +} + +impl_bytes!( + 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/ffi/evm.rs b/zink/src/ffi/evm.rs index c3b00e297..fede71d7b 100644 --- a/zink/src/ffi/evm.rs +++ b/zink/src/ffi/evm.rs @@ -110,6 +110,12 @@ extern "C" { /// Load a value from the storage pub fn sload(); + /// Store a value in the transient storage + pub fn tstore(); + + /// Load a value from the transient storage + pub fn tload(); + /// Save word to memory pub fn mstore(); @@ -119,12 +125,21 @@ extern "C" { /// Load word from memory pub fn mload(); + /// Copy memory to memory + pub fn mcopy(); + /// Compute Keccak-256 hash pub fn keccak256(); /// Get the current message sender pub fn caller() -> Address; + /// Get the current blob hash at index + pub fn blobhash(); + + /// Get the current blob base fee + pub fn blobbasefee(); + /// Append log record with no topics pub fn log0(name: &'static [u8]); diff --git a/zink/src/ffi/mod.rs b/zink/src/ffi/mod.rs index 9ffad9ef9..1b7e3b328 100644 --- a/zink/src/ffi/mod.rs +++ b/zink/src/ffi/mod.rs @@ -1,8 +1,7 @@ //! Zink FFI. - use crate::primitives::{Address, Bytes32, U256}; - pub mod asm; +pub mod bytes; pub mod evm; #[link(wasm_import_module = "zinkc")] @@ -11,9 +10,6 @@ extern "C" { /// Emit ABI to host state. pub fn emit_abi(ptr: u32, len: u32); - /// Equal operation for addresses - pub fn address_eq(this: Address, other: Address) -> bool; - /// Equal operation for addresses pub fn u256_add(this: U256, other: U256) -> U256; @@ -25,10 +21,14 @@ extern "C" { /// Equal operation for addresses pub fn u256_max() -> U256; - + /// Cast U256 to bytes32 pub fn cast_bytes32(value: U256) -> Bytes32; + /// Addmod operation for addresses + pub fn u256_addmod(this: U256, other: U256, modulus: U256) -> U256; + /// Equal operation for addresses + pub fn u256_mulmod(this: U256, other: U256, modulus: U256) -> U256; /// Set up a label for reserving 32 bytes in memory pub fn label_reserve_mem_32(); diff --git a/zink/src/lib.rs b/zink/src/lib.rs index 3aeee8205..870538e3a 100644 --- a/zink/src/lib.rs +++ b/zink/src/lib.rs @@ -10,10 +10,9 @@ mod event; pub mod ffi; pub mod primitives; pub mod storage; - pub use self::{asm::Asm, event::Event}; -pub use storage::{DoubleKeyMapping, Mapping, Storage}; -pub use zink_codegen::{external, revert, storage, Event}; +pub use storage::{DoubleKeyMapping, Mapping, Storage, TransientStorage}; +pub use zink_codegen::{assert, external, revert, storage, transient_storage, Event}; /// Generate a keccak hash of the input (sha3) #[cfg(not(target_family = "wasm"))] diff --git a/zink/src/primitives/bytes.rs b/zink/src/primitives/bytes.rs new file mode 100644 index 000000000..8e4533b3a --- /dev/null +++ b/zink/src/primitives/bytes.rs @@ -0,0 +1,65 @@ +//! Fixed bytes +use crate::{ffi, storage::StorageValue, Asm}; +use paste::paste; + +macro_rules! impl_bytes { + ($count:expr) => { + paste! { + #[derive(Debug, Clone, Copy)] + pub struct [] ( + #[allow(unused)] + #[cfg(target_family = "wasm")] i32, + #[cfg(not(target_family = "wasm"))] pub [u8; $count], + ); + + impl [] { + /// Returns empty bytes + #[cfg(target_family = "wasm")] + pub const fn empty() -> Self { + [](0) + } + + #[cfg(not(target_family = "wasm"))] + pub const fn empty() -> Self { + []([0; $count]) + } + + /// if self equal to another + #[allow(clippy::should_implement_trait)] + #[inline(always)] + pub fn eq(self, other: Self) -> bool { + paste::paste! { + unsafe { ffi::bytes::[< bytes $count _eq >](self, other) } + } + } + } + + impl Asm for [] { + fn push(self) { + unsafe { ffi::bytes::[](self) } + } + + #[cfg(not(target_family = "wasm"))] + fn bytes32(&self) -> [u8; 32] { + let mut output = [0; 32]; + output[(32-$count)..].copy_from_slice(&self.0); + output + } + } + + impl StorageValue for [] { + fn sload() -> Self { + unsafe { ffi::bytes::[]() } + } + } + } + }; + ($($count:expr),+) => { + $(impl_bytes!($count);)+ + }; +} + +impl_bytes! { + 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/primitives/mod.rs b/zink/src/primitives/mod.rs index 67b4af318..f703ad767 100644 --- a/zink/src/primitives/mod.rs +++ b/zink/src/primitives/mod.rs @@ -1,11 +1,12 @@ //! Zink primitive types mod address; +pub mod bytes; +pub mod numeric; mod u256; -pub use address::Address; -pub use u256::U256; +pub use {address::Address, bytes::*, u256::U256}; -pub type Bytes20 = Address; -pub type Bytes32 = U256; +// pub type Address = Bytes20; +// pub type Bytes32 = U256; pub type String32 = U256; diff --git a/zink/src/primitives/numeric.rs b/zink/src/primitives/numeric.rs new file mode 100644 index 000000000..b819c6200 --- /dev/null +++ b/zink/src/primitives/numeric.rs @@ -0,0 +1,35 @@ +use crate::ffi; + +/// A trait for modular arithmetic operations on numeric types. +pub trait Numeric: Copy { + fn addmod(self, other: Self, n: Self) -> Self; + fn mulmod(self, other: Self, n: Self) -> Self; +} + +macro_rules! impl_numeric { + ($($t:ty, $addmod_fn:ident, $mulmod_fn:ident);* $(;)?) => { + $( + impl Numeric for $t { + #[inline(always)] + fn addmod(self, other: Self, n: Self) -> Self { + unsafe { ffi::asm::$addmod_fn(n, other, self) } + } + #[inline(always)] + fn mulmod(self, other: Self, n: Self) -> Self { + unsafe { ffi::asm::$mulmod_fn(n, other, self) } + } + } + )* + }; +} + +impl_numeric! { + i8, addmod_i8, mulmod_i8; + u8, addmod_u8, mulmod_u8; + i16, addmod_i16, mulmod_i16; + u16, addmod_u16, mulmod_u16; + i32, addmod_i32, mulmod_i32; + u32, addmod_u32, mulmod_u32; + i64, addmod_i64, mulmod_i64; + u64, addmod_u64, mulmod_u64; +} diff --git a/zink/src/storage/dkmapping.rs b/zink/src/storage/dkmapping.rs index d2c26c2c5..003f9a818 100644 --- a/zink/src/storage/dkmapping.rs +++ b/zink/src/storage/dkmapping.rs @@ -1,6 +1,10 @@ //! Double key mapping -use crate::{ffi, storage::StorageValue, Asm}; +use crate::{ + ffi, + storage::{StorageValue, TransientStorageValue}, + Asm, +}; /// Storage mapping interface pub trait DoubleKeyMapping { @@ -31,6 +35,35 @@ pub trait DoubleKeyMapping { } } +/// Transient storage mapping interface +pub trait DoubleKeyTransientMapping { + const STORAGE_SLOT: i32; + + type Key1: Asm; + type Key2: Asm; + type Value: TransientStorageValue; + + #[cfg(not(target_family = "wasm"))] + fn storage_key(key1: Self::Key1, key2: Self::Key2) -> [u8; 32]; + + /// Get value from transient storage key. + #[inline(always)] + fn get(key1: Self::Key1, key2: Self::Key2) -> Self::Value { + load_double_key(key1, key2, Self::STORAGE_SLOT); + Self::Value::tload() + } + + /// Set key and value in transient storage + #[inline(always)] + fn set(key1: Self::Key1, key2: Self::Key2, value: Self::Value) { + value.push(); + load_double_key(key1, key2, Self::STORAGE_SLOT); + unsafe { + ffi::evm::tstore(); + } + } +} + /// Load storage key to stack #[inline(always)] fn load_double_key(key1: impl Asm, key2: impl Asm, index: i32) { diff --git a/zink/src/storage/mapping.rs b/zink/src/storage/mapping.rs index 669c8f836..d38b1c201 100644 --- a/zink/src/storage/mapping.rs +++ b/zink/src/storage/mapping.rs @@ -1,6 +1,10 @@ //! Storage Mapping -use crate::{ffi, storage::StorageValue, Asm}; +use crate::{ + ffi, + storage::{StorageValue, TransientStorageValue}, + Asm, +}; /// Storage mapping interface pub trait Mapping { @@ -28,6 +32,32 @@ pub trait Mapping { } } +/// Transient storage mapping interface +pub trait TransientMapping { + const STORAGE_SLOT: i32; + + type Key: Asm; + type Value: TransientStorageValue; + + #[cfg(not(target_family = "wasm"))] + fn storage_key(key: Self::Key) -> [u8; 32]; + + /// Get value from transient storage key. + fn get(key: Self::Key) -> Self::Value { + load_key(key, Self::STORAGE_SLOT); + Self::Value::tload() + } + + /// Set key and value in transient storage + fn set(key: Self::Key, value: Self::Value) { + value.push(); + load_key(key, Self::STORAGE_SLOT); + unsafe { + ffi::evm::tstore(); + } + } +} + /// Load storage key to stack fn load_key(key: impl Asm, index: i32) { unsafe { diff --git a/zink/src/storage/mod.rs b/zink/src/storage/mod.rs index d7ecc018e..3d16da345 100644 --- a/zink/src/storage/mod.rs +++ b/zink/src/storage/mod.rs @@ -1,7 +1,11 @@ //! Zink storage implementation. use crate::{ffi, Asm}; -pub use {dkmapping::DoubleKeyMapping, mapping::Mapping, value::Storage}; +pub use { + dkmapping::{DoubleKeyMapping, DoubleKeyTransientMapping}, + mapping::{Mapping, TransientMapping}, + value::{Storage, TransientStorage}, +}; mod dkmapping; mod mapping; @@ -13,6 +17,12 @@ pub trait StorageValue: Asm { fn sload() -> Self; } +/// Interface for the value of kv based transient storage +pub trait TransientStorageValue: Asm { + /// Load from transient storage + fn tload() -> Self; +} + impl StorageValue for i32 { fn sload() -> Self { unsafe { ffi::asm::sload_i32() } @@ -24,3 +34,15 @@ impl StorageValue for u32 { unsafe { ffi::asm::sload_u32() } } } + +impl TransientStorageValue for i32 { + fn tload() -> Self { + unsafe { ffi::asm::tload_i32() } + } +} + +impl TransientStorageValue for u32 { + fn tload() -> Self { + unsafe { ffi::asm::tload_u32() } + } +} diff --git a/zink/src/storage/value.rs b/zink/src/storage/value.rs index d3c5caa3d..20d817fee 100644 --- a/zink/src/storage/value.rs +++ b/zink/src/storage/value.rs @@ -1,5 +1,9 @@ //! Key-Value storage -use crate::{ffi, storage::StorageValue, Asm}; +use crate::{ + ffi, + storage::{StorageValue, TransientStorageValue}, + Asm, +}; /// Storage trait. Currently not for public use pub trait Storage { @@ -24,3 +28,27 @@ pub trait Storage { } } } + +/// Transient storage trait. Currently not for public use +pub trait TransientStorage { + #[cfg(not(target_family = "wasm"))] + const STORAGE_KEY: [u8; 32]; + const STORAGE_SLOT: i32; + + type Value: TransientStorageValue + Asm; + + /// Get value from transient storage. + fn get() -> Self::Value { + Asm::push(Self::STORAGE_SLOT); + Self::Value::tload() + } + + /// Set value to transient storage. + fn set(value: Self::Value) { + value.push(); + Asm::push(Self::STORAGE_SLOT); + unsafe { + ffi::evm::tstore(); + } + } +} diff --git a/zint/Cargo.toml b/zink/zint/Cargo.toml similarity index 100% rename from zint/Cargo.toml rename to zink/zint/Cargo.toml diff --git a/zint/README.md b/zink/zint/README.md similarity index 100% rename from zint/README.md rename to zink/zint/README.md diff --git a/zint/src/bytes.rs b/zink/zint/src/bytes.rs similarity index 100% rename from zint/src/bytes.rs rename to zink/zint/src/bytes.rs diff --git a/zint/src/contract.rs b/zink/zint/src/contract.rs similarity index 100% rename from zint/src/contract.rs rename to zink/zint/src/contract.rs diff --git a/zint/src/evm.rs b/zink/zint/src/evm.rs similarity index 99% rename from zint/src/evm.rs rename to zink/zint/src/evm.rs index b12316282..93342c6b2 100644 --- a/zint/src/evm.rs +++ b/zink/zint/src/evm.rs @@ -43,7 +43,7 @@ impl<'e> Default for EVM<'e> { } } -impl<'e> EVM<'e> { +impl EVM<'_> { /// Interpret runtime bytecode with provided arguments pub fn interp(runtime_bytecode: &[u8], input: &[u8]) -> Result { Self::default() diff --git a/zint/src/lib.rs b/zink/zint/src/lib.rs similarity index 100% rename from zint/src/lib.rs rename to zink/zint/src/lib.rs diff --git a/zint/src/lookup.rs b/zink/zint/src/lookup.rs similarity index 100% rename from zint/src/lookup.rs rename to zink/zint/src/lookup.rs diff --git a/zint/tests/addition.rs b/zink/zint/tests/addition.rs similarity index 100% rename from zint/tests/addition.rs rename to zink/zint/tests/addition.rs