442 lines
14 KiB
Rust
442 lines
14 KiB
Rust
//! Task execution system with sandboxing and verification
|
|
|
|
use wasm_bindgen::prelude::*;
|
|
use serde::{Serialize, Deserialize};
|
|
use uuid::Uuid;
|
|
use aes_gcm::{
|
|
aead::{Aead, KeyInit},
|
|
Aes256Gcm, Nonce,
|
|
};
|
|
use rand::rngs::OsRng;
|
|
use sha2::{Sha256, Digest};
|
|
use rustc_hash::FxHashMap; // 30-50% faster than std HashMap
|
|
use std::collections::BinaryHeap;
|
|
use std::cmp::Ordering;
|
|
|
|
/// Task types supported by the network
|
|
#[wasm_bindgen]
|
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
|
|
pub enum TaskType {
|
|
/// Vector search in HNSW index
|
|
VectorSearch,
|
|
/// Vector insertion
|
|
VectorInsert,
|
|
/// Generate embeddings
|
|
Embedding,
|
|
/// Semantic task-to-agent matching
|
|
SemanticMatch,
|
|
/// Neural network inference
|
|
NeuralInference,
|
|
/// AES encryption/decryption
|
|
Encryption,
|
|
/// Data compression
|
|
Compression,
|
|
/// Custom WASM module (requires verification)
|
|
CustomWasm,
|
|
}
|
|
|
|
/// Task priority levels
|
|
#[wasm_bindgen]
|
|
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
|
pub enum TaskPriority {
|
|
Low = 0,
|
|
Normal = 1,
|
|
High = 2,
|
|
}
|
|
|
|
/// A task submitted to the network
|
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
|
pub struct Task {
|
|
pub id: String,
|
|
pub task_type: TaskType,
|
|
pub encrypted_payload: Vec<u8>,
|
|
pub payload_hash: [u8; 32],
|
|
pub submitter_id: String,
|
|
pub submitter_pubkey: Vec<u8>,
|
|
pub priority: TaskPriority,
|
|
pub base_reward: u64,
|
|
pub max_credits: u64,
|
|
pub redundancy: u8,
|
|
pub created_at: u64,
|
|
pub expires_at: u64,
|
|
pub signature: Vec<u8>,
|
|
}
|
|
|
|
/// Result of task execution
|
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
|
pub struct TaskResult {
|
|
pub task_id: String,
|
|
pub encrypted_result: Vec<u8>,
|
|
pub result_hash: [u8; 32],
|
|
pub worker_id: String,
|
|
pub execution_time_ms: u64,
|
|
pub signature: Vec<u8>,
|
|
pub proof: ExecutionProof,
|
|
}
|
|
|
|
/// Proof of correct execution
|
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
|
pub struct ExecutionProof {
|
|
/// Hash of input + output (for spot-checking)
|
|
pub io_hash: [u8; 32],
|
|
/// Intermediate state hashes (for verification)
|
|
pub checkpoints: Vec<[u8; 32]>,
|
|
/// Random challenge response (if spot-check)
|
|
pub challenge_response: Option<Vec<u8>>,
|
|
}
|
|
|
|
/// Sandboxed task executor
|
|
#[wasm_bindgen]
|
|
pub struct WasmTaskExecutor {
|
|
/// Maximum memory for task execution
|
|
max_memory: usize,
|
|
/// Maximum execution time in ms
|
|
max_time_ms: u64,
|
|
/// Encryption key for task payloads
|
|
task_key: Option<Vec<u8>>,
|
|
}
|
|
|
|
#[wasm_bindgen]
|
|
impl WasmTaskExecutor {
|
|
/// Create a new task executor
|
|
#[wasm_bindgen(constructor)]
|
|
pub fn new(max_memory: usize) -> Result<WasmTaskExecutor, JsValue> {
|
|
Ok(WasmTaskExecutor {
|
|
max_memory,
|
|
max_time_ms: 30_000, // 30 seconds default
|
|
task_key: None,
|
|
})
|
|
}
|
|
|
|
/// Set encryption key for payload decryption
|
|
#[wasm_bindgen(js_name = setTaskKey)]
|
|
pub fn set_task_key(&mut self, key: &[u8]) -> Result<(), JsValue> {
|
|
if key.len() != 32 {
|
|
return Err(JsValue::from_str("Key must be 32 bytes"));
|
|
}
|
|
self.task_key = Some(key.to_vec());
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// Non-wasm methods (internal use)
|
|
impl WasmTaskExecutor {
|
|
/// Execute a task with full sandboxing
|
|
pub async fn execute(&self, task: &Task) -> Result<TaskResult, JsValue> {
|
|
// Validate task hasn't expired
|
|
let now = js_sys::Date::now() as u64;
|
|
if now > task.expires_at {
|
|
return Err(JsValue::from_str("Task has expired"));
|
|
}
|
|
|
|
// Decrypt payload
|
|
let payload = self.decrypt_payload(&task.encrypted_payload)?;
|
|
|
|
// Verify payload hash
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(&payload);
|
|
let hash: [u8; 32] = hasher.finalize().into();
|
|
if hash != task.payload_hash {
|
|
return Err(JsValue::from_str("Payload hash mismatch - tampering detected"));
|
|
}
|
|
|
|
// Execute based on task type (with timeout)
|
|
let start = js_sys::Date::now() as u64;
|
|
let result = match task.task_type {
|
|
TaskType::VectorSearch => self.execute_vector_search(&payload).await?,
|
|
TaskType::VectorInsert => self.execute_vector_insert(&payload).await?,
|
|
TaskType::Embedding => self.execute_embedding(&payload).await?,
|
|
TaskType::SemanticMatch => self.execute_semantic_match(&payload).await?,
|
|
TaskType::Encryption => self.execute_encryption(&payload).await?,
|
|
TaskType::Compression => self.execute_compression(&payload).await?,
|
|
TaskType::NeuralInference => self.execute_neural(&payload).await?,
|
|
TaskType::CustomWasm => {
|
|
return Err(JsValue::from_str("Custom WASM requires explicit verification"));
|
|
}
|
|
};
|
|
let execution_time = (js_sys::Date::now() as u64) - start;
|
|
|
|
// Create execution proof
|
|
let mut io_hasher = Sha256::new();
|
|
io_hasher.update(&payload);
|
|
io_hasher.update(&result);
|
|
let io_hash: [u8; 32] = io_hasher.finalize().into();
|
|
|
|
// Encrypt result
|
|
let encrypted_result = self.encrypt_payload(&result, &task.submitter_pubkey)?;
|
|
|
|
// Hash result
|
|
let mut result_hasher = Sha256::new();
|
|
result_hasher.update(&result);
|
|
let result_hash: [u8; 32] = result_hasher.finalize().into();
|
|
|
|
Ok(TaskResult {
|
|
task_id: task.id.clone(),
|
|
encrypted_result,
|
|
result_hash,
|
|
worker_id: String::new(), // Set by caller
|
|
execution_time_ms: execution_time,
|
|
signature: Vec::new(), // Set by caller
|
|
proof: ExecutionProof {
|
|
io_hash,
|
|
checkpoints: Vec::new(),
|
|
challenge_response: None,
|
|
},
|
|
})
|
|
}
|
|
|
|
/// Decrypt task payload
|
|
fn decrypt_payload(&self, encrypted: &[u8]) -> Result<Vec<u8>, JsValue> {
|
|
let key = self.task_key.as_ref()
|
|
.ok_or_else(|| JsValue::from_str("No task key set"))?;
|
|
|
|
if encrypted.len() < 12 {
|
|
return Err(JsValue::from_str("Invalid encrypted payload"));
|
|
}
|
|
|
|
let (nonce_bytes, ciphertext) = encrypted.split_at(12);
|
|
let nonce = Nonce::from_slice(nonce_bytes);
|
|
|
|
let key_array: [u8; 32] = key.clone().try_into()
|
|
.map_err(|_| JsValue::from_str("Invalid key length"))?;
|
|
let cipher = Aes256Gcm::new_from_slice(&key_array)
|
|
.map_err(|_| JsValue::from_str("Failed to create cipher"))?;
|
|
|
|
cipher.decrypt(nonce, ciphertext)
|
|
.map_err(|_| JsValue::from_str("Decryption failed - invalid key or tampered data"))
|
|
}
|
|
|
|
/// Encrypt result for submitter
|
|
fn encrypt_payload(&self, plaintext: &[u8], _recipient_pubkey: &[u8]) -> Result<Vec<u8>, JsValue> {
|
|
// For now, use symmetric encryption (would use ECDH in production)
|
|
let key = self.task_key.as_ref()
|
|
.ok_or_else(|| JsValue::from_str("No task key set"))?;
|
|
|
|
let key_array: [u8; 32] = key.clone().try_into()
|
|
.map_err(|_| JsValue::from_str("Invalid key length"))?;
|
|
let cipher = Aes256Gcm::new_from_slice(&key_array)
|
|
.map_err(|_| JsValue::from_str("Failed to create cipher"))?;
|
|
|
|
// Generate random nonce
|
|
let mut nonce_bytes = [0u8; 12];
|
|
getrandom::getrandom(&mut nonce_bytes)
|
|
.map_err(|_| JsValue::from_str("Failed to generate nonce"))?;
|
|
let nonce = Nonce::from_slice(&nonce_bytes);
|
|
|
|
let ciphertext = cipher.encrypt(nonce, plaintext)
|
|
.map_err(|_| JsValue::from_str("Encryption failed"))?;
|
|
|
|
// Prepend nonce to ciphertext
|
|
let mut result = nonce_bytes.to_vec();
|
|
result.extend(ciphertext);
|
|
Ok(result)
|
|
}
|
|
|
|
// Task executors (stubs - would integrate with actual WASM modules)
|
|
|
|
async fn execute_vector_search(&self, _payload: &[u8]) -> Result<Vec<u8>, JsValue> {
|
|
// Would call WasmHnswIndex.search()
|
|
Ok(vec![])
|
|
}
|
|
|
|
async fn execute_vector_insert(&self, _payload: &[u8]) -> Result<Vec<u8>, JsValue> {
|
|
Ok(vec![])
|
|
}
|
|
|
|
async fn execute_embedding(&self, _payload: &[u8]) -> Result<Vec<u8>, JsValue> {
|
|
Ok(vec![])
|
|
}
|
|
|
|
async fn execute_semantic_match(&self, _payload: &[u8]) -> Result<Vec<u8>, JsValue> {
|
|
Ok(vec![])
|
|
}
|
|
|
|
async fn execute_encryption(&self, _payload: &[u8]) -> Result<Vec<u8>, JsValue> {
|
|
Ok(vec![])
|
|
}
|
|
|
|
async fn execute_compression(&self, _payload: &[u8]) -> Result<Vec<u8>, JsValue> {
|
|
Ok(vec![])
|
|
}
|
|
|
|
async fn execute_neural(&self, _payload: &[u8]) -> Result<Vec<u8>, JsValue> {
|
|
Ok(vec![])
|
|
}
|
|
}
|
|
|
|
/// Wrapper for priority queue ordering
|
|
#[derive(Clone)]
|
|
struct PrioritizedTask {
|
|
task: Task,
|
|
priority_score: u32,
|
|
}
|
|
|
|
impl PartialEq for PrioritizedTask {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.priority_score == other.priority_score
|
|
}
|
|
}
|
|
|
|
impl Eq for PrioritizedTask {}
|
|
|
|
impl PartialOrd for PrioritizedTask {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for PrioritizedTask {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
// Higher priority first (reverse for max-heap)
|
|
self.priority_score.cmp(&other.priority_score)
|
|
}
|
|
}
|
|
|
|
/// Task queue for P2P distribution - optimized with priority heap
|
|
#[wasm_bindgen]
|
|
pub struct WasmTaskQueue {
|
|
// BinaryHeap for O(log n) insertion and O(1) max lookup vs O(n) linear scan
|
|
pending: BinaryHeap<PrioritizedTask>,
|
|
claimed: FxHashMap<String, String>, // task_id -> worker_id - FxHashMap for faster lookups
|
|
}
|
|
|
|
impl WasmTaskQueue {
|
|
pub fn new() -> Result<WasmTaskQueue, JsValue> {
|
|
Ok(WasmTaskQueue {
|
|
pending: BinaryHeap::with_capacity(1000), // Pre-allocate
|
|
claimed: FxHashMap::default(),
|
|
})
|
|
}
|
|
|
|
/// Create a task for submission
|
|
pub fn create_task(
|
|
&self,
|
|
task_type: &str,
|
|
payload: &[u8],
|
|
max_credits: u64,
|
|
identity: &crate::identity::WasmNodeIdentity,
|
|
) -> Result<Task, JsValue> {
|
|
let task_type = match task_type {
|
|
"vectors" | "vector_search" => TaskType::VectorSearch,
|
|
"vector_insert" => TaskType::VectorInsert,
|
|
"embeddings" | "embedding" => TaskType::Embedding,
|
|
"semantic" | "semantic_match" => TaskType::SemanticMatch,
|
|
"neural" | "neural_inference" => TaskType::NeuralInference,
|
|
"encryption" => TaskType::Encryption,
|
|
"compression" => TaskType::Compression,
|
|
_ => return Err(JsValue::from_str("Unknown task type")),
|
|
};
|
|
|
|
// Hash payload
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(payload);
|
|
let payload_hash: [u8; 32] = hasher.finalize().into();
|
|
|
|
let now = js_sys::Date::now() as u64;
|
|
|
|
let task = Task {
|
|
id: Uuid::new_v4().to_string(),
|
|
task_type,
|
|
encrypted_payload: Vec::new(), // Set after encryption
|
|
payload_hash,
|
|
submitter_id: identity.node_id(),
|
|
submitter_pubkey: identity.public_key_bytes(),
|
|
priority: TaskPriority::Normal,
|
|
base_reward: calculate_base_reward(task_type, payload.len()),
|
|
max_credits,
|
|
redundancy: 3,
|
|
created_at: now,
|
|
expires_at: now + 60_000, // 1 minute default
|
|
signature: Vec::new(), // Set after signing
|
|
};
|
|
|
|
Ok(task)
|
|
}
|
|
|
|
/// Submit task to network - O(log n) with priority heap
|
|
pub async fn submit(&mut self, task: Task) -> Result<SubmitResult, JsValue> {
|
|
let priority_score = match task.priority {
|
|
TaskPriority::High => 100,
|
|
TaskPriority::Normal => 50,
|
|
TaskPriority::Low => 10,
|
|
};
|
|
|
|
let task_id = task.id.clone();
|
|
let cost = task.base_reward;
|
|
|
|
self.pending.push(PrioritizedTask {
|
|
task,
|
|
priority_score,
|
|
});
|
|
|
|
Ok(SubmitResult {
|
|
task_id,
|
|
cost,
|
|
})
|
|
}
|
|
|
|
/// Claim next available task - O(1) with priority heap vs O(n) linear scan
|
|
pub async fn claim_next(
|
|
&mut self,
|
|
identity: &crate::identity::WasmNodeIdentity,
|
|
) -> Result<Option<Task>, JsValue> {
|
|
// Peek at highest priority task (O(1))
|
|
while let Some(prioritized) = self.pending.peek() {
|
|
if !self.claimed.contains_key(&prioritized.task.id) {
|
|
let task = self.pending.pop().unwrap().task;
|
|
self.claimed.insert(task.id.clone(), identity.node_id());
|
|
return Ok(Some(task));
|
|
} else {
|
|
// Already claimed, remove and check next
|
|
self.pending.pop();
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
/// Complete a task - just remove claim (heap automatically filters completed)
|
|
pub async fn complete(
|
|
&mut self,
|
|
task_id: String,
|
|
_result: TaskResult,
|
|
_identity: &crate::identity::WasmNodeIdentity,
|
|
) -> Result<(), JsValue> {
|
|
// Just remove claim - completed tasks filtered in claim_next
|
|
self.claimed.remove(&task_id);
|
|
Ok(())
|
|
}
|
|
|
|
/// Disconnect from network
|
|
pub fn disconnect(&self) -> Result<(), JsValue> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub struct SubmitResult {
|
|
pub task_id: String,
|
|
pub cost: u64,
|
|
}
|
|
|
|
impl From<SubmitResult> for JsValue {
|
|
fn from(result: SubmitResult) -> Self {
|
|
let obj = js_sys::Object::new();
|
|
js_sys::Reflect::set(&obj, &"taskId".into(), &result.task_id.into()).unwrap();
|
|
js_sys::Reflect::set(&obj, &"cost".into(), &result.cost.into()).unwrap();
|
|
obj.into()
|
|
}
|
|
}
|
|
|
|
/// Calculate base reward based on task type and size
|
|
fn calculate_base_reward(task_type: TaskType, payload_size: usize) -> u64 {
|
|
match task_type {
|
|
TaskType::VectorSearch => 1 + (payload_size / 10000) as u64,
|
|
TaskType::VectorInsert => 1 + (payload_size / 20000) as u64,
|
|
TaskType::Embedding => 5 + (payload_size / 1000) as u64,
|
|
TaskType::SemanticMatch => 1,
|
|
TaskType::NeuralInference => 3 + (payload_size / 5000) as u64,
|
|
TaskType::Encryption => 1 + (payload_size / 100000) as u64,
|
|
TaskType::Compression => 1 + (payload_size / 50000) as u64,
|
|
TaskType::CustomWasm => 10, // Premium for custom code
|
|
}
|
|
}
|