use std::collections::HashMap;
use wasmtime::{
Caller, Config, Engine, Error as WasmError, Linker, StoreLimits,
StoreLimitsBuilder,
};
use thiserror::Error;
use crate::config::MachineSpec;
#[derive(Debug, Clone)]
pub struct WasmLimits {
pub max_wasm_stack: usize,
pub memory_size: usize,
pub max_single_alloc: usize,
pub max_total_memory: usize,
pub max_table_elements: usize,
pub aggressive_compilation: bool,
}
impl Default for WasmLimits {
fn default() -> Self {
Self::build(4_096, 2)
}
}
impl WasmLimits {
pub fn build(ram_mb: u64, cpu_cores: usize) -> Self {
let memory_size = ((ram_mb / 512) as usize)
.saturating_mul(4 * 1024 * 1024)
.clamp(4 * 1024 * 1024, 32 * 1024 * 1024);
let max_total_memory = ((ram_mb / 512) as usize)
.saturating_mul(3 * 1024 * 1024)
.clamp(3 * 1024 * 1024, 24 * 1024 * 1024);
let max_single_alloc =
(max_total_memory / 3).clamp(1024 * 1024, 8 * 1024 * 1024);
let max_table_elements = (256 * cpu_cores.max(2)).min(2_048);
Self {
max_wasm_stack: 1024 * 1024, memory_size,
max_single_alloc,
max_total_memory,
max_table_elements,
aggressive_compilation: cpu_cores >= 4,
}
}
}
pub fn resolve_wasm_limits(spec: Option<MachineSpec>) -> WasmLimits {
let resolved = crate::config::resolve_spec(spec.as_ref());
WasmLimits::build(resolved.ram_mb, resolved.cpu_cores)
}
#[derive(Debug, Error, Clone)]
pub enum ContractError {
#[error("memory allocation failed: {details}")]
MemoryAllocationFailed { details: String },
#[error("invalid pointer: {pointer}")]
InvalidPointer { pointer: usize },
#[error("write out of bounds: offset {offset} >= allocation size {size}")]
WriteOutOfBounds { offset: usize, size: usize },
#[error("allocation size {size} exceeds maximum of {max} bytes")]
AllocationTooLarge { size: usize, max: usize },
#[error("total memory {total} exceeds maximum of {max} bytes")]
TotalMemoryExceeded { total: usize, max: usize },
#[error("memory allocation would overflow")]
AllocationOverflow,
#[error("linker error [{function}]: {details}")]
LinkerError {
function: &'static str,
details: String,
},
}
#[derive(Debug)]
pub struct MemoryManager {
memory: Vec<u8>,
map: HashMap<usize, usize>,
pub store_limits: StoreLimits,
max_single_alloc: usize,
max_total_memory: usize,
}
impl MemoryManager {
pub fn from_limits(limits: &WasmLimits) -> Self {
Self {
memory: Vec::new(),
map: HashMap::new(),
store_limits: StoreLimitsBuilder::new()
.memory_size(limits.memory_size)
.table_elements(limits.max_table_elements) .instances(1)
.tables(1)
.memories(1)
.trap_on_grow_failure(true)
.build(),
max_single_alloc: limits.max_single_alloc,
max_total_memory: limits.max_total_memory,
}
}
}
impl Default for MemoryManager {
fn default() -> Self {
Self::from_limits(&WasmLimits::default())
}
}
pub const MAX_FUEL: u64 = 10_000_000;
pub const MAX_FUEL_COMPILATION: u64 = 50_000_000;
impl MemoryManager {
pub fn alloc(&mut self, len: usize) -> Result<usize, ContractError> {
if len > self.max_single_alloc {
return Err(ContractError::AllocationTooLarge {
size: len,
max: self.max_single_alloc,
});
}
let current_len = self.memory.len();
let new_len = current_len
.checked_add(len)
.ok_or(ContractError::AllocationOverflow)?;
if new_len > self.max_total_memory {
return Err(ContractError::TotalMemoryExceeded {
total: new_len,
max: self.max_total_memory,
});
}
self.memory.resize(new_len, 0);
self.map.insert(current_len, len);
Ok(current_len)
}
pub fn write_byte(
&mut self,
start_ptr: usize,
offset: usize,
data: u8,
) -> Result<(), ContractError> {
let len = self
.map
.get(&start_ptr)
.ok_or(ContractError::InvalidPointer { pointer: start_ptr })?;
if offset >= *len {
return Err(ContractError::WriteOutOfBounds { offset, size: *len });
}
self.memory[start_ptr + offset] = data;
Ok(())
}
pub fn read_byte(&self, ptr: usize) -> Result<u8, ContractError> {
if ptr >= self.memory.len() {
return Err(ContractError::InvalidPointer { pointer: ptr });
}
Ok(self.memory[ptr])
}
pub fn read_data(&self, ptr: usize) -> Result<&[u8], ContractError> {
let len = self
.map
.get(&ptr)
.ok_or(ContractError::InvalidPointer { pointer: ptr })?;
Ok(&self.memory[ptr..ptr + len])
}
pub fn get_pointer_len(&self, ptr: usize) -> isize {
let Some(result) = self.map.get(&ptr) else {
return -1;
};
*result as isize
}
pub fn add_data_raw(
&mut self,
bytes: &[u8],
) -> Result<usize, ContractError> {
let ptr = self.alloc(bytes.len())?;
for (index, byte) in bytes.iter().enumerate() {
self.memory[ptr + index] = *byte;
}
Ok(ptr)
}
}
pub fn create_secure_wasmtime_config(limits: &WasmLimits) -> Config {
let mut config = Config::default();
config.consume_fuel(true);
config.max_wasm_stack(limits.max_wasm_stack);
let opt_level = if limits.aggressive_compilation {
wasmtime::OptLevel::SpeedAndSize
} else {
wasmtime::OptLevel::Speed
};
config.cranelift_opt_level(opt_level);
config
}
pub struct WasmRuntime {
pub engine: Engine,
pub limits: WasmLimits,
}
impl WasmRuntime {
pub fn new(spec: Option<MachineSpec>) -> Result<Self, wasmtime::Error> {
let limits = resolve_wasm_limits(spec);
let engine = Engine::new(&create_secure_wasmtime_config(&limits))?;
Ok(Self { engine, limits })
}
}
pub fn generate_linker(
engine: &Engine,
) -> Result<Linker<MemoryManager>, ContractError> {
let mut linker: Linker<MemoryManager> = Linker::new(engine);
linker
.func_wrap(
"env",
"pointer_len",
|caller: Caller<'_, MemoryManager>, pointer: i32| {
caller.data().get_pointer_len(pointer as usize) as u32
},
)
.map_err(|e| ContractError::LinkerError {
function: "pointer_len",
details: e.to_string(),
})?;
linker
.func_wrap(
"env",
"alloc",
|mut caller: Caller<'_, MemoryManager>,
len: u32|
-> Result<u32, WasmError> {
caller
.data_mut()
.alloc(len as usize)
.map(|ptr| ptr as u32)
.map_err(WasmError::from)
},
)
.map_err(|e| ContractError::LinkerError {
function: "alloc",
details: e.to_string(),
})?;
linker
.func_wrap(
"env",
"write_byte",
|mut caller: Caller<'_, MemoryManager>,
ptr: u32,
offset: u32,
data: u32|
-> Result<(), WasmError> {
caller
.data_mut()
.write_byte(ptr as usize, offset as usize, data as u8)
.map_err(WasmError::from)
},
)
.map_err(|e| ContractError::LinkerError {
function: "write_byte",
details: e.to_string(),
})?;
linker
.func_wrap(
"env",
"read_byte",
|caller: Caller<'_, MemoryManager>,
index: i32|
-> Result<u32, WasmError> {
let ptr = usize::try_from(index).map_err(|_| {
ContractError::InvalidPointer { pointer: 0 }
})?;
caller
.data()
.read_byte(ptr)
.map(|b| b as u32)
.map_err(WasmError::from)
},
)
.map_err(|e| ContractError::LinkerError {
function: "read_byte",
details: e.to_string(),
})?;
Ok(linker)
}