diff --git a/fvm/Cargo.toml b/fvm/Cargo.toml index a442bd730..02392cc35 100644 --- a/fvm/Cargo.toml +++ b/fvm/Cargo.toml @@ -39,6 +39,7 @@ log = "0.4.14" byteorder = "1.4.3" anymap = "0.12.1" blake2b_simd = "1.0.0" +fvm-wasm-instrument = { version = "0.2.0", features = ["bulk"] } [dependencies.wasmtime] version = "0.35.2" diff --git a/fvm/src/call_manager/default.rs b/fvm/src/call_manager/default.rs index 9798e0a7d..838720cf8 100644 --- a/fvm/src/call_manager/default.rs +++ b/fvm/src/call_manager/default.rs @@ -1,5 +1,3 @@ -use std::cmp::max; - use anyhow::Context; use derive_more::{Deref, DerefMut}; use fvm_ipld_encoding::{RawBytes, DAG_CBOR}; @@ -7,9 +5,9 @@ use fvm_shared::actor::builtin::Type; use fvm_shared::address::{Address, Protocol}; use fvm_shared::econ::TokenAmount; use fvm_shared::error::{ErrorNumber, ExitCode}; -use fvm_shared::version::NetworkVersion; use fvm_shared::{ActorID, MethodNum, METHOD_SEND}; use num_traits::Zero; +use wasmtime::Val; use super::{Backtrace, CallManager, InvocationResult, NO_DATA_BLOCK_ID}; use crate::call_manager::backtrace::Frame; @@ -316,7 +314,6 @@ where // it returns a referenced copy. let engine = self.engine().clone(); - let gas_available = self.gas_tracker.gas_available(); log::trace!("calling {} -> {}::{}", from, to, method); self.map_mut(|cm| { // Make the kernel. @@ -334,21 +331,7 @@ where }; // Make a store. - let gas_used = kernel.gas_used(); - let exec_units_to_add = match kernel.network_version() { - NetworkVersion::V14 | NetworkVersion::V15 => i64::MAX, - _ => kernel - .price_list() - .gas_to_exec_units(max(gas_available.saturating_sub(gas_used), 0), false), - }; - let mut store = engine.new_store(kernel); - if let Err(err) = store.add_fuel(u64::try_from(exec_units_to_add).unwrap_or(0)) { - return ( - Err(ExecutionError::Fatal(err)), - store.into_data().kernel.into_call_manager(), - ); - } // Instantiate the module. let instance = match engine @@ -362,6 +345,26 @@ where // From this point on, there are no more syscall errors, only aborts. let result: std::result::Result = (|| { + let initial_milligas = + store + .data_mut() + .kernel + .borrow_milligas() + .map_err(|e| match e { + ExecutionError::OutOfGas => Abort::OutOfGas, + ExecutionError::Fatal(m) => Abort::Fatal(m), + _ => Abort::Fatal(anyhow::Error::msg( + "setting available gas on entering wasm", + )), + })?; + + store + .data() + .avail_gas_global + .clone() + .set(&mut store, Val::I64(initial_milligas)) + .map_err(Abort::Fatal)?; + // Lookup the invoke method. let invoke: wasmtime::TypedFunc<(u32,), u32> = instance .get_typed_func(&mut store, "invoke") @@ -371,19 +374,26 @@ where // Invoke it. let res = invoke.call(&mut store, (param_id,)); - // Charge gas for the "latest" use of execution units (all the exec units used since the most recent syscall) - // We do this by first loading the _total_ execution units consumed - let exec_units_consumed = store - .fuel_consumed() - .context("expected to find fuel consumed") - .map_err(Abort::Fatal)?; - // Then, pass the _total_ exec_units_consumed to the InvocationData, - // which knows how many execution units had been consumed at the most recent snapshot - // It will charge gas for the delta between the total units (the number we provide) and its snapshot + // Return gas tracking to GasTracker + let available_milligas = + match store.data_mut().avail_gas_global.clone().get(&mut store) { + Val::I64(g) => Ok(g), + _ => Err(Abort::Fatal(anyhow::Error::msg( + "failed to get available gas from wasm after returning", + ))), + }?; + store .data_mut() - .charge_gas_for_exec_units(exec_units_consumed) - .map_err(|e| Abort::from_error(ExitCode::SYS_ASSERTION_FAILED, e))?; + .kernel + .return_milligas("wasm_exec_last", available_milligas) + .map_err(|e| match e { + ExecutionError::OutOfGas => Abort::OutOfGas, + ExecutionError::Fatal(m) => Abort::Fatal(m), + _ => Abort::Fatal(anyhow::Error::msg( + "setting available gas on existing wasm", + )), + })?; // If the invocation failed due to running out of exec_units, we have already detected it and returned OutOfGas above. // Any other invocation failure is returned here as an Abort diff --git a/fvm/src/gas/mod.rs b/fvm/src/gas/mod.rs index b140fa2aa..dc462b8cf 100644 --- a/fvm/src/gas/mod.rs +++ b/fvm/src/gas/mod.rs @@ -3,61 +3,136 @@ pub use self::charge::GasCharge; pub(crate) use self::outputs::GasOutputs; -pub use self::price_list::{price_list_by_network_version, PriceList}; +pub use self::price_list::{price_list_by_network_version, PriceList, WasmGasPrices}; use crate::kernel::{ExecutionError, Result}; mod charge; mod outputs; mod price_list; +pub const MILLIGAS_PRECISION: i64 = 1000; + pub struct GasTracker { - gas_available: i64, - gas_used: i64, + milligas_limit: i64, + milligas_used: i64, + + /// A flag indicating whether this GasTracker is currently responsible for + /// gas accounting. A 'false' value indicates that gas accounting is + /// handled somewhere else (eg. in wasm execution). + /// + /// Creating gas charges is only allowed when own_limit is true. + own_limit: bool, } impl GasTracker { - pub fn new(gas_available: i64, gas_used: i64) -> Self { + pub fn new(gas_limit: i64, gas_used: i64) -> Self { Self { - gas_available, - gas_used, + milligas_limit: gas_to_milligas(gas_limit), + milligas_used: gas_to_milligas(gas_used), + own_limit: true, } } /// Safely consumes gas and returns an out of gas error if there is not sufficient /// enough gas remaining for charge. - pub fn charge_gas(&mut self, charge: GasCharge) -> Result<()> { - let to_use = charge.total(); - match self.gas_used.checked_add(to_use) { + fn charge_milligas(&mut self, name: &str, to_use: i64) -> Result<()> { + if !self.own_limit { + return Err(ExecutionError::Fatal(anyhow::Error::msg( + "charge_gas called when gas_limit owned by execution", + ))); + } + + match self.milligas_used.checked_add(to_use) { None => { - log::trace!("gas overflow: {}", charge.name); - self.gas_used = self.gas_available; + log::trace!("gas overflow: {}", name); + self.milligas_used = self.milligas_limit; Err(ExecutionError::OutOfGas) } Some(used) => { - log::trace!("charged {} gas: {}", to_use, charge.name); - if used > self.gas_available { - log::trace!("out of gas: {}", charge.name); - self.gas_used = self.gas_available; + log::trace!("charged {} gas: {}", to_use, name); + if used > self.milligas_limit { + log::trace!("out of gas: {}", name); + self.milligas_used = self.milligas_limit; Err(ExecutionError::OutOfGas) } else { - self.gas_used = used; + self.milligas_used = used; Ok(()) } } } } + pub fn charge_gas(&mut self, charge: GasCharge) -> Result<()> { + self.charge_milligas( + charge.name, + charge.total().saturating_mul(MILLIGAS_PRECISION), + ) + } + + /// returns available milligas; makes the gas tracker reject gas charges with + /// a fatal error until return_milligas is called. + pub fn borrow_milligas(&mut self) -> Result { + if !self.own_limit { + return Err(ExecutionError::Fatal(anyhow::Error::msg( + "borrow_milligas called on GasTracker which doesn't own gas limit", + ))); + } + self.own_limit = false; + + Ok(self.milligas_limit - self.milligas_used) + } + + /// sets new available gas, creating a new gas charge if needed + pub fn return_milligas(&mut self, name: &str, new_avail_mgas: i64) -> Result<()> { + if self.own_limit { + return Err(ExecutionError::Fatal(anyhow::Error::msg(format!( + "gastracker already owns gas_limit, charge: {}", + name + )))); + } + self.own_limit = true; + + let old_avail_milligas = self.milligas_limit - self.milligas_used; + let used = old_avail_milligas - new_avail_mgas; + + if used < 0 { + return Err(ExecutionError::Fatal(anyhow::Error::msg( + "negative gas charge in set_available_gas", + ))); + } + + self.charge_milligas(name, used) + } + /// Getter for gas available. - pub fn gas_available(&self) -> i64 { - self.gas_available + pub fn gas_limit(&self) -> i64 { + milligas_to_gas(self.milligas_limit, false) } /// Getter for gas used. pub fn gas_used(&self) -> i64 { - self.gas_used + milligas_to_gas(self.milligas_used, true) } } +/// Converts the specified gas into equivalent fractional gas units +#[inline] +fn gas_to_milligas(gas: i64) -> i64 { + gas.saturating_mul(MILLIGAS_PRECISION) +} + +/// Converts the specified fractional gas units into gas units +#[inline] +fn milligas_to_gas(milligas: i64, round_up: bool) -> i64 { + let mut div_result = milligas / MILLIGAS_PRECISION; + if milligas > 0 && round_up && milligas % MILLIGAS_PRECISION != 0 { + div_result = div_result.saturating_add(1); + } else if milligas < 0 && !round_up && milligas % MILLIGAS_PRECISION != 0 { + div_result = div_result.saturating_sub(1); + } + div_result +} + #[cfg(test)] mod tests { use super::*; @@ -71,4 +146,12 @@ mod tests { assert_eq!(t.gas_used(), 20); assert!(t.charge_gas(GasCharge::new("", 1, 0)).is_err()) } + + #[test] + fn milligas_to_gas_round() { + assert_eq!(milligas_to_gas(100, false), 0); + assert_eq!(milligas_to_gas(100, true), 1); + assert_eq!(milligas_to_gas(-100, false), -1); + assert_eq!(milligas_to_gas(-100, true), 0); + } } diff --git a/fvm/src/gas/price_list.rs b/fvm/src/gas/price_list.rs index a22b89379..a91c0ceac 100644 --- a/fvm/src/gas/price_list.rs +++ b/fvm/src/gas/price_list.rs @@ -11,6 +11,8 @@ use fvm_shared::sector::{ }; use fvm_shared::version::NetworkVersion; use fvm_shared::{MethodNum, METHOD_SEND}; +use fvm_wasm_instrument::gas_metering::{MemoryGrowCost, Rules}; +use fvm_wasm_instrument::parity_wasm::elements::Instruction; use lazy_static::lazy_static; use num_traits::Zero; @@ -118,7 +120,6 @@ lazy_static! { .copied() .collect(), - gas_per_exec_unit: 0, get_randomness_base: 0, get_randomness_per_byte: 0, @@ -131,6 +132,10 @@ lazy_static! { block_create_base: 0, block_link_base: 353640, block_stat: 0, + + wasm_rules: WasmGasPrices{ + exec_instruction_cost_milli: 0, + }, }; static ref SKYR_PRICES: PriceList = PriceList { @@ -240,8 +245,6 @@ lazy_static! { block_link_per_byte_cost: 1, // TODO: PARAM_FINISH - // TODO: PARAM_FINISH - gas_per_exec_unit: 2, // TODO: PARAM_FINISH get_randomness_base: 1, // TODO: PARAM_FINISH @@ -257,6 +260,11 @@ lazy_static! { block_link_base: 1, // TODO: PARAM_FINISH block_stat: 1, + + wasm_rules: WasmGasPrices{ + exec_instruction_cost_milli: 5000, + }, + // TODO: PARAM_FINISH }; } @@ -366,8 +374,6 @@ pub struct PriceList { pub(crate) verify_post_lookup: AHashMap, pub(crate) verify_consensus_fault: i64, pub(crate) verify_replica_update: i64, - // 1 Exec Unit = gas_per_exec_unit * 1 Gas - pub(crate) gas_per_exec_unit: i64, pub(crate) get_randomness_base: i64, pub(crate) get_randomness_per_byte: i64, @@ -381,6 +387,13 @@ pub struct PriceList { pub(crate) block_create_base: i64, pub(crate) block_link_base: i64, pub(crate) block_stat: i64, + + pub(crate) wasm_rules: WasmGasPrices, +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct WasmGasPrices { + pub(crate) exec_instruction_cost_milli: u64, } impl PriceList { @@ -537,33 +550,6 @@ impl PriceList { GasCharge::new("OnVerifyConsensusFault", self.verify_consensus_fault, 0) } - /// Returns the gas required for the specified exec_units. - #[inline] - pub fn on_consume_exec_units(&self, exec_units: u64) -> GasCharge<'static> { - GasCharge::new( - "OnConsumeExecUnits", - self.gas_per_exec_unit - .saturating_mul(i64::try_from(exec_units).unwrap_or(i64::MAX)), - 0, - ) - } - - /// Converts the specified gas into equivalent exec_units - /// Note: In rare cases the provided `gas` may be negative - #[inline] - pub fn gas_to_exec_units(&self, gas: i64, round_up: bool) -> i64 { - match self.gas_per_exec_unit { - 0 => 0, - v => { - let mut div_result = gas / v; - if round_up && gas % v != 0 { - div_result = div_result.saturating_add(1); - } - div_result - } - } - } - /// Returns the cost of the gas required for getting randomness from the client, based on the /// numebr of bytes of entropy. #[inline] @@ -648,3 +634,14 @@ pub fn price_list_by_network_version(network_version: NetworkVersion) -> &'stati _ => &SKYR_PRICES, } } + +impl Rules for WasmGasPrices { + fn instruction_cost(&self, _instruction: &Instruction) -> Option { + Some(self.exec_instruction_cost_milli) + } + + fn memory_grow_cost(&self) -> MemoryGrowCost { + // todo use pricelist + MemoryGrowCost::Free + } +} diff --git a/fvm/src/kernel/default.rs b/fvm/src/kernel/default.rs index 1aa894efc..c95761ce5 100644 --- a/fvm/src/kernel/default.rs +++ b/fvm/src/kernel/default.rs @@ -760,6 +760,16 @@ where self.call_manager.charge_gas(charge) } + fn return_milligas(&mut self, name: &str, new_gas: i64) -> Result<()> { + self.call_manager + .gas_tracker_mut() + .return_milligas(name, new_gas) + } + + fn borrow_milligas(&mut self) -> Result { + self.call_manager.gas_tracker_mut().borrow_milligas() + } + fn price_list(&self) -> &PriceList { self.call_manager.price_list() } diff --git a/fvm/src/kernel/mod.rs b/fvm/src/kernel/mod.rs index 0cdae1e3b..a9953a119 100644 --- a/fvm/src/kernel/mod.rs +++ b/fvm/src/kernel/mod.rs @@ -213,11 +213,6 @@ pub trait CircSupplyOps { } /// Operations for explicit gas charging. -/// -/// TODO this is unsafe; most gas charges should occur as part of syscalls, but -/// some built-in actors currently charge gas explicitly for concrete actions. -/// In the future (Phase 1), this should disappear and be replaced by gas instrumentation -/// at the WASM level. pub trait GasOps { /// GasUsed return the gas used by the transaction so far. fn gas_used(&self) -> i64; @@ -226,6 +221,12 @@ pub trait GasOps { /// `name` provides information about gas charging point fn charge_gas(&mut self, name: &str, compute: i64) -> Result<()>; + /// Returns available gas. + fn borrow_milligas(&mut self) -> Result; + + /// Sets available gas to a new value, creating a gas charge if needed + fn return_milligas(&mut self, name: &str, newgas: i64) -> Result<()>; + fn price_list(&self) -> &PriceList; } diff --git a/fvm/src/lib.rs b/fvm/src/lib.rs index 41cb409ca..705f8e67e 100644 --- a/fvm/src/lib.rs +++ b/fvm/src/lib.rs @@ -119,11 +119,13 @@ mod test { let actors_cid = bs.put_cbor(&(0, manifest_cid), Code::Blake2b256).unwrap(); + let mc = NetworkConfig::new(fvm_shared::version::NetworkVersion::V14) + .override_actors(actors_cid) + .for_epoch(0, root); + let machine = DefaultMachine::new( - &Engine::default(), - &NetworkConfig::new(fvm_shared::version::NetworkVersion::V14) - .override_actors(actors_cid) - .for_epoch(0, root), + &Engine::new_default((&mc.network).into()).unwrap(), + &mc, bs, DummyExterns, ) diff --git a/fvm/src/machine/engine.rs b/fvm/src/machine/engine.rs index 5fdb6062c..567da8e0b 100644 --- a/fvm/src/machine/engine.rs +++ b/fvm/src/machine/engine.rs @@ -1,13 +1,16 @@ -use std::collections::hash_map::Entry; +use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::HashMap; use std::ops::Deref; use std::sync::{Arc, Mutex}; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use cid::Cid; use fvm_ipld_blockstore::Blockstore; -use wasmtime::{Linker, Module}; +use fvm_wasm_instrument::gas_metering::GAS_COUNTER_NAME; +use wasmtime::{Global, GlobalType, Linker, Module, Mutability, Val, ValType}; +use crate::gas::WasmGasPrices; +use crate::machine::NetworkConfig; use crate::syscalls::{bind_syscalls, InvocationData}; use crate::Kernel; @@ -15,11 +18,56 @@ use crate::Kernel; #[derive(Clone)] pub struct Engine(Arc); +/// Container managing engines with different consensus-affecting configurations. +#[derive(Clone)] +pub struct MultiEngine(Arc>>); + +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct EngineConfig { + pub max_wasm_stack: u32, + pub wasm_prices: &'static WasmGasPrices, +} + +impl From<&NetworkConfig> for EngineConfig { + fn from(nc: &NetworkConfig) -> Self { + EngineConfig { + max_wasm_stack: nc.max_wasm_stack, + wasm_prices: &nc.price_list.wasm_rules, + } + } +} + +impl MultiEngine { + pub fn new() -> MultiEngine { + MultiEngine(Arc::new(Mutex::new(HashMap::new()))) + } + + pub fn get(&self, nc: &NetworkConfig) -> anyhow::Result { + let mut engines = self + .0 + .lock() + .map_err(|_| anyhow::Error::msg("multiengine lock is poisoned"))?; + + let ec: EngineConfig = nc.into(); + + let engine = match engines.entry(ec.clone()) { + Occupied(entry) => entry.into_mut(), + Vacant(entry) => entry.insert(Engine::new_default(ec)?), + }; + + Ok(engine.clone()) + } +} + +impl Default for MultiEngine { + fn default() -> Self { + Self::new() + } +} + pub fn default_wasmtime_config() -> wasmtime::Config { let mut c = wasmtime::Config::default(); - // c.max_wasm_stack(); https://github.com/filecoin-project/ref-fvm/issues/424 - // wasmtime default: false c.wasm_threads(false); @@ -43,6 +91,8 @@ pub fn default_wasmtime_config() -> wasmtime::Config { // wasmtime default: depends on the arch // > This is true by default on x86-64, and false by default on other architectures. + // + // Not supported in wasm-instrument/parity-wasm c.wasm_reference_types(false); // wasmtime default: false @@ -55,23 +105,27 @@ pub fn default_wasmtime_config() -> wasmtime::Config { // > not enabled by default. c.cranelift_nan_canonicalization(true); - // c.cranelift_opt_level(Speed); ? + // Set to something much higher than the instrumented limiter. + c.max_wasm_stack(64 << 20).unwrap(); - c.consume_fuel(true); + // Execution cost accouting is done through wasm instrumentation, + c.consume_fuel(false); - c -} + // c.cranelift_opt_level(Speed); ? -impl Default for Engine { - fn default() -> Self { - Engine::new(&default_wasmtime_config()).unwrap() - } + c } struct EngineInner { engine: wasmtime::Engine, + + /// dummy gas global used in store costructor to avoid making + /// InvocationData.avail_gas_global an Option + dummy_gas_global: Global, + module_cache: Mutex>, instance_cache: Mutex>, + config: EngineConfig, } impl Deref for Engine { @@ -83,25 +137,30 @@ impl Deref for Engine { } impl Engine { - /// Create a new Engine from a wasmtime config. - pub fn new(c: &wasmtime::Config) -> anyhow::Result { - Ok(wasmtime::Engine::new(c)?.into()) + pub fn new_default(ec: EngineConfig) -> anyhow::Result { + Engine::new(&default_wasmtime_config(), ec) } -} -impl From for Engine { - fn from(engine: wasmtime::Engine) -> Self { - Engine(Arc::new(EngineInner { + /// Create a new Engine from a wasmtime config. + pub fn new(c: &wasmtime::Config, ec: EngineConfig) -> anyhow::Result { + let engine = wasmtime::Engine::new(c)?; + + let mut dummy_store = wasmtime::Store::new(&engine, ()); + let gg_type = GlobalType::new(ValType::I64, Mutability::Var); + let dummy_gg = Global::new(&mut dummy_store, gg_type, Val::I64(0)) + .expect("failed to create dummy gas global"); + + Ok(Engine(Arc::new(EngineInner { engine, + dummy_gas_global: dummy_gg, module_cache: Default::default(), instance_cache: Mutex::new(anymap::Map::new()), - })) + config: ec, + }))) } } - struct Cache { linker: wasmtime::Linker>, - instances: HashMap>>, } impl Engine { @@ -125,7 +184,7 @@ impl Engine { &cid.to_string() ) })?; - let module = Module::from_binary(&self.0.engine, wasm.as_slice())?; + let module = self.load_raw(wasm.as_slice())?; cache.insert(*cid, module); } Ok(()) @@ -137,7 +196,7 @@ impl Engine { let module = match cache.get(k) { Some(module) => module.clone(), None => { - let module = Module::from_binary(&self.0.engine, wasm)?; + let module = self.load_raw(wasm)?; cache.insert(*k, module.clone()); module } @@ -145,6 +204,44 @@ impl Engine { Ok(module) } + fn load_raw(&self, raw_wasm: &[u8]) -> anyhow::Result { + // First make sure that non-instrumented wasm is valid + Module::validate(&self.0.engine, raw_wasm) + .map_err(anyhow::Error::msg) + .with_context(|| "failed to validate actor wasm")?; + + // Note: when adding debug mode support (with recorded syscall replay) don't instrument to + // avoid breaking debug info + + use fvm_wasm_instrument::gas_metering::inject; + use fvm_wasm_instrument::inject_stack_limiter; + use fvm_wasm_instrument::parity_wasm::deserialize_buffer; + + let m = deserialize_buffer(raw_wasm)?; + + // stack limiter adds post/pre-ambles to call instructions; We want to do that + // before injecting gas accounting calls to avoid this overhead in every single + // block of code. + let m = + inject_stack_limiter(m, self.0.config.max_wasm_stack).map_err(anyhow::Error::msg)?; + + // inject gas metering based on a price list. This function will + // * add a new mutable i64 global import, gas.gas_counter + // * push a gas counter function which deduces gas from the global, and + // traps when gas.gas_counter is less than zero + // * optionally push a function which wraps memory.grow instruction + // making it charge gas based on memory requested + // * divide code into metered blocks, and add a call to the gas counter + // function before entering each metered block + let m = inject(m, self.0.config.wasm_prices, "gas") + .map_err(|_| anyhow::Error::msg("injecting gas counter failed"))?; + + let wasm = m.to_bytes()?; + let module = Module::from_binary(&self.0.engine, wasm.as_slice())?; + + Ok(module) + } + /// Load compiled wasm code into the engine. /// /// # Safety @@ -186,32 +283,39 @@ impl Engine { anymap::Entry::Occupied(e) => e.into_mut(), anymap::Entry::Vacant(e) => e.insert({ let mut linker = Linker::new(&self.0.engine); + linker.allow_shadowing(true); + bind_syscalls(&mut linker)?; - Cache { - linker, - instances: HashMap::new(), - } + Cache { linker } }), }; - let instance_pre = match cache.instances.entry(*k) { - Entry::Occupied(e) => e.into_mut(), - Entry::Vacant(e) => { - let module_cache = self.0.module_cache.lock().expect("module_cache poisoned"); - let module = match module_cache.get(k) { - Some(module) => module, - None => return Ok(None), - }; - // We can cache the "pre instance" because our linker only has host functions. - let pre = cache.linker.instantiate_pre(&mut *store, module)?; - e.insert(pre) - } + cache + .linker + .define("gas", GAS_COUNTER_NAME, store.data_mut().avail_gas_global)?; + + let module_cache = self.0.module_cache.lock().expect("module_cache poisoned"); + let module = match module_cache.get(k) { + Some(module) => module, + None => return Ok(None), }; - let instance = instance_pre.instantiate(&mut *store)?; + let instance = cache.linker.instantiate(&mut *store, module)?; Ok(Some(instance)) } /// Construct a new wasmtime "store" from the given kernel. pub fn new_store(&self, kernel: K) -> wasmtime::Store> { - wasmtime::Store::new(&self.0.engine, InvocationData::new(kernel)) + let id = InvocationData { + kernel, + last_error: None, + avail_gas_global: self.0.dummy_gas_global, + }; + + let mut store = wasmtime::Store::new(&self.0.engine, id); + let ggtype = GlobalType::new(ValType::I64, Mutability::Var); + let gg = Global::new(&mut store, ggtype, Val::I64(0)) + .expect("failed to create available_gas global"); + store.data_mut().avail_gas_global = gg; + + store } } diff --git a/fvm/src/machine/mod.rs b/fvm/src/machine/mod.rs index 23ee33c27..d89cc8f2b 100644 --- a/fvm/src/machine/mod.rs +++ b/fvm/src/machine/mod.rs @@ -20,7 +20,7 @@ pub use default::DefaultMachine; mod engine; -pub use engine::Engine; +pub use engine::{Engine, MultiEngine}; mod boxed; @@ -98,6 +98,10 @@ pub struct NetworkConfig { /// DEFAULT: 4096 pub max_call_depth: u32, + /// The maximum number of elements on wasm stack + /// DEFAULT: 64Ki (512KiB of u64 elements) + pub max_wasm_stack: u32, + /// An override for builtin-actors. If specified, this should be the CID of a builtin-actors /// "manifest". /// @@ -121,6 +125,7 @@ impl NetworkConfig { NetworkConfig { network_version, max_call_depth: 4096, + max_wasm_stack: 64 * 1024, actor_debugging: false, builtin_actors_override: None, price_list: price_list_by_network_version(network_version), diff --git a/fvm/src/syscalls/bind.rs b/fvm/src/syscalls/bind.rs index dfd641be8..32fb264d7 100644 --- a/fvm/src/syscalls/bind.rs +++ b/fvm/src/syscalls/bind.rs @@ -2,7 +2,7 @@ use std::mem; use fvm_shared::error::ErrorNumber; use fvm_shared::sys::SyscallSafe; -use wasmtime::{Caller, Linker, Trap, WasmTy}; +use wasmtime::{Caller, Linker, Trap, Val, WasmTy}; use super::context::Memory; use super::error::Abort; @@ -98,34 +98,39 @@ fn memory_and_data<'a, K: Kernel>( Ok((Memory::new(mem), data)) } -fn charge_exec_units_for_gas(caller: &mut Caller>) -> Result<(), Trap> { - let exec_units = caller +fn gastracker_to_wasmgas(caller: &mut Caller>) -> Result<(), Trap> { + let avail_milligas = caller .data_mut() - .calculate_exec_units_for_gas() - .map_err(|_| Trap::new("failed to calculate exec_units"))?; - if exec_units.is_negative() { - caller.add_fuel(u64::try_from(exec_units.saturating_neg()).unwrap_or(0))?; - } else { - caller.consume_fuel(u64::try_from(exec_units).unwrap_or(0))?; - } - - let gas_used = caller.data().kernel.gas_used(); - let fuel_consumed = caller - .fuel_consumed() - .ok_or_else(|| Trap::new("expected to find exec_units consumed"))?; - caller.data_mut().set_snapshots(gas_used, fuel_consumed); - Ok(()) + .kernel + .borrow_milligas() + .map_err(|_| Trap::new("borrowing available gas"))?; + + let gas_global = caller.data_mut().avail_gas_global; + gas_global + .set(caller, Val::I64(avail_milligas)) + .map_err(|_| Trap::new("failed to set available gas")) } -fn charge_gas_for_exec_units(caller: &mut Caller>) -> Result<(), Trap> { - let exec_units_consumed = caller - .fuel_consumed() - .ok_or_else(|| Trap::new("expected to find exec_units consumed"))?; +fn wasmgas_to_gastracker(caller: &mut Caller>) -> Result<(), Trap> { + let global = caller.data_mut().avail_gas_global; + let milligas = match global.get(&mut *caller) { + Val::I64(g) => Ok(g), + _ => Err(Trap::new("failed to get wasm gas")), + }?; + + // note: this should never error: + // * It can't return out-of-gas, because that would mean that we got + // negative available milligas returned from wasm - and wasm + // instrumentation will trap when it sees available gas go below zero + // * If it errors because gastracker thinks it already owns gas, something + // is really wrong caller .data_mut() - .charge_gas_for_exec_units(exec_units_consumed) - .map_err(|_| Trap::new("failed to charge gas for exec_units")) + .kernel + .return_milligas("wasm_exec", milligas) + .map_err(|e| Trap::new(format!("returning available gas: {}", e)))?; + Ok(()) } // Unfortunately, we can't implement this for _all_ functions. So we implement it for functions of up to 6 arguments. @@ -148,30 +153,35 @@ macro_rules! impl_bind_syscalls { if mem::size_of::() == 0 { // If we're returning a zero-sized "value", we return no value therefore and expect no out pointer. self.func_wrap(module, name, move |mut caller: Caller<'_, InvocationData> $(, $t: $t)*| { - charge_gas_for_exec_units(&mut caller)?; + wasmgas_to_gastracker(&mut caller)?; + let (mut memory, mut data) = memory_and_data(&mut caller)?; let ctx = Context{kernel: &mut data.kernel, memory: &mut memory}; - let result = match syscall(ctx $(, $t)*).into()? { - Ok(_) => { + let out = syscall(ctx $(, $t)*).into(); + + let result = match out { + Ok(Ok(_)) => { log::trace!("syscall {}::{}: ok", module, name); data.last_error = None; - 0 + Ok(0) }, - Err(err) => { + Ok(Err(err)) => { let code = err.1; log::trace!("syscall {}::{}: fail ({})", module, name, code as u32); data.last_error = Some(backtrace::Cause::new(module, name, err)); - code as u32 + Ok(code as u32) }, + Err(e) => Err(e.into()), }; - charge_exec_units_for_gas(&mut caller)?; - Ok(result) + gastracker_to_wasmgas(&mut caller)?; + + result }) } else { // If we're returning an actual value, we need to write it back into the wasm module's memory. self.func_wrap(module, name, move |mut caller: Caller<'_, InvocationData>, ret: u32 $(, $t: $t)*| { - charge_gas_for_exec_units(&mut caller)?; + wasmgas_to_gastracker(&mut caller)?; let (mut memory, mut data) = memory_and_data(&mut caller)?; // We need to check to make sure we can store the return value _before_ we do anything. @@ -183,23 +193,25 @@ macro_rules! impl_bind_syscalls { } let ctx = Context{kernel: &mut data.kernel, memory: &mut memory}; - let result = match syscall(ctx $(, $t)*).into()? { - Ok(value) => { + let result = match syscall(ctx $(, $t)*).into() { + Ok(Ok(value)) => { log::trace!("syscall {}::{}: ok", module, name); unsafe { *(memory.as_mut_ptr().offset(ret as isize) as *mut Ret::Value) = value }; data.last_error = None; - 0 + Ok(0) }, - Err(err) => { + Ok(Err(err)) => { let code = err.1; log::trace!("syscall {}::{}: fail ({})", module, name, code as u32); data.last_error = Some(backtrace::Cause::new(module, name, err)); - code as u32 + Ok(code as u32) }, + Err(e) => Err(e.into()), }; - charge_exec_units_for_gas(&mut caller)?; - Ok(result) + gastracker_to_wasmgas(&mut caller)?; + + result }) } } diff --git a/fvm/src/syscalls/mod.rs b/fvm/src/syscalls/mod.rs index dd599fb59..e5919f680 100644 --- a/fvm/src/syscalls/mod.rs +++ b/fvm/src/syscalls/mod.rs @@ -1,8 +1,7 @@ use cid::Cid; -use wasmtime::Linker; +use wasmtime::{Global, Linker}; use crate::call_manager::backtrace; -use crate::kernel::Result; use crate::Kernel; pub(crate) mod error; @@ -31,63 +30,8 @@ pub struct InvocationData { /// after receiving this error without calling any other syscalls. pub last_error: Option, - /// This snapshot is used to track changes in gas_used during syscall invocations. - /// The snapshot gets taken when execution exits WASM _after_ charging gas for any newly incurred fuel costs. - /// When execution moves back into WASM, we consume fuel for the delta between the snapshot and the new gas_used value. - pub gas_used_snapshot: i64, - - /// This snapshot is used to track changes in fuel_consumed during WASM execution. - /// The snapshot gets taken when execution enters WASM _after_ consuming fuel for any syscall gas consumption. - /// When execution exits WASM, we charge gas for the delta between the new fuel_consumed value and the snapshot. - pub exec_units_consumed_snapshot: u64, -} - -impl InvocationData { - pub(crate) fn new(kernel: K) -> Self { - let gas_used = kernel.gas_used(); - Self { - kernel, - last_error: None, - gas_used_snapshot: gas_used, - exec_units_consumed_snapshot: 0, - } - } - - /// This method: - /// 1) calculates the gas_used delta from the previous snapshot, - /// 2) converts this to the corresponding amount of exec_units. - /// 3) returns the value calculated in 2) for its caller to actually consume that exec_units_consumed - /// The caller should also update the snapshots after doing so. - pub(crate) fn calculate_exec_units_for_gas(&self) -> Result { - let gas_used = self.kernel.gas_used(); - let exec_units_to_consume = self - .kernel - .price_list() - .gas_to_exec_units(gas_used - self.gas_used_snapshot, true); - Ok(exec_units_to_consume) - } - - pub(crate) fn set_snapshots(&mut self, gas_used: i64, exec_units_consumed: u64) { - self.gas_used_snapshot = gas_used; - self.exec_units_consumed_snapshot = exec_units_consumed; - } - - /// This method: - /// 1) charges gas corresponding to the exec_units_consumed delta based on the previous snapshot - /// 2) updates the exec_units_consumed and gas_used snapshots - pub(crate) fn charge_gas_for_exec_units(&mut self, exec_units_consumed: u64) -> Result<()> { - self.kernel.charge_gas( - "exec_units", - self.kernel - .price_list() - .on_consume_exec_units( - exec_units_consumed.saturating_sub(self.exec_units_consumed_snapshot), - ) - .total(), - )?; - self.set_snapshots(self.kernel.gas_used(), exec_units_consumed); - Ok(()) - } + /// The global containing remaining available gas + pub avail_gas_global: Global, } use self::bind::BindSyscall; diff --git a/testing/conformance/benches/bench_conformance.rs b/testing/conformance/benches/bench_conformance.rs index d335c0a09..606dcffad 100644 --- a/testing/conformance/benches/bench_conformance.rs +++ b/testing/conformance/benches/bench_conformance.rs @@ -8,7 +8,7 @@ use std::time::Duration; use colored::Colorize; use criterion::*; -use fvm::machine::Engine; +use fvm::machine::MultiEngine; use fvm_conformance_tests::driver::*; use fvm_conformance_tests::report; use fvm_conformance_tests::vector::MessageVector; @@ -39,7 +39,7 @@ fn bench_conformance(c: &mut Criterion) { ), }; - let engine = Engine::default(); + let engines = MultiEngine::default(); // TODO: this is 30 seconds per benchmark... yeesh! once we get the setup running faster (by cloning VMs more efficiently), we can probably bring this down. let mut group = c.benchmark_group("conformance-tests"); @@ -74,7 +74,7 @@ fn bench_conformance(c: &mut Criterion) { &message_vector, CheckStrength::FullTest, &vector_path.display().to_string(), - &engine, + &engines, ) { Ok(()) => report!( "SUCCESSFULLY BENCHED TEST FILE".on_green(), diff --git a/testing/conformance/benches/bench_conformance_overhead.rs b/testing/conformance/benches/bench_conformance_overhead.rs index faa017ddc..ac7cc6543 100644 --- a/testing/conformance/benches/bench_conformance_overhead.rs +++ b/testing/conformance/benches/bench_conformance_overhead.rs @@ -4,7 +4,7 @@ use std::path::Path; use std::time::Duration; use criterion::*; -use fvm::machine::{Engine, BURNT_FUNDS_ACTOR_ADDR}; +use fvm::machine::{MultiEngine, BURNT_FUNDS_ACTOR_ADDR}; use fvm_conformance_tests::driver::*; use fvm_conformance_tests::vector::{ApplyMessage, MessageVector}; use fvm_ipld_encoding::{Cbor, RawBytes}; @@ -19,7 +19,7 @@ use crate::bench_drivers::{bench_vector_file, CheckStrength}; fn bench_init_only( group: &mut BenchmarkGroup, path_to_setup: &Path, - engine: &Engine, + engines: &MultiEngine, ) -> anyhow::Result<()> { // compute measurement overhead by benching running a single empty vector of zero messages let mut message_vector = MessageVector::from_file(path_to_setup)?; @@ -35,7 +35,7 @@ fn bench_init_only( &message_vector, CheckStrength::OnlyCheckSuccess, "bench_init_only", - engine, + engines, ) } @@ -43,7 +43,7 @@ fn bench_init_only( fn bench_500_simple_state_access( group: &mut BenchmarkGroup, path_to_setup: &Path, - engine: &Engine, + engines: &MultiEngine, ) -> anyhow::Result<()> { let five_hundred_state_accesses = (0..500) .map(|i| ApplyMessage { @@ -78,7 +78,7 @@ fn bench_500_simple_state_access( &message_vector, CheckStrength::OnlyCheckSuccess, "bench_500_simple_state_access", - engine, + engines, ) } /// runs overhead benchmarks, using the contents of the environment variable VECTOR as the starting FVM state @@ -101,9 +101,9 @@ fn bench_conformance_overhead(c: &mut Criterion) { group.measurement_time(Duration::new(30, 0)); // start by getting some baselines! - let engine = Engine::default(); - bench_init_only(&mut group, &path_to_setup, &engine).unwrap(); - bench_500_simple_state_access(&mut group, &path_to_setup, &engine).unwrap(); + let engines = MultiEngine::default(); + bench_init_only(&mut group, &path_to_setup, &engines).unwrap(); + bench_500_simple_state_access(&mut group, &path_to_setup, &engines).unwrap(); group.finish(); } diff --git a/testing/conformance/benches/bench_drivers.rs b/testing/conformance/benches/bench_drivers.rs index 8d8f8a978..4f7b9ea77 100644 --- a/testing/conformance/benches/bench_drivers.rs +++ b/testing/conformance/benches/bench_drivers.rs @@ -2,7 +2,7 @@ extern crate criterion; use criterion::*; use fvm::executor::{ApplyKind, DefaultExecutor, Executor}; -use fvm::machine::Engine; +use fvm::machine::MultiEngine; use fvm_conformance_tests::driver::*; use fvm_conformance_tests::vector::{MessageVector, Variant}; use fvm_conformance_tests::vm::{TestKernel, TestMachine}; @@ -37,7 +37,7 @@ pub fn bench_vector_variant( vector: &MessageVector, messages_with_lengths: Vec<(Message, usize)>, bs: &MemoryBlockstore, - engine: &Engine, + engines: &MultiEngine, ) { group.bench_function(name, move |b| { b.iter_batched( @@ -45,7 +45,7 @@ pub fn bench_vector_variant( let vector = &(*vector).clone(); let bs = bs.clone(); // TODO next few lines don't impact the benchmarks, but it might make them run waaaay more slowly... ought to make a base copy of the machine and exec and deepcopy them each time. - let machine = TestMachine::new_for_vector(vector, variant, bs, engine); + let machine = TestMachine::new_for_vector(vector, variant, bs, engines); // can assume this works because it passed a test before this ran let exec: DefaultExecutor = DefaultExecutor::new(machine); (messages_with_lengths.clone(), exec) @@ -80,7 +80,7 @@ pub fn bench_vector_file( vector: &MessageVector, check_strength: CheckStrength, name: &str, - engine: &Engine, + engines: &MultiEngine, ) -> anyhow::Result<()> { let (bs, _) = async_std::task::block_on(vector.seed_blockstore()).unwrap(); @@ -89,12 +89,12 @@ pub fn bench_vector_file( // this tests the variant before we run the benchmark and record the bench results to disk. // if we broke the test, it's not a valid optimization :P let testresult = match check_strength { - CheckStrength::FullTest => run_variant(bs.clone(), vector, variant, engine, true) + CheckStrength::FullTest => run_variant(bs.clone(), vector, variant, engines, true) .map_err(|e| { anyhow::anyhow!("run_variant failed (probably a test parsing bug): {}", e) })?, CheckStrength::OnlyCheckSuccess => { - run_variant(bs.clone(), vector, variant, engine, false).map_err(|e| { + run_variant(bs.clone(), vector, variant, engines, false).map_err(|e| { anyhow::anyhow!("run_variant failed (probably a test parsing bug): {}", e) })? } @@ -124,7 +124,7 @@ pub fn bench_vector_file( vector, messages_with_lengths, &bs, - engine, + engines, ); } else { return Err(anyhow::anyhow!("a test failed, get the tests passing/running before running benchmarks in {:?} mode: {}", check_strength, name)); diff --git a/testing/conformance/src/driver.rs b/testing/conformance/src/driver.rs index 545cdf404..435f16b17 100644 --- a/testing/conformance/src/driver.rs +++ b/testing/conformance/src/driver.rs @@ -5,7 +5,7 @@ use cid::Cid; use fmt::Display; use fvm::executor::{ApplyKind, ApplyRet, DefaultExecutor, Executor}; use fvm::kernel::Context; -use fvm::machine::{Engine, Machine}; +use fvm::machine::{Machine, MultiEngine}; use fvm::state_tree::{ActorState, StateTree}; use fvm_ipld_blockstore::MemoryBlockstore; use fvm_ipld_encoding::{Cbor, CborStore}; @@ -184,13 +184,13 @@ pub fn run_variant( bs: MemoryBlockstore, v: &MessageVector, variant: &Variant, - engine: &Engine, + engines: &MultiEngine, check_correctness: bool, ) -> anyhow::Result { let id = variant.id.clone(); // Construct the Machine. - let machine = TestMachine::new_for_vector(v, variant, bs, engine); + let machine = TestMachine::new_for_vector(v, variant, bs, engines); let mut exec: DefaultExecutor = DefaultExecutor::new(machine); // Apply all messages in the vector. diff --git a/testing/conformance/src/vm.rs b/testing/conformance/src/vm.rs index bf60d121f..640077a91 100644 --- a/testing/conformance/src/vm.rs +++ b/testing/conformance/src/vm.rs @@ -6,7 +6,7 @@ use futures::executor::block_on; use fvm::call_manager::{CallManager, DefaultCallManager, FinishRet, InvocationResult}; use fvm::gas::{GasTracker, PriceList}; use fvm::kernel::*; -use fvm::machine::{DefaultMachine, Engine, Machine, MachineContext, NetworkConfig}; +use fvm::machine::{DefaultMachine, Engine, Machine, MachineContext, MultiEngine, NetworkConfig}; use fvm::state_tree::{ActorState, StateTree}; use fvm::DefaultKernel; use fvm_ipld_blockstore::MemoryBlockstore; @@ -49,7 +49,7 @@ impl TestMachine>> { v: &MessageVector, variant: &Variant, blockstore: MemoryBlockstore, - engine: &Engine, + engines: &MultiEngine, ) -> TestMachine>> { let network_version = NetworkVersion::try_from(variant.nv).expect("unrecognized network version"); @@ -71,16 +71,14 @@ impl TestMachine>> { .get(&network_version) .expect("no builtin actors index for nv"); - let machine = DefaultMachine::new( - engine, - NetworkConfig::new(network_version) - .override_actors(builtin_actors) - .for_epoch(epoch, state_root) - .set_base_fee(base_fee), - blockstore, - externs, - ) - .unwrap(); + let mut nc = NetworkConfig::new(network_version); + nc.override_actors(builtin_actors); + let mut mc = nc.for_epoch(epoch, state_root); + mc.set_base_fee(base_fee); + + let engine = engines.get(&mc.network).expect("getting engine"); + + let machine = DefaultMachine::new(&engine, &mc, blockstore, externs).unwrap(); let price_list = machine.context().price_list.clone(); @@ -496,6 +494,14 @@ where self.0.charge_gas(name, compute) } + fn borrow_milligas(&mut self) -> Result { + self.0.borrow_milligas() + } + + fn return_milligas(&mut self, name: &str, newgas: i64) -> Result<()> { + self.0.return_milligas(name, newgas) + } + fn price_list(&self) -> &PriceList { self.0.price_list() } diff --git a/testing/conformance/tests/runner.rs b/testing/conformance/tests/runner.rs index 5eecbe1bd..1d4754326 100644 --- a/testing/conformance/tests/runner.rs +++ b/testing/conformance/tests/runner.rs @@ -12,7 +12,7 @@ use anyhow::{anyhow, Context as _}; use async_std::{stream, sync, task}; use colored::*; use futures::{Future, StreamExt, TryFutureExt, TryStreamExt}; -use fvm::machine::Engine; +use fvm::machine::MultiEngine; use fvm_conformance_tests::driver::*; use fvm_conformance_tests::report; use fvm_conformance_tests::vector::{MessageVector, Selector}; @@ -33,13 +33,13 @@ lazy_static! { async fn conformance_test_runner() -> anyhow::Result<()> { pretty_env_logger::init(); - let engine = Engine::default(); + let engines = MultiEngine::new(); let vector_results = match var("VECTOR") { Ok(v) => either::Either::Left( iter::once(async move { let path = Path::new(v.as_str()).to_path_buf(); - let res = run_vector(path.clone(), engine) + let res = run_vector(path.clone(), engines) .await .with_context(|| format!("failed to run vector: {}", path.display()))?; anyhow::Ok((path, res)) @@ -51,10 +51,10 @@ async fn conformance_test_runner() -> anyhow::Result<()> { .into_iter() .filter_ok(is_runnable) .map(|e| { - let engine = engine.clone(); + let engines = engines.clone(); async move { let path = e?.path().to_path_buf(); - let res = run_vector(path.clone(), engine) + let res = run_vector(path.clone(), engines) .await .with_context(|| format!("failed to run vector: {}", path.display()))?; Ok((path, res)) @@ -128,7 +128,7 @@ async fn conformance_test_runner() -> anyhow::Result<()> { /// one per variant. async fn run_vector( path: PathBuf, - engine: Engine, + engines: MultiEngine, ) -> anyhow::Result>>> { let file = File::open(&path)?; let reader = BufReader::new(file); @@ -199,14 +199,20 @@ async fn run_vector( (0..v.preconditions.variants.len()).map(move |i| { let v = v.clone(); let bs = bs.clone(); - let engine = engine.clone(); + let engines = engines.clone(); let name = format!("{} | {}", path.display(), &v.preconditions.variants[i].id); futures::future::Either::Right( task::Builder::new() .name(name) .spawn(async move { - run_variant(bs, &v, &v.preconditions.variants[i], &engine, true) + run_variant( + bs, + &v, + &v.preconditions.variants[i], + &engines, + true, + ) }) .unwrap(), ) diff --git a/testing/integration/src/builtin.rs b/testing/integration/src/builtin.rs index 61f8c4df5..9e2ae32df 100644 --- a/testing/integration/src/builtin.rs +++ b/testing/integration/src/builtin.rs @@ -16,9 +16,10 @@ use crate::error::Error::{ FailedToLoadManifest, FailedToSetActor, FailedToSetState, MultipleRootCid, NoCidInManifest, }; -const BUNDLES: [(NetworkVersion, &[u8]); 2] = [ +const BUNDLES: [(NetworkVersion, &[u8]); 3] = [ (NetworkVersion::V14, actors_v6::BUNDLE_CAR), (NetworkVersion::V15, actors_v7::BUNDLE_CAR), + (NetworkVersion::V16, actors_v7::BUNDLE_CAR), // todo bad hack ]; // Import built-in actors diff --git a/testing/integration/src/tester.rs b/testing/integration/src/tester.rs index 92cc9b591..30918a116 100644 --- a/testing/integration/src/tester.rs +++ b/testing/integration/src/tester.rs @@ -152,12 +152,15 @@ impl Tester { let blockstore = self.state_tree.store(); + let mut nc = NetworkConfig::new(self.nv); + nc.override_actors(self.builtin_actors); + + let mut mc = nc.for_epoch(0, state_root); + mc.set_base_fee(TokenAmount::from(DEFAULT_BASE_FEE)); + let machine = DefaultMachine::new( - &Engine::default(), - NetworkConfig::new(self.nv) - .override_actors(self.builtin_actors) - .for_epoch(0, state_root) - .set_base_fee(TokenAmount::from(DEFAULT_BASE_FEE)), + &Engine::new_default((&mc.network.clone()).into())?, + &mc, blockstore.clone(), dummy::DummyExterns, )?; diff --git a/testing/integration/tests/lib.rs b/testing/integration/tests/lib.rs index 73884a507..00f45a2c0 100644 --- a/testing/integration/tests/lib.rs +++ b/testing/integration/tests/lib.rs @@ -5,10 +5,12 @@ use fvm_integration_tests::tester::{Account, Tester}; use fvm_ipld_encoding::tuple::*; use fvm_shared::address::Address; use fvm_shared::bigint::BigInt; +use fvm_shared::error::ExitCode; use fvm_shared::message::Message; use fvm_shared::state::StateTreeVersion; use fvm_shared::version::NetworkVersion; use num_traits::Zero; +use wabt::wat2wasm; /// The state object. #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug, Default)] @@ -65,3 +67,115 @@ fn hello_world() { assert_eq!(res.msg_receipt.exit_code.value(), 16) } + +#[test] +fn out_of_gas() { + const WAT: &str = r#" + ;; Mock invoke function + (module + (func (export "invoke") (param $x i32) (result i32) + (loop + (br 0) + ) + (i32.const 1) + ) + ) + "#; + + // Instantiate tester + let mut tester = Tester::new(NetworkVersion::V16, StateTreeVersion::V4).unwrap(); + + let sender: [Account; 1] = tester.create_accounts().unwrap(); + + // Get wasm bin + let wasm_bin = wat2wasm(WAT).unwrap(); + + // Set actor state + let actor_state = State { count: 0 }; + let state_cid = tester.set_state(&actor_state).unwrap(); + + // Set actor + let actor_address = Address::new_id(10000); + + tester + .set_actor_from_bin(&wasm_bin, state_cid, actor_address, BigInt::zero()) + .unwrap(); + + // Instantiate machine + tester.instantiate_machine().unwrap(); + + // Send message + let message = Message { + from: sender[0].1, + to: actor_address, + gas_limit: 10_000_000, + method_num: 1, + ..Message::default() + }; + + let res = tester + .executor + .unwrap() + .execute_message(message, ApplyKind::Explicit, 100) + .unwrap(); + + assert_eq!(res.msg_receipt.exit_code, ExitCode::SYS_OUT_OF_GAS) +} + +#[test] +fn out_of_stack() { + const WAT: &str = r#" + ;; Mock invoke function + (module + (func (export "invoke") (param $x i32) (result i32) + (i64.const 123) + (call 1) + (drop) + (i32.const 0) + ) + (func (param $x i64) (result i64) + (local.get 0) + (call 1) + ) + ) + "#; + + // Instantiate tester + let mut tester = Tester::new(NetworkVersion::V16, StateTreeVersion::V4).unwrap(); + + let sender: [Account; 1] = tester.create_accounts().unwrap(); + + // Get wasm bin + let wasm_bin = wat2wasm(WAT).unwrap(); + + // Set actor state + let actor_state = State { count: 0 }; + let state_cid = tester.set_state(&actor_state).unwrap(); + + // Set actor + let actor_address = Address::new_id(10000); + + tester + .set_actor_from_bin(&wasm_bin, state_cid, actor_address, BigInt::zero()) + .unwrap(); + + // Instantiate machine + tester.instantiate_machine().unwrap(); + + // Send message + let message = Message { + from: sender[0].1, + to: actor_address, + gas_limit: 10_000_000, + method_num: 1, + ..Message::default() + }; + + let res = tester + .executor + .unwrap() + .execute_message(message, ApplyKind::Explicit, 100) + .unwrap(); + + assert_eq!(res.msg_receipt.exit_code, ExitCode::SYS_ILLEGAL_INSTRUCTION) +}