Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bins/validator-node/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
77 changes: 49 additions & 28 deletions bins/validator-node/src/wasm_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,30 @@ 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)]
pub struct EvaluationInput {
pub agent_data: Vec<u8>,
pub challenge_id: String,
pub params: Vec<u8>,
#[serde(default)]
pub task_definition: Option<Vec<u8>>,
#[serde(default)]
pub environment_config: Option<Vec<u8>>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EvaluationOutput {
pub score: i64,
pub valid: bool,
pub message: String,
#[serde(default)]
pub metrics: Option<Vec<u8>>,
#[serde(default)]
pub details: Option<Vec<u8>>,
}

impl EvaluationOutput {
Expand All @@ -34,6 +41,8 @@ impl EvaluationOutput {
score,
valid: true,
message: String::from(message),
metrics: None,
details: None,
}
}

Expand All @@ -43,6 +52,8 @@ impl EvaluationOutput {
score: 0,
valid: false,
message: String::from(message),
metrics: None,
details: None,
}
}
}
Expand All @@ -53,6 +64,7 @@ pub struct WasmExecutorConfig {
pub enable_fuel: bool,
pub fuel_limit: Option<u64>,
pub storage_host_config: StorageHostConfig,
pub storage_backend: Arc<dyn StorageBackend>,
}

impl Default for WasmExecutorConfig {
Expand All @@ -63,6 +75,7 @@ impl Default for WasmExecutorConfig {
enable_fuel: false,
fuel_limit: None,
storage_host_config: StorageHostConfig::default(),
storage_backend: Arc::new(InMemoryStorageBackend::new()),
}
}
}
Expand Down Expand Up @@ -143,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 =
Expand All @@ -162,6 +177,15 @@ impl WasmChallengeExecutor {
validator_id: "validator".to_string(),
restart_id: String::new(),
config_version: 0,
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(),
..Default::default()
};

Expand All @@ -170,12 +194,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)?;
Expand Down Expand Up @@ -261,6 +279,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 =
Expand All @@ -280,6 +300,15 @@ impl WasmChallengeExecutor {
validator_id: "validator".to_string(),
restart_id: String::new(),
config_version: 0,
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(),
..Default::default()
};

Expand All @@ -288,12 +317,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)?;
Expand Down Expand Up @@ -396,6 +419,7 @@ impl WasmChallengeExecutor {
fixed_timestamp_ms: None,
consensus_policy: ConsensusPolicy::default(),
terminal_policy: TerminalPolicy::default(),
..Default::default()
};

let mut instance = self
Expand All @@ -405,21 +429,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()
};
Comment on lines +432 to 445
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the wasm_executor.rs file and check its structure
fd "wasm_executor.rs" --type f

Repository: PlatformNetwork/platform-v2

Length of output: 111


🏁 Script executed:

# Search for call_return_i64 method definition
rg "call_return_i64" --type rust -B 2 -A 5

Repository: PlatformNetwork/platform-v2

Length of output: 1332


🏁 Script executed:

# Search for the instance type and its methods in wasm_executor.rs
rg "fn call_return_i64|impl.*Instance|struct.*Instance" --type rust -A 3

Repository: PlatformNetwork/platform-v2

Length of output: 1970


🏁 Script executed:

# Find the read_memory method definition
rg "fn read_memory" --type rust -A 5

Repository: PlatformNetwork/platform-v2

Length of output: 2602


🏁 Script executed:

# Check broader context in wasm_executor.rs around the code in question
sed -n '400,500p' bins/validator-node/src/wasm_executor.rs | cat -n

Repository: PlatformNetwork/platform-v2

Length of output: 4547


🏁 Script executed:

# Search for other uses of read_memory in wasm_executor.rs to see error handling patterns
rg "read_memory" bins/validator-node/src/wasm_executor.rs -B 1 -A 2

Repository: PlatformNetwork/platform-v2

Length of output: 429


call_return_i64 method confirmed to exist — the ABI fix is correctly implemented. The bit-unpacking logic (len << 32) | ptr is properly inverted via (result >> 32) and (result & 0xFFFF_FFFF).

On line 442, .unwrap_or_default() silently discards read_memory errors, which diverges from the error-handling pattern used elsewhere in this file (e.g., map_err with explicit error messages). Since read_memory can fail for out-of-bounds access or invalid addresses, consider replacing .unwrap_or_default() with proper error handling or at least logging to preserve debuggability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bins/validator-node/src/wasm_executor.rs` around lines 432 - 445, The code
silently swallows errors from instance.read_memory when building result_data
(after calling instance.call_return_i64 for "get_tasks"); replace the
.unwrap_or_default() on the read_memory call with proper error propagation and
contextual messaging (e.g., map_err(...)?), so read_memory failures (invalid
address/out-of-bounds) produce an anyhow::Error with a clear message rather than
returning an empty Vec; update the result_data assignment to propagate the error
using the same pattern as the surrounding map_err usage for call_return_i64 and
include identifiers like instance.read_memory, call_return_i64("get_tasks"), and
result_data in the error context.

Expand Down Expand Up @@ -478,6 +498,7 @@ impl WasmChallengeExecutor {
fixed_timestamp_ms: None,
consensus_policy: ConsensusPolicy::default(),
terminal_policy: TerminalPolicy::default(),
..Default::default()
};

let mut instance = self
Expand Down
1 change: 1 addition & 0 deletions crates/challenge-sdk-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 5 additions & 2 deletions crates/challenge-sdk-wasm/src/alloc_impl.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
1 change: 1 addition & 0 deletions crates/challenge-sdk-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions crates/challenge-sdk-wasm/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,24 @@ pub struct TermEvaluationParams {
pub timeout_ms: u64,
pub environment_config: Option<Vec<u8>>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ContainerRunRequest {
pub image: String,
pub command: Vec<String>,
pub env_vars: Vec<(String, String)>,
pub working_dir: Option<String>,
pub stdin: Option<Vec<u8>>,
pub memory_limit_mb: Option<u64>,
pub cpu_limit: Option<u32>,
pub network_mode: Option<String>,
pub timeout_ms: u64,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ContainerRunResponse {
pub exit_code: i32,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub duration_ms: u64,
}
4 changes: 2 additions & 2 deletions crates/wasm-runtime-interface/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Score normalization now correctly matches score_f64_scaled.

The divisor 10_000.0 aligns with score_f64_scaled in the SDK which multiplies by 10_000.0. The test at line 188 is updated accordingly.

However, note that bins/validator-node/src/main.rs at line 1184 normalizes the same output.score differently:

let normalized = (output.score as f64) / i64::MAX as f64;

A challenge using score_f64_scaled(1.0) produces score = 10_000. The bridge correctly yields 1.0, but the validator's process_wasm_evaluations yields ≈ 1.08e-15. One of these paths is wrong — the validator path should likely use / 10_000.0 as well.

Proposed fix in bins/validator-node/src/main.rs
-                let normalized = (output.score as f64) / i64::MAX as f64;
+                let normalized = (output.score as f64) / 10_000.0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/wasm-runtime-interface/src/bridge.rs` at line 98, The bridge
normalizes output.score by dividing by 10_000.0 (matching SDK's
score_f64_scaled), but bins/validator-node's process_wasm_evaluations currently
normalizes with i64::MAX; update that path so the same score normalization is
applied: in the process_wasm_evaluations function change the computation of the
variable normalized (which uses output.score) to divide by 10_000.0 (consistent
with score_f64_scaled and the bridge implementation) and ensure any
comments/tests reflect this scaling.

let results = serde_json::json!({ "message": output.message });
EvalResponse::success(request_id, score, results).with_time(execution_time_ms)
} else {
Expand Down Expand Up @@ -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");
Expand Down
Loading