Skip to main content

ipfrs_network/
node.rs

1//! Network node implementation with full libp2p integration
2
3use dashmap::DashSet;
4use futures::StreamExt;
5use libp2p::{
6    autonat,
7    core::Transport as _,
8    dcutr, identify, identity, kad, mdns, noise, ping, relay,
9    swarm::{NetworkBehaviour, SwarmEvent},
10    Multiaddr, PeerId, Swarm,
11};
12use parking_lot::RwLock;
13use std::collections::HashMap;
14use std::fs;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use std::time::Duration;
18use tokio::sync::{mpsc, oneshot, Mutex};
19use tracing::{debug, info, warn};
20
21// Type alias for IPFRS results to avoid conflicts with libp2p types
22type IpfrsResult<T> = ipfrs_core::error::Result<T>;
23
24/// Type alias for provider waiters map to reduce type complexity
25type ProviderWaiters = Arc<Mutex<HashMap<String, Vec<oneshot::Sender<Vec<PeerId>>>>>>;
26
27/// Type alias for inference response waiters, keyed by session/request ID.
28///
29/// When `distributed_infer()` fires a request over GossipSub it registers a
30/// oneshot sender here; the event loop wakes it when a matching
31/// `InferenceResponse` arrives on the `INFERENCE_RESULT` topic.
32pub type InferenceWaiters =
33    Arc<Mutex<HashMap<String, Vec<oneshot::Sender<ipfrs_tensorlogic::InferenceResponse>>>>>;
34
35/// Kademlia DHT configuration
36#[derive(Debug, Clone)]
37pub struct KademliaConfig {
38    /// Query timeout in seconds
39    pub query_timeout_secs: u64,
40    /// Replication factor (k) - number of replicas to store
41    pub replication_factor: usize,
42    /// Alpha (α) - concurrency parameter for iterative queries
43    pub alpha: usize,
44    /// K-bucket size - maximum peers per bucket
45    pub kbucket_size: usize,
46}
47
48impl Default for KademliaConfig {
49    fn default() -> Self {
50        Self {
51            // Standard Kademlia timeout
52            query_timeout_secs: 60,
53            // IPFS uses 20 for replication
54            replication_factor: 20,
55            // Standard Kademlia alpha (3 is common, IPFS uses 3)
56            alpha: 3,
57            // Standard Kademlia k-bucket size (20 is standard)
58            kbucket_size: 20,
59        }
60    }
61}
62
63/// Network configuration
64#[derive(Debug, Clone)]
65pub struct NetworkConfig {
66    /// Listen addresses
67    pub listen_addrs: Vec<String>,
68    /// Bootstrap peers
69    pub bootstrap_peers: Vec<String>,
70    /// Enable QUIC transport
71    pub enable_quic: bool,
72    /// Data directory
73    pub data_dir: PathBuf,
74    /// Enable mDNS peer discovery
75    pub enable_mdns: bool,
76    /// Enable NAT traversal (AutoNAT + DCUtR)
77    pub enable_nat_traversal: bool,
78    /// Relay server addresses for NAT traversal
79    pub relay_servers: Vec<String>,
80    /// Kademlia DHT configuration
81    pub kademlia: KademliaConfig,
82    /// Maximum number of concurrent connections (None = unlimited)
83    pub max_connections: Option<usize>,
84    /// Maximum number of inbound connections (None = unlimited)
85    pub max_inbound_connections: Option<usize>,
86    /// Maximum number of outbound connections (None = unlimited)
87    pub max_outbound_connections: Option<usize>,
88    /// Connection buffer size in bytes
89    pub connection_buffer_size: usize,
90    /// Enable aggressive memory optimizations
91    pub low_memory_mode: bool,
92    /// Enable DCUtR hole-punching (default: true)
93    ///
94    /// When true the `dcutr` behaviour actively participates in NAT hole-punch
95    /// coordination with peers.  Disabling this is useful for low-memory
96    /// constrained environments where the small overhead of maintaining the
97    /// DCUtR state machine is undesirable.
98    pub dcutr_enabled: bool,
99    /// Enable Circuit Relay v2 client behaviour (default: true)
100    ///
101    /// The relay client transport is always compiled in (because removing it
102    /// from the combined transport would break the swarm type), but this flag
103    /// controls whether the node actively seeks relay reservations.
104    pub relay_v2_enabled: bool,
105    /// Timeout for a single hole-punch attempt (default: 30 s)
106    pub hole_punch_timeout: Duration,
107}
108
109impl Default for NetworkConfig {
110    fn default() -> Self {
111        Self {
112            listen_addrs: vec![
113                "/ip4/0.0.0.0/udp/0/quic-v1".to_string(),
114                "/ip6/::/udp/0/quic-v1".to_string(),
115            ],
116            bootstrap_peers: vec![],
117            enable_quic: true,
118            enable_mdns: false,
119            enable_nat_traversal: true,
120            relay_servers: vec![],
121            data_dir: PathBuf::from(".ipfrs"),
122            kademlia: KademliaConfig::default(),
123            max_connections: None,
124            max_inbound_connections: None,
125            max_outbound_connections: None,
126            connection_buffer_size: 64 * 1024, // 64 KB default
127            low_memory_mode: false,
128            // NAT traversal defaults – enabled by default for production use
129            dcutr_enabled: true,
130            relay_v2_enabled: true,
131            hole_punch_timeout: Duration::from_secs(30),
132        }
133    }
134}
135
136impl NetworkConfig {
137    /// Create a low-memory configuration for constrained devices
138    ///
139    /// This configuration minimizes memory usage at the cost of some features:
140    /// - Limited to 16 total connections
141    /// - Smaller connection buffers (8 KB)
142    /// - Reduced DHT parameters
143    /// - mDNS disabled
144    /// - NAT traversal disabled
145    ///
146    /// Suitable for embedded devices with < 128 MB RAM
147    pub fn low_memory() -> Self {
148        Self {
149            listen_addrs: vec!["/ip4/0.0.0.0/udp/0/quic-v1".to_string()],
150            bootstrap_peers: vec![],
151            enable_quic: true,
152            enable_mdns: false,          // Disabled to save memory
153            enable_nat_traversal: false, // Disabled to save memory
154            relay_servers: vec![],
155            data_dir: PathBuf::from(".ipfrs"),
156            kademlia: KademliaConfig {
157                query_timeout_secs: 30, // Shorter timeout
158                replication_factor: 10, // Reduced from 20
159                alpha: 2,               // Reduced from 3
160                kbucket_size: 10,       // Reduced from 20
161            },
162            max_connections: Some(16), // Very limited connections
163            max_inbound_connections: Some(8),
164            max_outbound_connections: Some(8),
165            connection_buffer_size: 8 * 1024, // 8 KB buffers
166            low_memory_mode: true,
167            // NAT traversal disabled for low-memory environments
168            dcutr_enabled: false,
169            relay_v2_enabled: false,
170            hole_punch_timeout: Duration::from_secs(30),
171        }
172    }
173
174    /// Create an IoT device configuration
175    ///
176    /// Balanced configuration for IoT devices:
177    /// - Limited to 32 total connections
178    /// - Moderate connection buffers (16 KB)
179    /// - Reduced DHT parameters
180    /// - mDNS enabled for local discovery
181    /// - NAT traversal enabled
182    ///
183    /// Suitable for IoT devices with 128-512 MB RAM
184    pub fn iot() -> Self {
185        Self {
186            listen_addrs: vec!["/ip4/0.0.0.0/udp/0/quic-v1".to_string()],
187            bootstrap_peers: vec![],
188            enable_quic: true,
189            enable_mdns: true, // Local discovery useful for IoT
190            enable_nat_traversal: true,
191            relay_servers: vec![],
192            data_dir: PathBuf::from(".ipfrs"),
193            kademlia: KademliaConfig {
194                query_timeout_secs: 45,
195                replication_factor: 15,
196                alpha: 2,
197                kbucket_size: 15,
198            },
199            max_connections: Some(32),
200            max_inbound_connections: Some(16),
201            max_outbound_connections: Some(16),
202            connection_buffer_size: 16 * 1024, // 16 KB buffers
203            low_memory_mode: false,
204            dcutr_enabled: true,
205            relay_v2_enabled: true,
206            hole_punch_timeout: Duration::from_secs(30),
207        }
208    }
209
210    /// Create a mobile device configuration
211    ///
212    /// Power and bandwidth-aware configuration for mobile devices:
213    /// - Limited to 64 total connections
214    /// - Standard connection buffers (32 KB)
215    /// - Standard DHT parameters
216    /// - mDNS disabled (battery saving)
217    /// - NAT traversal enabled
218    ///
219    /// Suitable for mobile devices with network switching
220    pub fn mobile() -> Self {
221        Self {
222            listen_addrs: vec!["/ip4/0.0.0.0/udp/0/quic-v1".to_string()],
223            bootstrap_peers: vec![],
224            enable_quic: true,
225            enable_mdns: false, // Battery saving
226            enable_nat_traversal: true,
227            relay_servers: vec![],
228            data_dir: PathBuf::from(".ipfrs"),
229            kademlia: KademliaConfig {
230                query_timeout_secs: 60,
231                replication_factor: 20,
232                alpha: 3,
233                kbucket_size: 20,
234            },
235            dcutr_enabled: true,
236            relay_v2_enabled: true,
237            hole_punch_timeout: Duration::from_secs(30),
238            max_connections: Some(64),
239            max_inbound_connections: Some(32),
240            max_outbound_connections: Some(32),
241            connection_buffer_size: 32 * 1024, // 32 KB buffers
242            low_memory_mode: false,
243        }
244    }
245
246    /// Create a high-performance configuration for servers
247    ///
248    /// Optimized for high throughput and many connections:
249    /// - Unlimited connections
250    /// - Large connection buffers (128 KB)
251    /// - Aggressive DHT parameters
252    /// - All features enabled
253    ///
254    /// Suitable for servers with > 2 GB RAM
255    pub fn high_performance() -> Self {
256        Self {
257            listen_addrs: vec![
258                "/ip4/0.0.0.0/udp/0/quic-v1".to_string(),
259                "/ip6/::/udp/0/quic-v1".to_string(),
260            ],
261            bootstrap_peers: vec![],
262            enable_quic: true,
263            enable_mdns: true,
264            enable_nat_traversal: true,
265            relay_servers: vec![],
266            data_dir: PathBuf::from(".ipfrs"),
267            kademlia: KademliaConfig {
268                query_timeout_secs: 60,
269                replication_factor: 20,
270                alpha: 3,
271                kbucket_size: 20,
272            },
273            max_connections: None, // Unlimited
274            max_inbound_connections: None,
275            max_outbound_connections: None,
276            connection_buffer_size: 128 * 1024, // 128 KB buffers
277            low_memory_mode: false,
278            dcutr_enabled: true,
279            relay_v2_enabled: true,
280            hole_punch_timeout: Duration::from_secs(30),
281        }
282    }
283}
284
285/// Network behavior combining multiple protocols
286#[derive(NetworkBehaviour)]
287#[behaviour(to_swarm = "IpfrsBehaviourEvent")]
288pub struct IpfrsBehaviour {
289    /// Kademlia DHT for content and peer discovery
290    pub kademlia: kad::Behaviour<kad::store::MemoryStore>,
291    /// Identify protocol for peer information
292    pub identify: identify::Behaviour,
293    /// Ping protocol for connectivity checks
294    pub ping: ping::Behaviour,
295    /// AutoNAT for NAT detection and address confirmation
296    pub autonat: autonat::Behaviour,
297    /// DCUtR for hole punching through NAT
298    pub dcutr: dcutr::Behaviour,
299    /// mDNS for local network peer discovery
300    pub mdns: mdns::tokio::Behaviour,
301    /// Relay client for NAT traversal
302    pub relay_client: relay::client::Behaviour,
303}
304
305/// Events generated by IpfrsBehaviour
306#[derive(Debug)]
307pub enum IpfrsBehaviourEvent {
308    Kademlia(kad::Event),
309    Identify(Box<identify::Event>),
310    Ping(ping::Event),
311    Autonat(autonat::Event),
312    Dcutr(dcutr::Event),
313    Mdns(mdns::Event),
314    RelayClient(relay::client::Event),
315}
316
317impl From<kad::Event> for IpfrsBehaviourEvent {
318    fn from(event: kad::Event) -> Self {
319        IpfrsBehaviourEvent::Kademlia(event)
320    }
321}
322
323impl From<identify::Event> for IpfrsBehaviourEvent {
324    fn from(event: identify::Event) -> Self {
325        IpfrsBehaviourEvent::Identify(Box::new(event))
326    }
327}
328
329impl From<ping::Event> for IpfrsBehaviourEvent {
330    fn from(event: ping::Event) -> Self {
331        IpfrsBehaviourEvent::Ping(event)
332    }
333}
334
335impl From<autonat::Event> for IpfrsBehaviourEvent {
336    fn from(event: autonat::Event) -> Self {
337        IpfrsBehaviourEvent::Autonat(event)
338    }
339}
340
341impl From<dcutr::Event> for IpfrsBehaviourEvent {
342    fn from(event: dcutr::Event) -> Self {
343        IpfrsBehaviourEvent::Dcutr(event)
344    }
345}
346
347impl From<mdns::Event> for IpfrsBehaviourEvent {
348    fn from(event: mdns::Event) -> Self {
349        IpfrsBehaviourEvent::Mdns(event)
350    }
351}
352
353impl From<relay::client::Event> for IpfrsBehaviourEvent {
354    fn from(event: relay::client::Event) -> Self {
355        IpfrsBehaviourEvent::RelayClient(event)
356    }
357}
358
359/// Commands forwarded from `NetworkNode` to the background swarm event loop.
360///
361/// After `start()` the swarm lives in a spawned task.  All operations that
362/// need to call into the swarm (dial, provide, get_providers, …) are sent
363/// over this channel and executed inside the event-loop task.
364enum SwarmCommand {
365    /// Dial a remote address
366    Dial(Multiaddr),
367    /// Disconnect a specific peer
368    Disconnect(PeerId),
369    /// Announce local content to the Kademlia DHT
370    Provide(cid::Cid),
371    /// Query the DHT for providers of a CID (fire-and-forget; waiters handle the result)
372    GetProviders(cid::Cid),
373    /// Ask the Kademlia routing table for the k-closest peers to our own ID
374    Bootstrap,
375    /// Add a peer address to the Kademlia routing table
376    AddPeerAddress(PeerId, Multiaddr),
377}
378
379/// Circuit Relay v2 configuration.
380///
381/// Controls whether the node actively seeks relay reservations for NAT
382/// traversal via the `/libp2p/circuit/relay/0.2.0/hop` protocol.
383#[derive(Debug, Clone)]
384pub struct RelayConfig {
385    /// Enable Circuit Relay v2 client (reservation) support.
386    pub relay_v2_enabled: bool,
387    /// Maximum number of simultaneous relay reservations to maintain.
388    pub max_reservations: usize,
389    /// Duration in seconds for which a relay reservation is considered valid.
390    pub reservation_duration_secs: u64,
391}
392
393impl Default for RelayConfig {
394    fn default() -> Self {
395        Self {
396            relay_v2_enabled: true,
397            max_reservations: 4,
398            reservation_duration_secs: 3600,
399        }
400    }
401}
402
403/// IPFRS network node
404pub struct NetworkNode {
405    config: NetworkConfig,
406    peer_id: PeerId,
407    swarm: Option<Swarm<IpfrsBehaviour>>,
408    shutdown_tx: Option<mpsc::Sender<()>>,
409    /// Command channel to the background swarm event loop (set after `start()`)
410    swarm_cmd_tx: Option<mpsc::Sender<SwarmCommand>>,
411    event_tx: mpsc::Sender<NetworkEvent>,
412    event_rx: Option<mpsc::Receiver<NetworkEvent>>,
413    /// External addresses discovered via AutoNAT
414    external_addrs: Arc<parking_lot::RwLock<Vec<Multiaddr>>>,
415    /// Set of currently connected peers
416    connected_peers: Arc<DashSet<PeerId>>,
417    /// Bandwidth tracking (bytes sent/received)
418    bandwidth_stats: Arc<parking_lot::RwLock<BandwidthStats>>,
419    /// Waiters for provider query results, keyed by CID string
420    provider_waiters: ProviderWaiters,
421    /// NAT traversal (DCUtR hole-punch) metrics
422    nat_metrics: Arc<parking_lot::RwLock<NatTraversalMetrics>>,
423    /// In-process GossipSub manager for topic-based pub/sub messaging.
424    ///
425    /// Shared with callers so that external code (e.g. `distributed_infer`)
426    /// can publish messages directly without going through the swarm command
427    /// channel.  The manager is `Arc`-wrapped so it can be cloned cheaply.
428    pub gossipsub: Arc<crate::gossipsub::GossipSubManager>,
429    /// Waiters for inference responses, keyed by request/session ID.
430    pub inference_waiters: InferenceWaiters,
431    /// Active Circuit Relay v2 reservations, keyed by relay peer ID.
432    ///
433    /// Each entry records the [`std::time::Instant`] at which the reservation
434    /// was obtained so that expired reservations can be detected and renewed.
435    pub active_relay_reservations: Arc<parking_lot::RwLock<HashMap<PeerId, std::time::Instant>>>,
436    /// Circuit Relay v2 configuration.
437    pub relay_config: RelayConfig,
438}
439
440/// Bandwidth statistics
441#[derive(Debug, Clone, Default)]
442struct BandwidthStats {
443    bytes_sent: u64,
444    bytes_received: u64,
445}
446
447/// Network events
448#[derive(Debug, Clone)]
449pub enum NetworkEvent {
450    /// Peer connected
451    PeerConnected {
452        peer_id: PeerId,
453        endpoint: ConnectionEndpoint,
454        established_in: std::time::Duration,
455    },
456    /// Peer disconnected
457    PeerDisconnected {
458        peer_id: PeerId,
459        cause: Option<String>,
460    },
461    /// Content found in DHT
462    ContentFound { cid: String, providers: Vec<PeerId> },
463    /// New peer discovered
464    PeerDiscovered {
465        peer_id: PeerId,
466        addrs: Vec<Multiaddr>,
467    },
468    /// Listening on a new address
469    ListeningOn { address: Multiaddr },
470    /// Connection error occurred
471    ConnectionError {
472        peer_id: Option<PeerId>,
473        error: String,
474    },
475    /// DHT bootstrap completed
476    DhtBootstrapCompleted,
477    /// NAT status changed
478    NatStatusChanged {
479        old_status: String,
480        new_status: String,
481    },
482}
483
484/// Connection endpoint information
485#[derive(Debug, Clone)]
486pub enum ConnectionEndpoint {
487    /// Dialer (outbound connection)
488    Dialer { address: Multiaddr },
489    /// Listener (inbound connection)
490    Listener {
491        local_addr: Multiaddr,
492        send_back_addr: Multiaddr,
493    },
494}
495
496/// Default keypair filename
497const KEYPAIR_FILENAME: &str = "identity.key";
498
499impl NetworkNode {
500    /// Create a new network node
501    pub fn new(config: NetworkConfig) -> IpfrsResult<Self> {
502        info!("Creating network node with libp2p");
503
504        // Load or generate keypair for stable identity
505        let keypair = Self::load_or_generate_keypair(&config.data_dir)?;
506        let peer_id = keypair.public().to_peer_id();
507
508        info!("Local peer ID: {}", peer_id);
509
510        // Create event channel
511        let (event_tx, event_rx) = mpsc::channel(1024);
512
513        // Build the swarm
514        let swarm = Self::build_swarm(keypair, &config)?;
515
516        // Build the GossipSub manager and subscribe to all inference topics.
517        let gossipsub = {
518            use crate::gossipsub::{GossipSubConfig, GossipSubManager};
519            let mgr = GossipSubManager::new(GossipSubConfig::default());
520            // Ignore errors – AlreadySubscribed is harmless on a fresh instance.
521            let _ = mgr.subscribe_inference_topics();
522            Arc::new(mgr)
523        };
524
525        Ok(Self {
526            config,
527            peer_id,
528            swarm: Some(swarm),
529            shutdown_tx: None,
530            swarm_cmd_tx: None,
531            event_tx,
532            event_rx: Some(event_rx),
533            external_addrs: Arc::new(RwLock::new(Vec::new())),
534            connected_peers: Arc::new(DashSet::new()),
535            bandwidth_stats: Arc::new(RwLock::new(BandwidthStats::default())),
536            provider_waiters: Arc::new(Mutex::new(HashMap::new())),
537            nat_metrics: Arc::new(RwLock::new(NatTraversalMetrics::default())),
538            gossipsub,
539            inference_waiters: Arc::new(Mutex::new(HashMap::new())),
540            active_relay_reservations: Arc::new(RwLock::new(HashMap::new())),
541            relay_config: RelayConfig::default(),
542        })
543    }
544
545    /// Load existing keypair or generate a new one
546    fn load_or_generate_keypair(data_dir: &Path) -> IpfrsResult<identity::Keypair> {
547        let key_path = data_dir.join(KEYPAIR_FILENAME);
548
549        if key_path.exists() {
550            info!("Loading existing identity from {:?}", key_path);
551            Self::load_keypair(&key_path)
552        } else {
553            info!("Generating new identity");
554            let keypair = identity::Keypair::generate_ed25519();
555
556            // Create data directory if it doesn't exist
557            if !data_dir.exists() {
558                fs::create_dir_all(data_dir).map_err(ipfrs_core::error::Error::Io)?;
559            }
560
561            // Save the new keypair
562            Self::save_keypair(&keypair, &key_path)?;
563            info!("Saved new identity to {:?}", key_path);
564
565            Ok(keypair)
566        }
567    }
568
569    /// Load keypair from file
570    fn load_keypair(path: &Path) -> IpfrsResult<identity::Keypair> {
571        let bytes = fs::read(path).map_err(ipfrs_core::error::Error::Io)?;
572
573        identity::Keypair::from_protobuf_encoding(&bytes).map_err(|e| {
574            ipfrs_core::error::Error::Network(format!("Failed to decode keypair: {}", e))
575        })
576    }
577
578    /// Save keypair to file
579    fn save_keypair(keypair: &identity::Keypair, path: &Path) -> IpfrsResult<()> {
580        let bytes = keypair.to_protobuf_encoding().map_err(|e| {
581            ipfrs_core::error::Error::Network(format!("Failed to encode keypair: {}", e))
582        })?;
583
584        fs::write(path, bytes).map_err(ipfrs_core::error::Error::Io)?;
585
586        // Set restrictive permissions on Unix
587        #[cfg(unix)]
588        {
589            use std::os::unix::fs::PermissionsExt;
590            let permissions = fs::Permissions::from_mode(0o600);
591            fs::set_permissions(path, permissions).map_err(ipfrs_core::error::Error::Io)?;
592        }
593
594        Ok(())
595    }
596
597    /// Build libp2p swarm with all protocols
598    #[allow(clippy::too_many_lines)]
599    fn build_swarm(
600        keypair: identity::Keypair,
601        config: &NetworkConfig,
602    ) -> IpfrsResult<Swarm<IpfrsBehaviour>> {
603        let peer_id = keypair.public().to_peer_id();
604
605        // Create relay client for NAT traversal.
606        // The relay *transport* must remain alive alongside the relay *behaviour*
607        // because they communicate via an internal channel.  Including it in the
608        // combined transport (even when nat-traversal is disabled) keeps the
609        // channel open and prevents the "polled after channel closed" panic.
610        let (relay_transport, relay_client) = relay::client::new(peer_id);
611
612        // Upgrade the relay transport so it can be combined with the others
613        let relay_transport = relay_transport
614            .upgrade(libp2p::core::upgrade::Version::V1)
615            .authenticate(noise::Config::new(&keypair).map_err(std::io::Error::other)?)
616            .multiplex(libp2p::yamux::Config::default())
617            .map(|(peer_id, muxer), _| (peer_id, libp2p::core::muxing::StreamMuxerBox::new(muxer)));
618
619        // Build TCP transport with noise and yamux
620        let tcp_transport = libp2p::tcp::tokio::Transport::default()
621            .upgrade(libp2p::core::upgrade::Version::V1)
622            .authenticate(noise::Config::new(&keypair).map_err(std::io::Error::other)?)
623            .multiplex(libp2p::yamux::Config::default())
624            .map(|(peer_id, muxer), _| (peer_id, libp2p::core::muxing::StreamMuxerBox::new(muxer)));
625
626        // Build QUIC transport
627        let quic_transport = libp2p::quic::tokio::Transport::new(libp2p::quic::Config::new(
628            &keypair,
629        ))
630        .map(|(peer_id, muxer), _| (peer_id, libp2p::core::muxing::StreamMuxerBox::new(muxer)));
631
632        // Combine transports: relay (for circuit relay v2) → QUIC → TCP
633        // The relay transport handles /p2p-circuit addresses; QUIC and TCP handle
634        // direct connections.  Order matters: relay is tried first for circuit
635        // addresses, then QUIC, then TCP.
636        let transport = if config.enable_quic {
637            relay_transport
638                .or_transport(quic_transport)
639                .map(|either, _| either.into_inner())
640                .or_transport(tcp_transport)
641                .map(|either, _| either.into_inner())
642                .boxed()
643        } else {
644            relay_transport
645                .or_transport(tcp_transport)
646                .map(|either, _| either.into_inner())
647                .boxed()
648        };
649
650        // Create Kademlia DHT with tunable config
651        let store = kad::store::MemoryStore::new(peer_id);
652        let mut kad_config = kad::Config::default();
653
654        // Apply tunable parameters
655        kad_config.set_query_timeout(Duration::from_secs(config.kademlia.query_timeout_secs));
656        kad_config.set_replication_factor(
657            std::num::NonZeroUsize::new(config.kademlia.replication_factor)
658                .expect("Replication factor must be > 0"),
659        );
660        kad_config.set_parallelism(
661            std::num::NonZeroUsize::new(config.kademlia.alpha).expect("Alpha must be > 0"),
662        );
663        kad_config.set_kbucket_inserts(kad::BucketInserts::OnConnected);
664
665        // Set max k-bucket size if possible (note: libp2p has a fixed K=20 in current versions)
666        // The kbucket_size parameter is kept for future compatibility
667
668        let kademlia = kad::Behaviour::with_config(peer_id, store, kad_config);
669
670        // Create Identify protocol
671        let identify = identify::Behaviour::new(
672            identify::Config::new("/ipfrs/1.0.0".to_string(), keypair.public())
673                .with_agent_version(format!("ipfrs/{}", env!("CARGO_PKG_VERSION"))),
674        );
675
676        // Create Ping protocol for connectivity checks
677        let ping = ping::Behaviour::new(ping::Config::new().with_interval(Duration::from_secs(15)));
678
679        // Create AutoNAT for NAT detection
680        let autonat = autonat::Behaviour::new(
681            peer_id,
682            autonat::Config {
683                only_global_ips: false,
684                ..Default::default()
685            },
686        );
687
688        // Create DCUtR for hole punching
689        let dcutr = dcutr::Behaviour::new(peer_id);
690
691        // Create mDNS for local network discovery (if enabled)
692        let mdns = if config.enable_mdns {
693            mdns::tokio::Behaviour::new(mdns::Config::default(), peer_id)
694                .map_err(|e| ipfrs_core::error::Error::Network(e.to_string()))?
695        } else {
696            // Disabled mDNS - still need to create one but it won't discover
697            mdns::tokio::Behaviour::new(
698                mdns::Config {
699                    ttl: Duration::from_secs(1),
700                    query_interval: Duration::from_secs(3600), // Very long interval
701                    enable_ipv6: false,
702                },
703                peer_id,
704            )
705            .map_err(|e| ipfrs_core::error::Error::Network(e.to_string()))?
706        };
707
708        // Combine into network behavior
709        let behaviour = IpfrsBehaviour {
710            kademlia,
711            identify,
712            ping,
713            autonat,
714            dcutr,
715            mdns,
716            relay_client,
717        };
718
719        // Create swarm with tokio executor
720        let mut swarm_config = libp2p::swarm::Config::with_executor(|fut| {
721            tokio::spawn(fut);
722        });
723        swarm_config = swarm_config.with_idle_connection_timeout(Duration::from_secs(60));
724
725        let swarm = Swarm::new(transport, behaviour, peer_id, swarm_config);
726
727        Ok(swarm)
728    }
729
730    /// Start the network node
731    pub async fn start(&mut self) -> IpfrsResult<()> {
732        info!("🚀 IPFRS Network Node Starting");
733        info!("   Peer ID: {}", self.peer_id);
734        info!("   QUIC enabled: {}", self.config.enable_quic);
735
736        let mut swarm = self.swarm.take().ok_or_else(|| {
737            ipfrs_core::error::Error::Network("Swarm already started".to_string())
738        })?;
739
740        // Listen on configured addresses
741        for addr_str in &self.config.listen_addrs {
742            let addr: Multiaddr = addr_str.parse().map_err(|e| {
743                ipfrs_core::error::Error::Network(format!("Invalid multiaddr: {}", e))
744            })?;
745
746            swarm
747                .listen_on(addr.clone())
748                .map_err(|e| ipfrs_core::error::Error::Network(e.to_string()))?;
749
750            info!("   Listening on: {}", addr);
751        }
752
753        // Bootstrap DHT with configured peers
754        for peer_str in &self.config.bootstrap_peers {
755            match peer_str.parse::<Multiaddr>() {
756                Ok(addr) => {
757                    if let Err(e) = swarm.dial(addr.clone()) {
758                        warn!("Failed to dial bootstrap peer {}: {}", addr, e);
759                    } else {
760                        info!("   Dialing bootstrap peer: {}", addr);
761                    }
762                }
763                Err(e) => {
764                    warn!("Invalid bootstrap peer address {}: {}", peer_str, e);
765                }
766            }
767        }
768
769        // Put DHT into server mode
770        swarm
771            .behaviour_mut()
772            .kademlia
773            .set_mode(Some(kad::Mode::Server));
774
775        // Bootstrap the DHT
776        if let Err(e) = swarm.behaviour_mut().kademlia.bootstrap() {
777            warn!("DHT bootstrap failed: {}", e);
778        }
779
780        // Create shutdown channel
781        let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
782        self.shutdown_tx = Some(shutdown_tx);
783
784        // Create swarm command channel so callers can drive the swarm from
785        // outside the event-loop task after start().
786        let (swarm_cmd_tx, mut swarm_cmd_rx) = mpsc::channel::<SwarmCommand>(256);
787        self.swarm_cmd_tx = Some(swarm_cmd_tx);
788
789        let event_tx = self.event_tx.clone();
790        let external_addrs = Arc::clone(&self.external_addrs);
791        let connected_peers = Arc::clone(&self.connected_peers);
792        let provider_waiters = Arc::clone(&self.provider_waiters);
793        let nat_metrics = Arc::clone(&self.nat_metrics);
794
795        info!("✅ Network node ready");
796        info!(
797            "   Transport: {}",
798            if self.config.enable_quic {
799                "QUIC"
800            } else {
801                "TCP"
802            }
803        );
804        info!("   DHT mode: Server");
805
806        // Event loop
807        tokio::spawn(async move {
808            loop {
809                tokio::select! {
810                    event = swarm.select_next_some() => {
811                        Self::handle_swarm_event(event, &event_tx, swarm.behaviour_mut(), &external_addrs, &connected_peers, &provider_waiters, &nat_metrics).await;
812                    }
813                    Some(cmd) = swarm_cmd_rx.recv() => {
814                        Self::handle_swarm_command(cmd, &mut swarm, &provider_waiters).await;
815                    }
816                    _ = shutdown_rx.recv() => {
817                        info!("Shutting down network node");
818                        break;
819                    }
820                }
821            }
822        });
823
824        Ok(())
825    }
826
827    /// Handle swarm events
828    async fn handle_swarm_event(
829        event: SwarmEvent<IpfrsBehaviourEvent>,
830        event_tx: &mpsc::Sender<NetworkEvent>,
831        _behaviour: &mut IpfrsBehaviour,
832        external_addrs: &Arc<RwLock<Vec<Multiaddr>>>,
833        connected_peers: &Arc<DashSet<PeerId>>,
834        provider_waiters: &ProviderWaiters,
835        nat_metrics: &Arc<RwLock<NatTraversalMetrics>>,
836    ) {
837        match event {
838            SwarmEvent::NewListenAddr { address, .. } => {
839                info!("Listening on {}", address);
840                let _ = event_tx
841                    .send(NetworkEvent::ListeningOn {
842                        address: address.clone(),
843                    })
844                    .await;
845            }
846            SwarmEvent::Behaviour(IpfrsBehaviourEvent::Identify(ev)) => {
847                if let identify::Event::Received { peer_id, info, .. } = *ev {
848                    debug!("Identified peer {}: {:?}", peer_id, info);
849                    let _ = event_tx
850                        .send(NetworkEvent::PeerDiscovered {
851                            peer_id,
852                            addrs: info.listen_addrs,
853                        })
854                        .await;
855                }
856            }
857            SwarmEvent::Behaviour(IpfrsBehaviourEvent::Kademlia(
858                kad::Event::OutboundQueryProgressed { result, .. },
859            )) => match result {
860                kad::QueryResult::GetProviders(Ok(kad::GetProvidersOk::FoundProviders {
861                    key,
862                    providers,
863                })) => {
864                    let cid = String::from_utf8_lossy(key.as_ref()).to_string();
865                    let provider_list: Vec<PeerId> = providers.into_iter().collect();
866                    debug!("Found {} providers for {}", provider_list.len(), cid);
867
868                    // Notify any registered waiters for this CID
869                    {
870                        let mut waiters = provider_waiters.lock().await;
871                        if let Some(senders) = waiters.remove(&cid) {
872                            for tx in senders {
873                                // Best-effort: ignore send errors (receiver may have timed out)
874                                let _ = tx.send(provider_list.clone());
875                            }
876                        }
877                    }
878
879                    let _ = event_tx
880                        .send(NetworkEvent::ContentFound {
881                            cid,
882                            providers: provider_list,
883                        })
884                        .await;
885                }
886                kad::QueryResult::GetProviders(Err(e)) => {
887                    debug!("GetProviders query failed: {:?}", e);
888                }
889                kad::QueryResult::Bootstrap(Ok(_)) => {
890                    info!("DHT bootstrap completed");
891                    let _ = event_tx.send(NetworkEvent::DhtBootstrapCompleted).await;
892                }
893                kad::QueryResult::Bootstrap(Err(e)) => {
894                    warn!("DHT bootstrap failed: {:?}", e);
895                }
896                _ => {}
897            },
898            SwarmEvent::ConnectionEstablished {
899                peer_id,
900                endpoint,
901                established_in,
902                ..
903            } => {
904                info!("Connected to peer: {} in {:?}", peer_id, established_in);
905
906                // Track connected peer
907                connected_peers.insert(peer_id);
908
909                let conn_endpoint = if endpoint.is_dialer() {
910                    ConnectionEndpoint::Dialer {
911                        address: endpoint.get_remote_address().clone(),
912                    }
913                } else {
914                    ConnectionEndpoint::Listener {
915                        local_addr: endpoint.get_remote_address().clone(),
916                        send_back_addr: endpoint.get_remote_address().clone(),
917                    }
918                };
919
920                let _ = event_tx
921                    .send(NetworkEvent::PeerConnected {
922                        peer_id,
923                        endpoint: conn_endpoint,
924                        established_in,
925                    })
926                    .await;
927            }
928            SwarmEvent::ConnectionClosed {
929                peer_id,
930                cause,
931                num_established,
932                ..
933            } => {
934                info!("Disconnected from peer {}: {:?}", peer_id, cause);
935
936                // Remove peer from tracking if no more connections remain
937                if num_established == 0 {
938                    connected_peers.remove(&peer_id);
939                }
940
941                let _ = event_tx
942                    .send(NetworkEvent::PeerDisconnected {
943                        peer_id,
944                        cause: cause.map(|c| format!("{:?}", c)),
945                    })
946                    .await;
947            }
948            SwarmEvent::IncomingConnection { .. } => {
949                debug!("Incoming connection");
950            }
951            SwarmEvent::IncomingConnectionError { error, .. } => {
952                debug!("Incoming connection error: {}", error);
953                let _ = event_tx
954                    .send(NetworkEvent::ConnectionError {
955                        peer_id: None,
956                        error: error.to_string(),
957                    })
958                    .await;
959            }
960            SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => {
961                warn!("Outgoing connection error to {:?}: {}", peer_id, error);
962                let _ = event_tx
963                    .send(NetworkEvent::ConnectionError {
964                        peer_id,
965                        error: error.to_string(),
966                    })
967                    .await;
968            }
969            SwarmEvent::Behaviour(IpfrsBehaviourEvent::Autonat(autonat_event)) => {
970                match autonat_event {
971                    autonat::Event::InboundProbe(_) => {
972                        debug!("AutoNAT inbound probe");
973                    }
974                    autonat::Event::OutboundProbe(_) => {
975                        debug!("AutoNAT outbound probe");
976                    }
977                    autonat::Event::StatusChanged { old, new } => {
978                        info!("AutoNAT status changed from {:?} to {:?}", old, new);
979
980                        let old_status = format!("{:?}", old);
981                        let new_status = format!("{:?}", new);
982
983                        let _ = event_tx
984                            .send(NetworkEvent::NatStatusChanged {
985                                old_status,
986                                new_status,
987                            })
988                            .await;
989
990                        match new {
991                            autonat::NatStatus::Public(addr) => {
992                                info!("Public address confirmed: {}", addr);
993                                // Track external address
994                                let mut addrs = external_addrs.write();
995                                if !addrs.contains(&addr) {
996                                    addrs.push(addr);
997                                }
998                            }
999                            autonat::NatStatus::Private => {
1000                                info!("Node is behind NAT");
1001                                // Clear external addresses when behind NAT
1002                                external_addrs.write().clear();
1003                            }
1004                            autonat::NatStatus::Unknown => {
1005                                debug!("NAT status unknown");
1006                            }
1007                        }
1008                    }
1009                }
1010            }
1011            SwarmEvent::Behaviour(IpfrsBehaviourEvent::Dcutr(dcutr_event)) => {
1012                debug!("DCUtR event: {:?}", dcutr_event);
1013                match dcutr_event {
1014                    dcutr::Event { result: Ok(_), .. } => {
1015                        let mut m = nat_metrics.write();
1016                        m.hole_punch_attempts = m.hole_punch_attempts.saturating_add(1);
1017                        m.hole_punch_successes = m.hole_punch_successes.saturating_add(1);
1018                        info!("DCUtR hole-punch succeeded");
1019                    }
1020                    dcutr::Event {
1021                        result: Err(ref e), ..
1022                    } => {
1023                        let mut m = nat_metrics.write();
1024                        m.hole_punch_attempts = m.hole_punch_attempts.saturating_add(1);
1025                        m.hole_punch_failures = m.hole_punch_failures.saturating_add(1);
1026                        warn!("DCUtR hole-punch failed: {}", e);
1027                    }
1028                }
1029            }
1030            SwarmEvent::Behaviour(IpfrsBehaviourEvent::Mdns(mdns_event)) => match mdns_event {
1031                mdns::Event::Discovered(peers) => {
1032                    for (peer_id, addr) in peers {
1033                        info!("mDNS discovered peer {} at {}", peer_id, addr);
1034                        let _ = event_tx
1035                            .send(NetworkEvent::PeerDiscovered {
1036                                peer_id,
1037                                addrs: vec![addr],
1038                            })
1039                            .await;
1040                    }
1041                }
1042                mdns::Event::Expired(peers) => {
1043                    for (peer_id, addr) in peers {
1044                        debug!("mDNS peer expired: {} at {}", peer_id, addr);
1045                    }
1046                }
1047            },
1048            SwarmEvent::Behaviour(IpfrsBehaviourEvent::RelayClient(relay_event)) => {
1049                debug!("Relay client event: {:?}", relay_event);
1050                match &relay_event {
1051                    relay::client::Event::ReservationReqAccepted { .. } => {
1052                        let mut m = nat_metrics.write();
1053                        m.relay_connections = m.relay_connections.saturating_add(1);
1054                        info!("Relay reservation accepted");
1055                    }
1056                    relay::client::Event::OutboundCircuitEstablished { .. } => {
1057                        let mut m = nat_metrics.write();
1058                        m.relay_connections = m.relay_connections.saturating_add(1);
1059                        debug!("Outbound relay circuit established");
1060                    }
1061                    _ => {}
1062                }
1063            }
1064            SwarmEvent::Behaviour(IpfrsBehaviourEvent::Ping(ping_event)) => {
1065                if let Ok(rtt) = ping_event.result {
1066                    debug!("Ping to {:?}: RTT = {:?}", ping_event.peer, rtt);
1067                }
1068            }
1069            _ => {}
1070        }
1071    }
1072
1073    /// Handle a command sent to the background swarm event loop.
1074    ///
1075    /// This runs inside the spawned task that owns the swarm, so it can call
1076    /// swarm methods directly.
1077    async fn handle_swarm_command(
1078        cmd: SwarmCommand,
1079        swarm: &mut Swarm<IpfrsBehaviour>,
1080        provider_waiters: &ProviderWaiters,
1081    ) {
1082        match cmd {
1083            SwarmCommand::Dial(addr) => match swarm.dial(addr.clone()) {
1084                Ok(()) => info!("Dialing peer: {}", addr),
1085                Err(e) => warn!("Dial error for {}: {}", addr, e),
1086            },
1087            SwarmCommand::Disconnect(peer_id) => {
1088                let _ = swarm.disconnect_peer_id(peer_id);
1089                info!("Disconnecting from peer: {}", peer_id);
1090            }
1091            SwarmCommand::Provide(cid) => {
1092                let key = kad::RecordKey::new(&cid.to_bytes());
1093                match swarm.behaviour_mut().kademlia.start_providing(key) {
1094                    Ok(_) => debug!("Announcing content: {}", cid),
1095                    Err(e) => warn!("Failed to announce {}: {}", cid, e),
1096                }
1097            }
1098            SwarmCommand::GetProviders(cid) => {
1099                let cid_str = String::from_utf8_lossy(&cid.to_bytes()).to_string();
1100                let key = kad::RecordKey::new(&cid.to_bytes());
1101                swarm.behaviour_mut().kademlia.get_providers(key);
1102                debug!("Querying DHT providers for: {}", cid_str);
1103            }
1104            SwarmCommand::Bootstrap => match swarm.behaviour_mut().kademlia.bootstrap() {
1105                Ok(_) => info!("DHT bootstrap initiated"),
1106                Err(e) => warn!("DHT bootstrap failed: {}", e),
1107            },
1108            SwarmCommand::AddPeerAddress(peer_id, addr) => {
1109                swarm
1110                    .behaviour_mut()
1111                    .kademlia
1112                    .add_address(&peer_id, addr.clone());
1113                debug!("Added address {} for peer {}", addr, peer_id);
1114                // Also try to proactively add the peer to our routing table
1115                // by dialing if not already connected
1116                if !swarm.is_connected(&peer_id) {
1117                    if let Err(e) = swarm.dial(addr.clone()) {
1118                        debug!("Auto-dial for routing table peer {}: {}", peer_id, e);
1119                    }
1120                }
1121            }
1122        }
1123        // Suppress unused warning on provider_waiters (it's used by GetProviders
1124        // via the event handler, not directly here)
1125        let _ = provider_waiters;
1126    }
1127
1128    /// Stop the network node
1129    pub async fn stop(&mut self) -> IpfrsResult<()> {
1130        if let Some(tx) = self.shutdown_tx.take() {
1131            let _ = tx.send(()).await;
1132        }
1133        self.swarm_cmd_tx = None;
1134        Ok(())
1135    }
1136
1137    /// Get local peer ID
1138    pub fn peer_id(&self) -> PeerId {
1139        self.peer_id
1140    }
1141
1142    /// Get listening addresses
1143    pub fn listeners(&self) -> Vec<String> {
1144        self.config.listen_addrs.clone()
1145    }
1146
1147    /// Get connected peers
1148    pub fn connected_peers(&self) -> Vec<PeerId> {
1149        self.connected_peers
1150            .iter()
1151            .map(|entry| *entry.key())
1152            .collect()
1153    }
1154
1155    /// Helper: send a command to the background swarm event loop.
1156    ///
1157    /// Before `start()` we still have the swarm locally, so we handle commands
1158    /// inline.  After `start()` we forward via the command channel.
1159    fn send_swarm_cmd(&self, cmd: SwarmCommand) -> IpfrsResult<()> {
1160        match &self.swarm_cmd_tx {
1161            Some(tx) => tx.try_send(cmd).map_err(|e| {
1162                ipfrs_core::error::Error::Network(format!("Swarm command channel error: {}", e))
1163            }),
1164            None => {
1165                // Node not yet started – silently ignore (pre-start dial attempts etc.)
1166                Ok(())
1167            }
1168        }
1169    }
1170
1171    /// Connect to a peer
1172    pub async fn connect(&mut self, addr: Multiaddr) -> IpfrsResult<()> {
1173        if let Some(swarm) = &mut self.swarm {
1174            // Node not yet started: drive swarm directly
1175            swarm
1176                .dial(addr.clone())
1177                .map_err(|e| ipfrs_core::error::Error::Network(e.to_string()))?;
1178            info!("Dialing peer: {}", addr);
1179        } else {
1180            // Node is running: forward to event-loop task
1181            self.send_swarm_cmd(SwarmCommand::Dial(addr))?;
1182        }
1183        Ok(())
1184    }
1185
1186    /// Disconnect from a peer
1187    pub async fn disconnect(&mut self, peer_id: PeerId) -> IpfrsResult<()> {
1188        if let Some(swarm) = &mut self.swarm {
1189            let _ = swarm.disconnect_peer_id(peer_id);
1190            info!("Disconnecting from peer: {}", peer_id);
1191        } else {
1192            self.send_swarm_cmd(SwarmCommand::Disconnect(peer_id))?;
1193        }
1194        Ok(())
1195    }
1196
1197    /// Announce content to DHT (provide)
1198    pub async fn provide(&mut self, cid: &cid::Cid) -> IpfrsResult<()> {
1199        if let Some(swarm) = &mut self.swarm {
1200            let key = kad::RecordKey::new(&cid.to_bytes());
1201            swarm
1202                .behaviour_mut()
1203                .kademlia
1204                .start_providing(key)
1205                .map_err(|e| ipfrs_core::error::Error::Network(e.to_string()))?;
1206            debug!("Announcing content: {}", cid);
1207        } else {
1208            self.send_swarm_cmd(SwarmCommand::Provide(*cid))?;
1209        }
1210        Ok(())
1211    }
1212
1213    /// Find providers for content in DHT (fire and forget)
1214    pub async fn find_providers(&mut self, cid: &cid::Cid) -> IpfrsResult<()> {
1215        if let Some(swarm) = &mut self.swarm {
1216            let key = kad::RecordKey::new(&cid.to_bytes());
1217            swarm.behaviour_mut().kademlia.get_providers(key);
1218            debug!("Searching for providers of: {}", cid);
1219        } else {
1220            self.send_swarm_cmd(SwarmCommand::GetProviders(*cid))?;
1221        }
1222        Ok(())
1223    }
1224
1225    /// Find providers for content in DHT and wait for results
1226    ///
1227    /// Queries the Kademlia DHT for providers of the given CID and waits up to
1228    /// `timeout` for the first set of results. Returns the provider peer IDs.
1229    pub async fn find_providers_await(
1230        &mut self,
1231        cid: &cid::Cid,
1232        timeout: Duration,
1233    ) -> IpfrsResult<Vec<PeerId>> {
1234        let cid_str = String::from_utf8_lossy(&cid.to_bytes()).to_string();
1235
1236        // Register a waiter before firing the query so we don't miss early responses
1237        let (tx, rx) = oneshot::channel::<Vec<PeerId>>();
1238        {
1239            let mut waiters = self.provider_waiters.lock().await;
1240            waiters.entry(cid_str.clone()).or_default().push(tx);
1241        }
1242
1243        // Fire the DHT query via command channel or directly
1244        if let Some(swarm) = &mut self.swarm {
1245            let key = kad::RecordKey::new(&cid.to_bytes());
1246            swarm.behaviour_mut().kademlia.get_providers(key);
1247            debug!(
1248                "Querying DHT providers for: {} (with timeout {:?})",
1249                cid, timeout
1250            );
1251        } else {
1252            match self.send_swarm_cmd(SwarmCommand::GetProviders(*cid)) {
1253                Ok(()) => {
1254                    debug!(
1255                        "Querying DHT providers for: {} (with timeout {:?})",
1256                        cid, timeout
1257                    );
1258                }
1259                Err(_) => {
1260                    // Command channel broken – clean up waiter and return empty
1261                    let mut waiters = self.provider_waiters.lock().await;
1262                    if let Some(senders) = waiters.get_mut(&cid_str) {
1263                        senders.retain(|_| false);
1264                    }
1265                    return Ok(Vec::new());
1266                }
1267            }
1268        }
1269
1270        // Wait for the result with a timeout
1271        match tokio::time::timeout(timeout, rx).await {
1272            Ok(Ok(providers)) => {
1273                debug!("Received {} providers for {}", providers.len(), cid);
1274                Ok(providers)
1275            }
1276            Ok(Err(_)) => {
1277                // Sender was dropped without sending (e.g. query failed)
1278                debug!("Provider query for {} completed with no results", cid);
1279                Ok(Vec::new())
1280            }
1281            Err(_) => {
1282                // Timeout – clean up the stale waiter
1283                debug!("Provider query for {} timed out after {:?}", cid, timeout);
1284                let mut waiters = self.provider_waiters.lock().await;
1285                if let Some(senders) = waiters.get_mut(&cid_str) {
1286                    senders.retain(|_| false);
1287                }
1288                Ok(Vec::new())
1289            }
1290        }
1291    }
1292
1293    /// Fetch a block from a specific peer via Bitswap
1294    ///
1295    /// This is a best-effort implementation. If the peer is connected and has the
1296    /// block, it will be returned. Otherwise an error is returned and the caller
1297    /// should try the next provider.
1298    pub async fn fetch_block_from_peer(
1299        &mut self,
1300        peer: &PeerId,
1301        cid: &cid::Cid,
1302    ) -> IpfrsResult<ipfrs_core::Block> {
1303        // For now, verify the peer is connected before attempting fetch
1304        if !self.connected_peers.contains(peer) {
1305            return Err(ipfrs_core::error::Error::Network(format!(
1306                "Peer {} is not connected; cannot fetch block {}",
1307                peer, cid
1308            )));
1309        }
1310
1311        // Full Bitswap block exchange over a live connection is a future milestone.
1312        // The plumbing (connected peer check, DHT provider discovery) is in place;
1313        // the actual Bitswap wire protocol exchange will be wired here in Task E.
1314        Err(ipfrs_core::error::Error::NotFound(format!(
1315            "Block {} not yet retrievable from peer {} (Bitswap exchange pending Task E)",
1316            cid, peer
1317        )))
1318    }
1319
1320    /// Find node (closest peers to a given peer ID) using Kademlia
1321    pub async fn find_node(&mut self, peer_id: PeerId) -> IpfrsResult<()> {
1322        if let Some(swarm) = &mut self.swarm {
1323            swarm.behaviour_mut().kademlia.get_closest_peers(peer_id);
1324            debug!("Finding closest peers to: {}", peer_id);
1325        }
1326        Ok(())
1327    }
1328
1329    /// Get the k-closest peers to our local peer ID
1330    pub async fn get_closest_local_peers(&mut self) -> IpfrsResult<Vec<PeerId>> {
1331        if let Some(swarm) = &mut self.swarm {
1332            let mut closest_peers = Vec::new();
1333
1334            // Get peers from the routing table
1335            for bucket in swarm.behaviour_mut().kademlia.kbuckets() {
1336                for entry in bucket.iter() {
1337                    closest_peers.push(*entry.node.key.preimage());
1338                }
1339            }
1340
1341            debug!("Found {} peers in routing table", closest_peers.len());
1342            Ok(closest_peers)
1343        } else {
1344            Ok(Vec::new())
1345        }
1346    }
1347
1348    /// Bootstrap the DHT (search for our own peer ID to populate routing table)
1349    pub async fn bootstrap_dht(&mut self) -> IpfrsResult<()> {
1350        if let Some(swarm) = &mut self.swarm {
1351            swarm
1352                .behaviour_mut()
1353                .kademlia
1354                .bootstrap()
1355                .map_err(|e| ipfrs_core::error::Error::Network(e.to_string()))?;
1356            info!("DHT bootstrap initiated");
1357        } else {
1358            self.send_swarm_cmd(SwarmCommand::Bootstrap)?;
1359        }
1360        Ok(())
1361    }
1362
1363    /// Add an address for a peer to the routing table
1364    pub fn add_peer_address(&mut self, peer_id: PeerId, addr: Multiaddr) -> IpfrsResult<()> {
1365        if let Some(swarm) = &mut self.swarm {
1366            swarm
1367                .behaviour_mut()
1368                .kademlia
1369                .add_address(&peer_id, addr.clone());
1370            debug!("Added address {} for peer {}", addr, peer_id);
1371        } else {
1372            self.send_swarm_cmd(SwarmCommand::AddPeerAddress(peer_id, addr))?;
1373        }
1374        Ok(())
1375    }
1376
1377    /// Get routing table information
1378    pub fn get_routing_table_info(&mut self) -> IpfrsResult<RoutingTableInfo> {
1379        if let Some(swarm) = &mut self.swarm {
1380            let mut total_peers = 0;
1381            let mut buckets_info = Vec::new();
1382
1383            for (index, bucket) in swarm.behaviour_mut().kademlia.kbuckets().enumerate() {
1384                let num_entries = bucket.iter().count();
1385                total_peers += num_entries;
1386                buckets_info.push(BucketInfo { index, num_entries });
1387            }
1388
1389            Ok(RoutingTableInfo {
1390                total_peers,
1391                num_buckets: buckets_info.len(),
1392                buckets: buckets_info,
1393            })
1394        } else {
1395            Ok(RoutingTableInfo {
1396                total_peers: 0,
1397                num_buckets: 0,
1398                buckets: Vec::new(),
1399            })
1400        }
1401    }
1402
1403    /// Get network statistics
1404    pub fn stats(&self) -> NetworkStats {
1405        let bandwidth = self.bandwidth_stats.read();
1406        NetworkStats {
1407            peer_id: self.peer_id.to_string(),
1408            listen_addrs: self.config.listen_addrs.clone(),
1409            connected_peers: self.connected_peers.len(),
1410            quic_enabled: self.config.enable_quic,
1411            bytes_received: bandwidth.bytes_received,
1412            bytes_sent: bandwidth.bytes_sent,
1413            bootstrap_peers: self.config.bootstrap_peers.clone(),
1414        }
1415    }
1416
1417    /// Take the event receiver
1418    pub fn take_event_receiver(&mut self) -> Option<mpsc::Receiver<NetworkEvent>> {
1419        self.event_rx.take()
1420    }
1421
1422    /// Get confirmed external addresses
1423    pub fn get_external_addresses(&self) -> Vec<Multiaddr> {
1424        self.external_addrs.read().clone()
1425    }
1426
1427    /// Check if node has public reachability
1428    pub fn is_publicly_reachable(&self) -> bool {
1429        !self.external_addrs.read().is_empty()
1430    }
1431
1432    /// Check if connected to a specific peer
1433    pub fn is_connected_to(&self, peer_id: &PeerId) -> bool {
1434        self.connected_peers.contains(peer_id)
1435    }
1436
1437    /// Get number of connected peers
1438    pub fn get_peer_count(&self) -> usize {
1439        self.connected_peers.len()
1440    }
1441
1442    /// Connect to multiple peers in batch
1443    pub async fn connect_to_peers(&mut self, addrs: Vec<Multiaddr>) -> Vec<IpfrsResult<()>> {
1444        let mut results = Vec::with_capacity(addrs.len());
1445
1446        for addr in addrs {
1447            let result = self.connect(addr).await;
1448            results.push(result);
1449        }
1450
1451        results
1452    }
1453
1454    /// Disconnect from all connected peers
1455    pub async fn disconnect_all(&mut self) -> IpfrsResult<()> {
1456        let peers: Vec<PeerId> = self.connected_peers().clone();
1457
1458        for peer in peers {
1459            let _ = self.disconnect(peer).await;
1460        }
1461
1462        Ok(())
1463    }
1464
1465    /// Update bandwidth statistics manually (for custom tracking)
1466    pub fn update_bandwidth(&self, bytes_sent: u64, bytes_received: u64) {
1467        let mut stats = self.bandwidth_stats.write();
1468        stats.bytes_sent += bytes_sent;
1469        stats.bytes_received += bytes_received;
1470    }
1471
1472    /// Get total bandwidth sent
1473    pub fn get_bytes_sent(&self) -> u64 {
1474        self.bandwidth_stats.read().bytes_sent
1475    }
1476
1477    /// Get total bandwidth received
1478    pub fn get_bytes_received(&self) -> u64 {
1479        self.bandwidth_stats.read().bytes_received
1480    }
1481
1482    /// Reset bandwidth statistics
1483    pub fn reset_bandwidth_stats(&self) {
1484        let mut stats = self.bandwidth_stats.write();
1485        stats.bytes_sent = 0;
1486        stats.bytes_received = 0;
1487    }
1488
1489    /// Get network health summary
1490    pub fn get_network_health(&self) -> NetworkHealthSummary {
1491        let peer_count = self.get_peer_count();
1492        let is_public = self.is_publicly_reachable();
1493        let has_external_addrs = !self.external_addrs.read().is_empty();
1494
1495        // Determine health status
1496        let status = if peer_count >= 10 && is_public {
1497            NetworkHealthLevel::Healthy
1498        } else if peer_count >= 3 || has_external_addrs {
1499            NetworkHealthLevel::Degraded
1500        } else if peer_count > 0 {
1501            NetworkHealthLevel::Limited
1502        } else {
1503            NetworkHealthLevel::Disconnected
1504        };
1505
1506        NetworkHealthSummary {
1507            status,
1508            connected_peers: peer_count,
1509            is_publicly_reachable: is_public,
1510            external_addresses: self.get_external_addresses().len(),
1511        }
1512    }
1513
1514    /// Check if node is healthy
1515    pub fn is_healthy(&self) -> bool {
1516        matches!(
1517            self.get_network_health().status,
1518            NetworkHealthLevel::Healthy
1519        )
1520    }
1521
1522    /// Get a snapshot of NAT traversal (hole-punch) metrics.
1523    pub fn nat_traversal_metrics(&self) -> NatTraversalMetrics {
1524        self.nat_metrics.read().clone()
1525    }
1526
1527    // ─── Distributed inference transport ─────────────────────────────────────
1528
1529    /// Publish an `InferenceRequest` to the GossipSub `INFERENCE_REQUEST`
1530    /// topic.
1531    ///
1532    /// Serialises `request` as JSON and hands it to the local
1533    /// `GossipSubManager`.  The manager fan-out to all subscribed peers is
1534    /// simulated in-process; wire integration is provided by the event loop
1535    /// once a real GossipSub swarm behaviour is wired in.
1536    ///
1537    /// # Errors
1538    /// Returns an error when JSON serialisation fails.
1539    pub fn publish_inference_request(
1540        &self,
1541        request: &ipfrs_tensorlogic::InferenceRequest,
1542    ) -> IpfrsResult<()> {
1543        let json = serde_json::to_vec(request).map_err(|e| {
1544            ipfrs_core::error::Error::Network(format!("Failed to serialize InferenceRequest: {e}"))
1545        })?;
1546        let peer_id_str = self.peer_id.to_string();
1547        self.gossipsub
1548            .publish_inference_request(&json, &peer_id_str)
1549            .map_err(|e| {
1550                ipfrs_core::error::Error::Network(format!(
1551                    "GossipSub publish_inference_request failed: {e}"
1552                ))
1553            })
1554    }
1555
1556    /// Register a one-shot waiter that will be resolved when an
1557    /// `InferenceResponse` with the given `request_id` is delivered to this
1558    /// node via `deliver_inference_response`.
1559    ///
1560    /// Returns the receiving half of the oneshot channel.  The caller should
1561    /// wrap the `await` with [`tokio::time::timeout`] to bound the wait.
1562    pub async fn register_inference_waiter(
1563        &self,
1564        request_id: String,
1565    ) -> oneshot::Receiver<ipfrs_tensorlogic::InferenceResponse> {
1566        let (tx, rx) = oneshot::channel();
1567        let mut waiters = self.inference_waiters.lock().await;
1568        waiters.entry(request_id).or_default().push(tx);
1569        rx
1570    }
1571
1572    /// Deliver an `InferenceResponse` to any registered waiters for
1573    /// `response.request_id`.
1574    ///
1575    /// This is the counterpart of `register_inference_waiter`.  Typically
1576    /// called from the event loop when a GossipSub message arrives on the
1577    /// `INFERENCE_RESULT` topic.
1578    pub async fn deliver_inference_response(&self, response: ipfrs_tensorlogic::InferenceResponse) {
1579        let mut waiters = self.inference_waiters.lock().await;
1580        if let Some(senders) = waiters.remove(&response.request_id) {
1581            for tx in senders {
1582                // Best-effort delivery – ignore closed receivers.
1583                let _ = tx.send(response.clone());
1584            }
1585        }
1586    }
1587
1588    /// Publish a local `InferenceResponse` to the GossipSub
1589    /// `INFERENCE_RESULT` topic so remote requesters can collect it.
1590    ///
1591    /// # Errors
1592    /// Returns an error when JSON serialisation fails.
1593    pub fn publish_inference_response(
1594        &self,
1595        response: &ipfrs_tensorlogic::InferenceResponse,
1596    ) -> IpfrsResult<()> {
1597        let json = serde_json::to_vec(response).map_err(|e| {
1598            ipfrs_core::error::Error::Network(format!("Failed to serialize InferenceResponse: {e}"))
1599        })?;
1600        let peer_id_str = self.peer_id.to_string();
1601        self.gossipsub
1602            .publish_inference_result(&json, &peer_id_str)
1603            .map_err(|e| {
1604                ipfrs_core::error::Error::Network(format!(
1605                    "GossipSub publish_inference_result failed: {e}"
1606                ))
1607            })
1608    }
1609}
1610
1611/// NAT traversal (hole-punching) metrics
1612///
1613/// Tracks the outcome of DCUtR hole-punch attempts so operators can assess
1614/// whether relay fallback is being relied on too heavily.
1615#[derive(Debug, Clone, Default, serde::Serialize)]
1616pub struct NatTraversalMetrics {
1617    /// Total number of hole-punch attempts initiated (both sides)
1618    pub hole_punch_attempts: u64,
1619    /// Hole-punch attempts that resulted in a direct connection
1620    pub hole_punch_successes: u64,
1621    /// Hole-punch attempts that failed (connection remained via relay)
1622    pub hole_punch_failures: u64,
1623    /// Number of connections currently established via a relay circuit
1624    pub relay_connections: u64,
1625}
1626
1627impl NatTraversalMetrics {
1628    /// Fraction of hole-punch attempts that succeeded (0.0 if no attempts).
1629    pub fn success_rate(&self) -> f32 {
1630        if self.hole_punch_attempts == 0 {
1631            return 0.0;
1632        }
1633        self.hole_punch_successes as f32 / self.hole_punch_attempts as f32
1634    }
1635}
1636
1637/// Network statistics
1638#[derive(Debug, Clone, serde::Serialize)]
1639pub struct NetworkStats {
1640    pub peer_id: String,
1641    pub listen_addrs: Vec<String>,
1642    pub connected_peers: usize,
1643    pub quic_enabled: bool,
1644    /// Total bytes received
1645    pub bytes_received: u64,
1646    /// Total bytes sent
1647    pub bytes_sent: u64,
1648    /// Bootstrap peers
1649    pub bootstrap_peers: Vec<String>,
1650}
1651
1652/// Information about a k-bucket in the routing table
1653#[derive(Debug, Clone, serde::Serialize)]
1654pub struct BucketInfo {
1655    /// Bucket index
1656    pub index: usize,
1657    /// Number of entries in this bucket
1658    pub num_entries: usize,
1659}
1660
1661/// Routing table information
1662#[derive(Debug, Clone, serde::Serialize)]
1663pub struct RoutingTableInfo {
1664    /// Total number of peers in routing table
1665    pub total_peers: usize,
1666    /// Number of buckets
1667    pub num_buckets: usize,
1668    /// Information about each bucket
1669    pub buckets: Vec<BucketInfo>,
1670}
1671
1672/// Network health summary
1673#[derive(Debug, Clone, serde::Serialize)]
1674pub struct NetworkHealthSummary {
1675    /// Overall health status
1676    pub status: NetworkHealthLevel,
1677    /// Number of connected peers
1678    pub connected_peers: usize,
1679    /// Whether node is publicly reachable
1680    pub is_publicly_reachable: bool,
1681    /// Number of external addresses
1682    pub external_addresses: usize,
1683}
1684
1685/// Network health level
1686#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
1687pub enum NetworkHealthLevel {
1688    /// Fully connected with good peer count and public reachability
1689    Healthy,
1690    /// Connected but with limited peers or no public reachability
1691    Degraded,
1692    /// Minimal connectivity
1693    Limited,
1694    /// No connections
1695    Disconnected,
1696}
1697
1698// ============================================================================
1699// Circuit Relay v2 — reservation management
1700// ============================================================================
1701
1702impl NetworkNode {
1703    /// Attempt to obtain a Circuit Relay v2 reservation from `relay_peer`.
1704    ///
1705    /// The method dials the relay peer (if not already connected) and records
1706    /// the reservation in `active_relay_reservations`.  In a full
1707    /// implementation the swarm's `relay::client::Behaviour` would send the
1708    /// actual reservation request; here we perform the dial and record the
1709    /// reservation optimistically, returning an error if relay v2 is disabled
1710    /// in the node's configuration.
1711    ///
1712    /// # Errors
1713    ///
1714    /// Returns an error if:
1715    /// - Relay v2 is disabled in the node or relay config.
1716    /// - The maximum number of simultaneous reservations is already reached.
1717    /// - The swarm command channel is not available (node not started).
1718    pub async fn reserve_relay(&mut self, relay_peer: PeerId) -> IpfrsResult<()> {
1719        if !self.config.relay_v2_enabled || !self.relay_config.relay_v2_enabled {
1720            return Err(ipfrs_core::error::Error::Network(
1721                "Circuit Relay v2 is disabled".to_string(),
1722            ));
1723        }
1724
1725        // Check max reservations limit.
1726        {
1727            let reservations = self.active_relay_reservations.read();
1728            if reservations.len() >= self.relay_config.max_reservations {
1729                return Err(ipfrs_core::error::Error::Network(format!(
1730                    "Maximum relay reservations ({}) already reached",
1731                    self.relay_config.max_reservations
1732                )));
1733            }
1734        }
1735
1736        // Build the relay circuit address:
1737        // /p2p/<relay_peer_id>/p2p-circuit
1738        let relay_addr: Multiaddr =
1739            format!("/p2p/{}/p2p-circuit", relay_peer)
1740                .parse()
1741                .map_err(|e| {
1742                    ipfrs_core::error::Error::Network(format!(
1743                        "Invalid relay address for peer {}: {}",
1744                        relay_peer, e
1745                    ))
1746                })?;
1747
1748        debug!(
1749            relay_peer = %relay_peer,
1750            addr = %relay_addr,
1751            "Requesting Circuit Relay v2 reservation"
1752        );
1753
1754        // Send the dial command to the background swarm event-loop.
1755        // The actual `/libp2p/circuit/relay/0.2.0/hop` RESERVE message is
1756        // handled by the relay::client::Behaviour inside the swarm.
1757        if let Some(ref cmd_tx) = self.swarm_cmd_tx {
1758            cmd_tx
1759                .send(SwarmCommand::Dial(relay_addr))
1760                .await
1761                .map_err(|_| {
1762                    ipfrs_core::error::Error::Network("Swarm command channel closed".to_string())
1763                })?;
1764        } else {
1765            // Node not started yet: record the reservation intent anyway so
1766            // that the caller can check it later.
1767            warn!(
1768                relay_peer = %relay_peer,
1769                "reserve_relay called before node.start(); \
1770                 reservation recorded but dial not sent"
1771            );
1772        }
1773
1774        // Record the reservation with the current timestamp.
1775        {
1776            let mut reservations = self.active_relay_reservations.write();
1777            reservations.insert(relay_peer, std::time::Instant::now());
1778        }
1779
1780        info!(
1781            relay_peer = %relay_peer,
1782            "Circuit Relay v2 reservation recorded"
1783        );
1784
1785        Ok(())
1786    }
1787
1788    /// Return a snapshot of the currently active relay reservations.
1789    ///
1790    /// Each entry maps a relay [`PeerId`] to the [`std::time::Instant`] at
1791    /// which the reservation was obtained.
1792    pub fn relay_reservations(&self) -> HashMap<PeerId, std::time::Instant> {
1793        self.active_relay_reservations.read().clone()
1794    }
1795
1796    /// Remove a relay reservation (e.g., when the relay peer disconnects).
1797    pub fn remove_relay_reservation(&mut self, relay_peer: &PeerId) {
1798        self.active_relay_reservations.write().remove(relay_peer);
1799    }
1800
1801    /// Remove reservations older than `max_age`.
1802    pub fn prune_expired_relay_reservations(&mut self, max_age: std::time::Duration) {
1803        let now = std::time::Instant::now();
1804        self.active_relay_reservations
1805            .write()
1806            .retain(|_, instant| now.duration_since(*instant) < max_age);
1807    }
1808}