diff --git a/arbitrator/arbutil/src/evm/api.rs b/arbitrator/arbutil/src/evm/api.rs index 1989d5e47..f84f92ad9 100644 --- a/arbitrator/arbutil/src/evm/api.rs +++ b/arbitrator/arbutil/src/evm/api.rs @@ -102,7 +102,8 @@ pub trait EvmApi: Send + 'static { &mut self, contract: Bytes20, calldata: &[u8], - gas: u64, + gas_left: u64, + gas_req: u64, value: Bytes32, ) -> (u32, u64, UserOutcomeKind); @@ -113,7 +114,8 @@ pub trait EvmApi: Send + 'static { &mut self, contract: Bytes20, calldata: &[u8], - gas: u64, + gas_left: u64, + gas_req: u64, ) -> (u32, u64, UserOutcomeKind); /// Static-calls the contract at the given address. @@ -123,7 +125,8 @@ pub trait EvmApi: Send + 'static { &mut self, contract: Bytes20, calldata: &[u8], - gas: u64, + gas_left: u64, + gas_req: u64, ) -> (u32, u64, UserOutcomeKind); /// Deploys a new contract using the init code provided. diff --git a/arbitrator/arbutil/src/evm/req.rs b/arbitrator/arbutil/src/evm/req.rs index a90931528..b1c8d9997 100644 --- a/arbitrator/arbutil/src/evm/req.rs +++ b/arbitrator/arbutil/src/evm/req.rs @@ -45,13 +45,15 @@ impl> EvmApiRequestor { call_type: EvmApiMethod, contract: Bytes20, input: &[u8], - gas: u64, + gas_left: u64, + gas_req: u64, value: Bytes32, ) -> (u32, u64, UserOutcomeKind) { - let mut request = Vec::with_capacity(20 + 32 + 8 + input.len()); + let mut request = Vec::with_capacity(20 + 32 + 8 + 8 + input.len()); request.extend(contract); request.extend(value); - request.extend(gas.to_be_bytes()); + request.extend(gas_left.to_be_bytes()); + request.extend(gas_req.to_be_bytes()); request.extend(input); let (res, data, cost) = self.request(call_type, &request); @@ -164,23 +166,33 @@ impl> EvmApi for EvmApiRequestor { &mut self, contract: Bytes20, input: &[u8], - gas: u64, + gas_left: u64, + gas_req: u64, value: Bytes32, ) -> (u32, u64, UserOutcomeKind) { - self.call_request(EvmApiMethod::ContractCall, contract, input, gas, value) + self.call_request( + EvmApiMethod::ContractCall, + contract, + input, + gas_left, + gas_req, + value, + ) } fn delegate_call( &mut self, contract: Bytes20, input: &[u8], - gas: u64, + gas_left: u64, + gas_req: u64, ) -> (u32, u64, UserOutcomeKind) { self.call_request( EvmApiMethod::DelegateCall, contract, input, - gas, + gas_left, + gas_req, Bytes32::default(), ) } @@ -189,13 +201,15 @@ impl> EvmApi for EvmApiRequestor { &mut self, contract: Bytes20, input: &[u8], - gas: u64, + gas_left: u64, + gas_req: u64, ) -> (u32, u64, UserOutcomeKind) { self.call_request( EvmApiMethod::StaticCall, contract, input, - gas, + gas_left, + gas_req, Bytes32::default(), ) } diff --git a/arbitrator/jit/src/program.rs b/arbitrator/jit/src/program.rs index aa719635b..c608a3cf8 100644 --- a/arbitrator/jit/src/program.rs +++ b/arbitrator/jit/src/program.rs @@ -23,10 +23,11 @@ pub fn activate( wasm_size: u32, pages_ptr: GuestPtr, asm_estimate_ptr: GuestPtr, - init_gas_ptr: GuestPtr, - cached_init_gas_ptr: GuestPtr, + init_cost_ptr: GuestPtr, + cached_init_cost_ptr: GuestPtr, version: u16, debug: u32, + codehash: GuestPtr, module_hash_ptr: GuestPtr, gas_ptr: GuestPtr, err_buf: GuestPtr, @@ -34,17 +35,18 @@ pub fn activate( ) -> Result { let (mut mem, _) = env.jit_env(); let wasm = mem.read_slice(wasm_ptr, wasm_size as usize); + let codehash = &mem.read_bytes32(codehash); let debug = debug != 0; let page_limit = mem.read_u16(pages_ptr); let gas_left = &mut mem.read_u64(gas_ptr); - match Module::activate(&wasm, version, page_limit, debug, gas_left) { + match Module::activate(&wasm, codehash, version, page_limit, debug, gas_left) { Ok((module, data)) => { mem.write_u64(gas_ptr, *gas_left); mem.write_u16(pages_ptr, data.footprint); mem.write_u32(asm_estimate_ptr, data.asm_estimate); - mem.write_u16(init_gas_ptr, data.init_gas); - mem.write_u16(cached_init_gas_ptr, data.cached_init_gas); + mem.write_u16(init_cost_ptr, data.init_cost); + mem.write_u16(cached_init_cost_ptr, data.cached_init_cost); mem.write_bytes32(module_hash_ptr, module.hash()); Ok(0) } @@ -55,8 +57,8 @@ pub fn activate( mem.write_u64(gas_ptr, 0); mem.write_u16(pages_ptr, 0); mem.write_u32(asm_estimate_ptr, 0); - mem.write_u16(init_gas_ptr, 0); - mem.write_u16(cached_init_gas_ptr, 0); + mem.write_u16(init_cost_ptr, 0); + mem.write_u16(cached_init_cost_ptr, 0); mem.write_bytes32(module_hash_ptr, Bytes32::default()); Ok(err_bytes.len() as u32) } diff --git a/arbitrator/prover/src/binary.rs b/arbitrator/prover/src/binary.rs index ee8c54785..18f9ecec0 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::{math::SaturatingSum, Color, DebugColor}; +use arbutil::{math::SaturatingSum, Bytes32, Color, DebugColor}; use eyre::{bail, ensure, eyre, Result, WrapErr}; use fnv::{FnvHashMap as HashMap, FnvHashSet as HashSet}; use nom::{ @@ -20,7 +20,7 @@ use nom::{ }; use serde::{Deserialize, Serialize}; use std::{convert::TryInto, fmt::Debug, hash::Hash, mem, path::Path, str::FromStr}; -use wasmer_types::{entity::EntityRef, FunctionIndex, LocalFunctionIndex}; +use wasmer_types::{entity::EntityRef, ExportIndex, FunctionIndex, LocalFunctionIndex}; use wasmparser::{ Data, Element, ExternalKind, MemoryType, Name, NameSectionReader, Naming, Operator, Parser, Payload, TableType, TypeRef, ValType, Validator, WasmFeatures, @@ -232,17 +232,27 @@ pub enum ExportKind { Tag, } -impl TryFrom for ExportKind { - type Error = eyre::Error; - - fn try_from(kind: ExternalKind) -> Result { +impl From for ExportKind { + fn from(kind: ExternalKind) -> Self { use ExternalKind as E; match kind { - E::Func => Ok(Self::Func), - E::Table => Ok(Self::Table), - E::Memory => Ok(Self::Memory), - E::Global => Ok(Self::Global), - E::Tag => Ok(Self::Tag), + E::Func => Self::Func, + E::Table => Self::Table, + E::Memory => Self::Memory, + E::Global => Self::Global, + E::Tag => Self::Tag, + } + } +} + +impl From for ExportKind { + fn from(value: ExportIndex) -> Self { + use ExportIndex as E; + match value { + E::Function(_) => Self::Func, + E::Table(_) => Self::Table, + E::Memory(_) => Self::Memory, + E::Global(_) => Self::Global, } } } @@ -271,7 +281,7 @@ pub type ExportMap = HashMap; pub struct WasmBinary<'a> { pub types: Vec, pub imports: Vec>, - /// Maps *local* function indices to global type signatures + /// Maps *local* function indices to global type signatures. pub functions: Vec, pub tables: Vec, pub memories: Vec, @@ -282,6 +292,10 @@ pub struct WasmBinary<'a> { pub codes: Vec>, pub datas: Vec>, pub names: NameCustomSection, + /// The source wasm, if known. + pub wasm: Option<&'a [u8]>, + /// Consensus data used to make module hashes unique. + pub extra_data: Vec, } pub fn parse<'a>(input: &'a [u8], path: &'_ Path) -> Result> { @@ -312,7 +326,10 @@ pub fn parse<'a>(input: &'a [u8], path: &'_ Path) -> Result> { .validate_all(input) .wrap_err_with(|| eyre!("failed to validate {}", path.to_string_lossy().red()))?; - let mut binary = WasmBinary::default(); + let mut binary = WasmBinary { + wasm: Some(input), + ..Default::default() + }; let sections: Vec<_> = Parser::new(0).parse_all(input).collect::>()?; for section in sections { @@ -390,14 +407,7 @@ pub fn parse<'a>(input: &'a [u8], path: &'_ Path) -> Result> { let name = || name.clone(); binary.names.functions.entry(index).or_insert_with(name); } - - // TODO: we'll only support the types also in wasmparser 0.95+ - if matches!(kind, E::Func | E::Table | E::Memory | E::Global | E::Tag) { - let kind = kind.try_into()?; - binary.exports.insert(name, (export.index, kind)); - } else { - bail!("unsupported export kind {:?}", export) - } + binary.exports.insert(name, (export.index, kind.into())); } } FunctionSection(functions) => process!(binary.functions, functions), @@ -508,18 +518,22 @@ impl<'a> Debug for WasmBinary<'a> { impl<'a> WasmBinary<'a> { /// Instruments a user wasm, producing a version bounded via configurable instrumentation. - pub fn instrument(&mut self, compile: &CompileConfig) -> Result { + pub fn instrument( + &mut self, + compile: &CompileConfig, + codehash: &Bytes32, + ) -> Result { + let start = StartMover::new(compile.debug.debug_info); let meter = Meter::new(&compile.pricing); let dygas = DynamicMeter::new(&compile.pricing); let depth = DepthChecker::new(compile.bounds); let bound = HeapBound::new(compile.bounds); - let start = StartMover::default(); + start.update_module(self)?; meter.update_module(self)?; dygas.update_module(self)?; depth.update_module(self)?; bound.update_module(self)?; - start.update_module(self)?; let count = compile.debug.count_ops.then(Counter::new); if let Some(count) = &count { @@ -550,11 +564,11 @@ impl<'a> WasmBinary<'a> { // add the instrumentation in the order of application // note: this must be consistent with native execution + apply!(start); apply!(meter); apply!(dygas); apply!(depth); apply!(bound); - apply!(start); if let Some(count) = &count { apply!(*count); @@ -563,6 +577,10 @@ impl<'a> WasmBinary<'a> { code.expr = build; } + let wasm = self.wasm.take().unwrap(); + self.extra_data.extend(*codehash); + self.extra_data.extend(compile.version.to_be_bytes()); + // 4GB maximum implies `footprint` fits in a u16 let footprint = self.memory_info()?.min.0 as u16; @@ -570,12 +588,38 @@ impl<'a> WasmBinary<'a> { let ty = FunctionType::new([ArbValueType::I32], [ArbValueType::I32]); let user_main = self.check_func(STYLUS_ENTRY_POINT, ty)?; - // naively assume for now an upper bound of 5Mb - let asm_estimate = 5 * 1024 * 1024; + // predict costs + let funcs = self.codes.len() as u64; + let globals = self.globals.len() as u64; + let wasm_len = wasm.len() as u64; + + let data_len: u64 = self.datas.iter().map(|x| x.range.len() as u64).sum(); + let elem_len: u64 = self.elements.iter().map(|x| x.range.len() as u64).sum(); + let data_len = data_len + elem_len; + + let mut type_len = 0; + for index in &self.functions { + let ty = &self.types[*index as usize]; + type_len += (ty.inputs.len() + ty.outputs.len()) as u64; + } + + let mut asm_estimate: u64 = 512000; + asm_estimate = asm_estimate.saturating_add(funcs.saturating_mul(996829) / 1000); + asm_estimate = asm_estimate.saturating_add(type_len.saturating_mul(11416) / 1000); + asm_estimate = asm_estimate.saturating_add(wasm_len.saturating_mul(62628) / 10000); + + let mut cached_init: u64 = 0; + cached_init = cached_init.saturating_add(funcs.saturating_mul(13420) / 100_000); + cached_init = cached_init.saturating_add(type_len.saturating_mul(89) / 100_000); + cached_init = cached_init.saturating_add(wasm_len.saturating_mul(122) / 100_000); + cached_init = cached_init.saturating_add(globals.saturating_mul(1628) / 1000); + cached_init = cached_init.saturating_add(data_len.saturating_mul(75244) / 100_000); + cached_init = cached_init.saturating_add(footprint as u64 * 5); - // TODO: determine safe value - let init_gas = 4096; - let cached_init_gas = 1024; + let mut init = cached_init; + init = init.saturating_add(funcs.saturating_mul(8252) / 1000); + init = init.saturating_add(type_len.saturating_mul(1059) / 1000); + init = init.saturating_add(wasm_len.saturating_mul(1286) / 10_000); let [ink_left, ink_status] = meter.globals(); let depth_left = depth.globals(); @@ -583,9 +627,9 @@ impl<'a> WasmBinary<'a> { ink_left: ink_left.as_u32(), ink_status: ink_status.as_u32(), depth_left: depth_left.as_u32(), - init_gas, - cached_init_gas, - asm_estimate, + init_cost: init.try_into()?, + cached_init_cost: cached_init.try_into()?, + asm_estimate: asm_estimate.try_into()?, footprint, user_main, }) @@ -596,9 +640,10 @@ impl<'a> WasmBinary<'a> { wasm: &'a [u8], page_limit: u16, compile: &CompileConfig, + codehash: &Bytes32, ) -> Result<(WasmBinary<'a>, StylusData)> { let mut bin = parse(wasm, Path::new("user"))?; - let stylus_data = bin.instrument(compile)?; + let stylus_data = bin.instrument(compile, codehash)?; let Some(memory) = bin.memories.first() else { bail!("missing memory with export name \"memory\"") diff --git a/arbitrator/prover/src/machine.rs b/arbitrator/prover/src/machine.rs index d83253fe1..fd7e22e1b 100644 --- a/arbitrator/prover/src/machine.rs +++ b/arbitrator/prover/src/machine.rs @@ -19,7 +19,7 @@ use crate::{ IBinOpType, IRelOpType, IUnOpType, Instruction, Opcode, }, }; -use arbutil::{math, Bytes32, Color, DebugColor, PreimageType}; +use arbutil::{crypto, math, Bytes32, Color, DebugColor, PreimageType}; use brotli::Dictionary; #[cfg(feature = "native")] use c_kzg::BYTES_PER_BLOB; @@ -157,7 +157,7 @@ impl Function { let code_hashes = (0..chunks).into_par_iter().map(crunch).collect(); #[cfg(not(feature = "rayon"))] - let code_hashes = (0..chunks).into_iter().map(crunch).collect(); + let code_hashes = (0..chunks).map(crunch).collect(); self.code_merkle = Merkle::new(MerkleType::Instruction, code_hashes); } @@ -305,6 +305,8 @@ pub struct Module { pub(crate) func_exports: Arc>, #[serde(default)] pub(crate) all_exports: Arc, + /// Used to make modules unique. + pub(crate) extra_hash: Arc, } lazy_static! { @@ -579,6 +581,7 @@ impl Module { func_types: Arc::new(func_types), func_exports: Arc::new(func_exports), all_exports: Arc::new(bin.exports.clone()), + extra_hash: Arc::new(crypto::keccak(&bin.extra_data).into()), }) } @@ -621,6 +624,7 @@ impl Module { h.update(self.memory.hash()); h.update(self.tables_merkle.root()); h.update(self.funcs_merkle.root()); + h.update(*self.extra_hash); h.update(self.internals_offset.to_be_bytes()); h.finalize().into() } @@ -642,6 +646,7 @@ impl Module { data.extend(self.tables_merkle.root()); data.extend(self.funcs_merkle.root()); + data.extend(*self.extra_hash); data.extend(self.internals_offset.to_be_bytes()); data } @@ -688,6 +693,7 @@ pub struct ModuleSerdeAll { func_types: Arc>, func_exports: Arc>, all_exports: Arc, + extra_hash: Arc, } impl From for Module { @@ -708,6 +714,7 @@ impl From for Module { func_types: module.func_types, func_exports: module.func_exports, all_exports: module.all_exports, + extra_hash: module.extra_hash, } } } @@ -730,6 +737,7 @@ impl From<&Module> for ModuleSerdeAll { func_types: module.func_types.clone(), func_exports: module.func_exports.clone(), all_exports: module.all_exports.clone(), + extra_hash: module.extra_hash.clone(), } } } @@ -1234,7 +1242,7 @@ impl Machine { let data = std::fs::read(path)?; let wasm = wasmer::wat2wasm(&data)?; let mut bin = binary::parse(&wasm, Path::new("user"))?; - let stylus_data = bin.instrument(compile)?; + let stylus_data = bin.instrument(compile, &Bytes32::default())?; let user_test = std::fs::read("../../target/machines/latest/user_test.wasm")?; let user_test = parse(&user_test, Path::new("user_test"))?; @@ -1264,10 +1272,16 @@ impl Machine { /// 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) -> Result { + pub fn add_program( + &mut self, + wasm: &[u8], + codehash: &Bytes32, + 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 stylus_data = bin.instrument(&config, codehash)?; // enable debug mode if debug funcs are available if debug_funcs { @@ -1467,11 +1481,12 @@ impl Machine { types: Arc::new(entrypoint_types), names: Arc::new(entrypoint_names), internals_offset: 0, - host_call_hooks: Arc::new(Vec::new()), + host_call_hooks: Default::default(), start_function: None, func_types: Arc::new(vec![FunctionType::default()]), - func_exports: Arc::new(HashMap::default()), - all_exports: Arc::new(HashMap::default()), + func_exports: Default::default(), + all_exports: Default::default(), + extra_hash: Default::default(), }; modules[0] = entrypoint; diff --git a/arbitrator/prover/src/main.rs b/arbitrator/prover/src/main.rs index 0162a6110..697d178fc 100644 --- a/arbitrator/prover/src/main.rs +++ b/arbitrator/prover/src/main.rs @@ -204,7 +204,9 @@ fn main() -> Result<()> { 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)?; + let codehash = &Bytes32::default(); + mach.add_program(&wasm, codehash, 1, true) + .wrap_err_with(err)?; } if opts.print_modules { diff --git a/arbitrator/prover/src/programs/config.rs b/arbitrator/prover/src/programs/config.rs index 9b4b2d83c..0b5ce1747 100644 --- a/arbitrator/prover/src/programs/config.rs +++ b/arbitrator/prover/src/programs/config.rs @@ -124,6 +124,8 @@ pub struct CompilePricingParams { pub struct CompileDebugParams { /// Allow debug functions pub debug_funcs: bool, + /// Retain debug info + pub debug_info: bool, /// Add instrumentation to count the number of times each kind of opcode is executed pub count_ops: bool, /// Whether to use the Cranelift compiler @@ -156,6 +158,7 @@ impl CompileConfig { let mut config = Self::default(); config.version = version; config.debug.debug_funcs = debug_chain; + config.debug.debug_info = debug_chain; match version { 0 => {} @@ -190,19 +193,19 @@ impl CompileConfig { compiler.canonicalize_nans(true); compiler.enable_verifier(); + let start = MiddlewareWrapper::new(StartMover::new(self.debug.debug_info)); let meter = MiddlewareWrapper::new(Meter::new(&self.pricing)); let dygas = MiddlewareWrapper::new(DynamicMeter::new(&self.pricing)); let depth = MiddlewareWrapper::new(DepthChecker::new(self.bounds)); let bound = MiddlewareWrapper::new(HeapBound::new(self.bounds)); - let start = MiddlewareWrapper::new(StartMover::default()); // add the instrumentation in the order of application // note: this must be consistent with the prover + compiler.push_middleware(Arc::new(start)); compiler.push_middleware(Arc::new(meter)); compiler.push_middleware(Arc::new(dygas)); compiler.push_middleware(Arc::new(depth)); compiler.push_middleware(Arc::new(bound)); - compiler.push_middleware(Arc::new(start)); if self.debug.count_ops { let counter = Counter::new(); diff --git a/arbitrator/prover/src/programs/mod.rs b/arbitrator/prover/src/programs/mod.rs index b561ab326..a5df2e31a 100644 --- a/arbitrator/prover/src/programs/mod.rs +++ b/arbitrator/prover/src/programs/mod.rs @@ -8,7 +8,7 @@ use crate::{ programs::config::CompileConfig, value::{FunctionType as ArbFunctionType, Value}, }; -use arbutil::{math::SaturatingSum, Color}; +use arbutil::{math::SaturatingSum, Bytes32, Color}; use eyre::{bail, eyre, Report, Result, WrapErr}; use fnv::FnvHashMap as HashMap; use std::fmt::Debug; @@ -48,7 +48,10 @@ pub trait ModuleMod { fn all_functions(&self) -> Result>; fn all_signatures(&self) -> Result>; fn get_import(&self, module: &str, name: &str) -> Result; - fn move_start_function(&mut self, name: &str) -> Result<()>; + /// Moves the start function, returning true if present. + fn move_start_function(&mut self, name: &str) -> Result; + /// Drops debug-only info like export names. + fn drop_exports_and_names(&mut self, keep: &HashMap<&str, ExportKind>); fn memory_info(&self) -> Result; } @@ -224,17 +227,26 @@ impl ModuleMod for ModuleInfo { .ok_or_else(|| eyre!("missing import {}", name.red())) } - fn move_start_function(&mut self, name: &str) -> Result<()> { + fn move_start_function(&mut self, name: &str) -> Result { if let Some(prior) = self.exports.get(name) { bail!("function {} already exists @ index {:?}", name.red(), prior) } - if let Some(start) = self.start_function.take() { + let start = self.start_function.take(); + if let Some(start) = start { let export = ExportIndex::Function(start); self.exports.insert(name.to_owned(), export); self.function_names.insert(start, name.to_owned()); } - Ok(()) + Ok(start.is_some()) + } + + fn drop_exports_and_names(&mut self, keep: &HashMap<&str, ExportKind>) { + self.exports.retain(|name, export| { + keep.get(name.as_str()) + .map_or(false, |x| *x == (*export).into()) + }); + self.function_names.clear(); } fn memory_info(&self) -> Result { @@ -336,17 +348,24 @@ impl<'a> ModuleMod for WasmBinary<'a> { .ok_or_else(|| eyre!("missing import {}", name.red())) } - fn move_start_function(&mut self, name: &str) -> Result<()> { + fn move_start_function(&mut self, name: &str) -> Result { if let Some(prior) = self.exports.get(name) { bail!("function {} already exists @ index {:?}", name.red(), prior) } - if let Some(start) = self.start.take() { + let start = self.start.take(); + if let Some(start) = start { let name = name.to_owned(); self.exports.insert(name.clone(), (start, ExportKind::Func)); self.names.functions.insert(start, name); } - Ok(()) + Ok(start.is_some()) + } + + fn drop_exports_and_names(&mut self, keep: &HashMap<&str, ExportKind>) { + self.exports + .retain(|name, ty| keep.get(name.as_str()).map_or(false, |x| *x == ty.1)); + self.names.functions.clear(); } fn memory_info(&self) -> Result { @@ -373,10 +392,10 @@ pub struct StylusData { pub ink_status: u32, /// Global index for the amount of stack space remaining. pub depth_left: u32, - /// Gas needed to invoke the program. - pub init_gas: u16, - /// Gas needed to invoke the program when stored in the init cache. - pub cached_init_gas: u16, + /// Cost paid to invoke the program. See `programs.go` for the translation to gas. + pub init_cost: u16, + /// Cost paid to invoke the program when stored in the init cache. + pub cached_init_cost: u16, /// Canonical estimate of the asm length in bytes. pub asm_estimate: u32, /// Initial memory size in pages. @@ -398,6 +417,7 @@ impl StylusData { impl Module { pub fn activate( wasm: &[u8], + codehash: &Bytes32, version: u16, page_limit: u16, debug: bool, @@ -428,8 +448,8 @@ impl Module { pay!(wasm_len.saturating_mul(31_733 / 100_000)); let compile = CompileConfig::version(version, debug); - let (bin, stylus_data) = - WasmBinary::parse_user(wasm, page_limit, &compile).wrap_err("failed to parse wasm")?; + let (bin, stylus_data) = WasmBinary::parse_user(wasm, page_limit, &compile, codehash) + .wrap_err("failed to parse wasm")?; // pay for funcs let funcs = bin.functions.len() as u64; diff --git a/arbitrator/prover/src/programs/start.rs b/arbitrator/prover/src/programs/start.rs index 9d9f66f3e..d3a19942f 100644 --- a/arbitrator/prover/src/programs/start.rs +++ b/arbitrator/prover/src/programs/start.rs @@ -1,23 +1,55 @@ // Copyright 2022-2023, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE -use super::{DefaultFuncMiddleware, Middleware, ModuleMod}; -use eyre::Result; +use crate::{ + binary::ExportKind, + programs::{DefaultFuncMiddleware, Middleware, ModuleMod, STYLUS_ENTRY_POINT}, +}; +use eyre::{bail, Result}; +use fnv::FnvHashMap as HashMap; +use lazy_static::lazy_static; use wasmer_types::LocalFunctionIndex; #[cfg(feature = "native")] use wasmer::TypedFunction; -pub const STYLUS_START: &str = "stylus_start"; +lazy_static! { + /// Lists the exports a user program map have + static ref EXPORT_WHITELIST: HashMap<&'static str, ExportKind> = { + let mut map = HashMap::default(); + map.insert(STYLUS_ENTRY_POINT, ExportKind::Func); + map.insert(StartMover::NAME, ExportKind::Func); + map.insert("memory", ExportKind::Memory); + map + }; +} + +#[derive(Debug)] +pub struct StartMover { + /// Whether to keep offchain information. + debug: bool, +} -#[derive(Debug, Default)] -pub struct StartMover {} +impl StartMover { + pub const NAME: &'static str = "stylus_start"; + + pub fn new(debug: bool) -> Self { + Self { debug } + } +} impl Middleware for StartMover { type FM<'a> = DefaultFuncMiddleware; fn update_module(&self, module: &mut M) -> Result<()> { - module.move_start_function(STYLUS_START) + let had_start = module.move_start_function(Self::NAME)?; + if had_start && !self.debug { + bail!("start functions not allowed"); + } + if !self.debug { + module.drop_exports_and_names(&EXPORT_WHITELIST); + } + Ok(()) } fn instrument<'a>(&self, _: LocalFunctionIndex) -> Result> { diff --git a/arbitrator/prover/test-cases/dynamic.wat b/arbitrator/prover/test-cases/dynamic.wat index 5d9ca1b14..97c55ba80 100644 --- a/arbitrator/prover/test-cases/dynamic.wat +++ b/arbitrator/prover/test-cases/dynamic.wat @@ -12,7 +12,7 @@ ;; WAVM Module hash (data (i32.const 0x000) - "\3a\eb\a0\67\68\ef\f5\f9\4e\ec\84\88\ac\54\b7\b7\07\a5\12\9c\fb\73\50\37\33\d9\9e\90\ea\72\97\8c") ;; user + "\87\12\6b\19\8a\ce\0c\ba\00\6a\ab\9b\b7\45\bb\0a\ac\48\4d\6b\b8\b5\f9\03\a2\99\8f\64\00\9f\e2\04") ;; user (func $start (local $user i32) (local $internals i32) ;; link in user.wat diff --git a/arbitrator/prover/test-cases/link.wat b/arbitrator/prover/test-cases/link.wat index 7ea72c70e..e033bf0e9 100644 --- a/arbitrator/prover/test-cases/link.wat +++ b/arbitrator/prover/test-cases/link.wat @@ -7,32 +7,32 @@ (import "env" "wavm_halt_and_set_finished" (func $halt )) ;; WAVM module hashes - (data (i32.const 0x000) - "\56\19\01\5f\5d\d4\1f\5a\f8\39\eb\a7\71\a5\8e\e8\a4\d1\3a\dd\ee\2e\75\29\9a\19\cc\89\a5\ab\d3\73") ;; block + (data (i32.const 0x000) + "\eb\12\b0\76\57\15\ad\16\0a\78\54\4d\c7\8d\d4\86\1c\58\a3\ee\77\f9\4a\4e\61\a3\f1\7f\d9\d2\be\8a") ;; block (data (i32.const 0x020) - "\7a\02\20\3c\1a\93\f8\0a\7c\1c\43\b3\95\79\c5\9d\f7\c3\84\5d\be\2e\1a\9d\6f\58\88\87\c0\a2\fe\13") ;; call + "\01\90\21\0c\1d\c8\45\9c\32\ef\a6\00\44\3b\e0\b6\31\70\1f\ce\7a\38\90\1c\e0\c5\40\6d\d8\ce\30\a6") ;; call (data (i32.const 0x040) - "\76\aa\58\26\ed\70\37\00\01\c1\f0\62\4c\cb\23\77\1e\03\a0\e7\34\a8\45\11\c3\bd\de\4e\03\40\4a\5c") ;; indirect + "\e1\a2\fa\8e\81\2a\34\2e\cf\0f\62\46\ba\a4\45\8e\2d\95\2f\ec\1e\79\8f\dc\1b\1c\b8\15\cf\26\02\6c") ;; indirect (data (i32.const 0x060) - "\79\54\72\df\45\56\6f\2f\5f\85\06\60\ec\3b\0a\43\ce\f0\3b\90\75\7d\86\82\d1\8d\c1\fe\da\31\40\bb") ;; const + "\ae\cb\eb\e9\0b\5e\1f\78\1b\66\5b\ff\8a\a4\18\a1\a2\e9\90\26\8b\df\df\95\64\54\82\07\6a\d4\e6\20") ;; const (data (i32.const 0x080) - "\9e\48\3c\16\fb\ec\9b\90\de\34\8f\38\26\a7\41\44\0a\fb\1c\21\f4\e3\76\be\a2\f3\d7\03\4a\1d\9c\a2") ;; div + "\8b\7b\7e\a8\b8\21\c8\d0\2a\80\7c\1e\4b\6d\0d\07\f3\2d\8b\4e\f1\6b\e4\44\03\cf\05\66\9b\09\be\6d") ;; div (data (i32.const 0x0a0) - "\38\cb\94\a1\4d\d1\ab\9a\29\b0\f7\5e\c7\f0\cb\db\1d\f5\fe\34\52\8e\26\7a\25\c8\a8\8e\d4\a4\16\f9") ;; globals + "\da\4a\41\74\d6\2e\20\36\8e\cb\8e\5d\45\12\1c\28\1d\c4\8f\1d\77\92\9f\07\a8\6b\35\ea\89\2e\f9\72") ;; globals (data (i32.const 0x0c0) - "\36\62\29\c5\f3\d2\3e\8e\21\02\8d\ef\95\04\2d\d8\a5\1b\08\2d\30\d7\6b\6c\85\83\4b\19\be\8e\dd\ba") ;; if-else + "\3f\ec\7c\06\04\b2\0d\99\bb\10\85\61\91\ea\b6\97\a7\a2\d1\19\67\2e\7c\d9\17\d4\6b\45\e8\4b\83\4b") ;; if-else (data (i32.const 0x0e0) - "\98\5d\8a\d6\ac\09\6b\bd\cc\ca\7c\87\a9\20\db\11\5f\b1\28\e1\a1\51\70\8a\9f\46\bf\f0\f8\c8\d0\e2") ;; locals + "\30\12\24\71\df\9f\a9\f8\9c\33\9b\37\a7\08\f5\aa\5f\53\68\b4\e4\de\66\bb\73\ff\30\29\47\5f\50\e5") ;; locals (data (i32.const 0x100) - "\9a\cc\60\ec\96\44\53\09\1c\0c\2e\19\42\f2\b4\db\56\a7\d4\40\2e\36\f3\03\33\43\05\de\ea\c5\6b\47") ;; loop + "\f3\95\dd\a7\e1\d7\df\94\06\ca\93\0f\53\bf\66\ce\1a\aa\b2\30\68\08\64\b5\5b\61\54\2c\1d\62\e8\25") ;; loop (data (i32.const 0x120) - "\2b\d8\a0\ed\09\1c\47\03\b1\55\d7\a6\b0\bd\24\68\e0\0b\92\a6\b8\fe\2c\71\b4\c7\bf\40\05\6d\f4\2d") ;; math + "\8c\a3\63\7c\4e\70\f7\79\13\0c\9a\94\5e\63\3b\a9\06\80\9f\a6\51\0e\32\34\e0\9d\78\05\6a\30\98\0f") ;; math (data (i32.const 0x140) - "\7e\01\98\c8\a1\f4\74\be\92\8c\2c\ec\5d\5f\be\04\65\b1\c0\74\43\71\c3\63\00\db\20\b3\a9\17\9b\ac") ;; iops + "\47\f7\4f\9c\21\51\4f\52\24\ea\d3\37\5c\bf\a9\1b\1a\5f\ef\22\a5\2a\60\30\c5\52\18\90\6b\b1\51\e5") ;; iops (data (i32.const 0x160) - "\3a\eb\a0\67\68\ef\f5\f9\4e\ec\84\88\ac\54\b7\b7\07\a5\12\9c\fb\73\50\37\33\d9\9e\90\ea\72\97\8c") ;; user + "\87\12\6b\19\8a\ce\0c\ba\00\6a\ab\9b\b7\45\bb\0a\ac\48\4d\6b\b8\b5\f9\03\a2\99\8f\64\00\9f\e2\04") ;; user (data (i32.const 0x180) - "\fa\91\57\09\98\8a\54\d2\d5\96\71\13\da\71\ae\80\eb\b1\b3\68\5e\90\d7\8e\0e\7d\a2\c4\d8\d9\72\cf") ;; return + "\ee\47\08\f6\47\b2\10\88\1f\89\86\e7\e3\79\6b\b2\77\43\f1\4e\ee\cf\45\4a\9b\7c\d7\c4\5b\63\b6\d7") ;; return (func $start (local $counter i32) diff --git a/arbitrator/stylus/src/cache.rs b/arbitrator/stylus/src/cache.rs index e2bcf65cb..2b83c6152 100644 --- a/arbitrator/stylus/src/cache.rs +++ b/arbitrator/stylus/src/cache.rs @@ -83,7 +83,7 @@ impl InitCache { None } - /// Inserts an item into the long term cache, stealing from the LRU cache if able. + /// Inserts an item into the long term cache, cloning from the LRU cache if able. pub fn insert( module_hash: Bytes32, module: &[u8], @@ -92,9 +92,9 @@ impl InitCache { ) -> Result<(Module, Store)> { let key = CacheKey::new(module_hash, version, debug); - // if in LRU, move to ArbOS + // if in LRU, add to ArbOS let mut cache = cache!(); - if let Some(item) = cache.lru.pop(&key) { + if let Some(item) = cache.lru.peek(&key).cloned() { cache.arbos.insert(key, item.clone()); return Ok(item.data()); } diff --git a/arbitrator/stylus/src/lib.rs b/arbitrator/stylus/src/lib.rs index c197fbae9..7abfb98bf 100644 --- a/arbitrator/stylus/src/lib.rs +++ b/arbitrator/stylus/src/lib.rs @@ -141,6 +141,7 @@ pub unsafe extern "C" fn stylus_activate( debug: bool, output: *mut RustBytes, asm_len: *mut usize, + codehash: *const Bytes32, module_hash: *mut Bytes32, stylus_data: *mut StylusData, gas: *mut u64, @@ -148,12 +149,14 @@ pub unsafe extern "C" fn stylus_activate( let wasm = wasm.slice(); let output = &mut *output; let module_hash = &mut *module_hash; + let codehash = &*codehash; let gas = &mut *gas; - let (asm, module, info) = match native::activate(wasm, version, page_limit, debug, gas) { - Ok(val) => val, - Err(err) => return output.write_err(err), - }; + let (asm, module, info) = + match native::activate(wasm, codehash, version, page_limit, debug, gas) { + Ok(val) => val, + Err(err) => return output.write_err(err), + }; *asm_len = asm.len(); *module_hash = module.hash(); *stylus_data = info; diff --git a/arbitrator/stylus/src/native.rs b/arbitrator/stylus/src/native.rs index 40d656943..6d5e4cd2e 100644 --- a/arbitrator/stylus/src/native.rs +++ b/arbitrator/stylus/src/native.rs @@ -12,7 +12,7 @@ use arbutil::{ EvmData, }, operator::OperatorCode, - Color, + Bytes32, Color, }; use eyre::{bail, eyre, ErrReport, Result}; use prover::{ @@ -23,7 +23,7 @@ use prover::{ depth::STYLUS_STACK_LEFT, meter::{STYLUS_INK_LEFT, STYLUS_INK_STATUS}, prelude::*, - start::STYLUS_START, + start::StartMover, StylusData, }, }; @@ -340,7 +340,7 @@ impl> StartlessMachine for NativeInstance { let store = &self.store; let exports = &self.instance.exports; exports - .get_typed_function(store, STYLUS_START) + .get_typed_function(store, StartMover::NAME) .map_err(ErrReport::new) } } @@ -434,13 +434,15 @@ pub fn module(wasm: &[u8], compile: CompileConfig) -> Result> { pub fn activate( wasm: &[u8], + codehash: &Bytes32, version: u16, page_limit: u16, debug: bool, gas: &mut u64, ) -> Result<(Vec, ProverModule, StylusData)> { let compile = CompileConfig::version(version, debug); - let (module, stylus_data) = ProverModule::activate(wasm, version, page_limit, debug, gas)?; + let (module, stylus_data) = + ProverModule::activate(wasm, codehash, version, page_limit, debug, gas)?; let asm = match self::module(wasm, compile) { Ok(asm) => asm, diff --git a/arbitrator/stylus/src/test/api.rs b/arbitrator/stylus/src/test/api.rs index 2673a5f22..92d731791 100644 --- a/arbitrator/stylus/src/test/api.rs +++ b/arbitrator/stylus/src/test/api.rs @@ -101,12 +101,14 @@ impl EvmApi for TestEvmApi { &mut self, contract: Bytes20, calldata: &[u8], - gas: u64, + _gas_left: u64, + gas_req: u64, _value: Bytes32, ) -> (u32, u64, UserOutcomeKind) { let compile = self.compile.clone(); let evm_data = self.evm_data; let config = *self.configs.lock().get(&contract).unwrap(); + let gas = gas_req; // Not consensus behavior let mut native = unsafe { let contracts = self.contracts.lock(); @@ -129,7 +131,8 @@ impl EvmApi for TestEvmApi { &mut self, _contract: Bytes20, _calldata: &[u8], - _gas: u64, + _gas_left: u64, + _gas_req: u64, ) -> (u32, u64, UserOutcomeKind) { todo!("delegate call not yet supported") } @@ -138,10 +141,11 @@ impl EvmApi for TestEvmApi { &mut self, contract: Bytes20, calldata: &[u8], - gas: u64, + gas_left: u64, + gas_req: u64, ) -> (u32, u64, UserOutcomeKind) { println!("note: overriding static call with call"); - self.contract_call(contract, calldata, gas, Bytes32::default()) + self.contract_call(contract, calldata, gas_left, gas_req, Bytes32::default()) } fn create1( diff --git a/arbitrator/stylus/src/test/misc.rs b/arbitrator/stylus/src/test/misc.rs index 868bc2d16..ae44a885f 100644 --- a/arbitrator/stylus/src/test/misc.rs +++ b/arbitrator/stylus/src/test/misc.rs @@ -8,7 +8,7 @@ use crate::{ test::{check_instrumentation, new_test_machine}, }; use eyre::Result; -use prover::programs::{prelude::*, start::STYLUS_START}; +use prover::programs::{prelude::*, start::StartMover}; use wasmer::{imports, Function}; #[test] @@ -77,6 +77,6 @@ fn test_console() -> Result<()> { native.call_func(starter, ink)?; let mut machine = new_test_machine(filename, &compile)?; - machine.call_user_func(STYLUS_START, vec![], ink)?; + machine.call_user_func(StartMover::NAME, vec![], ink)?; check_instrumentation(native, machine) } diff --git a/arbitrator/stylus/src/test/mod.rs b/arbitrator/stylus/src/test/mod.rs index 0f1cfd761..d7f3248d3 100644 --- a/arbitrator/stylus/src/test/mod.rs +++ b/arbitrator/stylus/src/test/mod.rs @@ -140,7 +140,7 @@ fn new_test_machine(path: &str, compile: &CompileConfig) -> Result { let wat = std::fs::read(path)?; let wasm = wasmer::wat2wasm(&wat)?; let mut bin = prover::binary::parse(&wasm, Path::new("user"))?; - let stylus_data = bin.instrument(compile)?; + let stylus_data = bin.instrument(compile, &Bytes32::default())?; let wat = std::fs::read("tests/test.wat")?; let wasm = wasmer::wat2wasm(&wat)?; diff --git a/arbitrator/stylus/src/test/native.rs b/arbitrator/stylus/src/test/native.rs index d26c3bf32..503e5875f 100644 --- a/arbitrator/stylus/src/test/native.rs +++ b/arbitrator/stylus/src/test/native.rs @@ -149,7 +149,7 @@ fn test_count() -> Result<()> { compiler.canonicalize_nans(true); compiler.enable_verifier(); - let starter = StartMover::default(); + let starter = StartMover::new(true); let counter = Counter::new(); compiler.push_middleware(Arc::new(MiddlewareWrapper::new(starter))); compiler.push_middleware(Arc::new(MiddlewareWrapper::new(counter))); @@ -183,22 +183,35 @@ fn test_import_export_safety() -> Result<()> { // bad-export2.wat there's a func named `stylus_global_with_random_name` // bad-import.wat there's an import named `stylus_global_with_random_name` - fn check(path: &str, both: bool) -> Result<()> { - if both { - let compile = test_compile_config(); - assert!(TestInstance::new_test(path, compile).is_err()); - } - let path = &Path::new(path); + fn check(file: &str, both: bool, instrument: bool) -> Result<()> { + let path = &Path::new(file); let wat = std::fs::read(path)?; let wasm = wasmer::wat2wasm(&wat)?; - assert!(binary::parse(&wasm, path).is_err()); + let bin = binary::parse(&wasm, path); + if !instrument { + assert!(bin.is_err()); + return Ok(()); + } + + let codehash = &Bytes32::default(); + let mut compile = test_compile_config(); + let mut bin = bin?; + assert!(bin.clone().instrument(&compile, codehash).is_err()); + compile.debug.debug_info = false; + assert!(bin.instrument(&compile, &codehash).is_err()); + + if both { + assert!(TestInstance::new_test(file, compile).is_err()); + } Ok(()) } // TODO: perform all the same checks in instances - check("tests/bad-export.wat", true)?; - check("tests/bad-export2.wat", false)?; - check("tests/bad-import.wat", false) + check("tests/bad-mods/bad-export.wat", true, false)?; + check("tests/bad-mods/bad-export2.wat", true, false)?; + check("tests/bad-mods/bad-export3.wat", true, true)?; + check("tests/bad-mods/bad-export4.wat", false, true)?; + check("tests/bad-mods/bad-import.wat", true, false) } #[test] diff --git a/arbitrator/stylus/tests/bad-export.wat b/arbitrator/stylus/tests/bad-mods/bad-export.wat similarity index 78% rename from arbitrator/stylus/tests/bad-export.wat rename to arbitrator/stylus/tests/bad-mods/bad-export.wat index ebe2181a1..80c029166 100644 --- a/arbitrator/stylus/tests/bad-export.wat +++ b/arbitrator/stylus/tests/bad-mods/bad-export.wat @@ -1,4 +1,4 @@ -;; Copyright 2022, Offchain Labs, Inc. +;; Copyright 2022-2024, Offchain Labs, Inc. ;; For license information, see https://github.com/nitro/blob/master/LICENSE (module diff --git a/arbitrator/stylus/tests/bad-export2.wat b/arbitrator/stylus/tests/bad-mods/bad-export2.wat similarity index 76% rename from arbitrator/stylus/tests/bad-export2.wat rename to arbitrator/stylus/tests/bad-mods/bad-export2.wat index 234007c3a..907cc299c 100644 --- a/arbitrator/stylus/tests/bad-export2.wat +++ b/arbitrator/stylus/tests/bad-mods/bad-export2.wat @@ -1,4 +1,4 @@ -;; Copyright 2022, Offchain Labs, Inc. +;; Copyright 2022-2024, Offchain Labs, Inc. ;; For license information, see https://github.com/nitro/blob/master/LICENSE (module diff --git a/arbitrator/stylus/tests/bad-mods/bad-export3.wat b/arbitrator/stylus/tests/bad-mods/bad-export3.wat new file mode 100644 index 000000000..30232916f --- /dev/null +++ b/arbitrator/stylus/tests/bad-mods/bad-export3.wat @@ -0,0 +1,5 @@ +;; Copyright 2024, Offchain Labs, Inc. +;; For license information, see https://github.com/nitro/blob/master/LICENSE + +(module + (func (export "memory"))) diff --git a/arbitrator/stylus/tests/bad-mods/bad-export4.wat b/arbitrator/stylus/tests/bad-mods/bad-export4.wat new file mode 100644 index 000000000..47142990a --- /dev/null +++ b/arbitrator/stylus/tests/bad-mods/bad-export4.wat @@ -0,0 +1,7 @@ +;; Copyright 2024, Offchain Labs, Inc. +;; For license information, see https://github.com/nitro/blob/master/LICENSE + +(module + (global (export "user_entrypoint") i32 (i32.const 0)) + (memory (export "memory") 0 0) +) diff --git a/arbitrator/stylus/tests/bad-import.wat b/arbitrator/stylus/tests/bad-mods/bad-import.wat similarity index 76% rename from arbitrator/stylus/tests/bad-import.wat rename to arbitrator/stylus/tests/bad-mods/bad-import.wat index b52c6e779..ec2a951fb 100644 --- a/arbitrator/stylus/tests/bad-import.wat +++ b/arbitrator/stylus/tests/bad-mods/bad-import.wat @@ -1,4 +1,4 @@ -;; Copyright 2022, Offchain Labs, Inc. +;; Copyright 2022-2024, Offchain Labs, Inc. ;; For license information, see https://github.com/nitro/blob/master/LICENSE (module diff --git a/arbitrator/tools/module_roots/Cargo.lock b/arbitrator/tools/module_roots/Cargo.lock index fe4f163d5..248a632d0 100644 --- a/arbitrator/tools/module_roots/Cargo.lock +++ b/arbitrator/tools/module_roots/Cargo.lock @@ -127,6 +127,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "bitvec" version = "1.0.1" @@ -266,7 +272,7 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", - "bitflags", + "bitflags 1.3.2", "strsim 0.8.0", "textwrap", "unicode-width", @@ -565,7 +571,7 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "add9a102807b524ec050363f09e06f1504214b0e1c7797f64261c891022dce8b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "byteorder", "lazy_static", "proc-macro-error", @@ -660,15 +666,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - [[package]] name = "funty" version = "2.0.0" @@ -783,16 +780,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "indenter" version = "0.3.3" @@ -930,9 +917,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] @@ -1156,12 +1143,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1311,7 +1292,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1332,7 +1313,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76e189c2369884dce920945e2ddf79b3dff49e071a167dd1817fa9c4c00d512e" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "mach", "winapi", @@ -1785,27 +1766,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -1818,17 +1784,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" -[[package]] -name = "url" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - [[package]] name = "uuid" version = "1.7.0" @@ -1878,29 +1833,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-downcast" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dac026d43bcca6e7ce1c0956ba68f59edf6403e8e930a5d891be72c31a44340" -dependencies = [ - "js-sys", - "once_cell", - "wasm-bindgen", - "wasm-bindgen-downcast-macros", -] - -[[package]] -name = "wasm-bindgen-downcast-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5020cfa87c7cecefef118055d44e3c1fc122c7ec25701d528ee458a0b45f38f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.92" @@ -1941,7 +1873,7 @@ dependencies = [ [[package]] name = "wasmer" -version = "4.2.3" +version = "4.2.8" dependencies = [ "bytes", "cfg-if 1.0.0", @@ -1955,8 +1887,8 @@ dependencies = [ "shared-buffer", "target-lexicon", "thiserror", + "tracing", "wasm-bindgen", - "wasm-bindgen-downcast", "wasmer-compiler", "wasmer-compiler-cranelift", "wasmer-derive", @@ -1968,7 +1900,7 @@ dependencies = [ [[package]] name = "wasmer-compiler" -version = "4.2.3" +version = "4.2.8" dependencies = [ "backtrace", "bytes", @@ -1993,7 +1925,7 @@ dependencies = [ [[package]] name = "wasmer-compiler-cranelift" -version = "4.2.3" +version = "4.2.8" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -2010,7 +1942,7 @@ dependencies = [ [[package]] name = "wasmer-compiler-singlepass" -version = "4.2.3" +version = "4.2.8" dependencies = [ "byteorder", "dynasm", @@ -2027,7 +1959,7 @@ dependencies = [ [[package]] name = "wasmer-derive" -version = "4.2.3" +version = "4.2.8" dependencies = [ "proc-macro-error", "proc-macro2", @@ -2037,7 +1969,7 @@ dependencies = [ [[package]] name = "wasmer-types" -version = "4.2.3" +version = "4.2.8" dependencies = [ "bytecheck", "enum-iterator", @@ -2051,7 +1983,7 @@ dependencies = [ [[package]] name = "wasmer-vm" -version = "4.2.3" +version = "4.2.8" dependencies = [ "backtrace", "cc", @@ -2077,12 +2009,13 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.95.0" +version = "0.121.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ea896273ea99b15132414be1da01ab0d8836415083298ecaffbe308eaac87a" +checksum = "9dbe55c8f9d0dbd25d9447a5a889ff90c0cc3feaa7395310d3d826b2c703eaab" dependencies = [ - "indexmap 1.9.3", - "url", + "bitflags 2.5.0", + "indexmap 2.2.5", + "semver", ] [[package]] diff --git a/arbitrator/tools/module_roots/src/main.rs b/arbitrator/tools/module_roots/src/main.rs index eae9bcef2..32e476484 100644 --- a/arbitrator/tools/module_roots/src/main.rs +++ b/arbitrator/tools/module_roots/src/main.rs @@ -1,6 +1,7 @@ // Copyright 2023, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE +use arbutil::Bytes32; use eyre::{Result, WrapErr}; use prover::{machine::GlobalState, utils::file_bytes, Machine}; use std::{collections::HashMap, fmt::Display, path::PathBuf, sync::Arc}; @@ -45,7 +46,10 @@ fn main() -> Result<()> { relocate!(module); 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).wrap_err_with(error)?; + let code = &Bytes32::default(); + let hash = mach + .add_program(&wasm, code, 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/user-host-trait/src/lib.rs b/arbitrator/wasm-libraries/user-host-trait/src/lib.rs index cb5fee5c0..0191718dc 100644 --- a/arbitrator/wasm-libraries/user-host-trait/src/lib.rs +++ b/arbitrator/wasm-libraries/user-host-trait/src/lib.rs @@ -245,8 +245,8 @@ pub trait UserHost: GasMeteredMachine { ret_len: GuestPtr, ) -> Result { let value = Some(value); - let call = |api: &mut Self::A, contract, data: &_, gas, value: Option<_>| { - api.contract_call(contract, data, gas, value.unwrap()) + let call = |api: &mut Self::A, contract, data: &_, left, req, value: Option<_>| { + api.contract_call(contract, data, left, req, value.unwrap()) }; self.do_call(contract, data, data_len, value, gas, ret_len, call, "") } @@ -273,8 +273,9 @@ pub trait UserHost: GasMeteredMachine { gas: u64, ret_len: GuestPtr, ) -> Result { - let call = - |api: &mut Self::A, contract, data: &_, gas, _| api.delegate_call(contract, data, gas); + let call = |api: &mut Self::A, contract, data: &_, left, req, _| { + api.delegate_call(contract, data, left, req) + }; self.do_call( contract, data, data_len, None, gas, ret_len, call, "delegate", ) @@ -302,8 +303,9 @@ pub trait UserHost: GasMeteredMachine { gas: u64, ret_len: GuestPtr, ) -> Result { - let call = - |api: &mut Self::A, contract, data: &_, gas, _| api.static_call(contract, data, gas); + let call = |api: &mut Self::A, contract, data: &_, left, req, _| { + api.static_call(contract, data, left, req) + }; self.do_call(contract, data, data_len, None, gas, ret_len, call, "static") } @@ -315,27 +317,33 @@ pub trait UserHost: GasMeteredMachine { calldata: GuestPtr, calldata_len: u32, value: Option, - mut gas: u64, + gas: u64, return_data_len: GuestPtr, call: F, name: &str, ) -> Result where - F: FnOnce(&mut Self::A, Address, &[u8], u64, Option) -> (u32, u64, UserOutcomeKind), + F: FnOnce( + &mut Self::A, + Address, + &[u8], + u64, + u64, + Option, + ) -> (u32, u64, UserOutcomeKind), { self.buy_ink(HOSTIO_INK + 3 * PTR_INK + EVM_API_INK)?; self.pay_for_read(calldata_len)?; self.pay_for_geth_bytes(calldata_len)?; - let gas_passed = gas; - gas = gas.min(self.gas_left()?); // provide no more than what the user has - + let gas_left = self.gas_left()?; + let gas_req = gas.min(gas_left); let contract = self.read_bytes20(contract)?; let input = self.read_slice(calldata, calldata_len)?; let value = value.map(|x| self.read_bytes32(x)).transpose()?; let api = self.evm_api(); - let (outs_len, gas_cost, status) = call(api, contract, &input, gas, value); + let (outs_len, gas_cost, status) = call(api, contract, &input, gas_left, gas_req, value); self.buy_gas(gas_cost)?; *self.evm_return_data_len() = outs_len; self.write_u32(return_data_len, outs_len)?; @@ -348,7 +356,7 @@ pub trait UserHost: GasMeteredMachine { return trace!( &name, self, - [contract, be!(gas_passed), value, &input], + [contract, be!(gas), value, &input], [be!(outs_len), be!(status)], status ); diff --git a/arbitrator/wasm-libraries/user-host/src/link.rs b/arbitrator/wasm-libraries/user-host/src/link.rs index efc535a2c..428611167 100644 --- a/arbitrator/wasm-libraries/user-host/src/link.rs +++ b/arbitrator/wasm-libraries/user-host/src/link.rs @@ -42,27 +42,29 @@ pub unsafe extern "C" fn programs__activate( wasm_size: usize, pages_ptr: GuestPtr, asm_estimate_ptr: GuestPtr, - init_gas_ptr: GuestPtr, - cached_init_gas_ptr: GuestPtr, + init_cost_ptr: GuestPtr, + cached_init_cost_ptr: GuestPtr, version: u16, debug: u32, + codehash: GuestPtr, module_hash_ptr: GuestPtr, gas_ptr: GuestPtr, err_buf: GuestPtr, err_buf_len: usize, ) -> usize { let wasm = STATIC_MEM.read_slice(wasm_ptr, wasm_size); + let codehash = &read_bytes32(codehash); let debug = debug != 0; let page_limit = STATIC_MEM.read_u16(pages_ptr); let gas_left = &mut STATIC_MEM.read_u64(gas_ptr); - match Module::activate(&wasm, version, page_limit, debug, gas_left) { + match Module::activate(&wasm, codehash, version, page_limit, debug, gas_left) { Ok((module, data)) => { STATIC_MEM.write_u64(gas_ptr, *gas_left); STATIC_MEM.write_u16(pages_ptr, data.footprint); STATIC_MEM.write_u32(asm_estimate_ptr, data.asm_estimate); - STATIC_MEM.write_u16(init_gas_ptr, data.init_gas); - STATIC_MEM.write_u16(cached_init_gas_ptr, data.cached_init_gas); + STATIC_MEM.write_u16(init_cost_ptr, data.init_cost); + STATIC_MEM.write_u16(cached_init_cost_ptr, data.cached_init_cost); STATIC_MEM.write_slice(module_hash_ptr, module.hash().as_slice()); 0 } @@ -73,8 +75,8 @@ pub unsafe extern "C" fn programs__activate( STATIC_MEM.write_u64(gas_ptr, 0); STATIC_MEM.write_u16(pages_ptr, 0); STATIC_MEM.write_u32(asm_estimate_ptr, 0); - STATIC_MEM.write_u16(init_gas_ptr, 0); - STATIC_MEM.write_u16(cached_init_gas_ptr, 0); + STATIC_MEM.write_u16(init_cost_ptr, 0); + STATIC_MEM.write_u16(cached_init_cost_ptr, 0); STATIC_MEM.write_slice(module_hash_ptr, Bytes32::default().as_slice()); err_bytes.len() } diff --git a/arbitrator/wasm-libraries/user-test/src/program.rs b/arbitrator/wasm-libraries/user-test/src/program.rs index 1581082bf..c56ea52ad 100644 --- a/arbitrator/wasm-libraries/user-test/src/program.rs +++ b/arbitrator/wasm-libraries/user-test/src/program.rs @@ -130,7 +130,8 @@ impl EvmApi for MockEvmApi { &mut self, _contract: Bytes20, _calldata: &[u8], - _gas: u64, + _gas_left: u64, + _gas_req: u64, _value: Bytes32, ) -> (u32, u64, UserOutcomeKind) { unimplemented!() @@ -140,7 +141,8 @@ impl EvmApi for MockEvmApi { &mut self, _contract: Bytes20, _calldata: &[u8], - _gas: u64, + _gas_left: u64, + _gas_req: u64, ) -> (u32, u64, UserOutcomeKind) { unimplemented!() } @@ -149,7 +151,8 @@ impl EvmApi for MockEvmApi { &mut self, _contract: Bytes20, _calldata: &[u8], - _gas: u64, + _gas_left: u64, + _gas_req: u64, ) -> (u32, u64, UserOutcomeKind) { unimplemented!() } diff --git a/arbos/programs/api.go b/arbos/programs/api.go index c572c0be0..05fb4b2ce 100644 --- a/arbos/programs/api.go +++ b/arbos/programs/api.go @@ -110,7 +110,7 @@ func newApiClosures( return Success } doCall := func( - contract common.Address, opcode vm.OpCode, input []byte, gas uint64, value *big.Int, + contract common.Address, opcode vm.OpCode, input []byte, gasLeft, gasReq uint64, value *big.Int, ) ([]byte, uint64, error) { // This closure can perform each kind of contract call based on the opcode passed in. // The implementation for each should match that of the EVM. @@ -127,18 +127,15 @@ func newApiClosures( return nil, 0, vm.ErrWriteProtection } - startGas := gas - // computes makeCallVariantGasCallEIP2929 and gasCall/gasDelegateCall/gasStaticCall - baseCost, err := vm.WasmCallCost(db, contract, value, startGas) + baseCost, err := vm.WasmCallCost(db, contract, value, gasLeft) if err != nil { - return nil, gas, err + return nil, gasLeft, err } - gas -= baseCost // apply the 63/64ths rule - one64th := gas / 64 - gas -= one64th + startGas := am.SaturatingUSub(gasLeft, baseCost) * 63 / 64 + gas := am.MinInt(startGas, gasReq) // Tracing: emit the call (value transfer is done later in evm.Call) if tracingInfo != nil { @@ -165,7 +162,7 @@ func newApiClosures( } interpreter.SetReturnData(ret) - cost := arbmath.SaturatingUSub(startGas, returnGas+one64th) // user gets 1/64th back + cost := am.SaturatingUAdd(baseCost, am.SaturatingUSub(gas, returnGas)) return ret, cost, err } create := func(code []byte, endowment, salt *big.Int, gas uint64) (common.Address, []byte, uint64, error) { @@ -352,10 +349,11 @@ func newApiClosures( } contract := takeAddress() value := takeU256() - gas := takeU64() + gasLeft := takeU64() + gasReq := takeU64() calldata := takeRest() - ret, cost, err := doCall(contract, opcode, calldata, gas, value) + ret, cost, err := doCall(contract, opcode, calldata, gasLeft, gasReq, value) statusByte := byte(0) if err != nil { statusByte = 2 // TODO: err value diff --git a/arbos/programs/native.go b/arbos/programs/native.go index 1d6d90d2c..123dda54c 100644 --- a/arbos/programs/native.go +++ b/arbos/programs/native.go @@ -46,6 +46,7 @@ type rustSlice = C.RustSlice func activateProgram( db vm.StateDB, program common.Address, + codehash common.Hash, wasm []byte, page_limit uint16, version uint16, @@ -56,6 +57,7 @@ func activateProgram( asmLen := usize(0) moduleHash := &bytes32{} stylusData := &C.StylusData{} + codeHash := hashToBytes32(codehash) status := userStatus(C.stylus_activate( goSlice(wasm), @@ -64,6 +66,7 @@ func activateProgram( cbool(debug), output, &asmLen, + &codeHash, moduleHash, stylusData, (*u64)(burner.GasLeft()), @@ -87,8 +90,8 @@ func activateProgram( info := &activationInfo{ moduleHash: hash, - initGas: uint16(stylusData.init_gas), - cachedInitGas: uint16(stylusData.cached_init_gas), + initGas: uint16(stylusData.init_cost), + cachedInitGas: uint16(stylusData.cached_init_cost), asmEstimate: uint32(stylusData.asm_estimate), footprint: uint16(stylusData.footprint), } diff --git a/arbos/programs/params.go b/arbos/programs/params.go index 6aae2ac02..6138e3603 100644 --- a/arbos/programs/params.go +++ b/arbos/programs/params.go @@ -21,14 +21,17 @@ const InitialPageGas = 1000 // linear cost per allocation. const initialPageRamp = 620674314 // targets 8MB costing 32 million gas, minus the linear term. const initialPageLimit = 128 // reject wasms with memories larger than 8MB. const initialInkPrice = 10000 // 1 evm gas buys 10k ink. -const initialMinInitGas = 0 // assume pricer is correct (update in case of emergency) -const initialMinCachedInitGas = 0 // assume pricer is correct (update in case of emergency) +const initialMinInitGas = 72 // charge 72 * 128 = 9216 gas. +const initialMinCachedGas = 11 // charge 11 * 32 = 352 gas. +const initialInitCostScalar = 50 // scale costs 1:1 (100%) +const initialCachedCostScalar = 50 // scale costs 1:1 (100%) const initialExpiryDays = 365 // deactivate after 1 year. -const initialKeepaliveDays = 31 // wait a month before allowing reactivation -const initialRecentCacheSize = 32 // cache the 32 most recent programs +const initialKeepaliveDays = 31 // wait a month before allowing reactivation. +const initialRecentCacheSize = 32 // cache the 32 most recent programs. -const minCachedInitGasUnits = 64 -const minInitGasUnits = 256 +const MinCachedGasUnits = 32 /// 32 gas for each unit +const MinInitGasUnits = 128 // 128 gas for each unit +const CostScalarPercent = 2 // 2% for each unit // This struct exists to collect the many Stylus configuration parameters into a single word. // The items here must only be modified in ArbOwner precompile methods (or in ArbOS upgrades). @@ -41,8 +44,10 @@ type StylusParams struct { PageGas uint16 PageRamp uint64 PageLimit uint16 - MinInitGas uint8 // measured in 256-gas increments - MinCachedInitGas uint8 // measured in 64-gas increments + MinInitGas uint8 // measured in 128-gas increments + MinCachedInitGas uint8 // measured in 32-gas increments + InitCostScalar uint8 // measured in 2% increments + CachedCostScalar uint8 // measured in 2% increments ExpiryDays uint16 KeepaliveDays uint16 BlockCacheSize uint16 @@ -80,10 +85,12 @@ func (p Programs) Params() (*StylusParams, error) { MaxStackDepth: am.BytesToUint32(take(4)), FreePages: am.BytesToUint16(take(2)), PageGas: am.BytesToUint16(take(2)), - PageRamp: am.BytesToUint(take(8)), + PageRamp: initialPageRamp, PageLimit: am.BytesToUint16(take(2)), MinInitGas: am.BytesToUint8(take(1)), MinCachedInitGas: am.BytesToUint8(take(1)), + InitCostScalar: am.BytesToUint8(take(1)), + CachedCostScalar: am.BytesToUint8(take(1)), ExpiryDays: am.BytesToUint16(take(2)), KeepaliveDays: am.BytesToUint16(take(2)), BlockCacheSize: am.BytesToUint16(take(2)), @@ -104,10 +111,11 @@ func (p *StylusParams) Save() error { am.Uint32ToBytes(p.MaxStackDepth), am.Uint16ToBytes(p.FreePages), am.Uint16ToBytes(p.PageGas), - am.UintToBytes(p.PageRamp), am.Uint16ToBytes(p.PageLimit), am.Uint8ToBytes(p.MinInitGas), am.Uint8ToBytes(p.MinCachedInitGas), + am.Uint8ToBytes(p.InitCostScalar), + am.Uint8ToBytes(p.CachedCostScalar), am.Uint16ToBytes(p.ExpiryDays), am.Uint16ToBytes(p.KeepaliveDays), am.Uint16ToBytes(p.BlockCacheSize), @@ -140,7 +148,9 @@ func initStylusParams(sto *storage.Storage) { PageRamp: initialPageRamp, PageLimit: initialPageLimit, MinInitGas: initialMinInitGas, - MinCachedInitGas: initialMinCachedInitGas, + MinCachedInitGas: initialMinCachedGas, + InitCostScalar: initialInitCostScalar, + CachedCostScalar: initialCachedCostScalar, ExpiryDays: initialExpiryDays, KeepaliveDays: initialKeepaliveDays, BlockCacheSize: initialRecentCacheSize, diff --git a/arbos/programs/programs.go b/arbos/programs/programs.go index bb686a840..e7a65aa8a 100644 --- a/arbos/programs/programs.go +++ b/arbos/programs/programs.go @@ -18,7 +18,7 @@ import ( "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/arbos/util" "github.com/offchainlabs/nitro/arbutil" - "github.com/offchainlabs/nitro/util/arbmath" + am "github.com/offchainlabs/nitro/util/arbmath" ) type Programs struct { @@ -31,8 +31,8 @@ type Programs struct { type Program struct { version uint16 - initGas uint16 - cachedInitGas uint16 + initCost uint16 + cachedCost uint16 footprint uint16 asmEstimateKb uint24 // Predicted size of the asm activatedAt uint24 // Hours since Arbitrum began @@ -40,7 +40,7 @@ type Program struct { cached bool } -type uint24 = arbmath.Uint24 +type uint24 = am.Uint24 var paramsKey = []byte{0} var programDataKey = []byte{1} @@ -113,9 +113,9 @@ func (p Programs) ActivateProgram(evm *vm.EVM, address common.Address, runMode c } // require the program's footprint not exceed the remaining memory budget - pageLimit := arbmath.SaturatingUSub(params.PageLimit, statedb.GetStylusPagesOpen()) + pageLimit := am.SaturatingUSub(params.PageLimit, statedb.GetStylusPagesOpen()) - info, err := activateProgram(statedb, address, wasm, pageLimit, stylusVersion, debugMode, burner) + info, err := activateProgram(statedb, address, codeHash, wasm, pageLimit, stylusVersion, debugMode, burner) if err != nil { return 0, codeHash, common.Hash{}, nil, true, err } @@ -133,7 +133,7 @@ func (p Programs) ActivateProgram(evm *vm.EVM, address common.Address, runMode c return 0, codeHash, common.Hash{}, nil, true, err } - estimateKb, err := arbmath.IntToUint24(arbmath.DivCeil(info.asmEstimate, 1024)) // stored in kilobytes + estimateKb, err := am.IntToUint24(am.DivCeil(info.asmEstimate, 1024)) // stored in kilobytes if err != nil { return 0, codeHash, common.Hash{}, nil, true, err } @@ -145,8 +145,8 @@ func (p Programs) ActivateProgram(evm *vm.EVM, address common.Address, runMode c programData := Program{ version: stylusVersion, - initGas: info.initGas, - cachedInitGas: info.cachedInitGas, + initCost: info.initGas, + cachedCost: info.cachedInitGas, footprint: info.footprint, asmEstimateKb: estimateKb, activatedAt: hoursSinceArbitrum(time), @@ -195,11 +195,9 @@ func (p Programs) CallProgram( // pay for program init cached := program.cached || statedb.GetRecentWasms().Insert(codeHash, params.BlockCacheSize) if cached { - callCost = arbmath.SaturatingUAdd(callCost, minCachedInitGasUnits*uint64(params.MinCachedInitGas)) - callCost = arbmath.SaturatingUAdd(callCost, uint64(program.cachedInitGas)) + callCost = am.SaturatingUAdd(callCost, program.cachedGas(params)) } else { - callCost = arbmath.SaturatingUAdd(callCost, minInitGasUnits*uint64(params.MinInitGas)) - callCost = arbmath.SaturatingUAdd(callCost, uint64(program.initGas)) + callCost = am.SaturatingUAdd(callCost, program.initGas(params)) } if err := contract.BurnGas(callCost); err != nil { return nil, err @@ -220,7 +218,7 @@ func (p Programs) CallProgram( msgValue: common.BigToHash(scope.Contract.Value()), txGasPrice: common.BigToHash(evm.TxContext.GasPrice), txOrigin: evm.TxContext.Origin, - reentrant: arbmath.BoolToUint32(reentrant), + reentrant: am.BoolToUint32(reentrant), cached: program.cached, tracing: tracingInfo != nil, } @@ -258,13 +256,13 @@ func getWasm(statedb vm.StateDB, program common.Address) ([]byte, error) { func (p Programs) getProgram(codeHash common.Hash, time uint64) (Program, error) { data, err := p.programs.Get(codeHash) program := Program{ - version: arbmath.BytesToUint16(data[:2]), - initGas: arbmath.BytesToUint16(data[2:4]), - cachedInitGas: arbmath.BytesToUint16(data[4:6]), - footprint: arbmath.BytesToUint16(data[6:8]), - activatedAt: arbmath.BytesToUint24(data[8:11]), - asmEstimateKb: arbmath.BytesToUint24(data[11:14]), - cached: arbmath.BytesToBool(data[14:15]), + version: am.BytesToUint16(data[:2]), + initCost: am.BytesToUint16(data[2:4]), + cachedCost: am.BytesToUint16(data[4:6]), + footprint: am.BytesToUint16(data[6:8]), + activatedAt: am.BytesToUint24(data[8:11]), + asmEstimateKb: am.BytesToUint24(data[11:14]), + cached: am.BytesToBool(data[14:15]), } program.ageSeconds = hoursToAge(time, program.activatedAt) return program, err @@ -287,7 +285,7 @@ func (p Programs) getActiveProgram(codeHash common.Hash, time uint64, params *St } // ensure the program hasn't expired - if program.ageSeconds > arbmath.DaysToSeconds(params.ExpiryDays) { + if program.ageSeconds > am.DaysToSeconds(params.ExpiryDays) { return program, ProgramExpiredError(program.ageSeconds) } return program, nil @@ -295,13 +293,13 @@ func (p Programs) getActiveProgram(codeHash common.Hash, time uint64, params *St func (p Programs) setProgram(codehash common.Hash, program Program) error { data := common.Hash{} - copy(data[0:], arbmath.Uint16ToBytes(program.version)) - copy(data[2:], arbmath.Uint16ToBytes(program.initGas)) - copy(data[4:], arbmath.Uint16ToBytes(program.cachedInitGas)) - copy(data[6:], arbmath.Uint16ToBytes(program.footprint)) - copy(data[8:], arbmath.Uint24ToBytes(program.activatedAt)) - copy(data[11:], arbmath.Uint24ToBytes(program.asmEstimateKb)) - copy(data[14:], arbmath.BoolToBytes(program.cached)) + copy(data[0:], am.Uint16ToBytes(program.version)) + copy(data[2:], am.Uint16ToBytes(program.initCost)) + copy(data[4:], am.Uint16ToBytes(program.cachedCost)) + copy(data[6:], am.Uint16ToBytes(program.footprint)) + copy(data[8:], am.Uint24ToBytes(program.activatedAt)) + copy(data[11:], am.Uint24ToBytes(program.asmEstimateKb)) + copy(data[14:], am.BoolToBytes(program.cached)) return p.programs.Set(codehash, data) } @@ -311,7 +309,7 @@ func (p Programs) programExists(codeHash common.Hash, time uint64, params *Stylu return 0, false, false, err } activatedAt := program.activatedAt - expired := activatedAt == 0 || hoursToAge(time, activatedAt) > arbmath.DaysToSeconds(params.ExpiryDays) + expired := activatedAt == 0 || hoursToAge(time, activatedAt) > am.DaysToSeconds(params.ExpiryDays) return program.version, expired, program.cached, err } @@ -320,7 +318,7 @@ func (p Programs) ProgramKeepalive(codeHash common.Hash, time uint64, params *St if err != nil { return nil, err } - if program.ageSeconds < arbmath.DaysToSeconds(params.KeepaliveDays) { + if program.ageSeconds < am.DaysToSeconds(params.KeepaliveDays) { return nil, ProgramKeepaliveTooSoon(program.ageSeconds) } @@ -340,7 +338,7 @@ func (p Programs) ProgramKeepalive(codeHash common.Hash, time uint64, params *St // Gets whether a program is cached. Note that the program may be expired. func (p Programs) ProgramCached(codeHash common.Hash) (bool, error) { data, err := p.programs.Get(codeHash) - return arbmath.BytesToBool(data[14:15]), err + return am.BytesToBool(data[14:15]), err } // Sets whether a program is cached. Errors if trying to cache an expired program. @@ -358,7 +356,7 @@ func (p Programs) SetProgramCached( if err != nil { return err } - expired := program.ageSeconds > arbmath.DaysToSeconds(params.ExpiryDays) + expired := program.ageSeconds > am.DaysToSeconds(params.ExpiryDays) if program.version == 0 && cache { return ProgramNeedsUpgradeError(0, params.Version) @@ -374,7 +372,7 @@ func (p Programs) SetProgramCached( } // pay to cache the program, or to re-cache in case of upcoming revert - if err := p.programs.Burner().Burn(uint64(program.initGas)); err != nil { + if err := p.programs.Burner().Burn(uint64(program.initCost)); err != nil { return err } moduleHash, err := p.moduleHashes.Get(codeHash) @@ -405,16 +403,16 @@ func (p Programs) ProgramTimeLeft(codeHash common.Hash, time uint64, params *Sty return 0, err } age := hoursToAge(time, program.activatedAt) - expirySeconds := arbmath.DaysToSeconds(params.ExpiryDays) + expirySeconds := am.DaysToSeconds(params.ExpiryDays) if age > expirySeconds { return 0, ProgramExpiredError(age) } - return arbmath.SaturatingUSub(expirySeconds, age), nil + return am.SaturatingUSub(expirySeconds, age), nil } -func (p Programs) ProgramInitGas(codeHash common.Hash, time uint64, params *StylusParams) (uint16, uint16, error) { +func (p Programs) ProgramInitGas(codeHash common.Hash, time uint64, params *StylusParams) (uint64, uint64, error) { program, err := p.getActiveProgram(codeHash, time, params) - return program.initGas, program.cachedInitGas, err + return program.initGas(params), program.cachedGas(params), err } func (p Programs) ProgramMemoryFootprint(codeHash common.Hash, time uint64, params *StylusParams) (uint16, error) { @@ -431,7 +429,19 @@ func (p Programs) ProgramAsmSize(codeHash common.Hash, time uint64, params *Styl } func (p Program) asmSize() uint32 { - return arbmath.SaturatingUMul(p.asmEstimateKb.ToUint32(), 1024) + return am.SaturatingUMul(p.asmEstimateKb.ToUint32(), 1024) +} + +func (p Program) initGas(params *StylusParams) uint64 { + base := uint64(params.MinInitGas) * MinInitGasUnits + dyno := am.SaturatingUMul(uint64(p.initCost), uint64(params.InitCostScalar)*CostScalarPercent) + return am.SaturatingUAdd(base, am.DivCeil(dyno, 100)) +} + +func (p Program) cachedGas(params *StylusParams) uint64 { + base := uint64(params.MinCachedInitGas) * MinCachedGasUnits + dyno := am.SaturatingUMul(uint64(p.cachedCost), uint64(params.CachedCostScalar)*CostScalarPercent) + return am.SaturatingUAdd(base, am.DivCeil(dyno, 100)) } type goParams struct { @@ -512,7 +522,7 @@ func hoursSinceArbitrum(time uint64) uint24 { // Computes program age in seconds from the hours passed since Arbitrum began. func hoursToAge(time uint64, hours uint24) uint64 { - seconds := arbmath.SaturatingUMul(uint64(hours), 3600) - activatedAt := arbmath.SaturatingUAdd(lastUpdateTimeOffset, seconds) - return arbmath.SaturatingUSub(time, activatedAt) + seconds := am.SaturatingUMul(uint64(hours), 3600) + activatedAt := am.SaturatingUAdd(lastUpdateTimeOffset, seconds) + return am.SaturatingUSub(time, activatedAt) } diff --git a/arbos/programs/wasm.go b/arbos/programs/wasm.go index 44b709716..77eb7e0f2 100644 --- a/arbos/programs/wasm.go +++ b/arbos/programs/wasm.go @@ -46,6 +46,7 @@ func programActivate( cached_init_gas_ptr unsafe.Pointer, version uint32, debug uint32, + codehash unsafe.Pointer, module_hash_ptr unsafe.Pointer, gas_ptr unsafe.Pointer, err_buf unsafe.Pointer, @@ -55,6 +56,7 @@ func programActivate( func activateProgram( db vm.StateDB, program addr, + codehash common.Hash, wasm []byte, pageLimit u16, version u16, @@ -79,6 +81,7 @@ func activateProgram( unsafe.Pointer(&cachedInitGas), uint32(version), debugMode, + arbutil.SliceToUnsafePointer(codehash[:]), arbutil.SliceToUnsafePointer(moduleHash[:]), unsafe.Pointer(gasPtr), arbutil.SliceToUnsafePointer(errBuf), diff --git a/contracts b/contracts index e3725f7df..a51e769b5 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit e3725f7dfe625248be2824e0e92aaf7b5d4164d5 +Subproject commit a51e769b59bec60556ce1bc317d665843621ef03 diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 3f6c3cd49..bf10fd99f 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -11,7 +11,9 @@ import ( "math/big" "github.com/offchainlabs/nitro/arbos/l1pricing" + "github.com/offchainlabs/nitro/arbos/programs" "github.com/offchainlabs/nitro/util/arbmath" + am "github.com/offchainlabs/nitro/util/arbmath" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" @@ -211,34 +213,34 @@ func (con ArbOwner) SetWasmPageGas(c ctx, evm mech, gas uint16) error { return params.Save() } -// Sets the ramp that drives exponential wasm memory costs -func (con ArbOwner) SetWasmPageRamp(c ctx, evm mech, ramp uint64) error { +// Sets the initial number of pages a wasm may allocate +func (con ArbOwner) SetWasmPageLimit(c ctx, evm mech, limit uint16) error { params, err := c.State.Programs().Params() if err != nil { return err } - params.PageRamp = ramp + params.PageLimit = limit return params.Save() } -// Sets the initial number of pages a wasm may allocate -func (con ArbOwner) SetWasmPageLimit(c ctx, evm mech, limit uint16) error { +// Sets the minimum costs to invoke a program +func (con ArbOwner) SetWasmMinInitGas(c ctx, _ mech, gas, cached uint64) error { params, err := c.State.Programs().Params() if err != nil { return err } - params.PageLimit = limit + params.MinInitGas = am.SaturatingUUCast[uint8](am.DivCeil(gas, programs.MinInitGasUnits)) + params.MinCachedInitGas = am.SaturatingUUCast[uint8](am.DivCeil(cached, programs.MinCachedGasUnits)) return params.Save() } -// Sets the minimum costs to invoke a program -func (con ArbOwner) SetWasmMinInitGas(c ctx, _ mech, gas, cached uint8) error { +// Sets the linear adjustment made to program init costs +func (con ArbOwner) SetWasmInitCostScalar(c ctx, _ mech, percent uint64) error { params, err := c.State.Programs().Params() if err != nil { return err } - params.MinInitGas = gas - params.MinCachedInitGas = cached + params.InitCostScalar = am.SaturatingUUCast[uint8](am.DivCeil(percent, programs.CostScalarPercent)) return params.Save() } diff --git a/precompiles/ArbWasm.go b/precompiles/ArbWasm.go index 8ab64345f..9f42cacb5 100644 --- a/precompiles/ArbWasm.go +++ b/precompiles/ArbWasm.go @@ -129,9 +129,17 @@ func (con ArbWasm) PageLimit(c ctx, _ mech) (uint16, error) { } // Gets the minimum costs to invoke a program -func (con ArbWasm) MinInitGas(c ctx, _ mech) (uint8, uint8, error) { +func (con ArbWasm) MinInitGas(c ctx, _ mech) (uint64, uint64, error) { params, err := c.State.Programs().Params() - return params.MinInitGas, params.MinCachedInitGas, err + init := uint64(params.MinInitGas) * programs.MinInitGasUnits + cached := uint64(params.MinCachedInitGas) * programs.MinCachedGasUnits + return init, cached, err +} + +// Gets the linear adjustment made to program init costs +func (con ArbWasm) InitCostScalar(c ctx, _ mech) (uint64, error) { + params, err := c.State.Programs().Params() + return uint64(params.InitCostScalar) * programs.CostScalarPercent, err } // Gets the number of days after which programs deactivate @@ -179,8 +187,8 @@ func (con ArbWasm) ProgramVersion(c ctx, evm mech, program addr) (uint16, error) return con.CodehashVersion(c, evm, codehash) } -// Gets the cost to invoke the program (not including MinInitGas) -func (con ArbWasm) ProgramInitGas(c ctx, evm mech, program addr) (uint16, uint16, error) { +// Gets the cost to invoke the program +func (con ArbWasm) ProgramInitGas(c ctx, evm mech, program addr) (uint64, uint64, error) { codehash, params, err := con.getCodeHash(c, program) if err != nil { return 0, 0, err diff --git a/precompiles/precompile.go b/precompiles/precompile.go index eb63c6143..c39f2bcb6 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -612,8 +612,9 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwner.methodsByName["SetChainConfig"].arbosVersion = 11 ArbOwner.methodsByName["SetBrotliCompressionLevel"].arbosVersion = 20 stylusMethods := []string{ - "SetInkPrice", "SetWasmMaxStackDepth", "SetWasmFreePages", "SetWasmPageGas", "SetWasmPageRamp", - "SetWasmPageLimit", "SetWasmMinInitGas", "SetWasmExpiryDays", "SetWasmKeepaliveDays", + "SetInkPrice", "SetWasmMaxStackDepth", "SetWasmFreePages", "SetWasmPageGas", + "SetWasmPageLimit", "SetWasmMinInitGas", "SetWasmInitCostScalar", + "SetWasmExpiryDays", "SetWasmKeepaliveDays", "SetWasmBlockCacheSize", "AddWasmCacheManager", "RemoveWasmCacheManager", } for _, method := range stylusMethods { diff --git a/precompiles/precompile_test.go b/precompiles/precompile_test.go index 6284a8658..24bc11a70 100644 --- a/precompiles/precompile_test.go +++ b/precompiles/precompile_test.go @@ -191,7 +191,7 @@ func TestPrecompilesPerArbosVersion(t *testing.T) { 10: 2, 11: 4, 20: 8, - 30: 37, + 30: 38, } precompiles := Precompiles() diff --git a/system_tests/program_test.go b/system_tests/program_test.go index ab7185926..8ce788c4b 100644 --- a/system_tests/program_test.go +++ b/system_tests/program_test.go @@ -941,7 +941,7 @@ func testActivateFails(t *testing.T, jit bool) { arbWasm, err := pgen.NewArbWasm(types.ArbWasmAddress, l2client) Require(t, err) - badExportWasm, _ := readWasmFile(t, watFile("bad-export")) + badExportWasm, _ := readWasmFile(t, watFile("bad-mods/bad-export")) auth.GasLimit = 32000000 // skip gas estimation badExportAddr := deployContract(t, ctx, auth, l2client, badExportWasm) @@ -1194,9 +1194,9 @@ func TestProgramCacheManager(t *testing.T) { assert(arbWasmCache.CodehashIsCached(nil, codehash)) // compare gas costs - keccak := func() uint16 { + keccak := func() uint64 { tx := l2info.PrepareTxTo("Owner", &program, 1e9, nil, []byte{0x00}) - return uint16(ensure(tx, l2client.SendTransaction(ctx, tx)).GasUsedForL2()) + return ensure(tx, l2client.SendTransaction(ctx, tx)).GasUsedForL2() } ensure(mock.EvictProgram(&userAuth, program)) miss := keccak() @@ -1276,6 +1276,7 @@ func setupProgramTest(t *testing.T, jit bool) ( } func readWasmFile(t *testing.T, file string) ([]byte, []byte) { + t.Helper() name := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)) source, err := os.ReadFile(file) Require(t, err)