diff --git a/Makefile b/Makefile index a6e3d63e9..7493eec92 100644 --- a/Makefile +++ b/Makefile @@ -105,10 +105,12 @@ stylus_test_fallible_wasm = $(call get_stylus_test_wasm,fallible) stylus_test_fallible_src = $(call get_stylus_test_rust,fallible) stylus_test_storage_wasm = $(call get_stylus_test_wasm,storage) stylus_test_storage_src = $(call get_stylus_test_rust,storage) +stylus_test_calls_wasm = $(call get_stylus_test_wasm,calls) +stylus_test_calls_src = $(call get_stylus_test_rust,calls) stylus_test_siphash_wasm = $(stylus_test_dir)/siphash/siphash.wasm stylus_test_siphash_src = $(call get_stylus_test_c,siphash) -stylus_test_wasms = $(stylus_test_keccak_wasm) $(stylus_test_keccak-100_wasm) $(stylus_test_fallible_wasm) $(stylus_test_storage_wasm) $(stylus_test_siphash_wasm) +stylus_test_wasms = $(stylus_test_keccak_wasm) $(stylus_test_keccak-100_wasm) $(stylus_test_fallible_wasm) $(stylus_test_storage_wasm) $(stylus_test_siphash_wasm) $(stylus_test_calls_wasm) stylus_benchmarks = $(wildcard $(stylus_dir)/*.toml $(stylus_dir)/src/*.rs) $(stylus_test_wasms) stylus_files = $(wildcard $(stylus_dir)/*.toml $(stylus_dir)/src/*.rs) $(rust_prover_files) @@ -342,6 +344,10 @@ $(stylus_test_storage_wasm): $(stylus_test_storage_src) cargo build --manifest-path $< --release --target wasm32-unknown-unknown @touch -c $@ # cargo might decide to not rebuild the binary +$(stylus_test_calls_wasm): $(stylus_test_calls_src) + cargo build --manifest-path $< --release --target wasm32-unknown-unknown + @touch -c $@ # cargo might decide to not rebuild the binary + $(stylus_test_siphash_wasm): $(stylus_test_siphash_src) clang $(filter %.c, $^) -o $@ --target=wasm32 --no-standard-libraries -Wl,--no-entry -Oz diff --git a/arbitrator/Cargo.lock b/arbitrator/Cargo.lock index e8e6d890c..25e753ab6 100644 --- a/arbitrator/Cargo.lock +++ b/arbitrator/Cargo.lock @@ -1163,6 +1163,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1294,6 +1300,18 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", "rand_core", ] @@ -1302,6 +1320,9 @@ name = "rand_core" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] [[package]] name = "rand_pcg" @@ -1700,6 +1721,7 @@ dependencies = [ "ouroboros", "parking_lot 0.12.1", "prover", + "rand", "sha3 0.10.6", "thiserror", "wasmer", diff --git a/arbitrator/jit/src/user.rs b/arbitrator/jit/src/user.rs index 91de6d69c..e6e2f248c 100644 --- a/arbitrator/jit/src/user.rs +++ b/arbitrator/jit/src/user.rs @@ -50,31 +50,28 @@ pub fn call_user_wasm(env: WasmEnvMut, sp: u32) { // skip the root since we don't use these sp.skip_u64(); - macro_rules! error { - ($msg:expr, $report:expr) => {{ - let outs = format!("{:?}", $report.wrap_err(eyre!($msg))).into_bytes(); - sp.write_u8(UserOutcomeKind::Failure as u8).skip_space(); - sp.write_ptr(heapify(outs)); - if pricing.wasm_gas_price != 0 { - sp.write_u64_raw(evm_gas, pricing.wasm_to_evm(wasm_gas)); - } - return; - }}; - } - // Safety: module came from compile_user_wasm let instance = unsafe { NativeInstance::deserialize(&module, config.clone()) }; - let mut instance = match instance { Ok(instance) => instance, - Err(error) => error!("failed to instantiate program", error), + Err(error) => panic!("failed to instantiate program {error:?}"), }; instance.set_gas(wasm_gas); instance.set_stack(config.depth.max_depth); - let (status, outs) = match instance.run_main(&calldata, &config) { - Err(err) | Ok(UserOutcome::Failure(err)) => error!("failed to execute program", err), - Ok(outcome) => outcome.into_data(), + let status = match instance.run_main(&calldata, &config) { + Err(err) | Ok(UserOutcome::Failure(err)) => { + let outs = format!("{:?}", err.wrap_err(eyre!("failed to execute program"))); + sp.write_u8(UserOutcomeKind::Failure as u8).skip_space(); + sp.write_ptr(heapify(outs.into_bytes())); + UserOutcomeKind::Failure + } + Ok(outcome) => { + let (status, outs) = outcome.into_data(); + sp.write_u8(status as u8).skip_space(); + sp.write_ptr(heapify(outs)); + status + } }; if pricing.wasm_gas_price != 0 { let wasm_gas = match status { @@ -83,8 +80,6 @@ pub fn call_user_wasm(env: WasmEnvMut, sp: u32) { }; sp.write_u64_raw(evm_gas, pricing.wasm_to_evm(wasm_gas)); } - sp.write_u8(status as u8).skip_space(); - sp.write_ptr(heapify(outs)); } /// Reads the length of a rust `Vec` diff --git a/arbitrator/langs/rust/src/contract.rs b/arbitrator/langs/rust/src/contract.rs new file mode 100644 index 000000000..48123db2c --- /dev/null +++ b/arbitrator/langs/rust/src/contract.rs @@ -0,0 +1,65 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +use super::util::{Bytes20, Bytes32}; + +#[derive(Copy, Clone)] +#[repr(C)] +struct RustVec { + ptr: *mut u8, + len: usize, + cap: usize, +} + +impl Default for RustVec { + fn default() -> Self { + Self { + ptr: std::ptr::null_mut(), + len: 0, + cap: 0, + } + } +} + +#[link(wasm_import_module = "forward")] +extern "C" { + fn call_contract( + contract: *const u8, + calldata: *const u8, + calldata_len: usize, + value: *const u8, + gas: u64, + return_data_len: *mut usize, + ) -> u8; + + /// A noop when there's never been a call + fn read_return_data(dest: *mut u8); +} + +/// Calls the contract at the given address, with options for passing value or limiting the amount of gas provided. +/// On failure, the output consists of the call's revert data. +pub fn call(contract: Bytes20, calldata: &[u8], value: Option, gas: Option) -> Result, Vec> { + let mut outs_len = 0; + let value = value.unwrap_or_default(); + let gas = gas.unwrap_or(u64::MAX); // will be clamped by 63/64 rule + let status = unsafe { + call_contract( + contract.ptr(), + calldata.as_ptr(), + calldata.len(), + value.ptr(), + gas, + &mut outs_len as *mut _, + ) + }; + let outs = unsafe { + let mut outs = Vec::with_capacity(outs_len); + read_return_data(outs.as_mut_ptr()); + outs.set_len(outs_len); + outs + }; + match status { + 0 => Ok(outs), + _ => Err(outs), + } +} diff --git a/arbitrator/langs/rust/src/lib.rs b/arbitrator/langs/rust/src/lib.rs index afdfba369..90d6107cf 100644 --- a/arbitrator/langs/rust/src/lib.rs +++ b/arbitrator/langs/rust/src/lib.rs @@ -3,6 +3,7 @@ pub use util::{Bytes20, Bytes32}; +pub mod contract; pub mod debug; mod util; diff --git a/arbitrator/langs/rust/src/util.rs b/arbitrator/langs/rust/src/util.rs index f972a8e46..131313ed2 100644 --- a/arbitrator/langs/rust/src/util.rs +++ b/arbitrator/langs/rust/src/util.rs @@ -1,6 +1,14 @@ -use std::{array::TryFromSliceError, borrow::Borrow, fmt::{self, Debug, Display, Formatter}, ops::{Deref, DerefMut}}; +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE -#[derive(Default)] +use std::{ + array::TryFromSliceError, + borrow::Borrow, + fmt::{self, Debug, Display, Formatter}, + ops::{Deref, DerefMut}, +}; + +#[derive(Copy, Clone, Default)] #[repr(C)] pub struct Bytes20(pub [u8; 20]); @@ -82,7 +90,7 @@ impl Debug for Bytes20 { } } -#[derive(Default)] +#[derive(Copy, Clone, Default)] #[repr(C)] pub struct Bytes32(pub [u8; 32]); @@ -163,4 +171,3 @@ impl Debug for Bytes32 { write!(f, "{}", hex::encode(self)) } } - diff --git a/arbitrator/prover/src/binary.rs b/arbitrator/prover/src/binary.rs index fd38114e3..7178e79c6 100644 --- a/arbitrator/prover/src/binary.rs +++ b/arbitrator/prover/src/binary.rs @@ -525,7 +525,7 @@ impl<'a> WasmBinary<'a> { bound.update_module(self)?; start.update_module(self)?; - let count = config.debug.count_ops.then(|| Counter::new()); + let count = config.debug.count_ops.then(Counter::new); if let Some(count) = &count { count.update_module(self)?; } diff --git a/arbitrator/prover/src/host.rs b/arbitrator/prover/src/host.rs index f504744c9..d796c9eca 100644 --- a/arbitrator/prover/src/host.rs +++ b/arbitrator/prover/src/host.rs @@ -3,7 +3,7 @@ use crate::{ machine::{Function, InboxIdentifier}, - programs::StylusGlobals, + programs::{run::UserOutcomeKind, StylusGlobals}, value::{ArbValueType, FunctionType, IntegerValType}, wavm::{IBinOpType, Instruction, Opcode}, }; @@ -202,7 +202,7 @@ pub fn get_host_impl(module: &str, name: &str) -> eyre::Result { // λ(module, main, args_len) -> status opcode!(PushErrorGuard); opcode!(ArbitraryJumpIf, code.len() + 3); - opcode!(I32Const, 1); + opcode!(I32Const, UserOutcomeKind::Failure as u32); opcode!(Return); // jumps here in the happy case diff --git a/arbitrator/prover/src/programs/config.rs b/arbitrator/prover/src/programs/config.rs index 27bfe4011..57df4c92c 100644 --- a/arbitrator/prover/src/programs/config.rs +++ b/arbitrator/prover/src/programs/config.rs @@ -1,6 +1,8 @@ // Copyright 2022-2023, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE +#![allow(clippy::field_reassign_with_default)] + use eyre::{bail, Result}; use std::fmt::Debug; use wasmer_types::Bytes; diff --git a/arbitrator/prover/src/programs/counter.rs b/arbitrator/prover/src/programs/counter.rs index 8b20c5495..95b4803fe 100644 --- a/arbitrator/prover/src/programs/counter.rs +++ b/arbitrator/prover/src/programs/counter.rs @@ -36,6 +36,12 @@ impl Counter { } } +impl Default for Counter { + fn default() -> Self { + Self::new() + } +} + impl Middleware for Counter where M: ModuleMod, @@ -115,7 +121,7 @@ impl<'a> FuncMiddleware<'a> for FuncCounter<'a> { for (op, count) in increments { let opslen = operators.len(); let offset = *operators.entry(op).or_insert(opslen); - let global = *counters.get(offset).ok_or(eyre!("no global"))?; + let global = *counters.get(offset).ok_or_else(|| eyre!("no global"))?; out.extend(update(global.as_u32(), count)); } diff --git a/arbitrator/prover/src/programs/meter.rs b/arbitrator/prover/src/programs/meter.rs index e45cc7624..7384cea2f 100644 --- a/arbitrator/prover/src/programs/meter.rs +++ b/arbitrator/prover/src/programs/meter.rs @@ -6,7 +6,7 @@ use crate::Machine; use arbutil::operator::OperatorInfo; use eyre::Result; use parking_lot::Mutex; -use std::fmt::Debug; +use std::fmt::{Debug, Display}; use wasmer_types::{GlobalIndex, GlobalInit, LocalFunctionIndex, Type}; use wasmparser::{Operator, Type as WpType, TypeOrFuncType}; @@ -191,6 +191,15 @@ impl Into for MachineMeter { } } +impl Display for MachineMeter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Ready(gas) => write!(f, "{gas} gas"), + Self::Exhausted => write!(f, "exhausted"), + } + } +} + /// Note: implementers may panic if uninstrumented pub trait MeteredMachine { fn gas_left(&mut self) -> MachineMeter; diff --git a/arbitrator/prover/src/programs/run.rs b/arbitrator/prover/src/programs/run.rs index 439d8e98e..610c1c151 100644 --- a/arbitrator/prover/src/programs/run.rs +++ b/arbitrator/prover/src/programs/run.rs @@ -68,9 +68,23 @@ impl Display for UserOutcome { OutOfGas => write!(f, "out of gas"), OutOfStack => write!(f, "out of stack"), Revert(data) => { - let text = String::from_utf8(data.clone()).unwrap_or(hex::encode(data)); + let text = String::from_utf8(data.clone()).unwrap_or_else(|_| hex::encode(data)); write!(f, "revert {text}") } } } } + +impl Display for UserOutcomeKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let as_u8 = *self as u8; + use UserOutcomeKind::*; + match self { + Success => write!(f, "success ({as_u8})"), + Revert => write!(f, "revert ({as_u8})"), + Failure => write!(f, "failure ({as_u8})"), + OutOfGas => write!(f, "out of gas ({as_u8})"), + OutOfStack => write!(f, "out of stack ({as_u8})"), + } + } +} diff --git a/arbitrator/stylus/Cargo.toml b/arbitrator/stylus/Cargo.toml index 4b8765fe0..8887f7da8 100644 --- a/arbitrator/stylus/Cargo.toml +++ b/arbitrator/stylus/Cargo.toml @@ -15,6 +15,7 @@ parking_lot = "0.12.1" thiserror = "1.0.33" libc = "0.2.108" eyre = "0.6.5" +rand = "0.8.5" fnv = "1.0.7" sha3 = "0.10.5" hex = "0.4.3" diff --git a/arbitrator/stylus/cbindgen.toml b/arbitrator/stylus/cbindgen.toml index 54777b9e1..597567186 100644 --- a/arbitrator/stylus/cbindgen.toml +++ b/arbitrator/stylus/cbindgen.toml @@ -5,3 +5,6 @@ include_guard = "arbitrator_bindings" parse_deps = true include = ["prover"] extra_bindings = ["prover"] + +[enum] +prefix_with_name = true diff --git a/arbitrator/stylus/src/env.rs b/arbitrator/stylus/src/env.rs index d785feea2..f1c252656 100644 --- a/arbitrator/stylus/src/env.rs +++ b/arbitrator/stylus/src/env.rs @@ -11,11 +11,14 @@ use prover::{ }, utils::{Bytes20, Bytes32}, }; -use std::ops::{Deref, DerefMut}; +use std::{ + io, + ops::{Deref, DerefMut}, +}; use thiserror::Error; use wasmer::{ AsStoreMut, AsStoreRef, FunctionEnvMut, Global, Memory, MemoryAccessError, MemoryView, - StoreMut, StoreRef, + StoreMut, StoreRef, WasmPtr, }; #[self_referencing] @@ -99,19 +102,24 @@ pub struct MeterData { } /// State load: key → (value, cost) -pub type LoadBytes32 = Box (Bytes32, u64) + Send>; +pub type GetBytes32 = Box (Bytes32, u64) + Send>; /// State store: (key, value) → (cost, error) -pub type StoreBytes32 = Box eyre::Result + Send>; +pub type SetBytes32 = Box eyre::Result + Send>; -/// Contract call: (contract, calldata, gas, value) → (return_data, gas, status) +/// Contract call: (contract, calldata, evm_gas, value) → (return_data_len, evm_cost, status) pub type CallContract = - Box, u64, Bytes32) -> (Vec, u64, UserOutcomeKind) + Send>; + Box, u64, Bytes32) -> (u32, u64, UserOutcomeKind) + Send>; + +/// Last call's return data: () → (return_data) +pub type GetReturnData = Box Vec + Send>; pub struct EvmAPI { - load_bytes32: LoadBytes32, - store_bytes32: StoreBytes32, + get_bytes32: GetBytes32, + set_bytes32: SetBytes32, call_contract: CallContract, + get_return_data: GetReturnData, + return_data_len: u32, } impl WasmEnv { @@ -124,25 +132,40 @@ impl WasmEnv { pub fn set_evm_api( &mut self, - load_bytes32: LoadBytes32, - store_bytes32: StoreBytes32, + get_bytes32: GetBytes32, + set_bytes32: SetBytes32, call_contract: CallContract, + get_return_data: GetReturnData, ) { self.evm = Some(EvmAPI { - load_bytes32, - store_bytes32, + get_bytes32, + set_bytes32, call_contract, + get_return_data, + return_data_len: 0, }) } - pub fn evm(&mut self) -> eyre::Result<&mut EvmAPI> { - self.evm.as_mut().ok_or_else(|| eyre!("no evm api")) + pub fn evm(&mut self) -> &mut EvmAPI { + self.evm.as_mut().expect("no evm api") + } + + pub fn evm_ref(&self) -> &EvmAPI { + self.evm.as_ref().expect("no evm api") } pub fn memory(env: &mut WasmEnvMut<'_>) -> MemoryViewContainer { MemoryViewContainer::create(env) } + pub fn return_data_len(&self) -> u32 { + self.evm_ref().return_data_len + } + + pub fn set_return_data_len(&mut self, len: u32) { + self.evm().return_data_len = len; + } + pub fn data<'a, 'b: 'a>(env: &'a mut WasmEnvMut<'b>) -> (&'a mut Self, MemoryViewContainer) { let memory = MemoryViewContainer::create(env); (env.data_mut(), memory) @@ -159,6 +182,144 @@ impl WasmEnv { state.buy_gas(state.pricing.hostio_cost)?; Ok(state) } + + pub fn start<'a, 'b>(env: &'a mut WasmEnvMut<'b>) -> Result, Escape> { + let (env, store) = env.data_and_store_mut(); + let memory = env.memory.clone().unwrap(); + let mut info = HostioInfo { env, memory, store }; + let cost = info.meter().pricing.hostio_cost; + info.buy_gas(cost)?; + Ok(info) + } +} + +pub struct HostioInfo<'a> { + pub env: &'a mut WasmEnv, + pub memory: Memory, + pub store: StoreMut<'a>, +} + +impl<'a> HostioInfo<'a> { + pub fn meter(&mut self) -> &mut MeterData { + self.meter.as_mut().unwrap() + } + + pub fn buy_gas(&mut self, gas: u64) -> MaybeEscape { + let MachineMeter::Ready(gas_left) = self.gas_left() else { + return Escape::out_of_gas(); + }; + if gas_left < gas { + return Escape::out_of_gas(); + } + self.set_gas(gas_left - gas); + Ok(()) + } + + pub fn buy_evm_gas(&mut self, evm: u64) -> MaybeEscape { + if let Ok(wasm_gas) = self.meter().pricing.evm_to_wasm(evm) { + self.buy_gas(wasm_gas)?; + } + Ok(()) + } + + /// Checks if the user has enough evm gas, but doesn't burn any + pub fn require_evm_gas(&mut self, evm: u64) -> MaybeEscape { + let Ok(wasm_gas) = self.meter().pricing.evm_to_wasm(evm) else { + return Ok(()) + }; + let MachineMeter::Ready(gas_left) = self.gas_left() else { + return Escape::out_of_gas(); + }; + match gas_left < wasm_gas { + true => Escape::out_of_gas(), + false => Ok(()), + } + } + + pub fn pay_for_evm_copy(&mut self, bytes: usize) -> MaybeEscape { + let evm_words = |count: u64| count.saturating_mul(31) / 32; + let evm_gas = evm_words(bytes as u64).saturating_mul(3); // 3 evm gas per word + self.buy_evm_gas(evm_gas) + } + + pub fn view(&self) -> MemoryView { + self.memory.view(&self.store.as_store_ref()) + } + + pub fn write_u8(&mut self, ptr: u32, x: u8) -> &mut Self { + let ptr: WasmPtr = WasmPtr::new(ptr); + ptr.deref(&self.view()).write(x).unwrap(); + self + } + + pub fn write_u32(&mut self, ptr: u32, x: u32) -> &mut Self { + let ptr: WasmPtr = WasmPtr::new(ptr); + ptr.deref(&self.view()).write(x).unwrap(); + self + } + + pub fn write_u64(&mut self, ptr: u32, x: u64) -> &mut Self { + let ptr: WasmPtr = WasmPtr::new(ptr); + ptr.deref(&self.view()).write(x).unwrap(); + self + } + + pub fn read_slice(&self, ptr: u32, len: u32) -> Result, MemoryAccessError> { + let mut data = vec![0; len as usize]; + self.view().read(ptr.into(), &mut data)?; + Ok(data) + } + + pub fn read_bytes20(&self, ptr: u32) -> eyre::Result { + let data = self.read_slice(ptr, 20)?; + Ok(data.try_into()?) + } + + pub fn read_bytes32(&self, ptr: u32) -> eyre::Result { + let data = self.read_slice(ptr, 32)?; + Ok(data.try_into()?) + } + + pub fn write_slice(&self, ptr: u32, src: &[u8]) -> Result<(), MemoryAccessError> { + self.view().write(ptr.into(), src) + } +} + +impl<'a> MeteredMachine for HostioInfo<'a> { + fn gas_left(&mut self) -> MachineMeter { + let store = &mut self.store; + let meter = self.env.meter.as_ref().unwrap(); + let status = meter.gas_status.get(store); + let status = status.try_into().expect("type mismatch"); + let gas = meter.gas_left.get(store); + let gas = gas.try_into().expect("type mismatch"); + + match status { + 0_u32 => MachineMeter::Ready(gas), + _ => MachineMeter::Exhausted, + } + } + + fn set_gas(&mut self, gas: u64) { + let store = &mut self.store; + let meter = self.env.meter.as_ref().unwrap(); + meter.gas_left.set(store, gas.into()).unwrap(); + meter.gas_status.set(store, 0.into()).unwrap(); + } +} + +impl<'a> Deref for HostioInfo<'a> { + type Target = WasmEnv; + + fn deref(&self) -> &Self::Target { + self.env + } +} + +impl<'a> DerefMut for HostioInfo<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.env + } } pub struct MeterState<'a> { @@ -181,7 +342,7 @@ impl<'a> DerefMut for MeterState<'a> { } impl<'a> MeterState<'a> { - fn new(state: MeterData, store: StoreMut<'a>) -> Self { + pub fn new(state: MeterData, store: StoreMut<'a>) -> Self { Self { state, store } } @@ -216,6 +377,12 @@ impl<'a> MeterState<'a> { false => Ok(()), } } + + pub fn pay_for_evm_copy(&mut self, bytes: usize) -> MaybeEscape { + let evm_words = |count: u64| count.saturating_mul(31) / 32; + let evm_gas = evm_words(bytes as u64).saturating_mul(3); // 3 evm gas per word + self.buy_evm_gas(evm_gas) + } } impl<'a> MeteredMachine for MeterState<'a> { @@ -244,21 +411,25 @@ impl<'a> MeteredMachine for MeterState<'a> { impl EvmAPI { pub fn load_bytes32(&mut self, key: Bytes32) -> (Bytes32, u64) { - (self.load_bytes32)(key) + (self.get_bytes32)(key) } pub fn store_bytes32(&mut self, key: Bytes32, value: Bytes32) -> eyre::Result { - (self.store_bytes32)(key, value) + (self.set_bytes32)(key, value) } pub fn call_contract( &mut self, contract: Bytes20, input: Vec, - gas: u64, + evm_gas: u64, value: Bytes32, - ) -> (Vec, u64, UserOutcomeKind) { - (self.call_contract)(contract, input, gas, value) + ) -> (u32, u64, UserOutcomeKind) { + (self.call_contract)(contract, input, evm_gas, value) + } + + pub fn load_return_data(&mut self) -> Vec { + (self.get_return_data)() } } @@ -290,6 +461,12 @@ impl From for Escape { } } +impl From for Escape { + fn from(err: io::Error) -> Self { + Self::Internal(eyre!(err)) + } +} + impl From for Escape { fn from(err: ErrReport) -> Self { Self::Internal(err) diff --git a/arbitrator/stylus/src/host.rs b/arbitrator/stylus/src/host.rs index d2b7f9505..924d89faa 100644 --- a/arbitrator/stylus/src/host.rs +++ b/arbitrator/stylus/src/host.rs @@ -1,8 +1,9 @@ // Copyright 2022-2023, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -use crate::env::{MaybeEscape, WasmEnv, WasmEnvMut}; +use crate::env::{Escape, MaybeEscape, WasmEnv, WasmEnvMut}; use arbutil::Color; +use prover::programs::prelude::*; // params.SstoreSentryGasEIP2200 (see operations_acl_arbitrum.go) const SSTORE_SENTRY_EVM_GAS: u64 = 2300; @@ -17,10 +18,7 @@ pub(crate) fn read_args(mut env: WasmEnvMut, ptr: u32) -> MaybeEscape { pub(crate) fn return_data(mut env: WasmEnvMut, ptr: u32, len: u32) -> MaybeEscape { let mut meter = WasmEnv::begin(&mut env)?; - - let evm_words = |count: u64| count.saturating_mul(31) / 32; - let evm_gas = evm_words(len.into()).saturating_mul(3); // 3 evm gas per word - meter.buy_evm_gas(evm_gas)?; + meter.pay_for_evm_copy(len as usize)?; let (env, memory) = WasmEnv::data(&mut env); env.outs = memory.read_slice(ptr, len)?; @@ -32,7 +30,7 @@ pub(crate) fn account_load_bytes32(mut env: WasmEnvMut, key: u32, dest: u32) -> let (data, memory) = WasmEnv::data(&mut env); let key = memory.read_bytes32(key)?; - let (value, cost) = data.evm()?.load_bytes32(key); + let (value, cost) = data.evm().load_bytes32(key); memory.write_slice(dest, &value.0)?; let mut meter = WasmEnv::meter(&mut env); @@ -46,12 +44,55 @@ pub(crate) fn account_store_bytes32(mut env: WasmEnvMut, key: u32, value: u32) - let (data, memory) = WasmEnv::data(&mut env); let key = memory.read_bytes32(key)?; let value = memory.read_bytes32(value)?; - let cost = data.evm()?.store_bytes32(key, value)?; + let cost = data.evm().store_bytes32(key, value)?; let mut meter = WasmEnv::meter(&mut env); meter.buy_evm_gas(cost) } +pub(crate) fn call_contract( + mut env: WasmEnvMut, + contract: u32, + calldata: u32, + calldata_len: u32, + value: u32, + mut wasm_gas: u64, + return_data_len: u32, +) -> Result { + let mut env = WasmEnv::start(&mut env)?; + env.pay_for_evm_copy(calldata_len as usize)?; + wasm_gas = wasm_gas.min(env.gas_left().into()); // provide no more than what the user has + + let pricing = env.meter().pricing; + let evm_gas = match pricing.wasm_gas_price { + 0 => u64::MAX, + _ => pricing.wasm_to_evm(wasm_gas), + }; + + let contract = env.read_bytes20(contract)?; + let input = env.read_slice(calldata, calldata_len)?; + let value = env.read_bytes32(value)?; + + let (outs_len, evm_cost, status) = env.evm().call_contract(contract, input, evm_gas, value); + env.set_return_data_len(outs_len); + env.write_u32(return_data_len, outs_len); + + let wasm_cost = pricing.evm_to_wasm(evm_cost).unwrap_or_default(); + env.buy_gas(wasm_cost)?; + Ok(status as u8) +} + +pub(crate) fn read_return_data(mut env: WasmEnvMut, dest: u32) -> MaybeEscape { + let mut env = WasmEnv::start(&mut env)?; + let len = env.return_data_len(); + env.pay_for_evm_copy(len as usize)?; + + let data = env.evm().load_return_data(); + env.write_slice(dest, &data)?; + assert_eq!(data.len(), len as usize); + Ok(()) +} + pub(crate) fn debug_println(mut env: WasmEnvMut, ptr: u32, len: u32) -> MaybeEscape { let memory = WasmEnv::memory(&mut env); let text = memory.read_slice(ptr, len)?; diff --git a/arbitrator/stylus/src/lib.rs b/arbitrator/stylus/src/lib.rs index 5a2fd88c1..6f805dd61 100644 --- a/arbitrator/stylus/src/lib.rs +++ b/arbitrator/stylus/src/lib.rs @@ -3,7 +3,10 @@ use eyre::{eyre, ErrReport}; use native::NativeInstance; -use prover::{programs::prelude::*, utils::Bytes32}; +use prover::{ + programs::prelude::*, + utils::{Bytes20, Bytes32}, +}; use run::RunProgram; use std::mem; @@ -113,10 +116,41 @@ pub unsafe extern "C" fn stylus_compile( } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum GoApiStatus { + Success, + Failure, +} + +impl From for UserOutcomeKind { + fn from(value: GoApiStatus) -> Self { + match value { + GoApiStatus::Success => UserOutcomeKind::Success, + GoApiStatus::Failure => UserOutcomeKind::Revert, + } + } +} + #[repr(C)] -pub struct GoAPI { - pub get_bytes32: unsafe extern "C" fn(usize, Bytes32, *mut u64) -> Bytes32, - pub set_bytes32: unsafe extern "C" fn(usize, Bytes32, Bytes32, *mut u64, *mut RustVec) -> u8, +pub struct GoApi { + pub get_bytes32: unsafe extern "C" fn(id: usize, key: Bytes32, evm_cost: *mut u64) -> Bytes32, // value + pub set_bytes32: unsafe extern "C" fn( + id: usize, + key: Bytes32, + value: Bytes32, + evm_cost: *mut u64, + error: *mut RustVec, + ) -> GoApiStatus, + pub call_contract: unsafe extern "C" fn( + id: usize, + contract: Bytes20, + calldata: *mut RustVec, + gas: *mut u64, + value: Bytes32, + return_data_len: *mut u32, + ) -> GoApiStatus, + pub get_return_data: unsafe extern "C" fn(id: usize, output: *mut RustVec), pub id: usize, } @@ -125,7 +159,7 @@ pub unsafe extern "C" fn stylus_call( module: GoSliceData, calldata: GoSliceData, params: GoParams, - go_api: GoAPI, + go_api: GoApi, output: *mut RustVec, evm_gas: *mut u64, ) -> UserOutcomeKind { @@ -136,29 +170,25 @@ pub unsafe extern "C" fn stylus_call( let wasm_gas = pricing.evm_to_wasm(*evm_gas).unwrap_or(u64::MAX); let output = &mut *output; - macro_rules! error { - ($msg:expr, $report:expr) => {{ - let report: ErrReport = $report.into(); - let report = report.wrap_err(eyre!($msg)); - output.write_err(report); - *evm_gas = 0; // burn all gas - return UserOutcomeKind::Failure; - }}; - } - // Safety: module came from compile_user_wasm let instance = unsafe { NativeInstance::deserialize(module, config.clone()) }; - let mut instance = match instance { Ok(instance) => instance, - Err(error) => error!("failed to instantiate program", error), + Err(error) => panic!("failed to instantiate program: {error:?}"), }; instance.set_go_api(go_api); instance.set_gas(wasm_gas); - let (status, outs) = match instance.run_main(&calldata, &config) { - Err(err) | Ok(UserOutcome::Failure(err)) => error!("failed to execute program", err), - Ok(outcome) => outcome.into_data(), + let status = match instance.run_main(&calldata, &config) { + Err(err) | Ok(UserOutcome::Failure(err)) => { + output.write_err(err.wrap_err(eyre!("failed to execute program"))); + UserOutcomeKind::Failure + } + Ok(outcome) => { + let (status, outs) = outcome.into_data(); + output.write(outs); + status + } }; if pricing.wasm_gas_price != 0 { let wasm_gas = match status { @@ -167,7 +197,6 @@ pub unsafe extern "C" fn stylus_call( }; *evm_gas = pricing.wasm_to_evm(wasm_gas); } - output.write(outs); status } diff --git a/arbitrator/stylus/src/native.rs b/arbitrator/stylus/src/native.rs index 8c35bf725..b0e628b18 100644 --- a/arbitrator/stylus/src/native.rs +++ b/arbitrator/stylus/src/native.rs @@ -3,20 +3,24 @@ use crate::{ env::{MeterData, WasmEnv}, - host, GoAPI, RustVec, + host, GoApi, GoApiStatus, RustVec, }; use arbutil::{operator::OperatorCode, Color}; use eyre::{bail, eyre, ErrReport, Result}; -use prover::programs::{ - counter::{Counter, CountingMachine, OP_OFFSETS}, - depth::STYLUS_STACK_LEFT, - meter::{STYLUS_GAS_LEFT, STYLUS_GAS_STATUS}, - prelude::*, - start::STYLUS_START, +use prover::{ + programs::{ + counter::{Counter, CountingMachine, OP_OFFSETS}, + depth::STYLUS_STACK_LEFT, + meter::{STYLUS_GAS_LEFT, STYLUS_GAS_STATUS}, + prelude::*, + start::STYLUS_START, + }, + utils::Bytes20, }; use std::{ collections::BTreeMap, fmt::Debug, + mem, ops::{Deref, DerefMut}, }; use wasmer::{ @@ -82,6 +86,8 @@ impl NativeInstance { "return_data" => Function::new_typed_with_env(&mut store, &func_env, host::return_data), "account_load_bytes32" => Function::new_typed_with_env(&mut store, &func_env, host::account_load_bytes32), "account_store_bytes32" => Function::new_typed_with_env(&mut store, &func_env, host::account_store_bytes32), + "call_contract" => Function::new_typed_with_env(&mut store, &func_env, host::call_contract), + "read_return_data" => Function::new_typed_with_env(&mut store, &func_env, host::read_return_data), }, }; if debug_funcs { @@ -135,32 +141,60 @@ impl NativeInstance { global.set(store, value.into()).map_err(ErrReport::msg) } - pub fn set_go_api(&mut self, api: GoAPI) { + pub fn set_go_api(&mut self, api: GoApi) { let env = self.env.as_mut(&mut self.store); + use GoApiStatus::*; + + macro_rules! ptr { + ($expr:expr) => { + &mut $expr as *mut _ + }; + } let get = api.get_bytes32; let set = api.set_bytes32; + let call = api.call_contract; + let get_return_data = api.get_return_data; let id = api.id; let get_bytes32 = Box::new(move |key| unsafe { let mut cost = 0; - let value = get(id, key, &mut cost as *mut _); + let value = get(id, key, ptr!(cost)); (value, cost) }); let set_bytes32 = Box::new(move |key, value| unsafe { let mut error = RustVec::new(vec![]); let mut cost = 0; - let status = set(id, key, value, &mut cost as *mut _, &mut error as *mut _); + let api_status = set(id, key, value, ptr!(cost), ptr!(error)); let error = error.into_vec(); // done here to always drop - match status { - 0 => Ok(cost), - _ => Err(ErrReport::msg(String::from_utf8_lossy(&error).to_string())), + match api_status { + Success => Ok(cost), + Failure => Err(ErrReport::msg(String::from_utf8_lossy(&error).to_string())), } }); - let call_contract = - Box::new(move |_contract, _input, _gas, _value| unimplemented!("contract call")); + let call_contract = Box::new(move |contract: Bytes20, input, evm_gas, value| unsafe { + let mut calldata = RustVec::new(input); + let mut call_gas = evm_gas; // becomes the call's cost + let mut return_data_len: u32 = 0; + + let api_status = call( + id, + contract, + ptr!(calldata), + ptr!(call_gas), + value, + ptr!(return_data_len), + ); + mem::drop(calldata.into_vec()); // only used for input + (return_data_len, call_gas, api_status.into()) + }); + let get_return_data = Box::new(move || unsafe { + let mut data = RustVec::new(vec![]); + get_return_data(id, ptr!(data)); + data.into_vec() + }); - env.set_evm_api(get_bytes32, set_bytes32, call_contract) + env.set_evm_api(get_bytes32, set_bytes32, call_contract, get_return_data) } } @@ -233,6 +267,15 @@ pub fn module(wasm: &[u8], config: StylusConfig) -> Result> { let mut store = config.store(); let module = Module::new(&store, wasm)?; macro_rules! stub { + (u8 <- $($types:tt)+) => { + Function::new_typed(&mut store, $($types)+ -> u8 { panic!("incomplete import") }) + }; + (u32 <- $($types:tt)+) => { + Function::new_typed(&mut store, $($types)+ -> u32 { panic!("incomplete import") }) + }; + (u64 <- $($types:tt)+) => { + Function::new_typed(&mut store, $($types)+ -> u64 { panic!("incomplete import") }) + }; ($($types:tt)+) => { Function::new_typed(&mut store, $($types)+ panic!("incomplete import")) }; @@ -243,6 +286,8 @@ pub fn module(wasm: &[u8], config: StylusConfig) -> Result> { "return_data" => stub!(|_: u32, _: u32|), "account_load_bytes32" => stub!(|_: u32, _: u32|), "account_store_bytes32" => stub!(|_: u32, _: u32|), + "call_contract" => stub!(u8 <- |_: u32, _: u32, _: u32, _: u32, _: u64, _: u32|), + "read_return_data" => stub!(|_: u32|), }, }; if config.debug.debug_funcs { diff --git a/arbitrator/stylus/src/run.rs b/arbitrator/stylus/src/run.rs index 7ac54dce0..c6ace55e7 100644 --- a/arbitrator/stylus/src/run.rs +++ b/arbitrator/stylus/src/run.rs @@ -75,11 +75,6 @@ impl RunProgram for NativeInstance { let status = match main.call(store, args.len() as u32) { Ok(status) => status, Err(outcome) => { - let escape = match outcome.downcast() { - Ok(escape) => escape, - Err(error) => return Ok(Failure(eyre!(error).wrap_err("hard user error"))), - }; - if self.stack_left() == 0 { return Ok(OutOfStack); } @@ -87,6 +82,10 @@ impl RunProgram for NativeInstance { return Ok(OutOfGas); } + let escape: Escape = match outcome.downcast() { + Ok(escape) => escape, + Err(error) => return Ok(Failure(eyre!(error).wrap_err("hard user error"))), + }; return Ok(match escape { Escape::OutOfGas => OutOfGas, Escape::Memory(error) => UserOutcome::revert(error.into()), diff --git a/arbitrator/stylus/src/test/api.rs b/arbitrator/stylus/src/test/api.rs index 410d77dda..e9e734bca 100644 --- a/arbitrator/stylus/src/test/api.rs +++ b/arbitrator/stylus/src/test/api.rs @@ -2,15 +2,43 @@ // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE use crate::{ - env::{LoadBytes32, StoreBytes32}, - native::NativeInstance, + env::{GetBytes32, SetBytes32}, + native::{self, NativeInstance}, + run::RunProgram, }; +use arbutil::Color; +use eyre::Result; use parking_lot::Mutex; -use prover::utils::{Bytes20, Bytes32}; +use prover::{ + programs::prelude::*, + utils::{Bytes20, Bytes32}, +}; use std::{collections::HashMap, sync::Arc}; -#[derive(Clone, Default)] -pub(crate) struct TestEvmContracts(Arc>>>); +#[derive(Clone)] +pub(crate) struct TestEvmContracts { + contracts: Arc>>>, + return_data: Arc>>, + config: StylusConfig, +} + +impl TestEvmContracts { + pub fn new(config: &StylusConfig) -> Self { + Self { + contracts: Arc::new(Mutex::new(HashMap::new())), + return_data: Arc::new(Mutex::new(vec![])), + config: config.clone(), + } + } + + pub fn insert(&mut self, address: Bytes20, name: &str) -> Result<()> { + let file = format!("tests/{name}/target/wasm32-unknown-unknown/release/{name}.wasm"); + let wasm = std::fs::read(file)?; + let module = native::module(&wasm, self.config.clone())?; + self.contracts.lock().insert(address, module); + Ok(()) + } +} #[derive(Clone, Default)] pub(crate) struct TestEvmStorage(Arc>>>); @@ -24,7 +52,7 @@ impl TestEvmStorage { self.0.lock().entry(program).or_default().insert(key, value); } - pub fn getter(&self, program: Bytes20) -> LoadBytes32 { + pub fn getter(&self, program: Bytes20) -> GetBytes32 { let storage = self.clone(); Box::new(move |key| { let value = storage.get_bytes32(program, key).unwrap().to_owned(); @@ -32,7 +60,7 @@ impl TestEvmStorage { }) } - pub fn setter(&self, program: Bytes20) -> StoreBytes32 { + pub fn setter(&self, program: Bytes20) -> SetBytes32 { let mut storage = self.clone(); Box::new(move |key, value| { drop(storage.set_bytes32(program, key, value)); @@ -46,13 +74,42 @@ impl NativeInstance { &mut self, address: Bytes20, storage: TestEvmStorage, - _contracts: TestEvmContracts, + contracts: TestEvmContracts, ) -> TestEvmStorage { let get_bytes32 = storage.getter(address); let set_bytes32 = storage.setter(address); + let moved_storage = storage.clone(); + let moved_contracts = contracts.clone(); + + let call = Box::new( + move |address: Bytes20, input: Vec, gas, _value| unsafe { + // this call function is for testing purposes only and deviates from onchain behavior + let contracts = moved_contracts.clone(); + let config = contracts.config.clone(); + *contracts.return_data.lock() = vec![]; + + let mut instance = match contracts.contracts.lock().get(&address) { + Some(module) => NativeInstance::deserialize(module, config.clone()).unwrap(), + None => panic!("No contract at address {}", address.red()), + }; + + instance.set_test_evm_api(address, moved_storage.clone(), contracts.clone()); + instance.set_gas(gas); + + let outcome = instance.run_main(&input, &config).unwrap(); + let gas_left: u64 = instance.gas_left().into(); + let (status, outs) = outcome.into_data(); + let outs_len = outs.len() as u32; + + *contracts.return_data.lock() = outs; + (outs_len, gas - gas_left, status) + }, + ); + let get_return_data = + Box::new(move || -> Vec { contracts.clone().return_data.lock().clone() }); - let call = Box::new(move |_address, _input, _gas, _value| unimplemented!("contract call")); - self.env_mut().set_evm_api(get_bytes32, set_bytes32, call); + self.env_mut() + .set_evm_api(get_bytes32, set_bytes32, call, get_return_data); storage } } diff --git a/arbitrator/stylus/src/test/mod.rs b/arbitrator/stylus/src/test/mod.rs index 58666bd9e..1fa01af83 100644 --- a/arbitrator/stylus/src/test/mod.rs +++ b/arbitrator/stylus/src/test/mod.rs @@ -1,6 +1,8 @@ // Copyright 2022-2023, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE +use prover::utils::{Bytes20, Bytes32}; +use rand::prelude::*; use wasmer::wasmparser::Operator; mod api; @@ -13,3 +15,15 @@ fn expensive_add(op: &Operator) -> u64 { _ => 0, } } + +pub fn random_bytes20() -> Bytes20 { + let mut data = [0; 20]; + rand::thread_rng().fill_bytes(&mut data); + data.into() +} + +fn random_bytes32() -> Bytes32 { + let mut data = [0; 32]; + rand::thread_rng().fill_bytes(&mut data); + data.into() +} diff --git a/arbitrator/stylus/src/test/native.rs b/arbitrator/stylus/src/test/native.rs index b076ec2eb..ae626db01 100644 --- a/arbitrator/stylus/src/test/native.rs +++ b/arbitrator/stylus/src/test/native.rs @@ -9,7 +9,10 @@ use crate::{ native::NativeInstance, run::RunProgram, - test::api::{TestEvmContracts, TestEvmStorage}, + test::{ + api::{TestEvmContracts, TestEvmStorage}, + random_bytes20, random_bytes32, + }, }; use arbutil::{crypto, Color}; use eyre::{bail, Result}; @@ -24,7 +27,7 @@ use prover::{ utils::{Bytes20, Bytes32}, Machine, }; -use std::{path::Path, sync::Arc}; +use std::{collections::HashMap, path::Path, sync::Arc}; use wasmer::wasmparser::Operator; use wasmer::{ imports, CompilerConfig, ExportIndex, Function, Imports, Instance, MemoryType, Module, Pages, @@ -61,6 +64,17 @@ fn new_vanilla_instance(path: &str) -> Result { Ok(NativeInstance::new_sans_env(instance, store)) } +fn new_native_with_evm( + file: &str, + config: &StylusConfig, +) -> Result<(NativeInstance, TestEvmContracts, TestEvmStorage)> { + let storage = TestEvmStorage::default(); + let contracts = TestEvmContracts::new(config); + let mut native = NativeInstance::from_path(file, config)?; + native.set_test_evm_api(Bytes20::default(), storage.clone(), contracts.clone()); + Ok((native, contracts, storage)) +} + fn uniform_cost_config() -> StylusConfig { let mut config = StylusConfig::default(); config.debug.count_ops = true; @@ -451,7 +465,7 @@ fn test_storage() -> Result<()> { let api = native.set_test_evm_api( address, TestEvmStorage::default(), - TestEvmContracts::default(), + TestEvmContracts::new(&config), ); run_native(&mut native, &args)?; @@ -462,3 +476,76 @@ fn test_storage() -> Result<()> { assert_eq!(output, value); Ok(()) } + +#[test] +fn test_calls() -> Result<()> { + // in call.rs + // the first bytes determines the number of calls to make + // each call starts with a length specifying how many input bytes it constitutes + // the first 20 bytes select the address you want to call, with the rest being calldata + // + // in storage.rs + // an input starting with 0x00 will induce a storage read + // all other inputs induce a storage write + + let calls_addr = random_bytes20(); + let store_addr = random_bytes20(); + println!("calls.wasm {}", calls_addr); + println!("store.wasm {}", store_addr); + + let mut slots = HashMap::new(); + + /// Forms a 2ary call tree where each leaf writes a random storage cell. + fn nest( + level: usize, + calls: Bytes20, + store: Bytes20, + slots: &mut HashMap, + ) -> Vec { + let mut args = vec![]; + + if level == 0 { + args.extend(store); // call storage.wasm + + let key = random_bytes32(); + let value = random_bytes32(); + slots.insert(key, value); + + // insert value @ key + args.push(0x01); + args.extend(key); + args.extend(value); + return args; + } + + // do the two following calls + args.extend(calls); + args.push(2); + + for _ in 0..2 { + let inner = nest(level - 1, calls, store, slots); + args.extend(u32::to_be_bytes(inner.len() as u32)); + args.extend(inner); + } + args + } + + // drop the first address to start the call tree + let tree = nest(3, calls_addr, store_addr, &mut slots); + let args = tree[20..].to_vec(); + println!("ARGS {}", hex::encode(&args)); + + let filename = "tests/calls/target/wasm32-unknown-unknown/release/calls.wasm"; + let config = uniform_cost_config(); + + let (mut native, mut contracts, storage) = new_native_with_evm(&filename, &config)?; + contracts.insert(calls_addr, "calls")?; + contracts.insert(store_addr, "storage")?; + + run_native(&mut native, &args)?; + + for (key, value) in slots { + assert_eq!(storage.get_bytes32(store_addr, key), Some(value)); + } + Ok(()) +} diff --git a/arbitrator/stylus/tests/calls/.cargo/config b/arbitrator/stylus/tests/calls/.cargo/config new file mode 100644 index 000000000..f4e8c002f --- /dev/null +++ b/arbitrator/stylus/tests/calls/.cargo/config @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" diff --git a/arbitrator/stylus/tests/calls/Cargo.lock b/arbitrator/stylus/tests/calls/Cargo.lock new file mode 100644 index 000000000..dfd455502 --- /dev/null +++ b/arbitrator/stylus/tests/calls/Cargo.lock @@ -0,0 +1,24 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arbitrum" +version = "0.1.0" +dependencies = [ + "hex", +] + +[[package]] +name = "calls" +version = "0.1.0" +dependencies = [ + "arbitrum", + "hex", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" diff --git a/arbitrator/stylus/tests/calls/Cargo.toml b/arbitrator/stylus/tests/calls/Cargo.toml new file mode 100644 index 000000000..c8ff11b1f --- /dev/null +++ b/arbitrator/stylus/tests/calls/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "calls" +version = "0.1.0" +edition = "2021" + +[dependencies] +arbitrum = { path = "../../../langs/rust/" } +hex = "0.4.3" + +[profile.release] +codegen-units = 1 +strip = true +lto = true +panic = "abort" + +# uncomment to optimize for size +# opt-level = "z" + +[workspace] diff --git a/arbitrator/stylus/tests/calls/src/main.rs b/arbitrator/stylus/tests/calls/src/main.rs new file mode 100644 index 000000000..0730529cf --- /dev/null +++ b/arbitrator/stylus/tests/calls/src/main.rs @@ -0,0 +1,36 @@ +// Copyright 2023, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +#![no_main] + +use arbitrum::{contract, debug, Bytes20}; + +arbitrum::arbitrum_main!(user_main); + +fn user_main(input: Vec) -> Result, Vec> { + let mut input = input.as_slice(); + let count = input[0]; + input = &input[1..]; + + // combined output of all calls + let mut output = vec![]; + + debug::println(format!("Calling {count} contract(s)")); + for _ in 0..count { + let length = u32::from_be_bytes(input[..4].try_into().unwrap()) as usize; + input = &input[4..]; + + let addr = Bytes20::from_slice(&input[..20]).unwrap(); + let data = &input[20..length]; + debug::println(format!("Calling {addr} with {} bytes", data.len())); + + let return_data = contract::call(addr, data, None, None)?; + if !return_data.is_empty() { + debug::println(format!("Contract {addr} returned {} bytes", return_data.len())); + } + output.extend(return_data); + input = &input[length..]; + } + + Ok(output) +} diff --git a/arbitrator/stylus/tests/storage/src/main.rs b/arbitrator/stylus/tests/storage/src/main.rs index a50ad165c..bd4582e85 100644 --- a/arbitrator/stylus/tests/storage/src/main.rs +++ b/arbitrator/stylus/tests/storage/src/main.rs @@ -3,21 +3,24 @@ #![no_main] -use arbitrum::{debug, Bytes32, load_bytes32, store_bytes32}; +use arbitrum::{debug, load_bytes32, store_bytes32, Bytes32}; arbitrum::arbitrum_main!(user_main); fn user_main(input: Vec) -> Result, Vec> { - debug::println("storage"); let read = input[0] == 0; let slot = Bytes32::from_slice(&input[1..33]).map_err(|_| vec![0x00])?; Ok(if read { + debug::println(format!("read {slot}")); let data = load_bytes32(slot); + debug::println(format!("value {data}")); data.0.into() } else { + debug::println(format!("write {slot}")); let data = Bytes32::from_slice(&input[33..]).map_err(|_| vec![0x01])?; store_bytes32(slot, data); + debug::println(format!("value {data}")); vec![] }) } diff --git a/arbitrator/wasm-libraries/user-host/src/link.rs b/arbitrator/wasm-libraries/user-host/src/link.rs index 964b83e88..55d93474d 100644 --- a/arbitrator/wasm-libraries/user-host/src/link.rs +++ b/arbitrator/wasm-libraries/user-host/src/link.rs @@ -147,10 +147,7 @@ pub unsafe extern "C" fn go__github_com_offchainlabs_nitro_arbos_programs_callUs // the program computed a final result let gas_left = program_gas_left(module, internals); - match status { - 0 => finish!(Success, heapify(outs), gas_left), - _ => finish!(Revert, heapify(outs), gas_left), - }; + finish!(status, heapify(outs), gas_left) } /// Reads the length of a rust `Vec` diff --git a/arbitrator/wasm-upstream/wasmer b/arbitrator/wasm-upstream/wasmer index 34a28983d..2ac3adce5 160000 --- a/arbitrator/wasm-upstream/wasmer +++ b/arbitrator/wasm-upstream/wasmer @@ -1 +1 @@ -Subproject commit 34a28983d693ed577b15b35c63b726d43199e649 +Subproject commit 2ac3adce5abd874b9ec5bbe2fb6f1627d14a8f42 diff --git a/arbos/programs/native.go b/arbos/programs/native.go index 8aa5ec2d6..322f0fbab 100644 --- a/arbos/programs/native.go +++ b/arbos/programs/native.go @@ -11,26 +11,37 @@ package programs #cgo LDFLAGS: ${SRCDIR}/../../target/lib/libstylus.a -ldl -lm #include "arbitrator.h" -Bytes32 getBytes32Wrap(size_t api, Bytes32 key, uint64_t * cost); -uint8_t setBytes32Wrap(size_t api, Bytes32 key, Bytes32 value, uint64_t * cost, RustVec * error); +typedef uint32_t u32; +typedef uint64_t u64; +typedef size_t usize; + +Bytes32 getBytes32Wrap(usize api, Bytes32 key, u64 * cost); +GoApiStatus setBytes32Wrap(usize api, Bytes32 key, Bytes32 value, u64 * cost, RustVec * error); +GoApiStatus callContractWrap(usize api, Bytes20 contract, RustVec * calldata, u64 * gas, Bytes32 value, u32 * len); +void getReturnDataWrap(usize api, RustVec * data); */ import "C" import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/util/arbmath" ) type u8 = C.uint8_t type u32 = C.uint32_t type u64 = C.uint64_t type usize = C.size_t +type bytes20 = C.Bytes20 type bytes32 = C.Bytes32 +type rustVec = C.RustVec func compileUserWasm(db vm.StateDB, program common.Address, wasm []byte, version uint32, debug bool) error { debugMode := 0 @@ -38,7 +49,7 @@ func compileUserWasm(db vm.StateDB, program common.Address, wasm []byte, version debugMode = 1 } - output := &C.RustVec{} + output := &rustVec{} status := userStatus(C.stylus_compile( goSlice(wasm), u32(version), @@ -57,6 +68,7 @@ func callUserWasm( db vm.StateDB, interpreter *vm.EVMInterpreter, tracingInfo *util.TracingInfo, + msg core.Message, calldata []byte, gas *uint64, stylusParams *goParams, @@ -88,57 +100,128 @@ func callUserWasm( db.SetState(program, key, value) return cost, nil } + callContract := func(contract common.Address, input []byte, gas uint64, value *big.Int) (uint32, uint64, error) { + // This closure performs a contract call. The implementation should match that of the EVM. + // + // Note that while the Yellow Paper is authoritative, the following go-ethereum + // functions provide a corresponding implementation in the vm package. + // - operations_acl.go makeCallVariantGasCallEIP2929() + // - gas_table.go gasCall() + // - instructions.go opCall() + // + + // read-only calls are not payable (opCall) + if readOnly && value.Sign() != 0 { + return 0, 0, vm.ErrWriteProtection + } + + evm := interpreter.Evm() + startGas := gas + + // computes makeCallVariantGasCallEIP2929 and gasCall + baseCost, err := vm.WasmCallCost(db, contract, value, startGas) + if err != nil { + return 0, 0, err + } + if gas < baseCost { + return 0, 0, vm.ErrOutOfGas + } + gas -= baseCost + gas = gas - gas/64 + + // Tracing: emit the call (value transfer is done later in evm.Call) + if tracingInfo != nil { + depth := evm.Depth() + tracingInfo.Tracer.CaptureState(0, vm.CALL, startGas-gas, startGas, scope, []byte{}, depth, nil) + } + + // EVM rule: calls that pay get a stipend (opCall) + if value.Sign() != 0 { + gas = arbmath.SaturatingUAdd(gas, params.CallStipend) + } + + ret, returnGas, err := evm.Call(scope.Contract, contract, input, gas, value) + interpreter.SetReturnData(ret) + cost := arbmath.SaturatingUSub(startGas, returnGas) + return uint32(len(ret)), cost, err + } + getReturnData := func() []byte { + data := interpreter.GetReturnData() + if data == nil { + return []byte{} + } + return data + } - output := &C.RustVec{} + output := &rustVec{} status := userStatus(C.stylus_call( goSlice(module), goSlice(calldata), stylusParams.encode(), - newAPI(getBytes32, setBytes32), + newAPI(getBytes32, setBytes32, callContract, getReturnData), output, (*u64)(gas), )) data, err := status.output(output.intoBytes()) + if status == userFailure { log.Debug("program failure", "err", string(data), "program", program) } return data, err } -const ( - apiSuccess u8 = iota - apiFailure -) +const apiSuccess C.GoApiStatus = C.GoApiStatus_Success +const apiFailure C.GoApiStatus = C.GoApiStatus_Failure //export getBytes32Impl func getBytes32Impl(api usize, key bytes32, cost *u64) bytes32 { - closure, err := getAPI(api) - if err != nil { - log.Error(err.Error()) - return bytes32{} - } + closure := getAPI(api) value, gas := closure.getBytes32(key.toHash()) *cost = u64(gas) return hashToBytes32(value) } //export setBytes32Impl -func setBytes32Impl(api usize, key, value bytes32, cost *u64, vec *C.RustVec) u8 { - closure, err := getAPI(api) +func setBytes32Impl(api usize, key, value bytes32, cost *u64, errVec *rustVec) C.GoApiStatus { + closure := getAPI(api) + + gas, err := closure.setBytes32(key.toHash(), value.toHash()) if err != nil { - vec.setString(err.Error()) - log.Error(err.Error()) + errVec.setString(err.Error()) return apiFailure } - gas, err := closure.setBytes32(key.toHash(), value.toHash()) + *cost = u64(gas) + return apiSuccess +} + +//export callContractImpl +func callContractImpl(api usize, contract bytes20, data *rustVec, evmGas *u64, value bytes32, len *u32) C.GoApiStatus { + closure := getAPI(api) + + ret_len, cost, err := closure.callContract(contract.toAddress(), data.read(), uint64(*evmGas), value.toBig()) + *evmGas = u64(cost) // evmGas becomes the call's cost + *len = u32(ret_len) if err != nil { - vec.setString(err.Error()) return apiFailure } - *cost = u64(gas) return apiSuccess } +//export getReturnDataImpl +func getReturnDataImpl(api usize, output *rustVec) { + closure := getAPI(api) + return_data := closure.getReturnData() + output.setBytes(return_data) +} + +func (value bytes20) toAddress() common.Address { + addr := common.Address{} + for index, b := range value.bytes { + addr[index] = byte(b) + } + return addr +} + func (value bytes32) toHash() common.Hash { hash := common.Hash{} for index, b := range value.bytes { @@ -159,21 +242,21 @@ func hashToBytes32(hash common.Hash) bytes32 { return value } -func (vec *C.RustVec) read() []byte { +func (vec *rustVec) read() []byte { return arbutil.PointerToSlice((*byte)(vec.ptr), int(vec.len)) } -func (vec *C.RustVec) intoBytes() []byte { +func (vec *rustVec) intoBytes() []byte { slice := vec.read() C.stylus_free(*vec) return slice } -func (vec *C.RustVec) setString(data string) { +func (vec *rustVec) setString(data string) { vec.setBytes([]byte(data)) } -func (vec *C.RustVec) setBytes(data []byte) { +func (vec *rustVec) setBytes(data []byte) { C.stylus_vec_set_bytes(vec, goSlice(data)) } diff --git a/arbos/programs/native_api.go b/arbos/programs/native_api.go index 6f9cc72ab..c2ca3d558 100644 --- a/arbos/programs/native_api.go +++ b/arbos/programs/native_api.go @@ -11,25 +11,38 @@ package programs #cgo LDFLAGS: ${SRCDIR}/../../target/lib/libstylus.a -ldl -lm #include "arbitrator.h" -Bytes32 getBytes32Impl(size_t api, Bytes32 key, uint64_t * cost); -Bytes32 getBytes32Wrap(size_t api, Bytes32 key, uint64_t * cost) { +typedef uint32_t u32; +typedef uint64_t u64; +typedef size_t usize; + +Bytes32 getBytes32Impl(usize api, Bytes32 key, u64 * cost); +Bytes32 getBytes32Wrap(usize api, Bytes32 key, u64 * cost) { return getBytes32Impl(api, key, cost); } -uint8_t setBytes32Impl(size_t api, Bytes32 key, Bytes32 value, uint64_t * cost, RustVec * error); -uint8_t setBytes32Wrap(size_t api, Bytes32 key, Bytes32 value, uint64_t * cost, RustVec * error) { +GoApiStatus setBytes32Impl(usize api, Bytes32 key, Bytes32 value, u64 * cost, RustVec * error); +GoApiStatus setBytes32Wrap(usize api, Bytes32 key, Bytes32 value, u64 * cost, RustVec * error) { return setBytes32Impl(api, key, value, cost, error); } + +GoApiStatus callContractImpl(usize api, Bytes20 contract, RustVec * calldata, u64 * gas, Bytes32 value, u32 * len); +GoApiStatus callContractWrap(usize api, Bytes20 contract, RustVec * calldata, u64 * gas, Bytes32 value, u32 * len) { + return callContractImpl(api, contract, calldata, gas, value, len); +} + +void getReturnDataImpl(usize api, RustVec * data); +void getReturnDataWrap(usize api, RustVec * data) { + return getReturnDataImpl(api, data); +} */ import "C" import ( - "errors" - "fmt" "math/big" "sync" "sync/atomic" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" ) var apiClosures sync.Map @@ -38,36 +51,48 @@ var apiIds int64 // atomic type getBytes32Type func(key common.Hash) (value common.Hash, cost uint64) type setBytes32Type func(key, value common.Hash) (cost uint64, err error) type callContractType func( - contract common.Address, input []byte, gas uint64, value *big.Int) (output []byte, gas_left uint64, err error, + contract common.Address, input []byte, gas uint64, value *big.Int) ( + retdata_len uint32, gas_left uint64, err error, ) +type getReturnDataType func() []byte type apiClosure struct { - getBytes32 getBytes32Type - setBytes32 setBytes32Type - callContract callContractType + getBytes32 getBytes32Type + setBytes32 setBytes32Type + callContract callContractType + getReturnData getReturnDataType } -func newAPI(getBytes32 getBytes32Type, setBytes32 setBytes32Type) C.GoAPI { +func newAPI( + getBytes32 getBytes32Type, + setBytes32 setBytes32Type, + callContract callContractType, + getReturnData getReturnDataType, +) C.GoApi { id := atomic.AddInt64(&apiIds, 1) apiClosures.Store(id, apiClosure{ - getBytes32: getBytes32, - setBytes32: setBytes32, + getBytes32: getBytes32, + setBytes32: setBytes32, + callContract: callContract, + getReturnData: getReturnData, }) - return C.GoAPI{ - get_bytes32: (*[0]byte)(C.getBytes32Wrap), - set_bytes32: (*[0]byte)(C.setBytes32Wrap), - id: u64(id), + return C.GoApi{ + get_bytes32: (*[0]byte)(C.getBytes32Wrap), + set_bytes32: (*[0]byte)(C.setBytes32Wrap), + call_contract: (*[0]byte)(C.callContractWrap), + get_return_data: (*[0]byte)(C.getReturnDataWrap), + id: u64(id), } } -func getAPI(api usize) (*apiClosure, error) { +func getAPI(api usize) *apiClosure { any, ok := apiClosures.Load(int64(api)) if !ok { - return nil, fmt.Errorf("failed to load stylus Go API %v", api) + log.Crit("failed to load stylus Go API", "id", api) } closures, ok := any.(apiClosure) if !ok { - return nil, errors.New("wrong type for stylus Go API") + log.Crit("wrong type for stylus Go API", "id", api) } - return &closures, nil + return &closures } diff --git a/arbos/programs/programs.go b/arbos/programs/programs.go index 88a0002cd..739d1903f 100644 --- a/arbos/programs/programs.go +++ b/arbos/programs/programs.go @@ -4,13 +4,14 @@ package programs import ( - "errors" "fmt" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/log" "github.com/offchainlabs/nitro/arbcompress" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" @@ -37,6 +38,10 @@ const ( wasmHostioCostOffset ) +var ProgramNotCompiledError func() error +var ProgramOutOfDateError func(version uint32) error +var ProgramUpToDateError func() error + func Initialize(sto *storage.Storage) { wasmGasPrice := sto.OpenStorageBackedBips(wasmGasPriceOffset) wasmMaxDepth := sto.OpenStorageBackedUint32(wasmMaxDepthOffset) @@ -97,7 +102,7 @@ func (p Programs) CompileProgram(statedb vm.StateDB, program common.Address, deb return 0, err } if latest >= version { - return 0, errors.New("program is current") + return 0, ProgramUpToDateError() } wasm, err := getWasm(statedb, program) @@ -115,6 +120,7 @@ func (p Programs) CallProgram( statedb vm.StateDB, interpreter *vm.EVMInterpreter, tracingInfo *util.TracingInfo, + msg core.Message, calldata []byte, gas *uint64, ) ([]byte, error) { @@ -127,16 +133,16 @@ func (p Programs) CallProgram( return nil, err } if programVersion == 0 { - return nil, errors.New("program not compiled") + return nil, ProgramNotCompiledError() } if programVersion != stylusVersion { - return nil, errors.New("program out of date, please recompile") + return nil, ProgramOutOfDateError(programVersion) } params, err := p.goParams(programVersion, interpreter.Evm().ChainConfig().DebugMode()) if err != nil { return nil, err } - return callUserWasm(scope, statedb, interpreter, tracingInfo, calldata, gas, params) + return callUserWasm(scope, statedb, interpreter, tracingInfo, msg, calldata, gas, params) } func getWasm(statedb vm.StateDB, program common.Address) ([]byte, error) { @@ -199,14 +205,15 @@ func (status userStatus) output(data []byte) ([]byte, error) { case userSuccess: return data, nil case userRevert: - return data, errors.New("program reverted") + return data, vm.ErrExecutionReverted case userFailure: - return nil, errors.New("program failure") + return nil, vm.ErrExecutionReverted case userOutOfGas: return nil, vm.ErrOutOfGas case userOutOfStack: return nil, vm.ErrDepth default: - return nil, errors.New("unknown status kind") + log.Error("program errored with unknown status", "status", status, "data", common.Bytes2Hex(data)) + return nil, vm.ErrExecutionReverted } } diff --git a/arbos/programs/wasm.go b/arbos/programs/wasm.go index e4115465b..dbdcf660f 100644 --- a/arbos/programs/wasm.go +++ b/arbos/programs/wasm.go @@ -10,6 +10,7 @@ import ( "errors" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" "github.com/offchainlabs/nitro/arbos/util" @@ -46,6 +47,7 @@ func callUserWasm( db vm.StateDB, _ *vm.EVMInterpreter, _ *util.TracingInfo, + _ core.Message, calldata []byte, gas *uint64, params *goParams, diff --git a/arbos/tx_processor.go b/arbos/tx_processor.go index d61ac3b00..e8cef7d33 100644 --- a/arbos/tx_processor.go +++ b/arbos/tx_processor.go @@ -113,6 +113,7 @@ func (p *TxProcessor) ExecuteWASM(scope *vm.ScopeContext, input []byte, interpre p.evm.StateDB, interpreter, tracingInfo, + p.msg, input, &contract.Gas, ) diff --git a/arbos/util/util.go b/arbos/util/util.go index 1514d6d10..f346c44be 100644 --- a/arbos/util/util.go +++ b/arbos/util/util.go @@ -63,37 +63,37 @@ func init() { } } - // Create a mechanism for packing and unpacking calls - callParser := func(source string, name string) (func(...interface{}) ([]byte, error), func([]byte) (map[string]interface{}, error)) { - contract, err := abi.JSON(strings.NewReader(source)) - if err != nil { - panic(fmt.Sprintf("failed to parse ABI for %s: %s", name, err)) - } - method, ok := contract.Methods[name] - if !ok { - panic(fmt.Sprintf("method %v does not exist", name)) - } - pack := func(args ...interface{}) ([]byte, error) { - return contract.Pack(name, args...) - } - unpack := func(data []byte) (map[string]interface{}, error) { - if len(data) < 4 { - return nil, errors.New("data not long enough") - } - args := make(map[string]interface{}) - return args, method.Inputs.UnpackIntoMap(args, data[4:]) - } - return pack, unpack - } - ParseRedeemScheduledLog = logParser(precompilesgen.ArbRetryableTxABI, "RedeemScheduled") ParseL2ToL1TxLog = logParser(precompilesgen.ArbSysABI, "L2ToL1Tx") ParseL2ToL1TransactionLog = logParser(precompilesgen.ArbSysABI, "L2ToL1Transaction") acts := precompilesgen.ArbosActsABI - PackInternalTxDataStartBlock, UnpackInternalTxDataStartBlock = callParser(acts, "startBlock") - PackInternalTxDataBatchPostingReport, UnpackInternalTxDataBatchPostingReport = callParser(acts, "batchPostingReport") - PackArbRetryableTxRedeem, _ = callParser(precompilesgen.ArbRetryableTxABI, "redeem") + PackInternalTxDataStartBlock, UnpackInternalTxDataStartBlock = NewCallParser(acts, "startBlock") + PackInternalTxDataBatchPostingReport, UnpackInternalTxDataBatchPostingReport = NewCallParser(acts, "batchPostingReport") + PackArbRetryableTxRedeem, _ = NewCallParser(precompilesgen.ArbRetryableTxABI, "redeem") +} + +// Create a mechanism for packing and unpacking calls +func NewCallParser(source string, name string) (func(...interface{}) ([]byte, error), func([]byte) (map[string]interface{}, error)) { + contract, err := abi.JSON(strings.NewReader(source)) + if err != nil { + panic(fmt.Sprintf("failed to parse ABI for %s: %s", name, err)) + } + method, ok := contract.Methods[name] + if !ok { + panic(fmt.Sprintf("method %v does not exist", name)) + } + pack := func(args ...interface{}) ([]byte, error) { + return contract.Pack(name, args...) + } + unpack := func(data []byte) (map[string]interface{}, error) { + if len(data) < 4 { + return nil, errors.New("data not long enough") + } + args := make(map[string]interface{}) + return args, method.Inputs.UnpackIntoMap(args, data[4:]) + } + return pack, unpack } func AddressToHash(address common.Address) common.Hash { diff --git a/contracts/src/precompiles/ArbWasm.sol b/contracts/src/precompiles/ArbWasm.sol index 32004859e..43032c5a8 100644 --- a/contracts/src/precompiles/ArbWasm.sol +++ b/contracts/src/precompiles/ArbWasm.sol @@ -29,4 +29,8 @@ interface ArbWasm { // @notice gets the fixed-cost overhead needed to initiate a hostio call // @return cost the cost (in wasm gas) of starting a stylus hostio call function wasmHostioCost() external view returns (uint64 price); + + error ProgramNotCompiled(); + error ProgramOutOfDate(uint32 version); + error ProgramUpToDate(); } diff --git a/go-ethereum b/go-ethereum index 55c15e939..fa5c410aa 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 55c15e939e65fc2e02ec07b1d1d9ee95afae5a88 +Subproject commit fa5c410aac32b7a4936915fe742a3f9e93b11670 diff --git a/precompiles/ArbDebug.go b/precompiles/ArbDebug.go index b7731125f..fa383536a 100644 --- a/precompiles/ArbDebug.go +++ b/precompiles/ArbDebug.go @@ -39,7 +39,7 @@ func (con ArbDebug) Events(c ctx, evm mech, paid huge, flag bool, value bytes32) } func (con ArbDebug) CustomRevert(c ctx, number uint64) error { - return con.CustomError(number, "This spider family wards off bugs: /\\oo/\\ //\\(oo)/\\ /\\oo/\\", true) + return con.CustomError(number, "This spider family wards off bugs: /\\oo/\\ //\\(oo)//\\ /\\oo/\\", true) } // Caller becomes a chain owner diff --git a/precompiles/ArbWasm.go b/precompiles/ArbWasm.go index 2c95206e1..e8fa9190b 100644 --- a/precompiles/ArbWasm.go +++ b/precompiles/ArbWasm.go @@ -5,6 +5,10 @@ package precompiles type ArbWasm struct { Address addr // 0x71 + + ProgramNotCompiledError func() error + ProgramOutOfDateError func(version uint32) error + ProgramUpToDateError func() error } // Compile a wasm program with the latest instrumentation diff --git a/precompiles/precompile.go b/precompiles/precompile.go index 968c67b02..39325da31 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -14,6 +14,7 @@ import ( "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/programs" "github.com/offchainlabs/nitro/arbos/util" templates "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/arbmath" @@ -548,8 +549,12 @@ func Precompiles() map[addr]ArbosPrecompile { ArbGasInfo := insert(MakePrecompile(templates.ArbGasInfoMetaData, &ArbGasInfo{Address: hex("6c")})) ArbGasInfo.methodsByName["GetL1FeesAvailable"].arbosVersion = 10 - ArbWasm := insert(MakePrecompile(templates.ArbWasmMetaData, &ArbWasm{Address: hex("71")})) + ArbWasmImpl := &ArbWasm{Address: types.ArbWasmAddress} + ArbWasm := insert(MakePrecompile(templates.ArbWasmMetaData, ArbWasmImpl)) ArbWasm.arbosVersion = 11 + programs.ProgramNotCompiledError = ArbWasmImpl.ProgramNotCompiledError + programs.ProgramOutOfDateError = ArbWasmImpl.ProgramOutOfDateError + programs.ProgramUpToDateError = ArbWasmImpl.ProgramUpToDateError ArbRetryableImpl := &ArbRetryableTx{Address: types.ArbRetryableTxAddress} ArbRetryable := insert(MakePrecompile(templates.ArbRetryableTxMetaData, ArbRetryableImpl)) @@ -730,7 +735,10 @@ func (p *Precompile) Call( return solErr.data, callerCtx.gasLeft, vm.ErrExecutionReverted } if !errors.Is(errRet, vm.ErrOutOfGas) { - log.Debug("precompile reverted with non-solidity error", "precompile", precompileAddress, "input", input, "err", errRet) + log.Debug( + "precompile reverted with non-solidity error", + "precompile", precompileAddress, "input", input, "err", errRet, + ) } // nolint:errorlint if arbosVersion >= 11 || errRet == vm.ErrExecutionReverted { diff --git a/system_tests/precompile_test.go b/system_tests/precompile_test.go index 6cc4a7f8d..a6c0db448 100644 --- a/system_tests/precompile_test.go +++ b/system_tests/precompile_test.go @@ -48,7 +48,7 @@ func TestCustomSolidityErrors(t *testing.T) { Fail(t, "customRevert call should have errored") } observedMessage := customError.Error() - expectedMessage := "execution reverted: error Custom(1024, This spider family wards off bugs: /\\oo/\\ //\\(oo)/\\ /\\oo/\\, true)" + expectedMessage := "execution reverted: error Custom(1024, This spider family wards off bugs: /\\oo/\\ //\\(oo)//\\ /\\oo/\\, true)" if observedMessage != expectedMessage { Fail(t, observedMessage) } diff --git a/system_tests/program_test.go b/system_tests/program_test.go index 47d5811af..67847bacc 100644 --- a/system_tests/program_test.go +++ b/system_tests/program_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" @@ -21,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/offchainlabs/nitro/arbcompress" "github.com/offchainlabs/nitro/arbnode" + "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/solgen/go/mocksgen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/util/arbmath" @@ -37,8 +39,7 @@ func TestProgramKeccakArb(t *testing.T) { } func keccakTest(t *testing.T, jit bool) { - file := "../arbitrator/stylus/tests/keccak/target/wasm32-unknown-unknown/release/keccak.wasm" - ctx, node, _, l2client, auth, programAddress, cleanup := setupProgramTest(t, file, jit) + ctx, node, _, l2client, auth, programAddress, cleanup := setupProgramTest(t, rustFile("keccak"), jit) defer cleanup() preimage := []byte("°º¤ø,¸,ø¤°º¤ø,¸,ø¤°º¤ø,¸ nyan nyan ~=[,,_,,]:3 nyan nyan") @@ -84,8 +85,7 @@ func TestProgramErrorsArb(t *testing.T) { } func errorTest(t *testing.T, jit bool) { - file := "../arbitrator/stylus/tests/fallible/target/wasm32-unknown-unknown/release/fallible.wasm" - ctx, node, l2info, l2client, _, programAddress, cleanup := setupProgramTest(t, file, jit) + ctx, node, l2info, l2client, _, programAddress, cleanup := setupProgramTest(t, rustFile("fallible"), jit) defer cleanup() // ensure tx passes @@ -107,8 +107,7 @@ func errorTest(t *testing.T, jit bool) { } func TestProgramStorage(t *testing.T) { - file := "../arbitrator/stylus/tests/storage/target/wasm32-unknown-unknown/release/storage.wasm" - ctx, _, l2info, l2client, _, programAddress, cleanup := setupProgramTest(t, file, true) + ctx, _, l2info, l2client, _, programAddress, cleanup := setupProgramTest(t, rustFile("storage"), true) defer cleanup() ensure := func(tx *types.Transaction, err error) *types.Receipt { @@ -140,6 +139,160 @@ func TestProgramStorage(t *testing.T) { // validateBlocks(t, 1, ctx, node, l2client) } +func TestProgramCalls(t *testing.T) { + ctx, _, l2info, l2client, auth, callsAddr, cleanup := setupProgramTest(t, rustFile("calls"), true) + defer cleanup() + + ensure := func(tx *types.Transaction, err error) *types.Receipt { + t.Helper() + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, l2client, tx) + Require(t, err) + return receipt + } + + storeAddr := deployWasm(t, ctx, auth, l2client, rustFile("storage")) + keccakAddr := deployWasm(t, ctx, auth, l2client, rustFile("keccak")) + mockAddr, tx, _, err := mocksgen.DeployProgramTest(&auth, l2client) + ensure(tx, err) + + colors.PrintGrey("calls.wasm ", callsAddr) + colors.PrintGrey("storage.wasm ", storeAddr) + colors.PrintGrey("keccak.wasm ", keccakAddr) + colors.PrintGrey("mock.evm ", mockAddr) + + slots := make(map[common.Hash]common.Hash) + + var nest func(level uint) []uint8 + nest = func(level uint) []uint8 { + args := []uint8{} + + if level == 0 { + args = append(args, storeAddr[:]...) + + key := testhelpers.RandomHash() + value := testhelpers.RandomHash() + slots[key] = value + + // insert value @ key + args = append(args, 0x01) + args = append(args, key[:]...) + args = append(args, value[:]...) + return args + } + + // do the two following calls + args = append(args, callsAddr[:]...) + args = append(args, 2) + + for i := 0; i < 2; i++ { + inner := nest(level - 1) + args = append(args, arbmath.Uint32ToBytes(uint32(len(inner)))...) + args = append(args, inner...) + } + return args + } + tree := nest(3)[20:] + colors.PrintGrey(common.Bytes2Hex(tree)) + + tx = l2info.PrepareTxTo("Owner", &callsAddr, 1e9, big.NewInt(0), tree) + ensure(tx, l2client.SendTransaction(ctx, tx)) + + for key, value := range slots { + storedBytes, err := l2client.StorageAt(ctx, storeAddr, key, nil) + Require(t, err) + storedValue := common.BytesToHash(storedBytes) + if value != storedValue { + Fail(t, "wrong value", value, storedValue) + } + } + + // mechanisms for creating calldata + burnArbGas, _ := util.NewCallParser(precompilesgen.ArbosTestABI, "burnArbGas") + customRevert, _ := util.NewCallParser(precompilesgen.ArbDebugABI, "customRevert") + legacyError, _ := util.NewCallParser(precompilesgen.ArbDebugABI, "legacyError") + callKeccak, _ := util.NewCallParser(mocksgen.ProgramTestABI, "callKeccak") + pack := func(data []byte, err error) []byte { + Require(t, err) + return data + } + makeCalldata := func(address common.Address, calldata []byte) []byte { + args := []byte{0x01} + args = append(args, arbmath.Uint32ToBytes(uint32(20+len(calldata)))...) + args = append(args, address.Bytes()...) + args = append(args, calldata...) + return args + } + + // Set a random, non-zero gas price + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, l2client) + Require(t, err) + wasmGasPrice := testhelpers.RandomUint64(1, 2000) + ensure(arbOwner.SetWasmGasPrice(&auth, wasmGasPrice)) + colors.PrintBlue("Calling the ArbosTest precompile with wasmGasPrice=", wasmGasPrice) + + testPrecompile := func(gas uint64) uint64 { + // Call the burnArbGas() precompile from Rust + args := makeCalldata(types.ArbosTestAddress, pack(burnArbGas(big.NewInt(int64(gas))))) + tx := l2info.PrepareTxTo("Owner", &callsAddr, 1e9, big.NewInt(0), args) + return ensure(tx, l2client.SendTransaction(ctx, tx)).GasUsed + } + + smallGas := testhelpers.RandomUint64(2000, 8000) + largeGas := smallGas + testhelpers.RandomUint64(2000, 8000) + small := testPrecompile(smallGas) + large := testPrecompile(largeGas) + + if large-small != largeGas-smallGas { + ratio := float64(large-small) / float64(largeGas-smallGas) + Fail(t, "inconsistent burns", smallGas, largeGas, small, large, ratio) + } + + expectFailure := func(to common.Address, data []byte, errMsg string) { + t.Helper() + msg := ethereum.CallMsg{ + To: &to, + Value: big.NewInt(0), + Data: data, + } + _, err := l2client.CallContract(ctx, msg, nil) + if err == nil { + Fail(t, "call should have failed with", errMsg) + } + expected := fmt.Sprintf("execution reverted%v", errMsg) + if err.Error() != expected { + Fail(t, "wrong error", err.Error(), expected) + } + + // execute onchain for proving's sake + tx := l2info.PrepareTxTo("Owner", &callsAddr, 1e9, big.NewInt(0), data) + Require(t, l2client.SendTransaction(ctx, tx)) + receipt, err := WaitForTx(ctx, l2client, tx.Hash(), 5*time.Second) + Require(t, err) + if receipt.Status != types.ReceiptStatusFailed { + Fail(t, "unexpected success") + } + } + + colors.PrintBlue("Checking consensus revert data (Rust => precompile)") + args := makeCalldata(types.ArbDebugAddress, pack(customRevert(uint64(32)))) + spider := ": error Custom(32, This spider family wards off bugs: /\\oo/\\ //\\(oo)//\\ /\\oo/\\, true)" + expectFailure(callsAddr, args, spider) + + colors.PrintBlue("Checking non-consensus revert data (Rust => precompile)") + args = makeCalldata(types.ArbDebugAddress, pack(legacyError())) + expectFailure(callsAddr, args, "") + + colors.PrintBlue("Checking success (Rust => Solidity => Rust)") + rustArgs := append([]byte{0x01}, []byte(spider)...) + mockArgs := makeCalldata(mockAddr, pack(callKeccak(keccakAddr, rustArgs))) + tx = l2info.PrepareTxTo("Owner", &callsAddr, 1e9, big.NewInt(0), mockArgs) + ensure(tx, l2client.SendTransaction(ctx, tx)) + + // TODO: enable validation when prover side is PR'd + // validateBlocks(t, 1, ctx, node, l2client) +} + func setupProgramTest(t *testing.T, file string, jit bool) ( context.Context, *arbnode.Node, *BlockchainTestInfo, *ethclient.Client, bind.TransactOpts, common.Address, func(), ) { @@ -151,6 +304,7 @@ func setupProgramTest(t *testing.T, file string, jit bool) ( l2config.BlockValidator.Enable = true l2config.BatchPoster.Enable = true l2config.L1Reader.Enable = true + l2config.Sequencer.MaxRevertGasReject = 0 AddDefaultValNode(t, ctx, l2config, jit) l2info, node, l2client, _, _, _, l1stack := createTestNodeOnL1WithConfig(t, ctx, true, l2config, chainConfig, nil) @@ -162,8 +316,6 @@ func setupProgramTest(t *testing.T, file string, jit bool) ( } auth := l2info.GetDefaultTransactOpts("Owner", ctx) - arbWasm, err := precompilesgen.NewArbWasm(types.ArbWasmAddress, l2client) - Require(t, err) arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, l2client) Require(t, err) @@ -194,27 +346,46 @@ func setupProgramTest(t *testing.T, file string, jit bool) ( ensure(arbOwner.SetWasmGasPrice(&auth, wasmGasPrice)) ensure(arbOwner.SetWasmHostioCost(&auth, wasmHostioCost)) + programAddress := deployWasm(t, ctx, auth, l2client, file) + + return ctx, node, l2info, l2client, auth, programAddress, cleanup +} + +func deployWasm( + t *testing.T, ctx context.Context, auth bind.TransactOpts, l2client *ethclient.Client, file string, +) common.Address { wasmSource, err := os.ReadFile(file) Require(t, err) wasm, err := arbcompress.CompressWell(wasmSource) Require(t, err) - wasm = append(state.StylusPrefix, wasm...) - toKb := func(data []byte) float64 { return float64(len(data)) / 1024.0 } colors.PrintMint(fmt.Sprintf("WASM len %.2fK vs %.2fK", toKb(wasm), toKb(wasmSource))) + wasm = append(state.StylusPrefix, wasm...) + programAddress := deployContract(t, ctx, auth, l2client, wasm) colors.PrintBlue("program deployed to ", programAddress.Hex()) + arbWasm, err := precompilesgen.NewArbWasm(types.ArbWasmAddress, l2client) + Require(t, err) + timed(t, "compile", func() { - ensure(arbWasm.CompileProgram(&auth, programAddress)) + tx, err := arbWasm.CompileProgram(&auth, programAddress) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, l2client, tx) + Require(t, err) }) - return ctx, node, l2info, l2client, auth, programAddress, cleanup + return programAddress +} + +func rustFile(name string) string { + return fmt.Sprintf("../arbitrator/stylus/tests/%v/target/wasm32-unknown-unknown/release/%v.wasm", name, name) } func validateBlocks(t *testing.T, start uint64, ctx context.Context, node *arbnode.Node, l2client *ethclient.Client) { + colors.PrintGrey("Validating blocks from ", start, " onward") doUntil(t, 20*time.Millisecond, 50, func() bool { batchCount, err := node.InboxTracker.GetBatchCount() diff --git a/util/colors/colors.go b/util/colors/colors.go index 5267d688e..c652d80ca 100644 --- a/util/colors/colors.go +++ b/util/colors/colors.go @@ -52,6 +52,12 @@ func PrintYellow(args ...interface{}) { println(Clear) } +func PrintPink(args ...interface{}) { + print(Pink) + fmt.Print(args...) + println(Clear) +} + func Uncolor(text string) string { uncolor := regexp.MustCompile("\x1b\\[([0-9]+;)*[0-9]+m") unwhite := regexp.MustCompile(`\s+`)