mirror of
https://github.com/ruvnet/RuView
synced 2026-08-04 19:31:42 +00:00
Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user