diff --git a/codegen/src/visitor/call.rs b/codegen/src/visitor/call.rs index 115c86080..72e94f9e2 100644 --- a/codegen/src/visitor/call.rs +++ b/codegen/src/visitor/call.rs @@ -104,7 +104,6 @@ impl Function { .ok_or(Error::ImportedFuncNotFound(index))?; tracing::trace!("call imported function, index={index}, func={func:?}"); - match func { HostFunc::Evm(OpCode::LOG0) => self.log(0), HostFunc::Evm(OpCode::LOG1) => self.log(1), @@ -112,6 +111,7 @@ impl Function { HostFunc::Evm(OpCode::LOG3) => self.log(3), HostFunc::Evm(OpCode::LOG4) => self.log(4), HostFunc::Evm(op) => self.masm.emit_op(op), + HostFunc::Revert(count) => self.revert(count), HostFunc::NoOp | HostFunc::Label(_) => Ok(()), _ => { tracing::error!("unsupported host function {func:?}"); diff --git a/codegen/src/visitor/log.rs b/codegen/src/visitor/log.rs index d0268db00..801c95bd1 100644 --- a/codegen/src/visitor/log.rs +++ b/codegen/src/visitor/log.rs @@ -4,7 +4,13 @@ use crate::{masm::MemoryInfo, wasm::ToLSBytes, Error, Function, Result}; impl Function { /// Parse log data from the bytecode. - fn log_data(&mut self) -> Result<(i32, i32)> { + /// + /// WASM example: + /// ``` + /// i32.const 1048576 ;; offset + /// i32.const 4 ;; 4 bytes + /// ``` + fn data(&mut self) -> Result<(i32, i32)> { let buffer: Vec = self.masm.buffer().into(); // Pop offset and size from the bytecode. @@ -53,7 +59,7 @@ impl Function { pub fn log(&mut self, count: usize) -> Result<()> { let mut topics = Vec::>::default(); for topic in (1..=count).rev() { - let (offset, size) = self.log_data()?; + let (offset, size) = self.data()?; let size = size as usize; let data = self.env.data.load(offset, size)?; @@ -62,7 +68,7 @@ impl Function { } let name = { - let (offset, size) = self.log_data()?; + let (offset, size) = self.data()?; let size = size as usize; let data = self.env.data.load(offset, size)?; @@ -93,4 +99,35 @@ impl Function { Ok(()) } + + /// Revert with message. + pub fn revert(&mut self, count: usize) -> Result<()> { + let mut message = Vec::>::default(); + for slot in 0..count { + let (offset, size) = self.data()?; + let size = size as usize; + let data = self.env.data.load(offset, size)?; + + self.masm.push(&data)?; + if slot == 0 { + self.masm._push0()?; + } else { + self.masm.push(&slot.to_ls_bytes())?; + } + self.masm._mstore()?; + message.push(data); + } + + tracing::debug!( + "revert message: {}", + String::from_utf8_lossy(&message.into_iter().flatten().collect::>()) + ); + + self.masm.push(&(count * 32).to_ls_bytes())?; + self.masm._push0()?; + + // 3. run log for the data + self.masm._revert()?; + Ok(()) + } } diff --git a/codegen/src/wasm/host.rs b/codegen/src/wasm/host.rs index 22301c9c8..9e9ec5dab 100644 --- a/codegen/src/wasm/host.rs +++ b/codegen/src/wasm/host.rs @@ -1,6 +1,7 @@ //! Host functions use crate::{Error, Result}; +use anyhow::anyhow; use core::str::FromStr; use opcodes::{OpCode as _, ShangHai as OpCode}; @@ -17,6 +18,8 @@ pub enum HostFunc { EmitABI, /// check equal of two addresses AddressEq, + /// Revert messages with length of slots + Revert(usize), /// Compiler labels Label(CompilerLabel), } @@ -48,6 +51,11 @@ impl TryFrom<(&str, &str)> for HostFunc { ("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) } diff --git a/examples/revert.rs b/examples/revert.rs new file mode 100644 index 000000000..d0ae2d7cf --- /dev/null +++ b/examples/revert.rs @@ -0,0 +1,24 @@ +//! Example of revert +#![cfg_attr(target_arch = "wasm32", no_std)] +#![cfg_attr(target_arch = "wasm32", no_main)] + +extern crate zink; + +#[cfg(not(target_arch = "wasm32"))] +fn main() {} + +/// check if the passing address is owner +#[zink::external] +pub fn run_revert() { + zink::revert!("revert works") +} + +#[test] +fn test_revert() -> anyhow::Result<()> { + use zint::Contract; + let mut contract = Contract::search("revert")?.compile()?; + + let info = contract.execute(["revert()".as_bytes()])?; + assert_eq!(info.revert, Some("revert works".into())); + Ok(()) +} diff --git a/zink/codegen/src/lib.rs b/zink/codegen/src/lib.rs index b2edf79c1..9974113bf 100644 --- a/zink/codegen/src/lib.rs +++ b/zink/codegen/src/lib.rs @@ -1,13 +1,25 @@ //! Code generation library for the zink API #![allow(unused)] +extern crate proc_macro; + use proc_macro::TokenStream; -use syn::{parse_macro_input, Attribute, DeriveInput, ItemFn, ItemStruct}; +use syn::{parse_macro_input, Attribute, DeriveInput, ItemFn, ItemStruct, LitStr}; mod event; +mod revert; mod selector; mod storage; +/// Revert with the input message +/// +/// Only raw string is supported, formatter currently doesn't work. +#[proc_macro] +pub fn revert(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as LitStr); + revert::parse(input) +} + /// Event logging interface /// /// ```ignore diff --git a/zink/codegen/src/revert.rs b/zink/codegen/src/revert.rs new file mode 100644 index 000000000..e03076cdb --- /dev/null +++ b/zink/codegen/src/revert.rs @@ -0,0 +1,36 @@ +//! Revert macro + +use proc_macro::TokenStream; +use proc_macro2::{Literal, Span}; +use quote::{quote, ToTokens}; +use syn::{Ident, LitStr}; + +/// Revert with message +pub fn parse(input: LitStr) -> TokenStream { + let message = input.value(); + let len = message.len() as i32; + if len > 128 { + panic!("Only support revert message less than 128 bytes atm."); + } + + // TODO: handle the string correctly + let lit = Literal::string(&message.replace("\"", "")); + let rev = Ident::new( + &format!( + "revert{}", + match len { + len if len > 96 => 4, + len if len > 64 => 3, + len if len > 32 => 2, + len if len > 0 => 1, + _ => panic!("Only support revert message less than 128 bytes atm."), + }, + ), + Span::call_site(), + ); + + quote! { + unsafe { zink::ffi::asm::#rev(#lit) } + } + .into() +} diff --git a/zink/codegen/src/storage.rs b/zink/codegen/src/storage.rs index dad1b3c24..aa2723612 100644 --- a/zink/codegen/src/storage.rs +++ b/zink/codegen/src/storage.rs @@ -1,5 +1,3 @@ -extern crate proc_macro; - use heck::AsSnakeCase; use proc_macro::TokenStream; use proc_macro2::{Literal, Span, TokenTree}; diff --git a/zink/src/ffi/asm.rs b/zink/src/ffi/asm.rs index 77d88ec08..fc8930e84 100644 --- a/zink/src/ffi/asm.rs +++ b/zink/src/ffi/asm.rs @@ -32,6 +32,18 @@ extern "C" { /// Push address to stack pub fn push_address(address: Address); + /// Revert with message in 32 bytes + pub fn revert1(message: &'static str); + + /// Revert with message in 64 bytes + pub fn revert2(message: &'static str); + + /// Revert with message in 96 bytes + pub fn revert3(message: &'static str); + + /// Revert with message in 128 bytes + pub fn revert4(message: &'static str); + /// Load a 8-bit signed integer from the storage. pub fn sload_i8() -> i8; diff --git a/zink/src/lib.rs b/zink/src/lib.rs index 8fa280035..3aeee8205 100644 --- a/zink/src/lib.rs +++ b/zink/src/lib.rs @@ -13,7 +13,7 @@ pub mod storage; pub use self::{asm::Asm, event::Event}; pub use storage::{DoubleKeyMapping, Mapping, Storage}; -pub use zink_codegen::{external, storage, Event}; +pub use zink_codegen::{external, revert, storage, Event}; /// Generate a keccak hash of the input (sha3) #[cfg(not(target_family = "wasm"))] diff --git a/zint/src/evm.rs b/zint/src/evm.rs index 9d82c6f47..d0d034ef1 100644 --- a/zint/src/evm.rs +++ b/zint/src/evm.rs @@ -124,6 +124,8 @@ pub struct Info { pub logs: Vec, /// Transaction halt reason. pub halt: Option, + /// The revert message. + pub revert: Option, } impl TryFrom for Info { @@ -166,7 +168,14 @@ impl TryFrom for Info { ExecutionResult::Halt { reason, .. } => { info.halt = Some(reason); } - _ => unreachable!("This should never happen"), + ExecutionResult::Revert { gas_used, output } => { + info.gas = gas_used; + info.revert = Some( + String::from_utf8_lossy(&output) + .trim_start_matches("\0") + .to_string(), + ); + } } Ok(info)