diff --git a/Makefile b/Makefile index f8605e3c1..1082e7a12 100644 --- a/Makefile +++ b/Makefile @@ -153,6 +153,7 @@ build-node-deps: $(go_source) build-prover-header build-prover-lib build-jit .ma test-go-deps: \ build-replay-env \ $(stylus_test_wasms) \ + $(arbitrator_stylus_lib) \ $(patsubst %,$(arbitrator_cases)/%.wasm, global-state read-inboxmsg-10 global-state-wrapper const) build-prover-header: $(arbitrator_generated_header) diff --git a/arbitrator/Cargo.lock b/arbitrator/Cargo.lock index f0de36f23..20a7e8335 100644 --- a/arbitrator/Cargo.lock +++ b/arbitrator/Cargo.lock @@ -65,6 +65,7 @@ dependencies = [ "digest 0.9.0", "eyre", "hex", + "num-traits", "serde", "sha3 0.10.8", "siphasher", @@ -1097,9 +1098,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -1802,6 +1803,7 @@ name = "stylus" version = "0.1.0" dependencies = [ "arbutil", + "bincode", "derivative", "eyre", "fnv", diff --git a/arbitrator/arbutil/Cargo.toml b/arbitrator/arbutil/Cargo.toml index f48556548..e2a3a750e 100644 --- a/arbitrator/arbutil/Cargo.toml +++ b/arbitrator/arbutil/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" digest = "0.9.0" eyre = "0.6.5" hex = "0.4.3" +num-traits = "0.2.17" sha3 = "0.10.5" siphasher = "0.3.10" wasmparser = "0.83" diff --git a/arbitrator/arbutil/src/math.rs b/arbitrator/arbutil/src/math.rs index 2e8631214..a7556974d 100644 --- a/arbitrator/arbutil/src/math.rs +++ b/arbitrator/arbutil/src/math.rs @@ -1,6 +1,7 @@ // Copyright 2023, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE +use num_traits::{ops::saturating::SaturatingAdd, Zero}; use std::ops::{BitAnd, Sub}; /// Checks if a number is a power of 2. @@ -13,3 +14,22 @@ where } value & (value - 1.into()) == 0.into() } + +/// Calculates a sum, saturating in cases of overflow. +pub trait SaturatingSum { + type Number; + + fn saturating_sum(self) -> Self::Number; +} + +impl SaturatingSum for I +where + I: Iterator, + T: SaturatingAdd + Zero, +{ + type Number = T; + + fn saturating_sum(self) -> Self::Number { + self.fold(T::zero(), |acc, x| acc.saturating_add(&x)) + } +} diff --git a/arbitrator/jit/src/gostack.rs b/arbitrator/jit/src/gostack.rs index 14b1d4084..ad166f4a5 100644 --- a/arbitrator/jit/src/gostack.rs +++ b/arbitrator/jit/src/gostack.rs @@ -11,7 +11,10 @@ use crate::{ use arbutil::Color; use ouroboros::self_referencing; use rand_pcg::Pcg32; -use std::collections::{BTreeSet, BinaryHeap}; +use std::{ + collections::{BTreeSet, BinaryHeap}, + fmt::Debug, +}; use wasmer::{AsStoreRef, Memory, MemoryView, StoreMut, StoreRef, WasmPtr}; #[self_referencing] @@ -138,6 +141,10 @@ impl GoStack { self.read_u64() as *mut T } + pub unsafe fn read_ref<'a, T>(&mut self) -> &'a T { + &*self.read_ptr() + } + /// TODO: replace `unbox` with a safe id-based API pub fn unbox(&mut self) -> T { let ptr: *mut T = self.read_ptr_mut(); @@ -236,9 +243,12 @@ impl GoStack { data } - pub fn write_slice(&self, ptr: u64, src: &[u8]) { - u32::try_from(ptr).expect("Go pointer not a u32"); - self.view().write(ptr, src).unwrap(); + pub fn write_slice>(&self, ptr: T, src: &[u8]) + where + T::Error: Debug, + { + let ptr: u32 = ptr.try_into().expect("Go pointer not a u32"); + self.view().write(ptr.into(), src).unwrap(); } pub fn read_value_slice(&self, mut ptr: u64, len: u64) -> Vec { diff --git a/arbitrator/jit/src/machine.rs b/arbitrator/jit/src/machine.rs index 9b91f0ff5..40feb9d7e 100644 --- a/arbitrator/jit/src/machine.rs +++ b/arbitrator/jit/src/machine.rs @@ -21,6 +21,7 @@ use std::{ io::{self, Write}, io::{BufReader, BufWriter, ErrorKind, Read}, net::TcpStream, + sync::Arc, time::{Duration, Instant}, }; @@ -114,11 +115,10 @@ pub fn create(opts: &Opts, env: WasmEnv) -> (Instance, FunctionEnv, Sto github!("wavmio.readDelayedInboxMessage") => func!(wavmio::read_delayed_inbox_message), github!("wavmio.resolvePreImage") => func!(wavmio::resolve_preimage), - github!("arbos/programs.compileUserWasmRustImpl") => func!(user::compile_user_wasm), - github!("arbos/programs.callUserWasmRustImpl") => func!(user::call_user_wasm), + github!("arbos/programs.activateProgramRustImpl") => func!(user::stylus_activate), + github!("arbos/programs.callProgramRustImpl") => func!(user::stylus_call), github!("arbos/programs.readRustVecLenImpl") => func!(user::read_rust_vec_len), github!("arbos/programs.rustVecIntoSliceImpl") => func!(user::rust_vec_into_slice), - github!("arbos/programs.rustMachineDropImpl") => func!(user::drop_machine), github!("arbos/programs.rustConfigImpl") => func!(user::rust_config_impl), github!("arbos/programs.rustEvmDataImpl") => func!(user::evm_data_impl), @@ -193,10 +193,7 @@ impl From for Escape { pub type WasmEnvMut<'a> = FunctionEnvMut<'a, WasmEnv>; pub type Inbox = BTreeMap>; pub type Oracle = BTreeMap>; - -/// Represents a mapping of a WASM program codehash and version to the compiled wasm -/// code itself and its noncanonical program hash. -pub type UserWasms = HashMap<(Bytes32, u16), (Vec, Bytes32)>; +pub type ModuleAsm = Arc<[u8]>; #[derive(Default)] pub struct WasmEnv { @@ -212,8 +209,8 @@ pub struct WasmEnv { pub large_globals: [Bytes32; 2], /// An oracle allowing the prover to reverse keccak256 pub preimages: Oracle, - /// A collection of user wasms called during the course of execution - pub user_wasms: UserWasms, + /// A collection of programs called during the course of execution + pub module_asms: HashMap, /// The sequencer inbox's messages pub sequencer_messages: Inbox, /// The delayed inbox's messages diff --git a/arbitrator/jit/src/socket.rs b/arbitrator/jit/src/socket.rs index 6b4370196..f63653ad4 100644 --- a/arbitrator/jit/src/socket.rs +++ b/arbitrator/jit/src/socket.rs @@ -20,11 +20,6 @@ pub fn read_u8(reader: &mut BufReader) -> Result { reader.read_exact(&mut buf).map(|_| u8::from_be_bytes(buf)) } -pub fn read_u16(reader: &mut BufReader) -> Result { - let mut buf = [0; 2]; - reader.read_exact(&mut buf).map(|_| u16::from_be_bytes(buf)) -} - pub fn read_u32(reader: &mut BufReader) -> Result { let mut buf = [0; 4]; reader.read_exact(&mut buf).map(|_| u32::from_be_bytes(buf)) @@ -47,6 +42,10 @@ pub fn read_bytes(reader: &mut BufReader) -> Result, io::Err Ok(buf) } +pub fn read_boxed_slice(reader: &mut BufReader) -> Result, io::Error> { + Ok(Vec::into_boxed_slice(read_bytes(reader)?)) +} + pub fn write_u8(writer: &mut BufWriter, data: u8) -> Result<(), io::Error> { let buf = [data; 1]; writer.write_all(&buf) diff --git a/arbitrator/jit/src/user/evm_api.rs b/arbitrator/jit/src/user/evm_api.rs index 183ebfb0e..d17290226 100644 --- a/arbitrator/jit/src/user/evm_api.rs +++ b/arbitrator/jit/src/user/evm_api.rs @@ -5,7 +5,7 @@ use crate::{ gostack::GoStack, - machine::WasmEnvMut, + machine::{ModuleAsm, WasmEnvMut}, syscall::{DynamicObject, GoValue, JsValue, STYLUS_ID}, }; use arbutil::{ @@ -53,7 +53,7 @@ impl JsCallIntoGo for ApiCaller { pub(super) fn exec_wasm( sp: &mut GoStack, mut env: WasmEnvMut, - module: Vec, + module: ModuleAsm, calldata: Vec, compile: CompileConfig, config: StylusConfig, diff --git a/arbitrator/jit/src/user/mod.rs b/arbitrator/jit/src/user/mod.rs index 9d15ed5fe..30e6d062f 100644 --- a/arbitrator/jit/src/user/mod.rs +++ b/arbitrator/jit/src/user/mod.rs @@ -5,6 +5,7 @@ use crate::{ gostack::GoStack, machine::{Escape, MaybeEscape, WasmEnvMut}, user::evm_api::exec_wasm, + wavmio::Bytes32, }; use arbutil::{ evm::{user::UserOutcome, EvmData}, @@ -12,53 +13,56 @@ use arbutil::{ heapify, }; use prover::{ + machine::Module, programs::{config::PricingParams, prelude::*}, - Machine, }; use std::mem; -use stylus::native; mod evm_api; -/// Compiles and instruments a user wasm. +/// Instruments and "activates" a user wasm, producing a unique module hash. +/// +/// Note that this operation costs gas and is limited by the amount supplied via the `gas` pointer. +/// The amount left is written back at the end of the call. /// /// # Go side /// +/// The `modHash` and `gas` pointers must not be null. +/// /// The Go compiler expects the call to take the form -/// λ(wasm []byte, pageLimit, version u16, debug u32) (module *Vec, info WasmInfo, err *Vec) +/// λ(wasm []byte, pageLimit, version u16, debug u32, modHash *hash, gas *u64) (footprint u16, err *Vec) /// /// These values are placed on the stack as follows -/// stack: || wasm... || pageLimit | version | debug || mod ptr || info... || err ptr || -/// info: || footprint | 2 pad | size || +/// || wasm... || pageLimit | version | debug || modhash ptr || gas ptr || footprint | 6 pad || err ptr || /// -pub fn compile_user_wasm(env: WasmEnvMut, sp: u32) { +pub fn stylus_activate(env: WasmEnvMut, sp: u32) { let mut sp = GoStack::simple(sp, &env); let wasm = sp.read_go_slice_owned(); let page_limit = sp.read_u16(); let version = sp.read_u16(); let debug = sp.read_bool32(); - let compile = CompileConfig::version(version, debug); + let module_hash = sp.read_go_ptr(); + let gas = sp.read_go_ptr(); macro_rules! error { ($error:expr) => {{ - let error = $error.wrap_err("failed to compile").debug_bytes(); - sp.write_nullptr(); - sp.skip_space(); // skip info + let error = $error.wrap_err("failed to activate").debug_bytes(); + sp.write_u64_raw(gas, 0); + sp.write_slice(module_hash, &Bytes32::default()); + sp.skip_space(); sp.write_ptr(heapify(error)); return; }}; } - let (footprint, size) = match Machine::new_user_stub(&wasm, page_limit, version, debug) { - Ok((_, info)) => (info.footprint, info.size), - Err(error) => error!(error), - }; - let module = match native::module(&wasm, compile) { - Ok(module) => module, + let gas_left = &mut sp.read_u64_raw(gas); + let (module, pages) = match Module::activate(&wasm, version, page_limit, debug, gas_left) { + Ok(result) => result, Err(error) => error!(error), }; - sp.write_ptr(heapify(module)); - sp.write_u16(footprint).skip_u16().write_u32(size); // wasm info + sp.write_u64_raw(gas, *gas_left); + sp.write_slice(module_hash, &module.hash().0); + sp.write_u16(pages).skip_space(); sp.write_nullptr(); } @@ -67,32 +71,35 @@ pub fn compile_user_wasm(env: WasmEnvMut, sp: u32) { /// # Go side /// /// The Go compiler expects the call to take the form -/// λ( -/// mach *Machine, calldata []byte, params *Configs, evmApi []byte, evmData: *EvmData, -/// gas *u64, root *[32]byte -/// ) -> (status byte, out *Vec) +/// λ(moduleHash *[32]byte, calldata []byte, params *Configs, evmApi []byte, evmData: *EvmData, gas *u64) ( +/// status byte, out *Vec, +/// ) /// /// These values are placed on the stack as follows -/// || mach || calldata... || params || evmApi... || evmData || gas || root || status | 3 pad | out ptr || +/// || modHash || calldata... || params || evmApi... || evmData || gas || status | 7 pad | out ptr || /// -pub fn call_user_wasm(env: WasmEnvMut, sp: u32) -> MaybeEscape { +pub fn stylus_call(env: WasmEnvMut, sp: u32) -> MaybeEscape { let sp = &mut GoStack::simple(sp, &env); use UserOutcome::*; // move inputs - let module: Vec = sp.unbox(); + let module_hash = sp.read_bytes32(); let calldata = sp.read_go_slice_owned(); let (compile, config): (CompileConfig, StylusConfig) = sp.unbox(); let evm_api = sp.read_go_slice_owned(); let evm_data: EvmData = sp.unbox(); + let gas = sp.read_go_ptr(); // buy ink let pricing = config.pricing; - let gas = sp.read_go_ptr(); let ink = pricing.gas_to_ink(sp.read_u64_raw(gas)); - // skip the root since we don't use these - sp.skip_u64(); + let Some(module) = env.data().module_asms.get(&module_hash).cloned() else { + return Escape::failure(format!( + "module hash {module_hash:?} not found in {:?}", + env.data().module_asms.keys() + )); + }; let result = exec_wasm( sp, env, module, calldata, compile, config, evm_api, evm_data, ink, @@ -122,7 +129,7 @@ pub fn call_user_wasm(env: WasmEnvMut, sp: u32) -> MaybeEscape { /// pub fn read_rust_vec_len(env: WasmEnvMut, sp: u32) { let mut sp = GoStack::simple(sp, &env); - let vec: &Vec = unsafe { &*sp.read_ptr() }; + let vec: &Vec = unsafe { sp.read_ref() }; sp.write_u32(vec.len() as u32); } @@ -144,20 +151,6 @@ pub fn rust_vec_into_slice(env: WasmEnvMut, sp: u32) { mem::drop(vec) } -/// Drops module bytes. Note that in user-host this would be a `Machine`. -/// -/// # Go side -/// -/// The Go compiler expects the call to take the form -/// λ(module *Vec) -/// -pub fn drop_machine(env: WasmEnvMut, sp: u32) { - let mut sp = GoStack::simple(sp, &env); - if let Some(module) = sp.unbox_option::>() { - mem::drop(module); - } -} - /// Creates a `StylusConfig` from its component parts. /// /// # Go side diff --git a/arbitrator/jit/src/wavmio.rs b/arbitrator/jit/src/wavmio.rs index 85c71074b..3831d16c7 100644 --- a/arbitrator/jit/src/wavmio.rs +++ b/arbitrator/jit/src/wavmio.rs @@ -314,11 +314,9 @@ fn ready_hostio(env: &mut WasmEnv) -> MaybeEscape { let programs_count = socket::read_u32(stream)?; for _ in 0..programs_count { - let codehash = socket::read_bytes32(stream)?; - let wasm = socket::read_bytes(stream)?; - let hash = socket::read_bytes32(stream)?; - let version = socket::read_u16(stream)?; - env.user_wasms.insert((codehash, version), (wasm, hash)); + let module_hash = socket::read_bytes32(stream)?; + let module_asm = socket::read_boxed_slice(stream)?; + env.module_asms.insert(module_hash, module_asm.into()); } if socket::read_u8(stream)? != socket::READY { diff --git a/arbitrator/prover/src/binary.rs b/arbitrator/prover/src/binary.rs index 8880f8048..c9a3d6040 100644 --- a/arbitrator/prover/src/binary.rs +++ b/arbitrator/prover/src/binary.rs @@ -9,7 +9,7 @@ use crate::{ }, value::{ArbValueType, FunctionType, IntegerValType, Value}, }; -use arbutil::{Color, DebugColor}; +use arbutil::{math::SaturatingSum, Color, DebugColor}; use eyre::{bail, ensure, eyre, Result, WrapErr}; use fnv::{FnvHashMap as HashMap, FnvHashSet as HashSet}; use nom::{ @@ -80,22 +80,16 @@ pub enum FloatInstruction { impl FloatInstruction { pub fn signature(&self) -> FunctionType { match *self { - FloatInstruction::UnOp(t, _) => FunctionType::new(vec![t.into()], vec![t.into()]), - FloatInstruction::BinOp(t, _) => FunctionType::new(vec![t.into(); 2], vec![t.into()]), - FloatInstruction::RelOp(t, _) => { - FunctionType::new(vec![t.into(); 2], vec![ArbValueType::I32]) - } - FloatInstruction::TruncIntOp(i, f, ..) => { - FunctionType::new(vec![f.into()], vec![i.into()]) - } - FloatInstruction::ConvertIntOp(f, i, _) => { - FunctionType::new(vec![i.into()], vec![f.into()]) - } + FloatInstruction::UnOp(t, _) => FunctionType::new([t.into()], [t.into()]), + FloatInstruction::BinOp(t, _) => FunctionType::new([t.into(); 2], [t.into()]), + FloatInstruction::RelOp(t, _) => FunctionType::new([t.into(); 2], [ArbValueType::I32]), + FloatInstruction::TruncIntOp(i, f, ..) => FunctionType::new([f.into()], [i.into()]), + FloatInstruction::ConvertIntOp(f, i, _) => FunctionType::new([i.into()], [f.into()]), FloatInstruction::F32DemoteF64 => { - FunctionType::new(vec![ArbValueType::F64], vec![ArbValueType::F32]) + FunctionType::new([ArbValueType::F64], [ArbValueType::F32]) } FloatInstruction::F64PromoteF32 => { - FunctionType::new(vec![ArbValueType::F32], vec![ArbValueType::F64]) + FunctionType::new([ArbValueType::F32], [ArbValueType::F64]) } } } @@ -585,6 +579,10 @@ impl<'a> WasmBinary<'a> { // 4GB maximum implies `footprint` fits in a u16 let footprint = self.memory_info()?.min.0 as u16; + // check the entrypoint + let ty = FunctionType::new([ArbValueType::I32], [ArbValueType::I32]); + let user_main = self.check_func(STYLUS_ENTRY_POINT, ty)?; + let [ink_left, ink_status] = meter.globals(); let depth_left = depth.globals(); Ok(StylusData { @@ -592,6 +590,7 @@ impl<'a> WasmBinary<'a> { ink_status, depth_left, footprint, + user_main, }) } @@ -634,6 +633,9 @@ impl<'a> WasmBinary<'a> { limit!(4096, function.locals.len(), "locals") } + let table_entries = bin.tables.iter().map(|x| x.initial).saturating_sum(); + limit!(10_000, table_entries, "table entries"); + let max_len = 500; macro_rules! too_long { ($name:expr, $len:expr) => { @@ -654,27 +656,22 @@ impl<'a> WasmBinary<'a> { if bin.start.is_some() { bail!("wasm start functions not allowed"); } + Ok((bin, stylus_data, pages as u16)) + } - // check the entrypoint - let Some(&(entrypoint, kind)) = bin.exports.get(STYLUS_ENTRY_POINT) else { - bail!("missing export with name {}", STYLUS_ENTRY_POINT.red()); + /// Ensures a func exists and has the right type. + fn check_func(&self, name: &str, ty: FunctionType) -> Result { + let Some(&(func, kind)) = self.exports.get(name) else { + bail!("missing export with name {}", name.red()); }; if kind != ExportKind::Func { - bail!( - "export {} must be a function but is a {}", - STYLUS_ENTRY_POINT.red(), - kind.debug_red(), - ); + let kind = kind.debug_red(); + bail!("export {} must be a function but is a {kind}", name.red()); } - let entrypoint_ty = bin.get_function(FunctionIndex::new(entrypoint.try_into()?))?; - if entrypoint_ty != FunctionType::new(vec![ArbValueType::I32], vec![ArbValueType::I32]) { - bail!( - "wrong type for {}: {}", - STYLUS_ENTRY_POINT.red(), - entrypoint_ty.red(), - ); + let func_ty = self.get_function(FunctionIndex::new(func.try_into()?))?; + if func_ty != ty { + bail!("wrong type for {}: {}", name.red(), func_ty.red()); } - - Ok((bin, stylus_data, pages as u16)) + Ok(func) } } diff --git a/arbitrator/prover/src/host.rs b/arbitrator/prover/src/host.rs index d9b0c794a..c98ebdf65 100644 --- a/arbitrator/prover/src/host.rs +++ b/arbitrator/prover/src/host.rs @@ -1,15 +1,15 @@ // Copyright 2021-2023, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -#![allow(clippy::vec_init_then_push)] +#![allow(clippy::vec_init_then_push, clippy::redundant_closure)] use crate::{ binary, host, machine::{Function, InboxIdentifier}, programs::StylusData, utils, - value::{ArbValueType, FunctionType, IntegerValType}, - wavm::{wasm_to_wavm, IBinOpType, Instruction, Opcode}, + value::{ArbValueType, FunctionType}, + wavm::{wasm_to_wavm, Instruction, Opcode}, }; use arbutil::{evm::user::UserOutcomeKind, Color}; use eyre::{bail, ErrReport, Result}; @@ -31,6 +31,7 @@ pub enum InternalFunc { UserSetInk, UserStackLeft, UserSetStack, + CallMain, } impl InternalFunc { @@ -52,6 +53,7 @@ impl InternalFunc { UserSetInk => func!([I64, I32], []), // λ(ink_left, ink_status) UserStackLeft => func!([], [I32]), // λ() → stack_left UserSetStack => func!([I32], []), // λ(stack_left) + CallMain => func!([I32], [I32]), // λ(args_len) → status }; ty } @@ -169,23 +171,23 @@ impl Hostio { WavmReadInboxMessage => func!([I64, I32, I32], [I32]), WavmReadDelayedInboxMessage => func!([I64, I32, I32], [I32]), WavmHaltAndSetFinished => func!(), - WavmLinkModule => func!([I32], [I32]), // λ(module_hash) → module - WavmUnlinkModule => func!(), // λ() - ProgramInkLeft => func!([I32, I32], [I64]), // λ(module, internals) → ink_left - ProgramInkStatus => func!([I32, I32], [I32]), // λ(module, internals) → ink_status - ProgramSetInk => func!([I32, I32, I64]), // λ(module, internals, ink_left) - ProgramStackLeft => func!([I32, I32], [I32]), // λ(module, internals) → stack_left - ProgramSetStack => func!([I32, I32, I32]), // λ(module, internals, stack_left) - ProgramCallMain => func!([I32, I32, I32], [I32]), // λ(module, main, args_len) → status - ConsoleLogTxt => func!([I32, I32]), // λ(text, len) - ConsoleLogI32 => func!([I32]), // λ(value) - ConsoleLogI64 => func!([I64]), // λ(value) - ConsoleLogF32 => func!([F32]), // λ(value) - ConsoleLogF64 => func!([F64]), // λ(value) - ConsoleTeeI32 => func!([I32], [I32]), // λ(value) → value - ConsoleTeeI64 => func!([I64], [I64]), // λ(value) → value - ConsoleTeeF32 => func!([F32], [F32]), // λ(value) → value - ConsoleTeeF64 => func!([F64], [F64]), // λ(value) → value + WavmLinkModule => func!([I32], [I32]), // λ(module_hash) → module + WavmUnlinkModule => func!(), // λ() + ProgramInkLeft => func!([I32], [I64]), // λ(module) → ink_left + ProgramInkStatus => func!([I32], [I32]), // λ(module) → ink_status + ProgramSetInk => func!([I32, I64]), // λ(module, ink_left) + ProgramStackLeft => func!([I32], [I32]), // λ(module) → stack_left + ProgramSetStack => func!([I32, I32]), // λ(module, stack_left) + ProgramCallMain => func!([I32, I32], [I32]), // λ(module, args_len) → status + ConsoleLogTxt => func!([I32, I32]), // λ(text, len) + ConsoleLogI32 => func!([I32]), // λ(value) + ConsoleLogI64 => func!([I64]), // λ(value) + ConsoleLogF32 => func!([F32]), // λ(value) + ConsoleLogF64 => func!([F64]), // λ(value) + ConsoleTeeI32 => func!([I32], [I32]), // λ(value) → value + ConsoleTeeI64 => func!([I64], [I64]), // λ(value) → value + ConsoleTeeF32 => func!([F32], [F32]), // λ(value) → value + ConsoleTeeF64 => func!([F64], [F64]), // λ(value) → value UserInkLeft => InternalFunc::UserInkLeft.ty(), UserInkStatus => InternalFunc::UserInkStatus.ty(), UserSetInk => InternalFunc::UserSetInk.ty(), @@ -204,13 +206,10 @@ impl Hostio { body.push(Instruction::with_data($opcode, $value as u64)) }; } - macro_rules! dynamic { + macro_rules! cross_internal { ($func:ident) => { opcode!(LocalGet, 0); // module - opcode!(LocalGet, 1); // internals offset - opcode!(I32Const, InternalFunc::$func); // relative position of the func - opcode!(IBinOp(IntegerValType::I32, IBinOpType::Add)); // absolute position of the func - opcode!(CrossModuleDynamicCall); // consumes module and func + opcode!(CrossModuleInternalCall, InternalFunc::$func); // consumes module }; } macro_rules! intern { @@ -289,40 +288,38 @@ impl Hostio { opcode!(UnlinkModule); } ProgramInkLeft => { - // λ(module, internals) → ink_left - dynamic!(UserInkLeft); + // λ(module) → ink_left + cross_internal!(UserInkLeft); } ProgramInkStatus => { - // λ(module, internals) → ink_status - dynamic!(UserInkStatus); + // λ(module) → ink_status + cross_internal!(UserInkStatus); } ProgramSetInk => { - // λ(module, internals, ink_left) - opcode!(LocalGet, 2); // ink_left + // λ(module, ink_left) + opcode!(LocalGet, 1); // ink_left opcode!(I32Const, 0); // ink_status - dynamic!(UserSetInk); + cross_internal!(UserSetInk); } ProgramStackLeft => { - // λ(module, internals) → stack_left - dynamic!(UserStackLeft); + // λ(module) → stack_left + cross_internal!(UserStackLeft); } ProgramSetStack => { - // λ(module, internals, stack_left) - opcode!(LocalGet, 2); // stack_left - dynamic!(UserSetStack); + // λ(module, stack_left) + opcode!(LocalGet, 1); // stack_left + cross_internal!(UserSetStack); } ProgramCallMain => { - // λ(module, main, args_len) → status + // λ(module, args_len) → status opcode!(PushErrorGuard); opcode!(ArbitraryJumpIf, prior + body.len() + 3); opcode!(I32Const, UserOutcomeKind::Failure as u32); opcode!(Return); // jumps here in the happy case - opcode!(LocalGet, 2); // args_len - opcode!(LocalGet, 0); // module - opcode!(LocalGet, 1); // main - opcode!(CrossModuleDynamicCall); // consumes module and main, passing args_len + opcode!(LocalGet, 1); // args_len + cross_internal!(CallMain); opcode!(PopErrorGuard); } UserInkLeft => { @@ -363,7 +360,7 @@ pub fn get_impl(module: &str, name: &str) -> Result<(Function, bool)> { /// Adds internal functions to a module. /// Note: the order of the functions must match that of the `InternalFunc` enum -pub fn new_internal_funcs(globals: Option) -> Vec { +pub fn new_internal_funcs(stylus_data: Option) -> Vec { use ArbValueType::*; use InternalFunc::*; use Opcode::*; @@ -412,19 +409,17 @@ pub fn new_internal_funcs(globals: Option) -> Vec { let mut add_func = |code: &[_], internal| add_func(code_func(code, internal), internal); - if let Some(globals) = globals { - let (gas, status, depth) = globals.global_offsets(); - add_func(&[Instruction::with_data(GlobalGet, gas)], UserInkLeft); - add_func(&[Instruction::with_data(GlobalGet, status)], UserInkStatus); - add_func( - &[ - Instruction::with_data(GlobalSet, status), - Instruction::with_data(GlobalSet, gas), - ], - UserSetInk, - ); - add_func(&[Instruction::with_data(GlobalGet, depth)], UserStackLeft); - add_func(&[Instruction::with_data(GlobalSet, depth)], UserSetStack); + if let Some(info) = stylus_data { + let (gas, status, depth) = info.global_offsets(); + let main = info.user_main.into(); + let inst = |op, data| Instruction::with_data(op, data); + + add_func(&[inst(GlobalGet, gas)], UserInkLeft); + add_func(&[inst(GlobalGet, status)], UserInkStatus); + add_func(&[inst(GlobalSet, status), inst(GlobalSet, gas)], UserSetInk); + add_func(&[inst(GlobalGet, depth)], UserStackLeft); + add_func(&[inst(GlobalSet, depth)], UserSetStack); + add_func(&[inst(Call, main)], CallMain); } funcs } diff --git a/arbitrator/prover/src/lib.rs b/arbitrator/prover/src/lib.rs index e489c3cc0..3e5267b8b 100644 --- a/arbitrator/prover/src/lib.rs +++ b/arbitrator/prover/src/lib.rs @@ -21,9 +21,9 @@ mod test; pub use machine::Machine; use arbutil::Bytes32; -use eyre::Result; +use eyre::{Report, Result}; use machine::{ - argument_data_to_inbox, get_empty_preimage_resolver, GlobalState, MachineStatus, + argument_data_to_inbox, get_empty_preimage_resolver, GlobalState, MachineStatus, Module, PreimageResolver, }; use sha3::{Digest, Keccak256}; @@ -32,6 +32,7 @@ use std::{ ffi::CStr, os::raw::{c_char, c_int}, path::Path, + ptr, sync::{ atomic::{self, AtomicU8}, Arc, @@ -67,7 +68,7 @@ pub unsafe extern "C" fn arbitrator_load_machine( Ok(mach) => mach, Err(err) => { eprintln!("Error loading binary: {:?}", err); - std::ptr::null_mut() + ptr::null_mut() } } } @@ -111,7 +112,7 @@ pub unsafe extern "C" fn arbitrator_load_wavm_binary(binary_path: *const c_char) Ok(mach) => Box::into_raw(Box::new(mach)), Err(err) => { eprintln!("Error loading binary: {}", err); - std::ptr::null_mut() + ptr::null_mut() } } } @@ -120,6 +121,23 @@ unsafe fn cstr_to_string(c_str: *const c_char) -> String { CStr::from_ptr(c_str).to_string_lossy().into_owned() } +pub fn err_to_c_string(err: Report) -> *mut libc::c_char { + str_to_c_string(&format!("{err:?}")) +} + +/// Copies the str-data into a libc free-able C string +pub fn str_to_c_string(text: &str) -> *mut libc::c_char { + unsafe { + let buf = libc::malloc(text.len() + 1); // includes null-terminating byte + if buf.is_null() { + panic!("Failed to allocate memory for error string"); + } + ptr::copy_nonoverlapping(text.as_ptr(), buf as *mut u8, text.len()); + *(buf as *mut u8).add(text.len()) = 0; + buf as *mut libc::c_char + } +} + #[no_mangle] pub unsafe extern "C" fn arbitrator_free_machine(mach: *mut Machine) { drop(Box::from_raw(mach)); @@ -137,19 +155,6 @@ pub unsafe extern "C" fn atomic_u8_store(ptr: *mut u8, contents: u8) { (*(ptr as *mut AtomicU8)).store(contents, atomic::Ordering::Relaxed); } -pub fn err_to_c_string(err: eyre::Report) -> *mut libc::c_char { - let err = format!("{:?}", err); - unsafe { - let buf = libc::malloc(err.len() + 1); - if buf.is_null() { - panic!("Failed to allocate memory for error string"); - } - std::ptr::copy_nonoverlapping(err.as_ptr(), buf as *mut u8, err.len()); - *(buf.add(err.len()) as *mut u8) = 0; - buf as *mut libc::c_char - } -} - /// Runs the machine while the condition variable is zero. May return early if num_steps is hit. /// Returns a c string error (freeable with libc's free) on error, or nullptr on success. #[no_mangle] @@ -172,7 +177,7 @@ pub unsafe extern "C" fn arbitrator_step( } remaining_steps -= stepping; } - std::ptr::null_mut() + ptr::null_mut() } #[no_mangle] @@ -198,22 +203,13 @@ pub unsafe extern "C" fn arbitrator_add_inbox_message( #[no_mangle] pub unsafe extern "C" fn arbitrator_add_user_wasm( mach: *mut Machine, - wasm: *const u8, - wasm_len: u32, - version: u16, - debug: u32, - root: *const Bytes32, -) -> *mut libc::c_char { - let wasm = std::slice::from_raw_parts(wasm, wasm_len as usize); - - // provide the opportunity to skip calculating the module root - let root = (!root.is_null()).then(|| *root); - let debug = debug != 0; - - match (*mach).add_program(wasm, version, debug, root) { - Ok(_) => std::ptr::null_mut(), - Err(err) => err_to_c_string(err), - } + module: *const u8, + module_len: usize, + module_hash: *const Bytes32, +) { + let module = std::slice::from_raw_parts(module, module_len); + let module = Module::from_bytes(module); + (*mach).add_stylus_module(module, *module_hash); } /// Like arbitrator_step, but stops early if it hits a host io operation. @@ -228,10 +224,10 @@ pub unsafe extern "C" fn arbitrator_step_until_host_io( while condition.load(atomic::Ordering::Relaxed) == 0 { for _ in 0..1_000_000 { if mach.is_halted() { - return std::ptr::null_mut(); + return ptr::null_mut(); } if mach.next_instruction_is_host_io() { - return std::ptr::null_mut(); + return ptr::null_mut(); } match mach.step_n(1) { Ok(()) => {} @@ -239,7 +235,7 @@ pub unsafe extern "C" fn arbitrator_step_until_host_io( } } } - std::ptr::null_mut() + ptr::null_mut() } #[no_mangle] @@ -250,7 +246,7 @@ pub unsafe extern "C" fn arbitrator_serialize_state( let mach = &*mach; let res = CStr::from_ptr(path) .to_str() - .map_err(eyre::Report::from) + .map_err(Report::from) .and_then(|path| mach.serialize_state(path)); if let Err(err) = res { eprintln!("Failed to serialize machine state: {}", err); @@ -268,7 +264,7 @@ pub unsafe extern "C" fn arbitrator_deserialize_and_replace_state( let mach = &mut *mach; let res = CStr::from_ptr(path) .to_str() - .map_err(eyre::Report::from) + .map_err(Report::from) .and_then(|path| mach.deserialize_and_replace_state(path)); if let Err(err) = res { eprintln!("Failed to deserialize machine state: {}", err); diff --git a/arbitrator/prover/src/machine.rs b/arbitrator/prover/src/machine.rs index 7c99e27bc..396744074 100644 --- a/arbitrator/prover/src/machine.rs +++ b/arbitrator/prover/src/machine.rs @@ -8,11 +8,7 @@ use crate::{ host, memory::Memory, merkle::{Merkle, MerkleType}, - programs::{ - config::{CompileConfig, WasmPricingInfo}, - meter::MeteredMachine, - ModuleMod, StylusData, STYLUS_ENTRY_POINT, - }, + programs::{config::CompileConfig, meter::MeteredMachine, ModuleMod, StylusData}, reinterpret::{ReinterpretAsSigned, ReinterpretAsUnsigned}, utils::{file_bytes, CBytes, RemoteTableType}, value::{ArbValueType, FunctionType, IntegerValType, ProgramCounter, Value}, @@ -26,6 +22,7 @@ use digest::Digest; use eyre::{bail, ensure, eyre, Result, WrapErr}; use fnv::FnvHashMap as HashMap; use itertools::izip; +use lazy_static::lazy_static; use num::{traits::PrimInt, Zero}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -265,7 +262,7 @@ impl AvailableImport { } #[derive(Clone, Debug, Default, Serialize, Deserialize)] -struct Module { +pub struct Module { globals: Vec, memory: Memory, tables: Vec, @@ -288,6 +285,27 @@ struct Module { all_exports: Arc, } +lazy_static! { + static ref USER_IMPORTS: HashMap = { + let mut imports = HashMap::default(); + + let forward = include_bytes!("../../../target/machines/latest/forward_stub.wasm"); + let forward = binary::parse(forward, Path::new("forward")).unwrap(); + + for (name, &(export, kind)) in &forward.exports { + if kind == ExportKind::Func { + let ty = match forward.get_function(FunctionIndex::from_u32(export)) { + Ok(ty) => ty, + Err(error) => panic!("failed to read export {name}: {error:?}"), + }; + let import = AvailableImport::new(ty, 1, export); + imports.insert(name.to_owned(), import); + } + } + imports + }; +} + impl Module { const FORWARDING_PREFIX: &str = "arbitrator_forward__"; @@ -359,6 +377,14 @@ impl Module { } func_type_idxs.extend(bin.functions.iter()); + let func_exports: HashMap = bin + .exports + .iter() + .filter_map(|(name, (offset, kind))| { + (kind == &ExportKind::Func).then(|| (name.to_owned(), *offset)) + }) + .collect(); + let internals = host::new_internal_funcs(stylus_data); let internals_offset = (code.len() + bin.codes.len()) as u32; let internals_types = internals.iter().map(|f| f.ty.clone()); @@ -515,13 +541,6 @@ impl Module { ensure!(!code.is_empty(), "Module has no code"); let tables_hashes: Result<_, _> = tables.iter().map(Table::hash).collect(); - let func_exports = bin - .exports - .iter() - .filter_map(|(name, (offset, kind))| { - (kind == &ExportKind::Func).then(|| (name.to_owned(), *offset)) - }) - .collect(); Ok(Module { memory, @@ -544,6 +563,21 @@ impl Module { }) } + pub fn from_user_binary( + bin: &WasmBinary, + debug_funcs: bool, + stylus_data: Option, + ) -> Result { + Self::from_binary( + bin, + &USER_IMPORTS, + &HashMap::default(), + false, + debug_funcs, + stylus_data, + ) + } + fn name(&self) -> &str { &self.names.module } @@ -555,7 +589,7 @@ impl Module { Ok(*func.1) } - fn hash(&self) -> Bytes32 { + pub fn hash(&self) -> Bytes32 { let mut h = Keccak256::new(); h.update("Module:"); h.update( @@ -594,6 +628,14 @@ impl Module { data } + + pub fn into_bytes(&self) -> Box<[u8]> { + bincode::serialize(self).unwrap().into_boxed_slice() + } + + pub unsafe fn from_bytes(bytes: &[u8]) -> Self { + bincode::deserialize(bytes).unwrap() + } } // Globalstate holds: @@ -1080,77 +1122,22 @@ impl Machine { Ok(machine) } - /// Produces a compile-only `Machine` from a user program. - /// Note: the machine's imports are stubbed, so execution isn't possible. - pub fn new_user_stub( - wasm: &[u8], - page_limit: u16, - version: u16, - debug: bool, - ) -> Result<(Machine, WasmPricingInfo)> { - let compile = CompileConfig::version(version, debug); - let forward = include_bytes!("../../../target/machines/latest/forward_stub.wasm"); - let forward = binary::parse(forward, Path::new("forward")).unwrap(); - - let binary = WasmBinary::parse_user(wasm, page_limit, &compile); - let (bin, stylus_data, footprint) = match binary { - Ok(data) => data, - Err(err) => return Err(err.wrap_err("failed to parse program")), - }; - let info = WasmPricingInfo { - footprint, - size: wasm.len() as u32, - }; - let mach = Self::from_binaries( - &[forward], - bin, - false, - false, - false, - compile.debug.debug_funcs, - debug, - GlobalState::default(), - HashMap::default(), - Arc::new(|_, _| panic!("user program tried to read preimage")), - Some(stylus_data), - )?; - Ok((mach, info)) - } - /// Adds a user program to the machine's known set of wasms, compiling it into a link-able module. /// Note that the module produced will need to be configured before execution via hostio calls. - pub fn add_program( - &mut self, - wasm: &[u8], - version: u16, - debug_funcs: bool, - hash: Option, - ) -> Result { + pub fn add_program(&mut self, wasm: &[u8], version: u16, debug_funcs: bool) -> Result { let mut bin = binary::parse(wasm, Path::new("user"))?; let config = CompileConfig::version(version, debug_funcs); let stylus_data = bin.instrument(&config)?; - let forward = include_bytes!("../../../target/machines/latest/forward_stub.wasm"); - let forward = binary::parse(forward, Path::new("forward")).unwrap(); - - let mut machine = Self::from_binaries( - &[forward], - bin, - false, - false, - false, - debug_funcs, - self.debug_info, - GlobalState::default(), - HashMap::default(), - Arc::new(|_, _| panic!("tried to read preimage")), - Some(stylus_data), - )?; + let module = Module::from_user_binary(&bin, debug_funcs, Some(stylus_data))?; + let hash = module.hash(); + self.add_stylus_module(module, hash); + Ok(hash) + } - let module = machine.modules.pop().unwrap(); - let hash = hash.unwrap_or_else(|| module.hash()); + /// Adds a pre-built program to the machine's known set of wasms. + pub fn add_stylus_module(&mut self, module: Module, hash: Bytes32) { self.stylus_modules.insert(hash, module); - Ok(hash) } pub fn from_binaries( @@ -1284,7 +1271,7 @@ impl Machine { // Rust support let rust_fn = "__main_void"; if let Some(&f) = main_exports.get(rust_fn).filter(|_| runtime_support) { - let expected_type = FunctionType::new(vec![], vec![I32]); + let expected_type = FunctionType::new([], [I32]); ensure!( main_module.func_types[f as usize] == expected_type, "Main function doesn't match expected signature of [] -> [ret]", @@ -1594,12 +1581,6 @@ impl Machine { } } - pub fn program_info(&self) -> (u32, u32) { - let module = self.modules.last().unwrap(); - let main = module.find_func(STYLUS_ENTRY_POINT).unwrap(); - (main, module.internals_offset) - } - pub fn main_module_name(&self) -> String { self.modules.last().expect("no module").name().to_owned() } @@ -1943,15 +1924,16 @@ impl Machine { self.pc.inst = 0; reset_refs!(); } - Opcode::CrossModuleDynamicCall => { + Opcode::CrossModuleInternalCall => { flush_module!(); - let call_func = self.value_stack.pop().unwrap().assume_u32(); + let call_internal = inst.argument_data as u32; let call_module = self.value_stack.pop().unwrap().assume_u32(); self.value_stack.push(Value::InternalRef(self.pc)); self.value_stack.push(self.pc.module.into()); self.value_stack.push(module.internals_offset.into()); + module = &mut self.modules[call_module as usize]; self.pc.module = call_module; - self.pc.func = call_func; + self.pc.func = module.internals_offset + call_internal; self.pc.inst = 0; reset_refs!(); } @@ -2798,6 +2780,14 @@ impl Machine { .expect("Failed to prove elements merkle")); } } + CrossModuleInternalCall => { + let module_idx = self.value_stack.last().unwrap().assume_u32() as usize; + let called_module = &self.modules[module_idx]; + out!(called_module.serialize_for_proof(&called_module.memory.merkelize())); + out!(mod_merkle + .prove(module_idx) + .expect("Failed to prove module for CrossModuleInternalCall")); + } GetGlobalStateBytes32 | SetGlobalStateBytes32 => { out!(self.global_state.serialize()); let ptr = self.value_stack.last().unwrap().assume_u32(); diff --git a/arbitrator/prover/src/main.rs b/arbitrator/prover/src/main.rs index 50831cd02..22476f16d 100644 --- a/arbitrator/prover/src/main.rs +++ b/arbitrator/prover/src/main.rs @@ -4,7 +4,7 @@ #![cfg(feature = "native")] use arbutil::{format, Bytes32, Color, DebugColor}; -use eyre::{Context, Result}; +use eyre::{eyre, Context, Result}; use fnv::{FnvHashMap as HashMap, FnvHashSet as HashSet}; use prover::{ machine::{GlobalState, InboxIdentifier, Machine, MachineStatus, PreimageResolver, ProofInfo}, @@ -201,16 +201,10 @@ fn main() -> Result<()> { preimage_resolver, )?; - for module in &opts.stylus_modules { - let error = || { - format!( - "failed to read module at {}", - module.to_string_lossy().red() - ) - }; - let wasm = file_bytes(module).wrap_err_with(error)?; - mach.add_program(&wasm, 1, true, None) - .wrap_err_with(error)?; + for path in &opts.stylus_modules { + let err = || eyre!("failed to read module at {}", path.to_string_lossy().red()); + let wasm = file_bytes(path).wrap_err_with(err)?; + mach.add_program(&wasm, 1, true).wrap_err_with(err)?; } if let Some(output_path) = opts.generate_binaries { diff --git a/arbitrator/prover/src/programs/config.rs b/arbitrator/prover/src/programs/config.rs index bdb41291f..4d4f331ef 100644 --- a/arbitrator/prover/src/programs/config.rs +++ b/arbitrator/prover/src/programs/config.rs @@ -208,10 +208,3 @@ impl CompileConfig { Store::new(compiler) } } - -/// Information about a wasm for pricing purposes. -#[repr(C)] -pub struct WasmPricingInfo { - pub footprint: u16, - pub size: u32, -} diff --git a/arbitrator/prover/src/programs/mod.rs b/arbitrator/prover/src/programs/mod.rs index a519714d9..423cb0974 100644 --- a/arbitrator/prover/src/programs/mod.rs +++ b/arbitrator/prover/src/programs/mod.rs @@ -3,11 +3,13 @@ use crate::{ binary::{ExportKind, WasmBinary}, + machine::Module, memory::MemoryType, + programs::config::CompileConfig, value::{FunctionType as ArbFunctionType, Value}, }; use arbutil::Color; -use eyre::{bail, eyre, Report, Result}; +use eyre::{bail, eyre, Report, Result, WrapErr}; use fnv::FnvHashMap as HashMap; use std::fmt::Debug; use wasmer_types::{ @@ -367,6 +369,7 @@ pub struct StylusData { pub ink_status: GlobalIndex, pub depth_left: GlobalIndex, pub footprint: u16, + pub user_main: u32, } impl StylusData { @@ -378,3 +381,31 @@ impl StylusData { ) } } + +impl Module { + pub fn activate( + wasm: &[u8], + version: u16, + page_limit: u16, + debug: bool, + gas: &mut u64, + ) -> Result<(Self, u16)> { + // paid for by the 3 million gas charge in program.go + let compile = CompileConfig::version(version, debug); + let (bin, stylus_data, footprint) = + WasmBinary::parse_user(wasm, page_limit, &compile).wrap_err("failed to parse wasm")?; + + // naively charge 11 million gas to do the rest. + // in the future we'll implement a proper compilation pricing mechanism. + if *gas < 11_000_000 { + *gas = 0; + bail!("out of gas"); + } + *gas -= 11_000_000; + + let module = Self::from_user_binary(&bin, compile.debug.debug_funcs, Some(stylus_data)) + .wrap_err("failed to build user module")?; + + Ok((module, footprint)) + } +} diff --git a/arbitrator/prover/src/programs/prelude.rs b/arbitrator/prover/src/programs/prelude.rs index edf782beb..4db8e0341 100644 --- a/arbitrator/prover/src/programs/prelude.rs +++ b/arbitrator/prover/src/programs/prelude.rs @@ -2,7 +2,7 @@ // For license information, see https://github.com/nitro/blob/master/LICENSE pub use super::{ - config::{CompileConfig, StylusConfig, WasmPricingInfo}, + config::{CompileConfig, StylusConfig}, counter::CountingMachine, depth::DepthCheckedMachine, meter::{GasMeteredMachine, MachineMeter, MeteredMachine}, diff --git a/arbitrator/prover/src/value.rs b/arbitrator/prover/src/value.rs index e7bd77fc3..6bd688604 100644 --- a/arbitrator/prover/src/value.rs +++ b/arbitrator/prover/src/value.rs @@ -409,8 +409,15 @@ pub struct FunctionType { } impl FunctionType { - pub fn new(inputs: Vec, outputs: Vec) -> FunctionType { - FunctionType { inputs, outputs } + pub fn new(inputs: T, outputs: U) -> FunctionType + where + T: Into>, + U: Into>, + { + FunctionType { + inputs: inputs.into(), + outputs: outputs.into(), + } } pub fn hash(&self) -> Bytes32 { diff --git a/arbitrator/prover/src/wavm.rs b/arbitrator/prover/src/wavm.rs index e4b732485..f7f9434b9 100644 --- a/arbitrator/prover/src/wavm.rs +++ b/arbitrator/prover/src/wavm.rs @@ -146,7 +146,7 @@ pub enum Opcode { /// Call a function in a different module, acting as the caller's module CrossModuleForward, /// Call a function in a different module provided via the stack - CrossModuleDynamicCall, + CrossModuleInternalCall, /// Call a caller module's internal method with a given function offset CallerModuleInternalCall, /// Gets bytes32 from global state @@ -274,7 +274,7 @@ impl Opcode { Opcode::Dup => 0x8008, Opcode::CrossModuleCall => 0x8009, Opcode::CrossModuleForward => 0x800B, - Opcode::CrossModuleDynamicCall => 0x800C, + Opcode::CrossModuleInternalCall => 0x800C, Opcode::CallerModuleInternalCall => 0x800A, Opcode::GetGlobalStateBytes32 => 0x8010, Opcode::SetGlobalStateBytes32 => 0x8011, diff --git a/arbitrator/prover/test-cases/block.wat b/arbitrator/prover/test-cases/block.wat index 5ce55f829..2ea3d087d 100644 --- a/arbitrator/prover/test-cases/block.wat +++ b/arbitrator/prover/test-cases/block.wat @@ -66,5 +66,9 @@ (br_if 0) ) +(func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) +) + (start 0) (memory (export "memory") 0 0) diff --git a/arbitrator/prover/test-cases/bulk-memory.wat b/arbitrator/prover/test-cases/bulk-memory.wat index ab2aac3d8..3ae072853 100644 --- a/arbitrator/prover/test-cases/bulk-memory.wat +++ b/arbitrator/prover/test-cases/bulk-memory.wat @@ -79,5 +79,10 @@ local.get 1 i32.ne (if (then (unreachable)))) + + (func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) + ) + (start $start) (memory (export "memory") 1 1)) diff --git a/arbitrator/prover/test-cases/call-indirect.wat b/arbitrator/prover/test-cases/call-indirect.wat index c51a8661a..1f6bee6d3 100644 --- a/arbitrator/prover/test-cases/call-indirect.wat +++ b/arbitrator/prover/test-cases/call-indirect.wat @@ -26,5 +26,9 @@ (i32.mul) ) +(func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) +) + (start 0) (memory (export "memory") 0 0) diff --git a/arbitrator/prover/test-cases/call.wat b/arbitrator/prover/test-cases/call.wat index a6513b066..f56849ab7 100644 --- a/arbitrator/prover/test-cases/call.wat +++ b/arbitrator/prover/test-cases/call.wat @@ -16,5 +16,9 @@ (call 1) ) +(func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) +) + (start 0) (memory (export "memory") 0 0) diff --git a/arbitrator/prover/test-cases/const.wat b/arbitrator/prover/test-cases/const.wat index 981956cfd..4f3ffbd8d 100644 --- a/arbitrator/prover/test-cases/const.wat +++ b/arbitrator/prover/test-cases/const.wat @@ -8,5 +8,9 @@ (drop) ) +(func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) +) + (start 0) (memory (export "memory") 0 0) diff --git a/arbitrator/prover/test-cases/div-overflow.wat b/arbitrator/prover/test-cases/div-overflow.wat index 14b533113..a76493e74 100644 --- a/arbitrator/prover/test-cases/div-overflow.wat +++ b/arbitrator/prover/test-cases/div-overflow.wat @@ -9,5 +9,9 @@ (drop) ) +(func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) +) + (start 0) (memory (export "memory") 0 0) diff --git a/arbitrator/prover/test-cases/dynamic.wat b/arbitrator/prover/test-cases/dynamic.wat index 8ab2ca747..1f9c8d029 100644 --- a/arbitrator/prover/test-cases/dynamic.wat +++ b/arbitrator/prover/test-cases/dynamic.wat @@ -1,32 +1,29 @@ (module - (import "hostio" "wavm_link_module" (func $link (param i32) (result i32))) - (import "hostio" "wavm_unlink_module" (func $unlink )) - (import "hostio" "program_set_ink" (func $set_ink (param i32 i32 i64) )) - (import "hostio" "program_ink_left" (func $ink_left (param i32 i32) (result i64))) - (import "hostio" "program_ink_status" (func $ink_status (param i32 i32) (result i32))) - (import "hostio" "program_call_main" (func $user_func (param i32 i32 i32) (result i32))) + (import "hostio" "wavm_link_module" (func $link (param i32) (result i32))) + (import "hostio" "wavm_unlink_module" (func $unlink )) + (import "hostio" "program_set_ink" (func $set_ink (param i32 i64) )) + (import "hostio" "program_ink_left" (func $ink_left (param i32) (result i64))) + (import "hostio" "program_ink_status" (func $ink_status (param i32) (result i32))) + (import "hostio" "program_call_main" (func $user_func (param i32 i32) (result i32))) + + ;; WAVM Module hash (data (i32.const 0x0) - "\10\a4\b0\c7\91\26\6b\fb\f7\92\f5\e5\67\e0\03\d7\ee\7f\cf\7e\0a\52\6e\b3\92\46\c3\94\6f\21\b8\f8") ;; user + "\97\0c\df\6a\a9\bf\d4\3c\03\80\7f\8a\7e\67\9a\5c\12\05\94\4f\c6\5e\39\9e\00\df\5c\b3\7d\de\55\ad") ;; user + (func $start (local $user i32) (local $internals i32) ;; link in user.wat i32.const 0 call $link local.set $user - ;; set internals offset - i32.const 3 - local.set $internals - ;; set gas globals local.get $user - local.get $internals i64.const 1024 call $set_ink ;; get gas local.get $user - local.get $internals call $ink_left i64.const 1024 i64.ne @@ -35,7 +32,6 @@ ;; get gas status local.get $user - local.get $internals call $ink_status i32.const 0 i32.ne @@ -44,18 +40,16 @@ ;; call a successful func in user.wat ($safe) local.get $user - i32.const 0 ;; $safe - i32.const 64 + i32.const 1 ;; $safe call $user_func - i32.const 64 + i32.const 5 i32.ne (if (then (unreachable))) ;; recover from an unreachable local.get $user - i32.const 1 ;; $unreachable - i32.const 0 + i32.const 2 ;; $unreachable call $user_func i32.const 1 ;; indicates failure i32.ne @@ -69,8 +63,7 @@ ;; recover from an out-of-bounds memory access local.get $user - i32.const 2 ;; $out_of_bounds - i32.const 0 + i32.const 3 ;; $out_of_bounds call $user_func i32.const 1 ;; indicates failure i32.ne diff --git a/arbitrator/prover/test-cases/globals.wat b/arbitrator/prover/test-cases/globals.wat index 9335bcca9..451b83a01 100644 --- a/arbitrator/prover/test-cases/globals.wat +++ b/arbitrator/prover/test-cases/globals.wat @@ -18,5 +18,9 @@ (drop) ) +(func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) +) + (start 0) (memory (export "memory") 0 0) diff --git a/arbitrator/prover/test-cases/if-else.wat b/arbitrator/prover/test-cases/if-else.wat index 289ea3ef1..6a2d3a5bc 100644 --- a/arbitrator/prover/test-cases/if-else.wat +++ b/arbitrator/prover/test-cases/if-else.wat @@ -18,5 +18,9 @@ (drop) ) +(func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) +) + (start 0) (memory (export "memory") 0 0) diff --git a/arbitrator/prover/test-cases/iops.wat b/arbitrator/prover/test-cases/iops.wat index a004f9d09..906ae4362 100644 --- a/arbitrator/prover/test-cases/iops.wat +++ b/arbitrator/prover/test-cases/iops.wat @@ -80,5 +80,9 @@ (drop) ) +(func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) +) + (start 0) (memory (export "memory") 0 0) diff --git a/arbitrator/prover/test-cases/link.wat b/arbitrator/prover/test-cases/link.wat index dda3c8a71..f3f24fb28 100644 --- a/arbitrator/prover/test-cases/link.wat +++ b/arbitrator/prover/test-cases/link.wat @@ -4,32 +4,35 @@ (module (import "hostio" "wavm_link_module" (func $link (param i32) (result i32))) (import "hostio" "wavm_unlink_module" (func $unlink (param) (result))) + + ;; WAVM module hashes (data (i32.const 0x000) - "\a6\44\c7\fc\d3\a2\7b\00\60\f2\7c\32\47\2c\3b\0f\c0\88\94\8c\5b\9f\b1\9c\17\11\9d\70\04\6e\9e\25") ;; block + "\74\22\43\ad\22\2e\e5\6d\f4\bb\3f\0b\09\76\0a\bf\51\b7\17\a4\c5\50\c9\5b\45\be\ea\ed\4c\57\4d\17") ;; block (data (i32.const 0x020) - "\4f\0f\fa\e9\f1\a2\5b\72\85\9d\c8\23\aa\ed\42\18\54\ed\b1\14\9f\08\97\26\fc\e2\ff\ad\ca\2b\96\bc") ;; call + "\53\36\71\e6\bf\90\0f\50\fd\18\5f\44\d6\18\77\2f\70\17\19\2a\1a\8d\b6\92\5a\3c\14\1a\af\86\81\d4") ;; call (data (i32.const 0x040) - "\71\4b\0c\ab\49\45\e7\e1\e5\34\83\c7\33\0f\36\6a\29\42\45\a5\91\e0\91\7a\f7\0a\ae\f2\fe\2a\72\b4") ;; indirect + "\57\27\40\77\40\da\77\f8\1f\fd\81\cb\00\e0\02\17\40\f0\be\e4\11\89\0a\56\ba\80\e4\b9\31\74\13\a2") ;; indirect (data (i32.const 0x060) - "\fc\ef\2f\e4\98\5c\63\b5\4d\f2\39\86\98\91\c6\70\93\18\d6\22\45\7a\f4\be\fb\ac\34\19\8f\9a\69\3b") ;; const + "\3f\c3\a1\eb\a6\62\70\2b\3b\fa\dc\5b\29\22\11\6f\58\4a\6e\e5\70\60\6f\cf\6c\66\d8\c9\77\c5\c9\23") ;; const (data (i32.const 0x080) - "\ce\85\04\55\06\33\44\e6\30\3b\14\33\b3\8e\c5\41\ac\bf\96\60\cb\45\47\97\8c\b6\99\6e\ef\76\d1\36") ;; div + "\83\46\03\41\b4\5f\a6\e6\a3\0d\e9\fc\79\fc\3c\d6\c9\c3\c7\ac\97\42\bc\48\54\92\e6\84\08\37\07\a6") ;; div (data (i32.const 0x0a0) - "\01\05\9b\42\54\f2\80\00\0e\2c\41\ed\79\e3\f5\69\d1\28\e6\d3\4e\f5\20\b9\4d\ee\31\5e\78\a4\6b\3e") ;; globals + "\16\90\98\f2\7f\8d\bf\73\90\b9\eb\94\9f\b9\41\cd\c3\93\2e\30\b8\12\1b\d5\87\98\18\26\f2\62\7d\2c") ;; globals (data (i32.const 0x0c0) - "\e7\ac\97\8c\df\27\ca\1d\50\30\4d\b4\0c\1f\23\1a\76\bb\eb\5e\2a\2e\5b\e5\4d\24\a4\cc\9d\91\eb\93") ;; if-else + "\f5\6b\4c\c7\19\da\61\01\e4\e4\9a\f1\04\ca\29\97\fd\07\05\d6\c2\3b\e6\55\70\c5\54\65\a0\3f\3d\ee") ;; if-else (data (i32.const 0x0e0) - "\f3\3e\62\9a\ee\08\b3\4e\cd\15\a0\38\dc\cc\80\71\b0\31\35\16\fb\4e\77\34\c6\4d\77\54\85\38\7f\35") ;; locals + "\42\1d\62\e9\9a\51\d4\71\ce\50\6e\b4\83\72\18\ea\f8\ab\ab\b9\29\b8\bd\6d\66\ea\52\b3\3d\50\26\34") ;; locals (data (i32.const 0x100) - "\1d\c4\11\d8\36\83\4a\04\c0\7b\e0\46\a7\8d\4e\91\0b\13\f2\d5\1a\9e\fe\ed\9d\e6\2f\ee\54\6f\94\95") ;; loop + "\6d\c0\9f\17\5f\5b\e8\73\64\bc\79\62\e8\13\fd\cb\09\2a\12\24\87\4a\af\15\f2\e1\2e\93\b0\95\30\9a") ;; loop (data (i32.const 0x120) - "\8a\f6\10\f0\c6\a1\91\55\0a\72\1e\4d\36\91\88\6b\18\f5\42\73\9d\c5\9a\ea\1d\4d\b5\fb\bf\cf\06\f0") ;; math + "\a7\66\cb\0e\c4\31\ea\16\fd\c6\2f\d3\11\ca\4a\78\f8\48\6a\69\0a\4c\b9\1c\fc\47\f8\b6\63\6d\80\fa") ;; math (data (i32.const 0x140) - "\fc\27\e9\2e\12\23\f2\d6\ef\2a\83\3b\c8\1a\22\99\77\76\23\d8\f5\cf\51\f8\28\ba\a4\27\98\af\aa\24") ;; iops + "\ea\02\78\f7\a3\b3\e0\0e\55\f6\8f\13\87\d6\6f\04\38\b3\6b\4c\d5\33\e2\3d\0b\36\71\9f\57\f5\f0\59") ;; iops (data (i32.const 0x160) - "\10\a4\b0\c7\91\26\6b\fb\f7\92\f5\e5\67\e0\03\d7\ee\7f\cf\7e\0a\52\6e\b3\92\46\c3\94\6f\21\b8\f8") ;; user + "\97\0c\df\6a\a9\bf\d4\3c\03\80\7f\8a\7e\67\9a\5c\12\05\94\4f\c6\5e\39\9e\00\df\5c\b3\7d\de\55\ad") ;; user (data (i32.const 0x180) - "\f6\ad\69\79\fc\db\8a\af\27\48\ac\7c\54\5f\b2\a8\f2\80\f8\69\a6\75\59\a7\80\58\ba\26\39\5e\aa\c9") ;; return + "\c7\db\9f\8e\ed\13\ac\66\72\62\76\65\93\1b\9a\64\03\c3\c8\21\44\92\5c\8d\bc\1a\d6\bd\65\f8\2b\20") ;; return + (func $start (local $counter i32) ;; add modules diff --git a/arbitrator/prover/test-cases/locals.wat b/arbitrator/prover/test-cases/locals.wat index c07abca28..01b91937c 100644 --- a/arbitrator/prover/test-cases/locals.wat +++ b/arbitrator/prover/test-cases/locals.wat @@ -16,5 +16,9 @@ (drop) ) +(func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) +) + (start 0) (memory (export "memory") 0 0) diff --git a/arbitrator/prover/test-cases/loop.wat b/arbitrator/prover/test-cases/loop.wat index 552dc3536..4c32d6a5b 100644 --- a/arbitrator/prover/test-cases/loop.wat +++ b/arbitrator/prover/test-cases/loop.wat @@ -29,5 +29,9 @@ (unreachable) ) +(func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) +) + (start 0) (memory (export "memory") 0 0) diff --git a/arbitrator/prover/test-cases/math.wat b/arbitrator/prover/test-cases/math.wat index 5db7c3011..2d78dbeb5 100644 --- a/arbitrator/prover/test-cases/math.wat +++ b/arbitrator/prover/test-cases/math.wat @@ -81,5 +81,9 @@ (drop) ) +(func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) +) + (start 0) (memory (export "memory") 0 0) diff --git a/arbitrator/prover/test-cases/return.wat b/arbitrator/prover/test-cases/return.wat index 2baf2c0ac..278b1651f 100644 --- a/arbitrator/prover/test-cases/return.wat +++ b/arbitrator/prover/test-cases/return.wat @@ -20,5 +20,9 @@ ) ) +(func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) +) + (start 0) (memory (export "memory") 0 0) diff --git a/arbitrator/prover/test-cases/user.wat b/arbitrator/prover/test-cases/user.wat index 7bbc48ed8..410209bdb 100644 --- a/arbitrator/prover/test-cases/user.wat +++ b/arbitrator/prover/test-cases/user.wat @@ -2,13 +2,35 @@ ;; For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE (module - (func $safe (param $args_len i32) (result i32) - local.get $args_len) - (func $unreachable (param $args_len i32) (result i32) + (func $safe + i32.const 5 + drop) + (func $unreachable i32.const 0 i64.const 4 unreachable) - (func $out_of_bounds (param $args_len i32) (result i32) + (func $out_of_bounds i32.const 0xFFFFFF - i32.load) + i32.load + drop) + (func (export "user_entrypoint") (param $args_len i32) (result i32) + local.get $args_len + i32.const 1 + i32.eq + (if + (then (call $safe)) + ) + local.get $args_len + i32.const 2 + i32.eq + (if + (then (call $unreachable)) + ) + local.get $args_len + i32.const 3 + i32.eq + (if + (then (call $out_of_bounds)) + ) + i32.const 100) (memory (export "memory") 1 1)) diff --git a/arbitrator/stylus/Cargo.toml b/arbitrator/stylus/Cargo.toml index c91fc4106..b469288dc 100644 --- a/arbitrator/stylus/Cargo.toml +++ b/arbitrator/stylus/Cargo.toml @@ -15,6 +15,7 @@ wasmer-compiler-llvm = { path = "../tools/wasmer/lib/compiler-llvm", optional = derivative = "2.2.0" parking_lot = "0.12.1" thiserror = "1.0.33" +bincode = "1.3.3" libc = "0.2.108" eyre = "0.6.5" rand = "0.8.5" diff --git a/arbitrator/stylus/src/evm_api.rs b/arbitrator/stylus/src/evm_api.rs index 8cda407fc..3b1b261dc 100644 --- a/arbitrator/stylus/src/evm_api.rs +++ b/arbitrator/stylus/src/evm_api.rs @@ -1,7 +1,7 @@ // Copyright 2022-2023, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE -use crate::{RustSlice, RustVec}; +use crate::{RustBytes, RustSlice}; use arbutil::{ evm::{ api::{EvmApi, EvmApiStatus}, @@ -19,7 +19,7 @@ pub struct GoEvmApi { key: Bytes32, value: Bytes32, gas_cost: *mut u64, - error: *mut RustVec, + error: *mut RustBytes, ) -> EvmApiStatus, pub contract_call: unsafe extern "C" fn( id: usize, @@ -45,22 +45,23 @@ pub struct GoEvmApi { ) -> EvmApiStatus, pub create1: unsafe extern "C" fn( id: usize, - code: *mut RustVec, + code: *mut RustBytes, endowment: Bytes32, gas: *mut u64, return_data_len: *mut u32, ) -> EvmApiStatus, pub create2: unsafe extern "C" fn( id: usize, - code: *mut RustVec, + code: *mut RustBytes, endowment: Bytes32, salt: Bytes32, gas: *mut u64, return_data_len: *mut u32, ) -> EvmApiStatus, pub get_return_data: - unsafe extern "C" fn(id: usize, output: *mut RustVec, offset: u32, size: u32), - pub emit_log: unsafe extern "C" fn(id: usize, data: *mut RustVec, topics: u32) -> EvmApiStatus, + unsafe extern "C" fn(id: usize, output: *mut RustBytes, offset: u32, size: u32), + pub emit_log: + unsafe extern "C" fn(id: usize, data: *mut RustBytes, topics: u32) -> EvmApiStatus, pub account_balance: unsafe extern "C" fn(id: usize, address: Bytes20, gas_cost: *mut u64) -> Bytes32, // balance pub account_codehash: @@ -68,7 +69,7 @@ pub struct GoEvmApi { pub add_pages: unsafe extern "C" fn(id: usize, pages: u16) -> u64, // gas cost pub capture_hostio: unsafe extern "C" fn( id: usize, - name: *mut RustVec, + name: *mut RustBytes, args: *mut RustSlice, outs: *mut RustSlice, start_ink: u64, @@ -106,7 +107,7 @@ impl EvmApi for GoEvmApi { } fn set_bytes32(&mut self, key: Bytes32, value: Bytes32) -> Result { - let mut error = RustVec::new(vec![]); + let mut error = RustBytes::new(vec![]); let mut cost = 0; let api_status = call!(self, set_bytes32, key, value, ptr!(cost), ptr!(error)); let error = into_vec!(error); // done here to always drop @@ -183,7 +184,7 @@ impl EvmApi for GoEvmApi { ) -> (Result, u32, u64) { let mut call_gas = gas; // becomes the call's cost let mut return_data_len = 0; - let mut code = RustVec::new(code); + let mut code = RustBytes::new(code); let api_status = call!( self, create1, @@ -209,7 +210,7 @@ impl EvmApi for GoEvmApi { ) -> (Result, u32, u64) { let mut call_gas = gas; // becomes the call's cost let mut return_data_len = 0; - let mut code = RustVec::new(code); + let mut code = RustBytes::new(code); let api_status = call!( self, create2, @@ -228,13 +229,13 @@ impl EvmApi for GoEvmApi { } fn get_return_data(&mut self, offset: u32, size: u32) -> Vec { - let mut data = RustVec::new(vec![]); + let mut data = RustBytes::new(vec![]); call!(self, get_return_data, ptr!(data), offset, size); into_vec!(data) } fn emit_log(&mut self, data: Vec, topics: u32) -> Result<()> { - let mut data = RustVec::new(data); + let mut data = RustBytes::new(data); let api_status = call!(self, emit_log, ptr!(data), topics); let error = into_vec!(data); // done here to always drop match api_status { @@ -263,7 +264,7 @@ impl EvmApi for GoEvmApi { call!( self, capture_hostio, - ptr!(RustVec::new(name.as_bytes().to_vec())), + ptr!(RustBytes::new(name.as_bytes().to_vec())), ptr!(RustSlice::new(args)), ptr!(RustSlice::new(outs)), start_ink, diff --git a/arbitrator/stylus/src/lib.rs b/arbitrator/stylus/src/lib.rs index e91ecbeca..40aecdebd 100644 --- a/arbitrator/stylus/src/lib.rs +++ b/arbitrator/stylus/src/lib.rs @@ -8,10 +8,11 @@ use arbutil::{ EvmData, }, format::DebugBytes, + Bytes32, }; use eyre::ErrReport; use native::NativeInstance; -use prover::{programs::prelude::*, Machine}; +use prover::programs::prelude::*; use run::RunProgram; use std::{marker::PhantomData, mem}; @@ -59,13 +60,13 @@ impl<'a> RustSlice<'a> { } #[repr(C)] -pub struct RustVec { +pub struct RustBytes { ptr: *mut u8, len: usize, cap: usize, } -impl RustVec { +impl RustBytes { fn new(vec: Vec) -> Self { let mut rust_vec = Self { ptr: std::ptr::null_mut(), @@ -99,55 +100,45 @@ impl RustVec { } } -/// Ensures a user program can be proven. -/// On success, `wasm_info` is populated with pricing information. -/// On error, a message is written to `output`. +/// Instruments and "activates" a user wasm. /// -/// # Safety +/// The `output` is either the serialized asm & module pair or an error string. +/// Returns consensus info such as the module hash and footprint on success. /// -/// `output` and `wasm_info` must not be null. -#[no_mangle] -pub unsafe extern "C" fn stylus_parse_wasm( - wasm: GoSliceData, - page_limit: u16, - version: u16, - debug: bool, - wasm_info: *mut WasmPricingInfo, - output: *mut RustVec, -) -> UserOutcomeKind { - let wasm = wasm.slice(); - let info = &mut *wasm_info; - let output = &mut *output; - - match Machine::new_user_stub(wasm, page_limit, version, debug) { - Ok((_, data)) => *info = data, - Err(error) => return output.write_err(error), - } - UserOutcomeKind::Success -} - -/// Compiles a user program to its native representation. -/// The `output` is either the serialized module or an error string. +/// Note that this operation costs gas and is limited by the amount supplied via the `gas` pointer. +/// The amount left is written back at the end of the call. /// /// # Safety /// -/// Output must not be null. +/// `output`, `asm_len`, `module_hash`, `footprint`, and `gas` must not be null. #[no_mangle] -pub unsafe extern "C" fn stylus_compile( +pub unsafe extern "C" fn stylus_activate( wasm: GoSliceData, + page_limit: u16, version: u16, - debug_mode: bool, - output: *mut RustVec, + debug: bool, + output: *mut RustBytes, + asm_len: *mut usize, + module_hash: *mut Bytes32, + footprint: *mut u16, + gas: *mut u64, ) -> UserOutcomeKind { let wasm = wasm.slice(); let output = &mut *output; - let compile = CompileConfig::version(version, debug_mode); + let module_hash = &mut *module_hash; + let gas = &mut *gas; - let module = match native::module(wasm, compile) { - Ok(module) => module, + let (asm, module, pages) = match native::activate(wasm, version, page_limit, debug, gas) { + Ok(val) => val, Err(err) => return output.write_err(err), }; - output.write(module); + *asm_len = asm.len(); + *module_hash = module.hash(); + *footprint = pages; + + let mut data = asm; + data.extend(&*module.into_bytes()); + output.write(data); UserOutcomeKind::Success } @@ -155,7 +146,7 @@ pub unsafe extern "C" fn stylus_compile( /// /// # Safety /// -/// `module` must represent a valid module produced from `stylus_compile`. +/// `module` must represent a valid module produced from `stylus_activate`. /// `output` and `gas` must not be null. #[no_mangle] pub unsafe extern "C" fn stylus_call( @@ -165,7 +156,7 @@ pub unsafe extern "C" fn stylus_call( go_api: GoEvmApi, evm_data: EvmData, debug_chain: u32, - output: *mut RustVec, + output: *mut RustBytes, gas: *mut u64, ) -> UserOutcomeKind { let module = module.slice(); @@ -200,7 +191,7 @@ pub unsafe extern "C" fn stylus_call( /// /// Must only be called once per vec. #[no_mangle] -pub unsafe extern "C" fn stylus_drop_vec(vec: RustVec) { +pub unsafe extern "C" fn stylus_drop_vec(vec: RustBytes) { if !vec.ptr.is_null() { mem::drop(vec.into_vec()) } @@ -212,7 +203,7 @@ pub unsafe extern "C" fn stylus_drop_vec(vec: RustVec) { /// /// `rust` must not be null. #[no_mangle] -pub unsafe extern "C" fn stylus_vec_set_bytes(rust: *mut RustVec, data: GoSliceData) { +pub unsafe extern "C" fn stylus_vec_set_bytes(rust: *mut RustBytes, data: GoSliceData) { let rust = &mut *rust; let mut vec = Vec::from_raw_parts(rust.ptr, rust.len, rust.cap); vec.clear(); diff --git a/arbitrator/stylus/src/native.rs b/arbitrator/stylus/src/native.rs index d072de687..e0ac105b2 100644 --- a/arbitrator/stylus/src/native.rs +++ b/arbitrator/stylus/src/native.rs @@ -11,13 +11,16 @@ use arbutil::{ Color, }; use eyre::{bail, eyre, ErrReport, Result}; -use prover::programs::{ - config::PricingParams, - counter::{Counter, CountingMachine, OP_OFFSETS}, - depth::STYLUS_STACK_LEFT, - meter::{STYLUS_INK_LEFT, STYLUS_INK_STATUS}, - prelude::*, - start::STYLUS_START, +use prover::{ + machine::Module as ProverModule, + programs::{ + config::PricingParams, + counter::{Counter, CountingMachine, OP_OFFSETS}, + depth::STYLUS_STACK_LEFT, + meter::{STYLUS_INK_LEFT, STYLUS_INK_STATUS}, + prelude::*, + start::STYLUS_START, + }, }; use std::{ collections::BTreeMap, @@ -369,3 +372,17 @@ pub fn module(wasm: &[u8], compile: CompileConfig) -> Result> { let module = module.serialize()?; Ok(module.to_vec()) } + +pub fn activate( + wasm: &[u8], + version: u16, + page_limit: u16, + debug: bool, + gas: &mut u64, +) -> Result<(Vec, ProverModule, u16)> { + let compile = CompileConfig::version(version, debug); + let (module, footprint) = ProverModule::activate(wasm, version, page_limit, debug, gas)?; + + let asm = self::module(wasm, compile).expect("failed to generate stylus module"); + Ok((asm, module, footprint)) +} diff --git a/arbitrator/stylus/src/run.rs b/arbitrator/stylus/src/run.rs index f50ba161a..a9ff1836f 100644 --- a/arbitrator/stylus/src/run.rs +++ b/arbitrator/stylus/src/run.rs @@ -44,12 +44,12 @@ impl RunProgram for Machine { self.set_stack(config.max_depth); let status: u32 = call!("user", STYLUS_ENTRY_POINT, vec![args_len], |error| { - if self.ink_left() == MachineMeter::Exhausted { - return UserOutcome::OutOfInk; - } if self.stack_left() == 0 { return UserOutcome::OutOfStack; } + if self.ink_left() == MachineMeter::Exhausted { + return UserOutcome::OutOfInk; + } UserOutcome::Failure(error) }); diff --git a/arbitrator/stylus/tests/add.wat b/arbitrator/stylus/tests/add.wat index 5e7a61e2f..70aa71078 100644 --- a/arbitrator/stylus/tests/add.wat +++ b/arbitrator/stylus/tests/add.wat @@ -8,4 +8,7 @@ (func $add_one (export "add_one") (type $t0) (param $p0 i32) (result i32) get_local $p0 i32.const 1 - i32.add)) + i32.add) + (func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) + )) diff --git a/arbitrator/stylus/tests/bulk-memory-oob.wat b/arbitrator/stylus/tests/bulk-memory-oob.wat index 80ef17e59..dceba8ec2 100644 --- a/arbitrator/stylus/tests/bulk-memory-oob.wat +++ b/arbitrator/stylus/tests/bulk-memory-oob.wat @@ -10,5 +10,8 @@ (memory.copy (i32.const 0xfffe) (i32.const 0xffff) (i32.const 2))) (func (export "copy_same") (memory.copy (i32.const 0xffff) (i32.const 0xffff) (i32.const 2))) + (func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) + ) (data (i32.const 0xfffe) "\01\02") ;; last two bytes shouldn't change (memory (export "memory") 1 1)) diff --git a/arbitrator/stylus/tests/console.wat b/arbitrator/stylus/tests/console.wat index e3372e712..28162cf2c 100644 --- a/arbitrator/stylus/tests/console.wat +++ b/arbitrator/stylus/tests/console.wat @@ -31,4 +31,7 @@ f64.const -64.32 call $tee_f64 call $log_f64) + (func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) + ) (start $start)) diff --git a/arbitrator/stylus/tests/depth.wat b/arbitrator/stylus/tests/depth.wat index 3b258aab6..21ca44066 100644 --- a/arbitrator/stylus/tests/depth.wat +++ b/arbitrator/stylus/tests/depth.wat @@ -12,4 +12,7 @@ i32.const 1 ;; push 1 -- 3 on stack <- 3 words max i32.add ;; pop 2, push 1 -- 2 on stack global.set $depth ;; pop 1 -- 1 on stack - call $recurse)) + call $recurse) + (func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) + )) diff --git a/arbitrator/stylus/tests/start.wat b/arbitrator/stylus/tests/start.wat index 85b861b1f..9d74df4c1 100644 --- a/arbitrator/stylus/tests/start.wat +++ b/arbitrator/stylus/tests/start.wat @@ -12,4 +12,7 @@ i32.add set_global $status ;; increment the global ) + (func (export "user_entrypoint") (param $args_len i32) (result i32) + (i32.const 0) + ) (start $start)) diff --git a/arbitrator/tools/module_roots/Cargo.lock b/arbitrator/tools/module_roots/Cargo.lock index 4bfaa7db1..b4ccea9ca 100644 --- a/arbitrator/tools/module_roots/Cargo.lock +++ b/arbitrator/tools/module_roots/Cargo.lock @@ -44,6 +44,7 @@ dependencies = [ "digest 0.9.0", "eyre", "hex", + "num-traits", "serde", "sha3 0.10.6", "siphasher", @@ -894,9 +895,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] diff --git a/arbitrator/tools/module_roots/src/main.rs b/arbitrator/tools/module_roots/src/main.rs index 2cc52d614..2a42a0131 100644 --- a/arbitrator/tools/module_roots/src/main.rs +++ b/arbitrator/tools/module_roots/src/main.rs @@ -35,7 +35,7 @@ fn main() -> Result<()> { for module in &opts.stylus_modules { let error = || format!("failed to read module at {}", module.to_string_lossy()); let wasm = file_bytes(module).wrap_err_with(error)?; - let hash = mach.add_program(&wasm, 1, true, None).wrap_err_with(error)?; + let hash = mach.add_program(&wasm, 1, true).wrap_err_with(error)?; let name = module.file_stem().unwrap().to_string_lossy(); stylus.push((name.to_owned(), hash)); println!("{} {}", name, hash); diff --git a/arbitrator/wasm-libraries/Cargo.lock b/arbitrator/wasm-libraries/Cargo.lock index daaa28fee..cdb4bea75 100644 --- a/arbitrator/wasm-libraries/Cargo.lock +++ b/arbitrator/wasm-libraries/Cargo.lock @@ -29,6 +29,7 @@ dependencies = [ "digest 0.9.0", "eyre", "hex", + "num-traits", "serde", "sha3 0.10.6", "siphasher", @@ -593,9 +594,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] diff --git a/arbitrator/wasm-libraries/user-host/src/link.rs b/arbitrator/wasm-libraries/user-host/src/link.rs index ecafa0d37..fb3b65444 100644 --- a/arbitrator/wasm-libraries/user-host/src/link.rs +++ b/arbitrator/wasm-libraries/user-host/src/link.rs @@ -5,12 +5,12 @@ use crate::{evm_api::ApiCaller, Program, PROGRAMS}; use arbutil::{ evm::{js::JsEvmApi, user::UserOutcomeKind, EvmData}, format::DebugBytes, - heapify, wavm, + heapify, wavm, Bytes32, }; use go_abi::GoStack; use prover::{ + machine::Module, programs::config::{PricingParams, StylusConfig}, - Machine, }; use std::mem; @@ -24,30 +24,34 @@ extern "C" { // these dynamic hostio methods allow introspection into user modules #[link(wasm_import_module = "hostio")] extern "C" { - fn program_set_ink(module: u32, internals: u32, ink: u64); - fn program_set_stack(module: u32, internals: u32, stack: u32); - fn program_ink_left(module: u32, internals: u32) -> u64; - fn program_ink_status(module: u32, internals: u32) -> u32; - fn program_stack_left(module: u32, internals: u32) -> u32; - fn program_call_main(module: u32, main: u32, args_len: usize) -> u32; + fn program_set_ink(module: u32, ink: u64); + fn program_set_stack(module: u32, stack: u32); + fn program_ink_left(module: u32) -> u64; + fn program_ink_status(module: u32) -> u32; + fn program_stack_left(module: u32) -> u32; + fn program_call_main(module: u32, args_len: usize) -> u32; } #[repr(C, align(256))] struct MemoryLeaf([u8; 32]); -/// Compiles and instruments a user wasm. +/// Instruments and "activates" a user wasm, producing a unique module hash. +/// +/// Note that this operation costs gas and is limited by the amount supplied via the `gas` pointer. +/// The amount left is written back at the end of the call. /// /// # Safety /// +/// The `modHash` and `gas` pointers must not be null. +/// /// The Go compiler expects the call to take the form -/// λ(wasm []byte, pageLimit, version u16, debug u32) (mach *Machine, info WasmInfo, err *Vec) +/// λ(wasm []byte, pageLimit, version u16, debug u32, modHash *hash, gas *u64) (footprint u16, err *Vec) /// /// These values are placed on the stack as follows -/// stack: || wasm... || pageLimit | version | debug || mach ptr || info... || err ptr || -/// info: || footprint | 2 pad | size || +/// || wasm... || pageLimit | version | debug || modhash ptr || gas ptr || footprint | 6 pad || err ptr || /// #[no_mangle] -pub unsafe extern "C" fn go__github_com_offchainlabs_nitro_arbos_programs_compileUserWasmRustImpl( +pub unsafe extern "C" fn go__github_com_offchainlabs_nitro_arbos_programs_activateProgramRustImpl( sp: usize, ) { let mut sp = GoStack::new(sp); @@ -55,21 +59,29 @@ pub unsafe extern "C" fn go__github_com_offchainlabs_nitro_arbos_programs_compil let page_limit = sp.read_u16(); let version = sp.read_u16(); let debug = sp.read_bool32(); + let module_hash = sp.read_go_ptr(); + let gas = sp.read_go_ptr(); - match Machine::new_user_stub(&wasm, page_limit, version, debug) { - Ok((machine, info)) => { - let footprint = info.footprint; - let size = info.size; - sp.write_ptr(heapify(machine)); - sp.write_u16(footprint).skip_u16().write_u32(size); // wasm info - sp.write_nullptr(); - } - Err(error) => { - sp.write_nullptr(); - sp.skip_space(); // skip wasm info - sp.write_ptr(heapify(error.debug_bytes())); - } + macro_rules! error { + ($msg:expr, $error:expr) => {{ + let error = $error.wrap_err($msg).debug_bytes(); + wavm::caller_store64(gas, 0); + wavm::write_bytes32(module_hash, Bytes32::default()); + sp.skip_space(); + sp.write_ptr(heapify(error)); + return; + }}; } + + let gas_left = &mut wavm::caller_load64(gas); + let (module, pages) = match Module::activate(&wasm, version, page_limit, debug, gas_left) { + Ok(data) => data, + Err(error) => error!("failed to activate", error), + }; + wavm::caller_store64(gas, *gas_left); + wavm::write_bytes32(module_hash, module.hash()); + sp.write_u16(pages).skip_space(); + sp.write_nullptr(); } /// Links and executes a user wasm. @@ -77,40 +89,33 @@ pub unsafe extern "C" fn go__github_com_offchainlabs_nitro_arbos_programs_compil /// # Safety /// /// The Go compiler expects the call to take the form -/// λ( -/// mach *Machine, calldata []byte, params *Configs, evmApi []byte, evmData: *EvmData, -/// gas *u64, root *[32]byte -/// ) -> (status byte, out *Vec) +/// λ(moduleHash *[32]byte, calldata []byte, params *Configs, evmApi []byte, evmData: *EvmData, gas *u64) ( +/// status byte, out *Vec, +/// ) /// /// These values are placed on the stack as follows -/// || mach || calldata... || params || evmApi... || evmData || gas || root || status | 3 pad | out ptr || +/// || modHash || calldata... || params || evmApi... || evmData || gas || status | 7 pad | out ptr || /// #[no_mangle] -pub unsafe extern "C" fn go__github_com_offchainlabs_nitro_arbos_programs_callUserWasmRustImpl( +pub unsafe extern "C" fn go__github_com_offchainlabs_nitro_arbos_programs_callProgramRustImpl( sp: usize, ) { let mut sp = GoStack::new(sp); - let machine: Machine = sp.unbox(); + let compiled_hash = wavm::read_bytes32(sp.read_go_ptr()); let calldata = sp.read_go_slice_owned(); let config: StylusConfig = sp.unbox(); let evm_api = JsEvmApi::new(sp.read_go_slice_owned(), ApiCaller::new()); let evm_data: EvmData = sp.unbox(); + let gas = sp.read_go_ptr(); // buy ink let pricing = config.pricing; - let gas = sp.read_go_ptr(); let ink = pricing.gas_to_ink(wavm::caller_load64(gas)); - // compute the module root, or accept one from the caller - let root = sp.read_go_ptr(); - let root = (root != 0).then(|| wavm::read_bytes32(root)); - let module = root.unwrap_or_else(|| machine.main_module_hash()); - let (main, internals) = machine.program_info(); - // link the program and ready its instrumentation - let module = wavm_link_module(&MemoryLeaf(module.0)); - program_set_ink(module, internals, ink); - program_set_stack(module, internals, config.max_depth); + let module = wavm_link_module(&MemoryLeaf(*compiled_hash)); + program_set_ink(module, ink); + program_set_stack(module, config.max_depth); // provide arguments let args_len = calldata.len(); @@ -118,7 +123,7 @@ pub unsafe extern "C" fn go__github_com_offchainlabs_nitro_arbos_programs_callUs // call the program let go_stack = sp.save_stack(); - let status = program_call_main(module, main, args_len); + let status = program_call_main(module, args_len); let outs = PROGRAMS.pop().unwrap().into_outs(); sp.restore_stack(go_stack); @@ -138,15 +143,15 @@ pub unsafe extern "C" fn go__github_com_offchainlabs_nitro_arbos_programs_callUs // check if instrumentation stopped the program use UserOutcomeKind::*; - if program_ink_status(module, internals) != 0 { + if program_ink_status(module) != 0 { finish!(OutOfInk, 0); } - if program_stack_left(module, internals) == 0 { + if program_stack_left(module) == 0 { finish!(OutOfStack, 0); } // the program computed a final result - let ink_left = program_ink_left(module, internals); + let ink_left = program_ink_left(module); finish!(status, heapify(outs), ink_left) } @@ -190,23 +195,6 @@ pub unsafe extern "C" fn go__github_com_offchainlabs_nitro_arbos_programs_rustVe mem::drop(vec) } -/// Drops a `Machine`. -/// -/// # Safety -/// -/// The Go compiler expects the call to take the form -/// λ(mach *Machine) -/// -#[no_mangle] -pub unsafe extern "C" fn go__github_com_offchainlabs_nitro_arbos_programs_rustMachineDropImpl( - sp: usize, -) { - let mut sp = GoStack::new(sp); - if let Some(mach) = sp.unbox_option::() { - mem::drop(mach); - } -} - /// Creates a `StylusConfig` from its component parts. /// /// # Safety diff --git a/arbnode/execution/block_recorder.go b/arbnode/execution/block_recorder.go index deef5e815..468fb3bf9 100644 --- a/arbnode/execution/block_recorder.go +++ b/arbnode/execution/block_recorder.go @@ -12,11 +12,9 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" - "github.com/offchainlabs/nitro/arbcompress" "github.com/offchainlabs/nitro/arbos" "github.com/offchainlabs/nitro/arbos/arbosState" "github.com/offchainlabs/nitro/arbos/arbostypes" - "github.com/offchainlabs/nitro/arbos/programs" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/validator" ) @@ -166,16 +164,6 @@ func (r *BlockRecorder) RecordBlockCreation( return nil, err } - userWasms := recordingdb.UserWasms() - for _, wasm := range userWasms { - inflated, err := arbcompress.Decompress(wasm.CompressedWasm, programs.MaxWasmSize) - if err != nil { - return nil, fmt.Errorf("error decompressing program: %w", err) - } - wasm.CompressedWasm = nil // release the memory - wasm.Wasm = inflated - } - // check we got the canonical hash canonicalHash := r.execEngine.bc.GetCanonicalHash(uint64(blockNum)) if canonicalHash != blockHash { @@ -186,7 +174,7 @@ func (r *BlockRecorder) RecordBlockCreation( r.updateLastHdr(prevHeader) r.updateValidCandidateHdr(prevHeader) - return &RecordResult{pos, blockHash, preimages, userWasms, readBatchInfo}, err + return &RecordResult{pos, blockHash, preimages, recordingdb.UserWasms(), readBatchInfo}, err } func (r *BlockRecorder) updateLastHdr(hdr *types.Header) { diff --git a/arbos/burn/burn.go b/arbos/burn/burn.go index 973452cab..7d30ad12e 100644 --- a/arbos/burn/burn.go +++ b/arbos/burn/burn.go @@ -13,6 +13,7 @@ import ( type Burner interface { Burn(amount uint64) error Burned() uint64 + GasLeft() *uint64 // `SystemBurner`s panic (no notion of GasLeft) BurnOut() error Restrict(err error) HandleError(err error) error @@ -46,6 +47,10 @@ func (burner *SystemBurner) BurnOut() error { panic("called BurnOut on a system burner") } +func (burner *SystemBurner) GasLeft() *uint64 { + panic("called GasLeft on a system burner") +} + func (burner *SystemBurner) Restrict(err error) { if err != nil { glog.Error("Restrict() received an error", "err", err) diff --git a/arbos/programs/native.go b/arbos/programs/native.go index 3886883c5..f99ae8f4b 100644 --- a/arbos/programs/native.go +++ b/arbos/programs/native.go @@ -21,6 +21,7 @@ import "C" import ( "errors" "fmt" + "math" "math/big" "github.com/ethereum/go-ethereum/common" @@ -40,10 +41,10 @@ type usize = C.size_t type cbool = C._Bool type bytes20 = C.Bytes20 type bytes32 = C.Bytes32 -type rustVec = C.RustVec +type rustBytes = C.RustBytes type rustSlice = C.RustSlice -func compileUserWasm( +func activateProgram( db vm.StateDB, program common.Address, wasm []byte, @@ -51,53 +52,47 @@ func compileUserWasm( version uint16, debug bool, burner burn.Burner, -) (*wasmPricingInfo, error) { +) (common.Hash, uint16, error) { + output := &rustBytes{} + asmLen := usize(0) + moduleHash := &bytes32{} + footprint := uint16(math.MaxUint16) - // check that compilation would succeed during proving - rustInfo := &C.WasmPricingInfo{} - output := &rustVec{} - status := userStatus(C.stylus_parse_wasm( + status := userStatus(C.stylus_activate( goSlice(wasm), u16(page_limit), u16(version), cbool(debug), - rustInfo, output, + &asmLen, + moduleHash, + (*u16)(&footprint), + (*u64)(burner.GasLeft()), )) - if status != userSuccess { - _, msg, err := status.toResult(output.intoBytes(), debug) + + data, msg, err := status.toResult(output.intoBytes(), debug) + if err != nil { if debug { - log.Warn("stylus parse failed", "err", err, "msg", msg, "program", program) + log.Warn("activation failed", "err", err, "msg", msg, "program", program) } if errors.Is(err, vm.ErrExecutionReverted) { - return nil, fmt.Errorf("%w: %s", ErrProgramActivation, msg) + return common.Hash{}, footprint, fmt.Errorf("%w: %s", ErrProgramActivation, msg) } - return nil, err + return common.Hash{}, footprint, err } - info := rustInfo.decode() - if err := payForCompilation(burner, &info); err != nil { - return nil, err - } + hash := moduleHash.toHash() + split := int(asmLen) + asm := data[:split] + module := data[split:] - // compilation succeeds during proving, so failure should not be possible - status = userStatus(C.stylus_compile( - goSlice(wasm), - u16(version), - cbool(debug), - output, - )) - data, msg, err := status.toResult(output.intoBytes(), debug) - if err != nil { - log.Crit("compile failed", "err", err, "msg", msg, "program", program) - } - db.SetCompiledWasmCode(program, data, version) - return &info, err + db.ActivateWasm(hash, asm, module) + return hash, footprint, err } -func callUserWasm( +func callProgram( address common.Address, - program Program, + moduleHash common.Hash, scope *vm.ScopeContext, db vm.StateDB, interpreter *vm.EVMInterpreter, @@ -108,16 +103,16 @@ func callUserWasm( memoryModel *MemoryModel, ) ([]byte, error) { if db, ok := db.(*state.StateDB); ok { - db.RecordProgram(address, scope.Contract.CodeHash, stylusParams.version) + db.RecordProgram(moduleHash) } - module := db.GetCompiledWasmCode(address, stylusParams.version) + asm := db.GetActivatedAsm(moduleHash) evmApi, id := newApi(interpreter, tracingInfo, scope, memoryModel) defer dropApi(id) - output := &rustVec{} + output := &rustBytes{} status := userStatus(C.stylus_call( - goSlice(module), + goSlice(asm), goSlice(calldata), stylusParams.encode(), evmApi, @@ -149,7 +144,7 @@ func getBytes32Impl(api usize, key bytes32, cost *u64) bytes32 { } //export setBytes32Impl -func setBytes32Impl(api usize, key, value bytes32, cost *u64, errVec *rustVec) apiStatus { +func setBytes32Impl(api usize, key, value bytes32, cost *u64, errVec *rustBytes) apiStatus { closures := getApi(api) gas, err := closures.setBytes32(key.toHash(), value.toHash()) @@ -198,7 +193,7 @@ func staticCallImpl(api usize, contract bytes20, data *rustSlice, evmGas *u64, l } //export create1Impl -func create1Impl(api usize, code *rustVec, endowment bytes32, evmGas *u64, len *u32) apiStatus { +func create1Impl(api usize, code *rustBytes, endowment bytes32, evmGas *u64, len *u32) apiStatus { closures := getApi(api) addr, ret_len, cost, err := closures.create1(code.read(), endowment.toBig(), uint64(*evmGas)) *evmGas = u64(cost) // evmGas becomes the call's cost @@ -212,7 +207,7 @@ func create1Impl(api usize, code *rustVec, endowment bytes32, evmGas *u64, len * } //export create2Impl -func create2Impl(api usize, code *rustVec, endowment, salt bytes32, evmGas *u64, len *u32) apiStatus { +func create2Impl(api usize, code *rustBytes, endowment, salt bytes32, evmGas *u64, len *u32) apiStatus { closures := getApi(api) addr, ret_len, cost, err := closures.create2(code.read(), endowment.toBig(), salt.toBig(), uint64(*evmGas)) *evmGas = u64(cost) // evmGas becomes the call's cost @@ -226,14 +221,14 @@ func create2Impl(api usize, code *rustVec, endowment, salt bytes32, evmGas *u64, } //export getReturnDataImpl -func getReturnDataImpl(api usize, output *rustVec, offset u32, size u32) { +func getReturnDataImpl(api usize, output *rustBytes, offset u32, size u32) { closures := getApi(api) returnData := closures.getReturnData(uint32(offset), uint32(size)) output.setBytes(returnData) } //export emitLogImpl -func emitLogImpl(api usize, data *rustVec, topics u32) apiStatus { +func emitLogImpl(api usize, data *rustBytes, topics u32) apiStatus { closures := getApi(api) err := closures.emitLog(data.read(), uint32(topics)) if err != nil { @@ -312,25 +307,25 @@ func (slice *rustSlice) read() []byte { return arbutil.PointerToSlice((*byte)(slice.ptr), int(slice.len)) } -func (vec *rustVec) read() []byte { +func (vec *rustBytes) read() []byte { return arbutil.PointerToSlice((*byte)(vec.ptr), int(vec.len)) } -func (vec *rustVec) intoBytes() []byte { +func (vec *rustBytes) intoBytes() []byte { slice := vec.read() vec.drop() return slice } -func (vec *rustVec) drop() { +func (vec *rustBytes) drop() { C.stylus_drop_vec(*vec) } -func (vec *rustVec) setString(data string) { +func (vec *rustBytes) setString(data string) { vec.setBytes([]byte(data)) } -func (vec *rustVec) setBytes(data []byte) { +func (vec *rustBytes) setBytes(data []byte) { C.stylus_vec_set_bytes(vec, goSlice(data)) } @@ -370,10 +365,3 @@ func (data *evmData) encode() C.EvmData { tracing: cbool(data.tracing), } } - -func (info *C.WasmPricingInfo) decode() wasmPricingInfo { - return wasmPricingInfo{ - footprint: uint16(info.footprint), - size: uint32(info.size), - } -} diff --git a/arbos/programs/native_api.go b/arbos/programs/native_api.go index 6956d493b..121ff79ea 100644 --- a/arbos/programs/native_api.go +++ b/arbos/programs/native_api.go @@ -21,8 +21,8 @@ Bytes32 getBytes32Wrap(usize api, Bytes32 key, u64 * cost) { return getBytes32Impl(api, key, cost); } -EvmApiStatus setBytes32Impl(usize api, Bytes32 key, Bytes32 value, u64 * cost, RustVec * error); -EvmApiStatus setBytes32Wrap(usize api, Bytes32 key, Bytes32 value, u64 * cost, RustVec * error) { +EvmApiStatus setBytes32Impl(usize api, Bytes32 key, Bytes32 value, u64 * cost, RustBytes * error); +EvmApiStatus setBytes32Wrap(usize api, Bytes32 key, Bytes32 value, u64 * cost, RustBytes * error) { return setBytes32Impl(api, key, value, cost, error); } @@ -41,23 +41,23 @@ EvmApiStatus staticCallWrap(usize api, Bytes20 contract, RustSlice * calldata, u return staticCallImpl(api, contract, calldata, gas, len); } -EvmApiStatus create1Impl(usize api, RustVec * code, Bytes32 endowment, u64 * gas, u32 * len); -EvmApiStatus create1Wrap(usize api, RustVec * code, Bytes32 endowment, u64 * gas, u32 * len) { +EvmApiStatus create1Impl(usize api, RustBytes * code, Bytes32 endowment, u64 * gas, u32 * len); +EvmApiStatus create1Wrap(usize api, RustBytes * code, Bytes32 endowment, u64 * gas, u32 * len) { return create1Impl(api, code, endowment, gas, len); } -EvmApiStatus create2Impl(usize api, RustVec * code, Bytes32 endowment, Bytes32 salt, u64 * gas, u32 * len); -EvmApiStatus create2Wrap(usize api, RustVec * code, Bytes32 endowment, Bytes32 salt, u64 * gas, u32 * len) { +EvmApiStatus create2Impl(usize api, RustBytes * code, Bytes32 endowment, Bytes32 salt, u64 * gas, u32 * len); +EvmApiStatus create2Wrap(usize api, RustBytes * code, Bytes32 endowment, Bytes32 salt, u64 * gas, u32 * len) { return create2Impl(api, code, endowment, salt, gas, len); } -void getReturnDataImpl(usize api, RustVec * data, u32 offset, u32 size); -void getReturnDataWrap(usize api, RustVec * data, u32 offset, u32 size) { +void getReturnDataImpl(usize api, RustBytes * data, u32 offset, u32 size); +void getReturnDataWrap(usize api, RustBytes * data, u32 offset, u32 size) { return getReturnDataImpl(api, data, offset, size); } -EvmApiStatus emitLogImpl(usize api, RustVec * data, usize topics); -EvmApiStatus emitLogWrap(usize api, RustVec * data, usize topics) { +EvmApiStatus emitLogImpl(usize api, RustBytes * data, usize topics); +EvmApiStatus emitLogWrap(usize api, RustBytes * data, usize topics) { return emitLogImpl(api, data, topics); } diff --git a/arbos/programs/programs.go b/arbos/programs/programs.go index f17fa9c05..2bddb929f 100644 --- a/arbos/programs/programs.go +++ b/arbos/programs/programs.go @@ -13,7 +13,6 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/log" "github.com/offchainlabs/nitro/arbcompress" - "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/arbutil" @@ -23,6 +22,7 @@ import ( type Programs struct { backingStorage *storage.Storage programs *storage.Storage + moduleHashes *storage.Storage inkPrice storage.StorageBackedUint24 maxStackDepth storage.StorageBackedUint32 freePages storage.StorageBackedUint16 @@ -30,7 +30,7 @@ type Programs struct { pageRamp storage.StorageBackedUint64 pageLimit storage.StorageBackedUint16 callScalar storage.StorageBackedUint16 - version storage.StorageBackedUint16 + version storage.StorageBackedUint16 // Must only be changed during ArbOS upgrades } type Program struct { @@ -42,6 +42,7 @@ type Program struct { type uint24 = arbmath.Uint24 var programDataKey = []byte{0} +var moduleHashesKey = []byte{1} const ( versionOffset uint64 = iota @@ -91,6 +92,7 @@ func Open(sto *storage.Storage) *Programs { return &Programs{ backingStorage: sto, programs: sto.OpenSubStorage(programDataKey), + moduleHashes: sto.OpenSubStorage(moduleHashesKey), inkPrice: sto.OpenStorageBackedUint24(inkPriceOffset), maxStackDepth: sto.OpenStorageBackedUint32(maxStackDepthOffset), freePages: sto.OpenStorageBackedUint16(freePagesOffset), @@ -166,42 +168,43 @@ func (p Programs) SetCallScalar(scalar uint16) error { return p.callScalar.Set(scalar) } -func (p Programs) ActivateProgram(evm *vm.EVM, program common.Address, debugMode bool) (uint16, bool, error) { +func (p Programs) ActivateProgram(evm *vm.EVM, address common.Address, debugMode bool) ( + uint16, common.Hash, common.Hash, bool, error, +) { statedb := evm.StateDB - codeHash := statedb.GetCodeHash(program) + codeHash := statedb.GetCodeHash(address) + burner := p.programs.Burner() version, err := p.StylusVersion() if err != nil { - return 0, false, err + return 0, codeHash, common.Hash{}, false, err } latest, err := p.CodehashVersion(codeHash) if err != nil { - return 0, false, err + return 0, codeHash, common.Hash{}, false, err } // Already compiled and found in the machine versions mapping. if latest >= version { - return 0, false, ProgramUpToDateError() + return 0, codeHash, common.Hash{}, false, ProgramUpToDateError() } - wasm, err := getWasm(statedb, program) + wasm, err := getWasm(statedb, address) if err != nil { - return 0, false, err + return 0, codeHash, common.Hash{}, false, err } // require the program's footprint not exceed the remaining memory budget pageLimit, err := p.PageLimit() if err != nil { - return 0, false, err + return 0, codeHash, common.Hash{}, false, err } pageLimit = arbmath.SaturatingUSub(pageLimit, statedb.GetStylusPagesOpen()) - // charge 3 million up front to begin compilation - burner := p.programs.Burner() - if err := burner.Burn(3000000); err != nil { - return 0, false, err - } - info, err := compileUserWasm(statedb, program, wasm, pageLimit, version, debugMode, burner) + moduleHash, footprint, err := activateProgram(statedb, address, wasm, pageLimit, version, debugMode, burner) if err != nil { - return 0, true, err + return 0, codeHash, common.Hash{}, true, err + } + if err := p.moduleHashes.Set(codeHash, moduleHash); err != nil { + return 0, codeHash, common.Hash{}, true, err } // wasmSize is stored as half kb units, rounding up @@ -209,10 +212,10 @@ func (p Programs) ActivateProgram(evm *vm.EVM, program common.Address, debugMode programData := Program{ wasmSize: wasmSize, - footprint: info.footprint, + footprint: footprint, version: version, } - return version, false, p.programs.Set(codeHash, programData.serialize()) + return version, codeHash, moduleHash, false, p.setProgram(codeHash, programData) } func (p Programs) CallProgram( @@ -241,6 +244,11 @@ func (p Programs) CallProgram( return nil, ProgramOutOfDateError(program.version) } + moduleHash, err := p.moduleHashes.Get(contract.CodeHash) + if err != nil { + return nil, err + } + debugMode := interpreter.Evm().ChainConfig().DebugMode() params, err := p.goParams(program.version, debugMode) if err != nil { @@ -292,7 +300,10 @@ func (p Programs) CallProgram( if contract.CodeAddr != nil { address = *contract.CodeAddr } - return callUserWasm(address, program, scope, statedb, interpreter, tracingInfo, calldata, evmData, params, model) + return callProgram( + address, moduleHash, scope, statedb, interpreter, + tracingInfo, calldata, evmData, params, model, + ) } func getWasm(statedb vm.StateDB, program common.Address) ([]byte, error) { @@ -308,7 +319,6 @@ func getWasm(statedb vm.StateDB, program common.Address) ([]byte, error) { } func (p Programs) getProgram(contract *vm.Contract) (Program, error) { - return p.deserializeProgram(contract.CodeHash) } @@ -321,17 +331,20 @@ func (p Programs) deserializeProgram(codeHash common.Hash) (Program, error) { }, err } -func (p Program) serialize() common.Hash { +func (p Programs) setProgram(codehash common.Hash, program Program) error { data := common.Hash{} - copy(data[26:], arbmath.Uint16ToBytes(p.wasmSize)) - copy(data[28:], arbmath.Uint16ToBytes(p.footprint)) - copy(data[30:], arbmath.Uint16ToBytes(p.version)) - return data + copy(data[26:], arbmath.Uint16ToBytes(program.wasmSize)) + copy(data[28:], arbmath.Uint16ToBytes(program.footprint)) + copy(data[30:], arbmath.Uint16ToBytes(program.version)) + return p.programs.Set(codehash, data) } func (p Programs) CodehashVersion(codeHash common.Hash) (uint16, error) { program, err := p.deserializeProgram(codeHash) - return program.version, err + if err != nil { + return 0, err + } + return program.version, nil } func (p Programs) ProgramSize(codeHash common.Hash) (uint32, error) { @@ -417,15 +430,3 @@ func (status userStatus) toResult(data []byte, debug bool) ([]byte, string, erro return nil, msg, vm.ErrExecutionReverted } } - -type wasmPricingInfo struct { - footprint uint16 - size uint32 -} - -// Pay for compilation. Right now this is a fixed amount of gas. -// In the future, costs will be variable and based on the wasm. -// Note: memory expansion costs are baked into compilation charging. -func payForCompilation(burner burn.Burner, _info *wasmPricingInfo) error { - return burner.Burn(11000000) -} diff --git a/arbos/programs/raw.s b/arbos/programs/raw.s index d17ce906d..c0b3dc45e 100644 --- a/arbos/programs/raw.s +++ b/arbos/programs/raw.s @@ -6,11 +6,11 @@ #include "textflag.h" -TEXT ·compileUserWasmRustImpl(SB), NOSPLIT, $0 +TEXT ·activateProgramRustImpl(SB), NOSPLIT, $0 CallImport RET -TEXT ·callUserWasmRustImpl(SB), NOSPLIT, $0 +TEXT ·callProgramRustImpl(SB), NOSPLIT, $0 CallImport RET @@ -22,10 +22,6 @@ TEXT ·rustVecIntoSliceImpl(SB), NOSPLIT, $0 CallImport RET -TEXT ·rustMachineDropImpl(SB), NOSPLIT, $0 - CallImport - RET - TEXT ·rustConfigImpl(SB), NOSPLIT, $0 CallImport RET diff --git a/arbos/programs/wasm.go b/arbos/programs/wasm.go index dcadd4675..cba3f8e1b 100644 --- a/arbos/programs/wasm.go +++ b/arbos/programs/wasm.go @@ -7,11 +7,8 @@ package programs import ( - "math" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/log" "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/arbutil" @@ -31,21 +28,20 @@ type usize = uintptr // opaque types type rustVec byte type rustConfig byte -type rustMachine byte +type rustModule byte type rustEvmData byte -func compileUserWasmRustImpl( - wasm []byte, pageLimit, version u16, debugMode u32, -) (machine *rustMachine, info wasmPricingInfo, err *rustVec) +func activateProgramRustImpl( + wasm []byte, pageLimit, version u16, debugMode u32, moduleHash *hash, gas *u64, +) (footprint u16, err *rustVec) -func callUserWasmRustImpl( - machine *rustMachine, calldata []byte, params *rustConfig, evmApi []byte, - evmData *rustEvmData, gas *u64, root *hash, +func callProgramRustImpl( + moduleHash *hash, calldata []byte, params *rustConfig, evmApi []byte, evmData *rustEvmData, gas *u64, ) (status userStatus, out *rustVec) func readRustVecLenImpl(vec *rustVec) (len u32) func rustVecIntoSliceImpl(vec *rustVec, ptr *byte) -func rustMachineDropImpl(mach *rustMachine) +func rustModuleDropImpl(mach *rustModule) func rustConfigImpl(version u16, maxDepth, inkPrice, debugMode u32) *rustConfig func rustEvmDataImpl( blockBasefee *hash, @@ -62,7 +58,7 @@ func rustEvmDataImpl( reentrant u32, ) *rustEvmData -func compileUserWasm( +func activateProgram( db vm.StateDB, program addr, wasm []byte, @@ -70,23 +66,22 @@ func compileUserWasm( version u16, debug bool, burner burn.Burner, -) (*wasmPricingInfo, error) { +) (common.Hash, u16, error) { debugMode := arbmath.BoolToUint32(debug) - machine, info, err := compileUserWasmRustImpl(wasm, pageLimit, version, debugMode) - defer rustMachineDropImpl(machine) + moduleHash := common.Hash{} + gasPtr := burner.GasLeft() + + footprint, err := activateProgramRustImpl(wasm, pageLimit, version, debugMode, &moduleHash, gasPtr) if err != nil { _, _, err := userFailure.toResult(err.intoSlice(), debug) - return nil, err - } - if err := payForCompilation(burner, &info); err != nil { - return nil, err + return moduleHash, footprint, err } - return &info, nil + return moduleHash, footprint, nil } -func callUserWasm( +func callProgram( address common.Address, - program Program, + moduleHash common.Hash, scope *vm.ScopeContext, db vm.StateDB, interpreter *vm.EVMInterpreter, @@ -96,34 +91,17 @@ func callUserWasm( params *goParams, memoryModel *MemoryModel, ) ([]byte, error) { - // since the program has previously passed compilation, don't limit memory - pageLimit := uint16(math.MaxUint16) - debug := arbmath.UintToBool(params.debugMode) - - wasm, err := getWasm(db, address) - if err != nil { - log.Crit("failed to get wasm", "program", program, "err", err) - } - - // compile the machine (TODO: reuse these) - machine, _, errVec := compileUserWasmRustImpl(wasm, pageLimit, params.version, params.debugMode) - if err != nil { - _, _, err := userFailure.toResult(errVec.intoSlice(), debug) - return nil, err - } - - root := db.NoncanonicalProgramHash(scope.Contract.CodeHash, params.version) evmApi := newApi(interpreter, tracingInfo, scope, memoryModel) defer evmApi.drop() + debug := arbmath.UintToBool(params.debugMode) - status, output := callUserWasmRustImpl( - machine, + status, output := callProgramRustImpl( + &moduleHash, calldata, params.encode(), evmApi.funcs, evmData.encode(), &scope.Contract.Gas, - &root, ) data, _, err := status.toResult(output.intoSlice(), debug) return data, err diff --git a/contracts b/contracts index d5ce09372..70682f242 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit d5ce0937222aa51f67eda9b3b5f3a1cc833df2a1 +Subproject commit 70682f242380296c18359a2e4e2b994e5e099cac diff --git a/go-ethereum b/go-ethereum index b1ed2b401..4ae0e6497 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit b1ed2b40164eda7f2d79e33a24de9960734940b4 +Subproject commit 4ae0e649726e0079d142d1050331c068edd9acfd diff --git a/precompiles/ArbWasm.go b/precompiles/ArbWasm.go index aeb61d082..902985543 100644 --- a/precompiles/ArbWasm.go +++ b/precompiles/ArbWasm.go @@ -6,6 +6,8 @@ package precompiles type ArbWasm struct { Address addr // 0x71 + ProgramActivated func(ctx, mech, hash, hash, addr, uint16) error + ProgramActivatedGasCost func(hash, hash, addr, uint16) (uint64, error) ProgramNotActivatedError func() error ProgramOutOfDateError func(version uint16) error ProgramUpToDateError func() error @@ -13,12 +15,20 @@ type ArbWasm struct { // Compile a wasm program with the latest instrumentation func (con ArbWasm) ActivateProgram(c ctx, evm mech, program addr) (uint16, error) { - version, takeAllGas, err := c.State.Programs().ActivateProgram(evm, program, evm.ChainConfig().DebugMode()) + debug := evm.ChainConfig().DebugMode() + + // charge 3 million up front to begin activation + if err := c.Burn(3000000); err != nil { + return 0, err + } + version, codeHash, moduleHash, takeAllGas, err := c.State.Programs().ActivateProgram(evm, program, debug) if takeAllGas { _ = c.BurnOut() + } + if err != nil { return version, err } - return version, err + return version, con.ProgramActivated(c, evm, codeHash, moduleHash, program, version) } // Gets the latest stylus version diff --git a/precompiles/context.go b/precompiles/context.go index bea90fbc6..670ffa744 100644 --- a/precompiles/context.go +++ b/precompiles/context.go @@ -53,6 +53,10 @@ func (c *Context) BurnOut() error { return vm.ErrOutOfGas } +func (c *Context) GasLeft() *uint64 { + return &c.gasLeft +} + func (c *Context) Restrict(err error) { log.Crit("A metered burner was used for access-controlled work", "error", err) } diff --git a/system_tests/common_test.go b/system_tests/common_test.go index a4a2c4f2c..9d665fc35 100644 --- a/system_tests/common_test.go +++ b/system_tests/common_test.go @@ -938,7 +938,7 @@ func deployContract( t *testing.T, ctx context.Context, auth bind.TransactOpts, client *ethclient.Client, code []byte, ) common.Address { deploy := deployContractInitCode(code, false) - basefee := GetBaseFee(t, client, ctx) + basefee := arbmath.BigMulByFrac(GetBaseFee(t, client, ctx), 6, 5) // current*1.2 nonce, err := client.NonceAt(ctx, auth.From, nil) Require(t, err) gas, err := client.EstimateGas(ctx, ethereum.CallMsg{ diff --git a/system_tests/program_test.go b/system_tests/program_test.go index 36bcb2f45..6339f0293 100644 --- a/system_tests/program_test.go +++ b/system_tests/program_test.go @@ -59,9 +59,9 @@ func keccakTest(t *testing.T, jit bool) { Require(t, err) colors.PrintBlue("program deployed to ", programAddress.Hex()) - timed(t, "compile same code", func() { + timed(t, "activate same code", func() { if _, err := arbWasm.ActivateProgram(&auth, otherAddressSameCode); err == nil || !strings.Contains(err.Error(), "ProgramUpToDate") { - Fatal(t, "compile should have failed with ProgramUpToDate") + Fatal(t, "activate should have failed with ProgramUpToDate", err) } }) @@ -899,7 +899,7 @@ func TestProgramActivateFails(t *testing.T) { } func testActivateFails(t *testing.T, jit bool) { - ctx, node, _, l2client, auth, cleanup := setupProgramTest(t, false) + ctx, node, _, l2client, auth, cleanup := setupProgramTest(t, jit) defer cleanup() arbWasm, err := precompilesgen.NewArbWasm(types.ArbWasmAddress, l2client) @@ -993,6 +993,43 @@ func testSdkStorage(t *testing.T, jit bool) { check() } +func TestProgramAcivationLogs(t *testing.T) { + t.Parallel() + ctx, _, _, l2client, auth, cleanup := setupProgramTest(t, true) + defer cleanup() + + wasm, _ := readWasmFile(t, watFile("memory")) + arbWasm, err := precompilesgen.NewArbWasm(types.ArbWasmAddress, l2client) + Require(t, err) + + nolimitAuth := auth + nolimitAuth.GasLimit = 32000000 + + programAddress := deployContract(t, ctx, nolimitAuth, l2client, wasm) + + tx, err := arbWasm.ActivateProgram(&auth, programAddress) + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, l2client, tx) + Require(t, err) + + if len(receipt.Logs) != 1 { + Fatal(t, "expected 1 log while activating, got ", len(receipt.Logs)) + } + log, err := arbWasm.ParseProgramActivated(*receipt.Logs[0]) + if err != nil { + Fatal(t, "parsing activated log: ", err) + } + if log.Version == 0 { + Fatal(t, "activated program with version 0") + } + if log.Program != programAddress { + Fatal(t, "unexpected program in activation log: ", log.Program) + } + if crypto.Keccak256Hash(wasm) != log.Codehash { + Fatal(t, "unexpected codehash in activation log: ", log.Codehash) + } +} + func setupProgramTest(t *testing.T, jit bool) ( context.Context, *arbnode.Node, *BlockchainTestInfo, *ethclient.Client, bind.TransactOpts, func(), ) { @@ -1189,7 +1226,6 @@ func validateBlockRange( t *testing.T, blocks []uint64, jit bool, ctx context.Context, node *arbnode.Node, l2client *ethclient.Client, ) { - t.Helper() waitForSequencer(t, node, arbmath.MaxInt(blocks...)) blockHeight, err := l2client.BlockNumber(ctx) Require(t, err) diff --git a/validator/server_api/json.go b/validator/server_api/json.go index ba4ca2232..d81fee176 100644 --- a/validator/server_api/json.go +++ b/validator/server_api/json.go @@ -9,7 +9,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" - "github.com/offchainlabs/nitro/util/arbmath" "github.com/offchainlabs/nitro/util/jsonapi" "github.com/offchainlabs/nitro/validator" ) @@ -20,9 +19,8 @@ type BatchInfoJson struct { } type UserWasmJson struct { - NoncanonicalHash common.Hash - CompressedWasm string - Wasm string + Module string + Asm string } type ValidationInputJson struct { @@ -31,7 +29,7 @@ type ValidationInputJson struct { DelayedMsgNr uint64 PreimagesB64 jsonapi.PreimagesMapJson BatchInfo []BatchInfoJson - UserWasms map[string]UserWasmJson + UserWasms map[common.Hash]UserWasmJson DelayedMsgB64 string StartState validator.GoGlobalState DebugChain bool @@ -45,23 +43,19 @@ func ValidationInputToJson(entry *validator.ValidationInput) *ValidationInputJso DelayedMsgB64: base64.StdEncoding.EncodeToString(entry.DelayedMsg), StartState: entry.StartState, PreimagesB64: jsonapi.NewPreimagesMapJson(entry.Preimages), - UserWasms: make(map[string]UserWasmJson), + UserWasms: make(map[common.Hash]UserWasmJson), DebugChain: entry.DebugChain, } for _, binfo := range entry.BatchInfo { encData := base64.StdEncoding.EncodeToString(binfo.Data) res.BatchInfo = append(res.BatchInfo, BatchInfoJson{binfo.Number, encData}) } - for call, wasm := range entry.UserWasms { - callBytes := arbmath.Uint16ToBytes(call.Version) - callBytes = append(callBytes, call.CodeHash.Bytes()...) - encCall := base64.StdEncoding.EncodeToString(callBytes) + for moduleHash, info := range entry.UserWasms { encWasm := UserWasmJson{ - NoncanonicalHash: wasm.NoncanonicalHash, - CompressedWasm: base64.StdEncoding.EncodeToString(wasm.CompressedWasm), - Wasm: base64.StdEncoding.EncodeToString(wasm.Wasm), + Asm: base64.StdEncoding.EncodeToString(info.Asm), + Module: base64.StdEncoding.EncodeToString(info.Module), } - res.UserWasms[encCall] = encWasm + res.UserWasms[moduleHash] = encWasm } return res } @@ -92,29 +86,20 @@ func ValidationInputFromJson(entry *ValidationInputJson) (*validator.ValidationI } valInput.BatchInfo = append(valInput.BatchInfo, decInfo) } - for call, wasm := range entry.UserWasms { - callBytes, err := base64.StdEncoding.DecodeString(call) + for moduleHash, info := range entry.UserWasms { + asm, err := base64.StdEncoding.DecodeString(info.Asm) if err != nil { return nil, err } - decCall := state.WasmCall{ - Version: arbmath.BytesToUint16(callBytes[:2]), - CodeHash: common.BytesToHash(callBytes[2:]), - } - compressed, err := base64.StdEncoding.DecodeString(wasm.CompressedWasm) - if err != nil { - return nil, err - } - uncompressed, err := base64.StdEncoding.DecodeString(wasm.Wasm) + module, err := base64.StdEncoding.DecodeString(info.Module) if err != nil { return nil, err } - decWasm := state.UserWasm{ - NoncanonicalHash: wasm.NoncanonicalHash, - CompressedWasm: compressed, - Wasm: uncompressed, + decInfo := state.ActivatedWasm{ + Asm: asm, + Module: module, } - valInput.UserWasms[decCall] = &decWasm + valInput.UserWasms[moduleHash] = decInfo } return valInput, nil } diff --git a/validator/server_arb/machine.go b/validator/server_arb/machine.go index 71f4bb439..23642b90f 100644 --- a/validator/server_arb/machine.go +++ b/validator/server_arb/machine.go @@ -20,7 +20,6 @@ import ( "unsafe" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/log" "github.com/offchainlabs/nitro/arbutil" "github.com/offchainlabs/nitro/util/arbmath" @@ -397,30 +396,20 @@ func (m *ArbitratorMachine) SetPreimageResolver(resolver GoPreimageResolver) err return nil } -func (m *ArbitratorMachine) AddUserWasm(call state.WasmCall, wasm *state.UserWasm, debug bool) error { +func (m *ArbitratorMachine) AddUserWasm(moduleHash common.Hash, module []byte) error { defer runtime.KeepAlive(m) if m.frozen { return errors.New("machine frozen") } hashBytes := [32]u8{} - for index, byte := range wasm.NoncanonicalHash.Bytes() { + for index, byte := range moduleHash.Bytes() { hashBytes[index] = u8(byte) } - debugInt := 0 - if debug { - debugInt = 1 - } - err := C.arbitrator_add_user_wasm( + C.arbitrator_add_user_wasm( m.ptr, - (*u8)(arbutil.SliceToPointer(wasm.Wasm)), - u32(len(wasm.Wasm)), - u16(call.Version), - u32(debugInt), + (*u8)(arbutil.SliceToPointer(module)), + usize(len(module)), &C.struct_Bytes32{hashBytes}, ) - defer C.free(unsafe.Pointer(err)) - if err != nil { - return errors.New(C.GoString(err)) - } return nil } diff --git a/validator/server_arb/validator_spawner.go b/validator/server_arb/validator_spawner.go index e89cd20f6..85f55ef6c 100644 --- a/validator/server_arb/validator_spawner.go +++ b/validator/server_arb/validator_spawner.go @@ -106,12 +106,12 @@ func (v *ArbitratorSpawner) loadEntryToMachine(ctx context.Context, entry *valid return fmt.Errorf("error while trying to add sequencer msg for proving: %w", err) } } - for call, wasm := range entry.UserWasms { - err = mach.AddUserWasm(call, wasm, entry.DebugChain) + for moduleHash, info := range entry.UserWasms { + err = mach.AddUserWasm(moduleHash, info.Module) if err != nil { log.Error( "error adding user wasm for proving", - "err", err, "codehash", call.CodeHash, "blockNr", entry.Id, + "err", err, "moduleHash", moduleHash, "blockNr", entry.Id, ) return fmt.Errorf("error adding user wasm for proving: %w", err) } diff --git a/validator/server_jit/jit_machine.go b/validator/server_jit/jit_machine.go index 0742d4352..c1eb3fe45 100644 --- a/validator/server_jit/jit_machine.go +++ b/validator/server_jit/jit_machine.go @@ -118,9 +118,6 @@ func (machine *JitMachine) prove( writeUint8 := func(data uint8) error { return writeExact([]byte{data}) } - writeUint16 := func(data uint16) error { - return writeExact(arbmath.Uint16ToBytes(data)) - } writeUint32 := func(data uint32) error { return writeExact(arbmath.Uint32ToBytes(data)) } @@ -209,17 +206,11 @@ func (machine *JitMachine) prove( if err := writeUint32(uint32(len(userWasms))); err != nil { return state, err } - for call, wasm := range userWasms { - if err := writeExact(call.CodeHash[:]); err != nil { - return state, err - } - if err := writeBytes(wasm.Wasm); err != nil { - return state, err - } - if err := writeExact(wasm.NoncanonicalHash[:]); err != nil { + for moduleHash, info := range userWasms { + if err := writeExact(moduleHash[:]); err != nil { return state, err } - if err := writeUint16(call.Version); err != nil { + if err := writeBytes(info.Asm); err != nil { return state, err } }