synor/crates/synor-vm/src/engine.rs
2026-01-08 05:22:24 +05:30

486 lines
16 KiB
Rust

//! 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<u8>,
/// Compiled wasmtime module.
module: Module,
}
impl ContractModule {
/// Compiles a contract from WASM bytecode.
pub fn compile(engine: &Engine, bytecode: Vec<u8>) -> Result<Self, VmError> {
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<Item = (&str, wasmtime::ExternType)> + '_ {
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<MemoryStorage>,
/// Store limits.
pub limits: StoreLimits,
/// Return data buffer.
pub return_data: Vec<u8>,
/// Error message if reverted.
pub error: Option<String>,
}
/// Instance of a running contract.
pub struct VmInstance {
/// The store containing execution state.
store: Store<StoreData>,
/// 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<ExecutionResult, VmError> {
// 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<i32, VmError> {
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<Vec<u8>, 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<HashMap<ContractId, Arc<ContractModule>>>,
/// Linker with host functions.
linker: Linker<StoreData>,
}
impl VmEngine {
/// Creates a new VM engine.
pub fn new() -> Result<Self, VmError> {
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<StoreData>) -> 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<u8>) -> Result<ContractModule, VmError> {
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<Arc<ContractModule>> {
self.modules.read().get(id).cloned()
}
/// Instantiates a contract for execution.
pub fn instantiate(
&self,
module: &ContractModule,
context: ExecutionContext<MemoryStorage>,
gas_limit: u64,
) -> Result<VmInstance, VmError> {
// 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<MemoryStorage>,
gas_limit: u64,
) -> Result<ExecutionResult, VmError> {
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<MemoryStorage> {
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::<MemoryStorage>::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
}