From f32ec9fb64245ab54cc3031e2b9e8f144d2ce896 Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 12:29:13 +0000 Subject: [PATCH 01/10] feat(llm): add LLM host functions to challenge-sdk-wasm and wasm-runtime-interface - Add llm_types.rs with LlmRequest, LlmMessage, LlmResponse, LlmUsage types (no_std) - Add platform_llm extern block with llm_chat_completion and llm_is_available host functions - Add LlmPolicy, LlmState, LlmHostFunctions, LlmHostStatus to wasm-runtime-interface - Integrate LlmState into RuntimeState and LlmPolicy into InstanceConfig - Register LlmHostFunctions in the WASM linker during instantiation --- .../challenge-sdk-wasm/src/host_functions.rs | 27 ++ crates/challenge-sdk-wasm/src/lib.rs | 2 + crates/challenge-sdk-wasm/src/llm_types.rs | 30 ++ crates/wasm-runtime-interface/src/lib.rs | 4 + crates/wasm-runtime-interface/src/llm.rs | 429 ++++++++++++++++++ crates/wasm-runtime-interface/src/runtime.rs | 13 + 6 files changed, 505 insertions(+) create mode 100644 crates/challenge-sdk-wasm/src/llm_types.rs create mode 100644 crates/wasm-runtime-interface/src/llm.rs diff --git a/crates/challenge-sdk-wasm/src/host_functions.rs b/crates/challenge-sdk-wasm/src/host_functions.rs index fe11096d..4fe548f2 100644 --- a/crates/challenge-sdk-wasm/src/host_functions.rs +++ b/crates/challenge-sdk-wasm/src/host_functions.rs @@ -216,6 +216,33 @@ pub fn host_log(level: u8, msg: &str) { unsafe { log_message(level as i32, msg.as_ptr() as i32, msg.len() as i32) } } +#[link(wasm_import_module = "platform_llm")] +extern "C" { + fn llm_chat_completion(req_ptr: i32, req_len: i32, resp_ptr: i32, resp_len: i32) -> i32; + fn llm_is_available() -> i32; +} + +pub fn host_llm_chat_completion(request: &[u8]) -> Result, i32> { + let mut response_buf = vec![0u8; 262144]; + let status = unsafe { + llm_chat_completion( + request.as_ptr() as i32, + request.len() as i32, + response_buf.as_mut_ptr() as i32, + response_buf.len() as i32, + ) + }; + if status < 0 { + return Err(status); + } + response_buf.truncate(status as usize); + Ok(response_buf) +} + +pub fn host_llm_is_available() -> bool { + unsafe { llm_is_available() == 1 } +} + #[link(wasm_import_module = "platform_consensus")] extern "C" { fn consensus_get_epoch() -> i64; diff --git a/crates/challenge-sdk-wasm/src/lib.rs b/crates/challenge-sdk-wasm/src/lib.rs index 8d278dd4..796552e9 100644 --- a/crates/challenge-sdk-wasm/src/lib.rs +++ b/crates/challenge-sdk-wasm/src/lib.rs @@ -4,9 +4,11 @@ extern crate alloc; pub mod alloc_impl; pub mod host_functions; +pub mod llm_types; pub mod term_types; pub mod types; +pub use llm_types::{LlmMessage, LlmRequest, LlmResponse, LlmUsage}; pub use term_types::*; pub use types::{ score_f64_scaled, SandboxExecRequest, SandboxExecResponse, TaskDefinition, TaskResult, diff --git a/crates/challenge-sdk-wasm/src/llm_types.rs b/crates/challenge-sdk-wasm/src/llm_types.rs new file mode 100644 index 00000000..76a2ec77 --- /dev/null +++ b/crates/challenge-sdk-wasm/src/llm_types.rs @@ -0,0 +1,30 @@ +use alloc::string::String; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LlmRequest { + pub model: String, + pub messages: Vec, + pub max_tokens: u32, + pub temperature: f32, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LlmMessage { + pub role: String, + pub content: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LlmResponse { + pub content: String, + pub usage: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LlmUsage { + pub prompt_tokens: u32, + pub completion_tokens: u32, + pub total_tokens: u32, +} diff --git a/crates/wasm-runtime-interface/src/lib.rs b/crates/wasm-runtime-interface/src/lib.rs index 7b57a4db..a72b7362 100644 --- a/crates/wasm-runtime-interface/src/lib.rs +++ b/crates/wasm-runtime-interface/src/lib.rs @@ -18,6 +18,7 @@ pub mod network; pub mod runtime; pub mod sandbox; pub mod storage; +pub mod llm; pub mod terminal; pub mod time; pub use bridge::{ @@ -74,6 +75,9 @@ pub use terminal::{ TerminalHostFunctions, TerminalHostStatus, TerminalPolicy, TerminalState, HOST_TERMINAL_NAMESPACE, }; +pub use llm::{ + LlmHostFunctions, LlmHostStatus, LlmPolicy, LlmState, HOST_LLM_NAMESPACE, +}; pub use time::{TimeError, TimeHostFunction, TimeHostFunctions, TimeMode, TimePolicy, TimeState}; /// Host functions that may be exposed to WASM challenges. diff --git a/crates/wasm-runtime-interface/src/llm.rs b/crates/wasm-runtime-interface/src/llm.rs new file mode 100644 index 00000000..79440902 --- /dev/null +++ b/crates/wasm-runtime-interface/src/llm.rs @@ -0,0 +1,429 @@ +//! LLM Host Functions for WASM Challenges +//! +//! Provides host functions that allow WASM code to perform LLM inference +//! via the Chutes API (llm.chutes.ai). Gated by `LlmPolicy`. +//! +//! # Host Functions +//! +//! - `llm_chat_completion(req_ptr, req_len, resp_ptr, resp_len) -> i32` — Send chat completion request +//! - `llm_is_available() -> i32` — Check if LLM inference is available (has API key) + +use crate::runtime::{HostFunctionRegistrar, RuntimeState, WasmRuntimeError}; +use serde::{Deserialize, Serialize}; +use tracing::warn; +use wasmtime::{Caller, Linker, Memory}; + +pub const HOST_LLM_NAMESPACE: &str = "platform_llm"; +pub const HOST_LLM_CHAT_COMPLETION: &str = "llm_chat_completion"; +pub const HOST_LLM_IS_AVAILABLE: &str = "llm_is_available"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(i32)] +pub enum LlmHostStatus { + Success = 0, + Disabled = -1, + InvalidRequest = -2, + ApiError = -3, + BufferTooSmall = -4, + RateLimited = -5, + InternalError = -100, +} + +impl LlmHostStatus { + pub fn to_i32(self) -> i32 { + self as i32 + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LlmPolicy { + pub enabled: bool, + pub api_key: Option, + pub endpoint: String, + pub max_requests: u32, + pub allowed_models: Vec, +} + +impl Default for LlmPolicy { + fn default() -> Self { + Self { + enabled: false, + api_key: None, + endpoint: "https://llm.chutes.ai/v1/chat/completions".to_string(), + max_requests: 10, + allowed_models: Vec::new(), + } + } +} + +impl LlmPolicy { + pub fn with_api_key(api_key: String) -> Self { + Self { + enabled: true, + api_key: Some(api_key), + ..Default::default() + } + } + + pub fn is_available(&self) -> bool { + self.enabled && self.api_key.is_some() + } +} + +pub struct LlmState { + pub policy: LlmPolicy, + pub requests_made: u32, +} + +impl LlmState { + pub fn new(policy: LlmPolicy) -> Self { + Self { + policy, + requests_made: 0, + } + } +} + +#[derive(Clone, Debug)] +pub struct LlmHostFunctions; + +impl LlmHostFunctions { + pub fn new() -> Self { + Self + } +} + +impl Default for LlmHostFunctions { + fn default() -> Self { + Self::new() + } +} + +impl HostFunctionRegistrar for LlmHostFunctions { + fn register(&self, linker: &mut Linker) -> Result<(), WasmRuntimeError> { + linker + .func_wrap( + HOST_LLM_NAMESPACE, + HOST_LLM_CHAT_COMPLETION, + |mut caller: Caller, + req_ptr: i32, + req_len: i32, + resp_ptr: i32, + resp_len: i32| + -> i32 { + handle_chat_completion(&mut caller, req_ptr, req_len, resp_ptr, resp_len) + }, + ) + .map_err(|err| WasmRuntimeError::HostFunction(err.to_string()))?; + + linker + .func_wrap( + HOST_LLM_NAMESPACE, + HOST_LLM_IS_AVAILABLE, + |caller: Caller| -> i32 { handle_is_available(&caller) }, + ) + .map_err(|err| WasmRuntimeError::HostFunction(err.to_string()))?; + + Ok(()) + } +} + +fn handle_is_available(caller: &Caller) -> i32 { + let state = &caller.data().llm_state; + if state.policy.is_available() { + 1 + } else { + 0 + } +} + +fn handle_chat_completion( + caller: &mut Caller, + req_ptr: i32, + req_len: i32, + resp_ptr: i32, + resp_len: i32, +) -> i32 { + let policy_available; + let requests_made; + let max_requests; + { + let state = &caller.data().llm_state; + policy_available = state.policy.is_available(); + requests_made = state.requests_made; + max_requests = state.policy.max_requests; + } + + if !policy_available { + return LlmHostStatus::Disabled.to_i32(); + } + + if requests_made >= max_requests { + return LlmHostStatus::RateLimited.to_i32(); + } + + if req_ptr < 0 || req_len < 0 || resp_ptr < 0 || resp_len < 0 { + return LlmHostStatus::InvalidRequest.to_i32(); + } + + let request_bytes = match read_wasm_memory(caller, req_ptr, req_len as usize) { + Ok(b) => b, + Err(err) => { + warn!(error = %err, "llm_chat_completion: failed to read request from wasm memory"); + return LlmHostStatus::InternalError.to_i32(); + } + }; + + let api_key; + let endpoint; + { + let state = &caller.data().llm_state; + api_key = match &state.policy.api_key { + Some(k) => k.clone(), + None => return LlmHostStatus::Disabled.to_i32(), + }; + endpoint = state.policy.endpoint.clone(); + } + + #[derive(Deserialize)] + struct ChatRequest { + model: String, + messages: Vec, + max_tokens: Option, + temperature: Option, + } + + #[derive(Deserialize)] + struct ChatMessage { + role: String, + content: String, + } + + let chat_req: ChatRequest = match bincode::deserialize(&request_bytes) { + Ok(r) => r, + Err(_) => return LlmHostStatus::InvalidRequest.to_i32(), + }; + + #[derive(Serialize)] + struct OpenAiRequest { + model: String, + messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + } + + #[derive(Serialize)] + struct OpenAiMessage { + role: String, + content: String, + } + + let openai_req = OpenAiRequest { + model: chat_req.model, + messages: chat_req + .messages + .into_iter() + .map(|m| OpenAiMessage { + role: m.role, + content: m.content, + }) + .collect(), + max_tokens: chat_req.max_tokens, + temperature: chat_req.temperature, + }; + + let json_body = match serde_json::to_vec(&openai_req) { + Ok(b) => b, + Err(_) => return LlmHostStatus::InvalidRequest.to_i32(), + }; + + let client = reqwest::blocking::Client::new(); + let http_response = match client + .post(&endpoint) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", api_key)) + .body(json_body) + .timeout(std::time::Duration::from_secs(60)) + .send() + { + Ok(r) => r, + Err(err) => { + warn!(error = %err, "llm_chat_completion: HTTP request failed"); + return LlmHostStatus::ApiError.to_i32(); + } + }; + + let response_body = match http_response.bytes() { + Ok(b) => b.to_vec(), + Err(err) => { + warn!(error = %err, "llm_chat_completion: failed to read response body"); + return LlmHostStatus::ApiError.to_i32(); + } + }; + + #[derive(Deserialize)] + struct OpenAiResponse { + choices: Option>, + usage: Option, + } + + #[derive(Deserialize)] + struct OpenAiChoice { + message: Option, + } + + #[derive(Deserialize)] + struct OpenAiRespMessage { + content: Option, + } + + #[derive(Deserialize)] + struct OpenAiUsage { + prompt_tokens: Option, + completion_tokens: Option, + total_tokens: Option, + } + + let openai_resp: OpenAiResponse = match serde_json::from_slice(&response_body) { + Ok(r) => r, + Err(err) => { + warn!(error = %err, "llm_chat_completion: failed to parse OpenAI response"); + return LlmHostStatus::ApiError.to_i32(); + } + }; + + let content = openai_resp + .choices + .and_then(|mut c| c.pop()) + .and_then(|c| c.message) + .and_then(|m| m.content) + .unwrap_or_default(); + + #[derive(Serialize)] + struct LlmResponsePayload { + content: String, + usage: Option, + } + + #[derive(Serialize)] + struct LlmUsagePayload { + prompt_tokens: u32, + completion_tokens: u32, + total_tokens: u32, + } + + let usage = openai_resp.usage.map(|u| LlmUsagePayload { + prompt_tokens: u.prompt_tokens.unwrap_or(0), + completion_tokens: u.completion_tokens.unwrap_or(0), + total_tokens: u.total_tokens.unwrap_or(0), + }); + + let response_payload = LlmResponsePayload { content, usage }; + + let response_bytes = match bincode::serialize(&response_payload) { + Ok(b) => b, + Err(_) => return LlmHostStatus::InternalError.to_i32(), + }; + + if response_bytes.len() > resp_len as usize { + return LlmHostStatus::BufferTooSmall.to_i32(); + } + + if let Err(err) = write_wasm_memory(caller, resp_ptr, &response_bytes) { + warn!(error = %err, "llm_chat_completion: failed to write response to wasm memory"); + return LlmHostStatus::InternalError.to_i32(); + } + + caller.data_mut().llm_state.requests_made += 1; + + response_bytes.len() as i32 +} + +fn read_wasm_memory( + caller: &mut Caller, + ptr: i32, + len: usize, +) -> 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(len) + .ok_or_else(|| "pointer overflow".to_string())?; + let data = memory.data(caller); + 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_llm_host_status_values() { + assert_eq!(LlmHostStatus::Success.to_i32(), 0); + assert_eq!(LlmHostStatus::Disabled.to_i32(), -1); + assert_eq!(LlmHostStatus::InvalidRequest.to_i32(), -2); + assert_eq!(LlmHostStatus::ApiError.to_i32(), -3); + assert_eq!(LlmHostStatus::BufferTooSmall.to_i32(), -4); + assert_eq!(LlmHostStatus::RateLimited.to_i32(), -5); + assert_eq!(LlmHostStatus::InternalError.to_i32(), -100); + } + + #[test] + fn test_llm_policy_default() { + let policy = LlmPolicy::default(); + assert!(!policy.enabled); + assert!(policy.api_key.is_none()); + assert!(!policy.is_available()); + } + + #[test] + fn test_llm_policy_with_api_key() { + let policy = LlmPolicy::with_api_key("test-key".to_string()); + assert!(policy.enabled); + assert!(policy.is_available()); + assert_eq!(policy.api_key, Some("test-key".to_string())); + } + + #[test] + fn test_llm_state_creation() { + let state = LlmState::new(LlmPolicy::default()); + assert_eq!(state.requests_made, 0); + assert!(!state.policy.is_available()); + } +} diff --git a/crates/wasm-runtime-interface/src/runtime.rs b/crates/wasm-runtime-interface/src/runtime.rs index 5dbc408a..38c5939b 100644 --- a/crates/wasm-runtime-interface/src/runtime.rs +++ b/crates/wasm-runtime-interface/src/runtime.rs @@ -3,6 +3,7 @@ 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::llm::{LlmHostFunctions, LlmPolicy, LlmState}; use crate::sandbox::SandboxHostFunctions; use crate::storage::{ InMemoryStorageBackend, StorageBackend, StorageHostConfig, StorageHostFunctions, @@ -130,6 +131,8 @@ pub struct InstanceConfig { pub data_backend: Arc, /// Container policy for WASM access to container execution. pub container_policy: ContainerPolicy, + /// LLM policy for WASM access to LLM inference. + pub llm_policy: LlmPolicy, } impl Default for InstanceConfig { @@ -153,6 +156,7 @@ impl Default for InstanceConfig { data_policy: DataPolicy::default(), data_backend: Arc::new(NoopDataBackend), container_policy: ContainerPolicy::default(), + llm_policy: LlmPolicy::default(), } } } @@ -190,6 +194,8 @@ pub struct RuntimeState { pub data_state: DataState, /// Container state for container execution host operations. pub container_state: ContainerState, + /// LLM state for LLM inference host operations. + pub llm_state: LlmState, limits: StoreLimits, } @@ -205,6 +211,7 @@ impl RuntimeState { terminal_state: TerminalState, data_state: DataState, container_state: ContainerState, + llm_state: LlmState, memory_export: String, challenge_id: String, validator_id: String, @@ -224,6 +231,7 @@ impl RuntimeState { terminal_state, data_state, container_state, + llm_state, memory_export, challenge_id, validator_id, @@ -353,6 +361,7 @@ impl WasmRuntime { instance_config.challenge_id.clone(), instance_config.validator_id.clone(), ); + let llm_state = LlmState::new(instance_config.llm_policy.clone()); let runtime_state = RuntimeState::new( instance_config.network_policy.clone(), instance_config.sandbox_policy.clone(), @@ -363,6 +372,7 @@ impl WasmRuntime { terminal_state, data_state, container_state, + llm_state, instance_config.memory_export.clone(), instance_config.challenge_id.clone(), instance_config.validator_id.clone(), @@ -410,6 +420,9 @@ impl WasmRuntime { let container_host_fns = ContainerHostFunctions::new(); container_host_fns.register(&mut linker)?; + let llm_host_fns = LlmHostFunctions::new(); + llm_host_fns.register(&mut linker)?; + let sandbox_host_fns = SandboxHostFunctions::all(); sandbox_host_fns.register(&mut linker)?; From 65d923aaae740964492210b3a3729d6434b89e06 Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 12:41:17 +0000 Subject: [PATCH 02/10] feat: add LLM inference, review consensus, and enhanced storage - Add LLM host functions (challenge-sdk-wasm + wasm-runtime-interface): llm_types.rs, platform_llm extern block, LlmPolicy, LlmState, LlmHostFunctions - Add review assignment P2P messages: ReviewType, ReviewAssignmentMessage, ReviewDeclineMessage, ReviewResultMessage - Add deterministic review validator selection via SHA256 seed - Add ReviewRecord/ReviewResultEntry tracking in ChainState - Add DynamicStorage: query_by_prefix(), get_at_block(), list_keys() - Add LlmPolicy/chutes_api_key to validator-node wasm_executor - Update challenge copies (term-challenge, term-challenge-wasm): new types, scoring with decay, dataset/routes/tasks modules - Delete legacy evaluation.rs from term-challenge --- bins/validator-node/src/main.rs | 25 ++ bins/validator-node/src/wasm_executor.rs | 25 +- challenges/term-challenge-wasm/src/dataset.rs | 87 +++++++ challenges/term-challenge-wasm/src/lib.rs | 183 ++++++++++---- challenges/term-challenge-wasm/src/routes.rs | 49 ++++ challenges/term-challenge-wasm/src/scoring.rs | 72 ++---- challenges/term-challenge-wasm/src/tasks.rs | 49 ++++ challenges/term-challenge-wasm/src/types.rs | 64 +++-- challenges/term-challenge/src/dataset.rs | 87 +++++++ challenges/term-challenge/src/evaluation.rs | 76 ------ challenges/term-challenge/src/lib.rs | 228 ++++++++++++++++-- challenges/term-challenge/src/routes.rs | 49 ++++ challenges/term-challenge/src/scoring.rs | 96 ++++++-- challenges/term-challenge/src/tasks.rs | 176 +++----------- challenges/term-challenge/src/types.rs | 77 +++++- crates/p2p-consensus/src/consensus.rs | 63 +++++ crates/p2p-consensus/src/messages.rs | 83 ++++++- crates/p2p-consensus/src/network.rs | 3 + crates/p2p-consensus/src/state.rs | 60 +++++ crates/storage/src/dynamic.rs | 70 ++++++ 20 files changed, 1250 insertions(+), 372 deletions(-) create mode 100644 challenges/term-challenge-wasm/src/dataset.rs create mode 100644 challenges/term-challenge-wasm/src/routes.rs create mode 100644 challenges/term-challenge-wasm/src/tasks.rs create mode 100644 challenges/term-challenge/src/dataset.rs delete mode 100644 challenges/term-challenge/src/evaluation.rs create mode 100644 challenges/term-challenge/src/routes.rs diff --git a/bins/validator-node/src/main.rs b/bins/validator-node/src/main.rs index 60721271..9b348ca4 100644 --- a/bins/validator-node/src/main.rs +++ b/bins/validator-node/src/main.rs @@ -377,6 +377,7 @@ async fn main() -> Result<()> { 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()), + chutes_api_key: None, }) { Ok(executor) => { info!( @@ -952,6 +953,30 @@ async fn handle_network_event( "Received storage vote" ); } + P2PMessage::ReviewAssignment(msg) => { + debug!( + submission_id = %msg.submission_id, + assigner = %msg.assigner.to_hex(), + assigned_count = msg.assigned_validators.len(), + "Received review assignment" + ); + } + P2PMessage::ReviewDecline(msg) => { + debug!( + submission_id = %msg.submission_id, + validator = %msg.validator.to_hex(), + reason = %msg.reason, + "Received review decline" + ); + } + P2PMessage::ReviewResult(msg) => { + debug!( + submission_id = %msg.submission_id, + validator = %msg.validator.to_hex(), + score = msg.score, + "Received review result" + ); + } }, NetworkEvent::PeerConnected(peer_id) => { info!("Peer connected: {}", peer_id); diff --git a/bins/validator-node/src/wasm_executor.rs b/bins/validator-node/src/wasm_executor.rs index 0ce00dfe..e95ec8bf 100644 --- a/bins/validator-node/src/wasm_executor.rs +++ b/bins/validator-node/src/wasm_executor.rs @@ -7,9 +7,10 @@ use std::sync::Arc; use std::time::Instant; use tracing::{debug, info}; use wasm_runtime_interface::{ - ConsensusPolicy, ExecPolicy, InMemoryStorageBackend, InstanceConfig, NetworkHostFunctions, - NetworkPolicy, RuntimeConfig, SandboxHostFunctions, SandboxPolicy, StorageBackend, - StorageHostConfig, TerminalPolicy, TimePolicy, WasmModule, WasmRuntime, WasmRuntimeError, + ConsensusPolicy, ExecPolicy, InMemoryStorageBackend, InstanceConfig, LlmPolicy, + NetworkHostFunctions, NetworkPolicy, RuntimeConfig, SandboxHostFunctions, SandboxPolicy, + StorageBackend, StorageHostConfig, TerminalPolicy, TimePolicy, WasmModule, WasmRuntime, + WasmRuntimeError, }; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -65,6 +66,7 @@ pub struct WasmExecutorConfig { pub fuel_limit: Option, pub storage_host_config: StorageHostConfig, pub storage_backend: Arc, + pub chutes_api_key: Option, } impl Default for WasmExecutorConfig { @@ -76,6 +78,7 @@ impl Default for WasmExecutorConfig { fuel_limit: None, storage_host_config: StorageHostConfig::default(), storage_backend: Arc::new(InMemoryStorageBackend::new()), + chutes_api_key: None, } } } @@ -186,6 +189,10 @@ impl WasmChallengeExecutor { fixed_timestamp_ms: None, consensus_policy: ConsensusPolicy::default(), terminal_policy: TerminalPolicy::default(), + llm_policy: match &self.config.chutes_api_key { + Some(key) => LlmPolicy::with_api_key(key.clone()), + None => LlmPolicy::default(), + }, ..Default::default() }; @@ -309,6 +316,10 @@ impl WasmChallengeExecutor { fixed_timestamp_ms: None, consensus_policy: ConsensusPolicy::default(), terminal_policy: TerminalPolicy::default(), + llm_policy: match &self.config.chutes_api_key { + Some(key) => LlmPolicy::with_api_key(key.clone()), + None => LlmPolicy::default(), + }, ..Default::default() }; @@ -419,6 +430,10 @@ impl WasmChallengeExecutor { fixed_timestamp_ms: None, consensus_policy: ConsensusPolicy::default(), terminal_policy: TerminalPolicy::default(), + llm_policy: match &self.config.chutes_api_key { + Some(key) => LlmPolicy::with_api_key(key.clone()), + None => LlmPolicy::default(), + }, ..Default::default() }; @@ -498,6 +513,10 @@ impl WasmChallengeExecutor { fixed_timestamp_ms: None, consensus_policy: ConsensusPolicy::default(), terminal_policy: TerminalPolicy::default(), + llm_policy: match &self.config.chutes_api_key { + Some(key) => LlmPolicy::with_api_key(key.clone()), + None => LlmPolicy::default(), + }, ..Default::default() }; diff --git a/challenges/term-challenge-wasm/src/dataset.rs b/challenges/term-challenge-wasm/src/dataset.rs new file mode 100644 index 00000000..c0f91625 --- /dev/null +++ b/challenges/term-challenge-wasm/src/dataset.rs @@ -0,0 +1,87 @@ +use alloc::string::String; +use alloc::vec::Vec; +use alloc::collections::BTreeMap; +use core::fmt::Write as _; +use platform_challenge_sdk_wasm::host_functions::{ + host_consensus_get_epoch, host_random_seed, host_storage_set, +}; +use crate::types::{DatasetSelection, TaskDefinition}; + +const DATASET_SELECTION_PREFIX: &[u8] = b"dataset_selection:"; +const TOTAL_SWE_BENCH_TASKS: usize = 2294; +const TASKS_TO_SELECT: usize = 100; +const CONSENSUS_DATASET_SIZE: usize = 50; + +pub fn select_random_task_indices() -> Vec { + let mut seed = [0u8; 32]; + if host_random_seed(&mut seed).is_err() { + return Vec::new(); + } + let mut indices = Vec::with_capacity(TASKS_TO_SELECT); + let mut state: u64 = u64::from_le_bytes([ + seed[0], seed[1], seed[2], seed[3], seed[4], seed[5], seed[6], seed[7], + ]); + while indices.len() < TASKS_TO_SELECT { + state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + let idx = (state >> 33) as usize % TOTAL_SWE_BENCH_TASKS; + if !indices.contains(&idx) { + indices.push(idx); + } + } + indices +} + +pub fn store_my_selection(indices: &[usize]) -> bool { + let epoch = host_consensus_get_epoch(); + if epoch < 0 { + return false; + } + let mut key = Vec::with_capacity(DATASET_SELECTION_PREFIX.len() + 8); + key.extend_from_slice(DATASET_SELECTION_PREFIX); + key.extend_from_slice(&(epoch as u64).to_le_bytes()); + let data = match bincode::serialize(indices) { + Ok(d) => d, + Err(_) => return false, + }; + host_storage_set(&key, &data).is_ok() +} + +pub fn build_consensus_dataset( + all_tasks: &[TaskDefinition], + validator_selections: &[Vec], +) -> Vec { + if validator_selections.is_empty() || all_tasks.is_empty() { + return Vec::new(); + } + let threshold = validator_selections.len().div_ceil(2); + let mut counts = BTreeMap::new(); + for selection in validator_selections { + for &idx in selection { + *counts.entry(idx).or_insert(0usize) += 1; + } + } + let mut consensus_indices: Vec = counts + .into_iter() + .filter(|(_, count)| *count >= threshold) + .map(|(idx, _)| idx) + .collect(); + consensus_indices.sort_unstable(); + consensus_indices.truncate(CONSENSUS_DATASET_SIZE); + consensus_indices + .iter() + .filter_map(|&idx| all_tasks.get(idx).cloned()) + .collect() +} + +pub fn create_dataset_selection(tasks: Vec) -> DatasetSelection { + let epoch = host_consensus_get_epoch(); + let mut hash_input = String::new(); + for task in &tasks { + let _ = write!(hash_input, "{}:{};", task.id, task.name); + } + DatasetSelection { + tasks, + selected_at_epoch: if epoch >= 0 { epoch as u64 } else { 0 }, + dataset_hash: hash_input, + } +} diff --git a/challenges/term-challenge-wasm/src/lib.rs b/challenges/term-challenge-wasm/src/lib.rs index 90590ffe..1d34d435 100644 --- a/challenges/term-challenge-wasm/src/lib.rs +++ b/challenges/term-challenge-wasm/src/lib.rs @@ -2,27 +2,93 @@ extern crate alloc; +mod dataset; +mod routes; mod scoring; +mod tasks; mod types; -use alloc::string::String; use alloc::vec::Vec; -use platform_challenge_sdk_wasm::host_functions::host_http_post; +use bincode::Options; +use platform_challenge_sdk_wasm::host_functions::{ + host_consensus_get_epoch, host_http_post, host_storage_get, host_storage_set, +}; use platform_challenge_sdk_wasm::{Challenge, EvaluationInput, EvaluationOutput}; use crate::scoring::{calculate_aggregate, format_summary, to_weight}; -use crate::types::{ChallengeParams, LlmJudgeRequest, LlmJudgeResponse, Submission, TaskResult}; +use crate::types::{ChallengeParams, DatasetSelection, LlmJudgeRequest, LlmJudgeResponse, Submission, TaskResult}; + +use alloc::string::String; + +const MAX_SUBMISSION_SIZE: u64 = 64 * 1024 * 1024; +const MAX_PARAMS_SIZE: u64 = 4 * 1024 * 1024; +const MAX_LLM_RESPONSE_SIZE: u64 = 1024 * 1024; +const MAX_TASKS: usize = 256; +const EPOCH_RATE_LIMIT: u64 = 3; + +fn bincode_options_submission() -> impl Options { + bincode::DefaultOptions::new() + .with_limit(MAX_SUBMISSION_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() +} + +fn bincode_options_params() -> impl Options { + bincode::DefaultOptions::new() + .with_limit(MAX_PARAMS_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() +} + +fn bincode_options_llm() -> impl Options { + bincode::DefaultOptions::new() + .with_limit(MAX_LLM_RESPONSE_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() +} + +fn validate_task_result(result: &TaskResult) -> bool { + if result.task_id.is_empty() { + return false; + } + if !result.score.is_finite() || !(0.0..=1.0).contains(&result.score) { + return false; + } + true +} + +fn last_submission_key(miner_hotkey: &str) -> Vec { + let mut key = Vec::from(b"last_submission:" as &[u8]); + key.extend_from_slice(miner_hotkey.as_bytes()); + key +} + +fn get_last_submission_epoch(miner_hotkey: &str) -> Option { + let key = last_submission_key(miner_hotkey); + let data = host_storage_get(&key).ok()?; + if data.len() < 8 { + return None; + } + let mut buf = [0u8; 8]; + buf.copy_from_slice(&data[..8]); + Some(u64::from_le_bytes(buf)) +} + +fn set_last_submission_epoch(miner_hotkey: &str, epoch: u64) { + let key = last_submission_key(miner_hotkey); + let _ = host_storage_set(&key, &epoch.to_le_bytes()); +} -pub struct TermChallenge; +pub struct TermChallengeWasm; -impl Default for TermChallenge { +impl Default for TermChallengeWasm { fn default() -> Self { Self } } -impl TermChallenge { - const fn new() -> Self { +impl TermChallengeWasm { + pub const fn new() -> Self { Self } @@ -33,57 +99,60 @@ impl TermChallenge { agent_output: result.agent_output.clone(), test_output: result.test_output.clone(), }; - let url_bytes = url.as_bytes(); let body = match bincode::serialize(&request) { Ok(b) => b, Err(_) => return None, }; - let response_bytes = match host_http_post(url_bytes, &body) { Ok(b) => b, Err(_) => return None, }; - - let judge_resp: LlmJudgeResponse = match bincode::deserialize(&response_bytes) { + let judge_resp: LlmJudgeResponse = match bincode_options_llm().deserialize(&response_bytes) { Ok(r) => r, Err(_) => return None, }; - + if !judge_resp.score.is_finite() { + return None; + } Some(judge_resp.score.clamp(0.0, 1.0)) } } -impl Challenge for TermChallenge { +impl Challenge for TermChallengeWasm { fn name(&self) -> &'static str { "term-challenge" } fn version(&self) -> &'static str { - "2.0.0" + "4.0.0" } fn evaluate(&self, input: EvaluationInput) -> EvaluationOutput { - let submission: Submission = match bincode::deserialize(&input.agent_data) { - Ok(s) => s, - Err(_) => return EvaluationOutput::failure("failed to deserialize submission"), - }; - - let params: ChallengeParams = match bincode::deserialize(&input.params) { + let submission: Submission = + match bincode_options_submission().deserialize(&input.agent_data) { + Ok(s) => s, + Err(_) => return EvaluationOutput::failure("failed to deserialize submission"), + }; + let params: ChallengeParams = match bincode_options_params().deserialize(&input.params) { Ok(p) => p, Err(_) => return EvaluationOutput::failure("failed to deserialize challenge params"), }; - if submission.task_results.is_empty() { return EvaluationOutput::failure("submission contains no task results"); } - + if submission.task_results.len() > MAX_TASKS { + return EvaluationOutput::failure("submission exceeds maximum task count"); + } if submission.task_results.len() != params.tasks.len() { return EvaluationOutput::failure("task result count does not match task definitions"); } - + for result in &submission.task_results { + if !validate_task_result(result) { + return EvaluationOutput::failure("invalid task result: bad score or empty task_id"); + } + } let mut results: Vec = submission.task_results; - if let Some(ref url) = params.llm_judge_url { for (result, task) in results.iter_mut().zip(params.tasks.iter()) { if !result.passed { @@ -97,49 +166,77 @@ impl Challenge for TermChallenge { } } } - let aggregate = calculate_aggregate(¶ms.tasks, &results); let weight = to_weight(&aggregate); - let score = (weight * 10000.0) as i64; + let score = (weight * 10_000.0) as i64; let message = format_summary(&aggregate); - + set_last_submission_epoch(&submission.miner_hotkey, submission.epoch); EvaluationOutput::success(score, &message) } fn validate(&self, input: EvaluationInput) -> bool { - let submission: Submission = match bincode::deserialize(&input.agent_data) { - Ok(s) => s, - Err(_) => return false, - }; - - let params: ChallengeParams = match bincode::deserialize(&input.params) { + let submission: Submission = + match bincode_options_submission().deserialize(&input.agent_data) { + Ok(s) => s, + Err(_) => return false, + }; + let params: ChallengeParams = match bincode_options_params().deserialize(&input.params) { Ok(p) => p, Err(_) => return false, }; - if submission.agent_hash.is_empty() || submission.miner_hotkey.is_empty() { return false; } - + if submission.signature.is_empty() { + return false; + } + if submission.package_zip.is_empty() { + return false; + } + if submission.basilica_instance.is_empty() + || submission.executor_url.is_empty() + || submission.executor_token.is_empty() + { + return false; + } + let current_epoch = host_consensus_get_epoch(); + if current_epoch >= 0 { + if let Some(last_epoch) = get_last_submission_epoch(&submission.miner_hotkey) { + let current = current_epoch as u64; + if current < last_epoch.saturating_add(EPOCH_RATE_LIMIT) { + return false; + } + } + } if submission.task_results.is_empty() { return false; } - + if submission.task_results.len() > MAX_TASKS { + return false; + } if submission.task_results.len() != params.tasks.len() { return false; } - for result in &submission.task_results { - if result.task_id.is_empty() { - return false; - } - if !(0.0..=1.0).contains(&result.score) { + if !validate_task_result(result) { return false; } } - true } + + fn tasks(&self) -> Vec { + match tasks::get_active_dataset() { + Some(task_defs) => bincode::serialize(&task_defs).unwrap_or_default(), + None => Vec::new(), + } + } + + fn configure(&self, config: &[u8]) { + if let Ok(selection) = bincode::deserialize::(config) { + tasks::store_dataset(&selection); + } + } } -platform_challenge_sdk_wasm::register_challenge!(TermChallenge, TermChallenge::new()); +platform_challenge_sdk_wasm::register_challenge!(TermChallengeWasm, TermChallengeWasm::new()); diff --git a/challenges/term-challenge-wasm/src/routes.rs b/challenges/term-challenge-wasm/src/routes.rs new file mode 100644 index 00000000..c198a8fd --- /dev/null +++ b/challenges/term-challenge-wasm/src/routes.rs @@ -0,0 +1,49 @@ +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use crate::types::RouteDefinition; + +pub fn get_route_definitions() -> Vec { + vec![ + RouteDefinition { + method: String::from("GET"), + path: String::from("/leaderboard"), + description: String::from("Returns current leaderboard with scores, miner hotkeys, and ranks"), + }, + RouteDefinition { + method: String::from("GET"), + path: String::from("/submissions"), + description: String::from("Returns pending submissions awaiting evaluation"), + }, + RouteDefinition { + method: String::from("GET"), + path: String::from("/submissions/:id"), + description: String::from("Returns specific submission status"), + }, + RouteDefinition { + method: String::from("GET"), + path: String::from("/dataset"), + description: String::from("Returns current active dataset of 50 SWE-bench tasks"), + }, + RouteDefinition { + method: String::from("GET"), + path: String::from("/dataset/history"), + description: String::from("Returns historical dataset selections"), + }, + RouteDefinition { + method: String::from("POST"), + path: String::from("/submit"), + description: String::from("Submission endpoint: receives zip package and metadata"), + }, + RouteDefinition { + method: String::from("GET"), + path: String::from("/decay"), + description: String::from("Returns current decay status for top agents"), + }, + RouteDefinition { + method: String::from("GET"), + path: String::from("/stats"), + description: String::from("Challenge statistics: total submissions, active miners"), + }, + ] +} diff --git a/challenges/term-challenge-wasm/src/scoring.rs b/challenges/term-challenge-wasm/src/scoring.rs index f4808973..5daa6947 100644 --- a/challenges/term-challenge-wasm/src/scoring.rs +++ b/challenges/term-challenge-wasm/src/scoring.rs @@ -1,14 +1,12 @@ use alloc::string::String; use core::fmt::Write as _; -use crate::types::{Difficulty, DifficultyStats, TaskDefinition, TaskResult}; +use crate::types::{DecayParams, Difficulty, DifficultyStats, TaskDefinition, TaskResult}; -#[allow(dead_code)] pub struct AggregateScore { pub tasks_passed: u32, pub tasks_failed: u32, pub pass_rate: f64, - pub normalized_score: f64, pub total_execution_time_ms: u64, pub easy_stats: DifficultyStats, pub medium_stats: DifficultyStats, @@ -17,16 +15,7 @@ pub struct AggregateScore { impl AggregateScore { pub fn total_tasks(&self) -> u32 { - self.tasks_passed + self.tasks_failed - } -} - -#[allow(dead_code)] -pub fn score_task(result: &TaskResult) -> f64 { - if result.passed { - 1.0 - } else { - 0.0 + self.tasks_passed.saturating_add(self.tasks_failed) } } @@ -34,18 +23,9 @@ pub fn calculate_aggregate(tasks: &[TaskDefinition], results: &[TaskResult]) -> let mut passed: u32 = 0; let mut failed: u32 = 0; let mut total_execution_time_ms: u64 = 0; - let mut easy = DifficultyStats { - total: 0, - passed: 0, - }; - let mut medium = DifficultyStats { - total: 0, - passed: 0, - }; - let mut hard = DifficultyStats { - total: 0, - passed: 0, - }; + let mut easy = DifficultyStats { total: 0, passed: 0 }; + let mut medium = DifficultyStats { total: 0, passed: 0 }; + let mut hard = DifficultyStats { total: 0, passed: 0 }; for (task, result) in tasks.iter().zip(results.iter()) { if result.passed { @@ -53,9 +33,7 @@ pub fn calculate_aggregate(tasks: &[TaskDefinition], results: &[TaskResult]) -> } else { failed += 1; } - total_execution_time_ms = total_execution_time_ms.saturating_add(result.execution_time_ms); - let stats = match task.difficulty { Difficulty::Easy => &mut easy, Difficulty::Medium => &mut medium, @@ -68,17 +46,12 @@ pub fn calculate_aggregate(tasks: &[TaskDefinition], results: &[TaskResult]) -> } let total = passed + failed; - let pass_rate = if total > 0 { - passed as f64 / total as f64 - } else { - 0.0 - }; + let pass_rate = if total > 0 { passed as f64 / total as f64 } else { 0.0 }; AggregateScore { tasks_passed: passed, tasks_failed: failed, pass_rate, - normalized_score: pass_rate, total_execution_time_ms, easy_stats: easy, medium_stats: medium, @@ -90,6 +63,21 @@ pub fn to_weight(score: &AggregateScore) -> f64 { score.pass_rate.clamp(0.0, 1.0) } +pub fn apply_decay(weight: f64, hours_since_top: f64, params: &DecayParams) -> f64 { + let grace = params.grace_period_hours as f64; + if hours_since_top <= grace { + return weight; + } + let elapsed = hours_since_top - grace; + let half_life = params.half_life_hours as f64; + if half_life <= 0.0 { + return params.min_multiplier; + } + let multiplier = 0.5f64.powf(elapsed / half_life); + let clamped = multiplier.max(params.min_multiplier); + weight * clamped +} + pub fn format_summary(score: &AggregateScore) -> String { let mut msg = String::new(); let _ = write!( @@ -100,25 +88,13 @@ pub fn format_summary(score: &AggregateScore) -> String { score.pass_rate * 100.0, ); if score.easy_stats.total > 0 { - let _ = write!( - msg, - " easy={}/{}", - score.easy_stats.passed, score.easy_stats.total, - ); + let _ = write!(msg, " easy={}/{}", score.easy_stats.passed, score.easy_stats.total); } if score.medium_stats.total > 0 { - let _ = write!( - msg, - " med={}/{}", - score.medium_stats.passed, score.medium_stats.total, - ); + let _ = write!(msg, " med={}/{}", score.medium_stats.passed, score.medium_stats.total); } if score.hard_stats.total > 0 { - let _ = write!( - msg, - " hard={}/{}", - score.hard_stats.passed, score.hard_stats.total, - ); + let _ = write!(msg, " hard={}/{}", score.hard_stats.passed, score.hard_stats.total); } let _ = write!(msg, " time={}ms", score.total_execution_time_ms); msg diff --git a/challenges/term-challenge-wasm/src/tasks.rs b/challenges/term-challenge-wasm/src/tasks.rs new file mode 100644 index 00000000..4d46156c --- /dev/null +++ b/challenges/term-challenge-wasm/src/tasks.rs @@ -0,0 +1,49 @@ +use alloc::vec::Vec; +use platform_challenge_sdk_wasm::host_functions::{host_storage_get, host_storage_set}; +use crate::types::{DatasetSelection, TaskDefinition}; + +const ACTIVE_DATASET_KEY: &[u8] = b"active_dataset"; +const DATASET_HISTORY_KEY: &[u8] = b"dataset_history"; + +pub fn get_active_dataset() -> Option> { + let data = host_storage_get(ACTIVE_DATASET_KEY).ok()?; + if data.is_empty() { + return None; + } + bincode::deserialize(&data).ok() +} + +pub fn store_dataset(selection: &DatasetSelection) -> bool { + let data = match bincode::serialize(selection) { + Ok(d) => d, + Err(_) => return false, + }; + if host_storage_set(ACTIVE_DATASET_KEY, &data).is_err() { + return false; + } + let _ = append_dataset_history(selection); + true +} + +fn append_dataset_history(selection: &DatasetSelection) -> bool { + let mut history: Vec = host_storage_get(DATASET_HISTORY_KEY) + .ok() + .and_then(|d| if d.is_empty() { None } else { bincode::deserialize(&d).ok() }) + .unwrap_or_default(); + history.push(selection.clone()); + if history.len() > 100 { + history.drain(0..history.len() - 100); + } + let data = match bincode::serialize(&history) { + Ok(d) => d, + Err(_) => return false, + }; + host_storage_set(DATASET_HISTORY_KEY, &data).is_ok() +} + +pub fn get_dataset_history() -> Vec { + host_storage_get(DATASET_HISTORY_KEY) + .ok() + .and_then(|d| if d.is_empty() { None } else { bincode::deserialize(&d).ok() }) + .unwrap_or_default() +} diff --git a/challenges/term-challenge-wasm/src/types.rs b/challenges/term-challenge-wasm/src/types.rs index 1dd80304..e1547326 100644 --- a/challenges/term-challenge-wasm/src/types.rs +++ b/challenges/term-challenge-wasm/src/types.rs @@ -9,22 +9,14 @@ pub enum Difficulty { Hard, } -#[allow(dead_code)] -impl Difficulty { - pub fn weight(self) -> f64 { - match self { - Difficulty::Easy => 1.0, - Difficulty::Medium => 2.0, - Difficulty::Hard => 3.0, - } - } -} - #[derive(Clone, Debug, Serialize, Deserialize)] pub struct TaskDefinition { pub id: String, pub name: String, + pub repo: String, + pub base_commit: String, pub difficulty: Difficulty, + pub timeout_secs: u64, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -42,12 +34,20 @@ pub struct TaskResult { pub struct ChallengeParams { pub tasks: Vec, pub llm_judge_url: Option, + pub decay_params: Option, + pub active_dataset: Option>, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Submission { pub agent_hash: String, pub miner_hotkey: String, + pub signature: Vec, + pub epoch: u64, + pub package_zip: Vec, + pub basilica_instance: String, + pub executor_url: String, + pub executor_token: String, pub task_results: Vec, } @@ -57,17 +57,6 @@ pub struct DifficultyStats { pub passed: u32, } -#[allow(dead_code)] -impl DifficultyStats { - pub fn pass_rate(&self) -> f64 { - if self.total > 0 { - self.passed as f64 / self.total as f64 - } else { - 0.0 - } - } -} - #[derive(Clone, Debug, Serialize, Deserialize)] pub struct LlmJudgeRequest { pub task_id: String, @@ -81,3 +70,34 @@ pub struct LlmJudgeResponse { pub score: f64, pub reasoning: String, } + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DecayParams { + pub grace_period_hours: u64, + pub half_life_hours: u64, + pub min_multiplier: f64, +} + +impl Default for DecayParams { + fn default() -> Self { + Self { + grace_period_hours: 72, + half_life_hours: 24, + min_multiplier: 0.0, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DatasetSelection { + pub tasks: Vec, + pub selected_at_epoch: u64, + pub dataset_hash: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RouteDefinition { + pub method: String, + pub path: String, + pub description: String, +} diff --git a/challenges/term-challenge/src/dataset.rs b/challenges/term-challenge/src/dataset.rs new file mode 100644 index 00000000..c0f91625 --- /dev/null +++ b/challenges/term-challenge/src/dataset.rs @@ -0,0 +1,87 @@ +use alloc::string::String; +use alloc::vec::Vec; +use alloc::collections::BTreeMap; +use core::fmt::Write as _; +use platform_challenge_sdk_wasm::host_functions::{ + host_consensus_get_epoch, host_random_seed, host_storage_set, +}; +use crate::types::{DatasetSelection, TaskDefinition}; + +const DATASET_SELECTION_PREFIX: &[u8] = b"dataset_selection:"; +const TOTAL_SWE_BENCH_TASKS: usize = 2294; +const TASKS_TO_SELECT: usize = 100; +const CONSENSUS_DATASET_SIZE: usize = 50; + +pub fn select_random_task_indices() -> Vec { + let mut seed = [0u8; 32]; + if host_random_seed(&mut seed).is_err() { + return Vec::new(); + } + let mut indices = Vec::with_capacity(TASKS_TO_SELECT); + let mut state: u64 = u64::from_le_bytes([ + seed[0], seed[1], seed[2], seed[3], seed[4], seed[5], seed[6], seed[7], + ]); + while indices.len() < TASKS_TO_SELECT { + state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + let idx = (state >> 33) as usize % TOTAL_SWE_BENCH_TASKS; + if !indices.contains(&idx) { + indices.push(idx); + } + } + indices +} + +pub fn store_my_selection(indices: &[usize]) -> bool { + let epoch = host_consensus_get_epoch(); + if epoch < 0 { + return false; + } + let mut key = Vec::with_capacity(DATASET_SELECTION_PREFIX.len() + 8); + key.extend_from_slice(DATASET_SELECTION_PREFIX); + key.extend_from_slice(&(epoch as u64).to_le_bytes()); + let data = match bincode::serialize(indices) { + Ok(d) => d, + Err(_) => return false, + }; + host_storage_set(&key, &data).is_ok() +} + +pub fn build_consensus_dataset( + all_tasks: &[TaskDefinition], + validator_selections: &[Vec], +) -> Vec { + if validator_selections.is_empty() || all_tasks.is_empty() { + return Vec::new(); + } + let threshold = validator_selections.len().div_ceil(2); + let mut counts = BTreeMap::new(); + for selection in validator_selections { + for &idx in selection { + *counts.entry(idx).or_insert(0usize) += 1; + } + } + let mut consensus_indices: Vec = counts + .into_iter() + .filter(|(_, count)| *count >= threshold) + .map(|(idx, _)| idx) + .collect(); + consensus_indices.sort_unstable(); + consensus_indices.truncate(CONSENSUS_DATASET_SIZE); + consensus_indices + .iter() + .filter_map(|&idx| all_tasks.get(idx).cloned()) + .collect() +} + +pub fn create_dataset_selection(tasks: Vec) -> DatasetSelection { + let epoch = host_consensus_get_epoch(); + let mut hash_input = String::new(); + for task in &tasks { + let _ = write!(hash_input, "{}:{};", task.id, task.name); + } + DatasetSelection { + tasks, + selected_at_epoch: if epoch >= 0 { epoch as u64 } else { 0 }, + dataset_hash: hash_input, + } +} diff --git a/challenges/term-challenge/src/evaluation.rs b/challenges/term-challenge/src/evaluation.rs deleted file mode 100644 index 36e6a0c6..00000000 --- a/challenges/term-challenge/src/evaluation.rs +++ /dev/null @@ -1,76 +0,0 @@ -use alloc::string::String; -use alloc::vec::Vec; - -use platform_challenge_sdk_wasm::types::{EvaluationInput, EvaluationOutput}; - -use crate::scoring::score_submission; -use crate::types::{EvalParams, Submission}; - -pub fn evaluate(input: EvaluationInput) -> EvaluationOutput { - let params: EvalParams = match bincode::deserialize(&input.params) { - Ok(p) => p, - Err(_) => return EvaluationOutput::failure("failed to deserialize evaluation params"), - }; - - let submission: Submission = match bincode::deserialize(&input.agent_data) { - Ok(s) => s, - Err(_) => return EvaluationOutput::failure("failed to deserialize agent submission"), - }; - - if submission.tasks.is_empty() { - return EvaluationOutput::failure("submission contains no task results"); - } - - let expected_ids: Vec<&str> = params.tasks.iter().map(|t| t.id.as_str()).collect(); - for result in &submission.tasks { - if !expected_ids.contains(&result.task_id.as_str()) { - return EvaluationOutput::failure("submission contains unknown task id"); - } - } - - let (score, metrics) = score_submission(&submission); - - let message = match bincode::serialize(&metrics) { - Ok(encoded) => { - let _ = host_storage_set_metrics(&encoded); - alloc::format!( - "passed={}/{} rate={:.2}%", - metrics.tasks_passed, - metrics.total_tasks, - metrics.pass_rate * 100.0 - ) - } - Err(_) => String::from("scored"), - }; - - EvaluationOutput { - score, - valid: true, - message, - metrics: None, - details: None, - } -} - -fn host_storage_set_metrics(data: &[u8]) -> Result<(), i32> { - let key = b"term_eval_metrics"; - platform_challenge_sdk_wasm::host_functions::host_storage_set(key, data) -} - -pub fn validate(input: &EvaluationInput) -> bool { - if input.agent_data.is_empty() { - return false; - } - if input.params.is_empty() { - return false; - } - let _params: EvalParams = match bincode::deserialize(&input.params) { - Ok(p) => p, - Err(_) => return false, - }; - let submission: Submission = match bincode::deserialize(&input.agent_data) { - Ok(s) => s, - Err(_) => return false, - }; - !submission.tasks.is_empty() -} diff --git a/challenges/term-challenge/src/lib.rs b/challenges/term-challenge/src/lib.rs index d675fa81..1d34d435 100644 --- a/challenges/term-challenge/src/lib.rs +++ b/challenges/term-challenge/src/lib.rs @@ -2,43 +2,241 @@ extern crate alloc; -mod evaluation; -pub mod scoring; -pub mod tasks; -pub mod types; +mod dataset; +mod routes; +mod scoring; +mod tasks; +mod types; +use alloc::vec::Vec; +use bincode::Options; +use platform_challenge_sdk_wasm::host_functions::{ + host_consensus_get_epoch, host_http_post, host_storage_get, host_storage_set, +}; use platform_challenge_sdk_wasm::{Challenge, EvaluationInput, EvaluationOutput}; -pub struct TermChallenge; +use crate::scoring::{calculate_aggregate, format_summary, to_weight}; +use crate::types::{ChallengeParams, DatasetSelection, LlmJudgeRequest, LlmJudgeResponse, Submission, TaskResult}; -impl TermChallenge { - const fn new() -> Self { - Self +use alloc::string::String; + +const MAX_SUBMISSION_SIZE: u64 = 64 * 1024 * 1024; +const MAX_PARAMS_SIZE: u64 = 4 * 1024 * 1024; +const MAX_LLM_RESPONSE_SIZE: u64 = 1024 * 1024; +const MAX_TASKS: usize = 256; +const EPOCH_RATE_LIMIT: u64 = 3; + +fn bincode_options_submission() -> impl Options { + bincode::DefaultOptions::new() + .with_limit(MAX_SUBMISSION_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() +} + +fn bincode_options_params() -> impl Options { + bincode::DefaultOptions::new() + .with_limit(MAX_PARAMS_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() +} + +fn bincode_options_llm() -> impl Options { + bincode::DefaultOptions::new() + .with_limit(MAX_LLM_RESPONSE_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() +} + +fn validate_task_result(result: &TaskResult) -> bool { + if result.task_id.is_empty() { + return false; + } + if !result.score.is_finite() || !(0.0..=1.0).contains(&result.score) { + return false; + } + true +} + +fn last_submission_key(miner_hotkey: &str) -> Vec { + let mut key = Vec::from(b"last_submission:" as &[u8]); + key.extend_from_slice(miner_hotkey.as_bytes()); + key +} + +fn get_last_submission_epoch(miner_hotkey: &str) -> Option { + let key = last_submission_key(miner_hotkey); + let data = host_storage_get(&key).ok()?; + if data.len() < 8 { + return None; } + let mut buf = [0u8; 8]; + buf.copy_from_slice(&data[..8]); + Some(u64::from_le_bytes(buf)) } -impl Default for TermChallenge { +fn set_last_submission_epoch(miner_hotkey: &str, epoch: u64) { + let key = last_submission_key(miner_hotkey); + let _ = host_storage_set(&key, &epoch.to_le_bytes()); +} + +pub struct TermChallengeWasm; + +impl Default for TermChallengeWasm { fn default() -> Self { - Self::new() + Self + } +} + +impl TermChallengeWasm { + pub const fn new() -> Self { + Self + } + + fn try_llm_judge(url: &str, result: &TaskResult, instruction: &str) -> Option { + let request = LlmJudgeRequest { + task_id: result.task_id.clone(), + instruction: String::from(instruction), + agent_output: result.agent_output.clone(), + test_output: result.test_output.clone(), + }; + let url_bytes = url.as_bytes(); + let body = match bincode::serialize(&request) { + Ok(b) => b, + Err(_) => return None, + }; + let response_bytes = match host_http_post(url_bytes, &body) { + Ok(b) => b, + Err(_) => return None, + }; + let judge_resp: LlmJudgeResponse = match bincode_options_llm().deserialize(&response_bytes) { + Ok(r) => r, + Err(_) => return None, + }; + if !judge_resp.score.is_finite() { + return None; + } + Some(judge_resp.score.clamp(0.0, 1.0)) } } -impl Challenge for TermChallenge { +impl Challenge for TermChallengeWasm { fn name(&self) -> &'static str { "term-challenge" } fn version(&self) -> &'static str { - "0.1.0" + "4.0.0" } fn evaluate(&self, input: EvaluationInput) -> EvaluationOutput { - evaluation::evaluate(input) + let submission: Submission = + match bincode_options_submission().deserialize(&input.agent_data) { + Ok(s) => s, + Err(_) => return EvaluationOutput::failure("failed to deserialize submission"), + }; + let params: ChallengeParams = match bincode_options_params().deserialize(&input.params) { + Ok(p) => p, + Err(_) => return EvaluationOutput::failure("failed to deserialize challenge params"), + }; + if submission.task_results.is_empty() { + return EvaluationOutput::failure("submission contains no task results"); + } + if submission.task_results.len() > MAX_TASKS { + return EvaluationOutput::failure("submission exceeds maximum task count"); + } + if submission.task_results.len() != params.tasks.len() { + return EvaluationOutput::failure("task result count does not match task definitions"); + } + for result in &submission.task_results { + if !validate_task_result(result) { + return EvaluationOutput::failure("invalid task result: bad score or empty task_id"); + } + } + let mut results: Vec = submission.task_results; + if let Some(ref url) = params.llm_judge_url { + for (result, task) in results.iter_mut().zip(params.tasks.iter()) { + if !result.passed { + continue; + } + if let Some(llm_score) = Self::try_llm_judge(url, result, &task.name) { + result.score = llm_score; + if llm_score < 0.5 { + result.passed = false; + } + } + } + } + let aggregate = calculate_aggregate(¶ms.tasks, &results); + let weight = to_weight(&aggregate); + let score = (weight * 10_000.0) as i64; + let message = format_summary(&aggregate); + set_last_submission_epoch(&submission.miner_hotkey, submission.epoch); + EvaluationOutput::success(score, &message) } fn validate(&self, input: EvaluationInput) -> bool { - evaluation::validate(&input) + let submission: Submission = + match bincode_options_submission().deserialize(&input.agent_data) { + Ok(s) => s, + Err(_) => return false, + }; + let params: ChallengeParams = match bincode_options_params().deserialize(&input.params) { + Ok(p) => p, + Err(_) => return false, + }; + if submission.agent_hash.is_empty() || submission.miner_hotkey.is_empty() { + return false; + } + if submission.signature.is_empty() { + return false; + } + if submission.package_zip.is_empty() { + return false; + } + if submission.basilica_instance.is_empty() + || submission.executor_url.is_empty() + || submission.executor_token.is_empty() + { + return false; + } + let current_epoch = host_consensus_get_epoch(); + if current_epoch >= 0 { + if let Some(last_epoch) = get_last_submission_epoch(&submission.miner_hotkey) { + let current = current_epoch as u64; + if current < last_epoch.saturating_add(EPOCH_RATE_LIMIT) { + return false; + } + } + } + if submission.task_results.is_empty() { + return false; + } + if submission.task_results.len() > MAX_TASKS { + return false; + } + if submission.task_results.len() != params.tasks.len() { + return false; + } + for result in &submission.task_results { + if !validate_task_result(result) { + return false; + } + } + true + } + + fn tasks(&self) -> Vec { + match tasks::get_active_dataset() { + Some(task_defs) => bincode::serialize(&task_defs).unwrap_or_default(), + None => Vec::new(), + } + } + + fn configure(&self, config: &[u8]) { + if let Ok(selection) = bincode::deserialize::(config) { + tasks::store_dataset(&selection); + } } } -platform_challenge_sdk_wasm::register_challenge!(TermChallenge, TermChallenge::new()); +platform_challenge_sdk_wasm::register_challenge!(TermChallengeWasm, TermChallengeWasm::new()); diff --git a/challenges/term-challenge/src/routes.rs b/challenges/term-challenge/src/routes.rs new file mode 100644 index 00000000..c198a8fd --- /dev/null +++ b/challenges/term-challenge/src/routes.rs @@ -0,0 +1,49 @@ +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use crate::types::RouteDefinition; + +pub fn get_route_definitions() -> Vec { + vec![ + RouteDefinition { + method: String::from("GET"), + path: String::from("/leaderboard"), + description: String::from("Returns current leaderboard with scores, miner hotkeys, and ranks"), + }, + RouteDefinition { + method: String::from("GET"), + path: String::from("/submissions"), + description: String::from("Returns pending submissions awaiting evaluation"), + }, + RouteDefinition { + method: String::from("GET"), + path: String::from("/submissions/:id"), + description: String::from("Returns specific submission status"), + }, + RouteDefinition { + method: String::from("GET"), + path: String::from("/dataset"), + description: String::from("Returns current active dataset of 50 SWE-bench tasks"), + }, + RouteDefinition { + method: String::from("GET"), + path: String::from("/dataset/history"), + description: String::from("Returns historical dataset selections"), + }, + RouteDefinition { + method: String::from("POST"), + path: String::from("/submit"), + description: String::from("Submission endpoint: receives zip package and metadata"), + }, + RouteDefinition { + method: String::from("GET"), + path: String::from("/decay"), + description: String::from("Returns current decay status for top agents"), + }, + RouteDefinition { + method: String::from("GET"), + path: String::from("/stats"), + description: String::from("Challenge statistics: total submissions, active miners"), + }, + ] +} diff --git a/challenges/term-challenge/src/scoring.rs b/challenges/term-challenge/src/scoring.rs index 61c53ddb..5daa6947 100644 --- a/challenges/term-challenge/src/scoring.rs +++ b/challenges/term-challenge/src/scoring.rs @@ -1,35 +1,101 @@ -use crate::types::{EvalMetrics, Submission}; +use alloc::string::String; +use core::fmt::Write as _; -pub fn score_submission(submission: &Submission) -> (i64, EvalMetrics) { - let total = submission.tasks.len() as u32; +use crate::types::{DecayParams, Difficulty, DifficultyStats, TaskDefinition, TaskResult}; + +pub struct AggregateScore { + pub tasks_passed: u32, + pub tasks_failed: u32, + pub pass_rate: f64, + pub total_execution_time_ms: u64, + pub easy_stats: DifficultyStats, + pub medium_stats: DifficultyStats, + pub hard_stats: DifficultyStats, +} + +impl AggregateScore { + pub fn total_tasks(&self) -> u32 { + self.tasks_passed.saturating_add(self.tasks_failed) + } +} + +pub fn calculate_aggregate(tasks: &[TaskDefinition], results: &[TaskResult]) -> AggregateScore { let mut passed: u32 = 0; let mut failed: u32 = 0; let mut total_execution_time_ms: u64 = 0; + let mut easy = DifficultyStats { total: 0, passed: 0 }; + let mut medium = DifficultyStats { total: 0, passed: 0 }; + let mut hard = DifficultyStats { total: 0, passed: 0 }; - for result in &submission.tasks { + for (task, result) in tasks.iter().zip(results.iter()) { if result.passed { passed += 1; } else { failed += 1; } total_execution_time_ms = total_execution_time_ms.saturating_add(result.execution_time_ms); + let stats = match task.difficulty { + Difficulty::Easy => &mut easy, + Difficulty::Medium => &mut medium, + Difficulty::Hard => &mut hard, + }; + stats.total += 1; + if result.passed { + stats.passed += 1; + } } - let pass_rate = if total > 0 { - passed as f64 / total as f64 - } else { - 0.0 - }; + let total = passed + failed; + let pass_rate = if total > 0 { passed as f64 / total as f64 } else { 0.0 }; - let score = (pass_rate.clamp(0.0, 1.0) * 10_000.0) as i64; - - let metrics = EvalMetrics { + AggregateScore { tasks_passed: passed, tasks_failed: failed, - total_tasks: total, pass_rate, total_execution_time_ms, - }; + easy_stats: easy, + medium_stats: medium, + hard_stats: hard, + } +} + +pub fn to_weight(score: &AggregateScore) -> f64 { + score.pass_rate.clamp(0.0, 1.0) +} + +pub fn apply_decay(weight: f64, hours_since_top: f64, params: &DecayParams) -> f64 { + let grace = params.grace_period_hours as f64; + if hours_since_top <= grace { + return weight; + } + let elapsed = hours_since_top - grace; + let half_life = params.half_life_hours as f64; + if half_life <= 0.0 { + return params.min_multiplier; + } + let multiplier = 0.5f64.powf(elapsed / half_life); + let clamped = multiplier.max(params.min_multiplier); + weight * clamped +} - (score, metrics) +pub fn format_summary(score: &AggregateScore) -> String { + let mut msg = String::new(); + let _ = write!( + msg, + "passed={}/{} rate={:.2}%", + score.tasks_passed, + score.total_tasks(), + score.pass_rate * 100.0, + ); + if score.easy_stats.total > 0 { + let _ = write!(msg, " easy={}/{}", score.easy_stats.passed, score.easy_stats.total); + } + if score.medium_stats.total > 0 { + let _ = write!(msg, " med={}/{}", score.medium_stats.passed, score.medium_stats.total); + } + if score.hard_stats.total > 0 { + let _ = write!(msg, " hard={}/{}", score.hard_stats.passed, score.hard_stats.total); + } + let _ = write!(msg, " time={}ms", score.total_execution_time_ms); + msg } diff --git a/challenges/term-challenge/src/tasks.rs b/challenges/term-challenge/src/tasks.rs index b4524eb4..4d46156c 100644 --- a/challenges/term-challenge/src/tasks.rs +++ b/challenges/term-challenge/src/tasks.rs @@ -1,147 +1,49 @@ -use alloc::string::String; -use alloc::vec; use alloc::vec::Vec; +use platform_challenge_sdk_wasm::host_functions::{host_storage_get, host_storage_set}; +use crate::types::{DatasetSelection, TaskDefinition}; -use crate::types::{Difficulty, TaskDefinition}; +const ACTIVE_DATASET_KEY: &[u8] = b"active_dataset"; +const DATASET_HISTORY_KEY: &[u8] = b"dataset_history"; -fn task( - id: &str, - name: &str, - instruction: &str, - difficulty: Difficulty, - timeout_secs: u64, - docker_image: &str, - test_script: &str, -) -> TaskDefinition { - TaskDefinition { - id: String::from(id), - name: String::from(name), - instruction: String::from(instruction), - difficulty, - timeout_secs, - docker_image: String::from(docker_image), - test_script: String::from(test_script), +pub fn get_active_dataset() -> Option> { + let data = host_storage_get(ACTIVE_DATASET_KEY).ok()?; + if data.is_empty() { + return None; } + bincode::deserialize(&data).ok() } -pub fn builtin_tasks() -> Vec { - vec![ - task( - "create-file", - "Create a File", - "Create a file called /app/hello.txt containing the text 'Hello, World!'", - Difficulty::Easy, - 60, - "ubuntu:22.04", - "#!/bin/bash\ntest -f /app/hello.txt && grep -q 'Hello, World!' /app/hello.txt", - ), - task( - "list-processes", - "List Running Processes", - "Write the output of `ps aux` to /app/processes.txt", - Difficulty::Easy, - 60, - "ubuntu:22.04", - "#!/bin/bash\ntest -f /app/processes.txt && [ -s /app/processes.txt ]", - ), - task( - "find-largest-file", - "Find Largest File", - "Find the largest file in /var/log and write its name to /app/largest.txt", - Difficulty::Medium, - 120, - "ubuntu:22.04", - "#!/bin/bash\ntest -f /app/largest.txt && [ -s /app/largest.txt ]", - ), - task( - "setup-nginx", - "Setup Nginx Config", - "Install nginx and configure it to serve static files from /var/www/html on port 8080. \ - Create an index.html with 'Welcome' as content.", - Difficulty::Medium, - 180, - "ubuntu:22.04", - "#!/bin/bash\ntest -f /var/www/html/index.html && grep -q 'Welcome' /var/www/html/index.html", - ), - task( - "parse-json-log", - "Parse JSON Logs", - "Parse the JSON log file at /app/input.log, extract all entries with level 'ERROR', \ - and write them to /app/errors.json as a JSON array.", - Difficulty::Medium, - 120, - "ubuntu:22.04", - "#!/bin/bash\ntest -f /app/errors.json && python3 -c \"import json; d=json.load(open('/app/errors.json')); assert isinstance(d, list)\"", - ), - task( - "create-user", - "Create System User", - "Create a new system user called 'appuser' with home directory /home/appuser and bash as default shell.", - Difficulty::Easy, - 60, - "ubuntu:22.04", - "#!/bin/bash\nid appuser && [ -d /home/appuser ] && getent passwd appuser | grep -q '/bin/bash'", - ), - task( - "compress-directory", - "Compress Directory", - "Create a tar.gz archive of /app/data directory and save it as /app/data.tar.gz. \ - The archive must preserve directory structure.", - Difficulty::Easy, - 60, - "ubuntu:22.04", - "#!/bin/bash\ntest -f /app/data.tar.gz && tar tzf /app/data.tar.gz | head -1", - ), - task( - "setup-cron", - "Setup Cron Job", - "Create a cron job that runs '/usr/local/bin/cleanup.sh' every day at 3:00 AM as root. \ - Write the crontab entry to /app/crontab.txt as well.", - Difficulty::Medium, - 120, - "ubuntu:22.04", - "#!/bin/bash\ntest -f /app/crontab.txt && grep -q '0 3' /app/crontab.txt && grep -q 'cleanup.sh' /app/crontab.txt", - ), - task( - "docker-compose", - "Write Docker Compose", - "Write a docker-compose.yml at /app/docker-compose.yml that defines two services: \ - 'web' using nginx:latest on port 80 and 'db' using postgres:15 with POSTGRES_PASSWORD=secret.", - Difficulty::Hard, - 180, - "ubuntu:22.04", - "#!/bin/bash\ntest -f /app/docker-compose.yml && grep -q 'nginx' /app/docker-compose.yml && grep -q 'postgres' /app/docker-compose.yml", - ), - task( - "iptables-rule", - "Configure Firewall Rule", - "Write an iptables rule set to /app/rules.sh that blocks all incoming traffic on port 22 \ - except from 10.0.0.0/8, and allows all outgoing traffic.", - Difficulty::Hard, - 180, - "ubuntu:22.04", - "#!/bin/bash\ntest -f /app/rules.sh && grep -q 'iptables' /app/rules.sh && grep -q '10.0.0.0' /app/rules.sh", - ), - ] +pub fn store_dataset(selection: &DatasetSelection) -> bool { + let data = match bincode::serialize(selection) { + Ok(d) => d, + Err(_) => return false, + }; + if host_storage_set(ACTIVE_DATASET_KEY, &data).is_err() { + return false; + } + let _ = append_dataset_history(selection); + true } -pub fn select_tasks(seed: u64, count: usize) -> Vec { - let all = builtin_tasks(); - if count >= all.len() { - return all; - } - let mut indices: Vec = (0..all.len()).collect(); - let mut rng = seed; - for i in (1..indices.len()).rev() { - rng = rng - .wrapping_mul(6364136223846793005) - .wrapping_add(1442695040888963407); - let j = (rng >> 33) as usize % (i + 1); - indices.swap(i, j); +fn append_dataset_history(selection: &DatasetSelection) -> bool { + let mut history: Vec = host_storage_get(DATASET_HISTORY_KEY) + .ok() + .and_then(|d| if d.is_empty() { None } else { bincode::deserialize(&d).ok() }) + .unwrap_or_default(); + history.push(selection.clone()); + if history.len() > 100 { + history.drain(0..history.len() - 100); } - indices - .into_iter() - .take(count) - .map(|i| all[i].clone()) - .collect() + let data = match bincode::serialize(&history) { + Ok(d) => d, + Err(_) => return false, + }; + host_storage_set(DATASET_HISTORY_KEY, &data).is_ok() +} + +pub fn get_dataset_history() -> Vec { + host_storage_get(DATASET_HISTORY_KEY) + .ok() + .and_then(|d| if d.is_empty() { None } else { bincode::deserialize(&d).ok() }) + .unwrap_or_default() } diff --git a/challenges/term-challenge/src/types.rs b/challenges/term-challenge/src/types.rs index 987d2269..e1547326 100644 --- a/challenges/term-challenge/src/types.rs +++ b/challenges/term-challenge/src/types.rs @@ -13,11 +13,10 @@ pub enum Difficulty { pub struct TaskDefinition { pub id: String, pub name: String, - pub instruction: String, + pub repo: String, + pub base_commit: String, pub difficulty: Difficulty, pub timeout_secs: u64, - pub docker_image: String, - pub test_script: String, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -26,25 +25,79 @@ pub struct TaskResult { pub passed: bool, pub score: f64, pub execution_time_ms: u64, - pub output: String, + pub test_output: String, + pub agent_output: String, pub error: Option, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ChallengeParams { + pub tasks: Vec, + pub llm_judge_url: Option, + pub decay_params: Option, + pub active_dataset: Option>, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Submission { - pub tasks: Vec, + pub agent_hash: String, + pub miner_hotkey: String, + pub signature: Vec, + pub epoch: u64, + pub package_zip: Vec, + pub basilica_instance: String, + pub executor_url: String, + pub executor_token: String, + pub task_results: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DifficultyStats { + pub total: u32, + pub passed: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LlmJudgeRequest { + pub task_id: String, + pub instruction: String, + pub agent_output: String, + pub test_output: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LlmJudgeResponse { + pub score: f64, + pub reasoning: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DecayParams { + pub grace_period_hours: u64, + pub half_life_hours: u64, + pub min_multiplier: f64, +} + +impl Default for DecayParams { + fn default() -> Self { + Self { + grace_period_hours: 72, + half_life_hours: 24, + min_multiplier: 0.0, + } + } } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EvalParams { +pub struct DatasetSelection { pub tasks: Vec, + pub selected_at_epoch: u64, + pub dataset_hash: String, } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct EvalMetrics { - pub tasks_passed: u32, - pub tasks_failed: u32, - pub total_tasks: u32, - pub pass_rate: f64, - pub total_execution_time_ms: u64, +pub struct RouteDefinition { + pub method: String, + pub path: String, + pub description: String, } diff --git a/crates/p2p-consensus/src/consensus.rs b/crates/p2p-consensus/src/consensus.rs index 71b08dc8..86e746ce 100644 --- a/crates/p2p-consensus/src/consensus.rs +++ b/crates/p2p-consensus/src/consensus.rs @@ -1178,6 +1178,69 @@ impl ConsensusEngine { pub fn get_decision(&self, sequence: SequenceNumber) -> Option { self.decisions.read().get(&sequence).cloned() } + + /// Select validators for review assignment + /// + /// Uses a deterministic seed derived from the submission ID and epoch + /// to select validators for LLM and AST review. Returns two lists: + /// 3 validators for LLM review and 3 for AST review. + pub fn select_review_validators( + &self, + submission_id: &str, + epoch: u64, + ) -> ([Hotkey; 3], [Hotkey; 3], [u8; 32]) { + let mut hasher = Sha256::new(); + hasher.update(submission_id.as_bytes()); + hasher.update(epoch.to_le_bytes()); + let seed: [u8; 32] = hasher.finalize().into(); + + let validators: Vec = self + .validator_set + .active_validators() + .into_iter() + .map(|r| r.hotkey) + .collect(); + + let select_n = |offset: usize| -> [Hotkey; 3] { + if validators.is_empty() { + return [Hotkey([0u8; 32]), Hotkey([0u8; 32]), Hotkey([0u8; 32])]; + } + + let mut selected = Vec::with_capacity(3); + let mut state: u64 = u64::from_le_bytes([ + seed[offset], + seed[offset + 1], + seed[offset + 2], + seed[offset + 3], + seed[offset + 4], + seed[offset + 5], + seed[offset + 6], + seed[offset + 7], + ]); + + while selected.len() < 3 && selected.len() < validators.len() { + state = state + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + let idx = (state >> 33) as usize % validators.len(); + let candidate = &validators[idx]; + if !selected.contains(candidate) { + selected.push(candidate.clone()); + } + } + + while selected.len() < 3 { + selected.push(Hotkey([0u8; 32])); + } + + [selected[0].clone(), selected[1].clone(), selected[2].clone()] + }; + + let llm_reviewers = select_n(0); + let ast_reviewers = select_n(8); + + (llm_reviewers, ast_reviewers, seed) + } } #[cfg(test)] diff --git a/crates/p2p-consensus/src/messages.rs b/crates/p2p-consensus/src/messages.rs index 15106a11..9c220f9b 100644 --- a/crates/p2p-consensus/src/messages.rs +++ b/crates/p2p-consensus/src/messages.rs @@ -51,6 +51,11 @@ pub enum P2PMessage { ChallengeUpdate(ChallengeUpdateMessage), StorageProposal(StorageProposalMessage), StorageVote(StorageVoteMessage), + + // Review assignment + ReviewAssignment(ReviewAssignmentMessage), + ReviewDecline(ReviewDeclineMessage), + ReviewResult(ReviewResultMessage), } impl P2PMessage { @@ -91,6 +96,9 @@ impl P2PMessage { P2PMessage::ChallengeUpdate(_) => "ChallengeUpdate", P2PMessage::StorageProposal(_) => "StorageProposal", P2PMessage::StorageVote(_) => "StorageVote", + P2PMessage::ReviewAssignment(_) => "ReviewAssignment", + P2PMessage::ReviewDecline(_) => "ReviewDecline", + P2PMessage::ReviewResult(_) => "ReviewResult", } } } @@ -605,6 +613,79 @@ pub struct StorageVoteMessage { // Signed Message Wrapper // ============================================================================ +// ============================================================================ +// Review Assignment Messages +// ============================================================================ + +/// Type of review to be performed +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum ReviewType { + /// LLM-based code review + Llm, + /// AST-based structural review + Ast, +} + +/// Assignment of review validators for a submission +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ReviewAssignmentMessage { + /// Submission being reviewed + pub submission_id: String, + /// Type of review + pub review_type: ReviewType, + /// Validators assigned to perform the review + pub assigned_validators: Vec, + /// Deterministic seed used for selection + pub seed: [u8; 32], + /// Assignment timestamp + pub timestamp: i64, + /// Validator that made the assignment + pub assigner: Hotkey, + /// Assigner's signature + pub signature: Vec, +} + +/// Decline message when a validator cannot perform a review +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ReviewDeclineMessage { + /// Submission being reviewed + pub submission_id: String, + /// Validator declining the review + pub validator: Hotkey, + /// Reason for declining + pub reason: String, + /// Decline timestamp + pub timestamp: i64, + /// Validator's signature + pub signature: Vec, +} + +/// Result of a review from a validator +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ReviewResultMessage { + /// Submission being reviewed + pub submission_id: String, + /// Validator that performed the review + pub validator: Hotkey, + /// Type of review performed + pub review_type: ReviewType, + /// Review score (0.0 to 1.0) + pub score: f64, + /// Detailed review output + pub details: String, + /// Result timestamp + pub timestamp: i64, + /// Validator's signature + pub signature: Vec, +} + +// ============================================================================ +// Signed Message Wrapper +// ============================================================================ +// ============================================================================ +// Signed Message Wrapper +// ============================================================================ + /// Wrapper for signed P2P messages with validation #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SignedP2PMessage { @@ -730,4 +811,4 @@ mod tests { let bytes = msg.signing_bytes().expect("should get signing bytes"); assert!(!bytes.is_empty()); } -} +} \ No newline at end of file diff --git a/crates/p2p-consensus/src/network.rs b/crates/p2p-consensus/src/network.rs index 5d80c3b3..f600f4b6 100644 --- a/crates/p2p-consensus/src/network.rs +++ b/crates/p2p-consensus/src/network.rs @@ -711,6 +711,9 @@ fn expected_signer(message: &P2PMessage) -> Option<&Hotkey> { P2PMessage::ChallengeUpdate(msg) => Some(&msg.updater), P2PMessage::StorageProposal(msg) => Some(&msg.proposer), P2PMessage::StorageVote(msg) => Some(&msg.voter), + P2PMessage::ReviewAssignment(msg) => Some(&msg.assigner), + P2PMessage::ReviewDecline(msg) => Some(&msg.validator), + P2PMessage::ReviewResult(msg) => Some(&msg.validator), } } diff --git a/crates/p2p-consensus/src/state.rs b/crates/p2p-consensus/src/state.rs index c22b5191..c9c9702b 100644 --- a/crates/p2p-consensus/src/state.rs +++ b/crates/p2p-consensus/src/state.rs @@ -183,6 +183,27 @@ pub struct ChainState { /// Storage roots per challenge #[serde(default)] pub challenge_storage_roots: HashMap, + /// Review assignments per submission + #[serde(default)] + pub review_assignments: HashMap>, +} + +/// Record of a review assignment +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ReviewRecord { + pub submission_id: String, + pub review_type: crate::messages::ReviewType, + pub assigned_validators: Vec, + pub results: HashMap, + pub created_at: i64, +} + +/// Single review result entry +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ReviewResultEntry { + pub score: f64, + pub details: String, + pub timestamp: i64, } impl Default for ChainState { @@ -206,6 +227,7 @@ impl Default for ChainState { active_jobs: HashMap::new(), task_progress: HashMap::new(), challenge_storage_roots: HashMap::new(), + review_assignments: HashMap::new(), } } } @@ -683,6 +705,44 @@ impl ChainState { } removed } + + pub fn assign_review(&mut self, record: ReviewRecord) { + self.review_assignments + .entry(record.submission_id.clone()) + .or_default() + .push(record); + self.increment_sequence(); + } + + pub fn add_review_result( + &mut self, + submission_id: &str, + validator: &Hotkey, + score: f64, + details: String, + ) -> bool { + if let Some(reviews) = self.review_assignments.get_mut(submission_id) { + for review in reviews.iter_mut() { + if review.assigned_validators.contains(validator) { + review.results.insert( + validator.clone(), + ReviewResultEntry { + score, + details, + timestamp: chrono::Utc::now().timestamp_millis(), + }, + ); + self.update_hash(); + return true; + } + } + } + false + } + + pub fn get_review_status(&self, submission_id: &str) -> Option<&Vec> { + self.review_assignments.get(submission_id) + } } /// Thread-safe state manager diff --git a/crates/storage/src/dynamic.rs b/crates/storage/src/dynamic.rs index 134e50d6..b611f556 100644 --- a/crates/storage/src/dynamic.rs +++ b/crates/storage/src/dynamic.rs @@ -475,6 +475,66 @@ impl DynamicStorage { .map_err(|e| MiniChainError::Storage(e.to_string()))?; Ok(()) } + + /// Query entries by prefix within a challenge namespace + pub fn query_by_prefix( + &self, + challenge_id: &ChallengeId, + prefix: &str, + ) -> Result)>> { + let namespace = challenge_id.0.to_string(); + let entries = self.scan_namespace(&namespace)?; + + Ok(entries + .into_iter() + .filter(|(k, _)| k.validator.is_none() && k.key.starts_with(prefix)) + .map(|(k, entry)| { + let value_bytes = bincode::serialize(&entry.value).unwrap_or_default(); + (k.key, value_bytes) + }) + .collect()) + } + + /// Get a value as it existed at a specific block height + /// + /// Note: This is a best-effort operation. The current implementation + /// returns the current value if it was last modified at or before the + /// specified block height. Full block-level history requires a separate + /// versioned storage layer. + pub fn get_at_block( + &self, + challenge_id: &ChallengeId, + key: &str, + block: u64, + ) -> Result>> { + let storage_key = StorageKey::challenge(challenge_id, key); + let entry = self.get(&storage_key)?; + + match entry { + Some(e) => { + if e.version <= block { + let value_bytes = bincode::serialize(&e.value) + .map_err(|err| MiniChainError::Serialization(err.to_string()))?; + Ok(Some(value_bytes)) + } else { + Ok(None) + } + } + None => Ok(None), + } + } + + /// List all keys within a challenge namespace + pub fn list_keys(&self, challenge_id: &ChallengeId) -> Result> { + let namespace = challenge_id.0.to_string(); + let entries = self.scan_namespace(&namespace)?; + + Ok(entries + .into_iter() + .filter(|(k, _)| k.validator.is_none()) + .map(|(k, _)| k.key) + .collect()) + } } /// Scoped storage for a specific challenge @@ -552,6 +612,16 @@ impl<'a> ChallengeStorage<'a> { let storage_key = StorageKey::challenge(&self.challenge_id, key); self.storage.map_get(&storage_key, field) } + + /// Query entries by key prefix + pub fn query_by_prefix(&self, prefix: &str) -> Result)>> { + self.storage.query_by_prefix(&self.challenge_id, prefix) + } + + /// List all keys in this challenge + pub fn list_keys(&self) -> Result> { + self.storage.list_keys(&self.challenge_id) + } } /// Scoped storage for a specific validator From 42d53da0758a91810df996ad7c3a82658b77a28f Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 12:52:37 +0000 Subject: [PATCH 03/10] ci: trigger CI run From f5f0f04741e008c6917650f787a31e0026702247 Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 13:09:36 +0000 Subject: [PATCH 04/10] fix(security): address multiple security vulnerabilities in PR #51 - LlmPolicy: redact api_key from Debug output, skip from serialization to prevent credential leakage to logs/storage/blockchain - LlmPolicy: enforce allowed_models check before making LLM API calls - llm.rs: use bounded bincode deserialization for ChatRequest (4MB limit) - state.rs: validate review scores are finite and in [0.0, 1.0] range in add_review_result before storing - state.rs: add size check in from_bytes (256MB limit) - messages.rs: add size check in P2PMessage::from_bytes (16MB limit) - tasks.rs: replace unbounded bincode::deserialize with size-limited options (8MB) for dataset storage reads (both challenge variants) - lib.rs: use bounded bincode options for DatasetSelection in configure() - types.rs: custom Debug impl for Submission that redacts executor_token (both challenge variants) - wasm_executor.rs: use bounded bincode deserialization for EvaluationOutput (64MB limit) --- bins/validator-node/src/wasm_executor.rs | 9 +++- challenges/term-challenge-wasm/src/dataset.rs | 8 +-- challenges/term-challenge-wasm/src/lib.rs | 13 +++-- challenges/term-challenge-wasm/src/routes.rs | 6 ++- challenges/term-challenge-wasm/src/scoring.rs | 39 +++++++++++--- challenges/term-challenge-wasm/src/tasks.rs | 29 ++++++++-- challenges/term-challenge-wasm/src/types.rs | 25 ++++++++- challenges/term-challenge/src/dataset.rs | 8 +-- challenges/term-challenge/src/lib.rs | 13 +++-- challenges/term-challenge/src/routes.rs | 6 ++- challenges/term-challenge/src/scoring.rs | 39 +++++++++++--- challenges/term-challenge/src/tasks.rs | 29 ++++++++-- challenges/term-challenge/src/types.rs | 25 ++++++++- crates/p2p-consensus/src/consensus.rs | 6 ++- crates/p2p-consensus/src/messages.rs | 11 +++- crates/p2p-consensus/src/state.rs | 17 ++++++ crates/wasm-runtime-interface/src/lib.rs | 6 +-- crates/wasm-runtime-interface/src/llm.rs | 54 ++++++++++++++++++- 18 files changed, 292 insertions(+), 51 deletions(-) diff --git a/bins/validator-node/src/wasm_executor.rs b/bins/validator-node/src/wasm_executor.rs index e95ec8bf..d25d308c 100644 --- a/bins/validator-node/src/wasm_executor.rs +++ b/bins/validator-node/src/wasm_executor.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use bincode::Options; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -6,6 +7,8 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Instant; use tracing::{debug, info}; + +const MAX_EVALUATION_OUTPUT_SIZE: u64 = 64 * 1024 * 1024; use wasm_runtime_interface::{ ConsensusPolicy, ExecPolicy, InMemoryStorageBackend, InstanceConfig, LlmPolicy, NetworkHostFunctions, NetworkPolicy, RuntimeConfig, SandboxHostFunctions, SandboxPolicy, @@ -236,7 +239,11 @@ impl WasmChallengeExecutor { anyhow::anyhow!("Failed to read evaluation output from WASM memory: {}", e) })?; - let output: EvaluationOutput = bincode::deserialize(&output_bytes) + let output: EvaluationOutput = bincode::DefaultOptions::new() + .with_limit(MAX_EVALUATION_OUTPUT_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() + .deserialize(&output_bytes) .context("Failed to deserialize EvaluationOutput from WASM module")?; let fuel_consumed = match (initial_fuel, instance.fuel_remaining()) { diff --git a/challenges/term-challenge-wasm/src/dataset.rs b/challenges/term-challenge-wasm/src/dataset.rs index c0f91625..3189dec2 100644 --- a/challenges/term-challenge-wasm/src/dataset.rs +++ b/challenges/term-challenge-wasm/src/dataset.rs @@ -1,11 +1,11 @@ +use crate::types::{DatasetSelection, TaskDefinition}; +use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; -use alloc::collections::BTreeMap; use core::fmt::Write as _; use platform_challenge_sdk_wasm::host_functions::{ host_consensus_get_epoch, host_random_seed, host_storage_set, }; -use crate::types::{DatasetSelection, TaskDefinition}; const DATASET_SELECTION_PREFIX: &[u8] = b"dataset_selection:"; const TOTAL_SWE_BENCH_TASKS: usize = 2294; @@ -22,7 +22,9 @@ pub fn select_random_task_indices() -> Vec { seed[0], seed[1], seed[2], seed[3], seed[4], seed[5], seed[6], seed[7], ]); while indices.len() < TASKS_TO_SELECT { - state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + state = state + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); let idx = (state >> 33) as usize % TOTAL_SWE_BENCH_TASKS; if !indices.contains(&idx) { indices.push(idx); diff --git a/challenges/term-challenge-wasm/src/lib.rs b/challenges/term-challenge-wasm/src/lib.rs index 1d34d435..c1fa871e 100644 --- a/challenges/term-challenge-wasm/src/lib.rs +++ b/challenges/term-challenge-wasm/src/lib.rs @@ -16,7 +16,9 @@ use platform_challenge_sdk_wasm::host_functions::{ use platform_challenge_sdk_wasm::{Challenge, EvaluationInput, EvaluationOutput}; use crate::scoring::{calculate_aggregate, format_summary, to_weight}; -use crate::types::{ChallengeParams, DatasetSelection, LlmJudgeRequest, LlmJudgeResponse, Submission, TaskResult}; +use crate::types::{ + ChallengeParams, DatasetSelection, LlmJudgeRequest, LlmJudgeResponse, Submission, TaskResult, +}; use alloc::string::String; @@ -108,7 +110,8 @@ impl TermChallengeWasm { Ok(b) => b, Err(_) => return None, }; - let judge_resp: LlmJudgeResponse = match bincode_options_llm().deserialize(&response_bytes) { + let judge_resp: LlmJudgeResponse = match bincode_options_llm().deserialize(&response_bytes) + { Ok(r) => r, Err(_) => return None, }; @@ -149,7 +152,9 @@ impl Challenge for TermChallengeWasm { } for result in &submission.task_results { if !validate_task_result(result) { - return EvaluationOutput::failure("invalid task result: bad score or empty task_id"); + return EvaluationOutput::failure( + "invalid task result: bad score or empty task_id", + ); } } let mut results: Vec = submission.task_results; @@ -233,7 +238,7 @@ impl Challenge for TermChallengeWasm { } fn configure(&self, config: &[u8]) { - if let Ok(selection) = bincode::deserialize::(config) { + if let Ok(selection) = bincode_options_params().deserialize::(config) { tasks::store_dataset(&selection); } } diff --git a/challenges/term-challenge-wasm/src/routes.rs b/challenges/term-challenge-wasm/src/routes.rs index c198a8fd..153fef3d 100644 --- a/challenges/term-challenge-wasm/src/routes.rs +++ b/challenges/term-challenge-wasm/src/routes.rs @@ -1,14 +1,16 @@ +use crate::types::RouteDefinition; use alloc::string::String; use alloc::vec; use alloc::vec::Vec; -use crate::types::RouteDefinition; pub fn get_route_definitions() -> Vec { vec![ RouteDefinition { method: String::from("GET"), path: String::from("/leaderboard"), - description: String::from("Returns current leaderboard with scores, miner hotkeys, and ranks"), + description: String::from( + "Returns current leaderboard with scores, miner hotkeys, and ranks", + ), }, RouteDefinition { method: String::from("GET"), diff --git a/challenges/term-challenge-wasm/src/scoring.rs b/challenges/term-challenge-wasm/src/scoring.rs index 5daa6947..5afa0bb7 100644 --- a/challenges/term-challenge-wasm/src/scoring.rs +++ b/challenges/term-challenge-wasm/src/scoring.rs @@ -23,9 +23,18 @@ pub fn calculate_aggregate(tasks: &[TaskDefinition], results: &[TaskResult]) -> let mut passed: u32 = 0; let mut failed: u32 = 0; let mut total_execution_time_ms: u64 = 0; - let mut easy = DifficultyStats { total: 0, passed: 0 }; - let mut medium = DifficultyStats { total: 0, passed: 0 }; - let mut hard = DifficultyStats { total: 0, passed: 0 }; + let mut easy = DifficultyStats { + total: 0, + passed: 0, + }; + let mut medium = DifficultyStats { + total: 0, + passed: 0, + }; + let mut hard = DifficultyStats { + total: 0, + passed: 0, + }; for (task, result) in tasks.iter().zip(results.iter()) { if result.passed { @@ -46,7 +55,11 @@ pub fn calculate_aggregate(tasks: &[TaskDefinition], results: &[TaskResult]) -> } let total = passed + failed; - let pass_rate = if total > 0 { passed as f64 / total as f64 } else { 0.0 }; + let pass_rate = if total > 0 { + passed as f64 / total as f64 + } else { + 0.0 + }; AggregateScore { tasks_passed: passed, @@ -88,13 +101,25 @@ pub fn format_summary(score: &AggregateScore) -> String { score.pass_rate * 100.0, ); if score.easy_stats.total > 0 { - let _ = write!(msg, " easy={}/{}", score.easy_stats.passed, score.easy_stats.total); + let _ = write!( + msg, + " easy={}/{}", + score.easy_stats.passed, score.easy_stats.total + ); } if score.medium_stats.total > 0 { - let _ = write!(msg, " med={}/{}", score.medium_stats.passed, score.medium_stats.total); + let _ = write!( + msg, + " med={}/{}", + score.medium_stats.passed, score.medium_stats.total + ); } if score.hard_stats.total > 0 { - let _ = write!(msg, " hard={}/{}", score.hard_stats.passed, score.hard_stats.total); + let _ = write!( + msg, + " hard={}/{}", + score.hard_stats.passed, score.hard_stats.total + ); } let _ = write!(msg, " time={}ms", score.total_execution_time_ms); msg diff --git a/challenges/term-challenge-wasm/src/tasks.rs b/challenges/term-challenge-wasm/src/tasks.rs index 4d46156c..ef584f3e 100644 --- a/challenges/term-challenge-wasm/src/tasks.rs +++ b/challenges/term-challenge-wasm/src/tasks.rs @@ -1,16 +1,25 @@ +use crate::types::{DatasetSelection, TaskDefinition}; use alloc::vec::Vec; +use bincode::Options; use platform_challenge_sdk_wasm::host_functions::{host_storage_get, host_storage_set}; -use crate::types::{DatasetSelection, TaskDefinition}; const ACTIVE_DATASET_KEY: &[u8] = b"active_dataset"; const DATASET_HISTORY_KEY: &[u8] = b"dataset_history"; +const MAX_DATASET_SIZE: u64 = 8 * 1024 * 1024; + +fn bincode_options_dataset() -> impl Options { + bincode::DefaultOptions::new() + .with_limit(MAX_DATASET_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() +} pub fn get_active_dataset() -> Option> { let data = host_storage_get(ACTIVE_DATASET_KEY).ok()?; if data.is_empty() { return None; } - bincode::deserialize(&data).ok() + bincode_options_dataset().deserialize(&data).ok() } pub fn store_dataset(selection: &DatasetSelection) -> bool { @@ -28,7 +37,13 @@ pub fn store_dataset(selection: &DatasetSelection) -> bool { fn append_dataset_history(selection: &DatasetSelection) -> bool { let mut history: Vec = host_storage_get(DATASET_HISTORY_KEY) .ok() - .and_then(|d| if d.is_empty() { None } else { bincode::deserialize(&d).ok() }) + .and_then(|d| { + if d.is_empty() { + None + } else { + bincode_options_dataset().deserialize(&d).ok() + } + }) .unwrap_or_default(); history.push(selection.clone()); if history.len() > 100 { @@ -44,6 +59,12 @@ fn append_dataset_history(selection: &DatasetSelection) -> bool { pub fn get_dataset_history() -> Vec { host_storage_get(DATASET_HISTORY_KEY) .ok() - .and_then(|d| if d.is_empty() { None } else { bincode::deserialize(&d).ok() }) + .and_then(|d| { + if d.is_empty() { + None + } else { + bincode_options_dataset().deserialize(&d).ok() + } + }) .unwrap_or_default() } diff --git a/challenges/term-challenge-wasm/src/types.rs b/challenges/term-challenge-wasm/src/types.rs index e1547326..e389088f 100644 --- a/challenges/term-challenge-wasm/src/types.rs +++ b/challenges/term-challenge-wasm/src/types.rs @@ -1,5 +1,6 @@ use alloc::string::String; use alloc::vec::Vec; +use core::fmt; use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -38,7 +39,7 @@ pub struct ChallengeParams { pub active_dataset: Option>, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct Submission { pub agent_hash: String, pub miner_hotkey: String, @@ -51,6 +52,28 @@ pub struct Submission { pub task_results: Vec, } +impl fmt::Debug for Submission { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Submission") + .field("agent_hash", &self.agent_hash) + .field("miner_hotkey", &self.miner_hotkey) + .field( + "signature", + &format_args!("[{} bytes]", self.signature.len()), + ) + .field("epoch", &self.epoch) + .field( + "package_zip", + &format_args!("[{} bytes]", self.package_zip.len()), + ) + .field("basilica_instance", &self.basilica_instance) + .field("executor_url", &self.executor_url) + .field("executor_token", &"[REDACTED]") + .field("task_results", &self.task_results) + .finish() + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DifficultyStats { pub total: u32, diff --git a/challenges/term-challenge/src/dataset.rs b/challenges/term-challenge/src/dataset.rs index c0f91625..3189dec2 100644 --- a/challenges/term-challenge/src/dataset.rs +++ b/challenges/term-challenge/src/dataset.rs @@ -1,11 +1,11 @@ +use crate::types::{DatasetSelection, TaskDefinition}; +use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; -use alloc::collections::BTreeMap; use core::fmt::Write as _; use platform_challenge_sdk_wasm::host_functions::{ host_consensus_get_epoch, host_random_seed, host_storage_set, }; -use crate::types::{DatasetSelection, TaskDefinition}; const DATASET_SELECTION_PREFIX: &[u8] = b"dataset_selection:"; const TOTAL_SWE_BENCH_TASKS: usize = 2294; @@ -22,7 +22,9 @@ pub fn select_random_task_indices() -> Vec { seed[0], seed[1], seed[2], seed[3], seed[4], seed[5], seed[6], seed[7], ]); while indices.len() < TASKS_TO_SELECT { - state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + state = state + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); let idx = (state >> 33) as usize % TOTAL_SWE_BENCH_TASKS; if !indices.contains(&idx) { indices.push(idx); diff --git a/challenges/term-challenge/src/lib.rs b/challenges/term-challenge/src/lib.rs index 1d34d435..c1fa871e 100644 --- a/challenges/term-challenge/src/lib.rs +++ b/challenges/term-challenge/src/lib.rs @@ -16,7 +16,9 @@ use platform_challenge_sdk_wasm::host_functions::{ use platform_challenge_sdk_wasm::{Challenge, EvaluationInput, EvaluationOutput}; use crate::scoring::{calculate_aggregate, format_summary, to_weight}; -use crate::types::{ChallengeParams, DatasetSelection, LlmJudgeRequest, LlmJudgeResponse, Submission, TaskResult}; +use crate::types::{ + ChallengeParams, DatasetSelection, LlmJudgeRequest, LlmJudgeResponse, Submission, TaskResult, +}; use alloc::string::String; @@ -108,7 +110,8 @@ impl TermChallengeWasm { Ok(b) => b, Err(_) => return None, }; - let judge_resp: LlmJudgeResponse = match bincode_options_llm().deserialize(&response_bytes) { + let judge_resp: LlmJudgeResponse = match bincode_options_llm().deserialize(&response_bytes) + { Ok(r) => r, Err(_) => return None, }; @@ -149,7 +152,9 @@ impl Challenge for TermChallengeWasm { } for result in &submission.task_results { if !validate_task_result(result) { - return EvaluationOutput::failure("invalid task result: bad score or empty task_id"); + return EvaluationOutput::failure( + "invalid task result: bad score or empty task_id", + ); } } let mut results: Vec = submission.task_results; @@ -233,7 +238,7 @@ impl Challenge for TermChallengeWasm { } fn configure(&self, config: &[u8]) { - if let Ok(selection) = bincode::deserialize::(config) { + if let Ok(selection) = bincode_options_params().deserialize::(config) { tasks::store_dataset(&selection); } } diff --git a/challenges/term-challenge/src/routes.rs b/challenges/term-challenge/src/routes.rs index c198a8fd..153fef3d 100644 --- a/challenges/term-challenge/src/routes.rs +++ b/challenges/term-challenge/src/routes.rs @@ -1,14 +1,16 @@ +use crate::types::RouteDefinition; use alloc::string::String; use alloc::vec; use alloc::vec::Vec; -use crate::types::RouteDefinition; pub fn get_route_definitions() -> Vec { vec![ RouteDefinition { method: String::from("GET"), path: String::from("/leaderboard"), - description: String::from("Returns current leaderboard with scores, miner hotkeys, and ranks"), + description: String::from( + "Returns current leaderboard with scores, miner hotkeys, and ranks", + ), }, RouteDefinition { method: String::from("GET"), diff --git a/challenges/term-challenge/src/scoring.rs b/challenges/term-challenge/src/scoring.rs index 5daa6947..5afa0bb7 100644 --- a/challenges/term-challenge/src/scoring.rs +++ b/challenges/term-challenge/src/scoring.rs @@ -23,9 +23,18 @@ pub fn calculate_aggregate(tasks: &[TaskDefinition], results: &[TaskResult]) -> let mut passed: u32 = 0; let mut failed: u32 = 0; let mut total_execution_time_ms: u64 = 0; - let mut easy = DifficultyStats { total: 0, passed: 0 }; - let mut medium = DifficultyStats { total: 0, passed: 0 }; - let mut hard = DifficultyStats { total: 0, passed: 0 }; + let mut easy = DifficultyStats { + total: 0, + passed: 0, + }; + let mut medium = DifficultyStats { + total: 0, + passed: 0, + }; + let mut hard = DifficultyStats { + total: 0, + passed: 0, + }; for (task, result) in tasks.iter().zip(results.iter()) { if result.passed { @@ -46,7 +55,11 @@ pub fn calculate_aggregate(tasks: &[TaskDefinition], results: &[TaskResult]) -> } let total = passed + failed; - let pass_rate = if total > 0 { passed as f64 / total as f64 } else { 0.0 }; + let pass_rate = if total > 0 { + passed as f64 / total as f64 + } else { + 0.0 + }; AggregateScore { tasks_passed: passed, @@ -88,13 +101,25 @@ pub fn format_summary(score: &AggregateScore) -> String { score.pass_rate * 100.0, ); if score.easy_stats.total > 0 { - let _ = write!(msg, " easy={}/{}", score.easy_stats.passed, score.easy_stats.total); + let _ = write!( + msg, + " easy={}/{}", + score.easy_stats.passed, score.easy_stats.total + ); } if score.medium_stats.total > 0 { - let _ = write!(msg, " med={}/{}", score.medium_stats.passed, score.medium_stats.total); + let _ = write!( + msg, + " med={}/{}", + score.medium_stats.passed, score.medium_stats.total + ); } if score.hard_stats.total > 0 { - let _ = write!(msg, " hard={}/{}", score.hard_stats.passed, score.hard_stats.total); + let _ = write!( + msg, + " hard={}/{}", + score.hard_stats.passed, score.hard_stats.total + ); } let _ = write!(msg, " time={}ms", score.total_execution_time_ms); msg diff --git a/challenges/term-challenge/src/tasks.rs b/challenges/term-challenge/src/tasks.rs index 4d46156c..ef584f3e 100644 --- a/challenges/term-challenge/src/tasks.rs +++ b/challenges/term-challenge/src/tasks.rs @@ -1,16 +1,25 @@ +use crate::types::{DatasetSelection, TaskDefinition}; use alloc::vec::Vec; +use bincode::Options; use platform_challenge_sdk_wasm::host_functions::{host_storage_get, host_storage_set}; -use crate::types::{DatasetSelection, TaskDefinition}; const ACTIVE_DATASET_KEY: &[u8] = b"active_dataset"; const DATASET_HISTORY_KEY: &[u8] = b"dataset_history"; +const MAX_DATASET_SIZE: u64 = 8 * 1024 * 1024; + +fn bincode_options_dataset() -> impl Options { + bincode::DefaultOptions::new() + .with_limit(MAX_DATASET_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() +} pub fn get_active_dataset() -> Option> { let data = host_storage_get(ACTIVE_DATASET_KEY).ok()?; if data.is_empty() { return None; } - bincode::deserialize(&data).ok() + bincode_options_dataset().deserialize(&data).ok() } pub fn store_dataset(selection: &DatasetSelection) -> bool { @@ -28,7 +37,13 @@ pub fn store_dataset(selection: &DatasetSelection) -> bool { fn append_dataset_history(selection: &DatasetSelection) -> bool { let mut history: Vec = host_storage_get(DATASET_HISTORY_KEY) .ok() - .and_then(|d| if d.is_empty() { None } else { bincode::deserialize(&d).ok() }) + .and_then(|d| { + if d.is_empty() { + None + } else { + bincode_options_dataset().deserialize(&d).ok() + } + }) .unwrap_or_default(); history.push(selection.clone()); if history.len() > 100 { @@ -44,6 +59,12 @@ fn append_dataset_history(selection: &DatasetSelection) -> bool { pub fn get_dataset_history() -> Vec { host_storage_get(DATASET_HISTORY_KEY) .ok() - .and_then(|d| if d.is_empty() { None } else { bincode::deserialize(&d).ok() }) + .and_then(|d| { + if d.is_empty() { + None + } else { + bincode_options_dataset().deserialize(&d).ok() + } + }) .unwrap_or_default() } diff --git a/challenges/term-challenge/src/types.rs b/challenges/term-challenge/src/types.rs index e1547326..e389088f 100644 --- a/challenges/term-challenge/src/types.rs +++ b/challenges/term-challenge/src/types.rs @@ -1,5 +1,6 @@ use alloc::string::String; use alloc::vec::Vec; +use core::fmt; use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -38,7 +39,7 @@ pub struct ChallengeParams { pub active_dataset: Option>, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct Submission { pub agent_hash: String, pub miner_hotkey: String, @@ -51,6 +52,28 @@ pub struct Submission { pub task_results: Vec, } +impl fmt::Debug for Submission { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Submission") + .field("agent_hash", &self.agent_hash) + .field("miner_hotkey", &self.miner_hotkey) + .field( + "signature", + &format_args!("[{} bytes]", self.signature.len()), + ) + .field("epoch", &self.epoch) + .field( + "package_zip", + &format_args!("[{} bytes]", self.package_zip.len()), + ) + .field("basilica_instance", &self.basilica_instance) + .field("executor_url", &self.executor_url) + .field("executor_token", &"[REDACTED]") + .field("task_results", &self.task_results) + .finish() + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DifficultyStats { pub total: u32, diff --git a/crates/p2p-consensus/src/consensus.rs b/crates/p2p-consensus/src/consensus.rs index 86e746ce..2b902c9c 100644 --- a/crates/p2p-consensus/src/consensus.rs +++ b/crates/p2p-consensus/src/consensus.rs @@ -1233,7 +1233,11 @@ impl ConsensusEngine { selected.push(Hotkey([0u8; 32])); } - [selected[0].clone(), selected[1].clone(), selected[2].clone()] + [ + selected[0].clone(), + selected[1].clone(), + selected[2].clone(), + ] }; let llm_reviewers = select_n(0); diff --git a/crates/p2p-consensus/src/messages.rs b/crates/p2p-consensus/src/messages.rs index 9c220f9b..0e1cd212 100644 --- a/crates/p2p-consensus/src/messages.rs +++ b/crates/p2p-consensus/src/messages.rs @@ -6,6 +6,8 @@ use platform_core::{ChallengeId, Hotkey}; use serde::{Deserialize, Serialize}; +const MAX_P2P_MESSAGE_SIZE: u64 = 16 * 1024 * 1024; + /// Unique identifier for a consensus round pub type RoundId = u64; @@ -66,6 +68,13 @@ impl P2PMessage { /// Deserialize message from bytes pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() as u64 > MAX_P2P_MESSAGE_SIZE { + return Err(Box::new(bincode::ErrorKind::Custom(format!( + "message exceeds maximum size: {} > {}", + bytes.len(), + MAX_P2P_MESSAGE_SIZE + )))); + } bincode::deserialize(bytes) } @@ -811,4 +820,4 @@ mod tests { let bytes = msg.signing_bytes().expect("should get signing bytes"); assert!(!bytes.is_empty()); } -} \ No newline at end of file +} diff --git a/crates/p2p-consensus/src/state.rs b/crates/p2p-consensus/src/state.rs index c9c9702b..08ec3778 100644 --- a/crates/p2p-consensus/src/state.rs +++ b/crates/p2p-consensus/src/state.rs @@ -12,6 +12,8 @@ use std::collections::HashMap; use thiserror::Error; use tracing::{debug, info, warn}; +const MAX_STATE_DESERIALIZATION_SIZE: u64 = 256 * 1024 * 1024; + /// Errors related to state operations #[derive(Error, Debug)] pub enum StateError { @@ -320,6 +322,13 @@ impl ChainState { /// Deserialize state from bytes pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() as u64 > MAX_STATE_DESERIALIZATION_SIZE { + return Err(StateError::Serialization(format!( + "state data exceeds maximum size: {} > {}", + bytes.len(), + MAX_STATE_DESERIALIZATION_SIZE + ))); + } bincode::deserialize(bytes).map_err(|e| StateError::Serialization(e.to_string())) } @@ -721,6 +730,14 @@ impl ChainState { score: f64, details: String, ) -> bool { + if !score.is_finite() || !(0.0..=1.0).contains(&score) { + warn!( + score, + submission_id, + "Rejecting review result with invalid score (must be finite and in 0.0..=1.0)" + ); + return false; + } if let Some(reviews) = self.review_assignments.get_mut(submission_id) { for review in reviews.iter_mut() { if review.assigned_validators.contains(validator) { diff --git a/crates/wasm-runtime-interface/src/lib.rs b/crates/wasm-runtime-interface/src/lib.rs index a72b7362..1f8f9e41 100644 --- a/crates/wasm-runtime-interface/src/lib.rs +++ b/crates/wasm-runtime-interface/src/lib.rs @@ -14,11 +14,11 @@ pub mod consensus; pub mod container; pub mod data; pub mod exec; +pub mod llm; pub mod network; pub mod runtime; pub mod sandbox; pub mod storage; -pub mod llm; pub mod terminal; pub mod time; pub use bridge::{ @@ -63,6 +63,7 @@ pub use data::{ DataBackend, DataError, DataHostFunctions, DataHostStatus, DataPolicy, DataState, FilesystemDataBackend, NoopDataBackend, HOST_DATA_GET, HOST_DATA_LIST, HOST_DATA_NAMESPACE, }; +pub use llm::{LlmHostFunctions, LlmHostStatus, LlmPolicy, LlmState, HOST_LLM_NAMESPACE}; pub use runtime::{ ChallengeInstance, HostFunctionRegistrar, InstanceConfig, RuntimeConfig, RuntimeState, WasmModule, WasmRuntime, WasmRuntimeError, @@ -75,9 +76,6 @@ pub use terminal::{ TerminalHostFunctions, TerminalHostStatus, TerminalPolicy, TerminalState, HOST_TERMINAL_NAMESPACE, }; -pub use llm::{ - LlmHostFunctions, LlmHostStatus, LlmPolicy, LlmState, HOST_LLM_NAMESPACE, -}; pub use time::{TimeError, TimeHostFunction, TimeHostFunctions, TimeMode, TimePolicy, TimeState}; /// Host functions that may be exposed to WASM challenges. diff --git a/crates/wasm-runtime-interface/src/llm.rs b/crates/wasm-runtime-interface/src/llm.rs index 79440902..45561ab7 100644 --- a/crates/wasm-runtime-interface/src/llm.rs +++ b/crates/wasm-runtime-interface/src/llm.rs @@ -9,10 +9,14 @@ //! - `llm_is_available() -> i32` — Check if LLM inference is available (has API key) use crate::runtime::{HostFunctionRegistrar, RuntimeState, WasmRuntimeError}; +use bincode::Options; use serde::{Deserialize, Serialize}; +use std::fmt; use tracing::warn; use wasmtime::{Caller, Linker, Memory}; +const MAX_CHAT_REQUEST_SIZE: u64 = 4 * 1024 * 1024; + pub const HOST_LLM_NAMESPACE: &str = "platform_llm"; pub const HOST_LLM_CHAT_COMPLETION: &str = "llm_chat_completion"; pub const HOST_LLM_IS_AVAILABLE: &str = "llm_is_available"; @@ -35,15 +39,28 @@ impl LlmHostStatus { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct LlmPolicy { pub enabled: bool, + #[serde(skip)] pub api_key: Option, pub endpoint: String, pub max_requests: u32, pub allowed_models: Vec, } +impl fmt::Debug for LlmPolicy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LlmPolicy") + .field("enabled", &self.enabled) + .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]")) + .field("endpoint", &self.endpoint) + .field("max_requests", &self.max_requests) + .field("allowed_models", &self.allowed_models) + .finish() + } +} + impl Default for LlmPolicy { fn default() -> Self { Self { @@ -199,11 +216,28 @@ fn handle_chat_completion( content: String, } - let chat_req: ChatRequest = match bincode::deserialize(&request_bytes) { + let chat_req: ChatRequest = match bincode::DefaultOptions::new() + .with_limit(MAX_CHAT_REQUEST_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() + .deserialize(&request_bytes) + { Ok(r) => r, Err(_) => return LlmHostStatus::InvalidRequest.to_i32(), }; + { + let state = &caller.data().llm_state; + let allowed = &state.policy.allowed_models; + if !allowed.is_empty() && !allowed.contains(&chat_req.model) { + warn!( + model = %chat_req.model, + "llm_chat_completion: model not in allowed list" + ); + return LlmHostStatus::InvalidRequest.to_i32(); + } + } + #[derive(Serialize)] struct OpenAiRequest { model: String, @@ -426,4 +460,20 @@ mod tests { assert_eq!(state.requests_made, 0); assert!(!state.policy.is_available()); } + + #[test] + fn test_llm_policy_debug_redacts_api_key() { + let policy = LlmPolicy::with_api_key("super-secret-key-12345".to_string()); + let debug_output = format!("{:?}", policy); + assert!(!debug_output.contains("super-secret-key-12345")); + assert!(debug_output.contains("[REDACTED]")); + } + + #[test] + fn test_llm_policy_serialize_skips_api_key() { + let policy = LlmPolicy::with_api_key("secret-key".to_string()); + let serialized = bincode::serialize(&policy).unwrap(); + let deserialized: LlmPolicy = bincode::deserialize(&serialized).unwrap(); + assert!(deserialized.api_key.is_none()); + } } From ad6a412de16f5c9f8860e32a5c64c0e7725a3b2d Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 13:16:48 +0000 Subject: [PATCH 05/10] fix(challenges): remove dead code and orphan modules - Delete routes.rs (both crates): get_route_definitions() never called - Delete dataset.rs (both crates): all 4 functions never called - Remove scoring::apply_decay() from both crates: never called - Remove tasks::get_dataset_history() from both crates: never called - Remove RouteDefinition struct from both types.rs: only used by dead routes.rs - Remove mod routes and mod dataset declarations from both lib.rs - Remove unused DecayParams import from scoring.rs Kept as intentional public API: - challenge-sdk-wasm LLM host functions and types (SDK surface) - storage::DynamicStorage query/list/get_at_block methods - p2p-consensus::ChainState::get_review_status - AggregateScore::total_tasks() (called by format_summary) --- challenges/term-challenge-wasm/src/dataset.rs | 89 ------------------- challenges/term-challenge-wasm/src/lib.rs | 2 - challenges/term-challenge-wasm/src/routes.rs | 51 ----------- challenges/term-challenge-wasm/src/scoring.rs | 17 +--- challenges/term-challenge-wasm/src/tasks.rs | 13 --- challenges/term-challenge-wasm/src/types.rs | 7 -- challenges/term-challenge/src/dataset.rs | 89 ------------------- challenges/term-challenge/src/lib.rs | 2 - challenges/term-challenge/src/routes.rs | 51 ----------- challenges/term-challenge/src/scoring.rs | 17 +--- challenges/term-challenge/src/tasks.rs | 13 --- challenges/term-challenge/src/types.rs | 7 -- 12 files changed, 2 insertions(+), 356 deletions(-) delete mode 100644 challenges/term-challenge-wasm/src/dataset.rs delete mode 100644 challenges/term-challenge-wasm/src/routes.rs delete mode 100644 challenges/term-challenge/src/dataset.rs delete mode 100644 challenges/term-challenge/src/routes.rs diff --git a/challenges/term-challenge-wasm/src/dataset.rs b/challenges/term-challenge-wasm/src/dataset.rs deleted file mode 100644 index 3189dec2..00000000 --- a/challenges/term-challenge-wasm/src/dataset.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::types::{DatasetSelection, TaskDefinition}; -use alloc::collections::BTreeMap; -use alloc::string::String; -use alloc::vec::Vec; -use core::fmt::Write as _; -use platform_challenge_sdk_wasm::host_functions::{ - host_consensus_get_epoch, host_random_seed, host_storage_set, -}; - -const DATASET_SELECTION_PREFIX: &[u8] = b"dataset_selection:"; -const TOTAL_SWE_BENCH_TASKS: usize = 2294; -const TASKS_TO_SELECT: usize = 100; -const CONSENSUS_DATASET_SIZE: usize = 50; - -pub fn select_random_task_indices() -> Vec { - let mut seed = [0u8; 32]; - if host_random_seed(&mut seed).is_err() { - return Vec::new(); - } - let mut indices = Vec::with_capacity(TASKS_TO_SELECT); - let mut state: u64 = u64::from_le_bytes([ - seed[0], seed[1], seed[2], seed[3], seed[4], seed[5], seed[6], seed[7], - ]); - while indices.len() < TASKS_TO_SELECT { - state = state - .wrapping_mul(6364136223846793005) - .wrapping_add(1442695040888963407); - let idx = (state >> 33) as usize % TOTAL_SWE_BENCH_TASKS; - if !indices.contains(&idx) { - indices.push(idx); - } - } - indices -} - -pub fn store_my_selection(indices: &[usize]) -> bool { - let epoch = host_consensus_get_epoch(); - if epoch < 0 { - return false; - } - let mut key = Vec::with_capacity(DATASET_SELECTION_PREFIX.len() + 8); - key.extend_from_slice(DATASET_SELECTION_PREFIX); - key.extend_from_slice(&(epoch as u64).to_le_bytes()); - let data = match bincode::serialize(indices) { - Ok(d) => d, - Err(_) => return false, - }; - host_storage_set(&key, &data).is_ok() -} - -pub fn build_consensus_dataset( - all_tasks: &[TaskDefinition], - validator_selections: &[Vec], -) -> Vec { - if validator_selections.is_empty() || all_tasks.is_empty() { - return Vec::new(); - } - let threshold = validator_selections.len().div_ceil(2); - let mut counts = BTreeMap::new(); - for selection in validator_selections { - for &idx in selection { - *counts.entry(idx).or_insert(0usize) += 1; - } - } - let mut consensus_indices: Vec = counts - .into_iter() - .filter(|(_, count)| *count >= threshold) - .map(|(idx, _)| idx) - .collect(); - consensus_indices.sort_unstable(); - consensus_indices.truncate(CONSENSUS_DATASET_SIZE); - consensus_indices - .iter() - .filter_map(|&idx| all_tasks.get(idx).cloned()) - .collect() -} - -pub fn create_dataset_selection(tasks: Vec) -> DatasetSelection { - let epoch = host_consensus_get_epoch(); - let mut hash_input = String::new(); - for task in &tasks { - let _ = write!(hash_input, "{}:{};", task.id, task.name); - } - DatasetSelection { - tasks, - selected_at_epoch: if epoch >= 0 { epoch as u64 } else { 0 }, - dataset_hash: hash_input, - } -} diff --git a/challenges/term-challenge-wasm/src/lib.rs b/challenges/term-challenge-wasm/src/lib.rs index c1fa871e..b03ba5f3 100644 --- a/challenges/term-challenge-wasm/src/lib.rs +++ b/challenges/term-challenge-wasm/src/lib.rs @@ -2,8 +2,6 @@ extern crate alloc; -mod dataset; -mod routes; mod scoring; mod tasks; mod types; diff --git a/challenges/term-challenge-wasm/src/routes.rs b/challenges/term-challenge-wasm/src/routes.rs deleted file mode 100644 index 153fef3d..00000000 --- a/challenges/term-challenge-wasm/src/routes.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::types::RouteDefinition; -use alloc::string::String; -use alloc::vec; -use alloc::vec::Vec; - -pub fn get_route_definitions() -> Vec { - vec![ - RouteDefinition { - method: String::from("GET"), - path: String::from("/leaderboard"), - description: String::from( - "Returns current leaderboard with scores, miner hotkeys, and ranks", - ), - }, - RouteDefinition { - method: String::from("GET"), - path: String::from("/submissions"), - description: String::from("Returns pending submissions awaiting evaluation"), - }, - RouteDefinition { - method: String::from("GET"), - path: String::from("/submissions/:id"), - description: String::from("Returns specific submission status"), - }, - RouteDefinition { - method: String::from("GET"), - path: String::from("/dataset"), - description: String::from("Returns current active dataset of 50 SWE-bench tasks"), - }, - RouteDefinition { - method: String::from("GET"), - path: String::from("/dataset/history"), - description: String::from("Returns historical dataset selections"), - }, - RouteDefinition { - method: String::from("POST"), - path: String::from("/submit"), - description: String::from("Submission endpoint: receives zip package and metadata"), - }, - RouteDefinition { - method: String::from("GET"), - path: String::from("/decay"), - description: String::from("Returns current decay status for top agents"), - }, - RouteDefinition { - method: String::from("GET"), - path: String::from("/stats"), - description: String::from("Challenge statistics: total submissions, active miners"), - }, - ] -} diff --git a/challenges/term-challenge-wasm/src/scoring.rs b/challenges/term-challenge-wasm/src/scoring.rs index 5afa0bb7..eb9047ab 100644 --- a/challenges/term-challenge-wasm/src/scoring.rs +++ b/challenges/term-challenge-wasm/src/scoring.rs @@ -1,7 +1,7 @@ use alloc::string::String; use core::fmt::Write as _; -use crate::types::{DecayParams, Difficulty, DifficultyStats, TaskDefinition, TaskResult}; +use crate::types::{Difficulty, DifficultyStats, TaskDefinition, TaskResult}; pub struct AggregateScore { pub tasks_passed: u32, @@ -76,21 +76,6 @@ pub fn to_weight(score: &AggregateScore) -> f64 { score.pass_rate.clamp(0.0, 1.0) } -pub fn apply_decay(weight: f64, hours_since_top: f64, params: &DecayParams) -> f64 { - let grace = params.grace_period_hours as f64; - if hours_since_top <= grace { - return weight; - } - let elapsed = hours_since_top - grace; - let half_life = params.half_life_hours as f64; - if half_life <= 0.0 { - return params.min_multiplier; - } - let multiplier = 0.5f64.powf(elapsed / half_life); - let clamped = multiplier.max(params.min_multiplier); - weight * clamped -} - pub fn format_summary(score: &AggregateScore) -> String { let mut msg = String::new(); let _ = write!( diff --git a/challenges/term-challenge-wasm/src/tasks.rs b/challenges/term-challenge-wasm/src/tasks.rs index ef584f3e..f2f7ee0d 100644 --- a/challenges/term-challenge-wasm/src/tasks.rs +++ b/challenges/term-challenge-wasm/src/tasks.rs @@ -55,16 +55,3 @@ fn append_dataset_history(selection: &DatasetSelection) -> bool { }; host_storage_set(DATASET_HISTORY_KEY, &data).is_ok() } - -pub fn get_dataset_history() -> Vec { - host_storage_get(DATASET_HISTORY_KEY) - .ok() - .and_then(|d| { - if d.is_empty() { - None - } else { - bincode_options_dataset().deserialize(&d).ok() - } - }) - .unwrap_or_default() -} diff --git a/challenges/term-challenge-wasm/src/types.rs b/challenges/term-challenge-wasm/src/types.rs index e389088f..6c218aec 100644 --- a/challenges/term-challenge-wasm/src/types.rs +++ b/challenges/term-challenge-wasm/src/types.rs @@ -117,10 +117,3 @@ pub struct DatasetSelection { pub selected_at_epoch: u64, pub dataset_hash: String, } - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RouteDefinition { - pub method: String, - pub path: String, - pub description: String, -} diff --git a/challenges/term-challenge/src/dataset.rs b/challenges/term-challenge/src/dataset.rs deleted file mode 100644 index 3189dec2..00000000 --- a/challenges/term-challenge/src/dataset.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::types::{DatasetSelection, TaskDefinition}; -use alloc::collections::BTreeMap; -use alloc::string::String; -use alloc::vec::Vec; -use core::fmt::Write as _; -use platform_challenge_sdk_wasm::host_functions::{ - host_consensus_get_epoch, host_random_seed, host_storage_set, -}; - -const DATASET_SELECTION_PREFIX: &[u8] = b"dataset_selection:"; -const TOTAL_SWE_BENCH_TASKS: usize = 2294; -const TASKS_TO_SELECT: usize = 100; -const CONSENSUS_DATASET_SIZE: usize = 50; - -pub fn select_random_task_indices() -> Vec { - let mut seed = [0u8; 32]; - if host_random_seed(&mut seed).is_err() { - return Vec::new(); - } - let mut indices = Vec::with_capacity(TASKS_TO_SELECT); - let mut state: u64 = u64::from_le_bytes([ - seed[0], seed[1], seed[2], seed[3], seed[4], seed[5], seed[6], seed[7], - ]); - while indices.len() < TASKS_TO_SELECT { - state = state - .wrapping_mul(6364136223846793005) - .wrapping_add(1442695040888963407); - let idx = (state >> 33) as usize % TOTAL_SWE_BENCH_TASKS; - if !indices.contains(&idx) { - indices.push(idx); - } - } - indices -} - -pub fn store_my_selection(indices: &[usize]) -> bool { - let epoch = host_consensus_get_epoch(); - if epoch < 0 { - return false; - } - let mut key = Vec::with_capacity(DATASET_SELECTION_PREFIX.len() + 8); - key.extend_from_slice(DATASET_SELECTION_PREFIX); - key.extend_from_slice(&(epoch as u64).to_le_bytes()); - let data = match bincode::serialize(indices) { - Ok(d) => d, - Err(_) => return false, - }; - host_storage_set(&key, &data).is_ok() -} - -pub fn build_consensus_dataset( - all_tasks: &[TaskDefinition], - validator_selections: &[Vec], -) -> Vec { - if validator_selections.is_empty() || all_tasks.is_empty() { - return Vec::new(); - } - let threshold = validator_selections.len().div_ceil(2); - let mut counts = BTreeMap::new(); - for selection in validator_selections { - for &idx in selection { - *counts.entry(idx).or_insert(0usize) += 1; - } - } - let mut consensus_indices: Vec = counts - .into_iter() - .filter(|(_, count)| *count >= threshold) - .map(|(idx, _)| idx) - .collect(); - consensus_indices.sort_unstable(); - consensus_indices.truncate(CONSENSUS_DATASET_SIZE); - consensus_indices - .iter() - .filter_map(|&idx| all_tasks.get(idx).cloned()) - .collect() -} - -pub fn create_dataset_selection(tasks: Vec) -> DatasetSelection { - let epoch = host_consensus_get_epoch(); - let mut hash_input = String::new(); - for task in &tasks { - let _ = write!(hash_input, "{}:{};", task.id, task.name); - } - DatasetSelection { - tasks, - selected_at_epoch: if epoch >= 0 { epoch as u64 } else { 0 }, - dataset_hash: hash_input, - } -} diff --git a/challenges/term-challenge/src/lib.rs b/challenges/term-challenge/src/lib.rs index c1fa871e..b03ba5f3 100644 --- a/challenges/term-challenge/src/lib.rs +++ b/challenges/term-challenge/src/lib.rs @@ -2,8 +2,6 @@ extern crate alloc; -mod dataset; -mod routes; mod scoring; mod tasks; mod types; diff --git a/challenges/term-challenge/src/routes.rs b/challenges/term-challenge/src/routes.rs deleted file mode 100644 index 153fef3d..00000000 --- a/challenges/term-challenge/src/routes.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::types::RouteDefinition; -use alloc::string::String; -use alloc::vec; -use alloc::vec::Vec; - -pub fn get_route_definitions() -> Vec { - vec![ - RouteDefinition { - method: String::from("GET"), - path: String::from("/leaderboard"), - description: String::from( - "Returns current leaderboard with scores, miner hotkeys, and ranks", - ), - }, - RouteDefinition { - method: String::from("GET"), - path: String::from("/submissions"), - description: String::from("Returns pending submissions awaiting evaluation"), - }, - RouteDefinition { - method: String::from("GET"), - path: String::from("/submissions/:id"), - description: String::from("Returns specific submission status"), - }, - RouteDefinition { - method: String::from("GET"), - path: String::from("/dataset"), - description: String::from("Returns current active dataset of 50 SWE-bench tasks"), - }, - RouteDefinition { - method: String::from("GET"), - path: String::from("/dataset/history"), - description: String::from("Returns historical dataset selections"), - }, - RouteDefinition { - method: String::from("POST"), - path: String::from("/submit"), - description: String::from("Submission endpoint: receives zip package and metadata"), - }, - RouteDefinition { - method: String::from("GET"), - path: String::from("/decay"), - description: String::from("Returns current decay status for top agents"), - }, - RouteDefinition { - method: String::from("GET"), - path: String::from("/stats"), - description: String::from("Challenge statistics: total submissions, active miners"), - }, - ] -} diff --git a/challenges/term-challenge/src/scoring.rs b/challenges/term-challenge/src/scoring.rs index 5afa0bb7..eb9047ab 100644 --- a/challenges/term-challenge/src/scoring.rs +++ b/challenges/term-challenge/src/scoring.rs @@ -1,7 +1,7 @@ use alloc::string::String; use core::fmt::Write as _; -use crate::types::{DecayParams, Difficulty, DifficultyStats, TaskDefinition, TaskResult}; +use crate::types::{Difficulty, DifficultyStats, TaskDefinition, TaskResult}; pub struct AggregateScore { pub tasks_passed: u32, @@ -76,21 +76,6 @@ pub fn to_weight(score: &AggregateScore) -> f64 { score.pass_rate.clamp(0.0, 1.0) } -pub fn apply_decay(weight: f64, hours_since_top: f64, params: &DecayParams) -> f64 { - let grace = params.grace_period_hours as f64; - if hours_since_top <= grace { - return weight; - } - let elapsed = hours_since_top - grace; - let half_life = params.half_life_hours as f64; - if half_life <= 0.0 { - return params.min_multiplier; - } - let multiplier = 0.5f64.powf(elapsed / half_life); - let clamped = multiplier.max(params.min_multiplier); - weight * clamped -} - pub fn format_summary(score: &AggregateScore) -> String { let mut msg = String::new(); let _ = write!( diff --git a/challenges/term-challenge/src/tasks.rs b/challenges/term-challenge/src/tasks.rs index ef584f3e..f2f7ee0d 100644 --- a/challenges/term-challenge/src/tasks.rs +++ b/challenges/term-challenge/src/tasks.rs @@ -55,16 +55,3 @@ fn append_dataset_history(selection: &DatasetSelection) -> bool { }; host_storage_set(DATASET_HISTORY_KEY, &data).is_ok() } - -pub fn get_dataset_history() -> Vec { - host_storage_get(DATASET_HISTORY_KEY) - .ok() - .and_then(|d| { - if d.is_empty() { - None - } else { - bincode_options_dataset().deserialize(&d).ok() - } - }) - .unwrap_or_default() -} diff --git a/challenges/term-challenge/src/types.rs b/challenges/term-challenge/src/types.rs index e389088f..6c218aec 100644 --- a/challenges/term-challenge/src/types.rs +++ b/challenges/term-challenge/src/types.rs @@ -117,10 +117,3 @@ pub struct DatasetSelection { pub selected_at_epoch: u64, pub dataset_hash: String, } - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RouteDefinition { - pub method: String, - pub path: String, - pub description: String, -} From 616d73814504fbd2cf6d8a0b6561d7f64cec6d25 Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 13:21:30 +0000 Subject: [PATCH 06/10] fix(quality): address code quality issues in PR #51 - Remove duplicate 'Signed Message Wrapper' section headers in messages.rs - Fix silent error swallowing in dynamic.rs query_by_prefix (propagate serialization errors instead of unwrap_or_default) - Extract LLM_REQUEST_TIMEOUT_SECS constant in llm.rs - Extract RESPONSE_BUF_SMALL/MEDIUM/LARGE constants in host_functions.rs - Extract MAX_DATASET_HISTORY constant in both tasks.rs files --- challenges/term-challenge-wasm/src/tasks.rs | 5 ++-- challenges/term-challenge/src/tasks.rs | 5 ++-- .../challenge-sdk-wasm/src/host_functions.rs | 26 +++++++++++-------- crates/p2p-consensus/src/messages.rs | 7 ----- crates/storage/src/dynamic.rs | 9 ++++--- crates/wasm-runtime-interface/src/llm.rs | 3 ++- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/challenges/term-challenge-wasm/src/tasks.rs b/challenges/term-challenge-wasm/src/tasks.rs index f2f7ee0d..964e5238 100644 --- a/challenges/term-challenge-wasm/src/tasks.rs +++ b/challenges/term-challenge-wasm/src/tasks.rs @@ -6,6 +6,7 @@ use platform_challenge_sdk_wasm::host_functions::{host_storage_get, host_storage const ACTIVE_DATASET_KEY: &[u8] = b"active_dataset"; const DATASET_HISTORY_KEY: &[u8] = b"dataset_history"; const MAX_DATASET_SIZE: u64 = 8 * 1024 * 1024; +const MAX_DATASET_HISTORY: usize = 100; fn bincode_options_dataset() -> impl Options { bincode::DefaultOptions::new() @@ -46,8 +47,8 @@ fn append_dataset_history(selection: &DatasetSelection) -> bool { }) .unwrap_or_default(); history.push(selection.clone()); - if history.len() > 100 { - history.drain(0..history.len() - 100); + if history.len() > MAX_DATASET_HISTORY { + history.drain(0..history.len() - MAX_DATASET_HISTORY); } let data = match bincode::serialize(&history) { Ok(d) => d, diff --git a/challenges/term-challenge/src/tasks.rs b/challenges/term-challenge/src/tasks.rs index f2f7ee0d..964e5238 100644 --- a/challenges/term-challenge/src/tasks.rs +++ b/challenges/term-challenge/src/tasks.rs @@ -6,6 +6,7 @@ use platform_challenge_sdk_wasm::host_functions::{host_storage_get, host_storage const ACTIVE_DATASET_KEY: &[u8] = b"active_dataset"; const DATASET_HISTORY_KEY: &[u8] = b"dataset_history"; const MAX_DATASET_SIZE: u64 = 8 * 1024 * 1024; +const MAX_DATASET_HISTORY: usize = 100; fn bincode_options_dataset() -> impl Options { bincode::DefaultOptions::new() @@ -46,8 +47,8 @@ fn append_dataset_history(selection: &DatasetSelection) -> bool { }) .unwrap_or_default(); history.push(selection.clone()); - if history.len() > 100 { - history.drain(0..history.len() - 100); + if history.len() > MAX_DATASET_HISTORY { + history.drain(0..history.len() - MAX_DATASET_HISTORY); } let data = match bincode::serialize(&history) { Ok(d) => d, diff --git a/crates/challenge-sdk-wasm/src/host_functions.rs b/crates/challenge-sdk-wasm/src/host_functions.rs index 4fe548f2..f0562338 100644 --- a/crates/challenge-sdk-wasm/src/host_functions.rs +++ b/crates/challenge-sdk-wasm/src/host_functions.rs @@ -1,6 +1,10 @@ use alloc::vec; use alloc::vec::Vec; +const RESPONSE_BUF_SMALL: usize = 4096; +const RESPONSE_BUF_MEDIUM: usize = 64 * 1024; +const RESPONSE_BUF_LARGE: usize = 256 * 1024; + #[link(wasm_import_module = "platform_network")] extern "C" { fn http_get(req_ptr: i32, req_len: i32, resp_ptr: i32, resp_len: i32) -> i32; @@ -25,7 +29,7 @@ extern "C" { } pub fn host_http_get(request: &[u8]) -> Result, i32> { - let mut response_buf = vec![0u8; 65536]; + let mut response_buf = vec![0u8; RESPONSE_BUF_MEDIUM]; let status = unsafe { http_get( request.as_ptr() as i32, @@ -42,7 +46,7 @@ pub fn host_http_get(request: &[u8]) -> Result, i32> { } pub fn host_http_post(request: &[u8], body: &[u8]) -> Result, i32> { - let mut response_buf = vec![0u8; 65536]; + let mut response_buf = vec![0u8; RESPONSE_BUF_MEDIUM]; let status = unsafe { http_post( request.as_ptr() as i32, @@ -60,7 +64,7 @@ pub fn host_http_post(request: &[u8], body: &[u8]) -> Result, i32> { } pub fn host_dns_resolve(request: &[u8]) -> Result, i32> { - let mut response_buf = vec![0u8; 4096]; + let mut response_buf = vec![0u8; RESPONSE_BUF_SMALL]; let status = unsafe { dns_resolve( request.as_ptr() as i32, @@ -76,7 +80,7 @@ pub fn host_dns_resolve(request: &[u8]) -> Result, i32> { } pub fn host_storage_get(key: &[u8]) -> Result, i32> { - let mut value_buf = vec![0u8; 65536]; + let mut value_buf = vec![0u8; RESPONSE_BUF_MEDIUM]; let status = unsafe { storage_get( key.as_ptr() as i32, @@ -107,7 +111,7 @@ pub fn host_storage_set(key: &[u8], value: &[u8]) -> Result<(), i32> { } pub fn host_terminal_exec(request: &[u8]) -> Result, i32> { - let mut result_buf = vec![0u8; 262144]; + let mut result_buf = vec![0u8; RESPONSE_BUF_LARGE]; let status = unsafe { terminal_exec( request.as_ptr() as i32, @@ -124,7 +128,7 @@ pub fn host_terminal_exec(request: &[u8]) -> Result, i32> { } pub fn host_read_file(path: &[u8]) -> Result, i32> { - let mut buf = vec![0u8; 262144]; + let mut buf = vec![0u8; RESPONSE_BUF_LARGE]; let status = unsafe { terminal_read_file( path.as_ptr() as i32, @@ -156,7 +160,7 @@ pub fn host_write_file(path: &[u8], data: &[u8]) -> Result<(), i32> { } pub fn host_list_dir(path: &[u8]) -> Result, i32> { - let mut buf = vec![0u8; 65536]; + let mut buf = vec![0u8; RESPONSE_BUF_MEDIUM]; let status = unsafe { terminal_list_dir( path.as_ptr() as i32, @@ -192,7 +196,7 @@ extern "C" { } pub fn host_sandbox_exec(request: &[u8]) -> Result, i32> { - let mut response_buf = vec![0u8; 262144]; + let mut response_buf = vec![0u8; RESPONSE_BUF_LARGE]; let status = unsafe { sandbox_exec( request.as_ptr() as i32, @@ -223,7 +227,7 @@ extern "C" { } pub fn host_llm_chat_completion(request: &[u8]) -> Result, i32> { - let mut response_buf = vec![0u8; 262144]; + let mut response_buf = vec![0u8; RESPONSE_BUF_LARGE]; let status = unsafe { llm_chat_completion( request.as_ptr() as i32, @@ -259,7 +263,7 @@ pub fn host_consensus_get_epoch() -> i64 { } pub fn host_consensus_get_validators() -> Result, i32> { - let mut buf = vec![0u8; 65536]; + let mut buf = vec![0u8; RESPONSE_BUF_MEDIUM]; let status = unsafe { consensus_get_validators(buf.as_mut_ptr() as i32, buf.len() as i32) }; if status < 0 { return Err(status); @@ -277,7 +281,7 @@ pub fn host_consensus_propose_weight(uid: i32, weight: i32) -> Result<(), i32> { } pub fn host_consensus_get_votes() -> Result, i32> { - let mut buf = vec![0u8; 65536]; + let mut buf = vec![0u8; RESPONSE_BUF_MEDIUM]; let status = unsafe { consensus_get_votes(buf.as_mut_ptr() as i32, buf.len() as i32) }; if status < 0 { return Err(status); diff --git a/crates/p2p-consensus/src/messages.rs b/crates/p2p-consensus/src/messages.rs index 0e1cd212..6ce3426f 100644 --- a/crates/p2p-consensus/src/messages.rs +++ b/crates/p2p-consensus/src/messages.rs @@ -618,10 +618,6 @@ pub struct StorageVoteMessage { pub signature: Vec, } -// ============================================================================ -// Signed Message Wrapper -// ============================================================================ - // ============================================================================ // Review Assignment Messages // ============================================================================ @@ -688,9 +684,6 @@ pub struct ReviewResultMessage { pub signature: Vec, } -// ============================================================================ -// Signed Message Wrapper -// ============================================================================ // ============================================================================ // Signed Message Wrapper // ============================================================================ diff --git a/crates/storage/src/dynamic.rs b/crates/storage/src/dynamic.rs index b611f556..142cd964 100644 --- a/crates/storage/src/dynamic.rs +++ b/crates/storage/src/dynamic.rs @@ -485,14 +485,15 @@ impl DynamicStorage { let namespace = challenge_id.0.to_string(); let entries = self.scan_namespace(&namespace)?; - Ok(entries + entries .into_iter() .filter(|(k, _)| k.validator.is_none() && k.key.starts_with(prefix)) .map(|(k, entry)| { - let value_bytes = bincode::serialize(&entry.value).unwrap_or_default(); - (k.key, value_bytes) + let value_bytes = bincode::serialize(&entry.value) + .map_err(|e| MiniChainError::Serialization(e.to_string()))?; + Ok((k.key, value_bytes)) }) - .collect()) + .collect() } /// Get a value as it existed at a specific block height diff --git a/crates/wasm-runtime-interface/src/llm.rs b/crates/wasm-runtime-interface/src/llm.rs index 45561ab7..00974e4e 100644 --- a/crates/wasm-runtime-interface/src/llm.rs +++ b/crates/wasm-runtime-interface/src/llm.rs @@ -16,6 +16,7 @@ use tracing::warn; use wasmtime::{Caller, Linker, Memory}; const MAX_CHAT_REQUEST_SIZE: u64 = 4 * 1024 * 1024; +const LLM_REQUEST_TIMEOUT_SECS: u64 = 60; pub const HOST_LLM_NAMESPACE: &str = "platform_llm"; pub const HOST_LLM_CHAT_COMPLETION: &str = "llm_chat_completion"; @@ -279,7 +280,7 @@ fn handle_chat_completion( .header("Content-Type", "application/json") .header("Authorization", format!("Bearer {}", api_key)) .body(json_body) - .timeout(std::time::Duration::from_secs(60)) + .timeout(std::time::Duration::from_secs(LLM_REQUEST_TIMEOUT_SECS)) .send() { Ok(r) => r, From 71582ffa97aeeb0513559107b0282c6b44700daf Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 13:43:07 +0000 Subject: [PATCH 07/10] fix(security): harden deserialization limits, redact secrets, prevent log injection - Add bincode size limits to P2P message deserialization (messages.rs, network.rs) - Add bincode size limits to state deserialization (state.rs) - Add bincode size limits to storage entry deserialization (dynamic.rs) - Redact secret_key in Args Debug impl (main.rs) - Redact chutes_api_key in WasmExecutorConfig Debug impl (wasm_executor.rs) - Sanitize user-provided ReviewDecline reason before logging (main.rs) --- bins/validator-node/src/main.rs | 46 ++++++++++++++++++++++-- bins/validator-node/src/wasm_executor.rs | 15 ++++++++ crates/p2p-consensus/src/messages.rs | 9 +++-- crates/p2p-consensus/src/network.rs | 11 ++++-- crates/p2p-consensus/src/state.rs | 8 ++++- crates/storage/src/dynamic.rs | 18 ++++++++-- 6 files changed, 96 insertions(+), 11 deletions(-) diff --git a/bins/validator-node/src/main.rs b/bins/validator-node/src/main.rs index 9b348ca4..de2aca3f 100644 --- a/bins/validator-node/src/main.rs +++ b/bins/validator-node/src/main.rs @@ -39,6 +39,25 @@ use wasm_executor::{WasmChallengeExecutor, WasmExecutorConfig}; /// Storage key for persisted chain state const STATE_STORAGE_KEY: &str = "chain_state"; +/// Maximum length for user-provided strings logged from P2P messages +const MAX_LOG_FIELD_LEN: usize = 256; + +/// Sanitize a user-provided string for safe logging. +/// +/// Replaces control characters (newlines, tabs, ANSI escapes) with spaces +/// and truncates to `MAX_LOG_FIELD_LEN` to prevent log injection attacks. +fn sanitize_for_log(s: &str) -> String { + let truncated = if s.len() > MAX_LOG_FIELD_LEN { + &s[..MAX_LOG_FIELD_LEN] + } else { + s + }; + truncated + .chars() + .map(|c| if c.is_control() { ' ' } else { c }) + .collect() +} + // ==================== Shutdown Handler ==================== /// Handles graceful shutdown with state persistence @@ -121,7 +140,7 @@ impl ShutdownHandler { // ==================== CLI ==================== -#[derive(Parser, Debug)] +#[derive(Parser)] #[command(name = "validator-node")] #[command(about = "Platform Validator - Decentralized P2P Architecture")] struct Args { @@ -178,6 +197,28 @@ struct Args { wasm_fuel_limit: Option, } +impl std::fmt::Debug for Args { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Args") + .field( + "secret_key", + &self.secret_key.as_ref().map(|_| "[REDACTED]"), + ) + .field("data_dir", &self.data_dir) + .field("listen_addr", &self.listen_addr) + .field("bootstrap", &self.bootstrap) + .field("subtensor_endpoint", &self.subtensor_endpoint) + .field("netuid", &self.netuid) + .field("version_key", &self.version_key) + .field("no_bittensor", &self.no_bittensor) + .field("wasm_module_dir", &self.wasm_module_dir) + .field("wasm_max_memory", &self.wasm_max_memory) + .field("wasm_enable_fuel", &self.wasm_enable_fuel) + .field("wasm_fuel_limit", &self.wasm_fuel_limit) + .finish() + } +} + // ==================== Main ==================== #[tokio::main] @@ -962,10 +1003,11 @@ async fn handle_network_event( ); } P2PMessage::ReviewDecline(msg) => { + let safe_reason = sanitize_for_log(&msg.reason); debug!( submission_id = %msg.submission_id, validator = %msg.validator.to_hex(), - reason = %msg.reason, + reason = %safe_reason, "Received review decline" ); } diff --git a/bins/validator-node/src/wasm_executor.rs b/bins/validator-node/src/wasm_executor.rs index d25d308c..4cff26fe 100644 --- a/bins/validator-node/src/wasm_executor.rs +++ b/bins/validator-node/src/wasm_executor.rs @@ -72,6 +72,21 @@ pub struct WasmExecutorConfig { pub chutes_api_key: Option, } +impl std::fmt::Debug for WasmExecutorConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WasmExecutorConfig") + .field("module_dir", &self.module_dir) + .field("max_memory_bytes", &self.max_memory_bytes) + .field("enable_fuel", &self.enable_fuel) + .field("fuel_limit", &self.fuel_limit) + .field( + "chutes_api_key", + &self.chutes_api_key.as_ref().map(|_| "[REDACTED]"), + ) + .finish() + } +} + impl Default for WasmExecutorConfig { fn default() -> Self { Self { diff --git a/crates/p2p-consensus/src/messages.rs b/crates/p2p-consensus/src/messages.rs index 6ce3426f..efcabd3f 100644 --- a/crates/p2p-consensus/src/messages.rs +++ b/crates/p2p-consensus/src/messages.rs @@ -3,10 +3,11 @@ //! Defines all message types used for inter-validator communication //! over the libp2p gossipsub network. +use bincode::Options; use platform_core::{ChallengeId, Hotkey}; use serde::{Deserialize, Serialize}; -const MAX_P2P_MESSAGE_SIZE: u64 = 16 * 1024 * 1024; +pub const MAX_P2P_MESSAGE_SIZE: u64 = 16 * 1024 * 1024; /// Unique identifier for a consensus round pub type RoundId = u64; @@ -75,7 +76,11 @@ impl P2PMessage { MAX_P2P_MESSAGE_SIZE )))); } - bincode::deserialize(bytes) + bincode::DefaultOptions::new() + .with_limit(MAX_P2P_MESSAGE_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() + .deserialize(bytes) } /// Get the message type name for logging diff --git a/crates/p2p-consensus/src/network.rs b/crates/p2p-consensus/src/network.rs index f600f4b6..5f64237b 100644 --- a/crates/p2p-consensus/src/network.rs +++ b/crates/p2p-consensus/src/network.rs @@ -4,8 +4,9 @@ //! Provides the networking foundation for PBFT consensus. use crate::config::P2PConfig; -use crate::messages::{P2PMessage, SignedP2PMessage, WeightVoteMessage}; +use crate::messages::{P2PMessage, SignedP2PMessage, WeightVoteMessage, MAX_P2P_MESSAGE_SIZE}; use crate::validator::ValidatorSet; +use bincode::Options; use libp2p::{ gossipsub::{self, IdentTopic, MessageAuthenticity, MessageId, ValidationMode}, identify, @@ -443,8 +444,12 @@ impl P2PNetwork { source: PeerId, data: &[u8], ) -> Result { - let signed: SignedP2PMessage = - bincode::deserialize(data).map_err(|e| NetworkError::Serialization(e.to_string()))?; + let signed: SignedP2PMessage = bincode::DefaultOptions::new() + .with_limit(MAX_P2P_MESSAGE_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() + .deserialize(data) + .map_err(|e| NetworkError::Serialization(e.to_string()))?; // Verify signature first if !self.verify_message(&signed) { diff --git a/crates/p2p-consensus/src/state.rs b/crates/p2p-consensus/src/state.rs index 08ec3778..8667cde1 100644 --- a/crates/p2p-consensus/src/state.rs +++ b/crates/p2p-consensus/src/state.rs @@ -4,6 +4,7 @@ //! evaluations, weights, and validator information. use crate::messages::{MerkleNode, MerkleProof, SequenceNumber}; +use bincode::Options; use parking_lot::RwLock; use platform_core::{hash_data, ChallengeId, Hotkey, SignedMessage}; use serde::{Deserialize, Serialize}; @@ -329,7 +330,12 @@ impl ChainState { MAX_STATE_DESERIALIZATION_SIZE ))); } - bincode::deserialize(bytes).map_err(|e| StateError::Serialization(e.to_string())) + bincode::DefaultOptions::new() + .with_limit(MAX_STATE_DESERIALIZATION_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() + .deserialize(bytes) + .map_err(|e| StateError::Serialization(e.to_string())) } /// Add or update a validator diff --git a/crates/storage/src/dynamic.rs b/crates/storage/src/dynamic.rs index 142cd964..8f8126b4 100644 --- a/crates/storage/src/dynamic.rs +++ b/crates/storage/src/dynamic.rs @@ -14,6 +14,7 @@ use crate::types::{ NamespaceStats, StorageChange, StorageEntry, StorageKey, StorageStats, StorageValue, }; +use bincode::Options; use parking_lot::RwLock; use platform_core::{ChallengeId, Hotkey, MiniChainError, Result}; use sled::Tree; @@ -22,6 +23,15 @@ use std::sync::Arc; use std::time::{Duration, SystemTime}; use tracing::{info, trace}; +const MAX_STORAGE_ENTRY_SIZE: u64 = 64 * 1024 * 1024; + +fn bincode_options_storage() -> impl Options { + bincode::DefaultOptions::new() + .with_limit(MAX_STORAGE_ENTRY_SIZE) + .with_fixint_encoding() + .allow_trailing_bytes() +} + /// Dynamic storage manager #[allow(clippy::type_complexity)] pub struct DynamicStorage { @@ -116,7 +126,8 @@ impl DynamicStorage { .map_err(|e| MiniChainError::Storage(e.to_string()))? { Some(data) => { - let entry: StorageEntry = bincode::deserialize(&data) + let entry: StorageEntry = bincode_options_storage() + .deserialize(&data) .map_err(|e| MiniChainError::Serialization(e.to_string()))?; // Check expiry @@ -355,7 +366,8 @@ impl DynamicStorage { for item in self.tree.scan_prefix(&prefix) { let (key_bytes, data) = item.map_err(|e| MiniChainError::Storage(e.to_string()))?; - let entry: StorageEntry = bincode::deserialize(&data) + let entry: StorageEntry = bincode_options_storage() + .deserialize(&data) .map_err(|e| MiniChainError::Serialization(e.to_string()))?; if entry.is_expired() { @@ -411,7 +423,7 @@ impl DynamicStorage { for item in self.tree.iter() { let (key, data) = item.map_err(|e| MiniChainError::Storage(e.to_string()))?; - if let Ok(entry) = bincode::deserialize::(&data) { + if let Ok(entry) = bincode_options_storage().deserialize::(&data) { if entry.is_expired() { to_remove.push(key.to_vec()); } From f54ff200a62cfd1d8ae07b963a03540098a4f023 Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 13:48:16 +0000 Subject: [PATCH 08/10] fix(dead-code): remove orphaned select_review_validators, fix ignored return value - Remove select_review_validators from ConsensusEngine: defined but never called from any code path (no callers in bins/, crates/, or challenges/) - Make store_dataset return value explicitly ignored with let _ = in both term-challenge and term-challenge-wasm configure() methods Issues analyzed (10 total, 2 fixed, 8 intentionally kept): - select_review_validators: REMOVED (orphaned, 67 lines) - store_dataset ignored return: FIXED (explicit let _ =) - LLM types (llm_types.rs): kept (SDK API surface for challenge authors) - host_llm_* functions: kept (WASM FFI bridge, host-side registered) - ReviewRecord/ReviewResultEntry: kept (ChainState serialization + API) - assign_review/add_review_result/get_review_status: kept (review feature API) - DecayParams/active_dataset fields: kept (binary serialization compat) - query_by_prefix/get_at_block/list_keys: kept (public storage API) - ReviewType enum: kept (used by message types and state) - No unused imports found --- challenges/term-challenge-wasm/src/lib.rs | 2 +- challenges/term-challenge/src/lib.rs | 2 +- crates/p2p-consensus/src/consensus.rs | 67 ----------------------- 3 files changed, 2 insertions(+), 69 deletions(-) diff --git a/challenges/term-challenge-wasm/src/lib.rs b/challenges/term-challenge-wasm/src/lib.rs index b03ba5f3..edee6ea5 100644 --- a/challenges/term-challenge-wasm/src/lib.rs +++ b/challenges/term-challenge-wasm/src/lib.rs @@ -237,7 +237,7 @@ impl Challenge for TermChallengeWasm { fn configure(&self, config: &[u8]) { if let Ok(selection) = bincode_options_params().deserialize::(config) { - tasks::store_dataset(&selection); + let _ = tasks::store_dataset(&selection); } } } diff --git a/challenges/term-challenge/src/lib.rs b/challenges/term-challenge/src/lib.rs index b03ba5f3..edee6ea5 100644 --- a/challenges/term-challenge/src/lib.rs +++ b/challenges/term-challenge/src/lib.rs @@ -237,7 +237,7 @@ impl Challenge for TermChallengeWasm { fn configure(&self, config: &[u8]) { if let Ok(selection) = bincode_options_params().deserialize::(config) { - tasks::store_dataset(&selection); + let _ = tasks::store_dataset(&selection); } } } diff --git a/crates/p2p-consensus/src/consensus.rs b/crates/p2p-consensus/src/consensus.rs index 2b902c9c..71b08dc8 100644 --- a/crates/p2p-consensus/src/consensus.rs +++ b/crates/p2p-consensus/src/consensus.rs @@ -1178,73 +1178,6 @@ impl ConsensusEngine { pub fn get_decision(&self, sequence: SequenceNumber) -> Option { self.decisions.read().get(&sequence).cloned() } - - /// Select validators for review assignment - /// - /// Uses a deterministic seed derived from the submission ID and epoch - /// to select validators for LLM and AST review. Returns two lists: - /// 3 validators for LLM review and 3 for AST review. - pub fn select_review_validators( - &self, - submission_id: &str, - epoch: u64, - ) -> ([Hotkey; 3], [Hotkey; 3], [u8; 32]) { - let mut hasher = Sha256::new(); - hasher.update(submission_id.as_bytes()); - hasher.update(epoch.to_le_bytes()); - let seed: [u8; 32] = hasher.finalize().into(); - - let validators: Vec = self - .validator_set - .active_validators() - .into_iter() - .map(|r| r.hotkey) - .collect(); - - let select_n = |offset: usize| -> [Hotkey; 3] { - if validators.is_empty() { - return [Hotkey([0u8; 32]), Hotkey([0u8; 32]), Hotkey([0u8; 32])]; - } - - let mut selected = Vec::with_capacity(3); - let mut state: u64 = u64::from_le_bytes([ - seed[offset], - seed[offset + 1], - seed[offset + 2], - seed[offset + 3], - seed[offset + 4], - seed[offset + 5], - seed[offset + 6], - seed[offset + 7], - ]); - - while selected.len() < 3 && selected.len() < validators.len() { - state = state - .wrapping_mul(6364136223846793005) - .wrapping_add(1442695040888963407); - let idx = (state >> 33) as usize % validators.len(); - let candidate = &validators[idx]; - if !selected.contains(candidate) { - selected.push(candidate.clone()); - } - } - - while selected.len() < 3 { - selected.push(Hotkey([0u8; 32])); - } - - [ - selected[0].clone(), - selected[1].clone(), - selected[2].clone(), - ] - }; - - let llm_reviewers = select_n(0); - let ast_reviewers = select_n(8); - - (llm_reviewers, ast_reviewers, seed) - } } #[cfg(test)] From 672c41141b1b341e9aca3eba7229a5cd0482b18f Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 13:58:31 +0000 Subject: [PATCH 09/10] fix(quality): address 9 code quality issues in PR #51 - Move const after use block in wasm_executor.rs - Replace .unwrap() with if-let in consensus.rs - Propagate memory read error instead of silent default in wasm_executor.rs - Remove dead _sandbox_host_fns assignments in wasm_executor.rs - Extract SCORE_SCALE_FACTOR and LLM_JUDGE_PASS_THRESHOLD constants - Extract JOB_TIMEOUT_MS constant in main.rs - Use bounded bincode options for serialize calls in challenge crates - Remove incorrect #[allow(dead_code)] on validator_set field - Remove unused SandboxHostFunctions import --- bins/validator-node/src/main.rs | 3 ++- bins/validator-node/src/wasm_executor.rs | 15 +++++++-------- challenges/term-challenge-wasm/src/lib.rs | 12 ++++++++---- challenges/term-challenge-wasm/src/tasks.rs | 4 ++-- challenges/term-challenge/src/lib.rs | 12 ++++++++---- challenges/term-challenge/src/tasks.rs | 4 ++-- crates/p2p-consensus/src/consensus.rs | 16 ++++++++++------ crates/p2p-consensus/src/network.rs | 1 - 8 files changed, 39 insertions(+), 28 deletions(-) diff --git a/bins/validator-node/src/main.rs b/bins/validator-node/src/main.rs index de2aca3f..2b4e7987 100644 --- a/bins/validator-node/src/main.rs +++ b/bins/validator-node/src/main.rs @@ -41,6 +41,7 @@ const STATE_STORAGE_KEY: &str = "chain_state"; /// Maximum length for user-provided strings logged from P2P messages const MAX_LOG_FIELD_LEN: usize = 256; +const JOB_TIMEOUT_MS: i64 = 300_000; /// Sanitize a user-provided string for safe logging. /// @@ -889,7 +890,7 @@ async fn handle_network_event( challenge_id: assignment.challenge_id, assigned_validator: assignment.assigned_validator, assigned_at: assignment.timestamp, - timeout_at: assignment.timestamp + 300_000, + timeout_at: assignment.timestamp + JOB_TIMEOUT_MS, status: JobStatus::Pending, }; state_manager.apply(|state| { diff --git a/bins/validator-node/src/wasm_executor.rs b/bins/validator-node/src/wasm_executor.rs index 4cff26fe..6f488472 100644 --- a/bins/validator-node/src/wasm_executor.rs +++ b/bins/validator-node/src/wasm_executor.rs @@ -7,15 +7,14 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Instant; use tracing::{debug, info}; - -const MAX_EVALUATION_OUTPUT_SIZE: u64 = 64 * 1024 * 1024; use wasm_runtime_interface::{ ConsensusPolicy, ExecPolicy, InMemoryStorageBackend, InstanceConfig, LlmPolicy, - NetworkHostFunctions, NetworkPolicy, RuntimeConfig, SandboxHostFunctions, SandboxPolicy, - StorageBackend, StorageHostConfig, TerminalPolicy, TimePolicy, WasmModule, WasmRuntime, - WasmRuntimeError, + NetworkHostFunctions, NetworkPolicy, RuntimeConfig, SandboxPolicy, StorageBackend, + StorageHostConfig, TerminalPolicy, TimePolicy, WasmModule, WasmRuntime, WasmRuntimeError, }; +const MAX_EVALUATION_OUTPUT_SIZE: u64 = 64 * 1024 * 1024; + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct EvaluationInput { pub agent_data: Vec, @@ -185,7 +184,6 @@ impl WasmChallengeExecutor { bincode::serialize(&input).context("Failed to serialize EvaluationInput")?; let network_host_fns = Arc::new(NetworkHostFunctions::all()); - let _sandbox_host_fns = Arc::new(SandboxHostFunctions::all()); let instance_config = InstanceConfig { network_policy: network_policy.clone(), @@ -316,7 +314,6 @@ impl WasmChallengeExecutor { bincode::serialize(&input).context("Failed to serialize EvaluationInput")?; let network_host_fns = Arc::new(NetworkHostFunctions::all()); - let _sandbox_host_fns = Arc::new(SandboxHostFunctions::all()); let instance_config = InstanceConfig { network_policy: network_policy.clone(), @@ -476,7 +473,9 @@ impl WasmChallengeExecutor { let result_data = if out_ptr > 0 && out_len > 0 { instance .read_memory(out_ptr as usize, out_len as usize) - .unwrap_or_default() + .map_err(|e| { + anyhow::anyhow!("failed to read WASM memory for get_tasks output: {}", e) + })? } else { Vec::new() }; diff --git a/challenges/term-challenge-wasm/src/lib.rs b/challenges/term-challenge-wasm/src/lib.rs index edee6ea5..36d60798 100644 --- a/challenges/term-challenge-wasm/src/lib.rs +++ b/challenges/term-challenge-wasm/src/lib.rs @@ -25,6 +25,8 @@ const MAX_PARAMS_SIZE: u64 = 4 * 1024 * 1024; const MAX_LLM_RESPONSE_SIZE: u64 = 1024 * 1024; const MAX_TASKS: usize = 256; const EPOCH_RATE_LIMIT: u64 = 3; +const LLM_JUDGE_PASS_THRESHOLD: f64 = 0.5; +const SCORE_SCALE_FACTOR: f64 = 10_000.0; fn bincode_options_submission() -> impl Options { bincode::DefaultOptions::new() @@ -100,7 +102,7 @@ impl TermChallengeWasm { test_output: result.test_output.clone(), }; let url_bytes = url.as_bytes(); - let body = match bincode::serialize(&request) { + let body = match bincode_options_llm().serialize(&request) { Ok(b) => b, Err(_) => return None, }; @@ -163,7 +165,7 @@ impl Challenge for TermChallengeWasm { } if let Some(llm_score) = Self::try_llm_judge(url, result, &task.name) { result.score = llm_score; - if llm_score < 0.5 { + if llm_score < LLM_JUDGE_PASS_THRESHOLD { result.passed = false; } } @@ -171,7 +173,7 @@ impl Challenge for TermChallengeWasm { } let aggregate = calculate_aggregate(¶ms.tasks, &results); let weight = to_weight(&aggregate); - let score = (weight * 10_000.0) as i64; + let score = (weight * SCORE_SCALE_FACTOR) as i64; let message = format_summary(&aggregate); set_last_submission_epoch(&submission.miner_hotkey, submission.epoch); EvaluationOutput::success(score, &message) @@ -230,7 +232,9 @@ impl Challenge for TermChallengeWasm { fn tasks(&self) -> Vec { match tasks::get_active_dataset() { - Some(task_defs) => bincode::serialize(&task_defs).unwrap_or_default(), + Some(task_defs) => bincode_options_params() + .serialize(&task_defs) + .unwrap_or_default(), None => Vec::new(), } } diff --git a/challenges/term-challenge-wasm/src/tasks.rs b/challenges/term-challenge-wasm/src/tasks.rs index 964e5238..192fbb7f 100644 --- a/challenges/term-challenge-wasm/src/tasks.rs +++ b/challenges/term-challenge-wasm/src/tasks.rs @@ -24,7 +24,7 @@ pub fn get_active_dataset() -> Option> { } pub fn store_dataset(selection: &DatasetSelection) -> bool { - let data = match bincode::serialize(selection) { + let data = match bincode_options_dataset().serialize(selection) { Ok(d) => d, Err(_) => return false, }; @@ -50,7 +50,7 @@ fn append_dataset_history(selection: &DatasetSelection) -> bool { if history.len() > MAX_DATASET_HISTORY { history.drain(0..history.len() - MAX_DATASET_HISTORY); } - let data = match bincode::serialize(&history) { + let data = match bincode_options_dataset().serialize(&history) { Ok(d) => d, Err(_) => return false, }; diff --git a/challenges/term-challenge/src/lib.rs b/challenges/term-challenge/src/lib.rs index edee6ea5..36d60798 100644 --- a/challenges/term-challenge/src/lib.rs +++ b/challenges/term-challenge/src/lib.rs @@ -25,6 +25,8 @@ const MAX_PARAMS_SIZE: u64 = 4 * 1024 * 1024; const MAX_LLM_RESPONSE_SIZE: u64 = 1024 * 1024; const MAX_TASKS: usize = 256; const EPOCH_RATE_LIMIT: u64 = 3; +const LLM_JUDGE_PASS_THRESHOLD: f64 = 0.5; +const SCORE_SCALE_FACTOR: f64 = 10_000.0; fn bincode_options_submission() -> impl Options { bincode::DefaultOptions::new() @@ -100,7 +102,7 @@ impl TermChallengeWasm { test_output: result.test_output.clone(), }; let url_bytes = url.as_bytes(); - let body = match bincode::serialize(&request) { + let body = match bincode_options_llm().serialize(&request) { Ok(b) => b, Err(_) => return None, }; @@ -163,7 +165,7 @@ impl Challenge for TermChallengeWasm { } if let Some(llm_score) = Self::try_llm_judge(url, result, &task.name) { result.score = llm_score; - if llm_score < 0.5 { + if llm_score < LLM_JUDGE_PASS_THRESHOLD { result.passed = false; } } @@ -171,7 +173,7 @@ impl Challenge for TermChallengeWasm { } let aggregate = calculate_aggregate(¶ms.tasks, &results); let weight = to_weight(&aggregate); - let score = (weight * 10_000.0) as i64; + let score = (weight * SCORE_SCALE_FACTOR) as i64; let message = format_summary(&aggregate); set_last_submission_epoch(&submission.miner_hotkey, submission.epoch); EvaluationOutput::success(score, &message) @@ -230,7 +232,9 @@ impl Challenge for TermChallengeWasm { fn tasks(&self) -> Vec { match tasks::get_active_dataset() { - Some(task_defs) => bincode::serialize(&task_defs).unwrap_or_default(), + Some(task_defs) => bincode_options_params() + .serialize(&task_defs) + .unwrap_or_default(), None => Vec::new(), } } diff --git a/challenges/term-challenge/src/tasks.rs b/challenges/term-challenge/src/tasks.rs index 964e5238..192fbb7f 100644 --- a/challenges/term-challenge/src/tasks.rs +++ b/challenges/term-challenge/src/tasks.rs @@ -24,7 +24,7 @@ pub fn get_active_dataset() -> Option> { } pub fn store_dataset(selection: &DatasetSelection) -> bool { - let data = match bincode::serialize(selection) { + let data = match bincode_options_dataset().serialize(selection) { Ok(d) => d, Err(_) => return false, }; @@ -50,7 +50,7 @@ fn append_dataset_history(selection: &DatasetSelection) -> bool { if history.len() > MAX_DATASET_HISTORY { history.drain(0..history.len() - MAX_DATASET_HISTORY); } - let data = match bincode::serialize(&history) { + let data = match bincode_options_dataset().serialize(&history) { Ok(d) => d, Err(_) => return false, }; diff --git a/crates/p2p-consensus/src/consensus.rs b/crates/p2p-consensus/src/consensus.rs index 71b08dc8..1438e22d 100644 --- a/crates/p2p-consensus/src/consensus.rs +++ b/crates/p2p-consensus/src/consensus.rs @@ -749,12 +749,16 @@ impl ConsensusEngine { let (last_prepared_sequence, prepared_proof) = { let round = self.current_round.read(); if let Some(r) = round.as_ref() { - if r.phase >= ConsensusPhase::Prepared && r.pre_prepare.is_some() { - let proof = PreparedProof { - pre_prepare: r.pre_prepare.clone().unwrap(), - prepares: r.prepares.values().cloned().collect(), - }; - (Some(r.sequence), Some(proof)) + if let Some(pre_prepare) = r.pre_prepare.clone() { + if r.phase >= ConsensusPhase::Prepared { + let proof = PreparedProof { + pre_prepare, + prepares: r.prepares.values().cloned().collect(), + }; + (Some(r.sequence), Some(proof)) + } else { + (None, None) + } } else { (None, None) } diff --git a/crates/p2p-consensus/src/network.rs b/crates/p2p-consensus/src/network.rs index 5f64237b..6de620fb 100644 --- a/crates/p2p-consensus/src/network.rs +++ b/crates/p2p-consensus/src/network.rs @@ -170,7 +170,6 @@ pub struct P2PNetwork { /// Peer mapping peer_mapping: Arc, /// Reference to validator set - #[allow(dead_code)] validator_set: Arc, /// Event sender #[allow(dead_code)] From c6b3ca66f054d8ee5537b5b6e951c7ba67eaa720 Mon Sep 17 00:00:00 2001 From: echobt Date: Wed, 18 Feb 2026 14:07:54 +0000 Subject: [PATCH 10/10] fix(llm): align ChatRequest field types with guest LlmRequest for bincode compatibility The host-side ChatRequest struct used Option/Option for max_tokens and temperature, while the guest-side LlmRequest uses plain u32/f32. In bincode, Option encodes a 1-byte tag prefix before the value, making the formats binary-incompatible. Deserialization of guest-serialized LlmRequest bytes would fail on the host. Changed ChatRequest to use u32/f32 (matching the guest) and wrap values in Some() when constructing the OpenAI JSON request. --- crates/wasm-runtime-interface/src/llm.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/wasm-runtime-interface/src/llm.rs b/crates/wasm-runtime-interface/src/llm.rs index 00974e4e..074bacda 100644 --- a/crates/wasm-runtime-interface/src/llm.rs +++ b/crates/wasm-runtime-interface/src/llm.rs @@ -207,8 +207,8 @@ fn handle_chat_completion( struct ChatRequest { model: String, messages: Vec, - max_tokens: Option, - temperature: Option, + max_tokens: u32, + temperature: f32, } #[derive(Deserialize)] @@ -265,8 +265,8 @@ fn handle_chat_completion( content: m.content, }) .collect(), - max_tokens: chat_req.max_tokens, - temperature: chat_req.temperature, + max_tokens: Some(chat_req.max_tokens), + temperature: Some(chat_req.temperature), }; let json_body = match serde_json::to_vec(&openai_req) {