diff --git a/fvm/CHANGELOG.md b/fvm/CHANGELOG.md index da3dd2ac1..734a4808f 100644 --- a/fvm/CHANGELOG.md +++ b/fvm/CHANGELOG.md @@ -3,10 +3,25 @@ Changes to the reference FVM implementation. ## Unreleased + +- ... + +## 0.7.2 [2022-05-09] -- Added `testing` feature to change module visibility -- Changed visibility of `account_actor`, `init_actor` and `system_actor` to public to use them in the integration test -framework. +- Add `testing` feature to change module visibility; concretely changed + visibility of `account_actor`, `init_actor` and `system_actor` to `pub` + to use them in the integration test framework. +- Propagate gas outputs in ApplyRet. +- Migrate CBOR serde to [cbor4ii](https://github.com/quininer/cbor4ii). +- Instrument Wasm bytecode with [filecoin-project/fvm-wasm-instrument](https://github.com/filecoin-project/fvm-wasm-instrument), + a fork of [paritytech/wasm-instrument](https://github.com/paritytech/wasm-instrument) + for more accurate stack accounting and execution units metering. +- Abort when aborting fails. +- Fix syscall binding docs. +- Fix bugs in Wasm execution units gas accounting. +- Fix system actor state serialization. +- Remove unused dependencies from build graph. +- Optimize memory resolution so it only happens once. ## 0.7.1 [2022-04-18] @@ -37,4 +52,4 @@ BREAKING: Updates the FVM to the latest syscall struct alignment - `StateTree::consume` -> `StateTree::into_store` - BREAKING: remove unused (by the FVM) `verify_post_discount` from the FVM PriceList. -[FIP0032]: https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0032.md \ No newline at end of file +[FIP0032]: https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0032.md diff --git a/fvm/Cargo.toml b/fvm/Cargo.toml index a442bd730..3d26b89b5 100644 --- a/fvm/Cargo.toml +++ b/fvm/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "fvm" description = "Filecoin Virtual Machine reference implementation" -version = "0.7.1" +version = "0.7.2" license = "MIT OR Apache-2.0" authors = ["Protocol Labs", "Filecoin Core Devs"] edition = "2021" @@ -39,6 +39,8 @@ 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"] } +yastl = "0.1.2" [dependencies.wasmtime] version = "0.35.2" diff --git a/fvm/src/call_manager/backtrace.rs b/fvm/src/call_manager/backtrace.rs index fd5daa138..c45a58033 100644 --- a/fvm/src/call_manager/backtrace.rs +++ b/fvm/src/call_manager/backtrace.rs @@ -7,7 +7,9 @@ use fvm_shared::{ActorID, MethodNum}; use crate::kernel::SyscallError; -/// A call backtrace records _why_ an actor exited with a specific error code. +/// A call backtrace records the actors an error was propagated through, from +/// the moment it was emitted. The original error is the _cause_. Backtraces are +/// useful for identifying the root cause of an error. #[derive(Debug, Default, Clone)] pub struct Backtrace { /// The actors through which this error was propagated from bottom (source) to top. @@ -34,22 +36,35 @@ impl Backtrace { self.frames.is_empty() && self.cause.is_none() } - /// Clear the backtrace. This should be called: - /// - /// 1. Before all syscalls except "abort" - /// 2. After an actor returns with a 0 exit code. + /// Clear the backtrace. pub fn clear(&mut self) { self.cause = None; self.frames.clear(); } - /// Set the backtrace cause. If there is an existing backtrace, this will clear it. - pub fn set_cause(&mut self, cause: Cause) { + /// Begins a new backtrace. If there is an existing backtrace, this will clear it. + /// + /// Note: Backtraces are populated _backwards_. That is, a frame is inserted + /// every time an actor returns. That's why `begin()` resets any currently + /// accumulated state, as once an error occurs, we want to track its + /// propagation all the way up. + pub fn begin(&mut self, cause: Cause) { self.cause = Some(cause); self.frames.clear(); } + /// Sets the cause of a backtrace. + /// + /// This is useful to stamp a backtrace with its cause after the frames + /// have been collected, such as when we ultimately handle a fatal error at + /// the top of its propagation chain. + pub fn set_cause(&mut self, cause: Cause) { + self.cause = Some(cause); + } + /// Push a "frame" (actor exit) onto the backtrace. + /// + /// This should be called every time an actor exits. pub fn push_frame(&mut self, frame: Frame) { self.frames.push(frame) } @@ -85,34 +100,69 @@ impl Display for Frame { /// The ultimate "cause" of a failed message. #[derive(Clone, Debug)] -pub struct Cause { - /// The syscall "module". - pub module: &'static str, - /// The syscall function name. - pub function: &'static str, - /// The exact syscall error. - pub error: ErrorNumber, - /// The informational syscall message. - pub message: String, +pub enum Cause { + /// The original cause was a syscall error. + Syscall { + /// The syscall "module". + module: &'static str, + /// The syscall function name. + function: &'static str, + /// The exact syscall error. + error: ErrorNumber, + /// The informational syscall message. + message: String, + }, + /// The original cause was a fatal error. + Fatal { + /// The alternate-formatted message from the anyhow error. + error_msg: String, + /// The backtrace, captured if the relevant + /// [environment variables](https://doc.rust-lang.org/std/backtrace/index.html#environment-variables) are enabled. + backtrace: String, + }, } impl Cause { - pub fn new(module: &'static str, function: &'static str, err: SyscallError) -> Self { - Self { + /// Records a failing syscall as the cause of a backtrace. + pub fn from_syscall(module: &'static str, function: &'static str, err: SyscallError) -> Self { + Self::Syscall { module, function, error: err.1, message: err.0, } } + + /// Records a fatal error as the cause of a backtrace. + pub fn from_fatal(err: anyhow::Error) -> Self { + Self::Fatal { + error_msg: format!("{:#}", err), + backtrace: err.backtrace().to_string(), + } + } } impl Display for Cause { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}::{} -- {} ({}: {})", - self.module, self.function, &self.message, self.error as u32, self.error, - ) + match self { + Cause::Syscall { + module, + function, + error, + message, + } => { + write!( + f, + "{}::{} -- {} ({}: {})", + module, function, &message, *error as u32, error, + ) + } + Cause::Fatal { + error_msg, + backtrace, + } => { + write!(f, "[FATAL] Error: {}, Backtrace:\n{}", error_msg, backtrace) + } + } } } diff --git a/fvm/src/call_manager/default.rs b/fvm/src/call_manager/default.rs index 9798e0a7d..cee811b31 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,17 +5,17 @@ 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 super::{Backtrace, CallManager, InvocationResult, NO_DATA_BLOCK_ID}; use crate::call_manager::backtrace::Frame; use crate::call_manager::FinishRet; -use crate::gas::GasTracker; -use crate::kernel::{ClassifyResult, ExecutionError, Kernel, Result, SyscallError}; +use crate::gas::{Gas, GasTracker}; +use crate::kernel::{ExecutionError, Kernel, Result, SyscallError}; use crate::machine::Machine; use crate::syscalls::error::Abort; +use crate::syscalls::{charge_for_exec, update_gas_available}; use crate::trace::{ExecutionEvent, ExecutionTrace, SendParams}; use crate::{account_actor, syscall_error}; @@ -73,7 +71,7 @@ where fn new(machine: M, gas_limit: i64, origin: Address, nonce: u64) -> Self { DefaultCallManager(Some(Box::new(InnerDefaultCallManager { machine, - gas_tracker: GasTracker::new(gas_limit, 0), + gas_tracker: GasTracker::new(Gas::new(gas_limit), Gas::zero()), origin, nonce, num_actors_created: 0, @@ -156,7 +154,7 @@ where } fn finish(mut self) -> (FinishRet, Self::Machine) { - let gas_used = self.gas_tracker.gas_used().max(0); + let gas_used = self.gas_tracker.gas_used().max(Gas::zero()).round_up(); let inner = self.0.take().expect("call manager is poisoned"); // TODO: Having to check against zero here is fishy, but this is what lotus does. @@ -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,56 +331,38 @@ 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 - .get_instance(&mut store, &state.code) - .and_then(|i| i.context("actor code not found")) - .or_fatal() - { - Ok(ret) => ret, - Err(err) => return (Err(err), store.into_data().kernel.into_call_manager()), - }; // From this point on, there are no more syscall errors, only aborts. let result: std::result::Result = (|| { + // Instantiate the module. + let instance = engine + .get_instance(&mut store, &state.code) + .and_then(|i| i.context("actor code not found")) + .map_err(Abort::Fatal)?; + + // Resolve and store a reference to the exported memory. + let memory = instance + .get_memory(&mut store, "memory") + .context("actor has no memory export") + .map_err(Abort::Fatal)?; + store.data_mut().memory = memory; + // Lookup the invoke method. let invoke: wasmtime::TypedFunc<(u32,), u32> = instance .get_typed_func(&mut store, "invoke") // All actors will have an invoke method. .map_err(Abort::Fatal)?; + // Set the available gas. + update_gas_available(&mut store)?; + // 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 - store - .data_mut() - .charge_gas_for_exec_units(exec_units_consumed) - .map_err(|e| Abort::from_error(ExitCode::SYS_ASSERTION_FAILED, e))?; + // Charge for any remaining uncharged execution gas, returning an error if we run + // out. + charge_for_exec(&mut store)?; // 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 @@ -414,7 +393,7 @@ where Ok(value) => Ok(InvocationResult::Return(value)), Err(abort) => { if let Some(err) = last_error { - cm.backtrace.set_cause(err); + cm.backtrace.begin(err); } let (code, message, res) = match abort { diff --git a/fvm/src/call_manager/mod.rs b/fvm/src/call_manager/mod.rs index 141a205eb..e72975f73 100644 --- a/fvm/src/call_manager/mod.rs +++ b/fvm/src/call_manager/mod.rs @@ -116,7 +116,7 @@ pub trait CallManager: 'static { /// Charge gas. fn charge_gas(&mut self, charge: GasCharge) -> Result<()> { - self.gas_tracker_mut().charge_gas(charge)?; + self.gas_tracker_mut().apply_charge(charge)?; Ok(()) } } diff --git a/fvm/src/executor/default.rs b/fvm/src/executor/default.rs index 81f74d18e..21e8e97b3 100644 --- a/fvm/src/executor/default.rs +++ b/fvm/src/executor/default.rs @@ -11,11 +11,12 @@ use fvm_shared::error::{ErrorNumber, ExitCode}; use fvm_shared::message::Message; use fvm_shared::receipt::Receipt; use fvm_shared::ActorID; +use lazy_static::lazy_static; use num_traits::Zero; use super::{ApplyFailure, ApplyKind, ApplyRet, Executor}; use crate::call_manager::{backtrace, CallManager, InvocationResult}; -use crate::gas::{GasCharge, GasOutputs}; +use crate::gas::{Gas, GasCharge, GasOutputs}; use crate::kernel::{ClassifyResult, Context as _, ExecutionError, Kernel}; use crate::machine::{Machine, BURNT_FUNDS_ACTOR_ADDR, REWARD_ACTOR_ADDR}; @@ -38,9 +39,24 @@ impl DerefMut for DefaultExecutor { } } +lazy_static! { + static ref EXEC_POOL: yastl::Pool = yastl::Pool::with_config( + 8, + yastl::ThreadConfig::new() + .prefix("fvm-executor") + // fvm needs more than the deafault available stack (2MiB): + // - Max 2048 wasm stack elements, which is 16KiB of 64bit entries + // - Roughly 20KiB overhead per actor call + // - max 1024 nested calls, which means that in the worst case we need ~36MiB of stack + // We also want some more space just to be conservative, so 64MiB seems like a reasonable choice + .stack_size(64 << 20), + ); +} + impl Executor for DefaultExecutor where K: Kernel, + ::Machine: Send, { type Kernel = K; @@ -50,6 +66,26 @@ where msg: Message, apply_kind: ApplyKind, raw_length: usize, + ) -> anyhow::Result { + let mut ret = Err(anyhow!("failed to execute")); + + EXEC_POOL.scoped(|scope| { + scope.execute(|| ret = self.execute_message_inner(msg, apply_kind, raw_length)); + }); + + ret + } +} + +impl DefaultExecutor +where + K: Kernel, +{ + fn execute_message_inner( + &mut self, + msg: Message, + apply_kind: ApplyKind, + raw_length: usize, ) -> anyhow::Result { // Validate if the message was correct, charge for it, and extract some preliminary data. let (sender_id, gas_cost, inclusion_cost) = @@ -130,22 +166,36 @@ where ErrorNumber::Forbidden => ExitCode::SYS_ASSERTION_FAILED, }; - backtrace.set_cause(backtrace::Cause::new("send", "send", err)); + backtrace.begin(backtrace::Cause::from_syscall("send", "send", err)); Receipt { exit_code, return_data: Default::default(), gas_used, } } - Err(ExecutionError::Fatal(e)) => { - return Err(e.context(format!( - "[from={}, to={}, seq={}, m={}, h={}] fatal error", + Err(ExecutionError::Fatal(err)) => { + // We produce a receipt with SYS_ASSERTION_FAILED exit code, and + // we consume the full gas amount so that, in case of a network- + // wide fatal errors, all nodes behave deterministically. + // + // We set the backtrace from the fatal error to aid diagnosis. + // Note that we use backtrace#set_cause instead of backtrace#begin + // because we want to retain the propagation chain that we've + // accumulated on the way out. + let err = err.context(format!( + "[from={}, to={}, seq={}, m={}, h={}]", msg.from, msg.to, msg.sequence, msg.method_num, - self.context().epoch - ))); + self.context().epoch, + )); + backtrace.set_cause(backtrace::Cause::from_fatal(err)); + Receipt { + exit_code: ExitCode::SYS_ASSERTION_FAILED, + return_data: Default::default(), + gas_used: msg.gas_limit, + } } }; @@ -164,19 +214,19 @@ where }), ApplyKind::Implicit => Ok(ApplyRet { msg_receipt: receipt, - failure_info, penalty: TokenAmount::zero(), miner_tip: TokenAmount::zero(), + base_fee_burn: TokenAmount::from(0), + over_estimation_burn: TokenAmount::from(0), + refund: TokenAmount::from(0), + gas_refund: 0, + gas_burned: 0, + failure_info, exec_trace, }), } } -} -impl DefaultExecutor -where - K: Kernel, -{ /// Create a new [`DefaultExecutor`] for executing messages on the [`Machine`]. pub fn new(m: ::Machine) -> Self { Self(Some(m)) @@ -212,10 +262,13 @@ where let pl = &self.context().price_list; let (inclusion_cost, miner_penalty_amount) = match apply_kind { - ApplyKind::Implicit => (GasCharge::new("none", 0, 0), Default::default()), + ApplyKind::Implicit => ( + GasCharge::new("none", Gas::zero(), Gas::zero()), + Default::default(), + ), ApplyKind::Explicit => { let inclusion_cost = pl.on_chain_message(raw_length); - let inclusion_total = inclusion_cost.total(); + let inclusion_total = inclusion_cost.total().round_up(); // Verify the cost of the message is not over the message gas limit. if inclusion_total > msg.gas_limit { @@ -326,11 +379,12 @@ where // NOTE: we don't support old network versions in the FVM, so we always burn. let GasOutputs { base_fee_burn, - miner_tip, over_estimation_burn, - refund, miner_penalty, - .. + miner_tip, + refund, + gas_refund, + gas_burned, } = GasOutputs::compute( receipt.gas_used, msg.gas_limit, @@ -365,15 +419,20 @@ where // refund unused gas transfer_to_actor(&msg.from, &refund)?; - if (&base_fee_burn + over_estimation_burn + &refund + &miner_tip) != gas_cost { + if (&base_fee_burn + &over_estimation_burn + &refund + &miner_tip) != gas_cost { // Sanity check. This could be a fatal error. return Err(anyhow!("Gas handling math is wrong")); } Ok(ApplyRet { msg_receipt: receipt, - failure_info, penalty: miner_penalty, miner_tip, + base_fee_burn, + over_estimation_burn, + refund, + gas_refund, + gas_burned, + failure_info, exec_trace: vec![], }) } diff --git a/fvm/src/executor/mod.rs b/fvm/src/executor/mod.rs index d38e30ebf..4d430bf9f 100644 --- a/fvm/src/executor/mod.rs +++ b/fvm/src/executor/mod.rs @@ -5,6 +5,7 @@ use std::fmt::Display; pub use default::DefaultExecutor; use fvm_ipld_encoding::RawBytes; use fvm_shared::bigint::{BigInt, Sign}; +use fvm_shared::econ::TokenAmount; use fvm_shared::error::ExitCode; use fvm_shared::message::Message; use fvm_shared::receipt::Receipt; @@ -70,6 +71,14 @@ pub struct ApplyRet { pub penalty: BigInt, /// Tip given to miner from message. pub miner_tip: BigInt, + + // Gas stuffs + pub base_fee_burn: TokenAmount, + pub over_estimation_burn: TokenAmount, + pub refund: TokenAmount, + pub gas_refund: i64, + pub gas_burned: i64, + /// Additional failure information for debugging, if any. pub failure_info: Option, /// Execution trace information, for debugging. @@ -90,8 +99,13 @@ impl ApplyRet { gas_used: 0, }, penalty: miner_penalty, - failure_info: Some(ApplyFailure::PreValidation(message.into())), miner_tip: BigInt::zero(), + base_fee_burn: TokenAmount::from(0), + over_estimation_burn: TokenAmount::from(0), + refund: TokenAmount::from(0), + gas_refund: 0, + gas_burned: 0, + failure_info: Some(ApplyFailure::PreValidation(message.into())), exec_trace: vec![], } } diff --git a/fvm/src/gas/charge.rs b/fvm/src/gas/charge.rs index cc52bd03f..15ca323e7 100644 --- a/fvm/src/gas/charge.rs +++ b/fvm/src/gas/charge.rs @@ -1,16 +1,20 @@ // Copyright 2019-2022 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use super::Gas; + /// Single gas charge in the VM. Contains information about what gas was for, as well /// as the amount of gas needed for computation and storage respectively. pub struct GasCharge<'a> { pub name: &'a str, - pub compute_gas: i64, - pub storage_gas: i64, + /// Compute costs + pub compute_gas: Gas, + /// Storage costs + pub storage_gas: Gas, } impl<'a> GasCharge<'a> { - pub fn new(name: &'a str, compute_gas: i64, storage_gas: i64) -> Self { + pub fn new(name: &'a str, compute_gas: Gas, storage_gas: Gas) -> Self { Self { name, compute_gas, @@ -18,8 +22,9 @@ impl<'a> GasCharge<'a> { } } - /// Calculates total gas charge based on compute and storage multipliers. - pub fn total(&self) -> i64 { + /// Calculates total gas charge (in milligas) by summing compute and + /// storage gas associated with this charge. + pub fn total(&self) -> Gas { self.compute_gas + self.storage_gas } } diff --git a/fvm/src/gas/mod.rs b/fvm/src/gas/mod.rs index b140fa2aa..66da5cd7c 100644 --- a/fvm/src/gas/mod.rs +++ b/fvm/src/gas/mod.rs @@ -1,74 +1,241 @@ // Copyright 2019-2022 ChainSafe Systems // SPDX-License-Identifier: Apache-2.0, MIT +use std::fmt::{Debug, Display}; +use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; + 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; + +/// A typesafe representation of gas (internally stored as milligas). +/// +/// - All math operations are _saturating_ and never overflow. +/// - Enforces correct units by making it impossible to, e.g., get gas squared (by multiplying gas +/// by gas). +/// - Makes it harder to confuse gas and milligas. +#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default)] +pub struct Gas(i64 /* milligas */); + +impl Debug for Gas { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.0 == 0 { + f.debug_tuple("Gas").field(&0 as &dyn Debug).finish() + } else { + let integral = self.0 / MILLIGAS_PRECISION; + let fractional = self.0 % MILLIGAS_PRECISION; + f.debug_tuple("Gas") + .field(&format_args!("{integral}.{fractional:03}")) + .finish() + } + } +} + +impl Display for Gas { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.0 == 0 { + f.write_str("0") + } else { + let integral = self.0 / MILLIGAS_PRECISION; + let fractional = self.0 % MILLIGAS_PRECISION; + write!(f, "{integral}.{fractional:03}") + } + } +} + +impl Gas { + /// Construct a `Gas` from milligas. + #[inline] + pub fn from_milligas(milligas: i64) -> Gas { + Gas(milligas) + } + + /// Construct a `Gas` from gas, scaling up. If this exceeds the width of an i64, it saturates at + /// `i64::MAX` milligas. + #[inline] + pub fn new(gas: i64) -> Gas { + Gas(gas.saturating_mul(MILLIGAS_PRECISION)) + } + + #[inline] + pub fn is_saturated(&self) -> bool { + self.0 == i64::MAX + } + + /// Returns the gas value as an integer, rounding the fractional part up. + #[inline] + pub fn round_up(&self) -> i64 { + milligas_to_gas(self.0, true) + } + + /// Returns the gas value as an integer, truncating the fractional part. + #[inline] + pub fn round_down(&self) -> i64 { + milligas_to_gas(self.0, false) + } + + /// Returns the gas value as milligas, without loss of precision. + #[inline] + pub fn as_milligas(&self) -> i64 { + self.0 + } +} + +impl num_traits::Zero for Gas { + fn zero() -> Self { + Gas(0) + } + + fn is_zero(&self) -> bool { + self.0 == 0 + } +} + +impl Add for Gas { + type Output = Gas; + + #[inline] + fn add(self, rhs: Self) -> Self::Output { + Self(self.0.saturating_add(rhs.0)) + } +} + +impl AddAssign for Gas { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.0 = self.0.saturating_add(rhs.0) + } +} + +impl SubAssign for Gas { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.0 = self.0.saturating_sub(rhs.0) + } +} + +impl Sub for Gas { + type Output = Gas; + + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0.saturating_sub(rhs.0)) + } +} + +impl Mul for Gas { + type Output = Gas; + + #[inline] + fn mul(self, rhs: i64) -> Self::Output { + Self(self.0.saturating_mul(rhs)) + } +} + +impl Mul for Gas { + type Output = Gas; + + #[inline] + fn mul(self, rhs: i32) -> Self::Output { + Self(self.0.saturating_mul(rhs.into())) + } +} + pub struct GasTracker { - gas_available: i64, - gas_used: i64, + gas_limit: Gas, + gas_used: Gas, } impl GasTracker { - pub fn new(gas_available: i64, gas_used: i64) -> Self { + /// Gas limit and gas used are provided in protocol units (i.e. full units). + /// They are converted to milligas for internal canonical accounting. + pub fn new(gas_limit: Gas, gas_used: Gas) -> Self { Self { - gas_available, + gas_limit, gas_used, } } /// 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) { - None => { - log::trace!("gas overflow: {}", charge.name); - self.gas_used = self.gas_available; - 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; - Err(ExecutionError::OutOfGas) - } else { - self.gas_used = used; - Ok(()) - } - } + pub fn charge_gas(&mut self, name: &str, to_use: Gas) -> Result<()> { + log::trace!("charging gas: {} {}", name, to_use); + // The gas type uses saturating math. + self.gas_used += to_use; + if self.gas_used > self.gas_limit { + log::trace!("gas limit reached"); + self.gas_used = self.gas_limit; + Err(ExecutionError::OutOfGas) + } else { + Ok(()) } } - /// Getter for gas available. - pub fn gas_available(&self) -> i64 { - self.gas_available + /// Applies the specified gas charge, where quantities are supplied in milligas. + pub fn apply_charge(&mut self, charge: GasCharge) -> Result<()> { + self.charge_gas(charge.name, charge.total()) + } + + /// Getter for the maximum gas usable by this message. + pub fn gas_limit(&self) -> Gas { + self.gas_limit } /// Getter for gas used. - pub fn gas_used(&self) -> i64 { + pub fn gas_used(&self) -> Gas { self.gas_used } + + /// Getter for gas available. + pub fn gas_available(&self) -> Gas { + self.gas_limit - self.gas_used + } +} + +/// Converts the specified fractional gas units into gas units +#[inline] +pub(crate) 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 num_traits::Zero; + use super::*; #[test] - fn basic_gas_tracker() { - let mut t = GasTracker::new(20, 10); - t.charge_gas(GasCharge::new("", 5, 0)).unwrap(); - assert_eq!(t.gas_used(), 15); - t.charge_gas(GasCharge::new("", 5, 0)).unwrap(); - assert_eq!(t.gas_used(), 20); - assert!(t.charge_gas(GasCharge::new("", 1, 0)).is_err()) + #[allow(clippy::identity_op)] + fn basic_gas_tracker() -> Result<()> { + let mut t = GasTracker::new(Gas::new(20), Gas::new(10)); + t.apply_charge(GasCharge::new("", Gas::new(5), Gas::zero()))?; + assert_eq!(t.gas_used(), Gas::new(15)); + t.apply_charge(GasCharge::new("", Gas::new(5), Gas::zero()))?; + assert_eq!(t.gas_used(), Gas::new(20)); + assert!(t + .apply_charge(GasCharge::new("", Gas::new(1), Gas::zero())) + .is_err()); + Ok(()) + } + + #[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/outputs.rs b/fvm/src/gas/outputs.rs index d52cec83a..3086d7843 100644 --- a/fvm/src/gas/outputs.rs +++ b/fvm/src/gas/outputs.rs @@ -11,12 +11,14 @@ pub(crate) struct GasOutputs { pub miner_tip: TokenAmount, pub refund: TokenAmount, + // In whole gas units. pub gas_refund: i64, pub gas_burned: i64, } impl GasOutputs { pub fn compute( + // In whole gas units. gas_used: i64, gas_limit: i64, base_fee: &TokenAmount, diff --git a/fvm/src/gas/price_list.rs b/fvm/src/gas/price_list.rs index a22b89379..3a31d5df8 100644 --- a/fvm/src/gas/price_list.rs +++ b/fvm/src/gas/price_list.rs @@ -11,47 +11,49 @@ 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; use super::GasCharge; +use crate::gas::Gas; lazy_static! { static ref OH_SNAP_PRICES: PriceList = PriceList { - compute_gas_multiplier: 1, storage_gas_multiplier: 1300, - on_chain_message_compute_base: 38863, - on_chain_message_storage_base: 36, - on_chain_message_storage_per_byte: 1, + on_chain_message_compute_base: Gas::new(38863), + on_chain_message_storage_base: Gas::new(36), + on_chain_message_storage_per_byte: Gas::new(1), - on_chain_return_value_per_byte: 1, + on_chain_return_value_per_byte: Gas::new(1), - send_base: 29233, - send_transfer_funds: 27500, - send_transfer_only_premium: 159672, - send_invoke_method: -5377, + send_base: Gas::new(29233), + send_transfer_funds: Gas::new(27500), + send_transfer_only_premium: Gas::new(159672), + send_invoke_method: Gas::new(-5377), - create_actor_compute: 1108454, - create_actor_storage: 36 + 40, - delete_actor: -(36 + 40), + create_actor_compute: Gas::new(1108454), + create_actor_storage: Gas::new(36 + 40), + delete_actor: Gas::new(-(36 + 40)), - bls_sig_cost: 16598605, - secp256k1_sig_cost: 1637292, + bls_sig_cost: Gas::new(16598605), + secp256k1_sig_cost: Gas::new(1637292), - hashing_base: 31355, - compute_unsealed_sector_cid_base: 98647, - verify_seal_base: 2000, // TODO revisit potential removal of this + hashing_base: Gas::new(31355), + compute_unsealed_sector_cid_base: Gas::new(98647), + verify_seal_base: Gas::new(2000), // TODO revisit potential removal of this - verify_aggregate_seal_base: 0, + verify_aggregate_seal_base: Zero::zero(), verify_aggregate_seal_per: [ ( RegisteredSealProof::StackedDRG32GiBV1P1, - 449900 + Gas::new(449900) ), ( RegisteredSealProof::StackedDRG64GiBV1P1, - 359272 + Gas::new(359272) ) ].iter().copied().collect(), verify_aggregate_seal_steps: [ @@ -59,14 +61,14 @@ lazy_static! { RegisteredSealProof::StackedDRG32GiBV1P1, StepCost ( vec![ - Step{start: 4, cost: 103994170}, - Step{start: 7, cost: 112356810}, - Step{start: 13, cost: 122912610}, - Step{start: 26, cost: 137559930}, - Step{start: 52, cost: 162039100}, - Step{start: 103, cost: 210960780}, - Step{start: 205, cost: 318351180}, - Step{start: 410, cost: 528274980}, + Step{start: 4, cost: Gas::new(103994170)}, + Step{start: 7, cost: Gas::new(112356810)}, + Step{start: 13, cost: Gas::new(122912610)}, + Step{start: 26, cost: Gas::new(137559930)}, + Step{start: 52, cost: Gas::new(162039100)}, + Step{start: 103, cost: Gas::new(210960780)}, + Step{start: 205, cost: Gas::new(318351180)}, + Step{start: 410, cost: Gas::new(528274980)}, ] ) ), @@ -74,14 +76,14 @@ lazy_static! { RegisteredSealProof::StackedDRG64GiBV1P1, StepCost ( vec![ - Step{start: 4, cost: 102581240}, - Step{start: 7, cost: 110803030}, - Step{start: 13, cost: 120803700}, - Step{start: 26, cost: 134642130}, - Step{start: 52, cost: 157357890}, - Step{start: 103, cost: 203017690}, - Step{start: 205, cost: 304253590}, - Step{start: 410, cost: 509880640}, + Step{start: 4, cost: Gas::new(102581240)}, + Step{start: 7, cost: Gas::new(110803030)}, + Step{start: 13, cost: Gas::new(120803700)}, + Step{start: 26, cost: Gas::new(134642130)}, + Step{start: 52, cost: Gas::new(157357890)}, + Step{start: 103, cost: Gas::new(203017690)}, + Step{start: 205, cost: Gas::new(304253590)}, + Step{start: 410, cost: Gas::new(509880640)}, ] ) ) @@ -89,28 +91,28 @@ lazy_static! { .cloned() .collect(), - verify_consensus_fault: 495422, - verify_replica_update: 36316136, + verify_consensus_fault: Gas::new(495422), + verify_replica_update: Gas::new(36316136), verify_post_lookup: [ ( RegisteredPoStProof::StackedDRGWindow512MiBV1, ScalingCost { - flat: 117680921, - scale: 43780, + flat: Gas::new(117680921), + scale: Gas::new(43780), }, ), ( RegisteredPoStProof::StackedDRGWindow32GiBV1, ScalingCost { - flat: 117680921, - scale: 43780, + flat: Gas::new(117680921), + scale: Gas::new(43780), }, ), ( RegisteredPoStProof::StackedDRGWindow64GiBV1, ScalingCost { - flat: 117680921, - scale: 43780, + flat: Gas::new(117680921), + scale: Gas::new(43780), }, ), ] @@ -118,56 +120,65 @@ lazy_static! { .copied() .collect(), - gas_per_exec_unit: 0, - get_randomness_base: 0, - get_randomness_per_byte: 0, + get_randomness_base: Zero::zero(), + get_randomness_per_byte: Zero::zero(), - block_memcpy_per_byte_cost: 0, - block_io_per_byte_cost: 0, - block_link_per_byte_cost: 1, + block_memcpy_per_byte_cost: Zero::zero(), - block_open_base: 114617, - block_read_base: 0, - block_create_base: 0, - block_link_base: 353640, - block_stat: 0, + block_open_base: Gas::new(114617), + block_open_memret_per_byte_cost: Zero::zero(), + + block_link_base: Gas::new(353640), + block_link_storage_per_byte_cost: Gas::new(1), + + block_create_base: Zero::zero(), + block_create_memret_per_byte_cost: Zero::zero(), + + block_read_base: Zero::zero(), + block_stat_base: Zero::zero(), + + syscall_cost: Zero::zero(), + extern_cost: Zero::zero(), + + wasm_rules: WasmGasPrices{ + exec_instruction_cost: Zero::zero(), + }, }; static ref SKYR_PRICES: PriceList = PriceList { - compute_gas_multiplier: 1, storage_gas_multiplier: 1300, - on_chain_message_compute_base: 38863, - on_chain_message_storage_base: 36, - on_chain_message_storage_per_byte: 1, + on_chain_message_compute_base: Gas::new(38863), + on_chain_message_storage_base: Gas::new(36), + on_chain_message_storage_per_byte: Gas::new(1), - on_chain_return_value_per_byte: 1, + on_chain_return_value_per_byte: Gas::new(1), - send_base: 29233, - send_transfer_funds: 27500, - send_transfer_only_premium: 159672, - send_invoke_method: -5377, + send_base: Gas::new(29233), + send_transfer_funds: Gas::new(27500), + send_transfer_only_premium: Gas::new(159672), + send_invoke_method: Gas::new(-5377), - create_actor_compute: 1108454, - create_actor_storage: 36 + 40, - delete_actor: -(36 + 40), + create_actor_compute: Gas::new(1108454), + create_actor_storage: Gas::new(36 + 40), + delete_actor: Gas::new(-(36 + 40)), - bls_sig_cost: 16598605, - secp256k1_sig_cost: 1637292, + bls_sig_cost: Gas::new(16598605), + secp256k1_sig_cost: Gas::new(1637292), - hashing_base: 31355, - compute_unsealed_sector_cid_base: 98647, - verify_seal_base: 2000, // TODO revisit potential removal of this + hashing_base: Gas::new(31355), + compute_unsealed_sector_cid_base: Gas::new(98647), + verify_seal_base: Gas::new(2000), // TODO revisit potential removal of this - verify_aggregate_seal_base: 0, + verify_aggregate_seal_base: Zero::zero(), verify_aggregate_seal_per: [ ( RegisteredSealProof::StackedDRG32GiBV1P1, - 449900 + Gas::new(449900) ), ( RegisteredSealProof::StackedDRG64GiBV1P1, - 359272 + Gas::new(359272) ) ].iter().copied().collect(), verify_aggregate_seal_steps: [ @@ -175,14 +186,14 @@ lazy_static! { RegisteredSealProof::StackedDRG32GiBV1P1, StepCost ( vec![ - Step{start: 4, cost: 103994170}, - Step{start: 7, cost: 112356810}, - Step{start: 13, cost: 122912610}, - Step{start: 26, cost: 137559930}, - Step{start: 52, cost: 162039100}, - Step{start: 103, cost: 210960780}, - Step{start: 205, cost: 318351180}, - Step{start: 410, cost: 528274980}, + Step{start: 4, cost: Gas::new(103994170)}, + Step{start: 7, cost: Gas::new(112356810)}, + Step{start: 13, cost: Gas::new(122912610)}, + Step{start: 26, cost: Gas::new(137559930)}, + Step{start: 52, cost: Gas::new(162039100)}, + Step{start: 103, cost: Gas::new(210960780)}, + Step{start: 205, cost: Gas::new(318351180)}, + Step{start: 410, cost: Gas::new(528274980)}, ] ) ), @@ -190,14 +201,14 @@ lazy_static! { RegisteredSealProof::StackedDRG64GiBV1P1, StepCost ( vec![ - Step{start: 4, cost: 102581240}, - Step{start: 7, cost: 110803030}, - Step{start: 13, cost: 120803700}, - Step{start: 26, cost: 134642130}, - Step{start: 52, cost: 157357890}, - Step{start: 103, cost: 203017690}, - Step{start: 205, cost: 304253590}, - Step{start: 410, cost: 509880640}, + Step{start: 4, cost: Gas::new(102581240)}, + Step{start: 7, cost: Gas::new(110803030)}, + Step{start: 13, cost: Gas::new(120803700)}, + Step{start: 26, cost: Gas::new(134642130)}, + Step{start: 52, cost: Gas::new(157357890)}, + Step{start: 103, cost: Gas::new(203017690)}, + Step{start: 205, cost: Gas::new(304253590)}, + Step{start: 410, cost: Gas::new(509880640)}, ] ) ) @@ -206,28 +217,28 @@ lazy_static! { .collect(), // TODO: PARAM_FINISH: this may need to be increased to account for the cost of an extern - verify_consensus_fault: 495422, - verify_replica_update: 36316136, + verify_consensus_fault: Gas::new(495422), + verify_replica_update: Gas::new(36316136), verify_post_lookup: [ ( RegisteredPoStProof::StackedDRGWindow512MiBV1, ScalingCost { - flat: 117680921, - scale: 43780, + flat: Gas::new(117680921), + scale: Gas::new(43780), }, ), ( RegisteredPoStProof::StackedDRGWindow32GiBV1, ScalingCost { - flat: 117680921, - scale: 43780, + flat: Gas::new(117680921), + scale: Gas::new(43780), }, ), ( RegisteredPoStProof::StackedDRGWindow64GiBV1, ScalingCost { - flat: 117680921, - scale: 43780, + flat: Gas::new(117680921), + scale: Gas::new(43780), }, ), ] @@ -235,35 +246,36 @@ lazy_static! { .copied() .collect(), - block_memcpy_per_byte_cost: 4, - block_io_per_byte_cost: 2, - 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 - get_randomness_per_byte: 1, - - // TODO: PARAM_FINIuiSH - block_open_base: 1, - // TODO: PARAM_FINISH - block_read_base: 1, - // TODO: PARAM_FINISH - block_create_base: 1, - // TODO: PARAM_FINISH - block_link_base: 1, - // TODO: PARAM_FINISH - block_stat: 1, + get_randomness_base: Zero::zero(), + get_randomness_per_byte: Zero::zero(), + + block_memcpy_per_byte_cost: Gas::from_milligas(500), + + block_open_base: Gas::new(114617), + block_open_memret_per_byte_cost: Gas::new(10), + + block_link_base: Gas::new(353640), + block_link_storage_per_byte_cost: Gas::new(1), + + block_create_base: Zero::zero(), + block_create_memret_per_byte_cost: Gas::new(10), + + block_read_base: Zero::zero(), + block_stat_base: Zero::zero(), + + syscall_cost: Gas::new(14000), + extern_cost: Gas::new(21000), + + wasm_rules: WasmGasPrices{ + exec_instruction_cost: Gas::new(4), + }, }; } #[derive(Clone, Debug, Copy)] pub(crate) struct ScalingCost { - flat: i64, - scale: i64, + flat: Gas, + scale: Gas, } #[derive(Clone, Debug)] @@ -272,11 +284,11 @@ pub(crate) struct StepCost(Vec); #[derive(Clone, Debug, Copy)] pub(crate) struct Step { start: i64, - cost: i64, + cost: Gas, } impl StepCost { - pub(crate) fn lookup(&self, x: i64) -> i64 { + pub(crate) fn lookup(&self, x: i64) -> Gas { let mut i: i64 = 0; while i < self.0.len() as i64 { if self.0[i as usize].start > x { @@ -286,20 +298,16 @@ impl StepCost { } i -= 1; if i < 0 { - return 0; + return Gas::zero(); } self.0[i as usize].cost } } -/// Provides prices for operations in the VM +/// Provides prices for operations in the VM. +/// All costs are in milligas. #[derive(Clone, Debug)] pub struct PriceList { - /// Compute gas charge multiplier - // * This multiplier is not currently applied to anything, but is matching lotus. - // * If the possible values are non 1 or if Lotus adds, we should change also. - #[allow(unused)] - pub(crate) compute_gas_multiplier: i64, /// Storage gas charge multiplier pub(crate) storage_gas_multiplier: i64, @@ -309,78 +317,104 @@ pub struct PriceList { /// Together, these account for the cost of message propagation and validation, /// up to but excluding any actual processing by the VM. /// This is the cost a block producer burns when including an invalid message. - pub(crate) on_chain_message_compute_base: i64, - pub(crate) on_chain_message_storage_base: i64, - pub(crate) on_chain_message_storage_per_byte: i64, + pub(crate) on_chain_message_compute_base: Gas, + pub(crate) on_chain_message_storage_base: Gas, + pub(crate) on_chain_message_storage_per_byte: Gas, /// Gas cost charged to the originator of a non-nil return value produced /// by an on-chain message is given by: /// len(return value)*OnChainReturnValuePerByte - pub(crate) on_chain_return_value_per_byte: i64, + pub(crate) on_chain_return_value_per_byte: Gas, /// Gas cost for any message send execution(including the top-level one /// initiated by an on-chain message). /// This accounts for the cost of loading sender and receiver actors and /// (for top-level messages) incrementing the sender's sequence number. /// Load and store of actor sub-state is charged separately. - pub(crate) send_base: i64, + pub(crate) send_base: Gas, /// Gas cost charged, in addition to SendBase, if a message send /// is accompanied by any nonzero currency amount. /// Accounts for writing receiver's new balance (the sender's state is /// already accounted for). - pub(crate) send_transfer_funds: i64, + pub(crate) send_transfer_funds: Gas, /// Gas cost charged, in addition to SendBase, if message only transfers funds. - pub(crate) send_transfer_only_premium: i64, + pub(crate) send_transfer_only_premium: Gas, /// Gas cost charged, in addition to SendBase, if a message invokes /// a method on the receiver. /// Accounts for the cost of loading receiver code and method dispatch. - pub(crate) send_invoke_method: i64, + pub(crate) send_invoke_method: Gas, /// Gas cost for creating a new actor (via InitActor's Exec method). /// Note: this costs assume that the extra will be partially or totally refunded while /// the base is covering for the put. - pub(crate) create_actor_compute: i64, - pub(crate) create_actor_storage: i64, + pub(crate) create_actor_compute: Gas, + pub(crate) create_actor_storage: Gas, /// Gas cost for deleting an actor. /// Note: this partially refunds the create cost to incentivise the deletion of the actors. - pub(crate) delete_actor: i64, + pub(crate) delete_actor: Gas, /// Gas cost for verifying bls signature - pub(crate) bls_sig_cost: i64, + pub(crate) bls_sig_cost: Gas, /// Gas cost for verifying secp256k1 signature - pub(crate) secp256k1_sig_cost: i64, + pub(crate) secp256k1_sig_cost: Gas, - pub(crate) hashing_base: i64, + pub(crate) hashing_base: Gas, - pub(crate) compute_unsealed_sector_cid_base: i64, - pub(crate) verify_seal_base: i64, + pub(crate) compute_unsealed_sector_cid_base: Gas, + pub(crate) verify_seal_base: Gas, #[allow(unused)] - pub(crate) verify_aggregate_seal_base: i64, - pub(crate) verify_aggregate_seal_per: AHashMap, + pub(crate) verify_aggregate_seal_base: Gas, + pub(crate) verify_aggregate_seal_per: AHashMap, pub(crate) verify_aggregate_seal_steps: AHashMap, 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, - - pub(crate) block_memcpy_per_byte_cost: i64, - pub(crate) block_io_per_byte_cost: i64, - pub(crate) block_link_per_byte_cost: i64, - - pub(crate) block_open_base: i64, - pub(crate) block_read_base: i64, - pub(crate) block_create_base: i64, - pub(crate) block_link_base: i64, - pub(crate) block_stat: i64, + pub(crate) verify_consensus_fault: Gas, + pub(crate) verify_replica_update: Gas, + + /// Gas cost for fetching randomness. + pub(crate) get_randomness_base: Gas, + /// Gas cost per every byte of randomness fetched. + pub(crate) get_randomness_per_byte: Gas, + + /// Gas cost per every block byte memcopied across boundaries. + pub(crate) block_memcpy_per_byte_cost: Gas, + + /// Gas cost for opening a block. + pub(crate) block_open_base: Gas, + /// Gas cost for every byte retained in FVM space when opening a block. + pub(crate) block_open_memret_per_byte_cost: Gas, + + /// Gas cost for linking a block. + pub(crate) block_link_base: Gas, + /// Multiplier for storage gas per byte. + pub(crate) block_link_storage_per_byte_cost: Gas, + + /// Gas cost for creating a block. + pub(crate) block_create_base: Gas, + /// Gas cost for every byte retained in FVM space when writing a block. + pub(crate) block_create_memret_per_byte_cost: Gas, + + /// Gas cost for reading a block into actor space. + pub(crate) block_read_base: Gas, + /// Gas cost for statting a block. + pub(crate) block_stat_base: Gas, + + /// General gas cost for performing a syscall, accounting for the overhead thereof. + pub(crate) syscall_cost: Gas, + /// General gas cost for calling an extern, accounting for the overhead thereof. + pub(crate) extern_cost: Gas, + + /// Rules for execution gas. + pub(crate) wasm_rules: WasmGasPrices, +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct WasmGasPrices { + pub(crate) exec_instruction_cost: Gas, } impl PriceList { @@ -395,15 +429,17 @@ impl PriceList { * self.storage_gas_multiplier, ) } + /// Returns the gas required for storing the response of a message in the chain. #[inline] pub fn on_chain_return_value(&self, data_size: usize) -> GasCharge<'static> { GasCharge::new( "OnChainReturnValue", - 0, - data_size as i64 * self.on_chain_return_value_per_byte * self.storage_gas_multiplier, + Zero::zero(), + self.on_chain_return_value_per_byte * data_size as i64 * self.storage_gas_multiplier, ) } + /// Returns the gas required when invoking a method. #[inline] pub fn on_method_invocation( @@ -421,8 +457,14 @@ impl PriceList { if method_num != METHOD_SEND { ret += self.send_invoke_method; } - GasCharge::new("OnMethodInvocation", ret, 0) + GasCharge::new("OnMethodInvocation", ret, Zero::zero()) } + + /// Returns the gas cost to be applied on a syscall. + pub fn on_syscall(&self) -> GasCharge<'static> { + GasCharge::new("OnSyscall", self.syscall_cost, Zero::zero()) + } + /// Returns the gas required for creating an actor. #[inline] pub fn on_create_actor(&self) -> GasCharge<'static> { @@ -432,15 +474,17 @@ impl PriceList { self.create_actor_storage * self.storage_gas_multiplier, ) } + /// Returns the gas required for deleting an actor. #[inline] pub fn on_delete_actor(&self) -> GasCharge<'static> { GasCharge::new( "OnDeleteActor", - 0, + Zero::zero(), self.delete_actor * self.storage_gas_multiplier, ) } + /// Returns gas required for signature verification. #[inline] pub fn on_verify_signature(&self, sig_type: SignatureType) -> GasCharge<'static> { @@ -448,13 +492,15 @@ impl PriceList { SignatureType::BLS => self.bls_sig_cost, SignatureType::Secp256k1 => self.secp256k1_sig_cost, }; - GasCharge::new("OnVerifySignature", val, 0) + GasCharge::new("OnVerifySignature", val, Zero::zero()) } + /// Returns gas required for hashing data. #[inline] pub fn on_hashing(&self, _: usize) -> GasCharge<'static> { - GasCharge::new("OnHashing", self.hashing_base, 0) + GasCharge::new("OnHashing", self.hashing_base, Zero::zero()) } + /// Returns gas required for computing unsealed sector Cid. #[inline] pub fn on_compute_unsealed_sector_cid( @@ -465,13 +511,14 @@ impl PriceList { GasCharge::new( "OnComputeUnsealedSectorCid", self.compute_unsealed_sector_cid_base, - 0, + Zero::zero(), ) } + /// Returns gas required for seal verification. #[inline] pub fn on_verify_seal(&self, _info: &SealVerifyInfo) -> GasCharge<'static> { - GasCharge::new("OnVerifySeal", self.verify_seal_base, 0) + GasCharge::new("OnVerifySeal", self.verify_seal_base, Zero::zero()) } #[inline] pub fn on_verify_aggregate_seals( @@ -479,7 +526,7 @@ impl PriceList { aggregate: &AggregateSealVerifyProofAndInfos, ) -> GasCharge<'static> { let proof_type = aggregate.seal_proof; - let per_proof = self + let per_proof = *self .verify_aggregate_seal_per .get(&proof_type) .unwrap_or_else(|| { @@ -505,14 +552,20 @@ impl PriceList { GasCharge::new( "OnVerifyAggregateSeals", per_proof * num + step.lookup(num), - 0, + Zero::zero(), ) } + /// Returns gas required for replica verification. #[inline] pub fn on_verify_replica_update(&self, _replica: &ReplicaUpdateInfo) -> GasCharge<'static> { - GasCharge::new("OnVerifyReplicaUpdate", self.verify_replica_update, 0) + GasCharge::new( + "OnVerifyReplicaUpdate", + self.verify_replica_update, + Zero::zero(), + ) } + /// Returns gas required for PoSt verification. #[inline] pub fn on_verify_post(&self, info: &WindowPoStVerifyInfo) -> GasCharge<'static> { @@ -527,124 +580,135 @@ impl PriceList { .expect("512MiB lookup must exist in price table") }); - let gas_used = cost.flat + info.challenged_sectors.len() as i64 * cost.scale; + let gas_used = cost.flat + cost.scale * info.challenged_sectors.len() as i64; - GasCharge::new("OnVerifyPost", gas_used, 0) + GasCharge::new("OnVerifyPost", gas_used, Zero::zero()) } + /// Returns gas required for verifying consensus fault. #[inline] pub fn on_verify_consensus_fault(&self) -> GasCharge<'static> { - 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, + "OnVerifyConsensusFault", + self.extern_cost + self.verify_consensus_fault, + Zero::zero(), ) } - /// 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] pub fn on_get_randomness(&self, entropy_size: usize) -> GasCharge<'static> { GasCharge::new( "OnGetRandomness", - self.get_randomness_base.saturating_add( - self.get_randomness_per_byte - .saturating_mul(entropy_size as i64), - ), - 0, + self.extern_cost + + self.get_randomness_base + + (self.get_randomness_per_byte * entropy_size as i64), + Zero::zero(), ) } /// Returns the base gas required for loading an object, independent of the object's size. #[inline] pub fn on_block_open_base(&self) -> GasCharge<'static> { - GasCharge::new("OnBlockOpenBase", self.block_open_base, 0) + GasCharge::new( + "OnBlockOpenBase", + self.extern_cost + self.block_open_base, + Zero::zero(), + ) } /// Returns the gas required for loading an object based on the size of the object. #[inline] pub fn on_block_open_per_byte(&self, data_size: usize) -> GasCharge<'static> { - // TODO: Should we also throw on a memcpy cost here (see https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0032.md#ipld-state-management-fees) + let size = data_size as i64; GasCharge::new( "OnBlockOpenPerByte", - self.block_io_per_byte_cost.saturating_mul(data_size as i64), - 0, + (self.block_open_memret_per_byte_cost * size) + + (self.block_memcpy_per_byte_cost * size), + Zero::zero(), ) } + /// Returns the gas required for reading a loaded object. #[inline] pub fn on_block_read(&self, data_size: usize) -> GasCharge<'static> { GasCharge::new( "OnBlockRead", - self.block_read_base.saturating_add( - self.block_memcpy_per_byte_cost - .saturating_mul(data_size as i64), - ), - 0, + self.block_read_base + (self.block_memcpy_per_byte_cost * data_size as i64), + Zero::zero(), ) } /// Returns the gas required for adding an object to the FVM cache. #[inline] pub fn on_block_create(&self, data_size: usize) -> GasCharge<'static> { + let size = data_size as i64; + let mem_costs = (self.block_create_memret_per_byte_cost * size) + + (self.block_memcpy_per_byte_cost * size); GasCharge::new( "OnBlockCreate", - self.block_create_base.saturating_add( - self.block_memcpy_per_byte_cost - .saturating_mul(data_size as i64), - ), - 0, + self.block_create_base + mem_costs, + Zero::zero(), ) } /// Returns the gas required for committing an object to the state blockstore. #[inline] pub fn on_block_link(&self, data_size: usize) -> GasCharge<'static> { - // TODO: The FIP makes it sound like this would need 2 memcpys, is that what's desired? + let size = data_size as i64; + let memcpy = self.block_memcpy_per_byte_cost * size; GasCharge::new( "OnBlockLink", - self.block_link_base, - // data_size as i64 * self.block_link_per_byte_cost * self.storage_gas_multiplier, - self.block_link_per_byte_cost - .saturating_mul(self.storage_gas_multiplier) - .saturating_mul(data_size as i64), + // twice the memcpy cost: + // - one from the block registry to the FVM BufferedBlockstore + // - one from the FVM BufferedBlockstore to the Node's Blockstore + // when the machine finishes. + self.block_link_base + (memcpy * 2), + self.block_link_storage_per_byte_cost * self.storage_gas_multiplier * size, ) } /// Returns the gas required for storing an object. #[inline] pub fn on_block_stat(&self) -> GasCharge<'static> { - GasCharge::new("OnBlockStat", self.block_stat, 0) + GasCharge::new("OnBlockStat", self.block_stat_base, Zero::zero()) } } /// Returns gas price list by NetworkVersion for gas consumption. pub fn price_list_by_network_version(network_version: NetworkVersion) -> &'static PriceList { match network_version { - NetworkVersion::V14 | NetworkVersion::V15 => &OH_SNAP_PRICES, + NetworkVersion::V15 => &OH_SNAP_PRICES, _ => &SKYR_PRICES, } } + +impl Rules for WasmGasPrices { + fn instruction_cost(&self, instruction: &Instruction) -> Option { + if self.exec_instruction_cost.is_zero() { + return Some(0); + } + + // Rules valid for nv16. We will need to be generic over Rules (massive + // generics tax), use &dyn Rules (which breaks other things), or pass + // in the network version, or rules version, to vary these prices going + // forward. + match instruction { + // FIP-0032: nop, drop, block, loop, unreachable, return, else, end are priced 0. + Instruction::Nop + | Instruction::Drop + | Instruction::Block(_) + | Instruction::Loop(_) + | Instruction::Unreachable + | Instruction::Return + | Instruction::Else + | Instruction::End => Some(0), + _ => Some(self.exec_instruction_cost.as_milligas() as u64), + } + } + + fn memory_grow_cost(&self) -> MemoryGrowCost { + MemoryGrowCost::Free + } +} diff --git a/fvm/src/kernel/default.rs b/fvm/src/kernel/default.rs index 1aa894efc..53ff08692 100644 --- a/fvm/src/kernel/default.rs +++ b/fvm/src/kernel/default.rs @@ -33,15 +33,9 @@ use super::*; use crate::call_manager::{CallManager, InvocationResult}; use crate::externs::{Consensus, Rand}; use crate::gas::GasCharge; -use crate::market_actor::State as MarketActorState; -use crate::power_actor::State as PowerActorState; -use crate::reward_actor::State as RewardActorState; use crate::state_tree::ActorState; use crate::{syscall_error, EMPTY_ARR_CID}; -pub const BURN_ACTOR_ID: ActorID = 99; -pub const RESERVE_ACTOR_ID: ActorID = 90; - lazy_static! { static ref NUM_CPUS: usize = num_cpus::get(); static ref INITIAL_RESERVE_BALANCE: BigInt = BigInt::from(300_000_000) * FILECOIN_PRECISION; @@ -160,45 +154,6 @@ where Ok(state.address) } - fn get_burnt_funds(&self) -> Result { - Ok(self - .call_manager - .state_tree() - .get_actor_id(BURN_ACTOR_ID)? - .context("burn actor state couldn't be loaded") - .or_fatal()? - .balance) - } - - fn get_reserve_disbursed(&self) -> Result { - let reserve_balance = self - .call_manager - .state_tree() - .get_actor_id(RESERVE_ACTOR_ID)? - .context("failed to load reserve actor when determining reserve disbursed") - .or_fatal()? - .balance; - Ok(&*INITIAL_RESERVE_BALANCE - reserve_balance) - } - - fn get_fil_mined(&self) -> Result { - let (reward_state, _) = RewardActorState::load(self.call_manager.state_tree()) - .context("failed to load reward actor state when getting FIL mined")?; - Ok(reward_state.total_storage_power_reward()) - } - - fn power_locked(&self) -> Result { - let (power_state, _) = PowerActorState::load(self.call_manager.state_tree()) - .context("failed to load power actor state when determining locked FIL")?; - Ok(power_state.total_locked()) - } - - fn market_locked(&self) -> Result { - let (market_state, _) = MarketActorState::load(self.call_manager.state_tree()) - .context("failed to load market actor state when determining locked FIL")?; - Ok(market_state.total_locked()) - } - /// Returns `Some(actor_state)` or `None` if this actor has been deleted. fn get_self(&self) -> Result> { self.call_manager @@ -432,24 +387,10 @@ where C: CallManager, { fn total_fil_circ_supply(&self) -> Result { - let circ_supply = if self.network_version() <= NetworkVersion::V14 { - // Pre-v15 the circ supply was dynamically calculated on the Filecoin mainnet, - // meaning it fluctuated within an epoch (as messages were executed). This forced the FVM - // to do much of the calculation in order to support v14. - (&self.call_manager.context().circ_supply - + &self.get_fil_mined()? - + &self.get_reserve_disbursed()? - - &self.get_burnt_funds()? - - &self.power_locked()? - - &self.market_locked()?) - .max(Zero::zero()) - } else { - // From v15 and onwards, Filecoin mainnet was fixed to use a static circ supply per epoch. - // The value reported to the FVM from clients is now the static value, - // the FVM simply reports that value to actors. - self.call_manager.context().circ_supply.clone() - }; - Ok(circ_supply) + // From v15 and onwards, Filecoin mainnet was fixed to use a static circ supply per epoch. + // The value reported to the FVM from clients is now the static value, + // the FVM simply reports that value to actors. + Ok(self.call_manager.context().circ_supply.clone()) } } @@ -599,11 +540,12 @@ where .externs() .verify_consensus_fault(h1, h2, extra) .or_illegal_argument()?; + if self.network_version() <= NetworkVersion::V15 { self.call_manager.charge_gas(GasCharge::new( "verify_consensus_fault_accesses", - gas, - 0, + Gas::new(gas), + Gas::zero(), ))?; } @@ -751,13 +693,18 @@ impl GasOps for DefaultKernel where C: CallManager, { - fn gas_used(&self) -> i64 { + fn gas_used(&self) -> Gas { self.call_manager.gas_tracker().gas_used() } - fn charge_gas(&mut self, name: &str, compute: i64) -> Result<()> { - let charge = GasCharge::new(name, compute, 0); - self.call_manager.charge_gas(charge) + fn gas_available(&self) -> Gas { + self.call_manager.gas_tracker().gas_available() + } + + fn charge_gas(&mut self, name: &str, compute: Gas) -> Result<()> { + self.call_manager + .gas_tracker_mut() + .charge_gas(name, compute) } fn price_list(&self) -> &PriceList { @@ -798,6 +745,7 @@ where .price_list() .on_get_randomness(entropy.len()), )?; + // TODO: Check error code self.call_manager .externs() @@ -817,6 +765,7 @@ where .price_list() .on_get_randomness(entropy.len()), )?; + // TODO: Check error code self.call_manager .externs() diff --git a/fvm/src/kernel/mod.rs b/fvm/src/kernel/mod.rs index 0cdae1e3b..ab7a75e0e 100644 --- a/fvm/src/kernel/mod.rs +++ b/fvm/src/kernel/mod.rs @@ -24,10 +24,9 @@ mod error; pub use error::{ClassifyResult, Context, ExecutionError, Result, SyscallError}; use crate::call_manager::{CallManager, InvocationResult}; -use crate::gas::PriceList; +use crate::gas::{Gas, PriceList}; use crate::machine::Machine; -/// The "kernel" implements pub trait Kernel: ActorOps + BlockOps @@ -213,19 +212,18 @@ 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; + /// Returns the gas used by the transaction so far. + fn gas_used(&self) -> Gas; + + /// Returns the remaining gas for the transaction. + fn gas_available(&self) -> Gas; /// ChargeGas charges specified amount of `gas` for execution. - /// `name` provides information about gas charging point - fn charge_gas(&mut self, name: &str, compute: i64) -> Result<()>; + /// `name` provides information about gas charging point. + fn charge_gas(&mut self, name: &str, compute: Gas) -> Result<()>; + /// Returns the currently active gas price list. fn price_list(&self) -> &PriceList; } diff --git a/fvm/src/lib.rs b/fvm/src/lib.rs index 41cb409ca..2a4d8fb2d 100644 --- a/fvm/src/lib.rs +++ b/fvm/src/lib.rs @@ -37,10 +37,6 @@ pub mod init_actor; #[cfg(feature = "testing")] pub mod system_actor; -mod market_actor; -mod power_actor; -mod reward_actor; - pub mod trace; use cid::multihash::{Code, MultihashDigest}; @@ -119,11 +115,13 @@ mod test { let actors_cid = bs.put_cbor(&(0, manifest_cid), Code::Blake2b256).unwrap(); + let mc = NetworkConfig::new(fvm_shared::version::NetworkVersion::V15) + .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/default.rs b/fvm/src/machine/default.rs index 270e84037..42b0538b9 100644 --- a/fvm/src/machine/default.rs +++ b/fvm/src/machine/default.rs @@ -60,7 +60,7 @@ where externs: E, ) -> anyhow::Result { const SUPPORTED_VERSIONS: RangeInclusive = - NetworkVersion::V14..=NetworkVersion::V16; + NetworkVersion::V15..=NetworkVersion::V16; debug!( "initializing a new machine, epoch={}, base_fee={}, nv={:?}, root={}", @@ -113,6 +113,9 @@ where // This interface works for now because we know all actor CIDs // ahead of time, but with user-supplied code, we won't have that // guarantee. + // Skip preloading all builtin actors when testing. This results in JIT + // bytecode to machine code compilation, and leads to faster tests. + #[cfg(not(feature = "testing"))] engine.preload(state_tree.store(), builtin_actors.left_values())?; Ok(DefaultMachine { diff --git a/fvm/src/machine/engine.rs b/fvm/src/machine/engine.rs index 5fdb6062c..233b0f0d1 100644 --- a/fvm/src/machine/engine.rs +++ b/fvm/src/machine/engine.rs @@ -1,13 +1,17 @@ -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::OptLevel::Speed; +use wasmtime::{Global, GlobalType, Linker, Memory, MemoryType, Module, Mutability, Val, ValType}; +use crate::gas::WasmGasPrices; +use crate::machine::NetworkConfig; use crate::syscalls::{bind_syscalls, InvocationData}; use crate::Kernel; @@ -15,15 +19,63 @@ 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 + // We don't want threads, there is no way to ensure determisism c.wasm_threads(false); // wasmtime default: true + // simd isn't supported in wasm-instrument, but if we add support there, we can probably enable this. + // Note: stack limits may need adjusting after this is enabled c.wasm_simd(false); // wasmtime default: false @@ -33,16 +85,23 @@ pub fn default_wasmtime_config() -> wasmtime::Config { c.wasm_memory64(false); // wasmtime default: true + // Note: wasm-instrument only supports this at a basic level, for M2 we will + // need to add more advanced support c.wasm_bulk_memory(true); // wasmtime default: false c.wasm_module_linking(false); // wasmtime default: true - c.wasm_multi_value(false); // ?? + // we should be able to enable this for M2, just need to make sure that it's + // handled correctly in wasm-instrument + c.wasm_multi_value(false); // 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; adding support will be complicated. + // Note: stack limits may need adjusting after this is enabled c.wasm_reference_types(false); // wasmtime default: false @@ -55,23 +114,47 @@ pub fn default_wasmtime_config() -> wasmtime::Config { // > not enabled by default. c.cranelift_nan_canonicalization(true); - // c.cranelift_opt_level(Speed); ? + // wasmtime default: 512KiB + // Set to something much higher than the instrumented limiter. + // Note: This is in bytes, while the instrumented limit is in stack elements + c.max_wasm_stack(4 << 20).unwrap(); - c.consume_fuel(true); + // Execution cost accouting is done through wasm instrumentation, + c.consume_fuel(false); + c.epoch_interruption(false); - c -} + // Disable debug-related things, wasm-instrument doesn't fix debug info + // yet, so those aren't useful, just add overhead + c.debug_info(false); + c.generate_address_map(false); + c.cranelift_debug_verifier(false); -impl Default for Engine { - fn default() -> Self { - Engine::new(&default_wasmtime_config()).unwrap() - } + // Reiterate some defaults + c.guard_before_linear_memory(true); + c.interruptable(false); + c.parallel_compilation(true); + + // Doesn't seem to have significant impact on the time it takes to load code + // todo(M2): make sure this is guaranteed to run in linear time. + c.cranelift_opt_level(Speed); + + c } struct EngineInner { engine: wasmtime::Engine, + + /// These two fields are used used in the store constructor to avoid resolve a chicken & egg + /// situation: We need the store before we can get the real values, but we need to create the + /// `InvocationData` before we can make the store. + /// + /// Alternatively, we could use `Option`s. But then we need to unwrap everywhere. + dummy_gas_global: Global, + dummy_memory: Memory, + module_cache: Mutex>, instance_cache: Mutex>, + config: EngineConfig, } impl Deref for Engine { @@ -83,25 +166,34 @@ 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"); + + let dummy_memory = Memory::new(&mut dummy_store, MemoryType::new(0, Some(0))) + .expect("failed to create dummy memory"); + + Ok(Engine(Arc::new(EngineInner { engine, + dummy_memory, + 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 +217,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 +229,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 +237,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 +316,42 @@ 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, + last_milligas_available: 0, + memory: self.0.dummy_memory, + }; + + 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..0e6afc538 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". /// @@ -120,7 +124,8 @@ impl NetworkConfig { pub fn new(network_version: NetworkVersion) -> Self { NetworkConfig { network_version, - max_call_depth: 4096, + max_call_depth: 1024, + max_wasm_stack: 2048, actor_debugging: false, builtin_actors_override: None, price_list: price_list_by_network_version(network_version), diff --git a/fvm/src/market_actor.rs b/fvm/src/market_actor.rs deleted file mode 100644 index 957d025ea..000000000 --- a/fvm/src/market_actor.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! This module contains the types and functions to process the market actor's state. -//! This ONLY exists to support the circulating supply calc for version <= 14. -//! -//! It should be removed as soon as the Filecoin network updates to v15. - -use anyhow::Context; -use cid::Cid; -use fvm_ipld_blockstore::Blockstore; -use fvm_ipld_encoding::tuple::*; -use fvm_ipld_encoding::{Cbor, CborStore}; -use fvm_shared::address::Address; -use fvm_shared::bigint::bigint_ser; -use fvm_shared::clock::ChainEpoch; -use fvm_shared::deal::DealID; -use fvm_shared::econ::TokenAmount; - -use crate::kernel::{ClassifyResult, Result}; -use crate::state_tree::{ActorState, StateTree}; - -pub const MARKET_ACTOR_ADDR: Address = Address::new_id(5); - -/// Market power actor state - -impl Cbor for State {} -#[derive(Clone, Default, Serialize_tuple, Deserialize_tuple)] -pub struct State { - /// Proposals are deals that have been proposed and not yet cleaned up after expiry or termination. - /// Array - pub proposals: Cid, - - // States contains state for deals that have been activated and not yet cleaned up after expiry or termination. - // After expiration, the state exists until the proposal is cleaned up too. - // Invariant: keys(States) ⊆ keys(Proposals). - /// Array - pub states: Cid, - - /// PendingProposals tracks dealProposals that have not yet reached their deal start date. - /// We track them here to ensure that miners can't publish the same deal proposal twice - pub pending_proposals: Cid, - - /// Total amount held in escrow, indexed by actor address (including both locked and unlocked amounts). - pub escrow_table: Cid, - - /// Amount locked, indexed by actor address. - /// Note: the amounts in this table do not affect the overall amount in escrow: - /// only the _portion_ of the total escrow amount that is locked. - pub locked_table: Cid, - - /// Deal id state sequential incrementer - pub next_id: DealID, - - /// Metadata cached for efficient iteration over deals. - /// SetMultimap
- pub deal_ops_by_epoch: Cid, - pub last_cron: ChainEpoch, - - /// Total Client Collateral that is locked -> unlocked when deal is terminated - #[serde(with = "bigint_ser")] - pub total_client_locked_colateral: TokenAmount, - /// Total Provider Collateral that is locked -> unlocked when deal is terminated - #[serde(with = "bigint_ser")] - pub total_provider_locked_colateral: TokenAmount, - /// Total storage fee that is locked in escrow -> unlocked when payments are made - #[serde(with = "bigint_ser")] - pub total_client_storage_fee: TokenAmount, -} - -impl State { - /// Loads the market actor state with the supplied CID from the underlying store. - pub fn load(state_tree: &StateTree) -> Result<(Self, ActorState)> - where - B: Blockstore, - { - let market_act = state_tree - .get_actor(&MARKET_ACTOR_ADDR)? - .context("Market actor address could not be resolved") - .or_fatal()?; - - let state = state_tree - .store() - .get_cbor(&market_act.state) - .or_fatal()? - .context("market actor state not found") - .or_fatal()?; - Ok((state, market_act)) - } - - pub fn total_locked(&self) -> TokenAmount { - &self.total_client_locked_colateral - + &self.total_provider_locked_colateral - + &self.total_client_storage_fee - } -} diff --git a/fvm/src/power_actor.rs b/fvm/src/power_actor.rs deleted file mode 100644 index 221a53387..000000000 --- a/fvm/src/power_actor.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! This module contains the types and functions to process the power actor's state. -//! This ONLY exists to support the circulating supply calc for version <= 14. -//! -//! It should be removed as soon as the Filecoin network updates to v15. - -use anyhow::Context; -use cid::Cid; -use fvm_ipld_blockstore::Blockstore; -use fvm_ipld_encoding::tuple::*; -use fvm_ipld_encoding::{Cbor, CborStore}; -use fvm_shared::address::Address; -use fvm_shared::bigint::bigint_ser; -use fvm_shared::clock::ChainEpoch; -use fvm_shared::econ::TokenAmount; -use fvm_shared::sector::StoragePower; -use fvm_shared::smooth::FilterEstimate; - -use crate::kernel::{ClassifyResult, Result}; -use crate::state_tree::{ActorState, StateTree}; - -pub const POWER_ACTOR_ADDR: Address = Address::new_id(4); - -/// Storage power actor state -#[derive(Default, Serialize_tuple, Deserialize_tuple)] -pub struct State { - #[serde(with = "bigint_ser")] - pub total_raw_byte_power: StoragePower, - #[serde(with = "bigint_ser")] - pub total_bytes_committed: StoragePower, - #[serde(with = "bigint_ser")] - pub total_quality_adj_power: StoragePower, - #[serde(with = "bigint_ser")] - pub total_qa_bytes_committed: StoragePower, - #[serde(with = "bigint_ser")] - pub total_pledge_collateral: TokenAmount, - - #[serde(with = "bigint_ser")] - pub this_epoch_raw_byte_power: StoragePower, - #[serde(with = "bigint_ser")] - pub this_epoch_quality_adj_power: StoragePower, - #[serde(with = "bigint_ser")] - pub this_epoch_pledge_collateral: TokenAmount, - pub this_epoch_qa_power_smoothed: FilterEstimate, - - pub miner_count: i64, - /// Number of miners having proven the minimum consensus power. - pub miner_above_min_power_count: i64, - - /// A queue of events to be triggered by cron, indexed by epoch. - pub cron_event_queue: Cid, // Multimap, (HAMT[ChainEpoch]AMT[CronEvent] - - /// First epoch in which a cron task may be stored. Cron will iterate every epoch between this - /// and the current epoch inclusively to find tasks to execute. - pub first_cron_epoch: ChainEpoch, - - /// Claimed power for each miner. - pub claims: Cid, // Map, HAMT[address]Claim - - pub proof_validation_batch: Option, -} - -impl Cbor for State {} - -impl State { - /// Loads the power actor state with the supplied CID from the underlying store. - pub fn load(state_tree: &StateTree) -> Result<(Self, ActorState)> - where - B: Blockstore, - { - let power_act = state_tree - .get_actor(&POWER_ACTOR_ADDR)? - .context("Power actor address could not be resolved") - .or_fatal()?; - - let state = state_tree - .store() - .get_cbor(&power_act.state) - .or_fatal()? - .context("power actor state not found") - .or_fatal()?; - Ok((state, power_act)) - } - - pub fn total_locked(self) -> TokenAmount { - self.total_pledge_collateral - } -} diff --git a/fvm/src/reward_actor.rs b/fvm/src/reward_actor.rs deleted file mode 100644 index 7e9472bdf..000000000 --- a/fvm/src/reward_actor.rs +++ /dev/null @@ -1,94 +0,0 @@ -use anyhow::Context; -use fvm_ipld_blockstore::Blockstore; -use fvm_ipld_encoding::tuple::*; -use fvm_ipld_encoding::{Cbor, CborStore}; -use fvm_shared::bigint::bigint_ser; -use fvm_shared::clock::ChainEpoch; -use fvm_shared::econ::TokenAmount; -use fvm_shared::sector::{Spacetime, StoragePower}; -use fvm_shared::smooth::FilterEstimate; -use fvm_shared::ActorID; - -use crate::kernel::{ClassifyResult, Result}; -use crate::state_tree::{ActorState, StateTree}; - -pub const REWARD_ACTOR_ID: ActorID = 2; - -impl Cbor for State {} -/// Reward actor state -#[derive(Serialize_tuple, Deserialize_tuple, Default)] -pub struct State { - /// Target CumsumRealized needs to reach for EffectiveNetworkTime to increase - /// Expressed in byte-epochs. - #[serde(with = "bigint_ser")] - pub cumsum_baseline: Spacetime, - - /// CumsumRealized is cumulative sum of network power capped by BaselinePower(epoch). - /// Expressed in byte-epochs. - #[serde(with = "bigint_ser")] - pub cumsum_realized: Spacetime, - - /// Ceiling of real effective network time `theta` based on - /// CumsumBaselinePower(theta) == CumsumRealizedPower - /// Theta captures the notion of how much the network has progressed in its baseline - /// and in advancing network time. - pub effective_network_time: ChainEpoch, - - /// EffectiveBaselinePower is the baseline power at the EffectiveNetworkTime epoch. - #[serde(with = "bigint_ser")] - pub effective_baseline_power: StoragePower, - - /// The reward to be paid in per WinCount to block producers. - /// The actual reward total paid out depends on the number of winners in any round. - /// This value is recomputed every non-null epoch and used in the next non-null epoch. - #[serde(with = "bigint_ser")] - pub this_epoch_reward: TokenAmount, - /// Smoothed `this_epoch_reward`. - pub this_epoch_reward_smoothed: FilterEstimate, - - /// The baseline power the network is targeting at st.Epoch. - #[serde(with = "bigint_ser")] - pub this_epoch_baseline_power: StoragePower, - - /// Epoch tracks for which epoch the Reward was computed. - pub epoch: ChainEpoch, - - // TotalStoragePowerReward tracks the total FIL awarded to block miners - #[serde(with = "bigint_ser")] - pub total_storage_power_reward: TokenAmount, - - // Simple and Baseline totals are constants used for computing rewards. - // They are on chain because of a historical fix resetting baseline value - // in a way that depended on the history leading immediately up to the - // migration fixing the value. These values can be moved from state back - // into a code constant in a subsequent upgrade. - #[serde(with = "bigint_ser")] - pub simple_total: TokenAmount, - #[serde(with = "bigint_ser")] - pub baseline_total: TokenAmount, -} - -impl State { - /// Loads the reward actor state with the supplied CID from the underlying store. - pub fn load(state_tree: &StateTree) -> Result<(Self, ActorState)> - where - B: Blockstore, - { - let reward_act = state_tree - .get_actor_id(REWARD_ACTOR_ID)? - .context("Reward actor address could not be resolved") - .or_fatal()?; - - let state = state_tree - .store() - .get_cbor(&reward_act.state) - .or_fatal()? - .context("reward actor state not found") - .or_fatal()?; - Ok((state, reward_act)) - } - - pub fn total_storage_power_reward(&self) -> TokenAmount { - self.total_storage_power_reward.clone() - } -} diff --git a/fvm/src/syscalls/bind.rs b/fvm/src/syscalls/bind.rs index dfd641be8..14324b590 100644 --- a/fvm/src/syscalls/bind.rs +++ b/fvm/src/syscalls/bind.rs @@ -2,11 +2,11 @@ use std::mem; use fvm_shared::error::ErrorNumber; use fvm_shared::sys::SyscallSafe; -use wasmtime::{Caller, Linker, Trap, WasmTy}; +use wasmtime::{Caller, Linker, WasmTy}; use super::context::Memory; use super::error::Abort; -use super::{Context, InvocationData}; +use super::{charge_for_exec, update_gas_available, Context, InvocationData}; use crate::call_manager::backtrace; use crate::kernel::{self, ExecutionError, Kernel, SyscallError}; @@ -89,43 +89,19 @@ where fn memory_and_data<'a, K: Kernel>( caller: &'a mut Caller<'_, InvocationData>, -) -> Result<(&'a mut Memory, &'a mut InvocationData), Trap> { - let (mem, data) = caller - .get_export("memory") - .and_then(|m| m.into_memory()) - .ok_or_else(|| Trap::new("failed to lookup actor memory"))? - .data_and_store_mut(caller); - Ok((Memory::new(mem), data)) +) -> (&'a mut Memory, &'a mut InvocationData) { + let memory_handle = caller.data().memory; + let (mem, data) = memory_handle.data_and_store_mut(caller); + (Memory::new(mem), data) } -fn charge_exec_units_for_gas(caller: &mut Caller>) -> Result<(), Trap> { - let exec_units = 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(()) -} - -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"))?; - - caller - .data_mut() - .charge_gas_for_exec_units(exec_units_consumed) - .map_err(|_| Trap::new("failed to charge gas for exec_units")) +macro_rules! charge_syscall_gas { + ($kernel:expr) => { + let charge = $kernel.price_list().on_syscall(); + $kernel + .charge_gas(charge.name, charge.compute_gas) + .map_err(Abort::from_error_as_fatal)?; + }; } // Unfortunately, we can't implement this for _all_ functions. So we implement it for functions of up to 6 arguments. @@ -148,58 +124,69 @@ 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)?; - let (mut memory, mut data) = memory_and_data(&mut caller)?; + charge_for_exec(&mut caller)?; + + let (mut memory, mut data) = memory_and_data(&mut caller); + charge_syscall_gas!(data.kernel); + 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 + data.last_error = Some(backtrace::Cause::from_syscall(module, name, err)); + Ok(code as u32) }, + Err(e) => Err(e.into()), }; - charge_exec_units_for_gas(&mut caller)?; - Ok(result) + update_gas_available(&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)?; - let (mut memory, mut data) = memory_and_data(&mut caller)?; + charge_for_exec(&mut caller)?; + + let (mut memory, mut data) = memory_and_data(&mut caller); + charge_syscall_gas!(data.kernel); // We need to check to make sure we can store the return value _before_ we do anything. if (ret as u64) > (memory.len() as u64) || memory.len() - (ret as usize) < mem::size_of::() { let code = ErrorNumber::IllegalArgument; - data.last_error = Some(backtrace::Cause::new(module, name, SyscallError(format!("no space for return value"), code))); + data.last_error = Some(backtrace::Cause::from_syscall(module, name, SyscallError(format!("no space for return value"), code))); return Ok(code as u32); } 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 + data.last_error = Some(backtrace::Cause::from_syscall(module, name, err)); + Ok(code as u32) }, + Err(e) => Err(e.into()), }; - charge_exec_units_for_gas(&mut caller)?; - Ok(result) + update_gas_available(&mut caller)?; + + result }) } } diff --git a/fvm/src/syscalls/error.rs b/fvm/src/syscalls/error.rs index 5872da9cd..d8a7b0257 100644 --- a/fvm/src/syscalls/error.rs +++ b/fvm/src/syscalls/error.rs @@ -1,6 +1,7 @@ //! This module contains code used to convert errors to and from wasmtime traps. use std::sync::Mutex; +use anyhow::anyhow; use derive_more::Display; use fvm_shared::error::ExitCode; use wasmtime::Trap; @@ -34,6 +35,15 @@ impl Abort { ExecutionError::Fatal(err) => Abort::Fatal(err), } } + + /// Just like from_error, but escalating syscall errors as fatal. + pub fn from_error_as_fatal(e: ExecutionError) -> Self { + match e { + ExecutionError::OutOfGas => Abort::OutOfGas, + ExecutionError::Fatal(e) => Abort::Fatal(e), + ExecutionError::Syscall(e) => Abort::Fatal(anyhow!("unexpected syscall error: {}", e)), + } + } } /// Wraps an execution error in a Trap. diff --git a/fvm/src/syscalls/gas.rs b/fvm/src/syscalls/gas.rs index 75bfd14c6..0c33f4977 100644 --- a/fvm/src/syscalls/gas.rs +++ b/fvm/src/syscalls/gas.rs @@ -1,6 +1,7 @@ use std::str; use super::Context; +use crate::gas::Gas; use crate::kernel::{ClassifyResult, Result}; use crate::Kernel; @@ -12,5 +13,6 @@ pub fn charge_gas( ) -> Result<()> { let name = str::from_utf8(context.memory.try_slice(name_off, name_len)?).or_illegal_argument()?; - context.kernel.charge_gas(name, compute) + // Gas charges from actors are always in full gas units. We use milligas internally, so convert here. + context.kernel.charge_gas(name, Gas::new(compute)) } diff --git a/fvm/src/syscalls/mod.rs b/fvm/src/syscalls/mod.rs index dd599fb59..1e5bf50aa 100644 --- a/fvm/src/syscalls/mod.rs +++ b/fvm/src/syscalls/mod.rs @@ -1,8 +1,11 @@ +use std::mem; + +use anyhow::{anyhow, Context as _}; use cid::Cid; -use wasmtime::Linker; +use wasmtime::{AsContextMut, Global, Linker, Memory, Val}; use crate::call_manager::backtrace; -use crate::kernel::Result; +use crate::gas::Gas; use crate::Kernel; pub(crate) mod error; @@ -27,70 +30,69 @@ pub(self) use context::Context; pub struct InvocationData { /// The kernel on which this actor is being executed. pub kernel: K, + /// The last-seen syscall error. This error is considered the abort "cause" if an actor aborts /// 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, + /// The global containing remaining available gas. + pub avail_gas_global: Global, + + /// The last-set milligas limit. When `charge_for_exec` is called, we charge for the + /// _difference_ between the current gas available (the wasm global) and the + /// `last_milligas_available`. + pub last_milligas_available: 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, + /// The invocation's imported "memory". + pub memory: Memory, } -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, - } - } +pub fn update_gas_available( + ctx: &mut impl AsContextMut>, +) -> Result<(), Abort> { + let mut ctx = ctx.as_context_mut(); + let avail_milligas = ctx.data_mut().kernel.gas_available().as_milligas(); - /// 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) - } + let gas_global = ctx.data_mut().avail_gas_global; + gas_global + .set(&mut ctx, Val::I64(avail_milligas)) + .map_err(|e| Abort::Fatal(anyhow!("failed to set available gas global: {}", e)))?; - 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; - } + ctx.data_mut().last_milligas_available = avail_milligas; + Ok(()) +} - /// 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(()) - } +/// Updates the FVM-side gas tracker with newly accrued execution gas charges. +pub fn charge_for_exec( + ctx: &mut impl AsContextMut>, +) -> Result<(), Abort> { + let mut ctx = ctx.as_context_mut(); + let global = ctx.data_mut().avail_gas_global; + + let milligas_available = global + .get(&mut ctx) + .i64() + .context("failed to get wasm gas") + .map_err(Abort::Fatal)?; + + // Determine milligas used, and update the gas tracker. + let milligas_used = { + let data = ctx.data_mut(); + let last_milligas = mem::replace(&mut data.last_milligas_available, milligas_available); + // This should never be negative, but we might as well check. + last_milligas.saturating_sub(milligas_available) + }; + + ctx.data_mut() + .kernel + .charge_gas("wasm_exec", Gas::from_milligas(milligas_used)) + .map_err(Abort::from_error_as_fatal)?; + + Ok(()) } use self::bind::BindSyscall; +use self::error::Abort; /// The maximum supported CID size. (SPEC_AUDIT) pub const MAX_CID_LEN: usize = 100; diff --git a/fvm/src/system_actor.rs b/fvm/src/system_actor.rs index bbb11a309..7a8cca34f 100644 --- a/fvm/src/system_actor.rs +++ b/fvm/src/system_actor.rs @@ -1,16 +1,16 @@ use anyhow::Context; use cid::Cid; use fvm_ipld_blockstore::Blockstore; +use fvm_ipld_encoding::tuple::{Deserialize_tuple, Serialize_tuple}; use fvm_ipld_encoding::{Cbor, CborStore}; use fvm_shared::address::Address; -use serde::{Deserialize, Serialize}; use crate::kernel::{ClassifyResult, Result}; use crate::state_tree::{ActorState, StateTree}; pub const SYSTEM_ACTOR_ADDR: Address = Address::new_id(0); -#[derive(Default, Deserialize, Serialize)] +#[derive(Default, Deserialize_tuple, Serialize_tuple)] pub struct State { // builtin actor registry: Vec<(String, Cid)> pub builtin_actors: Cid, diff --git a/ipld/amt/benches/amt_benchmark.rs b/ipld/amt/benches/amt_benchmark.rs index c3827b722..773e778ef 100644 --- a/ipld/amt/benches/amt_benchmark.rs +++ b/ipld/amt/benches/amt_benchmark.rs @@ -28,13 +28,13 @@ impl Serialize for BenchData { ().serialize(serializer) } } + impl<'de> Deserialize<'de> for BenchData { fn deserialize(deserializer: D) -> Result>::Error> where D: Deserializer<'de>, { - let _s: () = Deserialize::deserialize(deserializer)?; - + Deserialize::deserialize(deserializer)?; Ok(Self::default()) } } diff --git a/ipld/bitfield/src/lib.rs b/ipld/bitfield/src/lib.rs index 6935c50a6..4e04a9f78 100644 --- a/ipld/bitfield/src/lib.rs +++ b/ipld/bitfield/src/lib.rs @@ -21,6 +21,12 @@ pub use rleplus::Error; use thiserror::Error; pub use unvalidated::{UnvalidatedBitField, Validate}; +/// MaxEncodedSize is the maximum encoded size of a bitfield. When expanded into +/// a slice of runs, a bitfield of this size should not exceed 2MiB of memory. +/// +/// This bitfield can fit at least 3072 sparse elements. +pub(crate) const MAX_ENCODED_SIZE: usize = 32 << 10; + #[derive(Clone, Error, Debug)] #[error("bitfields may not include u64::MAX")] pub struct OutOfRangeError; diff --git a/ipld/bitfield/src/rleplus/mod.rs b/ipld/bitfield/src/rleplus/mod.rs index f3bc0e35c..dfd8539d5 100644 --- a/ipld/bitfield/src/rleplus/mod.rs +++ b/ipld/bitfield/src/rleplus/mod.rs @@ -75,13 +75,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub use writer::BitWriter; use super::BitField; -use crate::RangeSize; - -// MaxEncodedSize is the maximum encoded size of a bitfield. When expanded into -// a slice of runs, a bitfield of this size should not exceed 2MiB of memory. -// -// This bitfield can fit at least 3072 sparse elements. -const MAX_ENCODED_SIZE: usize = 32 << 10; +use crate::{RangeSize, MAX_ENCODED_SIZE}; impl Serialize for BitField { fn serialize(&self, serializer: S) -> std::result::Result diff --git a/ipld/bitfield/src/unvalidated.rs b/ipld/bitfield/src/unvalidated.rs index d2412f6e7..f29c0c9e2 100644 --- a/ipld/bitfield/src/unvalidated.rs +++ b/ipld/bitfield/src/unvalidated.rs @@ -7,7 +7,7 @@ use fvm_ipld_encoding::serde_bytes; use serde::{Deserialize, Deserializer, Serialize}; use super::BitField; -use crate::Error; +use crate::{Error, MAX_ENCODED_SIZE}; /// A trait for types that can produce a `&BitField` (or fail to do so). /// Generalizes over `&BitField` and `&mut UnvalidatedBitField`. @@ -77,6 +77,12 @@ impl<'de> Deserialize<'de> for UnvalidatedBitField { D: Deserializer<'de>, { let bytes: Vec = serde_bytes::deserialize(deserializer)?; + if bytes.len() > MAX_ENCODED_SIZE { + return Err(serde::de::Error::custom(format!( + "encoded bitfield was too large {}", + bytes.len() + ))); + } Ok(Self::Unvalidated(bytes)) } } diff --git a/ipld/bitfield/tests/bitfield_tests.rs b/ipld/bitfield/tests/bitfield_tests.rs index 4dab4bd55..ee0df4553 100644 --- a/ipld/bitfield/tests/bitfield_tests.rs +++ b/ipld/bitfield/tests/bitfield_tests.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; -use fvm_ipld_bitfield::{bitfield, BitField}; +use fvm_ipld_bitfield::{bitfield, BitField, UnvalidatedBitField}; use rand::{Rng, SeedableRng}; use rand_xorshift::XorShiftRng; @@ -232,3 +232,67 @@ fn exceeds_bitfield_range() { BitField::try_from_bits([0, 1, 4, 99, u64::MAX - 1]) .expect("expected setting u64::MAX-1 to succeed"); } + +#[test] +fn bitfield_custom() { + let mut bf = BitField::new(); + + // Set alternating bits for worst-case size performance + let mut i = 0; + while i < 1_000_000 { + bf.set(i); + i += 2; + } + println!("# Set bits: {}", bf.len()); + + // Standard serialization catches MAX_ENCODING_SIZE issues + println!("Attempting to serialize..."); + match fvm_ipld_encoding::to_vec(&bf) { + Ok(_) => panic!("This should have failed!"), + Err(_) => println!("Standard serialization failed, as expected"), + } + + // Bypass to_vec enc size check so we can test deserialization + println!("Manually serializing..."); + // CBOR prefix for the bytes + let mut cbor = vec![0x5A, 0x00, 0x01, 0xE8, 0x49]; + cbor.extend_from_slice(&bf.to_bytes()); + println!("Success!"); + + println!("# bytes of cbor: {}", cbor.len()); + println!("Header: {:#010b}", cbor[0]); + println!("-- maj type {}", (cbor[0] & 0xe0) >> 5); + + // Get size of payload size + let info = cbor[0] & 0x1f; + println!("-- adtl info {}", info); + + // Get payload size + let size = match info { + 0..=23 => info as usize, + 24 => cbor[1] as usize, + 25 => u16::from_be_bytes([cbor[1], cbor[2]]) as usize, + 26 => u32::from_be_bytes([cbor[1], cbor[2], cbor[3], cbor[4]]) as usize, + 27 => u64::from_be_bytes([ + cbor[1], cbor[2], cbor[3], cbor[4], cbor[5], cbor[6], cbor[7], cbor[8], + ]) as usize, + _ => { + println!("OUT OF RANGE"); + 0 + } + }; + + println!("{} byte payload", size); + + // Deserialize and validate malicious payload + println!("Attempting to deserialize and validate..."); + match fvm_ipld_encoding::from_slice::(&cbor) { + Ok(mut bitfield) => { + bitfield.validate_mut().unwrap(); + panic!("Error - deserialized/validated payload over 32768 bytes."); + } + Err(_) => { + println!("Success - payload over 32768 bytes cannot be deserialized"); + } + } +} diff --git a/rust-toolchain b/rust-toolchain index 07ade694b..67d1f3fd0 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -nightly \ No newline at end of file +nightly-2022-05-09 diff --git a/sdk/src/debug.rs b/sdk/src/debug.rs index 74929495c..dd0980ae7 100644 --- a/sdk/src/debug.rs +++ b/sdk/src/debug.rs @@ -31,7 +31,7 @@ mod inner { sys::debug::log(msg.as_ptr(), msg.len() as u32).unwrap(); } } - /// Initialize logging if debuggig is enabled. + /// Initialize logging if debugging is enabled. pub fn init_logging() { if enabled() { log::set_logger(&Logger).expect("failed to enable logging"); diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 6c8b5f4c3..122a76ffe 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -8,7 +8,6 @@ authors = ["ChainSafe Systems ", "Protocol Labs", "Filecoin C repository = "https://github.com/filecoin-project/ref-fvm" [dependencies] -chrono = "0.4.9" blake2b_simd = "1.0.0" thiserror = "1.0" num-traits = "0.2" diff --git a/shared/src/actor/builtin.rs b/shared/src/actor/builtin.rs index 338a63405..eda60b112 100644 --- a/shared/src/actor/builtin.rs +++ b/shared/src/actor/builtin.rs @@ -84,8 +84,8 @@ impl TryFrom<&str> for Type { "multisig" => Type::Multisig, "reward" => Type::Reward, "verifiedregistry" => Type::VerifiedRegistry, - "sca" => Type::SCA, - "subnet" => Type::Subnet, + "hierarchical_sca" => Type::SCA, + "hierarchical_subnet" => Type::Subnet, _ => return Err(String::from("unrecognized actor type")), }; Ok(ret) diff --git a/testing/conformance/Cargo.toml b/testing/conformance/Cargo.toml index 3dc4593fc..fb222e956 100644 --- a/testing/conformance/Cargo.toml +++ b/testing/conformance/Cargo.toml @@ -9,7 +9,7 @@ publish = false repository = "https://github.com/filecoin-project/ref-fvm" [dependencies] -fvm = { version = "0.7.1", path = "../../fvm", default-features = false } +fvm = { version = "0.7.2", path = "../../fvm", default-features = false } fvm_shared = { version = "0.6.1", path = "../../shared" } fvm_ipld_hamt = { version = "0.5.1", path = "../../ipld/hamt"} fvm_ipld_amt = { version = "0.4.1", path = "../../ipld/amt"} @@ -47,7 +47,6 @@ serde_json = { version = "1.0", features = ["raw_value"] } walkdir = "2.3" regex = { version = "1.0" } ittapi-rs = { version = "0.1.6", optional = true } -actors-v6 = { version = "~6.3", package = "fil_builtin_actors_bundle" } actors-v7 = { version = "~7.3", package = "fil_builtin_actors_bundle" } libipld-core = { version = "0.13.1", features = ["serde-codec"] } 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..05c59ae8c 100644 --- a/testing/conformance/src/vm.rs +++ b/testing/conformance/src/vm.rs @@ -4,9 +4,9 @@ use std::convert::TryFrom; use cid::Cid; use futures::executor::block_on; use fvm::call_manager::{CallManager, DefaultCallManager, FinishRet, InvocationResult}; -use fvm::gas::{GasTracker, PriceList}; +use fvm::gas::{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(); @@ -98,10 +96,7 @@ impl TestMachine>> { } pub fn import_actors(blockstore: &MemoryBlockstore) -> BTreeMap { - let bundles = [ - (NetworkVersion::V14, actors_v6::BUNDLE_CAR), - (NetworkVersion::V15, actors_v7::BUNDLE_CAR), - ]; + let bundles = [(NetworkVersion::V15, actors_v7::BUNDLE_CAR)]; bundles .into_iter() .map(|(nv, car)| { @@ -488,17 +483,21 @@ where C: CallManager>, K: Kernel>, { - fn gas_used(&self) -> i64 { + fn gas_used(&self) -> Gas { self.0.gas_used() } - fn charge_gas(&mut self, name: &str, compute: i64) -> Result<()> { + fn charge_gas(&mut self, name: &str, compute: Gas) -> Result<()> { self.0.charge_gas(name, compute) } fn price_list(&self) -> &PriceList { self.0.price_list() } + + fn gas_available(&self) -> Gas { + self.0.gas_available() + } } impl MessageOps for TestKernel diff --git a/testing/conformance/test-vectors b/testing/conformance/test-vectors index 1cd64df0b..5a6e7ab9c 160000 --- a/testing/conformance/test-vectors +++ b/testing/conformance/test-vectors @@ -1 +1 @@ -Subproject commit 1cd64df0bbc7582d76acfff7b3534323604fc936 +Subproject commit 5a6e7ab9c2d7ffb5f54e5bd1a714ee5c1b7ad029 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/Cargo.toml b/testing/integration/Cargo.toml index 9404ee9e2..3ceba7a98 100644 --- a/testing/integration/Cargo.toml +++ b/testing/integration/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" repository = "https://github.com/filecoin-project/ref-fvm" [dependencies] -fvm = { version = "0.7.1", path = "../../fvm", default-features = false } +fvm = { version = "0.7.2", path = "../../fvm", default-features = false } fvm_shared = { version = "0.6.1", path = "../../shared" } fvm_ipld_hamt = { version = "0.5.1", path = "../../ipld/hamt"} fvm_ipld_amt = { version = "0.4.1", path = "../../ipld/amt"} @@ -39,6 +39,7 @@ features = ["cranelift", "pooling-allocator", "parallel-compilation"] wabt = "0.10.0" serde = { version = "1.0", features = ["derive"] } fil_hello_world_actor = { path = 'tests/fil-hello-world-actor', version = '0.1' } +fil_stack_overflow_actor = { path = 'tests/fil-stack-overflow-actor', version = '0.1' } [features] default = ["fvm/testing", "fvm_shared/testing"] diff --git a/testing/integration/examples/integration.rs b/testing/integration/examples/integration.rs index fc61b4046..485fbe3a4 100644 --- a/testing/integration/examples/integration.rs +++ b/testing/integration/examples/integration.rs @@ -1,5 +1,6 @@ use fvm::executor::{ApplyKind, Executor}; use fvm_integration_tests::tester::{Account, Tester}; +use fvm_ipld_blockstore::MemoryBlockstore; use fvm_ipld_encoding::tuple::*; use fvm_shared::address::Address; use fvm_shared::bigint::BigInt; @@ -25,7 +26,12 @@ struct State { pub fn main() { // Instantiate tester - let mut tester = Tester::new(NetworkVersion::V15, StateTreeVersion::V4).unwrap(); + let mut tester = Tester::new( + NetworkVersion::V15, + StateTreeVersion::V4, + MemoryBlockstore::default(), + ) + .unwrap(); let sender: [Account; 1] = tester.create_accounts().unwrap(); diff --git a/testing/integration/src/builtin.rs b/testing/integration/src/builtin.rs index 61f8c4df5..fef8a3aba 100644 --- a/testing/integration/src/builtin.rs +++ b/testing/integration/src/builtin.rs @@ -5,7 +5,7 @@ use cid::Cid; use futures::executor::block_on; use fvm::state_tree::{ActorState, StateTree}; use fvm::{init_actor, system_actor}; -use fvm_ipld_blockstore::{Blockstore, MemoryBlockstore}; +use fvm_ipld_blockstore::Blockstore; use fvm_ipld_car::load_car; use fvm_ipld_encoding::CborStore; use fvm_shared::actor::builtin::{load_manifest, Type}; @@ -16,14 +16,15 @@ 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 pub fn import_builtin_actors( - blockstore: &MemoryBlockstore, + blockstore: &impl Blockstore, ) -> Result> { BUNDLES .into_iter() @@ -39,7 +40,7 @@ pub fn import_builtin_actors( // Retrieve system, init and accounts actors code CID pub fn fetch_builtin_code_cid( - blockstore: &MemoryBlockstore, + blockstore: &impl Blockstore, builtin_actors: &Cid, ver: u32, ) -> Result<(Cid, Cid, Cid)> { diff --git a/testing/integration/src/tester.rs b/testing/integration/src/tester.rs index 92cc9b591..5613cd9cd 100644 --- a/testing/integration/src/tester.rs +++ b/testing/integration/src/tester.rs @@ -1,11 +1,11 @@ -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use cid::Cid; use fvm::call_manager::DefaultCallManager; use fvm::executor::DefaultExecutor; use fvm::machine::{DefaultMachine, Engine, Machine, NetworkConfig}; use fvm::state_tree::{ActorState, StateTree}; use fvm::{init_actor, system_actor, DefaultKernel}; -use fvm_ipld_blockstore::{Block, Blockstore, MemoryBlockstore}; +use fvm_ipld_blockstore::{Block, Blockstore}; use fvm_ipld_encoding::{ser, CborStore}; use fvm_ipld_hamt::Hamt; use fvm_shared::address::Address; @@ -13,6 +13,7 @@ use fvm_shared::econ::TokenAmount; use fvm_shared::state::StateTreeVersion; use fvm_shared::version::NetworkVersion; use fvm_shared::{ActorID, IPLD_RAW}; +use libsecp256k1::{PublicKey, SecretKey}; use multihash::Code; use crate::builtin::{ @@ -24,15 +25,14 @@ use crate::error::Error::{FailedToFlushTree, NoManifestInformation, NoRootCid}; const DEFAULT_BASE_FEE: u64 = 100; -trait Store: Blockstore + Sized {} +pub trait Store: Blockstore + Sized + 'static {} -pub type IntegrationExecutor = DefaultExecutor< - DefaultKernel>>, ->; +pub type IntegrationExecutor = + DefaultExecutor>>>; pub type Account = (ActorID, Address); -pub struct Tester { +pub struct Tester { // Network version used in the test nv: NetworkVersion, // Builtin actors root Cid used in the Machine @@ -42,16 +42,16 @@ pub struct Tester { // Custom code cid deployed by developer code_cids: Vec, // Executor used to interact with deployed actors. - pub executor: Option, + pub executor: Option>, // State tree constructed before instantiating the Machine - pub state_tree: StateTree, + pub state_tree: Option>, } -impl Tester { - pub fn new(nv: NetworkVersion, stv: StateTreeVersion) -> Result { - // Initialize blockstore - let blockstore = MemoryBlockstore::default(); - +impl Tester +where + B: Blockstore, +{ + pub fn new(nv: NetworkVersion, stv: StateTreeVersion, blockstore: B) -> Result { // Load the builtin actors bundles into the blockstore. let nv_actors = import_builtin_actors(&blockstore)?; @@ -92,21 +92,38 @@ impl Tester { builtin_actors, executor: None, code_cids: vec![], - state_tree, + state_tree: Some(state_tree), accounts_code_cid, }) } /// Creates new accounts in the testing context + /// Inserts the specified number of accounts in the state tree, all with 1000 FIL,returning their IDs and Addresses. pub fn create_accounts(&mut self) -> Result<[Account; N]> { - // Create accounts. - put_secp256k1_accounts(&mut self.state_tree, self.accounts_code_cid) + use rand::SeedableRng; + + let rng = &mut rand_chacha::ChaCha8Rng::seed_from_u64(8); + + let mut ret: [Account; N] = [(0, Address::default()); N]; + for account in ret.iter_mut().take(N) { + let priv_key = SecretKey::random(rng); + *account = self.make_secp256k1_account( + priv_key, + TokenAmount::from(10u8) * TokenAmount::from(1000), + )?; + } + Ok(ret) } /// Set a new state in the state tree pub fn set_state(&mut self, state: &S) -> Result { // Put state in tree - let state_cid = self.state_tree.store().put_cbor(state, Code::Blake2b256)?; + let state_cid = self + .state_tree + .as_mut() + .unwrap() + .store() + .put_cbor(state, Code::Blake2b256)?; Ok(state_cid) } @@ -121,11 +138,13 @@ impl Tester { ) -> Result<()> { // Register actor address self.state_tree + .as_mut() + .unwrap() .register_new_address(&actor_address) .unwrap(); // Put the WASM code into the blockstore. - let code_cid = put_wasm_code(self.state_tree.store(), wasm_bin)?; + let code_cid = put_wasm_code(self.state_tree.as_mut().unwrap().store(), wasm_bin)?; // Add code cid to list of deployed contract self.code_cids.push(code_cid); @@ -135,6 +154,8 @@ impl Tester { // Create actor self.state_tree + .as_mut() + .unwrap() .set_actor(&actor_address, actor_state) .map_err(anyhow::Error::from)?; @@ -143,22 +164,28 @@ impl Tester { /// Sets the Machine and the Executor in our Tester structure. pub fn instantiate_machine(&mut self) -> Result<()> { - // First flush tree and consume it - let state_root = self - .state_tree + // Take the state tree and leave None behind. + let mut state_tree = self.state_tree.take().unwrap(); + + // Calculate the state root. + let state_root = state_tree .flush() .map_err(anyhow::Error::from) .context(FailedToFlushTree)?; - let blockstore = self.state_tree.store(); + // Consume the state tree and take the blockstore. + let blockstore = state_tree.into_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)), - blockstore.clone(), + &Engine::new_default((&mc.network.clone()).into())?, + &mc, + blockstore, dummy::DummyExterns, )?; @@ -177,26 +204,23 @@ impl Tester { if self.executor.is_some() { self.executor.as_ref().unwrap().blockstore() } else { - self.state_tree.store() + self.state_tree.as_ref().unwrap().store() } } -} -/// Inserts the specified number of accounts in the state tree, all with 1000 FIL, -/// returning their IDs and Addresses. -fn put_secp256k1_accounts( - state_tree: &mut StateTree, - account_code_cid: Cid, -) -> Result<[Account; N]> { - use libsecp256k1::{PublicKey, SecretKey}; - use rand::SeedableRng; - - let rng = &mut rand_chacha::ChaCha8Rng::seed_from_u64(8); - - let mut ret: [Account; N] = [(0, Address::default()); N]; - for account in ret.iter_mut().take(N) { - let priv_key = SecretKey::random(rng); + + /// Put account with specified private key and balance + pub fn make_secp256k1_account( + &mut self, + priv_key: SecretKey, + init_balance: TokenAmount, + ) -> Result { let pub_key = PublicKey::from_secret_key(&priv_key); let pub_key_addr = Address::new_secp256k1(&pub_key.serialize())?; + + let state_tree = self + .state_tree + .as_mut() + .ok_or_else(|| anyhow!("unable get state tree"))?; let assigned_addr = state_tree.register_new_address(&pub_key_addr).unwrap(); let state = fvm::account_actor::State { address: pub_key_addr, @@ -205,23 +229,20 @@ fn put_secp256k1_accounts( let cid = state_tree.store().put_cbor(&state, Code::Blake2b256)?; let actor_state = ActorState { - code: account_code_cid, + code: self.accounts_code_cid, state: cid, sequence: 0, - balance: TokenAmount::from(10u8) * TokenAmount::from(1000), + balance: init_balance, }; state_tree .set_actor(&Address::new_id(assigned_addr), actor_state) .map_err(anyhow::Error::from)?; - - *account = (assigned_addr, pub_key_addr); + Ok((assigned_addr, pub_key_addr)) } - Ok(ret) } - /// Inserts the WASM code for the actor into the blockstore. -fn put_wasm_code(blockstore: &MemoryBlockstore, wasm_binary: &[u8]) -> Result { +fn put_wasm_code(blockstore: &impl Blockstore, wasm_binary: &[u8]) -> Result { let cid = blockstore.put( Code::Blake2b256, &Block { diff --git a/testing/integration/tests/fil-stack-overflow-actor/Cargo.toml b/testing/integration/tests/fil-stack-overflow-actor/Cargo.toml new file mode 100644 index 000000000..333f57121 --- /dev/null +++ b/testing/integration/tests/fil-stack-overflow-actor/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "fil_stack_overflow_actor" +version = "0.1.0" +edition = "2021" + +[dependencies] +fvm_sdk = { version = "0.6.1", path = "../../../../sdk" } +fvm_shared = { version = "0.6.1", path = "../../../../shared" } + + +[build-dependencies] +wasm-builder = "3.0.1" +wasmtime = "0.33.0" diff --git a/testing/integration/tests/fil-stack-overflow-actor/build.rs b/testing/integration/tests/fil-stack-overflow-actor/build.rs new file mode 100644 index 000000000..0f2aa8a56 --- /dev/null +++ b/testing/integration/tests/fil-stack-overflow-actor/build.rs @@ -0,0 +1,12 @@ +fn main() { + use wasm_builder::WasmBuilder; + WasmBuilder::new() + .with_current_project() + .import_memory() + .append_to_rust_flags("-Ctarget-feature=+crt-static") + .append_to_rust_flags("-Cpanic=abort") + .append_to_rust_flags("-Coverflow-checks=true") + .append_to_rust_flags("-Clto=true") + .append_to_rust_flags("-Copt-level=z") + .build() +} diff --git a/testing/integration/tests/fil-stack-overflow-actor/rust-toolchain b/testing/integration/tests/fil-stack-overflow-actor/rust-toolchain new file mode 100644 index 000000000..07ade694b --- /dev/null +++ b/testing/integration/tests/fil-stack-overflow-actor/rust-toolchain @@ -0,0 +1 @@ +nightly \ No newline at end of file diff --git a/testing/integration/tests/fil-stack-overflow-actor/src/lib.rs b/testing/integration/tests/fil-stack-overflow-actor/src/lib.rs new file mode 100644 index 000000000..2b60cb8c5 --- /dev/null +++ b/testing/integration/tests/fil-stack-overflow-actor/src/lib.rs @@ -0,0 +1,64 @@ +use fvm_sdk as sdk; +use fvm_shared::address::Address; +use fvm_shared::error::ExitCode; + +#[no_mangle] +pub fn invoke(_: u32) -> u32 { + let m = sdk::message::method_number(); + // If we start with method 1, we'll be over recursive send limit, starting + // with method 2 should be fine + if m > 1026 { + sdk::vm::abort(0x42, None); + } + + if m == 1 { + // if method 0, we want to run out of stack + recurse(m, 1000) + } else { + // 5 stack elems per level (wasm-instrument charges for highest use in the + // function) + some overhead mean that with the 2048 element wasm limit we + // can do 396 recursive calls while still being able do do a send at that + // depth + recurse(m, 396) + } +} + +// we need two recurse functions; just one gets optimized into wasm loop + +#[inline(never)] +pub fn recurse(m: u64, n: u64) -> u32 { + if n > 0 { + call_extern(); + + return recurse2(m, n - 1); + } + do_send(m) +} + +#[inline(never)] +pub fn recurse2(m: u64, n: u64) -> u32 { + if n > 0 { + call_extern(); + + return recurse(m, n - 1); + } + do_send(m) +} + +// external call to prevent the compiler from doing smart things +#[inline(never)] +pub fn call_extern() { + let _ = sdk::message::method_number(); +} + +#[inline(never)] +pub fn do_send(m: u64) -> u32 { + let r = sdk::send::send(&Address::new_id(10000), m + 1, Vec::new().into(), 0.into()); + match r { + Ok(rec) => match rec.exit_code { + ExitCode::OK => 0, + e => sdk::vm::abort(e.value() | 0x80000000, None), + }, + Err(e) => sdk::vm::abort((e as u32) | 0xc0000000, None), + } +} diff --git a/testing/integration/tests/lib.rs b/testing/integration/tests/lib.rs index 73884a507..887c10216 100644 --- a/testing/integration/tests/lib.rs +++ b/testing/integration/tests/lib.rs @@ -1,14 +1,23 @@ +use std::cell::RefCell; +use std::collections::HashSet; use std::env; +use cid::multihash::Multihash; +use cid::Cid; use fvm::executor::{ApplyKind, Executor}; -use fvm_integration_tests::tester::{Account, Tester}; +use fvm_integration_tests::tester::{Account, IntegrationExecutor, Tester}; +use fvm_ipld_blockstore::{Blockstore, MemoryBlockstore}; use fvm_ipld_encoding::tuple::*; +use fvm_ipld_encoding::DAG_CBOR; use fvm_shared::address::Address; use fvm_shared::bigint::BigInt; +use fvm_shared::error::{ErrorNumber, ExitCode}; use fvm_shared::message::Message; use fvm_shared::state::StateTreeVersion; use fvm_shared::version::NetworkVersion; +use fvm_shared::IDENTITY_HASH; use num_traits::Zero; +use wabt::wat2wasm; /// The state object. #[derive(Serialize_tuple, Deserialize_tuple, Clone, Debug, Default)] @@ -19,10 +28,18 @@ pub struct State { const WASM_COMPILED_PATH: &str = "../../target/debug/wbuild/fil_hello_world_actor/fil_hello_world_actor.compact.wasm"; +const WASM_COMPILED_PATH_OVERFLOW: &str = + "../../target/debug/wbuild/fil_stack_overflow_actor/fil_stack_overflow_actor.compact.wasm"; + #[test] fn hello_world() { // Instantiate tester - let mut tester = Tester::new(NetworkVersion::V15, StateTreeVersion::V4).unwrap(); + let mut tester = Tester::new( + NetworkVersion::V15, + StateTreeVersion::V4, + MemoryBlockstore::default(), + ) + .unwrap(); let sender: [Account; 1] = tester.create_accounts().unwrap(); @@ -65,3 +82,339 @@ fn hello_world() { assert_eq!(res.msg_receipt.exit_code.value(), 16) } + +#[test] +fn native_stack_overflow() { + // Instantiate tester + let mut tester = Tester::new( + NetworkVersion::V16, + StateTreeVersion::V4, + MemoryBlockstore::default(), + ) + .unwrap(); + + let sender: [Account; 1] = tester.create_accounts().unwrap(); + + // Get wasm bin + let wasm_path = env::current_dir() + .unwrap() + .join(WASM_COMPILED_PATH_OVERFLOW) + .canonicalize() + .unwrap(); + let wasm_bin = std::fs::read(wasm_path).expect("Unable to read file"); + + // Set actor state + let actor_state = State::default(); + 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(); + + let exec_test = |exec: &mut IntegrationExecutor, method| { + // Send message + let message = Message { + from: sender[0].1, + to: actor_address, + gas_limit: 10_000_000_000, + method_num: method, + sequence: method - 1, + ..Message::default() + }; + + let res = exec + .execute_message(message, ApplyKind::Explicit, 100) + .unwrap(); + + res.msg_receipt.exit_code.value() + }; + + let mut executor = tester.executor.unwrap(); + + // on method 0 the test actor should run out of stack + assert_eq!( + exec_test(&mut executor, 1), + ExitCode::SYS_ILLEGAL_INSTRUCTION.value() + ); + + // on method 1 the test actor should run out of recursive call limit + assert_eq!( + exec_test(&mut executor, 2), + 0xc0000000 + (ErrorNumber::LimitExceeded as u32) + ); + + // on method 2 the test actor should finish successfully + assert_eq!(exec_test(&mut executor, 3), 0x80000042); +} + +#[test] +fn out_of_gas() { + const WAT: &str = r#" + ;; Mock invoke function + (module + (memory (export "memory") 1) + (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, + MemoryBlockstore::default(), + ) + .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 + (memory (export "memory") 1) + (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, + MemoryBlockstore::default(), + ) + .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) +} + +#[test] +fn backtraces() { + // Note: this test **does not actually assert anything**, but it's useful to + // let us peep into FVM backtrace generation under different scenarios. + const WAT_ABORT: &str = r#" + (module + ;; ipld::open + (type (;0;) (func (param i32 i32) (result i32))) + (import "ipld" "open" (func $fvm_sdk::sys::ipld::open::syscall (type 0))) + ;; vm::abort + (type (;1;) (func (param i32 i32 i32) (result i32))) + (import "vm" "abort" (func $fvm_sdk::sys::vm::abort::syscall (type 1))) + (memory (export "memory") 1) + (func (export "invoke") (param $x i32) (result i32) + (i32.const 123) + (i32.const 123) + (call $fvm_sdk::sys::ipld::open::syscall) + (i32.const 0) + (i32.const 0) + (call $fvm_sdk::sys::vm::abort::syscall) + unreachable + ) + ) + "#; + + const WAT_FATAL: &str = r#" + (module + ;; ipld::open + (type (;0;) (func (param i32 i32) (result i32))) + (import "ipld" "open" (func $fvm_sdk::sys::ipld::open::syscall (type 0))) + ;; vm::abort + (type (;1;) (func (param i32 i32 i32) (result i32))) + (import "vm" "abort" (func $fvm_sdk::sys::vm::abort::syscall (type 1))) + (memory (export "memory") 1) + (func (export "invoke") (param $x i32) (result i32) + (i32.const 128) + (memory.grow) + (i32.const 4) + (i32.const 25493505) + (i32.store) + (i32.const 8) + (i32.const 0) + (i32.store) + (i32.const 4) + (call $fvm_sdk::sys::ipld::open::syscall) + (i32.const 0) + (i32.const 0) + (call $fvm_sdk::sys::vm::abort::syscall) + unreachable + ) + ) + "#; + + let blockstore = FailingBlockstore::default(); + let identity_cid = Cid::new_v1(DAG_CBOR, Multihash::wrap(IDENTITY_HASH, &[0]).unwrap()); + blockstore.add_fail(identity_cid); + + // Instantiate tester + let mut tester = Tester::new( + NetworkVersion::V16, + StateTreeVersion::V4, + MemoryBlockstore::default(), + ) + .unwrap(); + + let sender: [Account; 1] = tester.create_accounts().unwrap(); + + let state_cid = tester.set_state(&State { count: 0 }).unwrap(); + + // Set an actor that aborts. + let (wasm_abort, wasm_fatal) = (wat2wasm(WAT_ABORT).unwrap(), wat2wasm(WAT_FATAL).unwrap()); + let (abort_address, fatal_address) = (Address::new_id(10000), Address::new_id(10001)); + tester + .set_actor_from_bin(&wasm_abort, state_cid, abort_address, BigInt::zero()) + .unwrap(); + tester + .set_actor_from_bin(&wasm_fatal, state_cid, fatal_address, BigInt::zero()) + .unwrap(); + + // Instantiate machine + tester.instantiate_machine().unwrap(); + + // Send message + let message = Message { + from: sender[0].1, + to: abort_address, + gas_limit: 10_000_000, + method_num: 1, + ..Message::default() + }; + + let res = tester + .executor + .as_mut() + .unwrap() + .execute_message(message, ApplyKind::Explicit, 100) + .unwrap(); + + println!("abort backtrace: {}", res.failure_info.unwrap()); + + // Send message + let message = Message { + from: sender[0].1, + to: fatal_address, + gas_limit: 10_000_000, + method_num: 1, + sequence: 1, + ..Message::default() + }; + + let res = tester + .executor + .as_mut() + .unwrap() + .execute_message(message, ApplyKind::Explicit, 100) + .unwrap(); + + println!("fatal backtrace: {}", res.failure_info.unwrap()); +} + +#[derive(Default)] +pub struct FailingBlockstore { + fail_for: RefCell>, + target: MemoryBlockstore, +} + +impl FailingBlockstore { + pub fn add_fail(&self, cid: Cid) { + self.fail_for.borrow_mut().insert(cid); + } +} + +impl Blockstore for FailingBlockstore { + fn get(&self, k: &Cid) -> anyhow::Result>> { + self.target.get(k) + } + + fn put_keyed(&self, k: &Cid, block: &[u8]) -> anyhow::Result<()> { + self.target.put_keyed(k, block) + } +}