Skip to main content

saorsa_core/
network.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is licensed under the MIT license <LICENSE-MIT or
4// https://opensource.org/licenses/MIT> or the Apache License, Version 2.0
5// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, at your
6// option. This file may not be copied, modified, or distributed except
7// according to those terms.
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under these licenses is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
13//! Network module
14//!
15//! This module provides core networking functionality for the P2P Foundation.
16//! It handles peer connections, network events, and node lifecycle management.
17
18use crate::PeerId;
19use crate::adaptive::trust::{TrustRecord, TrustSnapshot};
20use crate::adaptive::{AdaptiveDHT, AdaptiveDhtConfig, TrustEngine, TrustEvent};
21use crate::bootstrap::cache::{CachedCloseGroupPeer, CloseGroupCache};
22use crate::dht::core_engine::AddressType;
23use crate::dht_network_manager::{DhtNetworkConfig, DhtNetworkEvent, DhtNetworkManager};
24use crate::error::{NetworkError, P2PError, P2pResult as Result};
25use crate::reachability::spawn_acquisition_driver;
26
27use crate::MultiAddr;
28use crate::identity::node_identity::{NodeIdentity, peer_id_from_public_key};
29use crate::quantum_crypto::saorsa_transport_integration::{MlDsaPublicKey, MlDsaSignature};
30use dashmap::DashMap;
31use futures::StreamExt;
32use parking_lot::Mutex as ParkingMutex;
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35use std::net::SocketAddr;
36use std::path::{Path, PathBuf};
37use std::sync::Arc;
38use std::sync::atomic::{AtomicBool, Ordering};
39use std::time::{Duration, SystemTime, UNIX_EPOCH};
40use tokio::sync::{Mutex as TokioMutex, RwLock, broadcast};
41use tokio::time::Instant;
42use tokio_util::sync::CancellationToken;
43use tracing::{debug, info, trace, warn};
44
45fn bootstrap_peer_identity_matches(expected: Option<PeerId>, actual: PeerId) -> bool {
46    expected.is_none_or(|expected| expected == actual)
47}
48
49/// Wire protocol message format for P2P communication.
50///
51/// Serialized with postcard for compact binary encoding.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub(crate) struct WireMessage {
54    /// Protocol/topic identifier
55    pub(crate) protocol: String,
56    /// Raw payload bytes
57    pub(crate) data: Vec<u8>,
58    /// Sender's peer ID (verified against transport-level identity)
59    pub(crate) from: PeerId,
60    /// Unix timestamp in seconds
61    pub(crate) timestamp: u64,
62    /// User agent string identifying the sender's software.
63    ///
64    /// Convention: `"node/<version>"` for full DHT participants,
65    /// `"client/<version>"` or `"<app>/<version>"` for ephemeral clients.
66    /// Included in the signed bytes — tamper-proof.
67    #[serde(default)]
68    pub(crate) user_agent: String,
69    /// Sender's ML-DSA-65 public key (1952 bytes). Empty if unsigned.
70    #[serde(default)]
71    pub(crate) public_key: Vec<u8>,
72    /// ML-DSA-65 signature over the signable bytes. Empty if unsigned.
73    #[serde(default)]
74    pub(crate) signature: Vec<u8>,
75}
76
77/// Operating mode of a P2P node.
78///
79/// Determines the default user agent and DHT participation behavior.
80/// `Node` peers participate in the DHT routing table; `Client` peers
81/// are treated as ephemeral and excluded from routing.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
83pub enum NodeMode {
84    /// Full DHT-participant node that maintains routing state and routes messages.
85    #[default]
86    Node,
87    /// Ephemeral client that connects to perform operations without joining the DHT.
88    Client,
89}
90
91/// Internal listen mode controlling which network interfaces the node binds to.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93enum ListenMode {
94    /// Bind to all interfaces (`0.0.0.0` / `::`).
95    Public,
96    /// Bind to loopback only (`127.0.0.1` / `::1`).
97    Local,
98}
99
100/// Returns the default user agent string for the given mode.
101///
102/// - `Node` → `"node/<saorsa-core-version>"`
103/// - `Client` → `"client/<saorsa-core-version>"`
104pub fn user_agent_for_mode(mode: NodeMode) -> String {
105    let prefix = match mode {
106        NodeMode::Node => "node",
107        NodeMode::Client => "client",
108    };
109    format!("{prefix}/{}", env!("CARGO_PKG_VERSION"))
110}
111
112/// Returns `true` if the user agent identifies a full DHT participant (prefix `"node/"`).
113pub fn is_dht_participant(user_agent: &str) -> bool {
114    user_agent.starts_with("node/")
115}
116
117/// Capacity of the internal channel used by the message receiving system.
118pub(crate) const MESSAGE_RECV_CHANNEL_CAPACITY: usize = 256;
119
120/// Maximum number of concurrent in-flight request/response operations.
121pub(crate) const MAX_ACTIVE_REQUESTS: usize = 256;
122
123/// Maximum allowed timeout for a single request (5 minutes).
124pub(crate) const MAX_REQUEST_TIMEOUT: Duration = Duration::from_secs(300);
125
126/// Default listen port for the P2P node.
127const DEFAULT_LISTEN_PORT: u16 = 9000;
128
129/// Default maximum number of concurrent connections.
130const DEFAULT_MAX_CONNECTIONS: usize = 10_000;
131
132/// Default connection timeout in seconds.
133///
134/// The transport adapter keeps each direct Happy Eyeballs attempt short so
135/// DHT lookups can move past offline peers quickly. 25s leaves room for
136/// multi-stage connection strategies and identity exchange while preserving
137/// the historical API default.
138const DEFAULT_CONNECTION_TIMEOUT_SECS: u64 = 25;
139
140/// Default maximum age of a close-group cache snapshot before it is skipped
141/// as Priority-0 bootstrap material.
142const DEFAULT_CLOSE_GROUP_CACHE_MAX_AGE_SECS: u64 = 60 * 60;
143
144/// Lower bound for periodic close-group-cache saves. Prevents a very short DHT
145/// refresh interval from turning cache persistence into a hot write loop.
146const MIN_CLOSE_GROUP_CACHE_SAVE_INTERVAL: Duration = Duration::from_secs(60);
147
148/// Timeout in seconds for waiting on a bootstrap peer's identity exchange.
149///
150/// Tighter than the post-bootstrap budget
151/// ([`crate::dht_network_manager::IDENTITY_EXCHANGE_TIMEOUT`],
152/// 5 s) on purpose: bootstrap candidates are unverified and a stuck one
153/// must not be allowed to head-of-line block convergence. 3 s covers
154/// loopback (<100 ms) and direct WAN paths (~1–2 s with one handshake
155/// retry); a relay-tunnelled path with congested ML-DSA verification
156/// can exceed this and will fail identity exchange, but bootstrap simply
157/// moves on to other candidates rather than retrying the same one.
158///
159/// `wait_for_peer_identity` short-circuits on channel close, so most dead
160/// channels surface in microseconds regardless of this budget.
161const BOOTSTRAP_IDENTITY_TIMEOUT_SECS: u64 = 3;
162
163/// Maximum number of bootstrap peers dialed concurrently in Phase B.
164///
165/// Bounds the fan-out of configured bootstrap dials so simultaneous QUIC+PQC
166/// handshakes don't spike CPU or saturate the UDP socket. Chosen
167/// low on purpose: each dial runs a full ML-KEM key exchange and ML-DSA
168/// verification, and a cold-start node has no spare compute budget.
169const MAX_CONCURRENT_BOOTSTRAP_DIALS: usize = 4;
170
171/// Number of successful bootstrap connections after which a client-mode
172/// node stops dialing further candidates.
173///
174/// Clients only need enough peers to route their own lookups (α=3 parallel
175/// queries → 6 gives ~2× redundancy) and don't serve the DHT, so a fully
176/// populated close-group buys them nothing. Stopping early cuts cold-start
177/// latency by skipping the tail of slow / dead candidates. Nodes always
178/// dial every candidate so their routing table converges fully.
179const CLIENT_BOOTSTRAP_TARGET: usize = 6;
180
181/// Serde helper — returns `true`.
182const fn default_true() -> bool {
183    true
184}
185
186/// Configuration for a P2P node
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct NodeConfig {
189    /// Bind to loopback only (`127.0.0.1` / `::1`).
190    ///
191    /// When `true`, the node listens on loopback addresses suitable for
192    /// local development and testing. When `false` (the default), the node
193    /// listens on all interfaces (`0.0.0.0` / `::`).
194    #[serde(default)]
195    pub local: bool,
196
197    /// Listen port. `0` means OS-assigned ephemeral port.
198    #[serde(default)]
199    pub port: u16,
200
201    /// Enable IPv6 dual-stack binding.
202    ///
203    /// When `true` (the default), both an IPv4 and an IPv6 address are
204    /// bound. When `false`, only IPv4 is used.
205    #[serde(default = "default_true")]
206    pub ipv6: bool,
207
208    /// Bootstrap peers to connect to on startup.
209    pub bootstrap_peers: Vec<crate::MultiAddr>,
210
211    // MCP removed; will be redesigned later
212    /// Connection timeout duration
213    pub connection_timeout: Duration,
214
215    /// Maximum number of concurrent connections
216    pub max_connections: usize,
217
218    /// DHT configuration
219    pub dht_config: DHTConfig,
220
221    /// Optional IP diversity configuration for Sybil protection tuning.
222    ///
223    /// When set, this configuration is used by diversity-enforcing subsystems.
224    /// If `None`, defaults are used.
225    pub diversity_config: Option<crate::security::IPDiversityConfig>,
226
227    /// Optional override for the maximum application-layer message size.
228    ///
229    /// When `None`, the underlying saorsa-transport default is used.
230    #[serde(default)]
231    pub max_message_size: Option<usize>,
232
233    /// Optional node identity for app-level message signing.
234    ///
235    /// When set, outgoing messages are signed with the node's ML-DSA-65 key
236    /// and incoming signed messages are verified at the transport layer.
237    #[serde(skip)]
238    pub node_identity: Option<Arc<NodeIdentity>>,
239
240    /// Operating mode of this node.
241    ///
242    /// Determines the default user agent and DHT participation:
243    /// - `Node` → user agent `"node/<version>"`, added to DHT routing tables.
244    /// - `Client` → user agent `"client/<version>"`, treated as ephemeral.
245    #[serde(default)]
246    pub mode: NodeMode,
247
248    /// Optional custom user agent override.
249    ///
250    /// When `Some`, this value is used instead of the mode-derived default.
251    /// When `None`, the user agent is derived from [`NodeConfig::mode`].
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub custom_user_agent: Option<String>,
254
255    /// Allow loopback addresses (127.0.0.1, ::1) in the transport layer.
256    ///
257    /// In production, loopback addresses are rejected because they are not
258    /// routable. Enable this for local devnets and testnets where all nodes
259    /// run on the same machine.
260    ///
261    /// Default: `false`
262    #[serde(default)]
263    pub allow_loopback: bool,
264
265    /// Adaptive DHT configuration (trust-based swap-out).
266    ///
267    /// Controls whether peers with low trust scores are eligible for
268    /// swap-out from the routing table when better candidates arrive. Use
269    /// `NodeConfigBuilder::trust_enforcement` for a simple on/off toggle.
270    ///
271    /// Default: enabled with a swap threshold of 0.35.
272    #[serde(default)]
273    pub adaptive_dht_config: AdaptiveDhtConfig,
274
275    /// Optional path for persisting the close group cache.
276    ///
277    /// Directory for persisting the close group cache.
278    ///
279    /// When set, the node saves its close group peers and their trust
280    /// scores to `{dir}/close_group_cache.json` periodically, on shutdown,
281    /// and after bootstrap. On startup, fresh cached peers are loaded and
282    /// contacted first, preserving close group consistency across restarts.
283    ///
284    /// When `None`, no close group cache is used.
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub close_group_cache_dir: Option<PathBuf>,
287
288    /// Maximum age for using a close-group cache snapshot as Priority-0
289    /// bootstrap material. Older snapshots are logged and skipped so the node
290    /// falls through to configured bootstrap peers. `None` disables the age
291    /// check. Default: one hour.
292    #[serde(default = "default_close_group_cache_max_age")]
293    pub close_group_cache_max_age: Option<Duration>,
294}
295
296fn default_close_group_cache_max_age() -> Option<Duration> {
297    Some(Duration::from_secs(DEFAULT_CLOSE_GROUP_CACHE_MAX_AGE_SECS))
298}
299
300/// DHT-specific configuration
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct DHTConfig {
303    /// Kademlia K parameter (bucket size)
304    pub k_value: usize,
305
306    /// Kademlia alpha parameter (parallelism)
307    pub alpha_value: usize,
308
309    /// DHT refresh interval
310    pub refresh_interval: Duration,
311}
312
313// ============================================================================
314// Address Construction Helpers
315// ============================================================================
316
317/// Build QUIC listen addresses based on port, IPv6 preference, and listen mode.
318///
319/// All returned addresses use the QUIC transport — the only transport
320/// currently supported for dialing. When additional transports are added,
321/// extend this function to produce addresses for those transports as well.
322///
323/// `ListenMode::Public` uses unspecified (all-interface) addresses;
324/// `ListenMode::Local` uses loopback addresses.
325#[inline]
326fn build_listen_addrs(port: u16, ipv6_enabled: bool, mode: ListenMode) -> Vec<MultiAddr> {
327    let mut addrs = Vec::with_capacity(if ipv6_enabled { 2 } else { 1 });
328
329    let (v4, v6) = match mode {
330        ListenMode::Public => (
331            std::net::Ipv4Addr::UNSPECIFIED,
332            std::net::Ipv6Addr::UNSPECIFIED,
333        ),
334        ListenMode::Local => (std::net::Ipv4Addr::LOCALHOST, std::net::Ipv6Addr::LOCALHOST),
335    };
336
337    if ipv6_enabled {
338        addrs.push(MultiAddr::quic(std::net::SocketAddr::new(
339            std::net::IpAddr::V6(v6),
340            port,
341        )));
342    }
343
344    addrs.push(MultiAddr::quic(std::net::SocketAddr::new(
345        std::net::IpAddr::V4(v4),
346        port,
347    )));
348
349    addrs
350}
351
352impl NodeConfig {
353    /// Returns the effective user agent string.
354    ///
355    /// If a custom user agent was set, returns that. Otherwise, derives
356    /// the user agent from the node's [`NodeMode`].
357    pub fn user_agent(&self) -> String {
358        self.custom_user_agent
359            .clone()
360            .unwrap_or_else(|| user_agent_for_mode(self.mode))
361    }
362
363    /// Compute the listen addresses from the configuration fields.
364    ///
365    /// The returned addresses are derived from [`local`](Self::local),
366    /// [`port`](Self::port), and [`ipv6`](Self::ipv6).
367    pub fn listen_addrs(&self) -> Vec<MultiAddr> {
368        let mode = if self.local {
369            ListenMode::Local
370        } else {
371            ListenMode::Public
372        };
373        build_listen_addrs(self.port, self.ipv6, mode)
374    }
375
376    /// Create a new NodeConfig with default values
377    ///
378    /// # Errors
379    ///
380    /// Returns an error if default addresses cannot be parsed
381    pub fn new() -> Result<Self> {
382        Ok(Self::default())
383    }
384
385    /// Create a builder for customized NodeConfig construction
386    pub fn builder() -> NodeConfigBuilder {
387        NodeConfigBuilder::default()
388    }
389}
390
391// ============================================================================
392// NodeConfig Builder Pattern
393// ============================================================================
394
395/// Builder for constructing [`NodeConfig`] with a transport-aware fluent API.
396///
397/// Defaults are chosen for quick local development:
398/// - QUIC on a random free port (`0`)
399/// - IPv6 enabled (dual-stack)
400/// - All interfaces (not local-only)
401///
402/// # Examples
403///
404/// ```rust,ignore
405/// // Simplest — QUIC on random port, IPv6 on, all interfaces
406/// let config = NodeConfig::builder().build()?;
407///
408/// // Local dev/test mode (loopback, auto-enables allow_loopback)
409/// let config = NodeConfig::builder()
410///     .local(true)
411///     .build()?;
412/// ```
413#[derive(Debug, Clone)]
414pub struct NodeConfigBuilder {
415    port: u16,
416    ipv6: bool,
417    local: bool,
418    bootstrap_peers: Vec<crate::MultiAddr>,
419    max_connections: Option<usize>,
420    connection_timeout: Option<Duration>,
421    dht_config: Option<DHTConfig>,
422    max_message_size: Option<usize>,
423    mode: NodeMode,
424    custom_user_agent: Option<String>,
425    allow_loopback: Option<bool>,
426    adaptive_dht_config: Option<AdaptiveDhtConfig>,
427    close_group_cache_dir: Option<PathBuf>,
428    /// Outer `None` means the builder setter was not called; inner `None`
429    /// explicitly disables age enforcement.
430    close_group_cache_max_age: Option<Option<Duration>>,
431}
432
433impl Default for NodeConfigBuilder {
434    fn default() -> Self {
435        Self {
436            port: 0,
437            ipv6: true,
438            local: false,
439            bootstrap_peers: Vec::new(),
440            max_connections: None,
441            connection_timeout: None,
442            dht_config: None,
443            max_message_size: None,
444            mode: NodeMode::default(),
445            custom_user_agent: None,
446            allow_loopback: None,
447            adaptive_dht_config: None,
448            close_group_cache_dir: None,
449            close_group_cache_max_age: None,
450        }
451    }
452}
453
454impl NodeConfigBuilder {
455    /// Set the listen port. Default: `0` (random free port).
456    pub fn port(mut self, port: u16) -> Self {
457        self.port = port;
458        self
459    }
460
461    /// Enable or disable IPv6 dual-stack. Default: `true`.
462    pub fn ipv6(mut self, enabled: bool) -> Self {
463        self.ipv6 = enabled;
464        self
465    }
466
467    /// Bind to loopback only (`true`) or all interfaces (`false`).
468    ///
469    /// When `true`, automatically enables `allow_loopback` unless explicitly
470    /// overridden via [`Self::allow_loopback`].
471    ///
472    /// Default: `false` (all interfaces).
473    pub fn local(mut self, local: bool) -> Self {
474        self.local = local;
475        self
476    }
477
478    /// Add a bootstrap peer.
479    pub fn bootstrap_peer(mut self, addr: crate::MultiAddr) -> Self {
480        self.bootstrap_peers.push(addr);
481        self
482    }
483
484    /// Set maximum connections.
485    pub fn max_connections(mut self, max: usize) -> Self {
486        self.max_connections = Some(max);
487        self
488    }
489
490    /// Set connection timeout.
491    pub fn connection_timeout(mut self, timeout: Duration) -> Self {
492        self.connection_timeout = Some(timeout);
493        self
494    }
495
496    /// Set DHT configuration.
497    pub fn dht_config(mut self, config: DHTConfig) -> Self {
498        self.dht_config = Some(config);
499        self
500    }
501
502    /// Set maximum application-layer message size in bytes.
503    ///
504    /// If this method is not called, saorsa-transport's built-in default is used.
505    pub fn max_message_size(mut self, max_message_size: usize) -> Self {
506        self.max_message_size = Some(max_message_size);
507        self
508    }
509
510    /// Set the operating mode (Node or Client).
511    pub fn mode(mut self, mode: NodeMode) -> Self {
512        self.mode = mode;
513        self
514    }
515
516    /// Set a custom user agent string, overriding the mode-derived default.
517    pub fn custom_user_agent(mut self, user_agent: impl Into<String>) -> Self {
518        self.custom_user_agent = Some(user_agent.into());
519        self
520    }
521
522    /// Explicitly control whether loopback addresses are allowed in the
523    /// transport layer. When not called, `local(true)` auto-enables this;
524    /// `local(false)` defaults to `false`.
525    pub fn allow_loopback(mut self, allow: bool) -> Self {
526        self.allow_loopback = Some(allow);
527        self
528    }
529
530    /// Enable or disable trust-based peer swap-out.
531    ///
532    /// When `false`, peers are never swapped out of the routing table
533    /// based on trust scores. Trust scores are still tracked but have
534    /// no enforcement effect.
535    ///
536    /// When `true` (the default), peers whose trust score falls below the
537    /// swap threshold (0.35) become eligible for replacement when a
538    /// better candidate arrives.
539    ///
540    /// For fine-grained control over the threshold, use
541    /// [`adaptive_dht_config`](Self::adaptive_dht_config) instead.
542    pub fn trust_enforcement(mut self, enabled: bool) -> Self {
543        let threshold = if enabled {
544            AdaptiveDhtConfig::default().swap_threshold
545        } else {
546            0.0
547        };
548        self.adaptive_dht_config = Some(AdaptiveDhtConfig {
549            swap_threshold: threshold,
550        });
551        self
552    }
553
554    /// Set the full adaptive DHT configuration.
555    ///
556    /// Overrides any previous call to [`trust_enforcement`](Self::trust_enforcement).
557    pub fn adaptive_dht_config(mut self, config: AdaptiveDhtConfig) -> Self {
558        self.adaptive_dht_config = Some(config);
559        self
560    }
561
562    /// Set the directory for persisting the close group cache.
563    ///
564    /// The node writes `close_group_cache.json` inside this directory on
565    /// shutdown and after bootstrap, and loads it on startup.
566    pub fn close_group_cache_dir(mut self, path: impl Into<PathBuf>) -> Self {
567        self.close_group_cache_dir = Some(path.into());
568        self
569    }
570
571    /// Set the maximum age for using a close-group cache as Priority-0
572    /// bootstrap material. `None` disables the age check.
573    pub fn close_group_cache_max_age(mut self, max_age: Option<Duration>) -> Self {
574        self.close_group_cache_max_age = Some(max_age);
575        self
576    }
577
578    /// Build the [`NodeConfig`].
579    ///
580    /// # Errors
581    ///
582    /// Returns an error if address construction fails.
583    pub fn build(self) -> Result<NodeConfig> {
584        // local mode auto-enables allow_loopback unless explicitly overridden
585        let allow_loopback = self.allow_loopback.unwrap_or(self.local);
586
587        Ok(NodeConfig {
588            local: self.local,
589            port: self.port,
590            ipv6: self.ipv6,
591            bootstrap_peers: self.bootstrap_peers,
592            connection_timeout: self
593                .connection_timeout
594                .unwrap_or(Duration::from_secs(DEFAULT_CONNECTION_TIMEOUT_SECS)),
595            max_connections: self.max_connections.unwrap_or(DEFAULT_MAX_CONNECTIONS),
596            dht_config: self.dht_config.unwrap_or_default(),
597            diversity_config: None,
598            max_message_size: self.max_message_size,
599            node_identity: None,
600            mode: self.mode,
601            custom_user_agent: self.custom_user_agent,
602            allow_loopback,
603            adaptive_dht_config: self.adaptive_dht_config.unwrap_or_default(),
604            close_group_cache_dir: self.close_group_cache_dir,
605            close_group_cache_max_age: self
606                .close_group_cache_max_age
607                .unwrap_or_else(default_close_group_cache_max_age),
608        })
609    }
610}
611
612impl Default for NodeConfig {
613    fn default() -> Self {
614        Self {
615            local: false,
616            port: DEFAULT_LISTEN_PORT,
617            ipv6: true,
618            bootstrap_peers: Vec::new(),
619            connection_timeout: Duration::from_secs(DEFAULT_CONNECTION_TIMEOUT_SECS),
620            max_connections: DEFAULT_MAX_CONNECTIONS,
621            dht_config: DHTConfig::default(),
622            diversity_config: None,
623            max_message_size: None,
624            node_identity: None,
625            mode: NodeMode::default(),
626            custom_user_agent: None,
627            allow_loopback: false,
628            adaptive_dht_config: AdaptiveDhtConfig::default(),
629            close_group_cache_dir: None,
630            close_group_cache_max_age: default_close_group_cache_max_age(),
631        }
632    }
633}
634
635impl DHTConfig {
636    /// Default K value (bucket size) for Kademlia routing.
637    pub const DEFAULT_K_VALUE: usize = 20;
638    const DEFAULT_ALPHA_VALUE: usize = 3;
639    const DEFAULT_REFRESH_INTERVAL_SECS: u64 = 600;
640    /// Minimum k_value — values below this produce degenerate routing behavior.
641    const MIN_K_VALUE: usize = 4;
642
643    /// Validate parameter safety constraints (Section 4 points 1-13).
644    ///
645    /// Returns `Err` if any constraint is violated.
646    pub fn validate(&self) -> Result<()> {
647        if self.k_value < Self::MIN_K_VALUE {
648            return Err(P2PError::Validation(
649                format!(
650                    "k_value must be >= {} (got {}), values below {} produce degenerate behavior",
651                    Self::MIN_K_VALUE,
652                    self.k_value,
653                    Self::MIN_K_VALUE,
654                )
655                .into(),
656            ));
657        }
658        if self.alpha_value < 1 {
659            return Err(P2PError::Validation(
660                format!("alpha_value must be >= 1 (got {})", self.alpha_value).into(),
661            ));
662        }
663        if self.refresh_interval.is_zero() {
664            return Err(P2PError::Validation("refresh_interval must be > 0".into()));
665        }
666        Ok(())
667    }
668}
669
670impl Default for DHTConfig {
671    fn default() -> Self {
672        Self {
673            k_value: Self::DEFAULT_K_VALUE,
674            alpha_value: Self::DEFAULT_ALPHA_VALUE,
675            refresh_interval: Duration::from_secs(Self::DEFAULT_REFRESH_INTERVAL_SECS),
676        }
677    }
678}
679
680/// Information about a connected peer
681#[derive(Debug, Clone)]
682pub struct PeerInfo {
683    /// Transport-level channel identifier (internal use only).
684    #[allow(dead_code)]
685    pub(crate) channel_id: String,
686
687    /// Peer's addresses
688    pub addresses: Vec<MultiAddr>,
689
690    /// Connection timestamp
691    pub connected_at: Instant,
692
693    /// Last seen timestamp
694    pub last_seen: Instant,
695
696    /// Connection status
697    pub status: ConnectionStatus,
698
699    /// Supported protocols
700    pub protocols: Vec<String>,
701
702    /// Number of heartbeats received
703    pub heartbeat_count: u64,
704}
705
706/// Connection status for a peer
707#[derive(Debug, Clone, PartialEq)]
708pub enum ConnectionStatus {
709    /// Connection is being established
710    Connecting,
711    /// Connection is established and active
712    Connected,
713    /// Connection is being closed
714    Disconnecting,
715    /// Connection is closed
716    Disconnected,
717    /// Connection failed
718    Failed(String),
719}
720
721/// Network events that can occur in the P2P system
722///
723/// Events are broadcast to all listeners and provide real-time
724/// notifications of network state changes and message arrivals.
725#[derive(Debug, Clone)]
726pub enum P2PEvent {
727    /// Message received from a peer on a specific topic
728    Message {
729        /// Topic or channel the message was sent on
730        topic: String,
731        /// For signed messages this is the authenticated app-level [`PeerId`];
732        /// `None` for unsigned messages.
733        source: Option<PeerId>,
734        /// IP transport address that delivered this message, when known.
735        ///
736        /// This is provenance metadata, not an identity signal.
737        transport_source: Option<MultiAddr>,
738        /// Sender-supplied Unix timestamp in seconds.
739        ///
740        /// For signed messages this value is covered by the ML-DSA-65 signature
741        /// alongside the payload, so handlers can use it for application-level
742        /// freshness or replay defense. Wire-level acceptance no longer gates
743        /// on this value; subscribers MUST do their own age/dedup checks when
744        /// the protocol requires them.
745        timestamp: u64,
746        /// Raw message data payload
747        data: Vec<u8>,
748    },
749    /// An authenticated peer has connected (first signed message verified on any channel).
750    /// The `user_agent` identifies the remote software (e.g. `"node/0.12.1"`, `"client/1.0"`).
751    PeerConnected(PeerId, String),
752    /// An authenticated peer has fully disconnected (all channels closed).
753    PeerDisconnected(PeerId),
754}
755
756/// Response from a peer to a request sent via [`P2PNode::send_request`].
757///
758/// Contains the response payload along with metadata about the responder
759/// and round-trip latency.
760#[derive(Debug, Clone)]
761pub struct PeerResponse {
762    /// The peer that sent the response.
763    pub peer_id: PeerId,
764    /// Raw response payload bytes.
765    pub data: Vec<u8>,
766    /// Round-trip latency from request to response.
767    pub latency: Duration,
768}
769
770/// Wire format for request/response correlation.
771///
772/// Wraps application payloads with a message ID and direction flag
773/// so the receive loop can route responses back to waiting callers.
774#[derive(Debug, Clone, Serialize, Deserialize)]
775pub(crate) struct RequestResponseEnvelope {
776    /// Unique identifier to correlate request ↔ response.
777    pub(crate) message_id: String,
778    /// `false` for requests, `true` for responses.
779    pub(crate) is_response: bool,
780    /// Application payload.
781    pub(crate) payload: Vec<u8>,
782}
783
784/// An in-flight request awaiting a response from a specific peer.
785pub(crate) struct PendingRequest {
786    /// Oneshot sender for delivering the response payload.
787    pub(crate) response_tx: tokio::sync::oneshot::Sender<Vec<u8>>,
788    /// The peer we expect the response from (for origin validation).
789    pub(crate) expected_peer: PeerId,
790}
791
792/// Short grace period after closing stale QUIC connections before re-dialing.
793///
794/// `disconnect_channel` is async and waits for the QUIC close, but the
795/// transport endpoint may need a moment to fully release internal state.
796/// Only applied when stale channels were actually disconnected.
797const QUIC_TEARDOWN_GRACE: Duration = Duration::from_millis(100);
798
799/// Main P2P network node that manages connections, routing, and communication
800///
801/// This struct represents a complete P2P network participant that can:
802/// - Connect to other peers via QUIC transport
803/// - Participate in distributed hash table (DHT) operations
804/// - Send and receive messages through various protocols
805/// - Handle network events and peer lifecycle
806///
807/// Transport concerns (connections, messaging, events) are delegated to
808/// `TransportHandle`.
809pub struct P2PNode {
810    /// Node configuration
811    config: NodeConfig,
812
813    /// Our peer ID
814    peer_id: PeerId,
815
816    /// Transport handle owning all QUIC / peer / event state
817    transport: Arc<crate::transport_handle::TransportHandle>,
818
819    /// Node start time
820    start_time: Instant,
821
822    /// Shutdown token — cancelled when the node should stop
823    shutdown: CancellationToken,
824
825    /// Dedicated cancellation token for periodic close-group-cache saves.
826    /// Cancelled and joined before the authoritative shutdown snapshot.
827    close_group_cache_save_shutdown: CancellationToken,
828
829    /// Periodic close-group-cache task, retained so shutdown can prevent a
830    /// late periodic write from replacing the final snapshot.
831    close_group_cache_save_handle: TokioMutex<Option<tokio::task::JoinHandle<()>>>,
832
833    /// Adaptive DHT layer — owns both the DHT manager and the trust engine.
834    /// All DHT operations and trust signals go through this component.
835    adaptive_dht: AdaptiveDHT,
836
837    /// Bootstrap state tracking - indicates whether peer discovery has completed
838    is_bootstrapped: Arc<AtomicBool>,
839
840    /// Whether `start()` has been called (and `stop()` has not yet completed)
841    is_started: Arc<AtomicBool>,
842
843    /// Per-peer locks that serialise reconnect attempts so concurrent sends
844    /// to the same stale peer don't race to dial.  Entries accumulate over
845    /// the node's lifetime; each is a lightweight `Arc<TokioMutex<()>>`.
846    reconnect_locks: ParkingMutex<HashMap<PeerId, Arc<TokioMutex<()>>>>,
847
848    /// The peer ID of the node currently relaying traffic for us (ADR-014).
849    ///
850    /// Set after the reachability classifier acquires a relay in `start()`.
851    /// The relayer monitor watches this against the K-closest set: if the
852    /// relayer drops out, it triggers rebinding.
853    ///
854    /// `None` when the node is publicly reachable (no relay needed) or
855    /// before classification has run.
856    relayer_peer_id: Arc<RwLock<Option<PeerId>>>,
857
858    /// The relay-allocated public address (ADR-014).
859    ///
860    /// Set after a proactive MASQUE relay is acquired in `start()`. This is
861    /// the address that external peers must dial to reach this node through
862    /// the relay. `None` when the node is publicly reachable (no relay) or
863    /// before classification has run.
864    relay_address: Arc<RwLock<Option<SocketAddr>>>,
865}
866
867/// Normalize wildcard bind addresses to localhost loopback addresses
868///
869/// saorsa-transport correctly rejects "unspecified" addresses (0.0.0.0 and [::]) for remote connections
870/// because you cannot connect TO an unspecified address - these are only valid for BINDING.
871///
872/// This function converts wildcard addresses to appropriate loopback addresses for local connections:
873/// - IPv6 [::]:port → ::1:port (IPv6 loopback)
874/// - IPv4 0.0.0.0:port → 127.0.0.1:port (IPv4 loopback)
875/// - All other addresses pass through unchanged
876///
877/// # Arguments
878/// * `addr` - The SocketAddr to normalize
879///
880/// # Returns
881/// * Normalized SocketAddr suitable for remote connections
882pub(crate) fn normalize_wildcard_to_loopback(addr: std::net::SocketAddr) -> std::net::SocketAddr {
883    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
884
885    if addr.ip().is_unspecified() {
886        // Convert unspecified addresses to loopback
887        let loopback_ip = match addr {
888            std::net::SocketAddr::V6(_) => IpAddr::V6(Ipv6Addr::LOCALHOST), // ::1
889            std::net::SocketAddr::V4(_) => IpAddr::V4(Ipv4Addr::LOCALHOST), // 127.0.0.1
890        };
891        std::net::SocketAddr::new(loopback_ip, addr.port())
892    } else {
893        // Not a wildcard address, pass through unchanged
894        addr
895    }
896}
897
898impl P2PNode {
899    /// Create a new P2P node with the given configuration
900    pub async fn new(config: NodeConfig) -> Result<Self> {
901        // Ensure a cryptographic identity exists — generate one if not provided.
902        let node_identity = match config.node_identity.clone() {
903            Some(identity) => identity,
904            None => Arc::new(NodeIdentity::generate()?),
905        };
906
907        // Derive the canonical peer ID from the cryptographic identity.
908        let peer_id = *node_identity.peer_id();
909
910        // Validate parameter safety constraints (Section 4 points 1-13).
911        // Reject invalid config early, before any resources are allocated.
912        config.dht_config.validate()?;
913        if let Some(ref diversity) = config.diversity_config {
914            diversity
915                .validate()
916                .map_err(|e| P2PError::Validation(format!("IP diversity config: {e}").into()))?;
917        }
918
919        // Build transport handle with all transport-level concerns
920        let transport_config = crate::transport_handle::TransportConfig::from_node_config(
921            &config,
922            crate::DEFAULT_EVENT_CHANNEL_CAPACITY,
923            node_identity.clone(),
924        );
925        let transport =
926            Arc::new(crate::transport_handle::TransportHandle::new(transport_config).await?);
927
928        // Initialize AdaptiveDHT — creates the trust engine and DHT manager
929        let dht_manager_config = DhtNetworkConfig {
930            peer_id,
931            node_config: config.clone(),
932            request_timeout: config.connection_timeout,
933            max_concurrent_operations: MAX_ACTIVE_REQUESTS,
934            enable_security: true,
935            swap_threshold: 0.0, // Set by AdaptiveDHT::new() from AdaptiveDhtConfig
936        };
937        let adaptive_dht = AdaptiveDHT::new(
938            transport.clone(),
939            dht_manager_config,
940            config.adaptive_dht_config.clone(),
941        )
942        .await?;
943
944        let node = Self {
945            config,
946            peer_id,
947            transport,
948            start_time: Instant::now(),
949            shutdown: CancellationToken::new(),
950            close_group_cache_save_shutdown: CancellationToken::new(),
951            close_group_cache_save_handle: TokioMutex::new(None),
952            adaptive_dht,
953            is_bootstrapped: Arc::new(AtomicBool::new(false)),
954            is_started: Arc::new(AtomicBool::new(false)),
955            reconnect_locks: ParkingMutex::new(HashMap::new()),
956            relayer_peer_id: Arc::new(RwLock::new(None)),
957            relay_address: Arc::new(RwLock::new(None)),
958        };
959        info!(
960            "Created P2P node with peer ID: {} (call start() to begin networking)",
961            node.peer_id
962        );
963
964        Ok(node)
965    }
966
967    /// Get the peer ID of this node.
968    pub fn peer_id(&self) -> &PeerId {
969        &self.peer_id
970    }
971
972    /// Get the transport handle for sharing with other components.
973    pub fn transport(&self) -> &Arc<crate::transport_handle::TransportHandle> {
974        &self.transport
975    }
976
977    /// The relay-allocated public address, if this node acquired a MASQUE relay.
978    ///
979    /// Returns `Some(addr)` when the node is behind NAT and successfully
980    /// acquired a proactive relay during `start()`. External peers must dial
981    /// this address to reach the node through the relay. Returns `None` when
982    /// the node is publicly reachable or no relay was established.
983    pub async fn relay_address(&self) -> Option<SocketAddr> {
984        *self.relay_address.read().await
985    }
986
987    pub fn local_addr(&self) -> Option<MultiAddr> {
988        self.transport.local_addr()
989    }
990
991    /// Check if the node has completed the initial bootstrap process
992    ///
993    /// Returns `true` if the node has successfully connected to at least one
994    /// bootstrap peer and performed peer discovery (FIND_NODE).
995    pub fn is_bootstrapped(&self) -> bool {
996        self.is_bootstrapped.load(Ordering::SeqCst)
997    }
998
999    /// Manually trigger re-bootstrap (useful for recovery or network rejoin)
1000    ///
1001    /// This clears the bootstrapped state and attempts to reconnect to
1002    /// bootstrap peers and discover new peers.
1003    pub async fn re_bootstrap(&self) -> Result<()> {
1004        self.is_bootstrapped.store(false, Ordering::SeqCst);
1005        self.connect_bootstrap_peers(None).await
1006    }
1007
1008    // =========================================================================
1009    // Trust API — delegates to AdaptiveDHT
1010    // =========================================================================
1011
1012    /// Get the trust engine for advanced use cases
1013    pub fn trust_engine(&self) -> Arc<TrustEngine> {
1014        self.adaptive_dht.trust_engine().clone()
1015    }
1016
1017    /// Report a trust event for a peer.
1018    ///
1019    /// Core only records penalties (connection failures). Positive trust
1020    /// signals are the consumer's responsibility via [`TrustEvent::ApplicationSuccess`].
1021    ///
1022    /// # Example
1023    ///
1024    /// ```rust,ignore
1025    /// use saorsa_core::adaptive::TrustEvent;
1026    ///
1027    /// node.report_trust_event(&peer_id, TrustEvent::ApplicationSuccess(1.0)).await;
1028    /// node.report_trust_event(&peer_id, TrustEvent::ConnectionFailed).await;
1029    /// ```
1030    pub async fn report_trust_event(&self, peer_id: &PeerId, event: TrustEvent) {
1031        self.adaptive_dht.report_trust_event(peer_id, event).await;
1032    }
1033
1034    /// Get the current trust score for a peer (0.0 to 1.0).
1035    ///
1036    /// Returns 0.5 (neutral) for unknown peers.
1037    pub fn peer_trust(&self, peer_id: &PeerId) -> f64 {
1038        self.adaptive_dht.peer_trust(peer_id)
1039    }
1040
1041    /// Get the AdaptiveDHT component for direct access
1042    pub fn adaptive_dht(&self) -> &AdaptiveDHT {
1043        &self.adaptive_dht
1044    }
1045
1046    // =========================================================================
1047    // Request/Response API — Automatic Trust Feedback
1048    // =========================================================================
1049
1050    /// Send a request to a peer and wait for a response with automatic trust penalty reporting.
1051    ///
1052    /// Unlike fire-and-forget `send_message()`, this method:
1053    /// 1. Wraps the payload in a `RequestResponseEnvelope` with a unique message ID
1054    /// 2. Sends it on the `/rr/<protocol>` protocol prefix
1055    /// 3. Waits for a matching response (or timeout)
1056    /// 4. Automatically reports failure to the trust engine (success is the expected baseline)
1057    ///
1058    /// The remote peer's handler should call `send_response()` with the
1059    /// incoming message ID to route the response back.
1060    ///
1061    /// # Arguments
1062    ///
1063    /// * `peer_id` - Target peer
1064    /// * `protocol` - Application protocol name (e.g. `"peer_info"`)
1065    /// * `data` - Request payload bytes
1066    /// * `timeout` - Maximum time to wait for a response
1067    ///
1068    /// # Returns
1069    ///
1070    /// A `PeerResponse` on success, or an error on timeout / connection failure.
1071    ///
1072    /// # Example
1073    ///
1074    /// ```rust,ignore
1075    /// let response = node.send_request(&peer_id, "peer_info", request_data, Duration::from_secs(10)).await?;
1076    /// println!("Got {} bytes from {}", response.data.len(), response.peer_id);
1077    /// ```
1078    pub async fn send_request(
1079        &self,
1080        peer_id: &PeerId,
1081        protocol: &str,
1082        data: Vec<u8>,
1083        timeout: Duration,
1084    ) -> Result<PeerResponse> {
1085        let result = self
1086            .send_request_reconnecting(peer_id, protocol, data, timeout)
1087            .await;
1088        if let Err(ref e) = result {
1089            let event = if matches!(e, P2PError::Timeout(_)) {
1090                TrustEvent::ConnectionTimeout
1091            } else {
1092                TrustEvent::ConnectionFailed
1093            };
1094            self.report_trust_event(peer_id, event).await;
1095        }
1096        result
1097    }
1098
1099    /// Request/response send with reconnect-on-demand.
1100    ///
1101    /// Mirrors [`Self::send_message`]: when there is no live channel to
1102    /// `peer_id` it dials one (serialised per peer via
1103    /// [`Self::reconnect_lock_for`]) before sending, and when an existing
1104    /// channel turns out to be stale it tears it down, reconnects, and retries
1105    /// the request exactly once. The plain transport `send_request` only sends
1106    /// over a pre-existing channel and fails fast with `PeerNotFound`
1107    /// otherwise; routing request/response through this reconnecting path means
1108    /// a request to a peer whose QUIC connection has dropped (e.g. a periodic
1109    /// audit of a close peer that idled out) re-establishes the connection
1110    /// instead of surfacing as a spurious timeout.
1111    ///
1112    /// `timeout` bounds only the response wait inside the transport; the dial
1113    /// is independently bounded by `connect_peer_typed` plus
1114    /// [`crate::dht_network_manager::IDENTITY_EXCHANGE_TIMEOUT`].
1115    async fn send_request_reconnecting(
1116        &self,
1117        peer_id: &PeerId,
1118        protocol: &str,
1119        data: Vec<u8>,
1120        timeout: Duration,
1121    ) -> Result<PeerResponse> {
1122        // Snapshot channel IDs before the send attempt — transport.send_request
1123        // prunes dead channels from bookkeeping but does NOT close the
1124        // underlying QUIC connection. We need the original IDs for
1125        // disconnect_channel later.
1126        let existing_channels = self.transport.channels_for_peer(peer_id).await;
1127
1128        // No live channel — serialise dials so concurrent requests to the same
1129        // unconnected peer don't each open their own QUIC connection.
1130        if existing_channels.is_empty() {
1131            // Hold the per-peer reconnect lock only across the dial so
1132            // concurrent requests to the same cold peer collapse onto one dial —
1133            // not across the response wait, which would serialise every such
1134            // request for the full `timeout`.
1135            {
1136                let lock = self.reconnect_lock_for(peer_id);
1137                let _guard = lock.lock().await;
1138                // Another caller may have connected while we waited for the lock.
1139                if !self.transport.is_peer_connected(peer_id).await {
1140                    self.ensure_channel(peer_id, &[], &[], &[]).await?;
1141                }
1142            }
1143            return self
1144                .transport
1145                .send_request(peer_id, protocol, data, timeout)
1146                .await;
1147        }
1148
1149        // Snapshot addresses before the attempt — transport.send_request prunes
1150        // stale channels, which removes peer_info.
1151        let saved_addrs: Vec<MultiAddr> = self
1152            .transport
1153            .peer_info(peer_id)
1154            .await
1155            .map(|info| info.addresses)
1156            .unwrap_or_default();
1157
1158        // Clone the payload for a possible retry — transport.send_request
1159        // consumes the Vec, and only stale-channel failures are retried.
1160        let retry_data = data.clone();
1161
1162        // Fast path: try the existing connection.
1163        match self
1164            .transport
1165            .send_request(peer_id, protocol, data, timeout)
1166            .await
1167        {
1168            Ok(resp) => return Ok(resp),
1169            Err(e) => {
1170                // A response-deadline timeout means the request WAS delivered
1171                // but went unanswered — reconnecting would not help, so do not
1172                // retry. Only a stale-channel send failure warrants a redial.
1173                if !e.is_stale_channel_send_failure() {
1174                    return Err(e);
1175                }
1176                debug!(
1177                    peer = %peer_id.to_hex(),
1178                    error = %e,
1179                    "stale channel request failed, attempting reconnect",
1180                );
1181            }
1182        }
1183
1184        // Serialise the reconnect (stale-channel teardown + dial) so concurrent
1185        // requests to the same stale peer don't race to dial, but release the
1186        // lock before the response wait so they don't serialise for the full
1187        // `timeout`.
1188        {
1189            let lock = self.reconnect_lock_for(peer_id);
1190            let _guard = lock.lock().await;
1191
1192            // Another caller may have reconnected while we waited for the lock.
1193            if self.transport.is_peer_connected(peer_id).await {
1194                // Close stale QUIC connections that transport.send_request's
1195                // bookkeeping cleanup didn't tear down (it only drops the mapping).
1196                for channel_id in &existing_channels {
1197                    self.transport.disconnect_channel(channel_id).await;
1198                }
1199            } else {
1200                self.ensure_channel(peer_id, &[], &saved_addrs, &existing_channels)
1201                    .await?;
1202            }
1203        }
1204        self.transport
1205            .send_request(peer_id, protocol, retry_data, timeout)
1206            .await
1207    }
1208
1209    pub async fn send_response(
1210        &self,
1211        peer_id: &PeerId,
1212        protocol: &str,
1213        message_id: &str,
1214        data: Vec<u8>,
1215    ) -> Result<()> {
1216        self.transport
1217            .send_response(peer_id, protocol, message_id, data)
1218            .await
1219    }
1220
1221    pub fn parse_request_envelope(data: &[u8]) -> Option<(String, bool, Vec<u8>)> {
1222        crate::transport_handle::TransportHandle::parse_request_envelope(data)
1223    }
1224
1225    pub async fn subscribe(&self, topic: &str) -> Result<()> {
1226        self.transport.subscribe(topic).await
1227    }
1228
1229    pub async fn publish(&self, topic: &str, data: &[u8]) -> Result<()> {
1230        self.transport.publish(topic, data).await
1231    }
1232
1233    /// Get the node configuration
1234    pub fn config(&self) -> &NodeConfig {
1235        &self.config
1236    }
1237
1238    /// Start the P2P node
1239    pub async fn start(&self) -> Result<()> {
1240        info!("Starting P2P node...");
1241
1242        // Start transport listeners and message receiving
1243        self.transport.start_network_listeners().await?;
1244
1245        // Start the adaptive DHT layer (DHT manager + trust engine)
1246        self.adaptive_dht.start().await?;
1247
1248        // Log current listen addresses
1249        let listen_addrs = self.transport.listen_addrs().await;
1250        info!("P2P node started on addresses: {:?}", listen_addrs);
1251
1252        // NOTE: Message receiving is now integrated into the accept loop in start_network_listeners()
1253        // The old start_message_receiving_system() is no longer needed as it competed with the accept
1254        // loop for incoming connections, causing messages to be lost.
1255
1256        // Load close group cache and import trust scores before connecting to peers.
1257        // This ensures trust scores are available when peers are added to the routing table.
1258        let close_group_cache = if let Some(ref dir) = self.config.close_group_cache_dir {
1259            match CloseGroupCache::load_from_dir(dir).await {
1260                Ok(Some(cache)) => {
1261                    let now_epoch = SystemTime::now()
1262                        .duration_since(UNIX_EPOCH)
1263                        .map_or(0, |duration| duration.as_secs());
1264                    if cache.is_stale(now_epoch, self.config.close_group_cache_max_age) {
1265                        warn!(
1266                            cache_age_secs = now_epoch.saturating_sub(cache.saved_at_epoch_secs),
1267                            max_age_secs = self
1268                                .config
1269                                .close_group_cache_max_age
1270                                .map(|age| age.as_secs()),
1271                            "Close group cache is stale; skipping cached trust and Priority-0 peers"
1272                        );
1273                        None
1274                    } else {
1275                        // Filter out peers with non-finite trust scores (NaN/Inf)
1276                        // that could corrupt trust engine state or sort ordering.
1277                        let original_count = cache.peers.len();
1278                        let cache = CloseGroupCache {
1279                            peers: cache
1280                                .peers
1281                                .into_iter()
1282                                .filter(|p| p.trust.score.is_finite())
1283                                .collect(),
1284                            ..cache
1285                        };
1286                        let filtered_count = original_count - cache.peers.len();
1287                        if filtered_count > 0 {
1288                            warn!(
1289                                "Filtered {filtered_count} peers with non-finite trust scores from close group cache"
1290                            );
1291                        }
1292
1293                        let trust_snapshot = TrustSnapshot {
1294                            peers: cache
1295                                .peers
1296                                .iter()
1297                                .map(|p| (p.peer_id, p.trust.clone()))
1298                                .collect(),
1299                        };
1300                        self.adaptive_dht
1301                            .trust_engine()
1302                            .import_snapshot(&trust_snapshot);
1303                        info!(
1304                            cache_age_secs = now_epoch.saturating_sub(cache.saved_at_epoch_secs),
1305                            saved_at_epoch_secs = cache.saved_at_epoch_secs,
1306                            "Loaded {} peers from close group cache (trust scores imported)",
1307                            cache.peers.len()
1308                        );
1309                        Some(cache)
1310                    }
1311                }
1312                Ok(None) => {
1313                    debug!(
1314                        "No close group cache found in {}, fresh start",
1315                        dir.display()
1316                    );
1317                    None
1318                }
1319                Err(e) => {
1320                    warn!(
1321                        "Failed to load close group cache from {}: {e}",
1322                        dir.display()
1323                    );
1324                    None
1325                }
1326            }
1327        } else {
1328            None
1329        };
1330
1331        // Connect to bootstrap peers
1332        self.connect_bootstrap_peers(close_group_cache.as_ref())
1333            .await?;
1334
1335        // Emit BootstrapComplete — the node is connected to the network and
1336        // the DHT routing table is populated; consumers waiting on this
1337        // event can start issuing queries. The relay-acquisition driver
1338        // runs asynchronously after this point, so the node's published
1339        // self-record may be direct-only for a brief window until the
1340        // driver's first acquisition attempt finishes.
1341        {
1342            let dht = self.adaptive_dht.dht_manager();
1343            let rt_size = dht.get_routing_table_size().await;
1344            dht.emit_event(DhtNetworkEvent::BootstrapComplete { num_peers: rt_size });
1345        }
1346
1347        // Spawn the relay-acquisition driver for Node mode.
1348        //
1349        // The driver unconditionally tries to acquire a MASQUE relay from
1350        // an XOR-closest peer right after bootstrap — there is no public/
1351        // private classification. Private candidates are filtered out
1352        // ambiently: their Direct addresses are unreachable from outside
1353        // their NAT, so the QUIC dial fails and the walker advances to
1354        // the next-closest peer.
1355        //
1356        // The driver also owns the relay-lost → republish → reacquire
1357        // state machine (see `reachability::driver` for the full flow).
1358        // Clients (`NodeMode::Client`) do not run the driver at all: they
1359        // are outbound-only and do not need to be reachable.
1360        if self.config.mode != NodeMode::Client {
1361            spawn_acquisition_driver(
1362                self.adaptive_dht.dht_manager().clone(),
1363                Arc::clone(&self.transport),
1364                Arc::clone(&self.relayer_peer_id),
1365                Arc::clone(&self.relay_address),
1366                self.shutdown.clone(),
1367            );
1368        } else {
1369            info!("client mode — skipping relay acquisition driver");
1370        }
1371
1372        if let Some(dir) = self.config.close_group_cache_dir.clone() {
1373            let interval = self
1374                .config
1375                .dht_config
1376                .refresh_interval
1377                .max(MIN_CLOSE_GROUP_CACHE_SAVE_INTERVAL);
1378            let mut task = self.close_group_cache_save_handle.lock().await;
1379            if task.is_none() {
1380                let dht_manager = Arc::clone(self.adaptive_dht.dht_manager());
1381                let trust_engine = Arc::clone(self.adaptive_dht.trust_engine());
1382                let peer_id = self.peer_id;
1383                let k_value = self.config.dht_config.k_value;
1384                let shutdown = self.close_group_cache_save_shutdown.clone();
1385                *task = Some(tokio::spawn(periodic_close_group_cache_save(
1386                    dht_manager,
1387                    trust_engine,
1388                    peer_id,
1389                    k_value,
1390                    dir,
1391                    interval,
1392                    shutdown,
1393                )));
1394                info!(
1395                    interval_secs = interval.as_secs(),
1396                    "Started periodic close group cache persistence"
1397                );
1398            }
1399        }
1400
1401        self.is_started
1402            .store(true, std::sync::atomic::Ordering::Release);
1403
1404        Ok(())
1405    }
1406
1407    // start_network_listeners and start_message_receiving_system
1408    // are now implemented in TransportHandle
1409
1410    /// Run the P2P node (blocks until shutdown)
1411    pub async fn run(&self) -> Result<()> {
1412        if !self.is_running() {
1413            self.start().await?;
1414        }
1415
1416        info!("P2P node running...");
1417
1418        // Block until shutdown is signalled. All background work (connection
1419        // lifecycle, DHT maintenance, EigenTrust) runs in dedicated tasks.
1420        self.shutdown.cancelled().await;
1421
1422        info!("P2P node stopped");
1423        Ok(())
1424    }
1425
1426    /// Stop the P2P node
1427    pub async fn stop(&self) -> Result<()> {
1428        info!("Stopping P2P node...");
1429
1430        // Stop periodic cache persistence and wait for any in-flight write.
1431        // The final save below is then the authoritative shutdown snapshot.
1432        self.close_group_cache_save_shutdown.cancel();
1433        let cache_task = self.close_group_cache_save_handle.lock().await.take();
1434        if let Some(cache_task) = cache_task
1435            && let Err(error) = cache_task.await
1436        {
1437            warn!("Periodic close group cache task failed during shutdown: {error}");
1438        }
1439
1440        // Save close group cache before tearing down the DHT and transport layers.
1441        if let Some(ref dir) = self.config.close_group_cache_dir
1442            && let Err(e) = self.save_close_group_cache(dir, "shutdown").await
1443        {
1444            warn!("Failed to save close group cache on shutdown: {e}");
1445        }
1446
1447        // Signal the run loop to exit
1448        self.shutdown.cancel();
1449
1450        // Stop DHT layer first so leave messages can be sent while transport is still active.
1451        self.adaptive_dht.stop().await?;
1452
1453        // Stop the transport layer (shutdown endpoints, join tasks, disconnect peers)
1454        self.transport.stop().await?;
1455
1456        self.is_started
1457            .store(false, std::sync::atomic::Ordering::Release);
1458
1459        info!("P2P node stopped");
1460        Ok(())
1461    }
1462
1463    /// Graceful shutdown alias for tests
1464    pub async fn shutdown(&self) -> Result<()> {
1465        self.stop().await
1466    }
1467
1468    /// Check if the node is running
1469    pub fn is_running(&self) -> bool {
1470        self.is_started.load(std::sync::atomic::Ordering::Acquire) && !self.shutdown.is_cancelled()
1471    }
1472
1473    /// Get the current listen addresses
1474    pub async fn listen_addrs(&self) -> Vec<MultiAddr> {
1475        self.transport.listen_addrs().await
1476    }
1477
1478    /// Get connected peers
1479    pub async fn connected_peers(&self) -> Vec<PeerId> {
1480        self.transport.connected_peers().await
1481    }
1482
1483    /// Get peer count
1484    pub async fn peer_count(&self) -> usize {
1485        self.transport.peer_count().await
1486    }
1487
1488    /// Get peer info
1489    pub async fn peer_info(&self, peer_id: &PeerId) -> Option<PeerInfo> {
1490        self.transport.peer_info(peer_id).await
1491    }
1492
1493    /// Get the channel ID for a given address, if connected (internal only).
1494    #[allow(dead_code)]
1495    pub(crate) async fn get_channel_id_by_address(&self, addr: &MultiAddr) -> Option<String> {
1496        self.transport.get_channel_id_by_address(addr).await
1497    }
1498
1499    /// List all active transport-level connections (internal only).
1500    #[allow(dead_code)]
1501    pub(crate) async fn list_active_connections(&self) -> Vec<(String, Vec<MultiAddr>)> {
1502        self.transport.list_active_connections().await
1503    }
1504
1505    /// Remove a channel from the peers map (internal only).
1506    #[allow(dead_code)]
1507    pub(crate) async fn remove_channel(&self, channel_id: &str) -> bool {
1508        self.transport.remove_channel(channel_id).await
1509    }
1510
1511    /// Close a channel's QUIC connection and remove it from all tracking maps.
1512    ///
1513    /// Use when a transport-level connection was established but identity
1514    /// exchange failed, so no [`PeerId`] is available for [`disconnect_peer`].
1515    pub(crate) async fn disconnect_channel(&self, channel_id: &str) {
1516        self.transport.disconnect_channel(channel_id).await;
1517    }
1518
1519    /// Check if an authenticated peer is connected (has at least one active channel).
1520    pub async fn is_peer_connected(&self, peer_id: &PeerId) -> bool {
1521        self.transport.is_peer_connected(peer_id).await
1522    }
1523
1524    /// Connect to a peer, returning the transport-level channel ID.
1525    ///
1526    /// The returned channel ID is **not** the app-level [`PeerId`]. To obtain
1527    /// the authenticated peer identity, call
1528    /// [`wait_for_peer_identity`](Self::wait_for_peer_identity) with the
1529    /// returned channel ID.
1530    ///
1531    /// Callers that already know how the address was classified should
1532    /// prefer [`Self::connect_peer_typed`] so the resulting log line
1533    /// carries an accurate `kind` field instead of `unknown`.
1534    pub async fn connect_peer(&self, address: &MultiAddr) -> Result<String> {
1535        self.transport.connect_peer(address).await
1536    }
1537
1538    /// Connect to a peer at the given typed address.
1539    ///
1540    /// Same as [`Self::connect_peer`] but threads the [`AddressType`]
1541    /// through to the transport-level dial log so an operator can tell,
1542    /// after the fact, whether a failed dial was against a `Direct`,
1543    /// `Relay`, `Unverified`, or `Lan` address.
1544    pub async fn connect_peer_typed(
1545        &self,
1546        address: &MultiAddr,
1547        kind: AddressType,
1548    ) -> Result<String> {
1549        self.transport.connect_peer_typed(address, kind).await
1550    }
1551
1552    /// Wait for the identity exchange on `channel_id` to complete, returning
1553    /// the authenticated [`PeerId`].
1554    ///
1555    /// Use this after [`connect_peer`](Self::connect_peer) to bridge the gap
1556    /// between the transport-level channel ID and the app-level peer identity
1557    /// required by [`send_message`](Self::send_message).
1558    pub async fn wait_for_peer_identity(
1559        &self,
1560        channel_id: &str,
1561        timeout: Duration,
1562    ) -> Result<PeerId> {
1563        self.transport
1564            .wait_for_peer_identity(channel_id, timeout)
1565            .await
1566    }
1567
1568    /// Disconnect from a peer
1569    pub async fn disconnect_peer(&self, peer_id: &PeerId) -> Result<()> {
1570        self.transport.disconnect_peer(peer_id).await
1571    }
1572
1573    /// Check if a connection to a peer is active (internal only).
1574    #[allow(dead_code)]
1575    pub(crate) async fn is_connection_active(&self, channel_id: &str) -> bool {
1576        self.transport.is_connection_active(channel_id).await
1577    }
1578
1579    /// Send a message to an authenticated peer, reconnecting on demand.
1580    ///
1581    /// Tries the existing connection first. If the send fails (stale QUIC
1582    /// session, peer not found, etc.), resolves a dial address from:
1583    ///
1584    /// 1. Caller-provided `addrs` (highest priority)
1585    /// 2. Addresses cached in the transport layer (snapshotted before the
1586    ///    send attempt, since stale-channel cleanup removes them)
1587    /// 3. DHT routing table
1588    ///
1589    /// Then dials, waits for identity exchange, and retries the send exactly
1590    /// once on the fresh connection.  Concurrent reconnects to the same peer
1591    /// are serialised so only one dial is attempted at a time.
1592    pub async fn send_message(
1593        &self,
1594        peer_id: &PeerId,
1595        protocol: &str,
1596        data: Vec<u8>,
1597        addrs: &[MultiAddr],
1598    ) -> Result<()> {
1599        // Snapshot channel IDs before the send attempt — transport.send_message
1600        // prunes dead channels from bookkeeping but does NOT close the
1601        // underlying QUIC connection.  We need the original IDs for
1602        // disconnect_channel later.
1603        let existing_channels = self.transport.channels_for_peer(peer_id).await;
1604
1605        // No existing connection — serialise so concurrent sends to the same
1606        // unconnected peer don't each open their own QUIC connection.
1607        if existing_channels.is_empty() {
1608            let lock = self.reconnect_lock_for(peer_id);
1609            let _guard = lock.lock().await;
1610
1611            // Another sender may have connected while we waited for the lock.
1612            if self.transport.is_peer_connected(peer_id).await {
1613                return self.transport.send_message(peer_id, protocol, data).await;
1614            }
1615
1616            return self
1617                .reconnect_and_send(peer_id, protocol, data, addrs, &[], &[])
1618                .await;
1619        }
1620
1621        // Snapshot addresses before the send attempt — transport.send_message
1622        // prunes stale channels, which removes peer_info.
1623        let saved_addrs: Vec<MultiAddr> = self
1624            .transport
1625            .peer_info(peer_id)
1626            .await
1627            .map(|info| info.addresses)
1628            .unwrap_or_default();
1629
1630        // Clone data for retry — only stale-channel failures are retried, but
1631        // transport.send_message consumes the Vec.
1632        let retry_data = data.clone();
1633
1634        // Fast path: try existing connection.
1635        let send_result = self.transport.send_message(peer_id, protocol, data).await;
1636        match send_result {
1637            Ok(()) => return Ok(()),
1638            Err(e) => {
1639                if !e.is_stale_channel_send_failure() {
1640                    debug!(
1641                        peer = %peer_id.to_hex(),
1642                        error = %e,
1643                        "send failed during active channel use, not reconnecting",
1644                    );
1645                    return Err(e);
1646                }
1647
1648                debug!(
1649                    peer = %peer_id.to_hex(),
1650                    error = %e,
1651                    "stale channel send failed, attempting reconnect",
1652                );
1653            }
1654        }
1655
1656        // Serialise reconnect attempts so concurrent sends to the same
1657        // stale peer don't race to dial.
1658        let lock = self.reconnect_lock_for(peer_id);
1659        let _guard = lock.lock().await;
1660
1661        // Another sender may have reconnected while we waited for the lock.
1662        if self.transport.is_peer_connected(peer_id).await {
1663            // Close stale QUIC connections that remove_channel (called inside
1664            // transport.send_message on failure) didn't tear down — it only
1665            // removes bookkeeping, not the underlying QUIC session.
1666            for channel_id in &existing_channels {
1667                self.transport.disconnect_channel(channel_id).await;
1668            }
1669            return self
1670                .transport
1671                .send_message(peer_id, protocol, retry_data)
1672                .await;
1673        }
1674
1675        self.reconnect_and_send(
1676            peer_id,
1677            protocol,
1678            retry_data,
1679            addrs,
1680            &saved_addrs,
1681            &existing_channels,
1682        )
1683        .await
1684    }
1685
1686    /// Ensure an identity-authenticated channel to `peer_id` exists, dialing a
1687    /// fresh connection when necessary.
1688    ///
1689    /// Resolves a dial address (caller-provided > saved > DHT routing table),
1690    /// tears down any stale channels, dials, waits for the identity exchange,
1691    /// and verifies the authenticated peer matches `peer_id`. On success the
1692    /// transport's `peer_to_channel` map is populated, so a subsequent
1693    /// `send_message` / `send_request` finds the channel instead of failing
1694    /// with `PeerNotFound`. Returns `PeerNotFound` when no dialable address is
1695    /// available.
1696    ///
1697    /// Shared by [`Self::reconnect_and_send`] and
1698    /// [`Self::send_request_reconnecting`] so both gain identical dial
1699    /// behaviour.
1700    async fn ensure_channel(
1701        &self,
1702        peer_id: &PeerId,
1703        addrs: &[MultiAddr],
1704        saved_addrs: &[MultiAddr],
1705        stale_channels: &[String],
1706    ) -> Result<()> {
1707        // Tear down stale QUIC connections using their actual channel IDs.
1708        // transport.send_message only removes bookkeeping (peer_to_channel,
1709        // peers, active_connections) — it does NOT close the underlying QUIC
1710        // connection.  We must use the real channel IDs, not the resolved
1711        // dial address, because NAT / port migration can make them differ.
1712        if !stale_channels.is_empty() {
1713            for channel_id in stale_channels {
1714                self.transport.disconnect_channel(channel_id).await;
1715            }
1716            tokio::time::sleep(QUIC_TEARDOWN_GRACE).await;
1717        }
1718
1719        let candidates = self
1720            .resolve_dial_candidates(peer_id, addrs, saved_addrs)
1721            .await;
1722        if candidates.is_empty() {
1723            return Err(P2PError::Network(NetworkError::PeerNotFound(
1724                peer_id.to_hex().into(),
1725            )));
1726        }
1727        self.adaptive_dht
1728            .ensure_peer_channel(peer_id, &candidates)
1729            .await
1730    }
1731
1732    /// Tear down stale channels, reconnect to a peer, and send a message.
1733    async fn reconnect_and_send(
1734        &self,
1735        peer_id: &PeerId,
1736        protocol: &str,
1737        data: Vec<u8>,
1738        addrs: &[MultiAddr],
1739        saved_addrs: &[MultiAddr],
1740        stale_channels: &[String],
1741    ) -> Result<()> {
1742        self.ensure_channel(peer_id, addrs, saved_addrs, stale_channels)
1743            .await?;
1744        // Send on the fresh connection.
1745        self.transport.send_message(peer_id, protocol, data).await
1746    }
1747
1748    /// Resolve typed dial candidates for `peer_id`, preferring caller-provided
1749    /// addresses over cached/DHT sources.
1750    ///
1751    /// Returns every dialable (QUIC, non-unspecified) address from the first
1752    /// non-empty source. Caller-provided / saved addresses inherit the
1753    /// [`AddressType`] from the DHT when possible and otherwise fall back to
1754    /// [`AddressType::Unverified`] — the same default the routing table
1755    /// applies to legacy peers that never asserted reachability.
1756    async fn resolve_dial_candidates(
1757        &self,
1758        peer_id: &PeerId,
1759        caller_addrs: &[MultiAddr],
1760        saved_addrs: &[MultiAddr],
1761    ) -> Vec<(MultiAddr, AddressType)> {
1762        let dht_candidates = self
1763            .adaptive_dht
1764            .peer_addresses_for_dial_typed(peer_id)
1765            .await;
1766        let preferred = if !caller_addrs.is_empty() {
1767            caller_addrs
1768        } else if !saved_addrs.is_empty() {
1769            saved_addrs
1770        } else {
1771            return dht_candidates;
1772        };
1773
1774        preferred
1775            .iter()
1776            .filter(|a| {
1777                let dialable = a
1778                    .dialable_socket_addr()
1779                    .is_some_and(|sa| !sa.ip().is_unspecified());
1780                if !dialable {
1781                    trace!(address = %a, "skipping non-dialable address");
1782                }
1783                dialable
1784            })
1785            .map(|addr| {
1786                let kind = dht_candidates
1787                    .iter()
1788                    .find_map(|(candidate, kind)| (candidate == addr).then_some(*kind))
1789                    .unwrap_or(AddressType::Unverified);
1790                (addr.clone(), kind)
1791            })
1792            .collect()
1793    }
1794
1795    /// Get or create a per-peer reconnect lock.
1796    fn reconnect_lock_for(&self, peer_id: &PeerId) -> Arc<TokioMutex<()>> {
1797        self.reconnect_locks
1798            .lock()
1799            .entry(*peer_id)
1800            .or_insert_with(|| Arc::new(TokioMutex::new(())))
1801            .clone()
1802    }
1803}
1804
1805/// Convenience constructor for `P2PError::Network(NetworkError::ProtocolError(...))`.
1806fn protocol_error(msg: impl std::fmt::Display) -> P2PError {
1807    P2PError::Network(NetworkError::ProtocolError(msg.to_string().into()))
1808}
1809
1810/// Helper to send an event via a broadcast sender, logging at trace level if no receivers.
1811pub(crate) fn broadcast_event(tx: &broadcast::Sender<P2PEvent>, event: P2PEvent) {
1812    if let Err(e) = tx.send(event) {
1813        tracing::trace!("Event broadcast has no receivers: {e}");
1814    }
1815}
1816
1817/// Result of parsing a protocol message, including optional authenticated identity.
1818///
1819/// The signed wire timestamp is carried on the inner [`P2PEvent::Message`]
1820/// (see its `timestamp` field) so subscribers can apply their own freshness
1821/// or replay policy now that the wire-level skew gate is gone.
1822pub(crate) struct ParsedMessage {
1823    /// The P2P event to broadcast.
1824    pub(crate) event: P2PEvent,
1825    /// If the message was signed and verified, the authenticated app-level [`PeerId`].
1826    pub(crate) authenticated_node_id: Option<PeerId>,
1827    /// The sender's user agent string from the wire message.
1828    pub(crate) user_agent: String,
1829    /// Decoded payload length (bytes). Lets the rx choke point compute wire
1830    /// envelope overhead (wire − payload) for V2-623 traffic accounting.
1831    pub(crate) payload_len: usize,
1832}
1833
1834/// Parse a postcard-encoded protocol message into a `P2PEvent::Message`.
1835///
1836/// Returns `None` if the bytes cannot be deserialized as a valid `WireMessage`.
1837///
1838/// The `from` field is a required part of the wire protocol but is **not**
1839/// used as the event source. Instead, `source` — the transport-level peer ID
1840/// derived from the authenticated QUIC connection — is used so that consumers
1841/// can pass it directly to `send_message()`. This eliminates a spoofing
1842/// vector where a peer could claim an arbitrary identity via the payload.
1843pub(crate) fn parse_protocol_message(bytes: &[u8], source: &str) -> Option<ParsedMessage> {
1844    let message: WireMessage = postcard::from_bytes(bytes).ok()?;
1845    let transport_source = source.parse::<SocketAddr>().ok().map(MultiAddr::quic);
1846
1847    // Verify app-level signature if present
1848    let authenticated_node_id = if !message.signature.is_empty() {
1849        match verify_message_signature(&message) {
1850            Ok(peer_id) => {
1851                debug!(
1852                    "Message from {} authenticated as app-level NodeId {}",
1853                    source, peer_id
1854                );
1855                Some(peer_id)
1856            }
1857            Err(e) => {
1858                warn!(
1859                    "Rejecting message from {}: signature verification failed: {}",
1860                    source, e
1861                );
1862                return None;
1863            }
1864        }
1865    } else {
1866        None
1867    };
1868
1869    debug!(
1870        "Parsed P2PEvent::Message - topic: {}, source: {:?} (transport: {}, logical: {}), payload_len: {}",
1871        message.protocol,
1872        authenticated_node_id,
1873        source,
1874        message.from,
1875        message.data.len()
1876    );
1877
1878    let payload_len = message.data.len();
1879    Some(ParsedMessage {
1880        event: P2PEvent::Message {
1881            topic: message.protocol,
1882            source: authenticated_node_id,
1883            transport_source,
1884            timestamp: message.timestamp,
1885            data: message.data,
1886        },
1887        authenticated_node_id,
1888        payload_len,
1889        user_agent: message.user_agent,
1890    })
1891}
1892
1893/// Verify the ML-DSA-65 signature on a WireMessage and return the authenticated [`PeerId`].
1894///
1895/// Besides verifying the cryptographic signature, this also checks that the
1896/// self-asserted `from` field matches the [`PeerId`] derived from the public
1897/// key. This prevents a sender from signing with their real key while
1898/// claiming a different identity in the `from` field.
1899fn verify_message_signature(message: &WireMessage) -> std::result::Result<PeerId, String> {
1900    let pubkey = MlDsaPublicKey::from_bytes(&message.public_key)
1901        .map_err(|e| format!("invalid public key: {e:?}"))?;
1902
1903    let peer_id = peer_id_from_public_key(&pubkey);
1904
1905    // Validate that the self-asserted `from` field matches the public key.
1906    if message.from != peer_id {
1907        return Err(format!(
1908            "from field mismatch: message claims '{}' but public key derives '{}'",
1909            message.from, peer_id
1910        ));
1911    }
1912
1913    let signable = postcard::to_stdvec(&(
1914        &message.protocol,
1915        &message.data as &[u8],
1916        &message.from,
1917        message.timestamp,
1918        &message.user_agent,
1919    ))
1920    .map_err(|e| format!("failed to serialize signable bytes: {e}"))?;
1921
1922    let sig = MlDsaSignature::from_bytes(&message.signature)
1923        .map_err(|e| format!("invalid signature: {e:?}"))?;
1924
1925    let valid = crate::quantum_crypto::ml_dsa_verify(&pubkey, &signable, &sig)
1926        .map_err(|e| format!("verification error: {e}"))?;
1927
1928    if valid {
1929        Ok(peer_id)
1930    } else {
1931        Err("signature is invalid".to_string())
1932    }
1933}
1934
1935impl P2PNode {
1936    /// Subscribe to network events
1937    pub fn subscribe_events(&self) -> broadcast::Receiver<P2PEvent> {
1938        self.transport.subscribe_events()
1939    }
1940
1941    /// Backwards-compat event stream accessor for tests
1942    pub fn events(&self) -> broadcast::Receiver<P2PEvent> {
1943        self.subscribe_events()
1944    }
1945
1946    /// Get node uptime
1947    pub fn uptime(&self) -> Duration {
1948        self.start_time.elapsed()
1949    }
1950
1951    // MCP removed: all MCP tool/service methods removed
1952
1953    // /// Handle MCP remote tool call with network integration
1954
1955    // /// List tools available on a specific remote peer
1956
1957    // /// Get MCP server statistics
1958
1959    // Background tasks (connection_lifecycle_monitor, keepalive, periodic_maintenance)
1960    // are now implemented in TransportHandle.
1961
1962    /// Check system health
1963    pub async fn health_check(&self) -> Result<()> {
1964        let peer_count = self.peer_count().await;
1965        if peer_count > self.config.max_connections {
1966            Err(protocol_error(format!(
1967                "Too many connections: {peer_count}"
1968            )))
1969        } else {
1970            Ok(())
1971        }
1972    }
1973
1974    /// Get the attached DHT manager.
1975    pub fn dht_manager(&self) -> &Arc<DhtNetworkManager> {
1976        self.adaptive_dht.dht_manager()
1977    }
1978
1979    /// Backwards-compatible alias for `dht_manager()`.
1980    pub fn dht(&self) -> &Arc<DhtNetworkManager> {
1981        self.dht_manager()
1982    }
1983
1984    /// Connect to bootstrap peers and perform initial peer discovery.
1985    ///
1986    /// If a `close_group_cache` was loaded on startup, its peers are injected
1987    /// as the highest-priority addresses before configured bootstrap peers.
1988    /// Their trust scores were already imported into the `TrustEngine` before
1989    /// this method is called.
1990    async fn connect_bootstrap_peers(
1991        &self,
1992        close_group_cache: Option<&CloseGroupCache>,
1993    ) -> Result<()> {
1994        // Each entry is a list of addresses for a single peer. Close-group
1995        // peers are dialed serially to preserve trust-priority ordering;
1996        // configured bootstrap peers are dialed concurrently to cut cold-start
1997        // latency when some peers are slow or dead.
1998        let mut serial_addr_sets: Vec<(PeerId, Vec<MultiAddr>)> = Vec::new();
1999        let mut parallel_addr_sets: Vec<Vec<MultiAddr>> = Vec::new();
2000        let mut seen_addresses = std::collections::HashSet::new();
2001
2002        // Priority 0: Cached close group peers (pre-trusted, highest priority).
2003        // These peers had trust scores loaded into the TrustEngine earlier in start(),
2004        // so they are already known-good when added to the routing table.
2005        // Sorted by trust score (highest first), then XOR distance (closest first)
2006        // as tiebreaker so we reconnect to the most trusted, closest peers first.
2007        if let Some(cache) = close_group_cache {
2008            let mut sorted_peers: Vec<&CachedCloseGroupPeer> = cache.peers.iter().collect();
2009            sorted_peers.sort_by(|a, b| {
2010                // NaN-safe comparison: push NaN scores to the back instead
2011                // of treating them as equal (which would silently promote
2012                // corrupted entries to the front of the reconnection queue).
2013                let score_ord = match b.trust.score.partial_cmp(&a.trust.score) {
2014                    Some(ord) => ord,
2015                    None => {
2016                        if a.trust.score.is_nan() {
2017                            std::cmp::Ordering::Greater // a is NaN, push to back
2018                        } else {
2019                            std::cmp::Ordering::Less // b is NaN, push b to back
2020                        }
2021                    }
2022                };
2023                score_ord.then_with(|| {
2024                    let da = self.peer_id.xor_distance(&a.peer_id);
2025                    let db = self.peer_id.xor_distance(&b.peer_id);
2026                    da.cmp(&db)
2027                })
2028            });
2029
2030            let mut added_from_close_group = 0usize;
2031            for peer in &sorted_peers {
2032                let new_addresses: Vec<MultiAddr> = peer
2033                    .addresses
2034                    .iter()
2035                    .filter(|a| {
2036                        a.dialable_socket_addr()
2037                            .is_some_and(|sa| !seen_addresses.contains(&sa))
2038                    })
2039                    .cloned()
2040                    .collect();
2041
2042                if !new_addresses.is_empty() {
2043                    for addr in &new_addresses {
2044                        if let Some(sa) = addr.socket_addr() {
2045                            seen_addresses.insert(sa);
2046                        }
2047                    }
2048                    serial_addr_sets.push((peer.peer_id, new_addresses));
2049                    added_from_close_group += 1;
2050                }
2051            }
2052            if added_from_close_group > 0 {
2053                info!(
2054                    "Added {} close group cache peers (highest trust first)",
2055                    added_from_close_group
2056                );
2057            }
2058        }
2059
2060        // Priority 1: Configured bootstrap peers.
2061        if !self.config.bootstrap_peers.is_empty() {
2062            info!(
2063                "Using {} configured bootstrap peers (priority)",
2064                self.config.bootstrap_peers.len()
2065            );
2066            for multiaddr in &self.config.bootstrap_peers {
2067                let Some(socket_addr) = multiaddr.dialable_socket_addr() else {
2068                    warn!("Skipping non-QUIC bootstrap peer: {}", multiaddr);
2069                    continue;
2070                };
2071                seen_addresses.insert(socket_addr);
2072                parallel_addr_sets.push(vec![multiaddr.clone()]);
2073            }
2074        }
2075
2076        if serial_addr_sets.is_empty() && parallel_addr_sets.is_empty() {
2077            info!("No bootstrap peers configured");
2078            return Ok(());
2079        }
2080
2081        // Connect to bootstrap peers, wait for identity exchange, then
2082        // perform DHT peer discovery using the real cryptographic PeerIds.
2083        let identity_timeout = Duration::from_secs(BOOTSTRAP_IDENTITY_TIMEOUT_SECS);
2084        let mut successful_connections = 0;
2085        let cache_dial_candidates = serial_addr_sets.len();
2086        let configured_dial_candidates = parallel_addr_sets.len();
2087        let mut cache_dial_successes = 0usize;
2088        let mut configured_dial_successes = 0usize;
2089        let mut connected_peer_ids: Vec<PeerId> = Vec::new();
2090
2091        // Phase A: serial close-group dials to preserve trust-priority ordering.
2092        let client_mode = matches!(self.config.mode, NodeMode::Client);
2093        for (expected_peer_id, addrs) in &serial_addr_sets {
2094            if let Some(peer_id) = self
2095                .dial_bootstrap_addr_set(addrs, identity_timeout, "cache", Some(*expected_peer_id))
2096                .await
2097            {
2098                successful_connections += 1;
2099                cache_dial_successes += 1;
2100                connected_peer_ids.push(peer_id);
2101                if client_mode && successful_connections >= CLIENT_BOOTSTRAP_TARGET {
2102                    debug!(
2103                        "Client bootstrap target reached ({successful_connections} peers) — skipping remaining serial dials"
2104                    );
2105                    break;
2106                }
2107            }
2108        }
2109
2110        // Phase B: concurrent dials of configured bootstrap peers, bounded by
2111        // `MAX_CONCURRENT_BOOTSTRAP_DIALS` to cap simultaneous QUIC+PQC
2112        // handshakes. Skipped entirely when a client has already hit its
2113        // target during Phase A.
2114        if !client_mode || successful_connections < CLIENT_BOOTSTRAP_TARGET {
2115            let mut parallel_stream =
2116                futures::stream::iter(parallel_addr_sets.into_iter().map(|addrs| async move {
2117                    self.dial_bootstrap_addr_set(&addrs, identity_timeout, "configured", None)
2118                        .await
2119                }))
2120                .buffer_unordered(MAX_CONCURRENT_BOOTSTRAP_DIALS);
2121            while let Some(result) = parallel_stream.next().await {
2122                if let Some(peer_id) = result {
2123                    successful_connections += 1;
2124                    configured_dial_successes += 1;
2125                    connected_peer_ids.push(peer_id);
2126                    if client_mode && successful_connections >= CLIENT_BOOTSTRAP_TARGET {
2127                        debug!(
2128                            "Client bootstrap target reached ({successful_connections} peers) — cancelling pending dials"
2129                        );
2130                        break;
2131                    }
2132                }
2133            }
2134            // `parallel_stream` is dropped here when the `if` block exits,
2135            // cancelling any in-flight futures inside `buffer_unordered`
2136            // before we proceed to the DHT discovery phase below.
2137        }
2138
2139        info!(
2140            cache_dial_candidates,
2141            cache_dial_successes,
2142            configured_dial_candidates,
2143            configured_dial_successes,
2144            outbound_bootstrap_successes = successful_connections,
2145            outbound_reachable = successful_connections > 0,
2146            "Bootstrap reachability summary"
2147        );
2148
2149        if successful_connections == 0 {
2150            // Outbound connections failed — but for nodes behind symmetric NAT,
2151            // the bootstrap peer may have already connected INBOUND to us.
2152            // Wait briefly and check if we have any transport-level connections.
2153            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
2154            let transport_peers = self.transport.connected_peers().await;
2155            if !transport_peers.is_empty() {
2156                info!(
2157                    "No outbound bootstrap succeeded, but {} inbound peer(s) connected — proceeding with DHT bootstrap",
2158                    transport_peers.len()
2159                );
2160                connected_peer_ids = transport_peers;
2161                successful_connections = connected_peer_ids.len();
2162            } else {
2163                warn!("Failed to connect to any bootstrap peers");
2164                // Starting a node should not be gated on immediate bootstrap connectivity.
2165                // Keep running and allow background discovery / retries to populate peers later.
2166                return Ok(());
2167            }
2168        }
2169
2170        info!(
2171            "Successfully connected to {} bootstrap peers",
2172            successful_connections
2173        );
2174
2175        // Perform DHT peer discovery from connected bootstrap peers.
2176        match self
2177            .dht_manager()
2178            .bootstrap_from_peers(&connected_peer_ids)
2179            .await
2180        {
2181            Ok(count) => info!("DHT peer discovery found {} peers", count),
2182            Err(e) => warn!("DHT peer discovery failed: {}", e),
2183        }
2184
2185        // Perform two consecutive self-lookups to fully refresh the close
2186        // neighborhood. The second lookup may discover peers that joined or
2187        // became reachable during the first lookup (Section 11.2 step 5).
2188        //
2189        // Client-mode nodes don't serve the DHT, so they don't need an
2190        // accurate close neighborhood — they just need enough peers to route
2191        // lookups for their own requests, which `bootstrap_from_peers` above
2192        // already provides. Skipping the self-lookups here cuts cold-start
2193        // latency by tens of seconds when α-sized batches include dead peers.
2194        if matches!(self.config.mode, NodeMode::Node) {
2195            const SELF_LOOKUP_ROUNDS: u8 = 2;
2196            for i in 1..=SELF_LOOKUP_ROUNDS {
2197                if let Err(e) = self.dht_manager().trigger_self_lookup().await {
2198                    warn!("Post-bootstrap self-lookup {i}/{SELF_LOOKUP_ROUNDS} failed: {e}");
2199                } else {
2200                    debug!("Post-bootstrap self-lookup {i}/{SELF_LOOKUP_ROUNDS} completed");
2201                }
2202            }
2203        } else {
2204            debug!("Skipping post-bootstrap self-lookups (client mode)");
2205        }
2206
2207        // Mark node as bootstrapped - we have connected to bootstrap peers
2208        // and initiated peer discovery
2209        self.is_bootstrapped.store(true, Ordering::SeqCst);
2210        info!(
2211            "Bootstrap complete: connected to {} peers, initiated {} discovery requests",
2212            successful_connections,
2213            connected_peer_ids.len()
2214        );
2215
2216        // Save close group cache after initial bootstrap so a crash before
2217        // graceful shutdown still preserves the newly-discovered close group.
2218        if let Some(ref dir) = self.config.close_group_cache_dir
2219            && let Err(e) = self.save_close_group_cache(dir, "post_bootstrap").await
2220        {
2221            warn!("Failed to save close group cache after bootstrap: {e}");
2222        }
2223
2224        Ok(())
2225    }
2226
2227    /// Dial a single bootstrap peer's address set, stopping at the first
2228    /// address that completes the identity handshake. Returns the remote peer's
2229    /// cryptographic PeerId on success. Safe to call concurrently for different
2230    /// peers.
2231    async fn dial_bootstrap_addr_set(
2232        &self,
2233        addrs: &[MultiAddr],
2234        identity_timeout: Duration,
2235        source: &'static str,
2236        expected_peer_id: Option<PeerId>,
2237    ) -> Option<PeerId> {
2238        for addr in addrs {
2239            // Bootstrap addresses come from operator-supplied seeds (CLI
2240            // flags or config file). The local reachability classifier hasn't
2241            // proven them yet, so log them as `Unverified` rather than
2242            // `unknown`.
2243            match self
2244                .transport
2245                .connect_peer_typed(addr, AddressType::Unverified)
2246                .await
2247            {
2248                Ok(channel_id) => match self
2249                    .transport
2250                    .wait_for_peer_identity(&channel_id, identity_timeout)
2251                    .await
2252                {
2253                    Ok(real_peer_id) => {
2254                        if !bootstrap_peer_identity_matches(expected_peer_id, real_peer_id) {
2255                            warn!(
2256                                bootstrap_source = source,
2257                                address = %addr,
2258                                expected_peer_id = ?expected_peer_id,
2259                                actual_peer_id = %real_peer_id,
2260                                "Bootstrap cache identity mismatch; rejecting connection"
2261                            );
2262                            self.disconnect_channel(&channel_id).await;
2263                            continue;
2264                        }
2265                        info!(
2266                            bootstrap_source = source,
2267                            outcome = "ok",
2268                            address = %addr,
2269                            peer_id = %real_peer_id,
2270                            "Bootstrap dial completed"
2271                        );
2272                        return Some(real_peer_id);
2273                    }
2274                    Err(e) => {
2275                        info!(
2276                            bootstrap_source = source,
2277                            outcome = "identity_timeout",
2278                            address = %addr,
2279                            error = %e,
2280                            "Bootstrap dial failed"
2281                        );
2282                        warn!(
2283                            "Timeout waiting for identity from bootstrap peer {}: {}, \
2284                             closing channel {}",
2285                            addr, e, channel_id
2286                        );
2287                        self.disconnect_channel(&channel_id).await;
2288                    }
2289                },
2290                Err(e) => {
2291                    info!(
2292                        bootstrap_source = source,
2293                        outcome = "connect_error",
2294                        address = %addr,
2295                        error = %e,
2296                        "Bootstrap dial failed"
2297                    );
2298                    warn!("Failed to connect to bootstrap peer {}: {}", addr, e);
2299                }
2300            }
2301        }
2302        None
2303    }
2304
2305    /// Persist the current close group peers and their trust scores to disk.
2306    async fn save_close_group_cache(
2307        &self,
2308        dir: &Path,
2309        save_reason: &'static str,
2310    ) -> anyhow::Result<()> {
2311        save_close_group_cache_snapshot(
2312            self.dht_manager(),
2313            self.adaptive_dht.trust_engine(),
2314            self.peer_id,
2315            self.config.dht_config.k_value,
2316            dir,
2317            save_reason,
2318        )
2319        .await
2320    }
2321
2322    // disconnect_all_peers and periodic_tasks are now in TransportHandle
2323}
2324
2325/// Persist a close-group snapshot using owned subsystem handles.
2326///
2327/// Keeping this separate from `P2PNode` allows the periodic task to own every
2328/// dependency it needs without borrowing the node across a spawned task.
2329async fn save_close_group_cache_snapshot(
2330    dht_manager: &DhtNetworkManager,
2331    trust_engine: &TrustEngine,
2332    peer_id: PeerId,
2333    k_value: usize,
2334    dir: &Path,
2335    save_reason: &'static str,
2336) -> anyhow::Result<()> {
2337    let key: crate::dht::Key = *peer_id.as_bytes();
2338    let close_group = dht_manager.find_closest_nodes_local(&key, k_value).await;
2339
2340    let now_epoch = SystemTime::now()
2341        .duration_since(UNIX_EPOCH)
2342        .map_or(0, |duration| duration.as_secs());
2343    let peers: Vec<CachedCloseGroupPeer> = close_group
2344        .into_iter()
2345        .filter_map(|dht_node| {
2346            let score = trust_engine.score(&dht_node.peer_id);
2347            // Guard against NaN/Infinity — serde_json cannot round-trip
2348            // non-finite f64 values, which would corrupt the cache file.
2349            if !score.is_finite() {
2350                return None;
2351            }
2352            Some(CachedCloseGroupPeer {
2353                peer_id: dht_node.peer_id,
2354                addresses: dht_node.addresses,
2355                trust: TrustRecord {
2356                    score,
2357                    last_updated_epoch_secs: now_epoch,
2358                },
2359            })
2360        })
2361        .collect();
2362
2363    let peer_count = peers.len();
2364    let cache = CloseGroupCache {
2365        peers,
2366        saved_at_epoch_secs: now_epoch,
2367    };
2368
2369    cache.save_to_dir(dir).await?;
2370    info!(
2371        save_reason,
2372        "Saved {} close group peers to cache in {}",
2373        peer_count,
2374        dir.display()
2375    );
2376    Ok(())
2377}
2378
2379/// Periodically persist the close group until cancelled.
2380async fn periodic_close_group_cache_save(
2381    dht_manager: Arc<DhtNetworkManager>,
2382    trust_engine: Arc<TrustEngine>,
2383    peer_id: PeerId,
2384    k_value: usize,
2385    dir: PathBuf,
2386    interval: Duration,
2387    shutdown: CancellationToken,
2388) {
2389    let start = tokio::time::Instant::now() + interval;
2390    let mut ticker = tokio::time::interval_at(start, interval);
2391    ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
2392
2393    loop {
2394        tokio::select! {
2395            biased;
2396            () = shutdown.cancelled() => break,
2397            _ = ticker.tick() => {
2398                if let Err(error) = save_close_group_cache_snapshot(
2399                    &dht_manager,
2400                    &trust_engine,
2401                    peer_id,
2402                    k_value,
2403                    &dir,
2404                    "periodic",
2405                ).await {
2406                    warn!("Periodic close group cache save failed: {error}");
2407                }
2408            }
2409        }
2410    }
2411}
2412
2413/// Network sender trait for sending messages
2414#[async_trait::async_trait]
2415#[allow(dead_code)]
2416pub trait NetworkSender: Send + Sync {
2417    /// Send a message to an authenticated peer.
2418    async fn send_message(&self, peer_id: &PeerId, protocol: &str, data: Vec<u8>) -> Result<()>;
2419
2420    /// Get our local peer ID (cryptographic identity).
2421    fn local_peer_id(&self) -> PeerId;
2422}
2423
2424// P2PNetworkSender removed — NetworkSender is now implemented directly on TransportHandle.
2425// NodeBuilder removed — use NodeConfigBuilder + P2PNode::new() instead.
2426
2427/// Helper function to register a new channel.
2428///
2429/// Sync because the underlying map is a sharded `DashMap` — no `.await` is
2430/// needed to take a write lock. Keeping this sync is what lets the inbound
2431/// accept loop in `TransportHandle` insert without yielding, so it cannot
2432/// stall and back-pressure the upstream handshake channel.
2433pub(crate) fn register_new_channel(
2434    peers: &DashMap<String, PeerInfo>,
2435    channel_id: &str,
2436    remote_addr: &MultiAddr,
2437) {
2438    let peer_info = PeerInfo {
2439        channel_id: channel_id.to_owned(),
2440        addresses: vec![remote_addr.clone()],
2441        connected_at: tokio::time::Instant::now(),
2442        last_seen: tokio::time::Instant::now(),
2443        status: ConnectionStatus::Connected,
2444        protocols: vec!["p2p-core/1.0.0".to_string()],
2445        heartbeat_count: 0,
2446    };
2447    peers.insert(channel_id.to_owned(), peer_info);
2448}
2449
2450#[cfg(test)]
2451mod tests {
2452    use super::*;
2453    // MCP removed from tests
2454    use std::time::Duration;
2455    use tokio::time::timeout;
2456
2457    /// 2 MiB — used in builder tests to verify max_message_size configuration.
2458    const TEST_MAX_MESSAGE_SIZE: usize = 2 * 1024 * 1024;
2459
2460    #[test]
2461    fn cached_bootstrap_identity_must_match_handshake_peer() {
2462        let expected = PeerId::from_bytes([1; 32]);
2463        let other = PeerId::from_bytes([2; 32]);
2464
2465        assert!(bootstrap_peer_identity_matches(Some(expected), expected));
2466        assert!(!bootstrap_peer_identity_matches(Some(expected), other));
2467        assert!(bootstrap_peer_identity_matches(None, other));
2468    }
2469
2470    // Test tool handler for network tests
2471
2472    // MCP removed
2473
2474    /// Helper function to create a test node configuration
2475    fn create_test_node_config() -> NodeConfig {
2476        NodeConfig {
2477            local: true,
2478            port: 0,
2479            ipv6: true,
2480            bootstrap_peers: vec![],
2481            connection_timeout: Duration::from_secs(2),
2482            max_connections: 100,
2483            dht_config: DHTConfig::default(),
2484            diversity_config: None,
2485            max_message_size: None,
2486            node_identity: None,
2487            mode: NodeMode::default(),
2488            custom_user_agent: None,
2489            allow_loopback: true,
2490            adaptive_dht_config: AdaptiveDhtConfig::default(),
2491            close_group_cache_dir: None,
2492            close_group_cache_max_age: default_close_group_cache_max_age(),
2493        }
2494    }
2495
2496    /// Helper function to create a test tool
2497    // MCP removed: test tool helper deleted
2498
2499    #[tokio::test]
2500    async fn test_node_config_default() {
2501        let config = NodeConfig::default();
2502
2503        assert_eq!(config.listen_addrs().len(), 2); // IPv4 + IPv6
2504        assert_eq!(config.max_connections, 10000);
2505        assert_eq!(config.connection_timeout, Duration::from_secs(25));
2506        assert_eq!(
2507            config.close_group_cache_max_age,
2508            Some(Duration::from_secs(DEFAULT_CLOSE_GROUP_CACHE_MAX_AGE_SECS))
2509        );
2510    }
2511
2512    #[test]
2513    fn close_group_cache_builder_default_and_explicit_disable() {
2514        let defaulted = NodeConfig::builder().build().unwrap();
2515        assert_eq!(
2516            defaulted.close_group_cache_max_age,
2517            Some(Duration::from_secs(DEFAULT_CLOSE_GROUP_CACHE_MAX_AGE_SECS))
2518        );
2519
2520        let disabled = NodeConfig::builder()
2521            .close_group_cache_max_age(None)
2522            .build()
2523            .unwrap();
2524        assert_eq!(disabled.close_group_cache_max_age, None);
2525        let disabled_json = serde_json::to_string(&disabled).unwrap();
2526        let disabled_roundtrip: NodeConfig = serde_json::from_str(&disabled_json).unwrap();
2527        assert_eq!(disabled_roundtrip.close_group_cache_max_age, None);
2528
2529        let custom = NodeConfig::builder()
2530            .close_group_cache_max_age(Some(Duration::from_secs(120)))
2531            .build()
2532            .unwrap();
2533        assert_eq!(
2534            custom.close_group_cache_max_age,
2535            Some(Duration::from_secs(120))
2536        );
2537    }
2538
2539    #[test]
2540    fn old_serialized_config_gets_default_cache_max_age() {
2541        let mut value = serde_json::to_value(NodeConfig::default()).unwrap();
2542        value
2543            .as_object_mut()
2544            .unwrap()
2545            .remove("close_group_cache_max_age");
2546
2547        let decoded: NodeConfig = serde_json::from_value(value).unwrap();
2548        assert_eq!(
2549            decoded.close_group_cache_max_age,
2550            Some(Duration::from_secs(DEFAULT_CLOSE_GROUP_CACHE_MAX_AGE_SECS))
2551        );
2552    }
2553
2554    #[tokio::test]
2555    async fn test_dht_config_default() {
2556        let config = DHTConfig::default();
2557
2558        assert_eq!(config.k_value, 20);
2559        assert_eq!(config.alpha_value, 3);
2560        assert_eq!(config.refresh_interval, Duration::from_secs(600));
2561    }
2562
2563    #[test]
2564    fn test_connection_status_variants() {
2565        let connecting = ConnectionStatus::Connecting;
2566        let connected = ConnectionStatus::Connected;
2567        let disconnecting = ConnectionStatus::Disconnecting;
2568        let disconnected = ConnectionStatus::Disconnected;
2569        let failed = ConnectionStatus::Failed("test error".to_string());
2570
2571        assert_eq!(connecting, ConnectionStatus::Connecting);
2572        assert_eq!(connected, ConnectionStatus::Connected);
2573        assert_eq!(disconnecting, ConnectionStatus::Disconnecting);
2574        assert_eq!(disconnected, ConnectionStatus::Disconnected);
2575        assert_ne!(connecting, connected);
2576
2577        if let ConnectionStatus::Failed(msg) = failed {
2578            assert_eq!(msg, "test error");
2579        } else {
2580            panic!("Expected Failed status");
2581        }
2582    }
2583
2584    #[tokio::test]
2585    async fn test_node_creation() -> Result<()> {
2586        let config = create_test_node_config();
2587        let node = P2PNode::new(config).await?;
2588
2589        // PeerId is derived from the cryptographic identity (32-byte BLAKE3 hash)
2590        assert_eq!(node.peer_id().to_hex().len(), 64);
2591        assert!(!node.is_running());
2592        assert_eq!(node.peer_count().await, 0);
2593        assert!(node.connected_peers().await.is_empty());
2594
2595        Ok(())
2596    }
2597
2598    #[tokio::test]
2599    async fn test_node_lifecycle() -> Result<()> {
2600        let config = create_test_node_config();
2601        let node = P2PNode::new(config).await?;
2602
2603        // Initially not running
2604        assert!(!node.is_running());
2605
2606        // Start the node
2607        node.start().await?;
2608        assert!(node.is_running());
2609
2610        // Check listen addresses were set (at least one)
2611        let listen_addrs = node.listen_addrs().await;
2612        assert!(
2613            !listen_addrs.is_empty(),
2614            "Expected at least one listening address"
2615        );
2616
2617        // Stop the node
2618        node.stop().await?;
2619        assert!(!node.is_running());
2620
2621        Ok(())
2622    }
2623
2624    #[tokio::test]
2625    async fn close_group_cache_task_is_joined_on_stop() -> Result<()> {
2626        let cache_dir = tempfile::tempdir().unwrap();
2627        let mut config = create_test_node_config();
2628        config.close_group_cache_dir = Some(cache_dir.path().to_path_buf());
2629        let node = P2PNode::new(config).await?;
2630
2631        node.start().await?;
2632        assert!(node.close_group_cache_save_handle.lock().await.is_some());
2633
2634        node.stop().await?;
2635        assert!(node.close_group_cache_save_handle.lock().await.is_none());
2636
2637        Ok(())
2638    }
2639
2640    #[tokio::test]
2641    async fn test_peer_connection() -> Result<()> {
2642        let config1 = create_test_node_config();
2643        let config2 = create_test_node_config();
2644
2645        let node1 = P2PNode::new(config1).await?;
2646        let node2 = P2PNode::new(config2).await?;
2647
2648        node1.start().await?;
2649        node2.start().await?;
2650
2651        let node2_addr = node2
2652            .listen_addrs()
2653            .await
2654            .into_iter()
2655            .find(|a| a.is_ipv4())
2656            .ok_or_else(|| {
2657                P2PError::Network(crate::error::NetworkError::InvalidAddress(
2658                    "Node 2 did not expose an IPv4 listen address".into(),
2659                ))
2660            })?;
2661
2662        // Connect to a real peer (unsigned — no node_identity configured).
2663        // connect_peer returns a transport-level channel ID (String), not a PeerId.
2664        let channel_id = node1.connect_peer(&node2_addr).await?;
2665
2666        // Unauthenticated connections don't appear in the app-level peer maps.
2667        // Verify transport-level tracking via is_connection_active / peers map.
2668        assert!(node1.is_connection_active(&channel_id).await);
2669
2670        // Get peer info from the transport-level peers map (keyed by channel ID)
2671        let peer_info = node1.transport.peer_info_by_channel(&channel_id).await;
2672        assert!(peer_info.is_some());
2673        let info = peer_info.expect("Peer info should exist after connect");
2674        assert_eq!(info.channel_id, channel_id);
2675        assert_eq!(info.status, ConnectionStatus::Connected);
2676        assert!(info.protocols.contains(&"p2p-foundation/1.0".to_string()));
2677
2678        // Disconnect the channel
2679        node1.remove_channel(&channel_id).await;
2680        assert!(!node1.is_connection_active(&channel_id).await);
2681
2682        node1.stop().await?;
2683        node2.stop().await?;
2684
2685        Ok(())
2686    }
2687
2688    #[tokio::test]
2689    async fn test_connect_peer_rejects_tcp_multiaddr() -> Result<()> {
2690        let config = create_test_node_config();
2691        let node = P2PNode::new(config).await?;
2692
2693        let tcp_addr: MultiAddr = "/ip4/127.0.0.1/tcp/1".parse().unwrap();
2694        let result = node.connect_peer(&tcp_addr).await;
2695
2696        assert!(
2697            matches!(
2698                result,
2699                Err(P2PError::Network(
2700                    crate::error::NetworkError::InvalidAddress(_)
2701                ))
2702            ),
2703            "TCP multiaddrs should be rejected before a QUIC dial is attempted, got: {:?}",
2704            result
2705        );
2706
2707        Ok(())
2708    }
2709
2710    // TODO(windows): Investigate QUIC connection issues on Windows CI
2711    // This test consistently fails on Windows GitHub Actions runners with
2712    // "All connect attempts failed" even with IPv4-only config, long delays,
2713    // and multiple retry attempts. The underlying saorsa-transport library may have
2714    // issues on Windows that need investigation.
2715    // See: https://github.com/WithAutonomi/saorsa-core/issues/TBD
2716    #[cfg_attr(target_os = "windows", ignore)]
2717    #[tokio::test]
2718    async fn test_event_subscription() -> Result<()> {
2719        // PeerConnected/PeerDisconnected only fire for authenticated peers
2720        // (nodes with node_identity that send signed messages).
2721        // Configure both nodes with identities so the event subscription test works.
2722        let identity1 =
2723            Arc::new(NodeIdentity::generate().expect("should generate identity for test node1"));
2724        let identity2 =
2725            Arc::new(NodeIdentity::generate().expect("should generate identity for test node2"));
2726
2727        let mut config1 = create_test_node_config();
2728        config1.ipv6 = false;
2729        config1.node_identity = Some(identity1);
2730
2731        let node2_peer_id = *identity2.peer_id();
2732        let mut config2 = create_test_node_config();
2733        config2.ipv6 = false;
2734        config2.node_identity = Some(identity2);
2735
2736        let node1 = P2PNode::new(config1).await?;
2737        let node2 = P2PNode::new(config2).await?;
2738
2739        node1.start().await?;
2740        node2.start().await?;
2741
2742        tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
2743
2744        // Subscribe to node2's events (node2 will receive the signed message)
2745        let mut events = node2.subscribe_events();
2746
2747        let node2_addr = node2.local_addr().ok_or_else(|| {
2748            P2PError::Network(crate::error::NetworkError::ProtocolError(
2749                "No listening address".to_string().into(),
2750            ))
2751        })?;
2752
2753        // Connect node1 → node2
2754        let mut channel_id = None;
2755        for attempt in 0..3 {
2756            if attempt > 0 {
2757                tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
2758            }
2759            match timeout(Duration::from_secs(2), node1.connect_peer(&node2_addr)).await {
2760                Ok(Ok(id)) => {
2761                    channel_id = Some(id);
2762                    break;
2763                }
2764                Ok(Err(_)) | Err(_) => continue,
2765            }
2766        }
2767        let channel_id = channel_id.expect("Failed to connect after 3 attempts");
2768
2769        // Wait for identity exchange to complete via wait_for_peer_identity.
2770        let target_peer_id = node1
2771            .wait_for_peer_identity(&channel_id, Duration::from_secs(2))
2772            .await?;
2773        assert_eq!(target_peer_id, node2_peer_id);
2774
2775        // node1 sends a signed message → node2 authenticates → PeerConnected fires on node2
2776        node1
2777            .send_message(&target_peer_id, "test-topic", b"hello".to_vec(), &[])
2778            .await?;
2779
2780        // Check for PeerConnected event on node2
2781        let event = timeout(Duration::from_secs(2), async {
2782            loop {
2783                match events.recv().await {
2784                    Ok(P2PEvent::PeerConnected(id, _)) => return Ok(id),
2785                    Ok(P2PEvent::Message { .. }) => continue, // skip messages
2786                    Ok(_) => continue,
2787                    Err(e) => return Err(e),
2788                }
2789            }
2790        })
2791        .await;
2792        assert!(event.is_ok(), "Should receive PeerConnected event");
2793        let connected_peer_id = event.expect("Timed out").expect("Channel error");
2794        // The connected peer ID should be node1's app-level ID (a valid PeerId)
2795        assert!(
2796            connected_peer_id.0.iter().any(|&b| b != 0),
2797            "PeerConnected should carry a non-zero peer ID"
2798        );
2799
2800        node1.stop().await?;
2801        node2.stop().await?;
2802
2803        Ok(())
2804    }
2805
2806    // TODO(windows): Same QUIC connection issues as test_event_subscription
2807    #[cfg_attr(target_os = "windows", ignore)]
2808    #[tokio::test]
2809    async fn test_message_sending() -> Result<()> {
2810        // Create two nodes (IPv4-only loopback)
2811        let mut config1 = create_test_node_config();
2812        config1.ipv6 = false;
2813        let node1 = P2PNode::new(config1).await?;
2814        node1.start().await?;
2815
2816        let mut config2 = create_test_node_config();
2817        config2.ipv6 = false;
2818        let node2 = P2PNode::new(config2).await?;
2819        node2.start().await?;
2820
2821        // Wait a bit for nodes to start listening
2822        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
2823
2824        // Get actual listening address of node2
2825        let node2_addr = node2.local_addr().ok_or_else(|| {
2826            P2PError::Network(crate::error::NetworkError::ProtocolError(
2827                "No listening address".to_string().into(),
2828            ))
2829        })?;
2830
2831        // Connect node1 to node2
2832        let channel_id =
2833            match timeout(Duration::from_millis(500), node1.connect_peer(&node2_addr)).await {
2834                Ok(res) => res?,
2835                Err(_) => return Err(P2PError::Network(NetworkError::Timeout)),
2836            };
2837
2838        // Wait for identity exchange via wait_for_peer_identity.
2839        let target_peer_id = node1
2840            .wait_for_peer_identity(&channel_id, Duration::from_secs(2))
2841            .await?;
2842        assert_eq!(target_peer_id, node2.peer_id().clone());
2843
2844        // Send a message
2845        let message_data = b"Hello, peer!".to_vec();
2846        let result = match timeout(
2847            Duration::from_millis(500),
2848            node1.send_message(&target_peer_id, "test-protocol", message_data, &[]),
2849        )
2850        .await
2851        {
2852            Ok(res) => res,
2853            Err(_) => return Err(P2PError::Network(NetworkError::Timeout)),
2854        };
2855        // For now, we'll just check that we don't get a "not connected" error
2856        // The actual send might fail due to no handler on the other side
2857        if let Err(e) = &result {
2858            assert!(!e.to_string().contains("not connected"), "Got error: {}", e);
2859        }
2860
2861        // Try to send to non-existent peer
2862        let non_existent_peer = PeerId::from_bytes([0xFFu8; 32]);
2863        let result = node1
2864            .send_message(&non_existent_peer, "test-protocol", vec![], &[])
2865            .await;
2866        assert!(result.is_err(), "Sending to non-existent peer should fail");
2867
2868        node1.stop().await?;
2869        node2.stop().await?;
2870
2871        Ok(())
2872    }
2873
2874    #[tokio::test]
2875    async fn test_remote_mcp_operations() -> Result<()> {
2876        let config = create_test_node_config();
2877        let node = P2PNode::new(config).await?;
2878
2879        // MCP removed; test reduced to simple start/stop
2880        node.start().await?;
2881        node.stop().await?;
2882        Ok(())
2883    }
2884
2885    #[tokio::test]
2886    async fn test_health_check() -> Result<()> {
2887        let config = create_test_node_config();
2888        let node = P2PNode::new(config).await?;
2889
2890        // Health check should pass with no connections
2891        let result = node.health_check().await;
2892        assert!(result.is_ok());
2893
2894        // Note: We're not actually connecting to real peers here
2895        // since that would require running bootstrap nodes.
2896        // The health check should still pass with no connections.
2897
2898        Ok(())
2899    }
2900
2901    #[tokio::test]
2902    async fn test_node_uptime() -> Result<()> {
2903        let config = create_test_node_config();
2904        let node = P2PNode::new(config).await?;
2905
2906        let uptime1 = node.uptime();
2907        assert!(uptime1 >= Duration::from_secs(0));
2908
2909        // Wait a bit
2910        tokio::time::sleep(Duration::from_millis(10)).await;
2911
2912        let uptime2 = node.uptime();
2913        assert!(uptime2 > uptime1);
2914
2915        Ok(())
2916    }
2917
2918    #[tokio::test]
2919    async fn test_node_config_access() -> Result<()> {
2920        let config = create_test_node_config();
2921        let node = P2PNode::new(config).await?;
2922
2923        let node_config = node.config();
2924        assert_eq!(node_config.max_connections, 100);
2925        // MCP removed
2926
2927        Ok(())
2928    }
2929
2930    #[tokio::test]
2931    async fn test_mcp_server_access() -> Result<()> {
2932        let config = create_test_node_config();
2933        let _node = P2PNode::new(config).await?;
2934
2935        // MCP removed
2936        Ok(())
2937    }
2938
2939    #[tokio::test]
2940    async fn test_dht_access() -> Result<()> {
2941        let config = create_test_node_config();
2942        let node = P2PNode::new(config).await?;
2943
2944        // DHT is always available
2945        let _dht = node.dht();
2946
2947        Ok(())
2948    }
2949
2950    #[tokio::test]
2951    async fn test_node_config_builder() -> Result<()> {
2952        let bootstrap: MultiAddr = "/ip4/127.0.0.1/udp/9000/quic".parse().unwrap();
2953
2954        let config = NodeConfig::builder()
2955            .local(true)
2956            .ipv6(true)
2957            .bootstrap_peer(bootstrap)
2958            .connection_timeout(Duration::from_secs(15))
2959            .max_connections(200)
2960            .max_message_size(TEST_MAX_MESSAGE_SIZE)
2961            .build()?;
2962
2963        assert_eq!(config.listen_addrs().len(), 2); // IPv4 + IPv6
2964        assert!(config.local);
2965        assert!(config.ipv6);
2966        assert_eq!(config.bootstrap_peers.len(), 1);
2967        assert_eq!(config.connection_timeout, Duration::from_secs(15));
2968        assert_eq!(config.max_connections, 200);
2969        assert_eq!(config.max_message_size, Some(TEST_MAX_MESSAGE_SIZE));
2970        assert!(config.allow_loopback); // auto-enabled by local(true)
2971
2972        Ok(())
2973    }
2974
2975    #[tokio::test]
2976    async fn test_bootstrap_peers() -> Result<()> {
2977        let mut config = create_test_node_config();
2978        config.bootstrap_peers = vec![
2979            crate::MultiAddr::from_ipv4(std::net::Ipv4Addr::LOCALHOST, 9200),
2980            crate::MultiAddr::from_ipv4(std::net::Ipv4Addr::LOCALHOST, 9201),
2981        ];
2982
2983        let node = P2PNode::new(config).await?;
2984
2985        // Start node (which attempts to connect to bootstrap peers)
2986        node.start().await?;
2987
2988        // In a test environment, bootstrap peers may not be available
2989        // The test verifies the node starts correctly with bootstrap configuration
2990        // Peer count may include local/internal tracking, so we just verify it's reasonable
2991        let _peer_count = node.peer_count().await;
2992
2993        node.stop().await?;
2994        Ok(())
2995    }
2996
2997    #[tokio::test]
2998    async fn test_peer_info_structure() {
2999        let peer_info = PeerInfo {
3000            channel_id: "test_peer".to_string(),
3001            addresses: vec!["/ip4/127.0.0.1/tcp/9000".parse::<MultiAddr>().unwrap()],
3002            connected_at: Instant::now(),
3003            last_seen: Instant::now(),
3004            status: ConnectionStatus::Connected,
3005            protocols: vec!["test-protocol".to_string()],
3006            heartbeat_count: 0,
3007        };
3008
3009        assert_eq!(peer_info.channel_id, "test_peer");
3010        assert_eq!(peer_info.addresses.len(), 1);
3011        assert_eq!(peer_info.status, ConnectionStatus::Connected);
3012        assert_eq!(peer_info.protocols.len(), 1);
3013    }
3014
3015    #[tokio::test]
3016    async fn test_serialization() -> Result<()> {
3017        // Test that configs can be serialized/deserialized
3018        let config = create_test_node_config();
3019        let serialized = serde_json::to_string(&config)?;
3020        let deserialized: NodeConfig = serde_json::from_str(&serialized)?;
3021
3022        assert_eq!(config.local, deserialized.local);
3023        assert_eq!(config.port, deserialized.port);
3024        assert_eq!(config.ipv6, deserialized.ipv6);
3025        assert_eq!(config.bootstrap_peers, deserialized.bootstrap_peers);
3026
3027        Ok(())
3028    }
3029
3030    #[tokio::test]
3031    async fn test_get_channel_id_by_address_found() -> Result<()> {
3032        let config = create_test_node_config();
3033        let node = P2PNode::new(config).await?;
3034
3035        // Manually insert a peer for testing
3036        let test_channel_id = "peer_test_123".to_string();
3037        let test_address = "192.168.1.100:9000";
3038        let test_multiaddr = MultiAddr::quic(test_address.parse().unwrap());
3039
3040        let peer_info = PeerInfo {
3041            channel_id: test_channel_id.clone(),
3042            addresses: vec![test_multiaddr],
3043            connected_at: Instant::now(),
3044            last_seen: Instant::now(),
3045            status: ConnectionStatus::Connected,
3046            protocols: vec!["test-protocol".to_string()],
3047            heartbeat_count: 0,
3048        };
3049
3050        node.transport
3051            .inject_peer(test_channel_id.clone(), peer_info)
3052            .await;
3053
3054        // Test: Find channel by address
3055        let lookup_addr = MultiAddr::quic(test_address.parse().unwrap());
3056        let found_channel_id = node.get_channel_id_by_address(&lookup_addr).await;
3057        assert_eq!(found_channel_id, Some(test_channel_id));
3058
3059        Ok(())
3060    }
3061
3062    #[tokio::test]
3063    async fn test_get_channel_id_by_address_not_found() -> Result<()> {
3064        let config = create_test_node_config();
3065        let node = P2PNode::new(config).await?;
3066
3067        // Test: Try to find a channel that doesn't exist
3068        let unknown_addr = MultiAddr::quic("192.168.1.200:9000".parse().unwrap());
3069        let result = node.get_channel_id_by_address(&unknown_addr).await;
3070        assert_eq!(result, None);
3071
3072        Ok(())
3073    }
3074
3075    #[tokio::test]
3076    async fn test_get_channel_id_by_address_invalid_format() -> Result<()> {
3077        let config = create_test_node_config();
3078        let node = P2PNode::new(config).await?;
3079
3080        // Test: Non-IP address should return None (no matching socket addr)
3081        let ble_addr = MultiAddr::new(crate::address::TransportAddr::Ble {
3082            mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x01],
3083            psm: 0x0025,
3084        });
3085        let result = node.get_channel_id_by_address(&ble_addr).await;
3086        assert_eq!(result, None);
3087
3088        Ok(())
3089    }
3090
3091    #[tokio::test]
3092    async fn test_get_channel_id_by_address_multiple_peers() -> Result<()> {
3093        let config = create_test_node_config();
3094        let node = P2PNode::new(config).await?;
3095
3096        // Add multiple peers with different addresses
3097        let peer1_id = "peer_1".to_string();
3098        let peer1_addr_str = "192.168.1.101:9001";
3099        let peer1_multiaddr = MultiAddr::quic(peer1_addr_str.parse().unwrap());
3100
3101        let peer2_id = "peer_2".to_string();
3102        let peer2_addr_str = "192.168.1.102:9002";
3103        let peer2_multiaddr = MultiAddr::quic(peer2_addr_str.parse().unwrap());
3104
3105        let peer1_info = PeerInfo {
3106            channel_id: peer1_id.clone(),
3107            addresses: vec![peer1_multiaddr],
3108            connected_at: Instant::now(),
3109            last_seen: Instant::now(),
3110            status: ConnectionStatus::Connected,
3111            protocols: vec!["test-protocol".to_string()],
3112            heartbeat_count: 0,
3113        };
3114
3115        let peer2_info = PeerInfo {
3116            channel_id: peer2_id.clone(),
3117            addresses: vec![peer2_multiaddr],
3118            connected_at: Instant::now(),
3119            last_seen: Instant::now(),
3120            status: ConnectionStatus::Connected,
3121            protocols: vec!["test-protocol".to_string()],
3122            heartbeat_count: 0,
3123        };
3124
3125        node.transport
3126            .inject_peer(peer1_id.clone(), peer1_info)
3127            .await;
3128        node.transport
3129            .inject_peer(peer2_id.clone(), peer2_info)
3130            .await;
3131
3132        // Test: Find each channel by their unique address
3133        let found_peer1 = node
3134            .get_channel_id_by_address(&MultiAddr::quic(peer1_addr_str.parse().unwrap()))
3135            .await;
3136        let found_peer2 = node
3137            .get_channel_id_by_address(&MultiAddr::quic(peer2_addr_str.parse().unwrap()))
3138            .await;
3139
3140        assert_eq!(found_peer1, Some(peer1_id));
3141        assert_eq!(found_peer2, Some(peer2_id));
3142
3143        Ok(())
3144    }
3145
3146    #[tokio::test]
3147    async fn test_list_active_connections_empty() -> Result<()> {
3148        let config = create_test_node_config();
3149        let node = P2PNode::new(config).await?;
3150
3151        // Test: No connections initially
3152        let connections = node.list_active_connections().await;
3153        assert!(connections.is_empty());
3154
3155        Ok(())
3156    }
3157
3158    #[tokio::test]
3159    async fn test_list_active_connections_with_peers() -> Result<()> {
3160        let config = create_test_node_config();
3161        let node = P2PNode::new(config).await?;
3162
3163        // Add multiple peers
3164        let peer1_id = "peer_1".to_string();
3165        let peer1_addrs = vec![
3166            MultiAddr::quic("192.168.1.101:9001".parse().unwrap()),
3167            MultiAddr::quic("192.168.1.101:9002".parse().unwrap()),
3168        ];
3169
3170        let peer2_id = "peer_2".to_string();
3171        let peer2_addrs = vec![MultiAddr::quic("192.168.1.102:9003".parse().unwrap())];
3172
3173        let peer1_info = PeerInfo {
3174            channel_id: peer1_id.clone(),
3175            addresses: peer1_addrs.clone(),
3176            connected_at: Instant::now(),
3177            last_seen: Instant::now(),
3178            status: ConnectionStatus::Connected,
3179            protocols: vec!["test-protocol".to_string()],
3180            heartbeat_count: 0,
3181        };
3182
3183        let peer2_info = PeerInfo {
3184            channel_id: peer2_id.clone(),
3185            addresses: peer2_addrs.clone(),
3186            connected_at: Instant::now(),
3187            last_seen: Instant::now(),
3188            status: ConnectionStatus::Connected,
3189            protocols: vec!["test-protocol".to_string()],
3190            heartbeat_count: 0,
3191        };
3192
3193        node.transport
3194            .inject_peer(peer1_id.clone(), peer1_info)
3195            .await;
3196        node.transport
3197            .inject_peer(peer2_id.clone(), peer2_info)
3198            .await;
3199
3200        // Also add to active_connections (list_active_connections iterates over this)
3201        node.transport
3202            .inject_active_connection(peer1_id.clone())
3203            .await;
3204        node.transport
3205            .inject_active_connection(peer2_id.clone())
3206            .await;
3207
3208        // Test: List all active connections
3209        let connections = node.list_active_connections().await;
3210        assert_eq!(connections.len(), 2);
3211
3212        // Verify peer1 and peer2 are in the list
3213        let peer1_conn = connections.iter().find(|(id, _)| id == &peer1_id);
3214        let peer2_conn = connections.iter().find(|(id, _)| id == &peer2_id);
3215
3216        assert!(peer1_conn.is_some());
3217        assert!(peer2_conn.is_some());
3218
3219        // Verify addresses match
3220        assert_eq!(peer1_conn.unwrap().1, peer1_addrs);
3221        assert_eq!(peer2_conn.unwrap().1, peer2_addrs);
3222
3223        Ok(())
3224    }
3225
3226    #[tokio::test]
3227    async fn test_remove_channel_success() -> Result<()> {
3228        let config = create_test_node_config();
3229        let node = P2PNode::new(config).await?;
3230
3231        // Add a peer
3232        let channel_id = "peer_to_remove".to_string();
3233        let channel_peer_id = PeerId::from_name(&channel_id);
3234        let peer_info = PeerInfo {
3235            channel_id: channel_id.clone(),
3236            addresses: vec![MultiAddr::quic("192.168.1.100:9000".parse().unwrap())],
3237            connected_at: Instant::now(),
3238            last_seen: Instant::now(),
3239            status: ConnectionStatus::Connected,
3240            protocols: vec!["test-protocol".to_string()],
3241            heartbeat_count: 0,
3242        };
3243
3244        node.transport
3245            .inject_peer(channel_id.clone(), peer_info)
3246            .await;
3247        node.transport
3248            .inject_peer_to_channel(channel_peer_id, channel_id.clone())
3249            .await;
3250
3251        // Verify peer exists
3252        assert!(node.is_peer_connected(&channel_peer_id).await);
3253
3254        // Remove the channel
3255        let removed = node.remove_channel(&channel_id).await;
3256        assert!(removed);
3257
3258        // Verify peer no longer exists
3259        assert!(!node.is_peer_connected(&channel_peer_id).await);
3260
3261        Ok(())
3262    }
3263
3264    #[tokio::test]
3265    async fn test_remove_channel_nonexistent() -> Result<()> {
3266        let config = create_test_node_config();
3267        let node = P2PNode::new(config).await?;
3268
3269        // Try to remove a channel that doesn't exist
3270        let removed = node.remove_channel("nonexistent_peer").await;
3271        assert!(!removed);
3272
3273        Ok(())
3274    }
3275
3276    #[tokio::test]
3277    async fn test_is_peer_connected() -> Result<()> {
3278        let config = create_test_node_config();
3279        let node = P2PNode::new(config).await?;
3280
3281        let channel_id = "test_peer".to_string();
3282        let channel_peer_id = PeerId::from_name(&channel_id);
3283
3284        // Initially not connected
3285        assert!(!node.is_peer_connected(&channel_peer_id).await);
3286
3287        // Add peer
3288        let peer_info = PeerInfo {
3289            channel_id: channel_id.clone(),
3290            addresses: vec![MultiAddr::quic("192.168.1.100:9000".parse().unwrap())],
3291            connected_at: Instant::now(),
3292            last_seen: Instant::now(),
3293            status: ConnectionStatus::Connected,
3294            protocols: vec!["test-protocol".to_string()],
3295            heartbeat_count: 0,
3296        };
3297
3298        node.transport
3299            .inject_peer(channel_id.clone(), peer_info)
3300            .await;
3301        node.transport
3302            .inject_peer_to_channel(channel_peer_id, channel_id.clone())
3303            .await;
3304
3305        // Now connected
3306        assert!(node.is_peer_connected(&channel_peer_id).await);
3307
3308        // Remove channel
3309        node.remove_channel(&channel_id).await;
3310
3311        // No longer connected
3312        assert!(!node.is_peer_connected(&channel_peer_id).await);
3313
3314        Ok(())
3315    }
3316
3317    #[test]
3318    fn test_normalize_ipv6_wildcard() {
3319        use std::net::{IpAddr, Ipv6Addr, SocketAddr};
3320
3321        let wildcard = SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8080);
3322        let normalized = normalize_wildcard_to_loopback(wildcard);
3323
3324        assert_eq!(normalized.ip(), IpAddr::V6(Ipv6Addr::LOCALHOST));
3325        assert_eq!(normalized.port(), 8080);
3326    }
3327
3328    #[test]
3329    fn test_normalize_ipv4_wildcard() {
3330        use std::net::{IpAddr, Ipv4Addr, SocketAddr};
3331
3332        let wildcard = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9000);
3333        let normalized = normalize_wildcard_to_loopback(wildcard);
3334
3335        assert_eq!(normalized.ip(), IpAddr::V4(Ipv4Addr::LOCALHOST));
3336        assert_eq!(normalized.port(), 9000);
3337    }
3338
3339    #[test]
3340    fn test_normalize_specific_address_unchanged() {
3341        let specific: std::net::SocketAddr = "192.168.1.100:3000".parse().unwrap();
3342        let normalized = normalize_wildcard_to_loopback(specific);
3343
3344        assert_eq!(normalized, specific);
3345    }
3346
3347    #[test]
3348    fn test_normalize_loopback_unchanged() {
3349        use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
3350
3351        let loopback_v6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 5000);
3352        let normalized_v6 = normalize_wildcard_to_loopback(loopback_v6);
3353        assert_eq!(normalized_v6, loopback_v6);
3354
3355        let loopback_v4 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5000);
3356        let normalized_v4 = normalize_wildcard_to_loopback(loopback_v4);
3357        assert_eq!(normalized_v4, loopback_v4);
3358    }
3359
3360    // ---- parse_protocol_message regression tests ----
3361
3362    /// Get current Unix timestamp for tests
3363    fn current_timestamp() -> u64 {
3364        std::time::SystemTime::now()
3365            .duration_since(std::time::UNIX_EPOCH)
3366            .map(|d| d.as_secs())
3367            .unwrap_or(0)
3368    }
3369
3370    /// Helper to create a postcard-serialized unsigned WireMessage for tests
3371    fn make_wire_bytes(protocol: &str, data: Vec<u8>, from: &str, timestamp: u64) -> Vec<u8> {
3372        let msg = WireMessage {
3373            protocol: protocol.to_string(),
3374            data,
3375            from: PeerId::from_name(from),
3376            timestamp,
3377            user_agent: String::new(),
3378            public_key: Vec::new(),
3379            signature: Vec::new(),
3380        };
3381        postcard::to_stdvec(&msg).unwrap()
3382    }
3383
3384    /// Helper to create a postcard-serialized signed WireMessage for tests.
3385    fn make_signed_wire_bytes(
3386        identity: &NodeIdentity,
3387        protocol: &str,
3388        data: Vec<u8>,
3389        timestamp: u64,
3390    ) -> Vec<u8> {
3391        let from = *identity.peer_id();
3392        let user_agent = "test/1.0";
3393        let signable =
3394            postcard::to_stdvec(&(protocol, data.as_slice(), &from, timestamp, user_agent))
3395                .unwrap();
3396        let sig = identity.sign(&signable).expect("signing should succeed");
3397        let msg = WireMessage {
3398            protocol: protocol.to_string(),
3399            data,
3400            from,
3401            timestamp,
3402            user_agent: user_agent.to_string(),
3403            public_key: identity.public_key().as_bytes().to_vec(),
3404            signature: sig.as_bytes().to_vec(),
3405        };
3406        postcard::to_stdvec(&msg).unwrap()
3407    }
3408
3409    #[test]
3410    fn test_parse_protocol_message_uses_transport_peer_id_as_source() {
3411        // Regression: For unsigned messages, P2PEvent::Message.source must be the
3412        // transport peer ID, NOT the "from" field from the wire message.
3413        let transport_id = "abcdef0123456789";
3414        let logical_id = "spoofed-logical-id";
3415        let bytes = make_wire_bytes("test/v1", vec![1, 2, 3], logical_id, current_timestamp());
3416
3417        let parsed =
3418            parse_protocol_message(&bytes, transport_id).expect("valid message should parse");
3419
3420        // Unsigned message: no authenticated node ID
3421        assert!(parsed.authenticated_node_id.is_none());
3422
3423        match parsed.event {
3424            P2PEvent::Message {
3425                topic,
3426                source,
3427                transport_source,
3428                timestamp: _,
3429                data,
3430            } => {
3431                assert!(source.is_none(), "unsigned message source must be None");
3432                assert!(
3433                    transport_source.is_none(),
3434                    "non-socket transport source should not produce an IP transport address"
3435                );
3436                assert_eq!(topic, "test/v1");
3437                assert_eq!(data, vec![1u8, 2, 3]);
3438            }
3439            other => panic!("expected P2PEvent::Message, got {:?}", other),
3440        }
3441    }
3442
3443    #[test]
3444    fn test_parse_protocol_message_rejects_invalid_bytes() {
3445        // Random bytes that are not valid bincode should be rejected
3446        assert!(parse_protocol_message(b"not valid bincode", "peer-id").is_none());
3447    }
3448
3449    #[test]
3450    fn test_parse_protocol_message_rejects_truncated_message() {
3451        // A truncated bincode message should fail to deserialize
3452        let full_bytes = make_wire_bytes("test/v1", vec![1, 2, 3], "sender", current_timestamp());
3453        let truncated = &full_bytes[..full_bytes.len() / 2];
3454        assert!(parse_protocol_message(truncated, "peer-id").is_none());
3455    }
3456
3457    #[test]
3458    fn test_parse_protocol_message_empty_payload() {
3459        let bytes = make_wire_bytes("ping", vec![], "sender", current_timestamp());
3460
3461        let parsed = parse_protocol_message(&bytes, "transport-peer")
3462            .expect("valid message with empty data should parse");
3463
3464        match parsed.event {
3465            P2PEvent::Message { data, .. } => assert!(data.is_empty()),
3466            other => panic!("expected P2PEvent::Message, got {:?}", other),
3467        }
3468    }
3469
3470    #[test]
3471    fn test_parse_protocol_message_records_ip_transport_source() {
3472        let bytes = make_wire_bytes("ping", vec![1], "sender", current_timestamp());
3473
3474        let parsed =
3475            parse_protocol_message(&bytes, "192.168.1.2:4567").expect("valid message should parse");
3476
3477        match parsed.event {
3478            P2PEvent::Message {
3479                transport_source, ..
3480            } => {
3481                assert_eq!(
3482                    transport_source,
3483                    Some(MultiAddr::quic("192.168.1.2:4567".parse().unwrap()))
3484                );
3485            }
3486            other => panic!("expected P2PEvent::Message, got {:?}", other),
3487        }
3488    }
3489
3490    #[test]
3491    fn test_parse_protocol_message_preserves_binary_payload() {
3492        // Verify that arbitrary byte values (including 0xFF, 0x00) survive round-trip
3493        let payload: Vec<u8> = (0..=255).collect();
3494        let bytes = make_wire_bytes("binary/v1", payload.clone(), "sender", current_timestamp());
3495
3496        let parsed = parse_protocol_message(&bytes, "peer-id")
3497            .expect("valid message with full byte range should parse");
3498
3499        match parsed.event {
3500            P2PEvent::Message { data, topic, .. } => {
3501                assert_eq!(topic, "binary/v1");
3502                assert_eq!(
3503                    data, payload,
3504                    "payload must survive bincode round-trip exactly"
3505                );
3506            }
3507            other => panic!("expected P2PEvent::Message, got {:?}", other),
3508        }
3509    }
3510
3511    #[test]
3512    fn test_parse_signed_message_verifies_and_uses_node_id() {
3513        let identity = NodeIdentity::generate().expect("should generate identity");
3514        let protocol = "test/signed";
3515        let data: Vec<u8> = vec![10, 20, 30];
3516        // The `from` field must match the PeerId derived from the public key.
3517        let from = *identity.peer_id();
3518        let timestamp = current_timestamp();
3519        let user_agent = "test/1.0";
3520
3521        // Compute signable bytes the same way create_protocol_message does
3522        let signable =
3523            postcard::to_stdvec(&(protocol, data.as_slice(), &from, timestamp, user_agent))
3524                .unwrap();
3525        let sig = identity.sign(&signable).expect("signing should succeed");
3526
3527        let msg = WireMessage {
3528            protocol: protocol.to_string(),
3529            data: data.clone(),
3530            from,
3531            timestamp,
3532            user_agent: user_agent.to_string(),
3533            public_key: identity.public_key().as_bytes().to_vec(),
3534            signature: sig.as_bytes().to_vec(),
3535        };
3536        let bytes = postcard::to_stdvec(&msg).unwrap();
3537
3538        let parsed =
3539            parse_protocol_message(&bytes, "transport-xyz").expect("signed message should parse");
3540
3541        let expected_peer_id = *identity.peer_id();
3542        assert_eq!(
3543            parsed.authenticated_node_id.as_ref(),
3544            Some(&expected_peer_id)
3545        );
3546
3547        match parsed.event {
3548            P2PEvent::Message { source, .. } => {
3549                assert_eq!(
3550                    source.as_ref(),
3551                    Some(&expected_peer_id),
3552                    "source should be the verified PeerId"
3553                );
3554            }
3555            other => panic!("expected P2PEvent::Message, got {:?}", other),
3556        }
3557    }
3558
3559    #[test]
3560    fn test_parse_message_with_bad_signature_is_rejected() {
3561        let identity = NodeIdentity::generate().expect("should generate identity");
3562        let protocol = "test/bad-sig";
3563        let data: Vec<u8> = vec![1, 2, 3];
3564        let from = *identity.peer_id();
3565        let timestamp = current_timestamp();
3566        let user_agent = "test/1.0";
3567
3568        // Sign correct signable bytes
3569        let signable =
3570            postcard::to_stdvec(&(protocol, data.as_slice(), &from, timestamp, user_agent))
3571                .unwrap();
3572        let sig = identity.sign(&signable).expect("signing should succeed");
3573
3574        // Tamper with the data (signature was over [1,2,3], not [99,99,99])
3575        let msg = WireMessage {
3576            protocol: protocol.to_string(),
3577            data: vec![99, 99, 99],
3578            from,
3579            timestamp,
3580            user_agent: user_agent.to_string(),
3581            public_key: identity.public_key().as_bytes().to_vec(),
3582            signature: sig.as_bytes().to_vec(),
3583        };
3584        let bytes = postcard::to_stdvec(&msg).unwrap();
3585
3586        assert!(
3587            parse_protocol_message(&bytes, "transport-xyz").is_none(),
3588            "message with bad signature should be rejected"
3589        );
3590    }
3591
3592    #[test]
3593    fn test_parse_message_with_mismatched_from_is_rejected() {
3594        let identity = NodeIdentity::generate().expect("should generate identity");
3595        let protocol = "test/from-mismatch";
3596        let data: Vec<u8> = vec![1, 2, 3];
3597        // Use a `from` field that does NOT match the public key's PeerId.
3598        let fake_from = PeerId::from_bytes([0xDE; 32]);
3599        let timestamp = current_timestamp();
3600        let user_agent = "test/1.0";
3601
3602        let signable =
3603            postcard::to_stdvec(&(protocol, data.as_slice(), &fake_from, timestamp, user_agent))
3604                .unwrap();
3605        let sig = identity.sign(&signable).expect("signing should succeed");
3606
3607        let msg = WireMessage {
3608            protocol: protocol.to_string(),
3609            data,
3610            from: fake_from,
3611            timestamp,
3612            user_agent: user_agent.to_string(),
3613            public_key: identity.public_key().as_bytes().to_vec(),
3614            signature: sig.as_bytes().to_vec(),
3615        };
3616        let bytes = postcard::to_stdvec(&msg).unwrap();
3617
3618        assert!(
3619            parse_protocol_message(&bytes, "transport-xyz").is_none(),
3620            "message with mismatched from field should be rejected"
3621        );
3622    }
3623
3624    #[test]
3625    fn test_parse_protocol_message_accepts_arbitrary_timestamps() {
3626        // Clock skew between peers must not drop messages.
3627        // Regression: previously ±5 min tolerance silently rejected all
3628        // traffic when client and node clocks differed.
3629        let payload = vec![1, 2, 3];
3630
3631        // 10 hours in the past
3632        let old_ts = current_timestamp().saturating_sub(36_000);
3633        let old_bytes = make_wire_bytes("test/old", payload.clone(), "sender", old_ts);
3634        assert!(
3635            parse_protocol_message(&old_bytes, "peer-id").is_some(),
3636            "should accept unsigned message with timestamp 10h in the past"
3637        );
3638
3639        // 10 hours in the future
3640        let future_ts = current_timestamp().saturating_add(36_000);
3641        let future_bytes = make_wire_bytes("test/future", payload.clone(), "sender", future_ts);
3642        assert!(
3643            parse_protocol_message(&future_bytes, "peer-id").is_some(),
3644            "should accept unsigned message with timestamp 10h in the future"
3645        );
3646
3647        // Signed messages must take the same path: timestamp remains part of the
3648        // signed bytes for integrity, but is not used for wall-clock rejection.
3649        let identity = NodeIdentity::generate().expect("should generate identity");
3650        let signed_old =
3651            make_signed_wire_bytes(&identity, "test/signed-old", payload.clone(), old_ts);
3652        assert!(
3653            parse_protocol_message(&signed_old, "transport-xyz").is_some(),
3654            "should accept signed message with timestamp 10h in the past"
3655        );
3656
3657        let signed_future =
3658            make_signed_wire_bytes(&identity, "test/signed-future", payload, future_ts);
3659        assert!(
3660            parse_protocol_message(&signed_future, "transport-xyz").is_some(),
3661            "should accept signed message with timestamp 10h in the future"
3662        );
3663    }
3664
3665    #[test]
3666    fn test_parse_protocol_message_exposes_timestamp_on_event() {
3667        // After removing the wall-clock skew gate, the signed timestamp must
3668        // remain reachable on `P2PEvent::Message` so application-layer handlers
3669        // can implement freshness / replay defense.
3670        let ts: u64 = 1_234_567_890;
3671        let bytes = make_wire_bytes("test/ts", vec![9, 9, 9], "sender", ts);
3672        let parsed = parse_protocol_message(&bytes, "peer-id").expect("valid message should parse");
3673        match parsed.event {
3674            P2PEvent::Message { timestamp, .. } => {
3675                assert_eq!(timestamp, ts, "P2PEvent::Message.timestamp must round-trip");
3676            }
3677            other => panic!("expected P2PEvent::Message, got {:?}", other),
3678        }
3679    }
3680
3681    #[test]
3682    fn test_signed_message_timestamp_is_signature_covered() {
3683        // Sign once, mutate only the timestamp, assert rejection. This is the
3684        // only timestamp property still enforced by `parse_protocol_message`
3685        // after the wall-clock gate was removed: signature integrity.
3686        let identity = NodeIdentity::generate().expect("should generate identity");
3687        let ts: u64 = 1_700_000_000;
3688        let signed = make_signed_wire_bytes(&identity, "test/sig", vec![1, 2, 3], ts);
3689
3690        // Sanity: unmodified bytes parse and authenticate.
3691        let parsed = parse_protocol_message(&signed, "transport-xyz")
3692            .expect("unmodified signed message should parse");
3693        assert!(parsed.authenticated_node_id.is_some());
3694
3695        // Now tamper with just the timestamp on the wire and re-serialize.
3696        let mut tampered: WireMessage =
3697            postcard::from_bytes(&signed).expect("signed bytes must deserialize");
3698        tampered.timestamp = ts.wrapping_add(1);
3699        let tampered_bytes = postcard::to_stdvec(&tampered).expect("re-serialize");
3700
3701        assert!(
3702            parse_protocol_message(&tampered_bytes, "transport-xyz").is_none(),
3703            "timestamp-only mutation on a signed message must fail signature verification"
3704        );
3705    }
3706}