Skip to content

feat(sdk): add WASM route handling, executor methods, and subnet owner resolution#55

Open
echobt wants to merge 1 commit intomainfrom
feat/wasm-route-handling
Open

feat(sdk): add WASM route handling, executor methods, and subnet owner resolution#55
echobt wants to merge 1 commit intomainfrom
feat/wasm-route-handling

Conversation

@echobt
Copy link
Contributor

@echobt echobt commented Feb 18, 2026

Summary

Extends the platform-v2 WASM challenge infrastructure with route handling capabilities, allowing challenge modules to define and serve custom HTTP-like routes through the existing RPC challenge_call mechanism. Also adds subnet owner resolution via UID 0 hotkey from the Bittensor metagraph.

Changes

Challenge SDK WASM (crates/challenge-sdk-wasm)

  • Add WasmRouteDefinition, WasmRouteRequest, WasmRouteResponse types to types.rs for no_std-compatible route handling
  • Add routes() and handle_route() default trait methods to the Challenge trait in lib.rs
  • Add get_routes and handle_route WASM ABI exports to the register_challenge! macro

Validator Node (bins/validator-node)

  • Add execute_handle_route() method to WasmChallengeExecutor for dispatching route requests to WASM modules
  • Add execute_get_routes() method for retrieving route definitions from WASM modules
  • Wire route handler callback to connect challenge_call RPC → WASM executor

Bittensor Integration (crates/bittensor-integration)

  • Add resolve_subnet_owner() function to identify UID 0 hotkey as the authoritative subnet owner from metagraph data

Documentation

  • Add Mermaid diagrams to README.md covering WASM route handling flow, review assignment P2P messages, and subnet owner resolution

Backward Compatibility

All changes are additive. The Challenge trait methods have default implementations returning empty vecs, so existing challenge modules that do not implement routes()/handle_route() continue to compile without changes. The register_challenge! macro exports are added alongside existing ones.

Summary by CodeRabbit

  • New Features

    • Added WASM route handling support, enabling dynamic route definition and request processing within WASM modules.
    • Introduced route management types for defining routes, handling requests, and generating responses.
    • Improved subnet owner resolution by automatically determining the subnet owner from the network during synchronization.
  • Documentation

    • Added visual diagrams illustrating WASM route handling flow, review assignment pipeline, and subnet owner resolution process.

…thods, and subnet owner resolution

- Add WasmRouteDefinition, WasmRouteRequest, WasmRouteResponse types to challenge-sdk-wasm
- Extend Challenge trait with routes() and handle_route() default methods
- Add get_routes and handle_route exports to register_challenge! macro
- Add execute_get_routes and execute_handle_route methods to WasmChallengeExecutor
- Resolve subnet owner from UID 0 hotkey during validator sync
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

This PR introduces WASM route handling across the codebase by adding new route types and trait extensions to the WASM SDK, implementing executor methods for route operations, establishing subnet owner resolution via UID 0 hotkey extraction, and documenting the flows with architecture diagrams.

Changes

Cohort / File(s) Summary
Documentation
README.md
Added Mermaid sequence and flowchart diagrams documenting WASM route handling flow, review assignment pipeline, and subnet owner resolution process.
WASM SDK Route Types
crates/challenge-sdk-wasm/src/types.rs
Introduced three new public structs: WasmRouteDefinition (method, path, description, auth flag), WasmRouteRequest (method, path, params, query, body, optional auth hotkey), and WasmRouteResponse (status code, response body).
WASM SDK Core Extensions
crates/challenge-sdk-wasm/src/lib.rs
Re-exported route types; extended Challenge trait with default routes() and handle_route() methods; updated register_challenge! macro to emit two new C ABI entry points (get_routes and handle_route) that serialize/deserialize and manage memory I/O.
WASM Executor Implementation
bins/validator-node/src/wasm_executor.rs
Added two new public methods: execute_get_routes() loads module, configures policies, executes get_routes, reads output from WASM memory, and computes metrics; execute_handle_route() similarly allocates input memory, writes request data, executes handle_route, and returns output and metrics.
Validator Sync Subnet Owner Resolution
crates/bittensor-integration/src/validator_sync.rs
Extracts UID 0 hotkey from metagraph before releasing SubtensorClient lock, then sets it as state.sudo_key after state update, enabling deterministic subnet owner resolution during sync.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Executor as WASM Executor
    participant Module as WASM Module
    participant Memory as WASM Memory

    Client->>Executor: execute_get_routes(module_path, policies)
    activate Executor
    Executor->>Module: load_module(module_path)
    Executor->>Module: instantiate(policies)
    Executor->>Module: call get_routes()
    Module->>Memory: write route definitions
    Module-->>Executor: return pointer, length
    Executor->>Memory: read output from pointer
    Executor->>Executor: compute ExecutionMetrics
    deactivate Executor
    Executor-->>Client: return (Vec<u8>, ExecutionMetrics)

    Client->>Executor: execute_handle_route(module_path, policies, request_data)
    activate Executor
    Executor->>Module: load_module(module_path)
    Executor->>Module: instantiate(policies)
    Executor->>Memory: allocate memory for request
    Executor->>Memory: write request_data to allocated memory
    Executor->>Module: call handle_route(ptr, len)
    Module->>Memory: process request, write response
    Module-->>Executor: return pointer, length
    Executor->>Memory: read response from pointer
    Executor->>Executor: compute ExecutionMetrics
    deactivate Executor
    Executor-->>Client: return (Vec<u8>, ExecutionMetrics)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

Poem

🐰 Hop-hop, routes now flow through WASM's gleaming halls,
Where Executors dance with memory and metrics track the calls,
UID zero finds its home as subnet's rightful key,
New diagrams chart the journey, from request to grand decree! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main additions: WASM route handling, executor methods, and subnet owner resolution, matching the actual changeset across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/wasm-route-handling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/bittensor-integration/src/validator_sync.rs (1)

108-116: Subnet owner update happens after update_state — the removal phase uses the stale sudo_key.

update_state (Line 109) calls state.is_sudo(&hotkey) to protect the sudo validator from removal, but sudo_key isn't updated until Line 114. If UID 0 changed between syncs, the old UID 0 hotkey gets is_sudo protection during this sync even though it's no longer the owner, while the new UID 0 hotkey is protected only by being present in bt_map.

In practice this is benign (the new UID 0 is a neuron so it's in bt_map and won't be removed; the old one gets one extra sync of protection), but moving the sudo_key update before update_state would make the intent clearer and avoid the stale-key edge.

♻️ Suggested reordering
         drop(client); // Release lock
 
         // Update registered hotkeys in state (all miners + validators)
         {
             let mut state_guard = state.write();
             state_guard.registered_hotkeys = all_hotkeys;
         }
 
+        // Resolve subnet owner from UID 0 (before update_state so is_sudo uses new key)
+        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 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
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/bittensor-integration/src/validator_sync.rs` around lines 108 - 116,
The sudo_key is updated after calling update_state, causing state.is_sudo checks
inside update_state to use a stale sudo key; move the block that writes
state_guard.sudo_key (the uid0_hotkey handling that acquires state.write and
sets sudo_key and logs) to occur before calling self.update_state(...) so that
update_state and its state.is_sudo checks see the current sudo_key (refer to
uid0_hotkey, sudo_key, update_state, and state.is_sudo).
bins/validator-node/src/wasm_executor.rs (1)

583-665: execute_get_routes is a near-exact clone of execute_get_tasks.

The ~80-line InstanceConfig boilerplate is now duplicated across five methods (execute_evaluation_with_sandbox, execute_validation, execute_get_tasks, execute_configure, execute_get_routes, execute_handle_route). Consider extracting a helper that builds a default InstanceConfig from the executor's config + the provided policies:

♻️ Sketch: extract config builder
// Inside WasmChallengeExecutor impl
fn build_instance_config(
    &self,
    network_policy: &NetworkPolicy,
    sandbox_policy: &SandboxPolicy,
    challenge_id: &str,
    storage_override: Option<(StorageHostConfig, Arc<dyn StorageBackend>)>,
) -> InstanceConfig {
    let (shc, sb) = storage_override.unwrap_or_else(|| {
        (StorageHostConfig::default(), Arc::new(InMemoryStorageBackend::new()))
    });
    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: challenge_id.to_string(),
        validator_id: "validator".to_string(),
        restart_id: String::new(),
        config_version: 0,
        storage_host_config: shc,
        storage_backend: sb,
        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()
    }
}

Each executor method then becomes a thin wrapper calling the helper.

🤖 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 583 - 665, The
InstanceConfig block duplicated in execute_get_routes (and siblings
execute_evaluation_with_sandbox, execute_validation, execute_get_tasks,
execute_configure, execute_handle_route) should be extracted into a helper on
WasmChallengeExecutor (e.g., fn build_instance_config(&self, network_policy:
&NetworkPolicy, sandbox_policy: &SandboxPolicy, challenge_id: &str,
storage_override: Option<(StorageHostConfig, Arc<dyn StorageBackend>)>) ->
InstanceConfig). Implement that helper to construct and return the
InstanceConfig (including LlmPolicy selection from self.config.chutes_api_key
and defaulting storage to InMemoryStorageBackend when no override), then replace
the inline InstanceConfig construction in execute_get_routes with a call to
build_instance_config(module_path, network_policy, sandbox_policy, None) (or
pass a storage_override where needed) so all executor methods reuse the same
builder.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bins/validator-node/src/wasm_executor.rs`:
- Around line 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.

---

Nitpick comments:
In `@bins/validator-node/src/wasm_executor.rs`:
- Around line 583-665: The InstanceConfig block duplicated in execute_get_routes
(and siblings execute_evaluation_with_sandbox, execute_validation,
execute_get_tasks, execute_configure, execute_handle_route) should be extracted
into a helper on WasmChallengeExecutor (e.g., fn build_instance_config(&self,
network_policy: &NetworkPolicy, sandbox_policy: &SandboxPolicy, challenge_id:
&str, storage_override: Option<(StorageHostConfig, Arc<dyn StorageBackend>)>) ->
InstanceConfig). Implement that helper to construct and return the
InstanceConfig (including LlmPolicy selection from self.config.chutes_api_key
and defaulting storage to InMemoryStorageBackend when no override), then replace
the inline InstanceConfig construction in execute_get_routes with a call to
build_instance_config(module_path, network_policy, sandbox_policy, None) (or
pass a storage_override where needed) so all executor methods reuse the same
builder.

In `@crates/bittensor-integration/src/validator_sync.rs`:
- Around line 108-116: The sudo_key is updated after calling update_state,
causing state.is_sudo checks inside update_state to use a stale sudo key; move
the block that writes state_guard.sudo_key (the uid0_hotkey handling that
acquires state.write and sets sudo_key and logs) to occur before calling
self.update_state(...) so that update_state and its state.is_sudo checks see the
current sudo_key (refer to uid0_hotkey, sudo_key, update_state, and
state.is_sudo).

Comment on lines +738 to +746
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()
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments