486 lines
16 KiB
Rust
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
|
|
}
|