Skip to content

Commit

Permalink
Switch to wasmi native fuel metering
Browse files Browse the repository at this point in the history
Add wasmi memory fuel metering; all tests pass
  • Loading branch information
jayz22 committed Jun 1, 2023
1 parent 05fb4d5 commit 326c877
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 139 deletions.
15 changes: 7 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,16 @@ rev = "a2f370c930bb94f7bc0c9ea0426ecef3700b90a9"
default-features = false

[workspace.dependencies.wasmi]
package = "wasmi"
version = "0.29.0"
git = "https://github.com/paritytech/wasmi"
rev = "23d8d8c684255f2d63526baa348ab3eb3d249de0"
package = "soroban-wasmi"
# version = "0.16.0-soroban2"
git = "https://github.com/stellar/wasmi"
# rev = "862b32f5"

# [patch."https://github.com/stellar/rs-stellar-xdr"]
# stellar-xdr = { path = "../rs-stellar-xdr/" }

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

[profile.release]
codegen-units = 1
Expand Down
47 changes: 26 additions & 21 deletions soroban-env-common/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,32 +135,37 @@ impl From<stellar_xdr::Error> for Error {
}

#[cfg(feature = "wasmi")]
impl From<wasmi::Error> for Error {
fn from(e: wasmi::Error) -> Self {
if let wasmi::Error::Trap(trap) = e {
if let Some(code) = trap.trap_code() {
let ec = match code {
wasmi::core::TrapCode::UnreachableCodeReached => ScErrorCode::InternalError,
impl From<wasmi::core::TrapCode> for Error {
fn from(code: wasmi::core::TrapCode) -> Self {
let ec = match code {
wasmi::core::TrapCode::UnreachableCodeReached => ScErrorCode::InternalError,

wasmi::core::TrapCode::MemoryOutOfBounds
| wasmi::core::TrapCode::TableOutOfBounds => ScErrorCode::IndexBounds,
wasmi::core::TrapCode::MemoryOutOfBounds | wasmi::core::TrapCode::TableOutOfBounds => {
ScErrorCode::IndexBounds
}

wasmi::core::TrapCode::IndirectCallToNull => ScErrorCode::MissingValue,

wasmi::core::TrapCode::IndirectCallToNull => ScErrorCode::MissingValue,
wasmi::core::TrapCode::IntegerDivisionByZero
| wasmi::core::TrapCode::IntegerOverflow
| wasmi::core::TrapCode::BadConversionToInteger => ScErrorCode::ArithDomain,

wasmi::core::TrapCode::IntegerDivisionByZero
| wasmi::core::TrapCode::IntegerOverflow
| wasmi::core::TrapCode::BadConversionToInteger => ScErrorCode::ArithDomain,
wasmi::core::TrapCode::BadSignature => ScErrorCode::UnexpectedType,

wasmi::core::TrapCode::BadSignature => ScErrorCode::UnexpectedType,
wasmi::core::TrapCode::StackOverflow | wasmi::core::TrapCode::OutOfFuel => {
return Error::from_type_and_code(ScErrorType::Budget, ScErrorCode::ExceededLimit)
}
};
return Error::from_type_and_code(ScErrorType::WasmVm, ec);
}
}

wasmi::core::TrapCode::StackOverflow | wasmi::core::TrapCode::OutOfFuel => {
return Error::from_type_and_code(
ScErrorType::Budget,
ScErrorCode::ExceededLimit,
)
}
};
return Error::from_type_and_code(ScErrorType::WasmVm, ec);
#[cfg(feature = "wasmi")]
impl From<wasmi::Error> for Error {
fn from(e: wasmi::Error) -> Self {
if let wasmi::Error::Trap(trap) = e {
if let Some(code) = trap.trap_code() {
return code.into();
}
}
Error::from_type_and_code(ScErrorType::WasmVm, ScErrorCode::InternalError)
Expand Down
103 changes: 92 additions & 11 deletions soroban-env-host/src/budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use crate::{
Host, HostError,
};

use wasmi::FuelCosts;

/// We provide a "cost model" object that evaluates a linear expression:
///
/// f(x) = a + b * Option<x>
Expand Down Expand Up @@ -41,7 +43,6 @@ pub trait HostCostModel {
impl HostCostModel for ContractCostParamEntry {
fn evaluate(&self, input: Option<u64>) -> Result<u64, HostError> {
if self.const_term < 0 || self.linear_term < 0 {
// TODO: consider more concrete error code "invalid input"
return Err((ScErrorType::Context, ScErrorCode::InvalidInput).into());
}

Expand Down Expand Up @@ -151,6 +152,10 @@ impl BudgetDimension {
self.limit
}

pub fn get_remaining(&self) -> u64 {
self.limit.saturating_sub(self.total_count)
}

pub fn reset(&mut self, limit: u64) {
self.limit = limit;
self.total_count = 0;
Expand Down Expand Up @@ -233,8 +238,8 @@ impl BudgetImpl {
// type -- we leave the input as `None`, otherwise, we initialize the input to 0.
let i = ct as usize;
match ct {
ContractCostType::WasmInsnExec => (),
ContractCostType::WasmMemAlloc => self.tracker[i].1 = Some(0), // number of pages in wasm linear memory to allocate (each page is 64kB)
ContractCostType::WasmInsnExec => self.tracker[i].1 = Some(0), // number of "fuel" units wasmi consumes
ContractCostType::WasmMemAlloc => self.tracker[i].1 = Some(0), // number of "memory fuel" units wasmi consumes
ContractCostType::HostMemAlloc => self.tracker[i].1 = Some(0), // number of bytes in host memory to allocate
ContractCostType::HostMemCpy => self.tracker[i].1 = Some(0), // number of bytes in host to copy
ContractCostType::HostMemCmp => self.tracker[i].1 = Some(0), // number of bytes in host to compare
Expand Down Expand Up @@ -450,6 +455,15 @@ impl Budget {
self.charge_in_bulk(ty, 1, input)
}

pub fn apply_wasmi_fuels(&self, cpu_fuel: u64, mem_fuel: u64) -> Result<(), HostError> {
self.mut_budget(|mut b| {
b.cpu_insns
.charge(ContractCostType::WasmInsnExec, 1, Some(cpu_fuel))?;
b.mem_bytes
.charge(ContractCostType::WasmMemAlloc, 1, Some(mem_fuel))
})
}

/// 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
Expand Down Expand Up @@ -496,14 +510,22 @@ impl Budget {
f(&mut self.0.borrow_mut().tracker[ty as usize])
}

pub fn get_cpu_insns_count(&self) -> u64 {
pub fn get_cpu_insns_consumed(&self) -> u64 {
self.0.borrow().cpu_insns.get_total_count()
}

pub fn get_mem_bytes_count(&self) -> u64 {
pub fn get_mem_bytes_consumed(&self) -> u64 {
self.0.borrow().mem_bytes.get_total_count()
}

pub fn get_cpu_insns_remaining(&self) -> u64 {
self.0.borrow().cpu_insns.get_remaining()
}

pub fn get_mem_bytes_remaining(&self) -> u64 {
self.0.borrow().mem_bytes.get_remaining()
}

pub fn reset_default(&self) {
*self.0.borrow_mut() = BudgetImpl::default()
}
Expand Down Expand Up @@ -558,6 +580,54 @@ impl Budget {
})
.unwrap(); // impossible to panic
}

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
.borrow()
.cpu_insns
.get_cost_model(ContractCostType::WasmInsnExec)
.linear_term;

if cpu_per_fuel < 0 {
return Err((ScErrorType::Context, ScErrorCode::InvalidInput).into());
}
let cpu_per_fuel = (cpu_per_fuel as u64).max(1);
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
.borrow()
.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);
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) -> FuelCosts {
let mut costs = FuelCosts::default();
costs.base = 1;
costs.entity = 1;
costs.store = 1;
costs.call = 10;
costs
}
}

/// Default settings for local/sandbox testing only. The actual operations will use parameters
Expand All @@ -575,12 +645,21 @@ impl Default for BudgetImpl {
// define the cpu cost model parameters
let cpu = &mut b.cpu_insns.get_cost_model_mut(ct);
match ct {
// This is the host cpu insn cost per wasm "fuel". Every "base" wasm
// instruction costs 1 fuel (by default), and some particular types of
// instructions may cost additional amount of fuel based on
// wasmi's config setting.
ContractCostType::WasmInsnExec => {
cpu.const_term = 22;
cpu.linear_term = 0;
}
cpu.const_term = 0;
cpu.linear_term = 22; // this is calibrated
}
// Host cpu insns per wasm "memory fuel". This has to be zero since
// the fuel (representing cpu cost) has been covered by `WasmInsnExec`.
// The extra cost of mem processing is accounted for by wasmi's
// `config.memory_bytes_per_fuel` parameter.
// This type is designated to the mem cost.
ContractCostType::WasmMemAlloc => {
cpu.const_term = 521;
cpu.const_term = 0;
cpu.linear_term = 0;
}
ContractCostType::HostMemAlloc => {
Expand Down Expand Up @@ -667,13 +746,15 @@ impl Default for BudgetImpl {
// define the memory cost model parameters
let mem = b.mem_bytes.get_cost_model_mut(ct);
match ct {
// This type is designated to the cpu cost.
ContractCostType::WasmInsnExec => {
mem.const_term = 0;
mem.linear_term = 0;
}
// Bytes per wasmi "memory fuel".
ContractCostType::WasmMemAlloc => {
mem.const_term = 66136;
mem.linear_term = 1;
mem.const_term = 66136; // TODO: this seems to be 1 page overhead?
mem.linear_term = 1; // this is always 1 (1-to-1 between bytes in the host and in Wasmi)
}
ContractCostType::HostMemAlloc => {
mem.const_term = 8;
Expand Down
8 changes: 6 additions & 2 deletions soroban-env-host/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,17 @@ impl Host {
/// Helper for mutating the [`Budget`] held in this [`Host`], either to
/// allocate it on contract creation or to deplete it on callbacks from
/// the VM or host functions.
pub fn with_budget<T, F>(&self, f: F) -> T
pub(crate) fn with_budget<T, F>(&self, f: F) -> Result<T, HostError>
where
F: FnOnce(Budget) -> T,
F: FnOnce(Budget) -> Result<T, HostError>,
{
f(self.0.budget.clone())
}

// pub (crate) fn with_mut_budget<T, F>(&self, mut f: F) -> Result<T, HostError>
// where
// F: FnMut()

pub(crate) fn budget_ref(&self) -> &Budget {
&self.0.budget
}
Expand Down
9 changes: 9 additions & 0 deletions soroban-env-host/src/host/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,15 @@ impl Host {
)
}

pub(crate) fn err_wasmi_fuel_metering_disabled(&self) -> HostError {
self.err(
ScErrorType::WasmVm,
ScErrorCode::InternalError,
"wasmi fuel metering is disabled",
&[],
)
}

/// Given a result carrying some error type that can be converted to an
/// [Error] and supports [core::fmt::Debug], calls [Host::error] with the
/// error when there's an error, also passing the result of
Expand Down
31 changes: 18 additions & 13 deletions soroban-env-host/src/test/budget_metering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ fn xdr_object_conversion() -> Result<(), HostError> {
// we wind up double-counting the conversion of "objects".
// Possibly this should be improved in the future.
assert_eq!(budget.get_tracker(ContractCostType::ValXdrConv).0, 6);
assert_eq!(budget.get_cpu_insns_count(), 60);
assert_eq!(budget.get_mem_bytes_count(), 6);
});
assert_eq!(budget.get_cpu_insns_consumed(), 60);
assert_eq!(budget.get_mem_bytes_consumed(), 6);
Ok(())
})?;
Ok(())
}

Expand All @@ -63,9 +64,10 @@ fn vm_hostfn_invocation() -> Result<(), HostError> {
budget.get_tracker(ContractCostType::InvokeHostFunction).0,
2
);
assert_eq!(budget.get_cpu_insns_count(), 30);
assert_eq!(budget.get_mem_bytes_count(), 3);
});
assert_eq!(budget.get_cpu_insns_consumed(), 30);
assert_eq!(budget.get_mem_bytes_consumed(), 3);
Ok(())
})?;

Ok(())
}
Expand Down Expand Up @@ -96,15 +98,17 @@ fn metered_xdr() -> Result<(), HostError> {
budget.get_tracker(ContractCostType::ValSer).1,
Some(w.len() as u64)
);
});
Ok(())
})?;

host.metered_from_xdr::<ScMap>(w.as_slice())?;
host.with_budget(|budget| {
assert_eq!(
budget.get_tracker(ContractCostType::ValDeser).1,
Some(w.len() as u64)
);
});
Ok(())
})?;
Ok(())
}

Expand Down Expand Up @@ -155,7 +159,8 @@ fn map_insert_key_vec_obj() -> Result<(), HostError> {
assert_eq!(budget.get_tracker(ContractCostType::VisitObject).0, 4);
// upper bound of number of map-accesses, counting both binary-search and point-access.
assert_eq!(budget.get_tracker(ContractCostType::MapEntry).0, 5);
});
Ok(())
})?;

Ok(())
}
Expand Down Expand Up @@ -221,7 +226,7 @@ fn total_amount_charged_from_random_inputs() -> Result<(), HostError> {
let host = Host::default();

let tracker: Vec<(u64, Option<u64>)> = vec![
(246, None),
(1, Some(246)),
(1, Some(184)),
(1, Some(152)),
(1, Some(65)),
Expand Down Expand Up @@ -250,12 +255,12 @@ fn total_amount_charged_from_random_inputs() -> Result<(), HostError> {
let actual = format!("{:?}", host.as_budget());
expect![[r#"
=====================================================================================================================================================================
Cpu limit: 40000000; used: 8426807
Cpu limit: 40000000; used: 8426286
Mem limit: 52428800; used: 1219916
=====================================================================================================================================================================
CostType iterations input cpu_insns mem_bytes const_term_cpu lin_term_cpu const_term_mem lin_term_mem
WasmInsnExec 246 None 5412 0 22 0 0 0
WasmMemAlloc 1 Some(184) 521 66320 521 0 66136 1
WasmInsnExec 1 Some(246) 5412 0 0 22 0 0
WasmMemAlloc 1 Some(184) 0 66320 0 0 66136 1
HostMemAlloc 1 Some(152) 883 160 883 0 8 1
HostMemCpy 1 Some(65) 24 0 24 0 0 0
HostMemCmp 1 Some(74) 116 0 42 1 0 0
Expand Down
4 changes: 2 additions & 2 deletions soroban-env-host/src/test/hostile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ fn hostile_objs_traps() -> Result<(), HostError> {
let args: ScVec = host.test_scvec::<i32>(&[])?;

host.set_diagnostic_level(crate::DiagnosticLevel::Debug);
host.with_budget(|b| b.reset_default());
host.with_budget(|b| b.reset_unlimited_cpu());
host.with_budget(|b| Ok(b.reset_default()))?;
host.with_budget(|b| Ok(b.reset_unlimited_cpu()))?;

// This one should just run out of memory
let res = vm.invoke_function(&host, "objs", &args);
Expand Down
Loading

0 comments on commit 326c877

Please sign in to comment.