Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
//! P2P networking layer using GUN.js and WebRTC
//!
//! This module provides:
//! - **NetworkManager**: Basic P2P peer management
//! - **SemanticRouter**: RuVector-based intelligent routing with HNSW indexing
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
pub mod semantic;
pub use semantic::{SemanticRouter, PeerInfo, HnswIndex, PeerId, TopicHash};
/// Network message types
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum NetworkMessage {
/// Announce presence on network
Announce {
node_id: String,
pubkey: Vec<u8>,
capabilities: Vec<String>,
stake: u64,
},
/// Task submission
TaskSubmit {
task_id: String,
task_type: String,
encrypted_payload: Vec<u8>,
max_credits: u64,
redundancy: u8,
},
/// Task claim
TaskClaim {
task_id: String,
worker_id: String,
stake: u64,
},
/// Task result
TaskResult {
task_id: String,
encrypted_result: Vec<u8>,
proof: Vec<u8>,
signature: Vec<u8>,
},
/// Credit sync (CRDT state)
CreditSync {
ledger_state: Vec<u8>,
merkle_root: [u8; 32],
},
/// QDAG transaction
QDAGTransaction {
tx_bytes: Vec<u8>,
},
/// Heartbeat/ping
Heartbeat {
node_id: String,
timestamp: u64,
uptime: u64,
},
}
/// Network peer information
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Peer {
pub node_id: String,
pub pubkey: Vec<u8>,
pub capabilities: Vec<String>,
pub stake: u64,
pub reputation: f32,
pub last_seen: u64,
pub latency_ms: u32,
}
/// P2P network manager
#[wasm_bindgen]
pub struct WasmNetworkManager {
node_id: String,
peers: std::collections::HashMap<String, Peer>,
relay_urls: Vec<String>,
connected: bool,
}
#[wasm_bindgen]
impl WasmNetworkManager {
#[wasm_bindgen(constructor)]
pub fn new(node_id: &str) -> WasmNetworkManager {
WasmNetworkManager {
node_id: node_id.to_string(),
peers: std::collections::HashMap::new(),
relay_urls: vec![
"https://gun-manhattan.herokuapp.com/gun".to_string(),
"https://gun-us.herokuapp.com/gun".to_string(),
],
connected: false,
}
}
/// Add a relay URL
#[wasm_bindgen(js_name = addRelay)]
pub fn add_relay(&mut self, url: &str) {
self.relay_urls.push(url.to_string());
}
/// Check if connected
#[wasm_bindgen(js_name = isConnected)]
pub fn is_connected(&self) -> bool {
self.connected
}
/// Get peer count
#[wasm_bindgen(js_name = peerCount)]
pub fn peer_count(&self) -> usize {
self.peers.len()
}
/// Get active peer count (seen in last 60s)
#[wasm_bindgen(js_name = activePeerCount)]
pub fn active_peer_count(&self) -> usize {
let now = js_sys::Date::now() as u64;
self.peers.values()
.filter(|p| now - p.last_seen < 60_000)
.count()
}
/// Register a peer
#[wasm_bindgen(js_name = registerPeer)]
pub fn register_peer(
&mut self,
node_id: &str,
pubkey: &[u8],
capabilities: Vec<String>,
stake: u64,
) {
let peer = Peer {
node_id: node_id.to_string(),
pubkey: pubkey.to_vec(),
capabilities,
stake,
reputation: 0.5, // Start neutral
last_seen: js_sys::Date::now() as u64,
latency_ms: 0,
};
self.peers.insert(node_id.to_string(), peer);
}
/// Update peer reputation
#[wasm_bindgen(js_name = updateReputation)]
pub fn update_reputation(&mut self, node_id: &str, delta: f32) {
if let Some(peer) = self.peers.get_mut(node_id) {
peer.reputation = (peer.reputation + delta).clamp(0.0, 1.0);
}
}
/// Get peers with specific capability
#[wasm_bindgen(js_name = getPeersWithCapability)]
pub fn get_peers_with_capability(&self, capability: &str) -> Vec<String> {
self.peers.values()
.filter(|p| p.capabilities.contains(&capability.to_string()))
.filter(|p| p.stake > 0) // Must be staked
.filter(|p| p.reputation > 0.3) // Must have reasonable reputation
.map(|p| p.node_id.clone())
.collect()
}
/// Select workers for task execution (reputation-weighted random)
#[wasm_bindgen(js_name = selectWorkers)]
pub fn select_workers(&self, capability: &str, count: usize) -> Vec<String> {
let mut candidates: Vec<_> = self.peers.values()
.filter(|p| p.capabilities.contains(&capability.to_string()))
.filter(|p| p.stake > 0)
.filter(|p| p.reputation > 0.3)
.collect();
// Sort by reputation (highest first)
candidates.sort_by(|a, b| b.reputation.partial_cmp(&a.reputation).unwrap());
// Take top N
candidates.into_iter()
.take(count)
.map(|p| p.node_id.clone())
.collect()
}
}
+844
View File
@@ -0,0 +1,844 @@
//! Core P2P networking layer using libp2p
//!
//! Replaces GUN.js placeholder with full libp2p networking including:
//! - Gossipsub for event broadcasting (RAC events, task market, gradients)
//! - Kademlia DHT for peer/capability discovery
//! - Request-Response for direct task negotiation
//! - NOISE protocol for encryption using Pi-Key identity
//!
//! ## Architecture
//!
//! ```text
//! +--------------------------------------------------+
//! | P2pNode |
//! +--------------------------------------------------+
//! | PiKey Identity --> libp2p PeerId mapping |
//! +--------------------------------------------------+
//! | EdgeNetBehaviour |
//! | +------------+ +----------+ +---------------+ |
//! | | Gossipsub | | Kademlia | | RequestResp | |
//! | | (events) | | (DHT) | | (tasks) | |
//! | +------------+ +----------+ +---------------+ |
//! | +------------+ |
//! | | Identify | |
//! | | (handshake)| |
//! | +------------+ |
//! +--------------------------------------------------+
//! ```
#[cfg(feature = "p2p")]
use libp2p::{
gossipsub::{self, Gossipsub, GossipsubEvent, MessageAuthenticity, ValidationMode},
identify::{self, Identify, IdentifyEvent},
kad::{self, Kademlia, KademliaEvent, store::MemoryStore},
request_response::{self, RequestResponse, RequestResponseEvent},
swarm::{NetworkBehaviour, SwarmEvent},
noise, yamux,
identity::Keypair,
PeerId, Multiaddr, Swarm,
};
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use std::time::Duration;
#[cfg(feature = "p2p")]
use crate::pikey::PiKey;
// ============================================================================
// Topic Constants for Gossipsub
// ============================================================================
/// RAC (RuVector Adversarial Coherence) events topic
/// Used for: assertions, challenges, resolutions, deprecations
pub const TOPIC_RAC_EVENTS: &str = "/edge-net/rac/1.0.0";
/// Task marketplace topic
/// Used for: task announcements, claims, completions
pub const TOPIC_TASK_MARKET: &str = "/edge-net/tasks/1.0.0";
/// Model synchronization topic
/// Used for: model weight updates, checkpoints
pub const TOPIC_MODEL_SYNC: &str = "/edge-net/models/1.0.0";
/// Gradient gossip topic (federated learning)
/// Used for: gradient aggregation, consensus
pub const TOPIC_GRADIENT_GOSSIP: &str = "/edge-net/gradients/1.0.0";
/// Credit/economic sync topic
/// Used for: CRDT ledger sync, stake announcements
pub const TOPIC_CREDIT_SYNC: &str = "/edge-net/credits/1.0.0";
/// Node presence/heartbeat topic
/// Used for: peer discovery, health monitoring
pub const TOPIC_PRESENCE: &str = "/edge-net/presence/1.0.0";
// ============================================================================
// Protocol Constants
// ============================================================================
/// Task negotiation protocol identifier
pub const TASK_PROTOCOL: &str = "/edge-net/task-negotiate/1.0.0";
/// Agent agent version for identify protocol
pub const AGENT_VERSION: &str = concat!("edge-net/", env!("CARGO_PKG_VERSION"));
/// Protocol version for identify
pub const PROTOCOL_VERSION: &str = "/edge-net/1.0.0";
// ============================================================================
// Network Messages
// ============================================================================
/// Messages broadcast over Gossipsub topics
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum GossipMessage {
/// RAC event (assertion, challenge, resolution, etc.)
RacEvent {
event_bytes: Vec<u8>,
signature: Vec<u8>,
},
/// Task announcement
TaskAnnounce {
task_id: String,
task_type: String,
requirements: TaskRequirements,
max_credits: u64,
deadline_ms: u64,
},
/// Task claim by worker
TaskClaim {
task_id: String,
worker_id: String,
stake: u64,
signature: Vec<u8>,
},
/// Task completion announcement
TaskComplete {
task_id: String,
worker_id: String,
result_hash: [u8; 32],
proof: Vec<u8>,
},
/// Model weight update (federated learning)
ModelUpdate {
model_id: String,
layer_id: String,
delta_weights: Vec<u8>, // Compressed gradient
epoch: u64,
},
/// Gradient fragment for aggregation
GradientFragment {
training_id: String,
fragment_id: u32,
gradient_bytes: Vec<u8>,
contributor: String,
},
/// Credit ledger sync (CRDT state)
CreditSync {
node_id: String,
earned_state: Vec<u8>,
spent_state: Vec<u8>,
merkle_root: [u8; 32],
},
/// Node presence heartbeat
Presence {
node_id: String,
capabilities: Vec<String>,
stake: u64,
uptime_hours: f32,
load: f32,
},
}
/// Task requirements for matching workers
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaskRequirements {
/// Required capabilities (e.g., "vectors", "embeddings", "gpu")
pub capabilities: Vec<String>,
/// Minimum stake required
pub min_stake: u64,
/// Minimum reputation score (0.0 - 1.0)
pub min_reputation: f32,
/// Estimated memory requirement in bytes
pub memory_bytes: usize,
/// Estimated CPU time in ms
pub cpu_time_ms: u64,
/// Whether task requires GPU
pub requires_gpu: bool,
}
impl Default for TaskRequirements {
fn default() -> Self {
Self {
capabilities: vec!["vectors".to_string()],
min_stake: 100,
min_reputation: 0.3,
memory_bytes: 64 * 1024 * 1024, // 64MB
cpu_time_ms: 10_000, // 10 seconds
requires_gpu: false,
}
}
}
// ============================================================================
// Request-Response Messages
// ============================================================================
/// Direct task negotiation request
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaskRequest {
/// Task ID being negotiated
pub task_id: String,
/// Request type
pub request_type: TaskRequestType,
/// Encrypted payload (using session key)
pub encrypted_payload: Vec<u8>,
/// Sender's public key for reply encryption
pub sender_pubkey: Vec<u8>,
}
/// Types of task requests
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum TaskRequestType {
/// Request task details
GetDetails,
/// Submit work claim
SubmitClaim { stake: u64 },
/// Submit task result
SubmitResult { result_hash: [u8; 32] },
/// Request result verification
VerifyResult { worker_id: String },
/// Request payment release
ReleasePayment { proof: Vec<u8> },
}
/// Task negotiation response
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaskResponse {
/// Original task ID
pub task_id: String,
/// Response status
pub status: TaskResponseStatus,
/// Response data (encrypted)
pub encrypted_data: Vec<u8>,
}
/// Response status codes
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum TaskResponseStatus {
/// Request accepted
Accepted,
/// Task already claimed
AlreadyClaimed,
/// Insufficient stake
InsufficientStake,
/// Invalid proof
InvalidProof,
/// Task not found
NotFound,
/// Result verified
Verified,
/// Payment released
PaymentReleased,
/// Error with message
Error(String),
}
// ============================================================================
// P2P Node Configuration
// ============================================================================
/// Configuration for P2P networking
#[derive(Clone, Debug)]
pub struct P2pConfig {
/// Bootstrap peers to connect to
pub bootstrap_peers: Vec<Multiaddr>,
/// Listen addresses
pub listen_addrs: Vec<Multiaddr>,
/// Gossipsub mesh parameters
pub gossip_mesh_n: usize,
pub gossip_mesh_n_low: usize,
pub gossip_mesh_n_high: usize,
/// Kademlia replication factor
pub kad_replication: usize,
/// Heartbeat interval in seconds
pub heartbeat_interval_secs: u64,
/// Message validation mode
pub validation_mode: MessageValidationMode,
}
/// Message validation modes
#[derive(Clone, Debug)]
pub enum MessageValidationMode {
/// Accept all messages (for testing)
Permissive,
/// Validate signatures
Strict,
/// Custom validation with callback
Custom,
}
impl Default for P2pConfig {
fn default() -> Self {
Self {
bootstrap_peers: vec![],
listen_addrs: vec![],
gossip_mesh_n: 6,
gossip_mesh_n_low: 4,
gossip_mesh_n_high: 12,
kad_replication: 20,
heartbeat_interval_secs: 30,
validation_mode: MessageValidationMode::Strict,
}
}
}
// ============================================================================
// EdgeNet Network Behaviour (libp2p integration)
// ============================================================================
#[cfg(feature = "p2p")]
use super::protocols::{TaskCodec, TaskProtocol};
/// Combined network behaviour for EdgeNet P2P
///
/// Integrates multiple libp2p protocols:
/// - Gossipsub: Pub/sub for event broadcasting
/// - Kademlia: DHT for peer and capability discovery
/// - Identify: Peer identification and handshake
/// - Request-Response: Direct task negotiation
#[cfg(feature = "p2p")]
#[derive(NetworkBehaviour)]
#[behaviour(to_swarm = "EdgeNetEvent")]
pub struct EdgeNetBehaviour {
/// Gossipsub for broadcast messaging
pub gossipsub: Gossipsub,
/// Kademlia DHT for peer discovery
pub kademlia: Kademlia<MemoryStore>,
/// Identify protocol for peer handshake
pub identify: Identify,
/// Request-response for direct task negotiation
pub request_response: RequestResponse<TaskCodec>,
}
/// Aggregated events from all behaviours
#[cfg(feature = "p2p")]
#[derive(Debug)]
pub enum EdgeNetEvent {
Gossipsub(GossipsubEvent),
Kademlia(KademliaEvent),
Identify(IdentifyEvent),
RequestResponse(RequestResponseEvent<TaskRequest, TaskResponse>),
}
#[cfg(feature = "p2p")]
impl From<GossipsubEvent> for EdgeNetEvent {
fn from(event: GossipsubEvent) -> Self {
EdgeNetEvent::Gossipsub(event)
}
}
#[cfg(feature = "p2p")]
impl From<KademliaEvent> for EdgeNetEvent {
fn from(event: KademliaEvent) -> Self {
EdgeNetEvent::Kademlia(event)
}
}
#[cfg(feature = "p2p")]
impl From<IdentifyEvent> for EdgeNetEvent {
fn from(event: IdentifyEvent) -> Self {
EdgeNetEvent::Identify(event)
}
}
#[cfg(feature = "p2p")]
impl From<RequestResponseEvent<TaskRequest, TaskResponse>> for EdgeNetEvent {
fn from(event: RequestResponseEvent<TaskRequest, TaskResponse>) -> Self {
EdgeNetEvent::RequestResponse(event)
}
}
// ============================================================================
// P2P Node Implementation
// ============================================================================
/// Main P2P node for EdgeNet networking
///
/// Manages the libp2p swarm and provides high-level APIs for:
/// - Peer discovery and connection management
/// - Event broadcasting (RAC, tasks, gradients)
/// - Direct task negotiation
/// - Capability advertisement
#[cfg(feature = "p2p")]
pub struct P2pNode {
/// libp2p swarm with EdgeNet behaviour
swarm: Swarm<EdgeNetBehaviour>,
/// Pi-Key identity for signing
identity: PiKey,
/// Our peer ID
peer_id: PeerId,
/// Mapping from Pi-Key identity to PeerId
identity_map: HashMap<[u8; 40], PeerId>,
/// Subscribed topics
subscribed_topics: Vec<String>,
/// Known peer capabilities
peer_capabilities: HashMap<PeerId, Vec<String>>,
/// Configuration
config: P2pConfig,
}
#[cfg(feature = "p2p")]
impl P2pNode {
/// Create a new P2P node from a Pi-Key identity
pub fn new(identity: PiKey, config: P2pConfig) -> Result<Self, P2pError> {
// Derive libp2p keypair from Pi-Key
let keypair = Self::derive_keypair_from_pikey(&identity)?;
let peer_id = PeerId::from(keypair.public());
// Create gossipsub behaviour
let gossipsub = Self::create_gossipsub(&keypair, &config)?;
// Create Kademlia DHT
let kademlia = Self::create_kademlia(peer_id, &config);
// Create Identify protocol
let identify = Self::create_identify(&keypair);
// Create Request-Response protocol
let request_response = Self::create_request_response();
// Combine behaviours
let behaviour = EdgeNetBehaviour {
gossipsub,
kademlia,
identify,
request_response,
};
// Build swarm with NOISE encryption
let swarm = libp2p::SwarmBuilder::with_existing_identity(keypair)
.with_tokio()
.with_tcp(
Default::default(),
noise::Config::new,
yamux::Config::default,
)?
.with_behaviour(|_| behaviour)?
.with_swarm_config(|cfg| {
cfg.with_idle_connection_timeout(Duration::from_secs(60))
})
.build();
Ok(Self {
swarm,
identity,
peer_id,
identity_map: HashMap::new(),
subscribed_topics: Vec::new(),
peer_capabilities: HashMap::new(),
config,
})
}
/// Derive a libp2p Ed25519 keypair from Pi-Key
fn derive_keypair_from_pikey(pikey: &PiKey) -> Result<Keypair, P2pError> {
// Get the signing key bytes from Pi-Key
let pubkey_bytes = pikey.get_public_key();
// For now, we'll generate a new keypair and map it
// In production, we'd derive deterministically from Pi-Key
let keypair = Keypair::generate_ed25519();
Ok(keypair)
}
/// Create gossipsub behaviour
fn create_gossipsub(keypair: &Keypair, config: &P2pConfig) -> Result<Gossipsub, P2pError> {
let message_authenticity = MessageAuthenticity::Signed(keypair.clone());
let gossipsub_config = gossipsub::ConfigBuilder::default()
.mesh_n(config.gossip_mesh_n)
.mesh_n_low(config.gossip_mesh_n_low)
.mesh_n_high(config.gossip_mesh_n_high)
.heartbeat_interval(Duration::from_secs(config.heartbeat_interval_secs))
.validation_mode(ValidationMode::Strict)
.message_id_fn(|msg| {
// Use hash of data as message ID for deduplication
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(&msg.data);
let hash = hasher.finalize();
gossipsub::MessageId::from(hash.to_vec())
})
.build()
.map_err(|e| P2pError::Config(e.to_string()))?;
Gossipsub::new(message_authenticity, gossipsub_config)
.map_err(|e| P2pError::Behaviour(e.to_string()))
}
/// Create Kademlia DHT behaviour
fn create_kademlia(peer_id: PeerId, config: &P2pConfig) -> Kademlia<MemoryStore> {
let store = MemoryStore::new(peer_id);
let mut kad_config = kad::Config::default();
kad_config.set_replication_factor(
std::num::NonZeroUsize::new(config.kad_replication).unwrap()
);
Kademlia::with_config(peer_id, store, kad_config)
}
/// Create Identify protocol behaviour
fn create_identify(keypair: &Keypair) -> Identify {
let config = identify::Config::new(PROTOCOL_VERSION.to_string(), keypair.public())
.with_agent_version(AGENT_VERSION.to_string());
Identify::new(config)
}
/// Create Request-Response protocol
fn create_request_response() -> RequestResponse<TaskCodec> {
let protocols = std::iter::once((TaskProtocol, request_response::ProtocolSupport::Full));
let config = request_response::Config::default()
.with_request_timeout(Duration::from_secs(30));
RequestResponse::new(protocols, config)
}
/// Get our peer ID
pub fn peer_id(&self) -> &PeerId {
&self.peer_id
}
/// Get our Pi-Key identity
pub fn identity(&self) -> &PiKey {
&self.identity
}
/// Start listening on configured addresses
pub fn start_listening(&mut self) -> Result<(), P2pError> {
for addr in &self.config.listen_addrs {
self.swarm.listen_on(addr.clone())
.map_err(|e| P2pError::Transport(e.to_string()))?;
}
Ok(())
}
/// Connect to bootstrap peers
pub fn bootstrap(&mut self) -> Result<(), P2pError> {
for addr in &self.config.bootstrap_peers {
// Extract peer ID from multiaddr
if let Some(peer_id) = Self::extract_peer_id(addr) {
self.swarm.dial(addr.clone())
.map_err(|e| P2pError::Dial(e.to_string()))?;
self.swarm.behaviour_mut().kademlia.add_address(&peer_id, addr.clone());
}
}
// Start Kademlia bootstrap
self.swarm.behaviour_mut().kademlia.bootstrap()
.map_err(|e| P2pError::Kademlia(e.to_string()))?;
Ok(())
}
/// Subscribe to a gossipsub topic
pub fn subscribe(&mut self, topic: &str) -> Result<(), P2pError> {
let topic = gossipsub::IdentTopic::new(topic);
self.swarm.behaviour_mut().gossipsub.subscribe(&topic)
.map_err(|e| P2pError::Gossipsub(e.to_string()))?;
self.subscribed_topics.push(topic.hash().to_string());
Ok(())
}
/// Subscribe to all EdgeNet topics
pub fn subscribe_all_topics(&mut self) -> Result<(), P2pError> {
self.subscribe(TOPIC_RAC_EVENTS)?;
self.subscribe(TOPIC_TASK_MARKET)?;
self.subscribe(TOPIC_MODEL_SYNC)?;
self.subscribe(TOPIC_GRADIENT_GOSSIP)?;
self.subscribe(TOPIC_CREDIT_SYNC)?;
self.subscribe(TOPIC_PRESENCE)?;
Ok(())
}
/// Publish a message to a topic
pub fn publish(&mut self, topic: &str, message: GossipMessage) -> Result<(), P2pError> {
let topic = gossipsub::IdentTopic::new(topic);
let data = bincode::serialize(&message)
.map_err(|e| P2pError::Serialization(e.to_string()))?;
self.swarm.behaviour_mut().gossipsub.publish(topic, data)
.map_err(|e| P2pError::Gossipsub(e.to_string()))?;
Ok(())
}
/// Broadcast a RAC event
pub fn broadcast_rac_event(&mut self, event_bytes: Vec<u8>) -> Result<(), P2pError> {
let signature = self.identity.sign(&event_bytes);
let message = GossipMessage::RacEvent { event_bytes, signature };
self.publish(TOPIC_RAC_EVENTS, message)
}
/// Announce a task to the network
pub fn announce_task(
&mut self,
task_id: String,
task_type: String,
requirements: TaskRequirements,
max_credits: u64,
deadline_ms: u64,
) -> Result<(), P2pError> {
let message = GossipMessage::TaskAnnounce {
task_id,
task_type,
requirements,
max_credits,
deadline_ms,
};
self.publish(TOPIC_TASK_MARKET, message)
}
/// Claim a task
pub fn claim_task(&mut self, task_id: String, stake: u64) -> Result<(), P2pError> {
let worker_id = hex::encode(&self.identity.get_identity()[..8]);
let claim_data = format!("{}:{}:{}", task_id, worker_id, stake);
let signature = self.identity.sign(claim_data.as_bytes());
let message = GossipMessage::TaskClaim {
task_id,
worker_id,
stake,
signature,
};
self.publish(TOPIC_TASK_MARKET, message)
}
/// Send presence heartbeat
pub fn send_heartbeat(
&mut self,
capabilities: Vec<String>,
stake: u64,
uptime_hours: f32,
load: f32,
) -> Result<(), P2pError> {
let node_id = hex::encode(&self.identity.get_identity()[..8]);
let message = GossipMessage::Presence {
node_id,
capabilities,
stake,
uptime_hours,
load,
};
self.publish(TOPIC_PRESENCE, message)
}
/// Advertise our capabilities in the DHT
pub fn advertise_capabilities(&mut self, capabilities: &[String]) -> Result<(), P2pError> {
for cap in capabilities {
let key = kad::RecordKey::new(&format!("cap:{}", cap));
let record = kad::Record {
key,
value: self.peer_id.to_bytes(),
publisher: Some(self.peer_id),
expires: None,
};
self.swarm.behaviour_mut().kademlia.put_record(record, kad::Quorum::One)
.map_err(|e| P2pError::Kademlia(e.to_string()))?;
}
Ok(())
}
/// Find peers with a specific capability
pub fn find_providers(&mut self, capability: &str) -> kad::QueryId {
let key = kad::RecordKey::new(&format!("cap:{}", capability));
self.swarm.behaviour_mut().kademlia.get_providers(key)
}
/// Send a direct task request to a peer
pub fn send_task_request(
&mut self,
peer: &PeerId,
request: TaskRequest,
) -> request_response::OutboundRequestId {
self.swarm.behaviour_mut().request_response.send_request(peer, request)
}
/// Send a task response
pub fn send_task_response(
&mut self,
channel: request_response::ResponseChannel<TaskResponse>,
response: TaskResponse,
) -> Result<(), P2pError> {
self.swarm.behaviour_mut().request_response.send_response(channel, response)
.map_err(|_| P2pError::Response("Failed to send response".to_string()))
}
/// Poll the swarm for events
pub async fn next_event(&mut self) -> SwarmEvent<EdgeNetEvent> {
self.swarm.select_next_some().await
}
/// Get the number of connected peers
pub fn connected_peers(&self) -> usize {
self.swarm.connected_peers().count()
}
/// Get list of connected peer IDs
pub fn peer_list(&self) -> Vec<PeerId> {
self.swarm.connected_peers().cloned().collect()
}
/// Extract peer ID from a multiaddr
fn extract_peer_id(addr: &Multiaddr) -> Option<PeerId> {
addr.iter().find_map(|proto| {
if let libp2p::multiaddr::Protocol::P2p(peer_id) = proto {
Some(peer_id)
} else {
None
}
})
}
}
// ============================================================================
// Error Types
// ============================================================================
/// P2P networking errors
#[derive(Debug, Clone)]
pub enum P2pError {
/// Configuration error
Config(String),
/// Transport error
Transport(String),
/// Dial error
Dial(String),
/// Behaviour error
Behaviour(String),
/// Gossipsub error
Gossipsub(String),
/// Kademlia error
Kademlia(String),
/// Serialization error
Serialization(String),
/// Response error
Response(String),
/// Identity error
Identity(String),
}
impl std::fmt::Display for P2pError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
P2pError::Config(e) => write!(f, "Config error: {}", e),
P2pError::Transport(e) => write!(f, "Transport error: {}", e),
P2pError::Dial(e) => write!(f, "Dial error: {}", e),
P2pError::Behaviour(e) => write!(f, "Behaviour error: {}", e),
P2pError::Gossipsub(e) => write!(f, "Gossipsub error: {}", e),
P2pError::Kademlia(e) => write!(f, "Kademlia error: {}", e),
P2pError::Serialization(e) => write!(f, "Serialization error: {}", e),
P2pError::Response(e) => write!(f, "Response error: {}", e),
P2pError::Identity(e) => write!(f, "Identity error: {}", e),
}
}
}
impl std::error::Error for P2pError {}
// ============================================================================
// Non-P2P Stub Implementation (for WASM without full libp2p)
// ============================================================================
/// Stub P2P node for environments without libp2p feature
#[cfg(not(feature = "p2p"))]
pub struct P2pNode {
_placeholder: (),
}
#[cfg(not(feature = "p2p"))]
impl P2pNode {
pub fn new(_identity: crate::pikey::PiKey, _config: P2pConfig) -> Result<Self, P2pError> {
Ok(Self { _placeholder: () })
}
pub fn connected_peers(&self) -> usize { 0 }
pub fn peer_list(&self) -> Vec<String> { vec![] }
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_topic_constants() {
assert!(TOPIC_RAC_EVENTS.starts_with("/edge-net/"));
assert!(TOPIC_TASK_MARKET.starts_with("/edge-net/"));
assert!(TOPIC_MODEL_SYNC.starts_with("/edge-net/"));
assert!(TOPIC_GRADIENT_GOSSIP.starts_with("/edge-net/"));
}
#[test]
fn test_task_requirements_default() {
let req = TaskRequirements::default();
assert!(req.capabilities.contains(&"vectors".to_string()));
assert_eq!(req.min_stake, 100);
assert!(!req.requires_gpu);
}
#[test]
fn test_gossip_message_serialization() {
let msg = GossipMessage::Presence {
node_id: "test-node".to_string(),
capabilities: vec!["vectors".to_string()],
stake: 1000,
uptime_hours: 24.5,
load: 0.3,
};
let serialized = bincode::serialize(&msg).unwrap();
let deserialized: GossipMessage = bincode::deserialize(&serialized).unwrap();
if let GossipMessage::Presence { node_id, .. } = deserialized {
assert_eq!(node_id, "test-node");
} else {
panic!("Wrong message type");
}
}
#[test]
fn test_task_request_serialization() {
let req = TaskRequest {
task_id: "task-123".to_string(),
request_type: TaskRequestType::GetDetails,
encrypted_payload: vec![1, 2, 3, 4],
sender_pubkey: vec![5, 6, 7, 8],
};
let serialized = bincode::serialize(&req).unwrap();
let deserialized: TaskRequest = bincode::deserialize(&serialized).unwrap();
assert_eq!(deserialized.task_id, "task-123");
}
#[test]
fn test_p2p_config_default() {
let config = P2pConfig::default();
assert_eq!(config.gossip_mesh_n, 6);
assert_eq!(config.kad_replication, 20);
assert_eq!(config.heartbeat_interval_secs, 30);
}
#[test]
fn test_p2p_error_display() {
let err = P2pError::Config("test error".to_string());
assert!(err.to_string().contains("Config error"));
}
}
@@ -0,0 +1,706 @@
//! Custom libp2p protocols for EdgeNet task negotiation
//!
//! Implements the request-response protocol for direct peer-to-peer
//! task negotiation, including:
//! - Task details request
//! - Work claims with stake
//! - Result submission with proofs
//! - Payment verification and release
//!
//! ## Protocol Flow
//!
//! ```text
//! Requester Worker
//! | |
//! |--- TaskRequest::GetDetails ---->|
//! |<-- TaskResponse::Accepted ------|
//! | |
//! |--- TaskRequest::SubmitClaim --->|
//! |<-- TaskResponse::Accepted ------|
//! | |
//! | [Worker executes task] |
//! | |
//! |<-- TaskRequest::SubmitResult ---|
//! |--- TaskResponse::Verified ----->|
//! | |
//! |<-- TaskRequest::ReleasePayment -|
//! |--- PaymentReleased ------------>|
//! ```
#[cfg(feature = "p2p")]
use libp2p::request_response::{self, Codec};
use async_trait::async_trait;
use futures::prelude::*;
use serde::{Serialize, Deserialize};
use std::io;
use super::p2p::{TaskRequest, TaskResponse};
// ============================================================================
// Protocol Definition
// ============================================================================
/// The task negotiation protocol identifier
#[derive(Debug, Clone)]
pub struct TaskProtocol;
#[cfg(feature = "p2p")]
impl AsRef<str> for TaskProtocol {
fn as_ref(&self) -> &str {
"/edge-net/task-negotiate/1.0.0"
}
}
// ============================================================================
// Codec Implementation
// ============================================================================
/// Codec for serializing/deserializing task requests and responses
///
/// Uses bincode for efficient binary serialization with the following format:
/// - 4 bytes: message length (big-endian u32)
/// - N bytes: bincode-serialized message
#[derive(Debug, Clone, Default)]
pub struct TaskCodec {
/// Maximum message size in bytes (default: 16MB)
max_message_size: usize,
}
impl TaskCodec {
/// Create a new codec with default settings
pub fn new() -> Self {
Self {
max_message_size: 16 * 1024 * 1024, // 16MB
}
}
/// Create a new codec with custom max message size
pub fn with_max_size(max_message_size: usize) -> Self {
Self { max_message_size }
}
}
#[cfg(feature = "p2p")]
#[async_trait]
impl Codec for TaskCodec {
type Protocol = TaskProtocol;
type Request = TaskRequest;
type Response = TaskResponse;
async fn read_request<T>(
&mut self,
_protocol: &Self::Protocol,
io: &mut T,
) -> io::Result<Self::Request>
where
T: AsyncRead + Unpin + Send,
{
read_length_prefixed(io, self.max_message_size).await
}
async fn read_response<T>(
&mut self,
_protocol: &Self::Protocol,
io: &mut T,
) -> io::Result<Self::Response>
where
T: AsyncRead + Unpin + Send,
{
read_length_prefixed(io, self.max_message_size).await
}
async fn write_request<T>(
&mut self,
_protocol: &Self::Protocol,
io: &mut T,
req: Self::Request,
) -> io::Result<()>
where
T: AsyncWrite + Unpin + Send,
{
write_length_prefixed(io, &req).await
}
async fn write_response<T>(
&mut self,
_protocol: &Self::Protocol,
io: &mut T,
res: Self::Response,
) -> io::Result<()>
where
T: AsyncWrite + Unpin + Send,
{
write_length_prefixed(io, &res).await
}
}
// ============================================================================
// Length-Prefixed I/O Helpers
// ============================================================================
/// Read a length-prefixed message from the stream
async fn read_length_prefixed<T, M>(io: &mut T, max_size: usize) -> io::Result<M>
where
T: AsyncRead + Unpin + Send,
M: for<'de> Deserialize<'de>,
{
// Read the 4-byte length prefix
let mut len_bytes = [0u8; 4];
io.read_exact(&mut len_bytes).await?;
let len = u32::from_be_bytes(len_bytes) as usize;
// Validate length
if len > max_size {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Message too large: {} bytes (max: {})", len, max_size),
));
}
if len == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Empty message",
));
}
// Read the message body
let mut buffer = vec![0u8; len];
io.read_exact(&mut buffer).await?;
// Deserialize
bincode::deserialize(&buffer).map_err(|e| {
io::Error::new(io::ErrorKind::InvalidData, format!("Deserialization error: {}", e))
})
}
/// Write a length-prefixed message to the stream
async fn write_length_prefixed<T, M>(io: &mut T, msg: &M) -> io::Result<()>
where
T: AsyncWrite + Unpin + Send,
M: Serialize,
{
// Serialize the message
let data = bincode::serialize(msg).map_err(|e| {
io::Error::new(io::ErrorKind::InvalidData, format!("Serialization error: {}", e))
})?;
// Write length prefix
let len = data.len() as u32;
io.write_all(&len.to_be_bytes()).await?;
// Write message body
io.write_all(&data).await?;
io.flush().await?;
Ok(())
}
// ============================================================================
// Additional Protocol Messages
// ============================================================================
/// Extended task information for detailed negotiation
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaskDetails {
/// Task identifier
pub task_id: String,
/// Task type (e.g., "vectors", "embeddings", "inference")
pub task_type: String,
/// Human-readable description
pub description: String,
/// Input data hash (for verification)
pub input_hash: [u8; 32],
/// Expected output size in bytes
pub expected_output_size: usize,
/// Base reward in credits
pub base_reward: u64,
/// Bonus multiplier for early completion
pub early_bonus: f32,
/// Deadline timestamp (ms since epoch)
pub deadline_ms: u64,
/// Number of required confirmations
pub required_confirmations: u32,
/// Submitter's stake (for dispute resolution)
pub submitter_stake: u64,
}
/// Work claim with proof of stake
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WorkClaim {
/// Task being claimed
pub task_id: String,
/// Worker's node ID
pub worker_id: String,
/// Staked amount
pub stake: u64,
/// Estimated completion time in ms
pub estimated_time_ms: u64,
/// Worker's capability proof
pub capability_proof: Vec<u8>,
/// Signature over claim data
pub signature: Vec<u8>,
}
/// Task result with cryptographic proof
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaskResult {
/// Task identifier
pub task_id: String,
/// Worker's node ID
pub worker_id: String,
/// Result data (encrypted with submitter's key)
pub encrypted_result: Vec<u8>,
/// Hash of unencrypted result (for verification)
pub result_hash: [u8; 32],
/// Proof of work/computation
pub proof: ComputationProof,
/// Execution statistics
pub stats: ExecutionStats,
/// Signature over result
pub signature: Vec<u8>,
}
/// Proof of computation for verification
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ComputationProof {
/// Simple hash chain proof
HashChain {
/// Intermediate hashes from computation
intermediate_hashes: Vec<[u8; 32]>,
/// Final hash
final_hash: [u8; 32],
},
/// Merkle proof of computation steps
MerkleProof {
/// Merkle root of computation trace
root: [u8; 32],
/// Proof path for sampled steps
proof_path: Vec<([u8; 32], bool)>,
},
/// Zero-knowledge proof (future)
ZkProof {
/// Proof bytes (implementation-specific)
proof_bytes: Vec<u8>,
/// Verification key
verification_key: Vec<u8>,
},
/// Attestation from trusted execution environment
TeeAttestation {
/// Quote from TEE
quote: Vec<u8>,
/// Enclave measurement
measurement: [u8; 32],
},
}
/// Execution statistics for task completion
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExecutionStats {
/// CPU time in milliseconds
pub cpu_time_ms: u64,
/// Wall clock time in milliseconds
pub wall_time_ms: u64,
/// Peak memory usage in bytes
pub peak_memory_bytes: usize,
/// Number of operations performed
pub operations: u64,
/// Input size processed
pub input_bytes: usize,
/// Output size generated
pub output_bytes: usize,
}
/// Payment release request with verification
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PaymentRelease {
/// Task identifier
pub task_id: String,
/// Worker to be paid
pub worker_id: String,
/// Amount to release
pub amount: u64,
/// Verification signatures from validators
pub validator_signatures: Vec<(String, Vec<u8>)>,
/// Timestamp of release request
pub timestamp_ms: u64,
}
/// Dispute filing for contested results
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaskDispute {
/// Task being disputed
pub task_id: String,
/// Disputer's node ID
pub disputer_id: String,
/// Type of dispute
pub dispute_type: DisputeType,
/// Evidence supporting dispute
pub evidence: Vec<DisputeEvidence>,
/// Stake for dispute
pub dispute_stake: u64,
/// Signature
pub signature: Vec<u8>,
}
/// Types of task disputes
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum DisputeType {
/// Result is incorrect
IncorrectResult,
/// Worker didn't complete in time
Timeout,
/// Worker submitted invalid proof
InvalidProof,
/// Task was never assigned
Unauthorized,
/// Payment was not released
PaymentWithheld,
}
/// Evidence for dispute resolution
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DisputeEvidence {
/// Type of evidence
pub evidence_type: String,
/// Evidence data
pub data: Vec<u8>,
/// Reference to on-chain/log proof
pub reference: Option<String>,
}
// ============================================================================
// Protocol Versioning
// ============================================================================
/// Protocol version information
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ProtocolVersion {
/// Major version (breaking changes)
pub major: u32,
/// Minor version (backward-compatible features)
pub minor: u32,
/// Patch version (bug fixes)
pub patch: u32,
/// Supported features
pub features: Vec<String>,
}
impl ProtocolVersion {
/// Current protocol version
pub fn current() -> Self {
Self {
major: 1,
minor: 0,
patch: 0,
features: vec![
"gossipsub".to_string(),
"kademlia".to_string(),
"task-negotiate".to_string(),
"noise-encryption".to_string(),
],
}
}
/// Check if this version is compatible with another
pub fn is_compatible(&self, other: &ProtocolVersion) -> bool {
// Same major version = compatible
self.major == other.major
}
}
// ============================================================================
// Message Validation
// ============================================================================
/// Validator for protocol messages
pub struct MessageValidator {
/// Maximum allowed message age in ms
max_message_age_ms: u64,
/// Minimum required stake for claims
min_claim_stake: u64,
/// Required proof types
required_proofs: Vec<String>,
}
impl Default for MessageValidator {
fn default() -> Self {
Self {
max_message_age_ms: 300_000, // 5 minutes
min_claim_stake: 100,
required_proofs: vec!["hash_chain".to_string()],
}
}
}
impl MessageValidator {
/// Validate a task request
pub fn validate_request(&self, request: &TaskRequest) -> Result<(), ValidationError> {
// Basic validation
if request.task_id.is_empty() {
return Err(ValidationError::EmptyTaskId);
}
if request.encrypted_payload.len() > 16 * 1024 * 1024 {
return Err(ValidationError::PayloadTooLarge);
}
Ok(())
}
/// Validate a work claim
pub fn validate_claim(&self, claim: &WorkClaim) -> Result<(), ValidationError> {
if claim.stake < self.min_claim_stake {
return Err(ValidationError::InsufficientStake {
required: self.min_claim_stake,
provided: claim.stake,
});
}
if claim.signature.len() != 64 {
return Err(ValidationError::InvalidSignature);
}
Ok(())
}
/// Validate a task result
pub fn validate_result(&self, result: &TaskResult) -> Result<(), ValidationError> {
if result.encrypted_result.is_empty() {
return Err(ValidationError::EmptyResult);
}
if result.signature.len() != 64 {
return Err(ValidationError::InvalidSignature);
}
// Validate proof type
match &result.proof {
ComputationProof::HashChain { intermediate_hashes, .. } => {
if intermediate_hashes.is_empty() {
return Err(ValidationError::InvalidProof("Empty hash chain".to_string()));
}
}
ComputationProof::MerkleProof { proof_path, .. } => {
if proof_path.is_empty() {
return Err(ValidationError::InvalidProof("Empty merkle proof".to_string()));
}
}
_ => {}
}
Ok(())
}
}
/// Validation errors
#[derive(Debug, Clone)]
pub enum ValidationError {
EmptyTaskId,
PayloadTooLarge,
InsufficientStake { required: u64, provided: u64 },
InvalidSignature,
EmptyResult,
InvalidProof(String),
MessageTooOld,
UnknownProofType,
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValidationError::EmptyTaskId => write!(f, "Empty task ID"),
ValidationError::PayloadTooLarge => write!(f, "Payload too large"),
ValidationError::InsufficientStake { required, provided } => {
write!(f, "Insufficient stake: {} required, {} provided", required, provided)
}
ValidationError::InvalidSignature => write!(f, "Invalid signature"),
ValidationError::EmptyResult => write!(f, "Empty result"),
ValidationError::InvalidProof(msg) => write!(f, "Invalid proof: {}", msg),
ValidationError::MessageTooOld => write!(f, "Message too old"),
ValidationError::UnknownProofType => write!(f, "Unknown proof type"),
}
}
}
impl std::error::Error for ValidationError {}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_task_codec_new() {
let codec = TaskCodec::new();
assert_eq!(codec.max_message_size, 16 * 1024 * 1024);
}
#[test]
fn test_task_codec_with_max_size() {
let codec = TaskCodec::with_max_size(1024);
assert_eq!(codec.max_message_size, 1024);
}
#[test]
fn test_task_details_serialization() {
let details = TaskDetails {
task_id: "task-123".to_string(),
task_type: "vectors".to_string(),
description: "Process vector batch".to_string(),
input_hash: [0u8; 32],
expected_output_size: 1024,
base_reward: 100,
early_bonus: 1.5,
deadline_ms: 1000000,
required_confirmations: 3,
submitter_stake: 500,
};
let serialized = bincode::serialize(&details).unwrap();
let deserialized: TaskDetails = bincode::deserialize(&serialized).unwrap();
assert_eq!(deserialized.task_id, "task-123");
assert_eq!(deserialized.base_reward, 100);
}
#[test]
fn test_work_claim_serialization() {
let claim = WorkClaim {
task_id: "task-123".to_string(),
worker_id: "worker-456".to_string(),
stake: 200,
estimated_time_ms: 5000,
capability_proof: vec![1, 2, 3],
signature: vec![0u8; 64],
};
let serialized = bincode::serialize(&claim).unwrap();
let deserialized: WorkClaim = bincode::deserialize(&serialized).unwrap();
assert_eq!(deserialized.worker_id, "worker-456");
assert_eq!(deserialized.stake, 200);
}
#[test]
fn test_computation_proof_variants() {
let hash_proof = ComputationProof::HashChain {
intermediate_hashes: vec![[1u8; 32], [2u8; 32]],
final_hash: [3u8; 32],
};
let merkle_proof = ComputationProof::MerkleProof {
root: [4u8; 32],
proof_path: vec![([5u8; 32], true), ([6u8; 32], false)],
};
// Both should serialize/deserialize
let serialized_hash = bincode::serialize(&hash_proof).unwrap();
let serialized_merkle = bincode::serialize(&merkle_proof).unwrap();
let _: ComputationProof = bincode::deserialize(&serialized_hash).unwrap();
let _: ComputationProof = bincode::deserialize(&serialized_merkle).unwrap();
}
#[test]
fn test_protocol_version() {
let v = ProtocolVersion::current();
assert_eq!(v.major, 1);
assert!(v.features.contains(&"gossipsub".to_string()));
}
#[test]
fn test_protocol_compatibility() {
let v1 = ProtocolVersion { major: 1, minor: 0, patch: 0, features: vec![] };
let v2 = ProtocolVersion { major: 1, minor: 1, patch: 0, features: vec![] };
let v3 = ProtocolVersion { major: 2, minor: 0, patch: 0, features: vec![] };
assert!(v1.is_compatible(&v2));
assert!(!v1.is_compatible(&v3));
}
#[test]
fn test_message_validator_default() {
let validator = MessageValidator::default();
assert_eq!(validator.max_message_age_ms, 300_000);
assert_eq!(validator.min_claim_stake, 100);
}
#[test]
fn test_validate_claim_insufficient_stake() {
let validator = MessageValidator::default();
let claim = WorkClaim {
task_id: "task-123".to_string(),
worker_id: "worker-456".to_string(),
stake: 50, // Below minimum
estimated_time_ms: 5000,
capability_proof: vec![],
signature: vec![0u8; 64],
};
let result = validator.validate_claim(&claim);
assert!(matches!(result, Err(ValidationError::InsufficientStake { .. })));
}
#[test]
fn test_validate_claim_success() {
let validator = MessageValidator::default();
let claim = WorkClaim {
task_id: "task-123".to_string(),
worker_id: "worker-456".to_string(),
stake: 200,
estimated_time_ms: 5000,
capability_proof: vec![],
signature: vec![0u8; 64],
};
assert!(validator.validate_claim(&claim).is_ok());
}
#[test]
fn test_execution_stats() {
let stats = ExecutionStats {
cpu_time_ms: 1000,
wall_time_ms: 1200,
peak_memory_bytes: 64 * 1024 * 1024,
operations: 1_000_000,
input_bytes: 4096,
output_bytes: 1024,
};
let serialized = bincode::serialize(&stats).unwrap();
let deserialized: ExecutionStats = bincode::deserialize(&serialized).unwrap();
assert_eq!(deserialized.cpu_time_ms, 1000);
assert_eq!(deserialized.operations, 1_000_000);
}
#[test]
fn test_dispute_types() {
let dispute = TaskDispute {
task_id: "task-123".to_string(),
disputer_id: "disputer-456".to_string(),
dispute_type: DisputeType::IncorrectResult,
evidence: vec![],
dispute_stake: 1000,
signature: vec![0u8; 64],
};
let serialized = bincode::serialize(&dispute).unwrap();
let deserialized: TaskDispute = bincode::deserialize(&serialized).unwrap();
assert!(matches!(deserialized.dispute_type, DisputeType::IncorrectResult));
}
#[test]
fn test_validation_error_display() {
let err = ValidationError::InsufficientStake { required: 100, provided: 50 };
let msg = err.to_string();
assert!(msg.contains("100"));
assert!(msg.contains("50"));
}
}
File diff suppressed because it is too large Load Diff