Skip to content

Commit

Permalink
WIP: Use ResourceLimiter
Browse files Browse the repository at this point in the history
  • Loading branch information
jayz22 committed Jul 13, 2023
1 parent a824e15 commit c0be886
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 120 deletions.
10 changes: 0 additions & 10 deletions Cargo.lock

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

5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ rev = "e6ba45c60c16de28c7522586b80ed0150157df73"

# [patch."https://github.com/stellar/rs-stellar-xdr"]
# stellar-xdr = { path = "../rs-stellar-xdr/" }
# [patch."https://github.com/stellar/wasmi"]
# soroban-wasmi = { path = "../wasmi/crates/wasmi/" }
# soroban-wasmi_core = { path = "../wasmi/crates/core/" }
[patch."https://github.com/stellar/wasmi"]
soroban-wasmi = { path = "../wasmi/crates/wasmi/" }

[profile.release]
codegen-units = 1
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 @@ -159,7 +159,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
145 changes: 94 additions & 51 deletions soroban-env-host/src/budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ use crate::{
Host, HostError,
};

use wasmi::FuelCosts;
use wasmi::{
errors::{MemoryError, TableError},
FuelCosts, ResourceLimiter, StoreLimits, StoreLimitsBuilder,
};

/// We provide a "cost model" object that evaluates a linear expression:
///
Expand Down Expand Up @@ -68,7 +71,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 @@ -204,7 +207,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 @@ -249,7 +252,7 @@ impl FuelConfig {
}
}

#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Clone)]
pub(crate) struct BudgetImpl {
pub cpu_insns: BudgetDimension,
pub mem_bytes: BudgetDimension,
Expand All @@ -258,6 +261,7 @@ pub(crate) struct BudgetImpl {
tracker: Vec<(u64, Option<u64>)>,
enabled: bool,
fuel_config: FuelConfig,
store_limits: StoreLimits,
}

impl BudgetImpl {
Expand All @@ -274,6 +278,8 @@ impl BudgetImpl {
tracker: vec![(0, None); ContractCostType::variants().len()],
enabled: true,
fuel_config: Default::default(),
// TODO: make this a const. Do we need to limit the number of memories too?
store_limits: StoreLimitsBuilder::new().memory_size(0x200000).build(),
};

b.init_tracker();
Expand Down Expand Up @@ -408,7 +414,7 @@ impl Display 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 @@ -469,7 +475,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 @@ -520,27 +532,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 @@ -649,7 +641,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 @@ -675,29 +667,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 All @@ -709,6 +678,26 @@ impl Budget {
costs.call = config.call;
Ok(costs)
}

pub fn wasmi_store_limits(&self) -> Result<StoreLimits, HostError> {
Ok(self.0.try_borrow_or_err()?.store_limits.clone())
}

pub fn get_store_limits_or_return_dummy(&self) -> StoreLimits {
match self.0.try_borrow() {
Ok(b) => b.store_limits.clone(),
// if borrow error happens, something is wrong so we create a dummy
// with minimal limits to fail asap
Err(_) => StoreLimitsBuilder::new()
.memory_size(0)
.table_elements(0)
.instances(0)
.tables(0)
.memories(0)
.trap_on_grow_failure(true)
.build(),
}
}
}

/// Default settings for local/sandbox testing only. The actual operations will use parameters
Expand All @@ -721,6 +710,7 @@ impl Default for BudgetImpl {
tracker: vec![(0, None); ContractCostType::variants().len()],
enabled: true,
fuel_config: Default::default(),
store_limits: Default::default(),
};

for ct in ContractCostType::variants() {
Expand Down Expand Up @@ -1014,3 +1004,56 @@ impl Default for BudgetImpl {
b
}
}

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

fn table_growing(
&mut self,
current: u32,
desired: u32,
maximum: Option<u32>,
) -> Result<bool, TableError> {
let mut store_limits =
self.as_budget()
.wasmi_store_limits()
.map_err(|_| TableError::GrowOutOfBounds {
maximum: 0,
current: 0,
delta: 0,
})?;
store_limits.table_growing(current, desired, maximum)
}

fn instances(&self) -> usize {
match self.as_budget().wasmi_store_limits() {
Ok(s) => s.instances(),
Err(_) => 0,
}
}

fn tables(&self) -> usize {
match self.as_budget().wasmi_store_limits() {
Ok(s) => s.tables(),
Err(_) => 0,
}
}

fn memories(&self) -> usize {
match self.as_budget().wasmi_store_limits() {
Ok(s) => s.memories(),
Err(_) => 0,
}
}
}
1 change: 1 addition & 0 deletions soroban-env-host/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ use soroban_env_common::xdr::{
ContractEntryBodyType, ContractIdPreimage, ContractIdPreimageFromAddress, ScContractInstance,
ScErrorCode, MASK_CONTRACT_DATA_FLAGS_V20,
};
use wasmi::ResourceLimiter;

use self::metered_clone::MeteredClone;
use self::{
Expand Down
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
2 changes: 1 addition & 1 deletion soroban-env-host/src/test/budget_metering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,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 c0be886

Please sign in to comment.