use std::future::Future;
use wasmtime::{AsContextMut, Caller, Func, FuncType, Trap, Val};
use super::StoreData;
#[derive(Clone, Copy, Debug)]
pub struct Rate {
fuel: u64,
per: u64,
}
impl Rate {
const fn per_unit(fuel: u64) -> Rate {
Rate { fuel, per: 1 }
}
const fn per_units(fuel: u64, per: u64) -> Rate {
Rate { fuel, per }
}
pub fn cost(self, n: u64) -> u64 {
n.saturating_mul(self.fuel).div_ceil(self.per)
}
}
pub const CALL: u64 = 16;
pub const COPY: Rate = Rate::per_units(1, 8);
pub const SCAN: Rate = Rate::per_unit(1);
pub const PARSE: Rate = Rate::per_unit(6);
pub const ELEM: Rate = Rate::per_unit(10);
pub const REGEX: Rate = Rate::per_unit(8);
pub const REGEX_COMPILE: u64 = 2_000;
pub const HASH: Rate = Rate::per_unit(4);
pub const IO: Rate = Rate::per_units(1, 4);
pub const SYSCALL: Rate = Rate::per_unit(400);
pub const TZ: u64 = 200;
pub const GATE: u64 = 400;
pub fn sort_cost(n: u64) -> u64 {
let log = u64::from(n.max(2).ilog2());
ELEM.cost(n.saturating_mul(log))
}
pub fn bigint_product_cost(a: u64, b: u64) -> u64 {
let (small, large) = (a.min(b).max(1), a.max(b).max(1));
let schoolbook = small.saturating_mul(large.min(64));
let beyond = large
.saturating_sub(64)
.saturating_mul(u64::from(large.ilog2()) + 1);
ELEM.cost(schoolbook.saturating_add(beyond))
}
pub fn charge_call(ctx: impl AsContextMut<Data = StoreData>) -> wasmtime::Result<()> {
charge_host_fuel(ctx, CALL)
}
pub fn charge(
ctx: impl AsContextMut<Data = StoreData>,
rate: Rate,
n: u64,
) -> wasmtime::Result<()> {
charge_host_fuel(ctx, rate.cost(n))
}
pub(crate) fn host_func(
mut store: impl AsContextMut<Data = StoreData>,
ty: FuncType,
body: impl Fn(&mut Caller<'_, StoreData>, &[Val], &mut [Val]) -> wasmtime::Result<()>
+ Send
+ Sync
+ 'static,
) -> Func {
let abi = ty.clone();
Func::new(&mut store, ty, move |mut caller, params, results| {
super::host::check_host_abi(&abi, params, results)?;
charge_call(&mut caller)?;
body(&mut caller, params, results)
})
}
pub(crate) fn host_func_async<F>(
mut store: impl AsContextMut<Data = StoreData>,
ty: FuncType,
body: F,
) -> Func
where
F: for<'a> Fn(
Caller<'a, StoreData>,
&'a [Val],
&'a mut [Val],
) -> Box<dyn Future<Output = wasmtime::Result<()>> + Send + 'a>
+ Send
+ Sync
+ 'static,
{
let abi = ty.clone();
Func::new_async(&mut store, ty, move |mut caller, params, results| {
if let Err(error) = super::host::check_host_abi(&abi, params, results) {
return Box::new(async move { Err(error) });
}
if let Err(error) = charge_call(&mut caller) {
return Box::new(async move { Err(error) });
}
body(caller, params, results)
})
}
pub fn settle(
ctx: impl AsContextMut<Data = StoreData>,
rate: Rate,
n: u64,
) -> wasmtime::Result<()> {
settle_host_fuel(ctx, rate.cost(n))
}
pub(crate) fn settle_result<T, C: AsContextMut<Data = StoreData>>(
caller: &mut C,
body: impl FnOnce(&mut C) -> wasmtime::Result<T>,
) -> wasmtime::Result<T> {
let previous = caller.as_context().data().settling_host_result;
caller.as_context_mut().data_mut().settling_host_result = true;
let result = body(caller);
caller.as_context_mut().data_mut().settling_host_result = previous;
result
}
pub fn settle_host_fuel(
mut ctx: impl AsContextMut<Data = StoreData>,
units: u64,
) -> wasmtime::Result<()> {
let mut ctx = ctx.as_context_mut();
let pending = ctx.data().host_fuel_pending;
let available = ctx.get_fuel()?.saturating_sub(pending);
if units <= available {
return charge_host_fuel(ctx, units);
}
ctx.set_fuel(0)?;
let data = ctx.data_mut();
data.host_fuel_pending = 0;
data.host_fuel = data.host_fuel.saturating_add(available);
Ok(())
}
pub const HOST_FUEL_BATCH: u64 = 10_000;
pub const HOST_FUEL_PENDING_CAP: u64 = 1_000 * HOST_FUEL_BATCH;
pub fn charge_host_fuel(
mut ctx: impl AsContextMut<Data = StoreData>,
units: u64,
) -> wasmtime::Result<()> {
if units == 0 {
return Ok(());
}
let mut ctx = ctx.as_context_mut();
let pending = ctx.data().host_fuel_pending;
let engine_fuel = ctx.get_fuel()?;
let available = engine_fuel.saturating_sub(pending);
if units > available {
ctx.set_fuel(0)?;
let data = ctx.data_mut();
data.host_fuel_pending = 0;
data.host_fuel = data.host_fuel.saturating_add(available);
if data.settling_host_result {
return Ok(());
}
return Err(Trap::OutOfFuel.into());
}
let batch = pending + units;
let data = ctx.data_mut();
data.host_fuel = data.host_fuel.saturating_add(units);
let wasm_burned = data
.host_fuel_applied_at
.map_or(u64::MAX, |mark| mark.saturating_sub(engine_fuel));
let apply = batch >= HOST_FUEL_PENDING_CAP
|| (batch >= HOST_FUEL_BATCH && wasm_burned > HOST_FUEL_BATCH);
if !apply {
data.host_fuel_pending = batch;
return Ok(());
}
data.host_fuel_pending = 0;
data.host_fuel_applied_at = Some(engine_fuel - batch);
ctx.set_fuel(engine_fuel - batch)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::{RuntimeConfig, Vfs};
use wasmtime::{Func, FuncType, Val, ValType};
#[test]
fn result_settlement_preserves_values_and_restores_refusing_charges() {
let config = RuntimeConfig::default();
let engine = config.engine().unwrap();
let mut store = config
.store(&engine, StoreData::with_vfs(Vfs::none()))
.unwrap();
let complete = Func::new(
&mut store,
FuncType::new(&engine, [], [ValType::I32]),
|mut caller, _, results| {
results[0] = settle_result(&mut caller, |caller| {
charge(caller, PARSE, 100)?;
Ok(Val::I32(42))
})?;
assert!(!caller.data().settling_host_result);
Ok(())
},
);
store.set_fuel(1).unwrap();
let mut results = [Val::I32(0)];
complete.call(&mut store, &[], &mut results).unwrap();
assert_eq!(results[0].i32(), Some(42));
assert_eq!(store.get_fuel().unwrap(), 0);
assert_eq!(store.data().host_fuel, 1);
assert!(charge_call(&mut store).unwrap_err().is::<Trap>());
let fail = Func::new(
&mut store,
FuncType::new(&engine, [], []),
|mut caller, _, _| {
let result = settle_result(&mut caller, |caller| {
charge(caller, COPY, 100)?;
Err::<(), _>(crate::runtime::host::range_error("copy entry limit"))
});
assert!(!caller.data().settling_host_result);
result
},
);
let error = fail.call(&mut store, &[], &mut []).unwrap_err();
assert!(error.to_string().contains("copy entry limit"), "{error}");
assert!(!store.data().settling_host_result);
assert!(charge_call(&mut store).unwrap_err().is::<Trap>());
}
#[test]
fn settling_never_refuses_and_leaves_a_short_budget_at_zero() {
let config = RuntimeConfig::default();
let engine = config.engine().unwrap();
let mut store = config
.store(&engine, StoreData::with_vfs(Vfs::none()))
.unwrap();
let settle = Func::new(
&mut store,
FuncType::new(&engine, [ValType::I64], []),
|mut caller, params, _| settle_host_fuel(&mut caller, params[0].unwrap_i64() as u64),
);
let charge = Func::new(
&mut store,
FuncType::new(&engine, [ValType::I64], []),
|mut caller, params, _| charge_host_fuel(&mut caller, params[0].unwrap_i64() as u64),
);
store.set_fuel(1_000).unwrap();
settle.call(&mut store, &[Val::I64(300)], &mut []).unwrap();
assert_eq!(store.get_fuel().unwrap(), 1_000);
assert_eq!(store.data().host_fuel_pending, 300);
assert_eq!(store.data().host_fuel, 300);
settle
.call(&mut store, &[Val::I64(5_000)], &mut [])
.unwrap();
assert_eq!(store.get_fuel().unwrap(), 0);
assert_eq!(store.data().host_fuel_pending, 0);
assert_eq!(store.data().host_fuel, 1_000);
let error = charge
.call(&mut store, &[Val::I64(1)], &mut [])
.unwrap_err();
assert_eq!(error.downcast_ref::<Trap>(), Some(&Trap::OutOfFuel));
}
#[test]
fn the_batch_covers_the_default_yield_interval() {
assert!(RuntimeConfig::default().async_yield_fuel <= Some(HOST_FUEL_BATCH));
}
#[test]
fn rates_round_up_and_never_make_work_free() {
assert_eq!(COPY.cost(0), 0);
assert_eq!(COPY.cost(1), 1);
assert_eq!(COPY.cost(8), 1);
assert_eq!(COPY.cost(9), 2);
assert_eq!(PARSE.cost(3), 18);
assert_eq!(SCAN.cost(u64::MAX), u64::MAX);
assert_eq!(sort_cost(0), 0);
assert_eq!(sort_cost(1), ELEM.cost(1));
assert_eq!(sort_cost(1024), ELEM.cost(10 * 1024));
}
#[test]
fn host_charges_are_batched_counted_and_refused_before_the_work() {
use crate::runtime::limits::ExecutionUsage;
let config = RuntimeConfig::default();
let engine = config.engine().unwrap();
let mut store = config
.store(&engine, StoreData::with_vfs(Vfs::none()))
.unwrap();
let charge = Func::new(
&mut store,
FuncType::new(&engine, [ValType::I64], []),
|mut caller, params, _| charge_host_fuel(&mut caller, params[0].unwrap_i64() as u64),
);
let charge_units = |store: &mut wasmtime::Store<StoreData>, units: u64| {
charge.call(&mut *store, &[Val::I64(units as i64)], &mut [])
};
let burn = |store: &mut wasmtime::Store<StoreData>, units: u64| {
let left = store.get_fuel().unwrap() - units;
store.set_fuel(left).unwrap();
};
store.set_fuel(100_000).unwrap();
charge_units(&mut store, 9_999).unwrap();
assert_eq!(store.get_fuel().unwrap(), 100_000);
assert_eq!(store.data().host_fuel, 9_999);
assert_eq!(store.data().host_fuel_pending, 9_999);
let usage = ExecutionUsage::capture(&store, 100_000).unwrap();
assert_eq!(
(usage.fuel, usage.wasm_fuel, usage.host_fuel),
(9_999, 0, 9_999)
);
charge_units(&mut store, 1).unwrap();
assert_eq!(store.get_fuel().unwrap(), 90_000);
assert_eq!(store.data().host_fuel_pending, 0);
charge_units(&mut store, 20_000).unwrap();
assert_eq!(store.get_fuel().unwrap(), 90_000);
assert_eq!(store.data().host_fuel_pending, 20_000);
burn(&mut store, 10_000);
charge_units(&mut store, 1).unwrap();
assert_eq!(store.get_fuel().unwrap(), 80_000);
burn(&mut store, 1);
charge_units(&mut store, 1).unwrap();
assert_eq!(store.get_fuel().unwrap(), 59_997);
assert_eq!(store.data().host_fuel_pending, 0);
let charged_so_far = store.data().host_fuel;
store.set_fuel(300).unwrap();
charge_units(&mut store, 100).unwrap();
let error = charge_units(&mut store, 201).unwrap_err();
assert_eq!(error.downcast_ref::<Trap>(), Some(&Trap::OutOfFuel));
assert_eq!(store.get_fuel().unwrap(), 0);
assert_eq!(store.data().host_fuel, charged_so_far + 100 + 200);
assert_eq!(store.data().host_fuel_pending, 0);
store.set_fuel(1_000).unwrap();
store.data_mut().host_fuel = 0;
charge_units(&mut store, 600).unwrap();
burn(&mut store, 700);
let error = charge_units(&mut store, 1).unwrap_err();
assert_eq!(error.downcast_ref::<Trap>(), Some(&Trap::OutOfFuel));
assert_eq!(store.data().host_fuel, 600);
let usage = ExecutionUsage::capture(&store, 1_000).unwrap();
assert_eq!(
(usage.fuel, usage.wasm_fuel, usage.host_fuel),
(1_000, 400, 600)
);
store.set_fuel(50).unwrap();
charge_units(&mut store, 0).unwrap();
assert_eq!(store.get_fuel().unwrap(), 50);
}
}