Skip to content

Commit

Permalink
Adapt to ResourceLimiter, replacing mem_fuel metering (#950)
Browse files Browse the repository at this point in the history
* Update wasmi and preliminary cleanup

* Adapt to `ResourceLimiter`, replacing `mem_fuel` metering

* fixup! Adapt to `ResourceLimiter`, replacing `mem_fuel` metering
  • Loading branch information
jayz22 authored Jul 14, 2023
1 parent d099f93 commit bd7b935
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 121 deletions.
13 changes: 3 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ default-features = false
package = "soroban-wasmi"
version = "0.30.0-soroban"
git = "https://github.com/stellar/wasmi"
rev = "3dc639fde3bebf0bf364a9fa4ac2f0efb7ee9995"
rev = "284c963ba080703061797e2a3cba0853edee0dd4"

[workspace.dependencies.stellar-strkey]
version = "0.0.7"
Expand Down
4 changes: 3 additions & 1 deletion soroban-env-common/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ impl From<wasmi::core::TrapCode> for Error {

wasmi::core::TrapCode::BadSignature => ScErrorCode::UnexpectedType,

wasmi::core::TrapCode::StackOverflow | wasmi::core::TrapCode::OutOfFuel => {
wasmi::core::TrapCode::StackOverflow
| wasmi::core::TrapCode::OutOfFuel
| wasmi::core::TrapCode::GrowthOperationLimited => {
return Error::from_type_and_code(ScErrorType::Budget, ScErrorCode::ExceededLimit)
}
};
Expand Down
2 changes: 1 addition & 1 deletion soroban-env-host/benches/common/measure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ impl Measurements {
///
/// f(x) = N_x * (a + b * Option<x>) [1]
///
/// The `N_x` here is batch size if the host is doing `batched_charge` for the
/// The `N_x` here is batch size if the host is doing `bulk_charge` for the
/// corresponding `x`. The goal of the HCM is to record the relation of x and
/// f(x) in order for a, b -- the constant and linear cost parameters -- to be
/// extracted. In the ideal setup, we pass in an array of samples with various
Expand Down
148 changes: 97 additions & 51 deletions soroban-env-host/src/budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
Error, Host, HostError, DEFAULT_HOST_DEPTH_LIMIT,
};

use wasmi::FuelCosts;
use wasmi::{errors, FuelCosts, ResourceLimiter};

/// We provide a "cost model" object that evaluates a linear expression:
///
Expand Down Expand Up @@ -69,7 +69,7 @@ impl HostCostModel for ContractCostParamEntry {
}
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Clone)]
pub struct BudgetDimension {
/// A set of cost models that map input values (eg. event counts, object
/// sizes) from some CostType to whatever concrete resource type is being
Expand Down Expand Up @@ -205,7 +205,7 @@ impl BudgetDimension {
/// doesn't derive all the traits we want. These fields (coarsely) define the
/// relative costs of different wasm instruction types and are for wasmi internal
/// fuel metering use only. Units are in "fuels".
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Clone)]
pub(crate) struct FuelConfig {
/// The base fuel costs for all instructions.
pub base: u64,
Expand Down Expand Up @@ -250,7 +250,21 @@ impl FuelConfig {
}
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct WasmiLimits {
pub table_elements: u32,
pub instances: usize,
pub tables: usize,
pub memories: usize,
}

pub(crate) const WASMI_LIMITS_CONFIG: WasmiLimits = WasmiLimits {
table_elements: 1000,
instances: 1,
tables: 1,
memories: 1,
};

#[derive(Clone)]
pub(crate) struct BudgetImpl {
pub cpu_insns: BudgetDimension,
pub mem_bytes: BudgetDimension,
Expand Down Expand Up @@ -435,7 +449,7 @@ impl DepthLimiter for BudgetImpl {
}
}

#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Default, Clone)]
pub struct Budget(pub(crate) Rc<RefCell<BudgetImpl>>);

impl Debug for Budget {
Expand Down Expand Up @@ -508,7 +522,13 @@ impl Budget {
f(self.0.try_borrow_mut_or_err()?)
}

fn charge_in_bulk(
/// Performs a bulk charge to the budget under the specified [`CostType`].
/// The `iterations` is the batch size. The caller needs to ensure:
/// 1. the batched charges have identical costs (having the same
/// [`CostType`] and `input`)
/// 2. The input passed in (Some/None) is consistent with the [`CostModel`]
/// underneath the [`CostType`] (linear/constant).
pub fn bulk_charge(
&self,
ty: ContractCostType,
iterations: u64,
Expand Down Expand Up @@ -559,27 +579,7 @@ impl Budget {
/// Otherwise it is a linear model. The caller needs to ensure the input
/// passed is consistent with the inherent model underneath.
pub fn charge(&self, ty: ContractCostType, input: Option<u64>) -> Result<(), HostError> {
self.charge_in_bulk(ty, 1, input)
}

pub fn apply_wasmi_fuels(&self, cpu_fuel: u64, mem_fuel: u64) -> Result<(), HostError> {
self.charge_in_bulk(ContractCostType::WasmInsnExec, cpu_fuel, None)?;
self.charge_in_bulk(ContractCostType::WasmMemAlloc, mem_fuel, None)
}

/// Performs a bulk charge to the budget under the specified [`CostType`].
/// The `iterations` is the batch size. The caller needs to ensure:
/// 1. the batched charges have identical costs (having the same
/// [`CostType`] and `input`)
/// 2. The input passed in (Some/None) is consistent with the [`CostModel`]
/// underneath the [`CostType`] (linear/constant).
pub fn batched_charge(
&self,
ty: ContractCostType,
iterations: u64,
input: Option<u64>,
) -> Result<(), HostError> {
self.charge_in_bulk(ty, iterations, input)
self.bulk_charge(ty, 1, input)
}

pub fn with_free_budget<F, T>(&self, f: F) -> Result<T, HostError>
Expand Down Expand Up @@ -688,7 +688,7 @@ impl Budget {
Ok(())
}

fn get_cpu_insns_remaining_as_fuel(&self) -> Result<u64, HostError> {
pub(crate) fn get_cpu_insns_remaining_as_fuel(&self) -> Result<u64, HostError> {
let cpu_remaining = self.get_cpu_insns_remaining()?;
let cpu_per_fuel = self
.0
Expand All @@ -714,29 +714,6 @@ impl Budget {
Ok(cpu_remaining / cpu_per_fuel)
}

fn get_mem_bytes_remaining_as_fuel(&self) -> Result<u64, HostError> {
let bytes_remaining = self.get_mem_bytes_remaining()?;
let bytes_per_fuel = self
.0
.try_borrow_or_err()?
.mem_bytes
.get_cost_model(ContractCostType::WasmMemAlloc)
.linear_term;

if bytes_per_fuel < 0 {
return Err((ScErrorType::Context, ScErrorCode::InvalidInput).into());
}
let bytes_per_fuel = (bytes_per_fuel as u64).max(1);
// See comment about rounding above.
Ok(bytes_remaining / bytes_per_fuel)
}

pub fn get_fuels_budget(&self) -> Result<(u64, u64), HostError> {
let cpu_fuel = self.get_cpu_insns_remaining_as_fuel()?;
let mem_fuel = self.get_mem_bytes_remaining_as_fuel()?;
Ok((cpu_fuel, mem_fuel))
}

// generate a wasmi fuel cost schedule based on our calibration
pub fn wasmi_fuel_costs(&self) -> Result<FuelCosts, HostError> {
let config = &self.0.try_borrow_or_err()?.fuel_config;
Expand Down Expand Up @@ -1054,3 +1031,72 @@ impl Default for BudgetImpl {
b
}
}

impl ResourceLimiter for Host {
fn memory_growing(
&mut self,
_current: usize,
desired: usize,
maximum: Option<usize>,
) -> Result<bool, errors::MemoryError> {
let host_limit = self
.as_budget()
.get_mem_bytes_remaining()
.map_err(|_| errors::MemoryError::OutOfBoundsGrowth)?;

let allow = if desired as u64 > host_limit {
false
} else {
match maximum {
Some(max) => desired <= max,
None => true,
}
};

if allow {
self.as_budget()
.bulk_charge(ContractCostType::WasmMemAlloc, desired as u64, None)
.map(|_| true)
.map_err(|_| errors::MemoryError::OutOfBoundsGrowth)
} else {
Err(errors::MemoryError::OutOfBoundsGrowth)
}
}

fn table_growing(
&mut self,
current: u32,
desired: u32,
maximum: Option<u32>,
) -> Result<bool, errors::TableError> {
let allow = if desired > WASMI_LIMITS_CONFIG.table_elements {
false
} else {
match maximum {
Some(max) => desired <= max,
None => true,
}
};
if allow {
Ok(allow)
} else {
Err(errors::TableError::GrowOutOfBounds {
maximum: maximum.unwrap_or(u32::MAX),
current,
delta: desired - current,
})
}
}

fn instances(&self) -> usize {
WASMI_LIMITS_CONFIG.instances
}

fn tables(&self) -> usize {
WASMI_LIMITS_CONFIG.tables
}

fn memories(&self) -> usize {
WASMI_LIMITS_CONFIG.memories
}
}
10 changes: 5 additions & 5 deletions soroban-env-host/src/host/metered_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,18 @@ where
{
fn charge_access<B: AsBudget>(&self, count: usize, b: &B) -> Result<(), HostError> {
b.as_budget()
.batched_charge(ContractCostType::MapEntry, count as u64, None)
.bulk_charge(ContractCostType::MapEntry, count as u64, None)
}

fn charge_scan<B: AsBudget>(&self, b: &B) -> Result<(), HostError> {
b.as_budget()
.batched_charge(ContractCostType::MapEntry, self.map.len() as u64, None)
.bulk_charge(ContractCostType::MapEntry, self.map.len() as u64, None)
}

fn charge_binsearch<B: AsBudget>(&self, b: &B) -> Result<(), HostError> {
let mag = 64 - (self.map.len() as u64).leading_zeros();
b.as_budget()
.batched_charge(ContractCostType::MapEntry, 1 + mag as u64, None)
.bulk_charge(ContractCostType::MapEntry, 1 + mag as u64, None)
}
}

Expand Down Expand Up @@ -337,7 +337,7 @@ where
a: &MeteredOrdMap<K, V, Host>,
b: &MeteredOrdMap<K, V, Host>,
) -> Result<Ordering, Self::Error> {
self.as_budget().batched_charge(
self.as_budget().bulk_charge(
ContractCostType::MapEntry,
a.map.len().min(b.map.len()) as u64,
None,
Expand All @@ -357,7 +357,7 @@ where
a: &MeteredOrdMap<K, V, Budget>,
b: &MeteredOrdMap<K, V, Budget>,
) -> Result<Ordering, Self::Error> {
self.batched_charge(
self.bulk_charge(
ContractCostType::MapEntry,
a.map.len().min(b.map.len()) as u64,
None,
Expand Down
10 changes: 5 additions & 5 deletions soroban-env-host/src/host/metered_vector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@ where
A: DeclaredSizeForMetering,
{
fn charge_access(&self, count: usize, budget: &Budget) -> Result<(), HostError> {
budget.batched_charge(ContractCostType::VecEntry, count as u64, None)
budget.bulk_charge(ContractCostType::VecEntry, count as u64, None)
}

fn charge_scan(&self, budget: &Budget) -> Result<(), HostError> {
budget.batched_charge(ContractCostType::VecEntry, self.vec.len() as u64, None)
budget.bulk_charge(ContractCostType::VecEntry, self.vec.len() as u64, None)
}

fn charge_binsearch(&self, budget: &Budget) -> Result<(), HostError> {
let mag = 64 - (self.vec.len() as u64).leading_zeros();
budget.batched_charge(ContractCostType::VecEntry, 1 + mag as u64, None)
budget.bulk_charge(ContractCostType::VecEntry, 1 + mag as u64, None)
}
}

Expand Down Expand Up @@ -335,7 +335,7 @@ where
a: &MeteredVector<Elt>,
b: &MeteredVector<Elt>,
) -> Result<Ordering, Self::Error> {
self.as_budget().batched_charge(
self.as_budget().bulk_charge(
ContractCostType::VecEntry,
a.vec.len().min(b.vec.len()) as u64,
None,
Expand All @@ -355,7 +355,7 @@ where
a: &MeteredVector<Elt>,
b: &MeteredVector<Elt>,
) -> Result<Ordering, Self::Error> {
self.as_budget().batched_charge(
self.as_budget().bulk_charge(
ContractCostType::VecEntry,
a.vec.len().min(b.vec.len()) as u64,
None,
Expand Down
5 changes: 3 additions & 2 deletions soroban-env-host/src/test/budget_metering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ fn xdr_object_conversion() -> Result<(), HostError> {
fn vm_hostfn_invocation() -> Result<(), HostError> {
let host = Host::test_host_with_recording_footprint();
let id_obj = host.register_test_contract_wasm(VEC);
// this contract requests initial pages = 16 worth of linear memory, not sure why
let host = host
.test_budget(100_000, 100_000)
.test_budget(100_000, 1_048_576)
.enable_model(ContractCostType::InvokeVmFunction, 10, 0, 1, 0)
.enable_model(ContractCostType::InvokeHostFunction, 10, 0, 1, 0);

Expand Down Expand Up @@ -262,7 +263,7 @@ fn total_amount_charged_from_random_inputs() -> Result<(), HostError> {
];

for ty in ContractCostType::variants() {
host.with_budget(|b| b.batched_charge(ty, tracker[ty as usize].0, tracker[ty as usize].1))?;
host.with_budget(|b| b.bulk_charge(ty, tracker[ty as usize].0, tracker[ty as usize].1))?;
}
let actual = format!("{:?}", host.as_budget());
expect![[r#"
Expand Down
Loading

0 comments on commit bd7b935

Please sign in to comment.