Skip to content

Commit a8c6301

Browse files
authored
feat(wasm): integrate evaluation pipeline with structured I/O and registry support (#35)
* feat(wasm): integrate WASM executor with evaluation pipeline and challenge registry Update the validator node WASM executor, challenge registry, build infrastructure, and documentation to support loading and executing term-challenge WASM modules through the evaluation pipeline. WASM Executor (wasm_executor.rs, main.rs): - Add EvaluationInput/EvaluationOutput structs for structured data exchange across the WASM boundary using bincode serialization - Refactor execute_evaluation() to accept challenge_id and params, construct EvaluationInput, and deserialize packed ptr+len return values into EvaluationOutput with score, valid flag, and message - Add StorageHostConfig/StorageHostState integration for persistent storage - Rename allocator call from "allocate" to "alloc" matching WASM SDK ABI - Handle WASM traps, OOM, fuel exhaustion with descriptive error types - Update main.rs call sites to pass challenge_id and params, and read structured output fields (score, valid, message) from EvaluationOutput Challenge Registry (registry.rs): - Deprecate docker_image field with compile warning in favor of wasm_module - Add register_wasm_challenge() method that validates the WASM file exists, computes its SHA256 hash, and stores the entry with WasmModuleMetadata - Add #[allow(deprecated)] annotations to suppress warnings in existing tests and constructors that still reference docker_image Build Script (build-wasm.sh): - Extract build logic into reusable build_challenge() function - Add wasm-strip support for reducing binary size before wasm-opt - Copy compiled artifacts to challenges/compiled/ standard location - Compute and print SHA256 hash of each compiled module - When no argument given, discover and build all challenge crates under challenges/*/ plus term-challenge-wasm if present as workspace member - Preserve chain-runtime fallback build when no challenges are found Documentation (challenges/README.md): - Document WASM Challenge trait as recommended approach alongside legacy ServerChallenge - Add WASM ABI export requirements (evaluate, validate, alloc) - Add term-challenge-wasm build instructions and link to term-challenge repo - Update external challenge guidance to reference Challenge trait and register_challenge! macro * ci: trigger CI run
1 parent 7846d16 commit a8c6301

7 files changed

Lines changed: 306 additions & 65 deletions

File tree

bins/validator-node/src/main.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ async fn main() -> Result<()> {
373373
max_memory_bytes: args.wasm_max_memory,
374374
enable_fuel: args.wasm_enable_fuel,
375375
fuel_limit: args.wasm_fuel_limit,
376+
storage_host_config: wasm_runtime_interface::StorageHostConfig::default(),
376377
}) {
377378
Ok(executor) => {
378379
info!(
@@ -985,28 +986,37 @@ async fn process_wasm_evaluations(
985986
let network_policy = wasm_runtime_interface::NetworkPolicy::default();
986987

987988
let input_data = submission_id.as_bytes().to_vec();
989+
let challenge_id_str = challenge_id.to_string();
988990

989991
let executor = Arc::clone(executor);
990992
let module_filename_clone = module_filename.clone();
991993

992994
let result = tokio::task::spawn_blocking(move || {
993-
executor.execute_evaluation(&module_filename_clone, &network_policy, &input_data)
995+
executor.execute_evaluation(
996+
&module_filename_clone,
997+
&network_policy,
998+
&input_data,
999+
&challenge_id_str,
1000+
&[],
1001+
)
9941002
})
9951003
.await;
9961004

9971005
let (score, eval_metrics) = match result {
998-
Ok(Ok((score, metrics))) => {
1006+
Ok(Ok((output, metrics))) => {
9991007
info!(
10001008
submission_id = %submission_id,
10011009
challenge_id = %challenge_id,
1002-
score,
1010+
score = output.score,
1011+
valid = output.valid,
1012+
message = %output.message,
10031013
execution_time_ms = metrics.execution_time_ms,
10041014
memory_bytes = metrics.memory_used_bytes,
10051015
network_requests = metrics.network_requests_made,
10061016
fuel_consumed = ?metrics.fuel_consumed,
10071017
"WASM evaluation succeeded"
10081018
);
1009-
let normalized = (score as f64) / i64::MAX as f64;
1019+
let normalized = (output.score as f64) / i64::MAX as f64;
10101020
let em = EvaluationMetrics {
10111021
primary_score: normalized,
10121022
secondary_metrics: vec![],

bins/validator-node/src/wasm_executor.rs

Lines changed: 116 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,57 @@
11
use anyhow::{Context, Result};
22
use parking_lot::RwLock;
3+
use serde::{Deserialize, Serialize};
34
use std::collections::HashMap;
45
use std::path::PathBuf;
56
use std::sync::Arc;
67
use std::time::Instant;
78
use tracing::{debug, info};
89
use wasm_runtime_interface::{
910
ExecPolicy, InMemoryStorageBackend, InstanceConfig, NetworkHostFunctions, NetworkPolicy,
10-
RuntimeConfig, SandboxHostFunctions, SandboxPolicy, StorageHostConfig, TimePolicy, WasmModule,
11-
WasmRuntime, WasmRuntimeError,
11+
NoopStorageBackend, RuntimeConfig, SandboxHostFunctions, SandboxPolicy, StorageHostConfig,
12+
StorageHostState, TimePolicy, WasmModule, WasmRuntime, WasmRuntimeError,
1213
};
1314

15+
#[derive(Clone, Debug, Serialize, Deserialize)]
16+
pub struct EvaluationInput {
17+
pub agent_data: Vec<u8>,
18+
pub challenge_id: String,
19+
pub params: Vec<u8>,
20+
}
21+
22+
#[derive(Clone, Debug, Serialize, Deserialize)]
23+
pub struct EvaluationOutput {
24+
pub score: i64,
25+
pub valid: bool,
26+
pub message: String,
27+
}
28+
29+
impl EvaluationOutput {
30+
#[allow(dead_code)]
31+
pub fn success(score: i64, message: &str) -> Self {
32+
Self {
33+
score,
34+
valid: true,
35+
message: String::from(message),
36+
}
37+
}
38+
39+
#[allow(dead_code)]
40+
pub fn failure(message: &str) -> Self {
41+
Self {
42+
score: 0,
43+
valid: false,
44+
message: String::from(message),
45+
}
46+
}
47+
}
48+
1449
pub struct WasmExecutorConfig {
1550
pub module_dir: PathBuf,
1651
pub max_memory_bytes: u64,
1752
pub enable_fuel: bool,
1853
pub fuel_limit: Option<u64>,
54+
pub storage_host_config: StorageHostConfig,
1955
}
2056

2157
impl Default for WasmExecutorConfig {
@@ -25,6 +61,7 @@ impl Default for WasmExecutorConfig {
2561
max_memory_bytes: 512 * 1024 * 1024,
2662
enable_fuel: false,
2763
fuel_limit: None,
64+
storage_host_config: StorageHostConfig::default(),
2865
}
2966
}
3067
}
@@ -72,13 +109,17 @@ impl WasmChallengeExecutor {
72109
&self,
73110
module_path: &str,
74111
network_policy: &NetworkPolicy,
75-
input_data: &[u8],
76-
) -> Result<(i64, ExecutionMetrics)> {
112+
agent_data: &[u8],
113+
challenge_id: &str,
114+
params: &[u8],
115+
) -> Result<(EvaluationOutput, ExecutionMetrics)> {
77116
self.execute_evaluation_with_sandbox(
78117
module_path,
79118
network_policy,
80119
&SandboxPolicy::default(),
81-
input_data,
120+
agent_data,
121+
challenge_id,
122+
params,
82123
)
83124
}
84125

@@ -87,14 +128,25 @@ impl WasmChallengeExecutor {
87128
module_path: &str,
88129
network_policy: &NetworkPolicy,
89130
sandbox_policy: &SandboxPolicy,
90-
input_data: &[u8],
91-
) -> Result<(i64, ExecutionMetrics)> {
131+
agent_data: &[u8],
132+
challenge_id: &str,
133+
params: &[u8],
134+
) -> Result<(EvaluationOutput, ExecutionMetrics)> {
92135
let start = Instant::now();
93136

94137
let module = self
95138
.load_module(module_path)
96139
.context("Failed to load WASM module")?;
97140

141+
let input = EvaluationInput {
142+
agent_data: agent_data.to_vec(),
143+
challenge_id: challenge_id.to_string(),
144+
params: params.to_vec(),
145+
};
146+
147+
let serialized =
148+
bincode::serialize(&input).context("Failed to serialize EvaluationInput")?;
149+
98150
let network_host_fns = Arc::new(NetworkHostFunctions::all());
99151
let _sandbox_host_fns = Arc::new(SandboxHostFunctions::all());
100152

@@ -105,7 +157,7 @@ impl WasmChallengeExecutor {
105157
time_policy: TimePolicy::default(),
106158
audit_logger: None,
107159
memory_export: "memory".to_string(),
108-
challenge_id: module_path.to_string(),
160+
challenge_id: challenge_id.to_string(),
109161
validator_id: "validator".to_string(),
110162
restart_id: String::new(),
111163
config_version: 0,
@@ -117,16 +169,22 @@ impl WasmChallengeExecutor {
117169
.instantiate(&module, instance_config, Some(network_host_fns))
118170
.map_err(|e| anyhow::anyhow!("WASM instantiation failed: {}", e))?;
119171

172+
let _storage_state = StorageHostState::new(
173+
challenge_id.to_string(),
174+
self.config.storage_host_config.clone(),
175+
Arc::new(NoopStorageBackend),
176+
);
177+
120178
let initial_fuel = instance.fuel_remaining();
121179

122-
let ptr = self.allocate_input(&mut instance, input_data)?;
180+
let ptr = self.allocate_input(&mut instance, &serialized)?;
123181

124182
instance
125-
.write_memory(ptr as usize, input_data)
183+
.write_memory(ptr as usize, &serialized)
126184
.map_err(|e| anyhow::anyhow!("Failed to write input data to WASM memory: {}", e))?;
127185

128-
let score = instance
129-
.call_i32_i32_return_i64("evaluate", ptr, input_data.len() as i32)
186+
let result = instance
187+
.call_i32_i32_return_i64("evaluate", ptr, serialized.len() as i32)
130188
.map_err(|e| match &e {
131189
WasmRuntimeError::FuelExhausted => {
132190
anyhow::anyhow!("WASM execution exceeded fuel limit")
@@ -137,6 +195,24 @@ impl WasmChallengeExecutor {
137195
_ => anyhow::anyhow!("WASM evaluate call failed: {}", e),
138196
})?;
139197

198+
let out_len = (result >> 32) as i32;
199+
let out_ptr = result as i32;
200+
201+
if out_ptr == 0 && out_len == 0 {
202+
return Err(anyhow::anyhow!(
203+
"WASM evaluate returned null pointer, deserialization failed inside module"
204+
));
205+
}
206+
207+
let output_bytes = instance
208+
.read_memory(out_ptr as usize, out_len as usize)
209+
.map_err(|e| {
210+
anyhow::anyhow!("Failed to read evaluation output from WASM memory: {}", e)
211+
})?;
212+
213+
let output: EvaluationOutput = bincode::deserialize(&output_bytes)
214+
.context("Failed to deserialize EvaluationOutput from WASM module")?;
215+
140216
let fuel_consumed = match (initial_fuel, instance.fuel_remaining()) {
141217
(Some(initial), Some(remaining)) => Some(initial.saturating_sub(remaining)),
142218
_ => None,
@@ -151,30 +227,44 @@ impl WasmChallengeExecutor {
151227

152228
info!(
153229
module = module_path,
154-
score,
230+
challenge_id,
231+
score = output.score,
232+
valid = output.valid,
233+
message = %output.message,
155234
execution_time_ms = metrics.execution_time_ms,
156235
memory_bytes = metrics.memory_used_bytes,
157236
network_requests = metrics.network_requests_made,
158237
fuel_consumed = ?metrics.fuel_consumed,
159238
"WASM evaluation completed"
160239
);
161240

162-
Ok((score, metrics))
241+
Ok((output, metrics))
163242
}
164243

165244
#[allow(dead_code)]
166245
pub fn execute_validation(
167246
&self,
168247
module_path: &str,
169248
network_policy: &NetworkPolicy,
170-
input_data: &[u8],
249+
agent_data: &[u8],
250+
challenge_id: &str,
251+
params: &[u8],
171252
) -> Result<(bool, ExecutionMetrics)> {
172253
let start = Instant::now();
173254

174255
let module = self
175256
.load_module(module_path)
176257
.context("Failed to load WASM module")?;
177258

259+
let input = EvaluationInput {
260+
agent_data: agent_data.to_vec(),
261+
challenge_id: challenge_id.to_string(),
262+
params: params.to_vec(),
263+
};
264+
265+
let serialized =
266+
bincode::serialize(&input).context("Failed to serialize EvaluationInput")?;
267+
178268
let network_host_fns = Arc::new(NetworkHostFunctions::all());
179269
let _sandbox_host_fns = Arc::new(SandboxHostFunctions::all());
180270

@@ -185,7 +275,7 @@ impl WasmChallengeExecutor {
185275
time_policy: TimePolicy::default(),
186276
audit_logger: None,
187277
memory_export: "memory".to_string(),
188-
challenge_id: module_path.to_string(),
278+
challenge_id: challenge_id.to_string(),
189279
validator_id: "validator".to_string(),
190280
restart_id: String::new(),
191281
config_version: 0,
@@ -197,16 +287,22 @@ impl WasmChallengeExecutor {
197287
.instantiate(&module, instance_config, Some(network_host_fns))
198288
.map_err(|e| anyhow::anyhow!("WASM instantiation failed: {}", e))?;
199289

290+
let _storage_state = StorageHostState::new(
291+
challenge_id.to_string(),
292+
self.config.storage_host_config.clone(),
293+
Arc::new(NoopStorageBackend),
294+
);
295+
200296
let initial_fuel = instance.fuel_remaining();
201297

202-
let ptr = self.allocate_input(&mut instance, input_data)?;
298+
let ptr = self.allocate_input(&mut instance, &serialized)?;
203299

204300
instance
205-
.write_memory(ptr as usize, input_data)
301+
.write_memory(ptr as usize, &serialized)
206302
.map_err(|e| anyhow::anyhow!("Failed to write input data to WASM memory: {}", e))?;
207303

208304
let result = instance
209-
.call_i32_i32_return_i32("validate", ptr, input_data.len() as i32)
305+
.call_i32_i32_return_i32("validate", ptr, serialized.len() as i32)
210306
.map_err(|e| match &e {
211307
WasmRuntimeError::FuelExhausted => {
212308
anyhow::anyhow!("WASM execution exceeded fuel limit")
@@ -233,6 +329,7 @@ impl WasmChallengeExecutor {
233329

234330
info!(
235331
module = module_path,
332+
challenge_id,
236333
valid,
237334
execution_time_ms = metrics.execution_time_ms,
238335
memory_bytes = metrics.memory_used_bytes,

0 commit comments

Comments
 (0)