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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
187 changes: 187 additions & 0 deletions bins/validator-node/src/wasm_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>, 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<u8>, 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()
};
Comment on lines +738 to +746
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

No output size limit on route response deserialization.

execute_evaluation applies MAX_EVALUATION_OUTPUT_SIZE (64 MiB) via bincode::DefaultOptions::new().with_limit(...) when deserializing evaluation output (Line 255-260). The route methods return raw Vec<u8> without any size cap — a malicious or buggy WASM module could return an arbitrarily large out_len, causing the host to allocate unbounded memory in read_memory.

Consider validating out_len against a reasonable upper bound before reading.

🛡️ Proposed guard
+        const MAX_ROUTE_OUTPUT_SIZE: usize = 64 * 1024 * 1024; // 64 MiB
+
         let result_data = if out_ptr > 0 && out_len > 0 {
+            if out_len as usize > MAX_ROUTE_OUTPUT_SIZE {
+                return Err(anyhow::anyhow!(
+                    "WASM handle_route output exceeds maximum size ({} bytes)",
+                    out_len
+                ));
+            }
             instance
                 .read_memory(out_ptr as usize, out_len as usize)

Apply the same check in execute_get_routes (Line 635).

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

In `@bins/validator-node/src/wasm_executor.rs` around lines 738 - 746, The
route-output branch in execute_get_routes/handle_route reads unbounded memory
(the out_len from WASM) — add a size check before calling instance.read_memory:
validate out_len is non-negative and <= a defined max (reuse
MAX_EVALUATION_OUTPUT_SIZE or introduce MAX_ROUTE_OUTPUT_SIZE ~64 MiB), and
return an error (anyhow::anyhow! with clear context referencing
handle_route/output size) if it exceeds the limit; apply the same guard where
route Vec<u8> is deserialized (the code around result_data and in
execute_get_routes/execute_evaluation) so the host never allocates or reads more
than the allowed limit.


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<Arc<WasmModule>> {
{
let cache = self.module_cache.read();
Expand Down
14 changes: 14 additions & 0 deletions crates/bittensor-integration/src/validator_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;

Expand Down
46 changes: 45 additions & 1 deletion crates/challenge-sdk-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,6 +34,14 @@ pub trait Challenge {
}

fn configure(&self, _config: &[u8]) {}

fn routes(&self) -> alloc::vec::Vec<u8> {
alloc::vec::Vec::new()
}

fn handle_route(&self, _request: &[u8]) -> alloc::vec::Vec<u8> {
alloc::vec::Vec::new()
}
}

/// Pack a pointer and length into a single i64 value.
Expand All @@ -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`.
Expand Down Expand Up @@ -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)
}
};
}
24 changes: 24 additions & 0 deletions crates/challenge-sdk-wasm/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,27 @@ pub struct ContainerRunResponse {
pub stderr: Vec<u8>,
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<u8>,
pub auth_hotkey: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WasmRouteResponse {
pub status: u16,
pub body: Vec<u8>,
}
Loading