diff --git a/README.md b/README.md index 93176f85..f1b45007 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,54 @@ flowchart LR --- +## WASM Route Handling + +```mermaid +sequenceDiagram + participant Client + participant RPC as RPC Server + participant WE as WASM Executor + participant WM as WASM Module + + Client->>RPC: challenge_call(id, method, path) + RPC->>WE: execute_handle_route(request) + WE->>WM: handle_route(serialized_request) + WM-->>WE: serialized_response + WE-->>RPC: WasmRouteResponse + RPC-->>Client: JSON-RPC result +``` + +--- + +## Review Assignment Flow + +```mermaid +flowchart LR + Submit[Submission] --> Select[Validator Selection] + Select --> LLM[3 LLM Reviewers] + Select --> AST[3 AST Reviewers] + LLM --> |Review Results| Aggregate[Result Aggregation] + AST --> |Review Results| Aggregate + Aggregate --> Score[Final Score] + LLM -.-> |Timeout| Replace1[Replacement Validator] + AST -.-> |Timeout| Replace2[Replacement Validator] +``` + +--- + +## Subnet Owner Resolution + +```mermaid +flowchart TB + Sync[Metagraph Sync] --> Parse[Parse Neurons] + Parse --> UID0{UID 0 Found?} + UID0 -->|Yes| Update[Update ChainState.sudo_key] + UID0 -->|No| Keep[Keep Existing] + Update --> Owner[Subnet Owner = UID 0 Hotkey] +``` + +--- + ## Quick Start (Validator) ```bash diff --git a/bins/validator-node/src/wasm_executor.rs b/bins/validator-node/src/wasm_executor.rs index 6f488472..c419297b 100644 --- a/bins/validator-node/src/wasm_executor.rs +++ b/bins/validator-node/src/wasm_executor.rs @@ -580,6 +580,193 @@ impl WasmChallengeExecutor { Ok((result, metrics)) } + #[allow(dead_code)] + pub fn execute_get_routes( + &self, + module_path: &str, + network_policy: &NetworkPolicy, + sandbox_policy: &SandboxPolicy, + ) -> Result<(Vec, ExecutionMetrics)> { + let start = Instant::now(); + + let module = self + .load_module(module_path) + .context("Failed to load WASM module")?; + + let network_host_fns = Arc::new(NetworkHostFunctions::all()); + + let instance_config = InstanceConfig { + network_policy: network_policy.clone(), + sandbox_policy: sandbox_policy.clone(), + exec_policy: ExecPolicy::default(), + time_policy: TimePolicy::default(), + audit_logger: None, + memory_export: "memory".to_string(), + challenge_id: module_path.to_string(), + validator_id: "validator".to_string(), + restart_id: String::new(), + config_version: 0, + storage_host_config: StorageHostConfig::default(), + storage_backend: Arc::new(InMemoryStorageBackend::new()), + 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() + }; + + let mut instance = self + .runtime + .instantiate(&module, instance_config, Some(network_host_fns)) + .map_err(|e| anyhow::anyhow!("WASM instantiation failed: {}", e))?; + + let initial_fuel = instance.fuel_remaining(); + + let result = instance + .call_return_i64("get_routes") + .map_err(|e| anyhow::anyhow!("WASM get_routes call failed: {}", e))?; + + let out_len = (result >> 32) as i32; + let out_ptr = (result & 0xFFFF_FFFF) as i32; + + let result_data = if out_ptr > 0 && out_len > 0 { + instance + .read_memory(out_ptr as usize, out_len as usize) + .map_err(|e| { + anyhow::anyhow!("failed to read WASM memory for get_routes output: {}", e) + })? + } else { + Vec::new() + }; + + let fuel_consumed = match (initial_fuel, instance.fuel_remaining()) { + (Some(initial), Some(remaining)) => Some(initial.saturating_sub(remaining)), + _ => None, + }; + + let metrics = ExecutionMetrics { + execution_time_ms: start.elapsed().as_millis(), + memory_used_bytes: instance.memory().data_size(instance.store()) as u64, + network_requests_made: instance.network_requests_made(), + fuel_consumed, + }; + + info!( + module = module_path, + result_bytes = result_data.len(), + execution_time_ms = metrics.execution_time_ms, + "WASM get_routes completed" + ); + + Ok((result_data, metrics)) + } + + #[allow(dead_code)] + pub fn execute_handle_route( + &self, + module_path: &str, + network_policy: &NetworkPolicy, + sandbox_policy: &SandboxPolicy, + request_data: &[u8], + ) -> Result<(Vec, ExecutionMetrics)> { + let start = Instant::now(); + + let module = self + .load_module(module_path) + .context("Failed to load WASM module")?; + + let network_host_fns = Arc::new(NetworkHostFunctions::all()); + + let instance_config = InstanceConfig { + network_policy: network_policy.clone(), + sandbox_policy: sandbox_policy.clone(), + exec_policy: ExecPolicy::default(), + time_policy: TimePolicy::default(), + audit_logger: None, + memory_export: "memory".to_string(), + challenge_id: module_path.to_string(), + validator_id: "validator".to_string(), + restart_id: String::new(), + config_version: 0, + storage_host_config: StorageHostConfig { + allow_direct_writes: true, + require_consensus: false, + ..self.config.storage_host_config.clone() + }, + storage_backend: Arc::clone(&self.config.storage_backend), + fixed_timestamp_ms: None, + consensus_policy: ConsensusPolicy::default(), + terminal_policy: TerminalPolicy::default(), + llm_policy: match &self.config.chutes_api_key { + Some(key) => LlmPolicy::with_api_key(key.clone()), + None => LlmPolicy::default(), + }, + ..Default::default() + }; + + let mut instance = self + .runtime + .instantiate(&module, instance_config, Some(network_host_fns)) + .map_err(|e| anyhow::anyhow!("WASM instantiation failed: {}", e))?; + + let initial_fuel = instance.fuel_remaining(); + + let ptr = self.allocate_input(&mut instance, request_data)?; + + instance + .write_memory(ptr as usize, request_data) + .map_err(|e| anyhow::anyhow!("Failed to write request data to WASM memory: {}", e))?; + + let result = instance + .call_i32_i32_return_i64("handle_route", ptr, request_data.len() as i32) + .map_err(|e| match &e { + WasmRuntimeError::FuelExhausted => { + anyhow::anyhow!("WASM execution exceeded fuel limit") + } + WasmRuntimeError::Execution(msg) if msg.contains("timeout") => { + anyhow::anyhow!("WASM execution timed out") + } + _ => anyhow::anyhow!("WASM handle_route call failed: {}", e), + })?; + + let out_len = (result >> 32) as i32; + let out_ptr = (result & 0xFFFF_FFFF) as i32; + + let result_data = if out_ptr > 0 && out_len > 0 { + instance + .read_memory(out_ptr as usize, out_len as usize) + .map_err(|e| { + anyhow::anyhow!("failed to read WASM memory for handle_route output: {}", e) + })? + } else { + Vec::new() + }; + + let fuel_consumed = match (initial_fuel, instance.fuel_remaining()) { + (Some(initial), Some(remaining)) => Some(initial.saturating_sub(remaining)), + _ => None, + }; + + let metrics = ExecutionMetrics { + execution_time_ms: start.elapsed().as_millis(), + memory_used_bytes: instance.memory().data_size(instance.store()) as u64, + network_requests_made: instance.network_requests_made(), + fuel_consumed, + }; + + info!( + module = module_path, + result_bytes = result_data.len(), + execution_time_ms = metrics.execution_time_ms, + "WASM handle_route completed" + ); + + Ok((result_data, metrics)) + } + fn load_module(&self, module_path: &str) -> Result> { { let cache = self.module_cache.read(); diff --git a/crates/bittensor-integration/src/validator_sync.rs b/crates/bittensor-integration/src/validator_sync.rs index 39763ec4..bf31f848 100644 --- a/crates/bittensor-integration/src/validator_sync.rs +++ b/crates/bittensor-integration/src/validator_sync.rs @@ -90,6 +90,13 @@ impl ValidatorSync { // Parse validators and all hotkeys from metagraph let (bt_validators, all_hotkeys) = self.parse_metagraph(metagraph)?; + + // Extract UID 0 hotkey before dropping client borrow + let uid0_hotkey = metagraph.neurons.get(&0).map(|neuron| { + let hotkey_bytes: &[u8; 32] = neuron.hotkey.as_ref(); + Hotkey(*hotkey_bytes) + }); + drop(client); // Release lock // Update registered hotkeys in state (all miners + validators) @@ -101,6 +108,13 @@ impl ValidatorSync { // Update state with validators let result = self.update_state(state, bt_validators, banned_validators); + // Resolve subnet owner from UID 0 + if let Some(hotkey) = uid0_hotkey { + let mut state_guard = state.write(); + state_guard.sudo_key = hotkey; + debug!("Subnet owner set to UID 0 hotkey: {}", state_guard.sudo_key); + } + // Update last sync block self.last_sync_block = state.read().block_height; diff --git a/crates/challenge-sdk-wasm/src/lib.rs b/crates/challenge-sdk-wasm/src/lib.rs index 820c7d4f..15f104da 100644 --- a/crates/challenge-sdk-wasm/src/lib.rs +++ b/crates/challenge-sdk-wasm/src/lib.rs @@ -13,6 +13,7 @@ pub use types::{ }; pub use types::{ContainerRunRequest, ContainerRunResponse}; pub use types::{EvaluationInput, EvaluationOutput}; +pub use types::{WasmRouteDefinition, WasmRouteRequest, WasmRouteResponse}; pub trait Challenge { fn name(&self) -> &'static str; @@ -33,6 +34,14 @@ pub trait Challenge { } fn configure(&self, _config: &[u8]) {} + + fn routes(&self) -> alloc::vec::Vec { + alloc::vec::Vec::new() + } + + fn handle_route(&self, _request: &[u8]) -> alloc::vec::Vec { + alloc::vec::Vec::new() + } } /// Pack a pointer and length into a single i64 value. @@ -46,7 +55,8 @@ pub fn pack_ptr_len(ptr: i32, len: i32) -> i64 { /// Register a [`Challenge`] implementation and export the required WASM ABI /// functions (`evaluate`, `validate`, `get_name`, `get_version`, -/// `generate_task`, `setup_environment`, `get_tasks`, `configure`, and `alloc`). +/// `generate_task`, `setup_environment`, `get_tasks`, `configure`, +/// `get_routes`, `handle_route`, and `alloc`). /// /// The type must provide a `const fn new() -> Self` constructor so that the /// challenge instance can be placed in a `static`. @@ -213,5 +223,39 @@ macro_rules! register_challenge { <$ty as $crate::Challenge>::configure(&_CHALLENGE, slice); 1 } + + #[no_mangle] + pub extern "C" fn get_routes() -> i64 { + let output = <$ty as $crate::Challenge>::routes(&_CHALLENGE); + if output.is_empty() { + return $crate::pack_ptr_len(0, 0); + } + let ptr = $crate::alloc_impl::sdk_alloc(output.len()); + if ptr.is_null() { + return $crate::pack_ptr_len(0, 0); + } + unsafe { + core::ptr::copy_nonoverlapping(output.as_ptr(), ptr, output.len()); + } + $crate::pack_ptr_len(ptr as i32, output.len() as i32) + } + + #[no_mangle] + pub extern "C" fn handle_route(req_ptr: i32, req_len: i32) -> i64 { + let slice = + unsafe { core::slice::from_raw_parts(req_ptr as *const u8, req_len as usize) }; + let output = <$ty as $crate::Challenge>::handle_route(&_CHALLENGE, slice); + if output.is_empty() { + return $crate::pack_ptr_len(0, 0); + } + let ptr = $crate::alloc_impl::sdk_alloc(output.len()); + if ptr.is_null() { + return $crate::pack_ptr_len(0, 0); + } + unsafe { + core::ptr::copy_nonoverlapping(output.as_ptr(), ptr, output.len()); + } + $crate::pack_ptr_len(ptr as i32, output.len() as i32) + } }; } diff --git a/crates/challenge-sdk-wasm/src/types.rs b/crates/challenge-sdk-wasm/src/types.rs index 3c3763fb..acc8f8c5 100644 --- a/crates/challenge-sdk-wasm/src/types.rs +++ b/crates/challenge-sdk-wasm/src/types.rs @@ -141,3 +141,27 @@ pub struct ContainerRunResponse { pub stderr: Vec, pub duration_ms: u64, } + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WasmRouteDefinition { + pub method: String, + pub path: String, + pub description: String, + pub requires_auth: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WasmRouteRequest { + pub method: String, + pub path: String, + pub params: Vec<(String, String)>, + pub query: Vec<(String, String)>, + pub body: Vec, + pub auth_hotkey: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WasmRouteResponse { + pub status: u16, + pub body: Vec, +}