Skip to content

Commit df4db95

Browse files
authored
feat(challenge-sdk-wasm): add WASM Guest SDK crate for building challenges (#11)
Introduce the new `platform-challenge-sdk-wasm` crate (`crates/challenge-sdk-wasm/`) targeting `wasm32-unknown-unknown`. This lightweight, `no_std`-compatible SDK provides everything challenge developers need to build WASM challenges without pulling in native dependencies (sled, tokio, axum, etc.). Core components: - **Challenge trait & register_challenge! macro** (`src/lib.rs`): Defines the `Challenge` trait (evaluate, validate, name, version) and a declarative macro that generates `extern "C"` WASM ABI export shims (evaluate, validate, get_name, get_version) from a user trait impl, handling serialization via bincode and memory allocation for returning data to the host. - **Host function imports** (`src/host_functions.rs`): Declares extern imports from `platform_network` (http_get, http_post, dns_resolve) and `platform_storage` (storage_get, storage_set) namespaces matching the wasm-runtime-interface contract, with safe Rust wrappers around each. - **Shared types** (`src/types.rs`): Defines `EvaluationInput` and `EvaluationOutput` structs with serde Serialize/Deserialize, mirroring the native SDK types without native-only dependencies. - **Bump allocator** (`src/alloc_impl.rs`): Provides a 1 MiB arena bump allocator for WASM linear memory, plus an exported `alloc(size) -> ptr` function so the host can allocate guest memory for data passing. - **Build script update** (`scripts/build-wasm.sh`): Extended to accept an optional crate name argument for building challenge crates with this SDK, while preserving the existing chain-runtime build as the default. - **Workspace integration** (`Cargo.toml`, `Cargo.lock`): Added `crates/challenge-sdk-wasm` to workspace members and updated the lockfile with the new crate and its minimal dependencies (serde, bincode).
1 parent 041b92b commit df4db95

File tree

8 files changed

+389
-22
lines changed

8 files changed

+389
-22
lines changed

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ members = [
1313
"crates/secure-container-runtime",
1414
"crates/p2p-consensus",
1515
"crates/wasm-runtime-interface",
16+
"crates/challenge-sdk-wasm",
1617
"bins/validator-node",
1718
"bins/utils",
1819
"bins/mock-subtensor",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "platform-challenge-sdk-wasm"
3+
version.workspace = true
4+
edition.workspace = true
5+
description = "WASM Guest SDK for building challenges targeting wasm32-unknown-unknown"
6+
7+
[lib]
8+
crate-type = ["cdylib", "rlib"]
9+
10+
[dependencies]
11+
serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] }
12+
bincode = { version = "1.3", default-features = false }
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use core::cell::UnsafeCell;
2+
3+
const ARENA_SIZE: usize = 1024 * 1024; // 1 MiB
4+
5+
struct BumpAllocator {
6+
arena: UnsafeCell<[u8; ARENA_SIZE]>,
7+
offset: UnsafeCell<usize>,
8+
}
9+
10+
unsafe impl Sync for BumpAllocator {}
11+
12+
impl BumpAllocator {
13+
const fn new() -> Self {
14+
Self {
15+
arena: UnsafeCell::new([0u8; ARENA_SIZE]),
16+
offset: UnsafeCell::new(0),
17+
}
18+
}
19+
20+
fn alloc(&self, size: usize, align: usize) -> *mut u8 {
21+
unsafe {
22+
let offset = &mut *self.offset.get();
23+
let aligned = (*offset + align - 1) & !(align - 1);
24+
let new_offset = aligned + size;
25+
if new_offset > ARENA_SIZE {
26+
return core::ptr::null_mut();
27+
}
28+
*offset = new_offset;
29+
let arena = &mut *self.arena.get();
30+
arena.as_mut_ptr().add(aligned)
31+
}
32+
}
33+
34+
fn reset(&self) {
35+
unsafe {
36+
*self.offset.get() = 0;
37+
}
38+
}
39+
}
40+
41+
static ALLOCATOR: BumpAllocator = BumpAllocator::new();
42+
43+
#[no_mangle]
44+
pub extern "C" fn alloc(size: i32) -> i32 {
45+
let ptr = ALLOCATOR.alloc(size as usize, 8);
46+
if ptr.is_null() {
47+
0
48+
} else {
49+
ptr as i32
50+
}
51+
}
52+
53+
pub fn sdk_alloc(size: usize) -> *mut u8 {
54+
ALLOCATOR.alloc(size, 8)
55+
}
56+
57+
pub fn sdk_reset() {
58+
ALLOCATOR.reset();
59+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
use alloc::vec;
2+
use alloc::vec::Vec;
3+
4+
#[link(wasm_import_module = "platform_network")]
5+
extern "C" {
6+
fn http_get(req_ptr: i32, req_len: i32, resp_ptr: i32) -> i32;
7+
fn http_post(req_ptr: i32, req_len: i32, resp_ptr: i32, resp_len: i32, extra: i32) -> i32;
8+
fn dns_resolve(req_ptr: i32, req_len: i32, resp_ptr: i32) -> i32;
9+
}
10+
11+
#[link(wasm_import_module = "platform_storage")]
12+
extern "C" {
13+
fn storage_get(key_ptr: i32, key_len: i32, value_ptr: i32) -> i32;
14+
fn storage_set(key_ptr: i32, key_len: i32, value_ptr: i32, value_len: i32) -> i32;
15+
}
16+
17+
pub fn host_http_get(request: &[u8]) -> Result<Vec<u8>, i32> {
18+
let mut response_buf = vec![0u8; 65536];
19+
let status = unsafe {
20+
http_get(
21+
request.as_ptr() as i32,
22+
request.len() as i32,
23+
response_buf.as_mut_ptr() as i32,
24+
)
25+
};
26+
if status < 0 {
27+
return Err(status);
28+
}
29+
response_buf.truncate(status as usize);
30+
Ok(response_buf)
31+
}
32+
33+
pub fn host_http_post(request: &[u8], body: &[u8]) -> Result<Vec<u8>, i32> {
34+
let mut response_buf = vec![0u8; 65536];
35+
let status = unsafe {
36+
http_post(
37+
request.as_ptr() as i32,
38+
request.len() as i32,
39+
response_buf.as_mut_ptr() as i32,
40+
response_buf.len() as i32,
41+
body.len() as i32,
42+
)
43+
};
44+
if status < 0 {
45+
return Err(status);
46+
}
47+
response_buf.truncate(status as usize);
48+
Ok(response_buf)
49+
}
50+
51+
pub fn host_dns_resolve(request: &[u8]) -> Result<Vec<u8>, i32> {
52+
let mut response_buf = vec![0u8; 4096];
53+
let status = unsafe {
54+
dns_resolve(
55+
request.as_ptr() as i32,
56+
request.len() as i32,
57+
response_buf.as_mut_ptr() as i32,
58+
)
59+
};
60+
if status < 0 {
61+
return Err(status);
62+
}
63+
response_buf.truncate(status as usize);
64+
Ok(response_buf)
65+
}
66+
67+
pub fn host_storage_get(key: &[u8]) -> Result<Vec<u8>, i32> {
68+
let mut value_buf = vec![0u8; 65536];
69+
let status = unsafe {
70+
storage_get(
71+
key.as_ptr() as i32,
72+
key.len() as i32,
73+
value_buf.as_mut_ptr() as i32,
74+
)
75+
};
76+
if status < 0 {
77+
return Err(status);
78+
}
79+
value_buf.truncate(status as usize);
80+
Ok(value_buf)
81+
}
82+
83+
pub fn host_storage_set(key: &[u8], value: &[u8]) -> Result<(), i32> {
84+
let status = unsafe {
85+
storage_set(
86+
key.as_ptr() as i32,
87+
key.len() as i32,
88+
value.as_ptr() as i32,
89+
value.len() as i32,
90+
)
91+
};
92+
if status < 0 {
93+
return Err(status);
94+
}
95+
Ok(())
96+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#![no_std]
2+
3+
extern crate alloc;
4+
5+
pub mod alloc_impl;
6+
pub mod host_functions;
7+
pub mod types;
8+
9+
pub use types::{EvaluationInput, EvaluationOutput};
10+
11+
pub trait Challenge {
12+
fn name(&self) -> &'static str;
13+
fn version(&self) -> &'static str;
14+
fn evaluate(&self, input: EvaluationInput) -> EvaluationOutput;
15+
fn validate(&self, input: EvaluationInput) -> bool;
16+
}
17+
18+
/// Pack a pointer and length into a single i64 value.
19+
///
20+
/// The high 32 bits hold the length and the low 32 bits hold the pointer.
21+
/// The host runtime uses this convention to locate serialized data in WASM
22+
/// linear memory.
23+
pub fn pack_ptr_len(ptr: i32, len: i32) -> i64 {
24+
((len as i64) << 32) | ((ptr as u32) as i64)
25+
}
26+
27+
/// Register a [`Challenge`] implementation and export the required WASM ABI
28+
/// functions (`evaluate`, `validate`, `get_name`, `get_version`, and `alloc`).
29+
///
30+
/// # Usage
31+
///
32+
/// ```ignore
33+
/// struct MyChallenge;
34+
///
35+
/// impl platform_challenge_sdk_wasm::Challenge for MyChallenge {
36+
/// fn name(&self) -> &'static str { "my-challenge" }
37+
/// fn version(&self) -> &'static str { "0.1.0" }
38+
/// fn evaluate(&self, input: EvaluationInput) -> EvaluationOutput {
39+
/// EvaluationOutput::success(100, "ok")
40+
/// }
41+
/// fn validate(&self, input: EvaluationInput) -> bool { true }
42+
/// }
43+
///
44+
/// platform_challenge_sdk_wasm::register_challenge!(MyChallenge);
45+
/// ```
46+
#[macro_export]
47+
macro_rules! register_challenge {
48+
($ty:ty) => {
49+
static _CHALLENGE: $ty = <$ty as Default>::default();
50+
51+
#[no_mangle]
52+
pub extern "C" fn evaluate(agent_ptr: i32, agent_len: i32) -> i64 {
53+
let slice =
54+
unsafe { core::slice::from_raw_parts(agent_ptr as *const u8, agent_len as usize) };
55+
let input: $crate::EvaluationInput = match bincode::deserialize(slice) {
56+
Ok(v) => v,
57+
Err(_) => {
58+
return $crate::pack_ptr_len(0, 0);
59+
}
60+
};
61+
let output = <$ty as $crate::Challenge>::evaluate(&_CHALLENGE, input);
62+
let encoded = match bincode::serialize(&output) {
63+
Ok(v) => v,
64+
Err(_) => {
65+
return $crate::pack_ptr_len(0, 0);
66+
}
67+
};
68+
let ptr = $crate::alloc_impl::sdk_alloc(encoded.len());
69+
if ptr.is_null() {
70+
return $crate::pack_ptr_len(0, 0);
71+
}
72+
unsafe {
73+
core::ptr::copy_nonoverlapping(encoded.as_ptr(), ptr, encoded.len());
74+
}
75+
$crate::pack_ptr_len(ptr as i32, encoded.len() as i32)
76+
}
77+
78+
#[no_mangle]
79+
pub extern "C" fn validate(agent_ptr: i32, agent_len: i32) -> i32 {
80+
let slice =
81+
unsafe { core::slice::from_raw_parts(agent_ptr as *const u8, agent_len as usize) };
82+
let input: $crate::EvaluationInput = match bincode::deserialize(slice) {
83+
Ok(v) => v,
84+
Err(_) => return 0,
85+
};
86+
if <$ty as $crate::Challenge>::validate(&_CHALLENGE, input) {
87+
1
88+
} else {
89+
0
90+
}
91+
}
92+
93+
#[no_mangle]
94+
pub extern "C" fn get_name() -> i32 {
95+
let name = <$ty as $crate::Challenge>::name(&_CHALLENGE);
96+
let ptr = $crate::alloc_impl::sdk_alloc(4 + name.len());
97+
if ptr.is_null() {
98+
return 0;
99+
}
100+
let len_bytes = (name.len() as u32).to_le_bytes();
101+
unsafe {
102+
core::ptr::copy_nonoverlapping(len_bytes.as_ptr(), ptr, 4);
103+
core::ptr::copy_nonoverlapping(name.as_ptr(), ptr.add(4), name.len());
104+
}
105+
ptr as i32
106+
}
107+
108+
#[no_mangle]
109+
pub extern "C" fn get_version() -> i32 {
110+
let ver = <$ty as $crate::Challenge>::version(&_CHALLENGE);
111+
let ptr = $crate::alloc_impl::sdk_alloc(4 + ver.len());
112+
if ptr.is_null() {
113+
return 0;
114+
}
115+
let len_bytes = (ver.len() as u32).to_le_bytes();
116+
unsafe {
117+
core::ptr::copy_nonoverlapping(len_bytes.as_ptr(), ptr, 4);
118+
core::ptr::copy_nonoverlapping(ver.as_ptr(), ptr.add(4), ver.len());
119+
}
120+
ptr as i32
121+
}
122+
};
123+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use alloc::string::String;
2+
use alloc::vec::Vec;
3+
use serde::{Deserialize, Serialize};
4+
5+
#[derive(Clone, Debug, Serialize, Deserialize)]
6+
pub struct EvaluationInput {
7+
pub agent_data: Vec<u8>,
8+
pub challenge_id: String,
9+
pub params: Vec<u8>,
10+
}
11+
12+
#[derive(Clone, Debug, Serialize, Deserialize)]
13+
pub struct EvaluationOutput {
14+
pub score: i64,
15+
pub valid: bool,
16+
pub message: String,
17+
}
18+
19+
impl EvaluationOutput {
20+
pub fn success(score: i64, message: &str) -> Self {
21+
Self {
22+
score,
23+
valid: true,
24+
message: String::from(message),
25+
}
26+
}
27+
28+
pub fn failure(message: &str) -> Self {
29+
Self {
30+
score: 0,
31+
valid: false,
32+
message: String::from(message),
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)