//! WASM execution engine using wasmtime. //! //! The engine compiles and executes WASM smart contracts with: //! - JIT compilation for performance //! - Fuel-based execution limits //! - Host function imports use std::collections::HashMap; use std::sync::Arc; use parking_lot::RwLock; use wasmtime::{ Config, Engine, Instance, Linker, Memory, Module, Store, StoreLimits, StoreLimitsBuilder, }; use crate::context::ExecutionContext; use crate::gas::GasMeter; use crate::storage::{ContractStorage, MemoryStorage}; use crate::{ContractId, ExecutionResult, VmError, MAX_CONTRACT_SIZE, MAX_MEMORY_PAGES}; /// Compiled contract module. #[derive(Clone)] pub struct ContractModule { /// Module hash (contract ID). pub id: ContractId, /// Original bytecode. pub bytecode: Vec, /// Compiled wasmtime module. module: Module, } impl ContractModule { /// Compiles a contract from WASM bytecode. pub fn compile(engine: &Engine, bytecode: Vec) -> Result { if bytecode.len() > MAX_CONTRACT_SIZE { return Err(VmError::BytecodeTooLarge { size: bytecode.len(), max: MAX_CONTRACT_SIZE, }); } // Compute contract ID from bytecode hash let hash: [u8; 32] = blake3::hash(&bytecode).into(); let id = ContractId::from_bytes(hash); // Compile the module let module = Module::new(engine, &bytecode).map_err(|e| VmError::InvalidBytecode(e.to_string()))?; Ok(ContractModule { id, bytecode, module, }) } /// Returns the module's exports. pub fn exports(&self) -> impl Iterator + '_ { self.module.exports().map(|e| (e.name(), e.ty())) } /// Checks if the module exports a function. pub fn has_export(&self, name: &str) -> bool { self.module .exports() .any(|e| e.name() == name && matches!(e.ty(), wasmtime::ExternType::Func(_))) } } /// Store data for WASM execution. pub struct StoreData { /// Gas meter. pub gas: GasMeter, /// Execution context. pub context: ExecutionContext, /// Store limits. pub limits: StoreLimits, /// Return data buffer. pub return_data: Vec, /// Error message if reverted. pub error: Option, } /// Instance of a running contract. pub struct VmInstance { /// The store containing execution state. store: Store, /// The instantiated module. instance: Instance, /// Memory export. memory: Memory, } impl VmInstance { /// Calls a function on the contract. pub fn call(&mut self, method: &str, args: &[u8]) -> Result { // Get the function export let func = self .instance .get_typed_func::<(i32, i32), i32>(&mut self.store, method) .map_err(|e| VmError::InvalidMethod(e.to_string()))?; // Write args to memory let args_ptr = self.write_to_memory(args)?; let args_len = args.len() as i32; // Call the function let result = func .call(&mut self.store, (args_ptr, args_len)) .map_err(|e| { // Check for gas exhaustion if self.store.data().gas.remaining() == 0 { VmError::OutOfGas { used: self.store.data().gas.consumed(), limit: self.store.data().gas.limit(), } } else if let Some(err) = &self.store.data().error { VmError::Revert(err.clone()) } else { VmError::ExecutionError(e.to_string()) } })?; // Read return data let return_data = if result >= 0 { self.read_from_memory(result as u32, self.store.data().return_data.len())? } else { Vec::new() }; // Extract storage changes from the pending storage operations let storage_changes = self.store.data().context.storage.pending_changes(); Ok(ExecutionResult { return_data, gas_used: self.store.data().gas.consumed(), logs: self.store.data().context.logs.clone(), storage_changes, internal_calls: Vec::new(), }) } /// Writes data to WASM memory. fn write_to_memory(&mut self, data: &[u8]) -> Result { let memory = self.memory; let data_ptr = memory.data_size(&self.store) as i32; // Grow memory if needed let pages_needed = ((data_ptr as usize + data.len()) / 65536) + 1; let current_pages = memory.size(&self.store) as usize; if pages_needed > current_pages { memory .grow(&mut self.store, (pages_needed - current_pages) as u64) .map_err(|e| VmError::MemoryViolation(e.to_string()))?; } // Write data memory.data_mut(&mut self.store)[data_ptr as usize..data_ptr as usize + data.len()] .copy_from_slice(data); Ok(data_ptr) } /// Reads data from WASM memory. fn read_from_memory(&self, ptr: u32, len: usize) -> Result, VmError> { let memory = self.memory; let data = memory.data(&self.store); if ptr as usize + len > data.len() { return Err(VmError::MemoryViolation(format!( "Read out of bounds: {} + {} > {}", ptr, len, data.len() ))); } Ok(data[ptr as usize..ptr as usize + len].to_vec()) } /// Returns gas consumed. pub fn gas_consumed(&self) -> u64 { self.store.data().gas.consumed() } /// Returns remaining gas. pub fn gas_remaining(&self) -> u64 { self.store.data().gas.remaining() } } /// The main VM engine. pub struct VmEngine { /// Wasmtime engine with configuration. engine: Engine, /// Compiled contract cache. modules: RwLock>>, /// Linker with host functions. linker: Linker, } impl VmEngine { /// Creates a new VM engine. pub fn new() -> Result { let mut config = Config::new(); config.consume_fuel(true); // Enable fuel-based execution limits config.wasm_bulk_memory(true); config.wasm_multi_value(true); config.wasm_reference_types(true); config.cranelift_opt_level(wasmtime::OptLevel::Speed); let engine = Engine::new(&config).map_err(|e| VmError::ExecutionError(e.to_string()))?; let mut linker = Linker::new(&engine); // Register host functions Self::register_host_functions(&mut linker)?; Ok(VmEngine { engine, modules: RwLock::new(HashMap::new()), linker, }) } /// Registers host functions with the linker. fn register_host_functions(linker: &mut Linker) -> Result<(), VmError> { // Storage read linker .func_wrap( "env", "synor_storage_read", |mut caller: wasmtime::Caller<'_, StoreData>, key_ptr: i32, out_ptr: i32| -> i32 { // Read key from memory let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); let mut key = [0u8; 32]; key.copy_from_slice( &memory.data(&caller)[key_ptr as usize..key_ptr as usize + 32], ); // Read from storage let data = caller.data_mut(); let storage_key = crate::storage::StorageKey::new(key); match data .context .storage .get(&data.context.call.contract, &storage_key) { Some(value) => { let len = value.0.len(); // Write to output buffer memory.data_mut(&mut caller)[out_ptr as usize..out_ptr as usize + len] .copy_from_slice(&value.0); len as i32 } None => -1, } }, ) .map_err(|e| VmError::ExecutionError(e.to_string()))?; // Storage write linker .func_wrap( "env", "synor_storage_write", |mut caller: wasmtime::Caller<'_, StoreData>, key_ptr: i32, value_ptr: i32, value_len: i32| -> i32 { let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); let mut key = [0u8; 32]; key.copy_from_slice( &memory.data(&caller)[key_ptr as usize..key_ptr as usize + 32], ); let value = memory.data(&caller) [value_ptr as usize..value_ptr as usize + value_len as usize] .to_vec(); let data = caller.data_mut(); if data.context.is_static() { return -1; } let storage_key = crate::storage::StorageKey::new(key); data.context.storage.set( &data.context.call.contract, storage_key, crate::storage::StorageValue::new(value), ); 0 }, ) .map_err(|e| VmError::ExecutionError(e.to_string()))?; // Get caller linker .func_wrap( "env", "synor_get_caller", |mut caller: wasmtime::Caller<'_, StoreData>, out_ptr: i32| { let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); let data = caller.data(); let address = data.context.caller(); memory.data_mut(&mut caller)[out_ptr as usize..out_ptr as usize + 32] .copy_from_slice(address.payload()); }, ) .map_err(|e| VmError::ExecutionError(e.to_string()))?; // Get value linker .func_wrap( "env", "synor_get_value", |caller: wasmtime::Caller<'_, StoreData>| -> i64 { caller.data().context.value() as i64 }, ) .map_err(|e| VmError::ExecutionError(e.to_string()))?; // Get timestamp linker .func_wrap( "env", "synor_get_timestamp", |caller: wasmtime::Caller<'_, StoreData>| -> i64 { caller.data().context.timestamp() as i64 }, ) .map_err(|e| VmError::ExecutionError(e.to_string()))?; // Get block height linker .func_wrap( "env", "synor_get_block_height", |caller: wasmtime::Caller<'_, StoreData>| -> i64 { caller.data().context.block_height() as i64 }, ) .map_err(|e| VmError::ExecutionError(e.to_string()))?; // Revert linker .func_wrap( "env", "synor_revert", |mut caller: wasmtime::Caller<'_, StoreData>, msg_ptr: i32, msg_len: i32| { let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); let msg_bytes = &memory.data(&caller) [msg_ptr as usize..msg_ptr as usize + msg_len as usize]; let msg = String::from_utf8_lossy(msg_bytes).to_string(); caller.data_mut().error = Some(msg); }, ) .map_err(|e| VmError::ExecutionError(e.to_string()))?; // Return linker .func_wrap( "env", "synor_return", |mut caller: wasmtime::Caller<'_, StoreData>, data_ptr: i32, data_len: i32| { let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); let data = memory.data(&caller) [data_ptr as usize..data_ptr as usize + data_len as usize] .to_vec(); caller.data_mut().return_data = data; }, ) .map_err(|e| VmError::ExecutionError(e.to_string()))?; Ok(()) } /// Compiles a contract module. pub fn compile(&self, bytecode: Vec) -> Result { ContractModule::compile(&self.engine, bytecode) } /// Caches a compiled module. pub fn cache_module(&self, module: ContractModule) { self.modules.write().insert(module.id, Arc::new(module)); } /// Gets a cached module. pub fn get_module(&self, id: &ContractId) -> Option> { self.modules.read().get(id).cloned() } /// Instantiates a contract for execution. pub fn instantiate( &self, module: &ContractModule, context: ExecutionContext, gas_limit: u64, ) -> Result { // Create store with limits let limits = StoreLimitsBuilder::new() .memory_size(MAX_MEMORY_PAGES as usize * 65536) .build(); let store_data = StoreData { gas: GasMeter::new(gas_limit), context, limits, return_data: Vec::new(), error: None, }; let mut store = Store::new(&self.engine, store_data); store.limiter(|data| &mut data.limits); store .set_fuel(gas_limit) .map_err(|e| VmError::ExecutionError(e.to_string()))?; // Instantiate let instance = self .linker .instantiate(&mut store, &module.module) .map_err(|e| VmError::ExecutionError(e.to_string()))?; // Get memory export let memory = instance .get_memory(&mut store, "memory") .ok_or_else(|| VmError::ExecutionError("No memory export".into()))?; Ok(VmInstance { store, instance, memory, }) } /// Executes a contract call. pub fn execute( &self, module: &ContractModule, method: &str, args: &[u8], context: ExecutionContext, gas_limit: u64, ) -> Result { let mut instance = self.instantiate(module, context, gas_limit)?; instance.call(method, args) } } impl Default for VmEngine { fn default() -> Self { Self::new().expect("Failed to create VM engine") } } #[cfg(test)] mod tests { use super::*; use crate::context::CallContext; use crate::storage::MemoryStorage; use synor_types::{Address, Network}; fn test_context() -> ExecutionContext { let contract = ContractId::from_bytes([0x42; 32]); let caller = Address::from_ed25519_pubkey(Network::Mainnet, &[0x11; 32]); let call = CallContext::new(contract, caller, 1000, vec![]); ExecutionContext::::testing(call) } #[test] fn test_engine_creation() { let engine = VmEngine::new(); assert!(engine.is_ok()); } #[test] fn test_compile_invalid() { let engine = VmEngine::new().unwrap(); let result = engine.compile(vec![0x00, 0x01, 0x02]); // Invalid WASM assert!(result.is_err()); } // Note: Testing actual WASM execution would require compiling Rust to WASM // which is beyond the scope of unit tests }