From 5ebb99e91dd01d858f1fba74d7b473535e4d7552 Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 09:41:23 +0000 Subject: [PATCH 1/4] feat(challenge-sdk-wasm): add huge-arena feature for 16 MiB arena allocation --- crates/challenge-sdk-wasm/Cargo.toml | 1 + crates/challenge-sdk-wasm/src/alloc_impl.rs | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/challenge-sdk-wasm/Cargo.toml b/crates/challenge-sdk-wasm/Cargo.toml index 1f122200..dbf51c8d 100644 --- a/crates/challenge-sdk-wasm/Cargo.toml +++ b/crates/challenge-sdk-wasm/Cargo.toml @@ -14,3 +14,4 @@ bincode = { version = "1.3", default-features = false } [features] default = [] large-arena = [] # 4 MiB arena instead of the default 1 MiB +huge-arena = [] # 16 MiB arena for complex challenges with large payloads diff --git a/crates/challenge-sdk-wasm/src/alloc_impl.rs b/crates/challenge-sdk-wasm/src/alloc_impl.rs index 9ddb51ff..b4e7a9d7 100644 --- a/crates/challenge-sdk-wasm/src/alloc_impl.rs +++ b/crates/challenge-sdk-wasm/src/alloc_impl.rs @@ -1,9 +1,12 @@ use core::cell::UnsafeCell; -#[cfg(feature = "large-arena")] +#[cfg(feature = "huge-arena")] +const ARENA_SIZE: usize = 16 * 1024 * 1024; + +#[cfg(all(feature = "large-arena", not(feature = "huge-arena")))] const ARENA_SIZE: usize = 4 * 1024 * 1024; -#[cfg(not(feature = "large-arena"))] +#[cfg(not(any(feature = "large-arena", feature = "huge-arena")))] const ARENA_SIZE: usize = 1024 * 1024; struct BumpAllocator { From 337d8bba77f612f7b1ce298dfb174ce7ecc6650b Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 09:43:23 +0000 Subject: [PATCH 2/4] fix(wasm-runtime): align http_get host function signature with guest SDK The host registered http_get with 3 params (req_ptr, req_len, resp_ptr) but the guest SDK declares 4 params (req_ptr, req_len, resp_ptr, resp_len). This mismatch caused WASM instantiation failures with 'incompatible import type' when a module used http_get. Added resp_len parameter to both the linker closure and handle_http_get function. Removed the now-unused DEFAULT_RESPONSE_BUF_SIZE constant. --- bins/validator-node/src/wasm_executor.rs | 9 +- crates/wasm-runtime-interface/src/bridge.rs | 4 +- crates/wasm-runtime-interface/src/network.rs | 8 +- crates/wasm-runtime-interface/src/storage.rs | 119 +++++++++++++++++++ 4 files changed, 131 insertions(+), 9 deletions(-) diff --git a/bins/validator-node/src/wasm_executor.rs b/bins/validator-node/src/wasm_executor.rs index b88b09e7..9aa743db 100644 --- a/bins/validator-node/src/wasm_executor.rs +++ b/bins/validator-node/src/wasm_executor.rs @@ -8,9 +8,8 @@ use std::time::Instant; use tracing::{debug, info}; use wasm_runtime_interface::{ ConsensusPolicy, ExecPolicy, InMemoryStorageBackend, InstanceConfig, NetworkHostFunctions, - NetworkPolicy, NoopStorageBackend, RuntimeConfig, SandboxHostFunctions, SandboxPolicy, - StorageHostConfig, StorageHostState, TerminalPolicy, TimePolicy, WasmModule, WasmRuntime, - WasmRuntimeError, + NetworkPolicy, RuntimeConfig, SandboxHostFunctions, SandboxPolicy, StorageBackend, + StorageHostConfig, TerminalPolicy, TimePolicy, WasmModule, WasmRuntime, WasmRuntimeError, }; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -18,6 +17,10 @@ pub struct EvaluationInput { pub agent_data: Vec, pub challenge_id: String, pub params: Vec, + #[serde(default)] + pub task_definition: Option>, + #[serde(default)] + pub environment_config: Option>, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/crates/wasm-runtime-interface/src/bridge.rs b/crates/wasm-runtime-interface/src/bridge.rs index d7cc69d0..9cfd29f9 100644 --- a/crates/wasm-runtime-interface/src/bridge.rs +++ b/crates/wasm-runtime-interface/src/bridge.rs @@ -95,7 +95,7 @@ pub fn output_to_response( execution_time_ms: i64, ) -> EvalResponse { if output.valid { - let score = output.score as f64 / 100.0; + let score = output.score as f64 / 10_000.0; let results = serde_json::json!({ "message": output.message }); EvalResponse::success(request_id, score, results).with_time(execution_time_ms) } else { @@ -185,7 +185,7 @@ mod tests { #[test] fn test_output_to_response_success() { - let output = EvaluationOutput::success(100, "perfect"); + let output = EvaluationOutput::success(10000, "perfect"); let resp = output_to_response(&output, "req-1", 42); assert!(resp.success); assert_eq!(resp.request_id, "req-1"); diff --git a/crates/wasm-runtime-interface/src/network.rs b/crates/wasm-runtime-interface/src/network.rs index b3245e0b..02d4b7b4 100644 --- a/crates/wasm-runtime-interface/src/network.rs +++ b/crates/wasm-runtime-interface/src/network.rs @@ -24,7 +24,6 @@ use crate::runtime::{HostFunctionRegistrar, RuntimeState, WasmRuntimeError}; pub const HOST_LOG_MESSAGE: &str = "log_message"; pub const HOST_GET_TIMESTAMP: &str = "get_timestamp"; -const DEFAULT_RESPONSE_BUF_SIZE: i32 = 65536; const DEFAULT_DNS_BUF_SIZE: i32 = 4096; #[derive(Debug, thiserror::Error)] @@ -92,9 +91,10 @@ impl HostFunctionRegistrar for NetworkHostFunctions { |mut caller: Caller, req_ptr: i32, req_len: i32, - resp_ptr: i32| + resp_ptr: i32, + resp_len: i32| -> i32 { - handle_http_get(&mut caller, req_ptr, req_len, resp_ptr) + handle_http_get(&mut caller, req_ptr, req_len, resp_ptr, resp_len) }, ) .map_err(|err| WasmRuntimeError::HostFunction(err.to_string()))?; @@ -596,8 +596,8 @@ fn handle_http_get( req_ptr: i32, req_len: i32, resp_ptr: i32, + resp_len: i32, ) -> i32 { - let resp_len = DEFAULT_RESPONSE_BUF_SIZE; let enforcement = "http_get"; let request_bytes = match read_memory(caller, req_ptr, req_len) { Ok(bytes) => bytes, diff --git a/crates/wasm-runtime-interface/src/storage.rs b/crates/wasm-runtime-interface/src/storage.rs index 19ccdcac..51e6bab3 100644 --- a/crates/wasm-runtime-interface/src/storage.rs +++ b/crates/wasm-runtime-interface/src/storage.rs @@ -453,6 +453,37 @@ impl HostFunctionRegistrar for StorageHostFunctions { ) .map_err(|err| WasmRuntimeError::HostFunction(err.to_string()))?; + linker + .func_wrap( + HOST_STORAGE_NAMESPACE, + HOST_STORAGE_DELETE, + |mut caller: Caller, key_ptr: i32, key_len: i32| -> i32 { + handle_storage_delete(&mut caller, key_ptr, key_len) + }, + ) + .map_err(|err| WasmRuntimeError::HostFunction(err.to_string()))?; + + linker + .func_wrap( + HOST_STORAGE_NAMESPACE, + HOST_STORAGE_PROPOSE_WRITE, + |mut caller: Caller, + key_ptr: i32, + key_len: i32, + value_ptr: i32, + value_len: i32| + -> i64 { + handle_storage_propose_write( + &mut caller, + key_ptr, + key_len, + value_ptr, + value_len, + ) + }, + ) + .map_err(|err| WasmRuntimeError::HostFunction(err.to_string()))?; + Ok(()) } } @@ -554,6 +585,94 @@ fn handle_storage_set( } } +fn handle_storage_delete(caller: &mut Caller, key_ptr: i32, key_len: i32) -> i32 { + let key = match read_wasm_memory(caller, key_ptr, key_len) { + Ok(bytes) => bytes, + Err(err) => { + warn!(error = %err, "storage_delete: failed to read key from wasm memory"); + return StorageHostStatus::InternalError.to_i32(); + } + }; + + let storage = &caller.data().storage_state; + if let Err(err) = storage.config.validate_key(&key) { + warn!(error = %err, "storage_delete: key validation failed"); + return StorageHostStatus::from(err).to_i32(); + } + + if storage.config.require_consensus && !storage.config.allow_direct_writes { + warn!("storage_delete: direct deletes require consensus or allow_direct_writes"); + return StorageHostStatus::ConsensusRequired.to_i32(); + } + + let challenge_id = storage.challenge_id.clone(); + let backend = Arc::clone(&storage.backend); + + match backend.delete(&challenge_id, &key) { + Ok(_deleted) => { + caller.data_mut().storage_state.operations_count += 1; + StorageHostStatus::Success.to_i32() + } + Err(err) => { + warn!(error = %err, "storage_delete: backend delete failed"); + StorageHostStatus::from(err).to_i32() + } + } +} + +fn handle_storage_propose_write( + caller: &mut Caller, + key_ptr: i32, + key_len: i32, + value_ptr: i32, + value_len: i32, +) -> i64 { + let key = match read_wasm_memory(caller, key_ptr, key_len) { + Ok(bytes) => bytes, + Err(err) => { + warn!(error = %err, "storage_propose_write: failed to read key from wasm memory"); + return pack_result(StorageHostStatus::InternalError, 0); + } + }; + + let value = match read_wasm_memory(caller, value_ptr, value_len) { + Ok(bytes) => bytes, + Err(err) => { + warn!(error = %err, "storage_propose_write: failed to read value from wasm memory"); + return pack_result(StorageHostStatus::InternalError, 0); + } + }; + + let storage = &caller.data().storage_state; + if let Err(err) = storage.config.validate_key(&key) { + warn!(error = %err, "storage_propose_write: key validation failed"); + return pack_result(StorageHostStatus::from(err), 0); + } + if let Err(err) = storage.config.validate_value(&value) { + warn!(error = %err, "storage_propose_write: value validation failed"); + return pack_result(StorageHostStatus::from(err), 0); + } + + let challenge_id = storage.challenge_id.clone(); + let backend = Arc::clone(&storage.backend); + + match backend.propose_write(&challenge_id, &key, &value) { + Ok(proposal_id) => { + caller.data_mut().storage_state.bytes_written += value.len() as u64; + caller.data_mut().storage_state.operations_count += 1; + let result_id = caller + .data_mut() + .storage_state + .store_result(proposal_id.to_vec()); + pack_result(StorageHostStatus::Success, result_id) + } + Err(err) => { + warn!(error = %err, "storage_propose_write: backend write failed"); + pack_result(StorageHostStatus::from(err), 0) + } + } +} + fn read_wasm_memory( caller: &mut Caller, ptr: i32, From c341103fd4515fd6e1cf2c7543226dcef848fa2b Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 09:47:50 +0000 Subject: [PATCH 3/4] feat(wasm-runtime-interface): add data host function module for WASM challenges --- bins/validator-node/src/main.rs | 1 + bins/validator-node/src/wasm_executor.rs | 69 ++- crates/challenge-sdk-wasm/src/lib.rs | 1 + crates/challenge-sdk-wasm/src/types.rs | 21 + .../wasm-runtime-interface/src/container.rs | 582 ++++++++++++++++++ crates/wasm-runtime-interface/src/data.rs | 455 ++++++++++++++ crates/wasm-runtime-interface/src/lib.rs | 11 + crates/wasm-runtime-interface/src/runtime.rs | 45 ++ 8 files changed, 1158 insertions(+), 27 deletions(-) create mode 100644 crates/wasm-runtime-interface/src/container.rs create mode 100644 crates/wasm-runtime-interface/src/data.rs diff --git a/bins/validator-node/src/main.rs b/bins/validator-node/src/main.rs index 671dce13..60721271 100644 --- a/bins/validator-node/src/main.rs +++ b/bins/validator-node/src/main.rs @@ -376,6 +376,7 @@ async fn main() -> Result<()> { enable_fuel: args.wasm_enable_fuel, fuel_limit: args.wasm_fuel_limit, storage_host_config: wasm_runtime_interface::StorageHostConfig::default(), + storage_backend: std::sync::Arc::new(wasm_runtime_interface::InMemoryStorageBackend::new()), }) { Ok(executor) => { info!( diff --git a/bins/validator-node/src/wasm_executor.rs b/bins/validator-node/src/wasm_executor.rs index 9aa743db..199960d4 100644 --- a/bins/validator-node/src/wasm_executor.rs +++ b/bins/validator-node/src/wasm_executor.rs @@ -28,6 +28,10 @@ pub struct EvaluationOutput { pub score: i64, pub valid: bool, pub message: String, + #[serde(default)] + pub metrics: Option>, + #[serde(default)] + pub details: Option>, } impl EvaluationOutput { @@ -37,6 +41,8 @@ impl EvaluationOutput { score, valid: true, message: String::from(message), + metrics: None, + details: None, } } @@ -46,6 +52,8 @@ impl EvaluationOutput { score: 0, valid: false, message: String::from(message), + metrics: None, + details: None, } } } @@ -56,6 +64,7 @@ pub struct WasmExecutorConfig { pub enable_fuel: bool, pub fuel_limit: Option, pub storage_host_config: StorageHostConfig, + pub storage_backend: Arc, } impl Default for WasmExecutorConfig { @@ -66,6 +75,7 @@ impl Default for WasmExecutorConfig { enable_fuel: false, fuel_limit: None, storage_host_config: StorageHostConfig::default(), + storage_backend: Arc::new(InMemoryStorageBackend::new()), } } } @@ -146,6 +156,8 @@ impl WasmChallengeExecutor { agent_data: agent_data.to_vec(), challenge_id: challenge_id.to_string(), params: params.to_vec(), + task_definition: None, + environment_config: None, }; let serialized = @@ -165,7 +177,15 @@ impl WasmChallengeExecutor { validator_id: "validator".to_string(), restart_id: String::new(), config_version: 0, - ..Default::default() + storage_host_config: StorageHostConfig { + allow_direct_writes: true, + require_consensus: false, + ..self.config.storage_host_config.clone() + }, + storage_backend: Arc::clone(&self.config.storage_backend), + fixed_timestamp_ms: None, + consensus_policy: ConsensusPolicy::default(), + terminal_policy: TerminalPolicy::default(), }; let mut instance = self @@ -173,12 +193,6 @@ impl WasmChallengeExecutor { .instantiate(&module, instance_config, Some(network_host_fns)) .map_err(|e| anyhow::anyhow!("WASM instantiation failed: {}", e))?; - let _storage_state = StorageHostState::new( - challenge_id.to_string(), - self.config.storage_host_config.clone(), - Arc::new(NoopStorageBackend), - ); - let initial_fuel = instance.fuel_remaining(); let ptr = self.allocate_input(&mut instance, &serialized)?; @@ -264,6 +278,8 @@ impl WasmChallengeExecutor { agent_data: agent_data.to_vec(), challenge_id: challenge_id.to_string(), params: params.to_vec(), + task_definition: None, + environment_config: None, }; let serialized = @@ -283,7 +299,15 @@ impl WasmChallengeExecutor { validator_id: "validator".to_string(), restart_id: String::new(), config_version: 0, - ..Default::default() + storage_host_config: StorageHostConfig { + allow_direct_writes: true, + require_consensus: false, + ..self.config.storage_host_config.clone() + }, + storage_backend: Arc::clone(&self.config.storage_backend), + fixed_timestamp_ms: None, + consensus_policy: ConsensusPolicy::default(), + terminal_policy: TerminalPolicy::default(), }; let mut instance = self @@ -291,12 +315,6 @@ impl WasmChallengeExecutor { .instantiate(&module, instance_config, Some(network_host_fns)) .map_err(|e| anyhow::anyhow!("WASM instantiation failed: {}", e))?; - let _storage_state = StorageHostState::new( - challenge_id.to_string(), - self.config.storage_host_config.clone(), - Arc::new(NoopStorageBackend), - ); - let initial_fuel = instance.fuel_remaining(); let ptr = self.allocate_input(&mut instance, &serialized)?; @@ -399,6 +417,7 @@ impl WasmChallengeExecutor { fixed_timestamp_ms: None, consensus_policy: ConsensusPolicy::default(), terminal_policy: TerminalPolicy::default(), + ..Default::default() }; let mut instance = self @@ -408,21 +427,17 @@ impl WasmChallengeExecutor { let initial_fuel = instance.fuel_remaining(); - let result_ptr = instance - .call_return_i32("get_tasks") + let result = instance + .call_return_i64("get_tasks") .map_err(|e| anyhow::anyhow!("WASM get_tasks call failed: {}", e))?; - let result_data = if result_ptr > 0 { - let len = instance - .call_return_i32("get_tasks_result_len") - .unwrap_or(0); - if len > 0 { - instance - .read_memory(result_ptr as usize, len as usize) - .unwrap_or_default() - } else { - Vec::new() - } + let out_len = (result >> 32) as i32; + let out_ptr = (result & 0xFFFF_FFFF) as i32; + + let result_data = if out_ptr > 0 && out_len > 0 { + instance + .read_memory(out_ptr as usize, out_len as usize) + .unwrap_or_default() } else { Vec::new() }; diff --git a/crates/challenge-sdk-wasm/src/lib.rs b/crates/challenge-sdk-wasm/src/lib.rs index 1a51f7ce..8d278dd4 100644 --- a/crates/challenge-sdk-wasm/src/lib.rs +++ b/crates/challenge-sdk-wasm/src/lib.rs @@ -12,6 +12,7 @@ pub use types::{ score_f64_scaled, SandboxExecRequest, SandboxExecResponse, TaskDefinition, TaskResult, TermEvaluationParams, }; +pub use types::{ContainerRunRequest, ContainerRunResponse}; pub use types::{EvaluationInput, EvaluationOutput}; pub trait Challenge { diff --git a/crates/challenge-sdk-wasm/src/types.rs b/crates/challenge-sdk-wasm/src/types.rs index 9d295438..9c8adeb3 100644 --- a/crates/challenge-sdk-wasm/src/types.rs +++ b/crates/challenge-sdk-wasm/src/types.rs @@ -128,3 +128,24 @@ pub struct TermEvaluationParams { pub timeout_ms: u64, pub environment_config: Option>, } + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ContainerRunRequest { + pub image: String, + pub command: Vec, + pub env_vars: Vec<(String, String)>, + pub working_dir: Option, + pub stdin: Option>, + pub memory_limit_mb: Option, + pub cpu_limit: Option, + pub network_mode: Option, + pub timeout_ms: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ContainerRunResponse { + pub exit_code: i32, + pub stdout: Vec, + pub stderr: Vec, + pub duration_ms: u64, +} diff --git a/crates/wasm-runtime-interface/src/container.rs b/crates/wasm-runtime-interface/src/container.rs new file mode 100644 index 00000000..26d1d3b6 --- /dev/null +++ b/crates/wasm-runtime-interface/src/container.rs @@ -0,0 +1,582 @@ +//! Container Host Functions for WASM Challenges +//! +//! This module provides host functions that allow WASM code to delegate +//! container execution to the host. All operations are gated by `ContainerPolicy`. +//! +//! # Host Functions +//! +//! - `container_run(req_ptr, req_len, resp_ptr, resp_len) -> i32` - Run a container + +use crate::runtime::{HostFunctionRegistrar, RuntimeState, WasmRuntimeError}; +use serde::{Deserialize, Serialize}; +use std::process::Command; +use std::time::{Duration, Instant}; +use tracing::{info, warn}; +use wasmtime::{Caller, Linker, Memory}; + +pub const HOST_CONTAINER_NAMESPACE: &str = "platform_container"; +pub const HOST_CONTAINER_RUN: &str = "container_run"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(i32)] +pub enum ContainerHostStatus { + Success = 0, + Disabled = 1, + ImageNotAllowed = -1, + ExecutionTimeout = -2, + ExecutionFailed = -3, + ResourceLimitExceeded = -4, + InternalError = -100, +} + +impl ContainerHostStatus { + pub fn to_i32(self) -> i32 { + self as i32 + } + + pub fn from_i32(code: i32) -> Self { + match code { + 0 => Self::Success, + 1 => Self::Disabled, + -1 => Self::ImageNotAllowed, + -2 => Self::ExecutionTimeout, + -3 => Self::ExecutionFailed, + -4 => Self::ResourceLimitExceeded, + _ => Self::InternalError, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContainerPolicy { + pub enabled: bool, + pub allowed_images: Vec, + pub max_memory_mb: u64, + pub max_cpu_count: u32, + pub max_execution_time_secs: u64, + pub allow_network: bool, + pub max_containers_per_execution: u32, +} + +impl Default for ContainerPolicy { + fn default() -> Self { + Self { + enabled: false, + allowed_images: Vec::new(), + max_memory_mb: 512, + max_cpu_count: 1, + max_execution_time_secs: 60, + allow_network: false, + max_containers_per_execution: 4, + } + } +} + +impl ContainerPolicy { + pub fn development() -> Self { + Self { + enabled: true, + allowed_images: vec!["*".to_string()], + max_memory_mb: 2048, + max_cpu_count: 4, + max_execution_time_secs: 300, + allow_network: true, + max_containers_per_execution: 16, + } + } + + pub fn is_image_allowed(&self, image: &str) -> bool { + if !self.enabled { + return false; + } + self.allowed_images + .iter() + .any(|i| i == "*" || i == image || image.starts_with(&format!("{}:", i))) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContainerRunRequest { + pub image: String, + pub command: Vec, + pub env_vars: Vec<(String, String)>, + pub working_dir: Option, + pub stdin: Option>, + pub memory_limit_mb: Option, + pub cpu_limit: Option, + pub network_mode: Option, + pub timeout_ms: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContainerRunResponse { + pub exit_code: i32, + pub stdout: Vec, + pub stderr: Vec, + pub duration_ms: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum ContainerExecError { + Disabled, + ImageNotAllowed(String), + ExecutionTimeout(u64), + ExecutionFailed(String), + ResourceLimitExceeded(String), + MemoryError(String), +} + +pub struct ContainerState { + pub policy: ContainerPolicy, + pub challenge_id: String, + pub validator_id: String, + pub containers_run: u32, +} + +impl ContainerState { + pub fn new(policy: ContainerPolicy, challenge_id: String, validator_id: String) -> Self { + Self { + policy, + challenge_id, + validator_id, + containers_run: 0, + } + } + + pub fn reset_counters(&mut self) { + self.containers_run = 0; + } +} + +#[derive(Clone, Debug)] +pub struct ContainerHostFunctions; + +impl ContainerHostFunctions { + pub fn new() -> Self { + Self + } +} + +impl Default for ContainerHostFunctions { + fn default() -> Self { + Self::new() + } +} + +impl HostFunctionRegistrar for ContainerHostFunctions { + fn register(&self, linker: &mut Linker) -> Result<(), WasmRuntimeError> { + linker + .func_wrap( + HOST_CONTAINER_NAMESPACE, + HOST_CONTAINER_RUN, + |mut caller: Caller, + req_ptr: i32, + req_len: i32, + resp_ptr: i32, + resp_len: i32| + -> i32 { + handle_container_run(&mut caller, req_ptr, req_len, resp_ptr, resp_len) + }, + ) + .map_err(|e| { + WasmRuntimeError::HostFunction(format!( + "failed to register {}: {}", + HOST_CONTAINER_RUN, e + )) + })?; + + Ok(()) + } +} + +fn handle_container_run( + caller: &mut Caller, + req_ptr: i32, + req_len: i32, + resp_ptr: i32, + resp_len: i32, +) -> i32 { + let request_bytes = match read_memory(caller, req_ptr, req_len) { + Ok(bytes) => bytes, + Err(err) => { + warn!( + challenge_id = %caller.data().challenge_id, + error = %err, + "container_run: host memory read failed" + ); + return write_result( + caller, + resp_ptr, + resp_len, + Err::(ContainerExecError::MemoryError( + err, + )), + ); + } + }; + + let request = match bincode::deserialize::(&request_bytes) { + Ok(req) => req, + Err(err) => { + warn!( + challenge_id = %caller.data().challenge_id, + error = %err, + "container_run: request decode failed" + ); + return write_result( + caller, + resp_ptr, + resp_len, + Err::( + ContainerExecError::ExecutionFailed(format!( + "invalid container run request: {err}" + )), + ), + ); + } + }; + + let policy = &caller.data().container_state.policy; + + if !policy.enabled { + return write_result( + caller, + resp_ptr, + resp_len, + Err::(ContainerExecError::Disabled), + ); + } + + if !policy.is_image_allowed(&request.image) { + warn!( + challenge_id = %caller.data().challenge_id, + image = %request.image, + "container_run: image not allowed" + ); + return write_result( + caller, + resp_ptr, + resp_len, + Err::(ContainerExecError::ImageNotAllowed( + request.image, + )), + ); + } + + if caller.data().container_state.containers_run + >= caller + .data() + .container_state + .policy + .max_containers_per_execution + { + return write_result( + caller, + resp_ptr, + resp_len, + Err::( + ContainerExecError::ResourceLimitExceeded("container limit exceeded".to_string()), + ), + ); + } + + let timeout_secs = policy.max_execution_time_secs; + let timeout_ms = if request.timeout_ms > 0 { + request.timeout_ms.min(timeout_secs.saturating_mul(1000)) + } else { + timeout_secs.saturating_mul(1000) + }; + let timeout = Duration::from_millis(timeout_ms); + + let memory_limit = request + .memory_limit_mb + .unwrap_or(policy.max_memory_mb) + .min(policy.max_memory_mb); + + let network_mode = if policy.allow_network { + request.network_mode.as_deref().unwrap_or("bridge") + } else { + "none" + }; + + let result = execute_container(&request, timeout, memory_limit, network_mode); + + let challenge_id = caller.data().challenge_id.clone(); + let validator_id = caller.data().validator_id.clone(); + + match &result { + Ok(resp) => { + caller.data_mut().container_state.containers_run += 1; + info!( + challenge_id = %challenge_id, + validator_id = %validator_id, + image = %request.image, + exit_code = resp.exit_code, + stdout_bytes = resp.stdout.len(), + stderr_bytes = resp.stderr.len(), + duration_ms = resp.duration_ms, + "container_run completed" + ); + } + Err(err) => { + warn!( + challenge_id = %challenge_id, + validator_id = %validator_id, + image = %request.image, + error = ?err, + "container_run failed" + ); + } + } + + write_result(caller, resp_ptr, resp_len, result) +} + +fn execute_container( + request: &ContainerRunRequest, + timeout: Duration, + memory_limit_mb: u64, + network_mode: &str, +) -> Result { + let start = Instant::now(); + + let mut cmd = Command::new("docker"); + cmd.arg("run"); + cmd.arg("--rm"); + cmd.args(["--network", network_mode]); + cmd.args(["--memory", &format!("{}m", memory_limit_mb)]); + + for (key, value) in &request.env_vars { + cmd.args(["-e", &format!("{}={}", key, value)]); + } + + if let Some(ref dir) = request.working_dir { + cmd.args(["-w", dir]); + } + + cmd.arg(&request.image); + for arg in &request.command { + cmd.arg(arg); + } + + let has_stdin = request.stdin.as_ref().is_some_and(|s| !s.is_empty()); + + if has_stdin { + cmd.stdin(std::process::Stdio::piped()); + } else { + cmd.stdin(std::process::Stdio::null()); + } + cmd.stdout(std::process::Stdio::piped()); + cmd.stderr(std::process::Stdio::piped()); + + let mut child = cmd + .spawn() + .map_err(|e| ContainerExecError::ExecutionFailed(e.to_string()))?; + + if has_stdin { + if let Some(ref stdin_data) = request.stdin { + if let Some(ref mut stdin) = child.stdin { + use std::io::Write; + let _ = stdin.write_all(stdin_data); + } + } + child.stdin.take(); + } + + let output = loop { + if start.elapsed() > timeout { + let _ = child.kill(); + return Err(ContainerExecError::ExecutionTimeout(timeout.as_secs())); + } + match child.try_wait() { + Ok(Some(_)) => { + break child + .wait_with_output() + .map_err(|e| ContainerExecError::ExecutionFailed(e.to_string()))? + } + Ok(None) => std::thread::sleep(Duration::from_millis(50)), + Err(e) => return Err(ContainerExecError::ExecutionFailed(e.to_string())), + } + }; + + let duration_ms = start.elapsed().as_millis() as u64; + + Ok(ContainerRunResponse { + exit_code: output.status.code().unwrap_or(-1), + stdout: output.stdout, + stderr: output.stderr, + duration_ms, + }) +} + +fn read_memory(caller: &mut Caller, ptr: i32, len: i32) -> Result, String> { + if ptr < 0 || len < 0 { + return Err("negative pointer/length".to_string()); + } + let ptr = ptr as usize; + let len = len as usize; + let memory = get_memory(caller).ok_or_else(|| "memory export not found".to_string())?; + let data = memory.data(caller); + let end = ptr + .checked_add(len) + .ok_or_else(|| "pointer overflow".to_string())?; + if end > data.len() { + return Err("memory read out of bounds".to_string()); + } + Ok(data[ptr..end].to_vec()) +} + +fn write_result( + caller: &mut Caller, + resp_ptr: i32, + resp_len: i32, + result: Result, +) -> i32 { + let response_bytes = match bincode::serialize(&result) { + Ok(bytes) => bytes, + Err(err) => { + warn!(error = %err, "failed to serialize container response"); + return -1; + } + }; + + write_bytes(caller, resp_ptr, resp_len, &response_bytes) +} + +fn write_bytes( + caller: &mut Caller, + resp_ptr: i32, + resp_len: i32, + bytes: &[u8], +) -> i32 { + if resp_ptr < 0 || resp_len < 0 { + return -1; + } + if bytes.len() > i32::MAX as usize { + return -1; + } + let resp_len = resp_len as usize; + if bytes.len() > resp_len { + return -(bytes.len() as i32); + } + + let memory = match get_memory(caller) { + Some(memory) => memory, + None => return -1, + }; + + let ptr = resp_ptr as usize; + let end = match ptr.checked_add(bytes.len()) { + Some(end) => end, + None => return -1, + }; + let data = memory.data_mut(caller); + if end > data.len() { + return -1; + } + data[ptr..end].copy_from_slice(bytes); + bytes.len() as i32 +} + +fn get_memory(caller: &mut Caller) -> Option { + let memory_export = caller.data().memory_export.clone(); + caller + .get_export(&memory_export) + .and_then(|export| export.into_memory()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_container_host_status_conversion() { + assert_eq!(ContainerHostStatus::Success.to_i32(), 0); + assert_eq!(ContainerHostStatus::Disabled.to_i32(), 1); + assert_eq!(ContainerHostStatus::ImageNotAllowed.to_i32(), -1); + assert_eq!(ContainerHostStatus::InternalError.to_i32(), -100); + + assert_eq!( + ContainerHostStatus::from_i32(0), + ContainerHostStatus::Success + ); + assert_eq!( + ContainerHostStatus::from_i32(1), + ContainerHostStatus::Disabled + ); + assert_eq!( + ContainerHostStatus::from_i32(-999), + ContainerHostStatus::InternalError + ); + } + + #[test] + fn test_container_policy_default() { + let policy = ContainerPolicy::default(); + assert!(!policy.enabled); + assert!(policy.allowed_images.is_empty()); + assert!(!policy.is_image_allowed("ubuntu")); + } + + #[test] + fn test_container_policy_development() { + let policy = ContainerPolicy::development(); + assert!(policy.enabled); + assert!(policy.is_image_allowed("ubuntu")); + assert!(policy.is_image_allowed("python:3.11")); + } + + #[test] + fn test_container_policy_image_check() { + let policy = ContainerPolicy { + enabled: true, + allowed_images: vec!["ubuntu".to_string(), "python".to_string()], + ..Default::default() + }; + assert!(policy.is_image_allowed("ubuntu")); + assert!(policy.is_image_allowed("python:3.11")); + assert!(!policy.is_image_allowed("alpine")); + } + + #[test] + fn test_container_state_creation() { + let state = ContainerState::new( + ContainerPolicy::default(), + "test".to_string(), + "test".to_string(), + ); + assert_eq!(state.containers_run, 0); + } + + #[test] + fn test_container_state_reset() { + let mut state = ContainerState::new( + ContainerPolicy::default(), + "test".to_string(), + "test".to_string(), + ); + state.containers_run = 5; + state.reset_counters(); + assert_eq!(state.containers_run, 0); + } + + #[test] + fn test_container_run_request_serialization() { + let request = ContainerRunRequest { + image: "ubuntu:22.04".to_string(), + command: vec!["echo".to_string(), "hello".to_string()], + env_vars: vec![("KEY".to_string(), "VALUE".to_string())], + working_dir: None, + stdin: None, + memory_limit_mb: Some(256), + cpu_limit: Some(1), + network_mode: None, + timeout_ms: 5000, + }; + + let bytes = bincode::serialize(&request).unwrap(); + let deserialized: ContainerRunRequest = bincode::deserialize(&bytes).unwrap(); + assert_eq!(deserialized.image, "ubuntu:22.04"); + assert_eq!(deserialized.command, vec!["echo", "hello"]); + } +} diff --git a/crates/wasm-runtime-interface/src/data.rs b/crates/wasm-runtime-interface/src/data.rs new file mode 100644 index 00000000..ab44b333 --- /dev/null +++ b/crates/wasm-runtime-interface/src/data.rs @@ -0,0 +1,455 @@ +//! Data Host Functions for WASM Challenges +//! +//! This module provides host functions that allow WASM code to load +//! challenge-specific data from the host. All operations are gated by `DataPolicy`. +//! +//! # Host Functions +//! +//! - `data_get(key_ptr, key_len, buf_ptr, buf_len) -> i32` - Read challenge data by key +//! - `data_list(prefix_ptr, prefix_len, buf_ptr, buf_len) -> i32` - List data keys under a prefix + +use crate::runtime::{HostFunctionRegistrar, RuntimeState, WasmRuntimeError}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use tracing::warn; +use wasmtime::{Caller, Linker, Memory}; + +pub const HOST_DATA_NAMESPACE: &str = "platform_data"; +pub const HOST_DATA_GET: &str = "data_get"; +pub const HOST_DATA_LIST: &str = "data_list"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(i32)] +pub enum DataHostStatus { + Success = 0, + Disabled = 1, + NotFound = -1, + KeyTooLarge = -2, + BufferTooSmall = -3, + PathNotAllowed = -4, + IoError = -5, + InternalError = -100, +} + +impl DataHostStatus { + pub fn to_i32(self) -> i32 { + self as i32 + } + + pub fn from_i32(code: i32) -> Self { + match code { + 0 => Self::Success, + 1 => Self::Disabled, + -1 => Self::NotFound, + -2 => Self::KeyTooLarge, + -3 => Self::BufferTooSmall, + -4 => Self::PathNotAllowed, + -5 => Self::IoError, + _ => Self::InternalError, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DataPolicy { + pub enabled: bool, + pub max_key_size: usize, + pub max_value_size: usize, + pub max_reads_per_execution: u32, +} + +impl Default for DataPolicy { + fn default() -> Self { + Self { + enabled: false, + max_key_size: 1024, + max_value_size: 10 * 1024 * 1024, + max_reads_per_execution: 64, + } + } +} + +impl DataPolicy { + pub fn development() -> Self { + Self { + enabled: true, + max_key_size: 4096, + max_value_size: 50 * 1024 * 1024, + max_reads_per_execution: 256, + } + } +} + +pub trait DataBackend: Send + Sync { + fn get(&self, challenge_id: &str, key: &str) -> Result>, DataError>; + fn list(&self, challenge_id: &str, prefix: &str) -> Result, DataError>; +} + +#[derive(Debug, thiserror::Error)] +pub enum DataError { + #[error("io error: {0}")] + Io(String), + #[error("key too large: {0}")] + KeyTooLarge(usize), + #[error("path not allowed: {0}")] + PathNotAllowed(String), +} + +pub struct NoopDataBackend; + +impl DataBackend for NoopDataBackend { + fn get(&self, _challenge_id: &str, _key: &str) -> Result>, DataError> { + Ok(None) + } + + fn list(&self, _challenge_id: &str, _prefix: &str) -> Result, DataError> { + Ok(Vec::new()) + } +} + +pub struct FilesystemDataBackend { + base_dir: PathBuf, +} + +impl FilesystemDataBackend { + pub fn new(base_dir: PathBuf) -> Self { + Self { base_dir } + } +} + +impl DataBackend for FilesystemDataBackend { + fn get(&self, challenge_id: &str, key: &str) -> Result>, DataError> { + let path = self.base_dir.join(challenge_id).join(key); + if !path.starts_with(self.base_dir.join(challenge_id)) { + return Err(DataError::PathNotAllowed(key.to_string())); + } + match std::fs::read(&path) { + Ok(data) => Ok(Some(data)), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(DataError::Io(e.to_string())), + } + } + + fn list(&self, challenge_id: &str, prefix: &str) -> Result, DataError> { + let dir = self.base_dir.join(challenge_id).join(prefix); + if !dir.starts_with(self.base_dir.join(challenge_id)) { + return Err(DataError::PathNotAllowed(prefix.to_string())); + } + let entries = match std::fs::read_dir(&dir) { + Ok(rd) => rd, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(e) => return Err(DataError::Io(e.to_string())), + }; + let mut names = Vec::new(); + for entry in entries { + match entry { + Ok(e) => { + if let Some(name) = e.file_name().to_str() { + names.push(name.to_string()); + } + } + Err(_) => continue, + } + } + Ok(names) + } +} + +pub struct DataState { + pub policy: DataPolicy, + pub backend: std::sync::Arc, + pub challenge_id: String, + pub reads: u32, +} + +impl DataState { + pub fn new( + policy: DataPolicy, + backend: std::sync::Arc, + challenge_id: String, + ) -> Self { + Self { + policy, + backend, + challenge_id, + reads: 0, + } + } + + pub fn reset_counters(&mut self) { + self.reads = 0; + } +} + +#[derive(Clone, Debug)] +pub struct DataHostFunctions; + +impl DataHostFunctions { + pub fn new() -> Self { + Self + } +} + +impl Default for DataHostFunctions { + fn default() -> Self { + Self::new() + } +} + +impl HostFunctionRegistrar for DataHostFunctions { + fn register(&self, linker: &mut Linker) -> Result<(), WasmRuntimeError> { + linker + .func_wrap( + HOST_DATA_NAMESPACE, + HOST_DATA_GET, + |mut caller: Caller, + key_ptr: i32, + key_len: i32, + buf_ptr: i32, + buf_len: i32| + -> i32 { + handle_data_get(&mut caller, key_ptr, key_len, buf_ptr, buf_len) + }, + ) + .map_err(|err| WasmRuntimeError::HostFunction(err.to_string()))?; + + linker + .func_wrap( + HOST_DATA_NAMESPACE, + HOST_DATA_LIST, + |mut caller: Caller, + prefix_ptr: i32, + prefix_len: i32, + buf_ptr: i32, + buf_len: i32| + -> i32 { + handle_data_list(&mut caller, prefix_ptr, prefix_len, buf_ptr, buf_len) + }, + ) + .map_err(|err| WasmRuntimeError::HostFunction(err.to_string()))?; + + Ok(()) + } +} + +fn handle_data_get( + caller: &mut Caller, + key_ptr: i32, + key_len: i32, + buf_ptr: i32, + buf_len: i32, +) -> i32 { + if !caller.data().data_state.policy.enabled { + return DataHostStatus::Disabled.to_i32(); + } + + let key_bytes = match read_wasm_memory(caller, key_ptr, key_len) { + Ok(bytes) => bytes, + Err(err) => { + warn!(error = %err, "data_get: failed to read key from wasm memory"); + return DataHostStatus::InternalError.to_i32(); + } + }; + + let key_str = match std::str::from_utf8(&key_bytes) { + Ok(s) => s.to_string(), + Err(_) => return DataHostStatus::InternalError.to_i32(), + }; + + if key_bytes.len() > caller.data().data_state.policy.max_key_size { + return DataHostStatus::KeyTooLarge.to_i32(); + } + + if caller.data().data_state.reads >= caller.data().data_state.policy.max_reads_per_execution { + return DataHostStatus::InternalError.to_i32(); + } + + let challenge_id = caller.data().data_state.challenge_id.clone(); + let backend = std::sync::Arc::clone(&caller.data().data_state.backend); + + let value = match backend.get(&challenge_id, &key_str) { + Ok(Some(v)) => v, + Ok(None) => return DataHostStatus::NotFound.to_i32(), + Err(err) => { + warn!(error = %err, "data_get: backend read failed"); + return DataHostStatus::IoError.to_i32(); + } + }; + + caller.data_mut().data_state.reads += 1; + + if buf_len < 0 || value.len() > buf_len as usize { + return DataHostStatus::BufferTooSmall.to_i32(); + } + + if let Err(err) = write_wasm_memory(caller, buf_ptr, &value) { + warn!(error = %err, "data_get: failed to write value to wasm memory"); + return DataHostStatus::InternalError.to_i32(); + } + + value.len() as i32 +} + +fn handle_data_list( + caller: &mut Caller, + prefix_ptr: i32, + prefix_len: i32, + buf_ptr: i32, + buf_len: i32, +) -> i32 { + if !caller.data().data_state.policy.enabled { + return DataHostStatus::Disabled.to_i32(); + } + + let prefix_bytes = match read_wasm_memory(caller, prefix_ptr, prefix_len) { + Ok(bytes) => bytes, + Err(err) => { + warn!(error = %err, "data_list: failed to read prefix from wasm memory"); + return DataHostStatus::InternalError.to_i32(); + } + }; + + let prefix_str = match std::str::from_utf8(&prefix_bytes) { + Ok(s) => s.to_string(), + Err(_) => return DataHostStatus::InternalError.to_i32(), + }; + + let challenge_id = caller.data().data_state.challenge_id.clone(); + let backend = std::sync::Arc::clone(&caller.data().data_state.backend); + + let entries = match backend.list(&challenge_id, &prefix_str) { + Ok(e) => e, + Err(err) => { + warn!(error = %err, "data_list: backend list failed"); + return DataHostStatus::IoError.to_i32(); + } + }; + + caller.data_mut().data_state.reads += 1; + + let result = entries.join("\n"); + let result_bytes = result.as_bytes(); + + if buf_len < 0 || result_bytes.len() > buf_len as usize { + return DataHostStatus::BufferTooSmall.to_i32(); + } + + if let Err(err) = write_wasm_memory(caller, buf_ptr, result_bytes) { + warn!(error = %err, "data_list: failed to write to wasm memory"); + return DataHostStatus::InternalError.to_i32(); + } + + result_bytes.len() as i32 +} + +fn read_wasm_memory( + caller: &mut Caller, + ptr: i32, + len: i32, +) -> Result, String> { + if ptr < 0 || len < 0 { + return Err("negative pointer/length".to_string()); + } + let ptr = ptr as usize; + let len = len as usize; + let memory = get_memory(caller).ok_or_else(|| "memory export not found".to_string())?; + let data = memory.data(caller); + let end = ptr + .checked_add(len) + .ok_or_else(|| "pointer overflow".to_string())?; + if end > data.len() { + return Err("memory read out of bounds".to_string()); + } + Ok(data[ptr..end].to_vec()) +} + +fn write_wasm_memory( + caller: &mut Caller, + ptr: i32, + bytes: &[u8], +) -> Result<(), String> { + if ptr < 0 { + return Err("negative pointer".to_string()); + } + let ptr = ptr as usize; + let memory = get_memory(caller).ok_or_else(|| "memory export not found".to_string())?; + let end = ptr + .checked_add(bytes.len()) + .ok_or_else(|| "pointer overflow".to_string())?; + let data = memory.data_mut(caller); + if end > data.len() { + return Err("memory write out of bounds".to_string()); + } + data[ptr..end].copy_from_slice(bytes); + Ok(()) +} + +fn get_memory(caller: &mut Caller) -> Option { + let memory_export = caller.data().memory_export.clone(); + caller + .get_export(&memory_export) + .and_then(|export| export.into_memory()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_data_host_status_conversion() { + assert_eq!(DataHostStatus::Success.to_i32(), 0); + assert_eq!(DataHostStatus::Disabled.to_i32(), 1); + assert_eq!(DataHostStatus::NotFound.to_i32(), -1); + assert_eq!(DataHostStatus::InternalError.to_i32(), -100); + + assert_eq!(DataHostStatus::from_i32(0), DataHostStatus::Success); + assert_eq!(DataHostStatus::from_i32(1), DataHostStatus::Disabled); + assert_eq!( + DataHostStatus::from_i32(-999), + DataHostStatus::InternalError + ); + } + + #[test] + fn test_data_policy_default() { + let policy = DataPolicy::default(); + assert!(!policy.enabled); + assert_eq!(policy.max_key_size, 1024); + } + + #[test] + fn test_data_policy_development() { + let policy = DataPolicy::development(); + assert!(policy.enabled); + assert_eq!(policy.max_key_size, 4096); + } + + #[test] + fn test_noop_data_backend() { + let backend = NoopDataBackend; + assert!(backend.get("challenge-1", "key1").unwrap().is_none()); + assert!(backend.list("challenge-1", "").unwrap().is_empty()); + } + + #[test] + fn test_data_state_creation() { + let state = DataState::new( + DataPolicy::default(), + std::sync::Arc::new(NoopDataBackend), + "test".to_string(), + ); + assert_eq!(state.reads, 0); + } + + #[test] + fn test_data_state_reset() { + let mut state = DataState::new( + DataPolicy::default(), + std::sync::Arc::new(NoopDataBackend), + "test".to_string(), + ); + state.reads = 10; + state.reset_counters(); + assert_eq!(state.reads, 0); + } +} diff --git a/crates/wasm-runtime-interface/src/lib.rs b/crates/wasm-runtime-interface/src/lib.rs index 828d5236..7b57a4db 100644 --- a/crates/wasm-runtime-interface/src/lib.rs +++ b/crates/wasm-runtime-interface/src/lib.rs @@ -11,6 +11,8 @@ use std::str::FromStr; pub mod bridge; pub mod consensus; +pub mod container; +pub mod data; pub mod exec; pub mod network; pub mod runtime; @@ -51,6 +53,15 @@ pub use consensus::{ ConsensusHostFunctions, ConsensusHostStatus, ConsensusPolicy, ConsensusState, HOST_CONSENSUS_NAMESPACE, }; +pub use container::{ + ContainerExecError, ContainerHostFunctions, ContainerHostStatus, ContainerPolicy, + ContainerRunRequest, ContainerRunResponse, ContainerState, HOST_CONTAINER_NAMESPACE, + HOST_CONTAINER_RUN, +}; +pub use data::{ + DataBackend, DataError, DataHostFunctions, DataHostStatus, DataPolicy, DataState, + FilesystemDataBackend, NoopDataBackend, HOST_DATA_GET, HOST_DATA_LIST, HOST_DATA_NAMESPACE, +}; pub use runtime::{ ChallengeInstance, HostFunctionRegistrar, InstanceConfig, RuntimeConfig, RuntimeState, WasmModule, WasmRuntime, WasmRuntimeError, diff --git a/crates/wasm-runtime-interface/src/runtime.rs b/crates/wasm-runtime-interface/src/runtime.rs index 00dfe797..5dbc408a 100644 --- a/crates/wasm-runtime-interface/src/runtime.rs +++ b/crates/wasm-runtime-interface/src/runtime.rs @@ -1,5 +1,7 @@ use crate::bridge::{self, BridgeError, EvalRequest, EvalResponse}; use crate::consensus::{ConsensusHostFunctions, ConsensusPolicy, ConsensusState}; +use crate::container::{ContainerHostFunctions, ContainerPolicy, ContainerState}; +use crate::data::{DataBackend, DataHostFunctions, DataPolicy, DataState, NoopDataBackend}; use crate::exec::{ExecHostFunctions, ExecPolicy, ExecState}; use crate::sandbox::SandboxHostFunctions; use crate::storage::{ @@ -122,6 +124,12 @@ pub struct InstanceConfig { pub consensus_policy: ConsensusPolicy, /// Terminal policy for WASM access to terminal operations. pub terminal_policy: TerminalPolicy, + /// Data policy for WASM access to challenge data. + pub data_policy: DataPolicy, + /// Data backend implementation. + pub data_backend: Arc, + /// Container policy for WASM access to container execution. + pub container_policy: ContainerPolicy, } impl Default for InstanceConfig { @@ -142,6 +150,9 @@ impl Default for InstanceConfig { fixed_timestamp_ms: None, consensus_policy: ConsensusPolicy::default(), terminal_policy: TerminalPolicy::default(), + data_policy: DataPolicy::default(), + data_backend: Arc::new(NoopDataBackend), + container_policy: ContainerPolicy::default(), } } } @@ -175,6 +186,10 @@ pub struct RuntimeState { pub consensus_state: ConsensusState, /// Terminal state for terminal host operations. pub terminal_state: TerminalState, + /// Data state for challenge data host operations. + pub data_state: DataState, + /// Container state for container execution host operations. + pub container_state: ContainerState, limits: StoreLimits, } @@ -188,6 +203,8 @@ impl RuntimeState { time_state: TimeState, consensus_state: ConsensusState, terminal_state: TerminalState, + data_state: DataState, + container_state: ContainerState, memory_export: String, challenge_id: String, validator_id: String, @@ -205,6 +222,8 @@ impl RuntimeState { time_state, consensus_state, terminal_state, + data_state, + container_state, memory_export, challenge_id, validator_id, @@ -227,6 +246,14 @@ impl RuntimeState { pub fn reset_exec_counters(&mut self) { self.exec_state.reset_counters(); } + + pub fn reset_container_counters(&mut self) { + self.container_state.reset_counters(); + } + + pub fn reset_data_counters(&mut self) { + self.data_state.reset_counters(); + } } impl ResourceLimiter for RuntimeState { @@ -316,6 +343,16 @@ impl WasmRuntime { instance_config.challenge_id.clone(), instance_config.validator_id.clone(), ); + let data_state = DataState::new( + instance_config.data_policy.clone(), + Arc::clone(&instance_config.data_backend), + instance_config.challenge_id.clone(), + ); + let container_state = ContainerState::new( + instance_config.container_policy.clone(), + instance_config.challenge_id.clone(), + instance_config.validator_id.clone(), + ); let runtime_state = RuntimeState::new( instance_config.network_policy.clone(), instance_config.sandbox_policy.clone(), @@ -324,6 +361,8 @@ impl WasmRuntime { time_state, consensus_state, terminal_state, + data_state, + container_state, instance_config.memory_export.clone(), instance_config.challenge_id.clone(), instance_config.validator_id.clone(), @@ -365,6 +404,12 @@ impl WasmRuntime { let terminal_host_fns = TerminalHostFunctions::new(); terminal_host_fns.register(&mut linker)?; + let data_host_fns = DataHostFunctions::new(); + data_host_fns.register(&mut linker)?; + + let container_host_fns = ContainerHostFunctions::new(); + container_host_fns.register(&mut linker)?; + let sandbox_host_fns = SandboxHostFunctions::all(); sandbox_host_fns.register(&mut linker)?; From f0e41dc2f14700471fd857bc8bab319c8679129e Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 09:53:49 +0000 Subject: [PATCH 4/4] fix(validator-node): add missing InstanceConfig fields for container and data modules Add ..Default::default() to three InstanceConfig initializations in wasm_executor.rs that were missing the container_policy, data_policy, and data_backend fields introduced by the container and data module integration. --- bins/validator-node/src/wasm_executor.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bins/validator-node/src/wasm_executor.rs b/bins/validator-node/src/wasm_executor.rs index 199960d4..0ce00dfe 100644 --- a/bins/validator-node/src/wasm_executor.rs +++ b/bins/validator-node/src/wasm_executor.rs @@ -186,6 +186,7 @@ impl WasmChallengeExecutor { fixed_timestamp_ms: None, consensus_policy: ConsensusPolicy::default(), terminal_policy: TerminalPolicy::default(), + ..Default::default() }; let mut instance = self @@ -308,6 +309,7 @@ impl WasmChallengeExecutor { fixed_timestamp_ms: None, consensus_policy: ConsensusPolicy::default(), terminal_policy: TerminalPolicy::default(), + ..Default::default() }; let mut instance = self @@ -496,6 +498,7 @@ impl WasmChallengeExecutor { fixed_timestamp_ms: None, consensus_policy: ConsensusPolicy::default(), terminal_policy: TerminalPolicy::default(), + ..Default::default() }; let mut instance = self