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::bootstrap::routing_snapshot::{RoutingSnapshot, SnapshotPeer};
23use crate::dht::core_engine::AddressType;
24use crate::dht_network_manager::{DhtNetworkConfig, DhtNetworkEvent, DhtNetworkManager};
25use crate::error::{NetworkError, P2PError, P2pResult as Result};
26use crate::reachability::spawn_acquisition_driver;
27
28use crate::MultiAddr;
29use crate::identity::node_identity::{NodeIdentity, peer_id_from_public_key};
30use crate::quantum_crypto::saorsa_transport_integration::{MlDsaPublicKey, MlDsaSignature};
31use bytes::Bytes;
32use dashmap::DashMap;
33use futures::StreamExt;
34use parking_lot::Mutex as ParkingMutex;
35use serde::{Deserialize, Serialize};
36use std::collections::{HashMap, HashSet};
37use std::net::SocketAddr;
38use std::path::{Path, PathBuf};
39use std::sync::Arc;
40use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
41use std::time::{Duration, SystemTime, UNIX_EPOCH};
42use tokio::sync::{Mutex as TokioMutex, RwLock, broadcast};
43use tokio::time::Instant;
44use tokio_util::sync::CancellationToken;
45use tracing::{debug, info, trace, warn};
46
47fn bootstrap_peer_identity_matches(expected: Option<PeerId>, actual: PeerId) -> bool {
48    expected.is_none_or(|expected| expected == actual)
49}
50
51/// Wire protocol message format for P2P communication.
52///
53/// Serialized with postcard for compact binary encoding.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub(crate) struct WireMessage {
56    /// Protocol/topic identifier
57    pub(crate) protocol: String,
58    /// Raw payload bytes
59    pub(crate) data: Vec<u8>,
60    /// Sender's peer ID (verified against transport-level identity)
61    pub(crate) from: PeerId,
62    /// Unix timestamp in seconds
63    pub(crate) timestamp: u64,
64    /// User agent string identifying the sender's software.
65    ///
66    /// Convention: `"node/<version>"` for full DHT participants,
67    /// `"client/<version>"` or `"<app>/<version>"` for ephemeral clients.
68    /// Included in the signed bytes — tamper-proof.
69    #[serde(default)]
70    pub(crate) user_agent: String,
71    /// Sender's ML-DSA-65 public key (1952 bytes). Empty if unsigned.
72    #[serde(default)]
73    pub(crate) public_key: Vec<u8>,
74    /// ML-DSA-65 signature over the signable bytes. Empty if unsigned.
75    #[serde(default)]
76    pub(crate) signature: Vec<u8>,
77}
78
79/// Borrowing view of a [`WireMessage`] for serialization.
80///
81/// Serializes to exactly the bytes [`WireMessage`] would produce for the
82/// same content (same field order and postcard encoding), so a sender can
83/// frame a payload it only borrows without first copying it into an owned
84/// message. Receivers keep decoding into [`WireMessage`].
85#[derive(Serialize)]
86pub(crate) struct WireMessageRef<'a> {
87    pub(crate) protocol: &'a str,
88    pub(crate) data: &'a [u8],
89    pub(crate) from: &'a PeerId,
90    pub(crate) timestamp: u64,
91    pub(crate) user_agent: &'a str,
92    pub(crate) public_key: &'a [u8],
93    pub(crate) signature: &'a [u8],
94}
95
96/// Operating mode of a P2P node.
97///
98/// Determines the default user agent and DHT participation behavior.
99/// `Node` peers participate in the DHT routing table; `Client` peers
100/// are treated as ephemeral and excluded from routing.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
102pub enum NodeMode {
103    /// Full DHT-participant node that maintains routing state and routes messages.
104    #[default]
105    Node,
106    /// Ephemeral client that connects to perform operations without joining the DHT.
107    Client,
108}
109
110/// Internal listen mode controlling which network interfaces the node binds to.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112enum ListenMode {
113    /// Bind to all interfaces (`0.0.0.0` / `::`).
114    Public,
115    /// Bind to loopback only (`127.0.0.1` / `::1`).
116    Local,
117}
118
119/// Returns the default software identifier for the given mode.
120///
121/// - `Node` → `"node/<saorsa-core-version>"`
122/// - `Client` → `"client/<saorsa-core-version>"`
123pub fn user_agent_for_mode(mode: NodeMode) -> String {
124    let prefix = match mode {
125        NodeMode::Node => "node",
126        NodeMode::Client => "client",
127    };
128    format!("{prefix}/{}", env!("CARGO_PKG_VERSION"))
129}
130
131/// Returns `true` if the user agent identifies a full DHT participant (prefix `"node/"`).
132pub fn is_dht_participant(user_agent: &str) -> bool {
133    user_agent.starts_with("node/")
134}
135
136/// Capacity of the internal channel used by the message receiving system.
137pub(crate) const MESSAGE_RECV_CHANNEL_CAPACITY: usize = 256;
138
139/// Maximum number of concurrent in-flight request/response operations.
140pub(crate) const MAX_ACTIVE_REQUESTS: usize = 256;
141
142/// Maximum allowed timeout for a single request (5 minutes).
143pub(crate) const MAX_REQUEST_TIMEOUT: Duration = Duration::from_secs(300);
144
145/// Default listen port for the P2P node.
146const DEFAULT_LISTEN_PORT: u16 = 9000;
147
148/// Default maximum number of concurrent connections.
149const DEFAULT_MAX_CONNECTIONS: usize = 10_000;
150
151/// Default connection timeout in seconds.
152///
153/// The transport adapter keeps each direct Happy Eyeballs attempt short so
154/// DHT lookups can move past offline peers quickly. 25s leaves room for
155/// multi-stage connection strategies and identity exchange while preserving
156/// the historical API default.
157const DEFAULT_CONNECTION_TIMEOUT_SECS: u64 = 25;
158
159/// Default maximum age of a close-group cache snapshot before it is skipped
160/// as Priority-0 bootstrap material.
161const DEFAULT_CLOSE_GROUP_CACHE_MAX_AGE_SECS: u64 = 60 * 60;
162
163/// Lower bound for periodic close-group-cache saves. Prevents a very short DHT
164/// refresh interval from turning cache persistence into a hot write loop.
165const MIN_CLOSE_GROUP_CACHE_SAVE_INTERVAL: Duration = Duration::from_secs(60);
166
167/// Timeout in seconds for waiting on a bootstrap peer's identity exchange.
168///
169/// Tighter than the post-bootstrap budget
170/// ([`crate::dht_network_manager::IDENTITY_EXCHANGE_TIMEOUT`],
171/// 5 s) on purpose: bootstrap candidates are unverified and a stuck one
172/// must not be allowed to head-of-line block convergence. 3 s covers
173/// loopback (<100 ms) and direct WAN paths (~1–2 s with one handshake
174/// retry); a relay-tunnelled path with congested ML-DSA verification
175/// can exceed this and will fail identity exchange, but bootstrap simply
176/// moves on to other candidates rather than retrying the same one.
177///
178/// `wait_for_peer_identity` short-circuits on channel close, so most dead
179/// channels surface in microseconds regardless of this budget.
180const BOOTSTRAP_IDENTITY_TIMEOUT_SECS: u64 = 3;
181
182/// Maximum number of bootstrap peers dialed concurrently in Phase B.
183///
184/// Bounds the fan-out of configured bootstrap dials so simultaneous QUIC+PQC
185/// handshakes don't spike CPU or saturate the UDP socket. Chosen
186/// low on purpose: each dial runs a full ML-KEM key exchange and ML-DSA
187/// verification, and a cold-start node has no spare compute budget.
188const MAX_CONCURRENT_BOOTSTRAP_DIALS: usize = 4;
189
190/// Number of successful bootstrap connections after which a client-mode
191/// node stops dialing further candidates.
192///
193/// Clients only need enough peers to route their own lookups (α=3 parallel
194/// queries → 6 gives ~2× redundancy) and don't serve the DHT, so a fully
195/// populated close-group buys them nothing. Stopping early cuts cold-start
196/// latency by skipping the tail of slow / dead candidates. Nodes always
197/// dial every candidate so their routing table converges fully.
198const CLIENT_BOOTSTRAP_TARGET: usize = 6;
199
200/// Maximum routing-snapshot peers dialled concurrently.
201///
202/// Higher than [`MAX_CONCURRENT_BOOTSTRAP_DIALS`] because this set is an order
203/// of magnitude larger — a full table rather than a handful of seeds — and the
204/// whole value of restoring it is that the table is usable in seconds. Still
205/// bounded, so a restart cannot open an unbounded number of simultaneous
206/// QUIC+PQC handshakes.
207const MAX_CONCURRENT_SNAPSHOT_DIALS: usize = 16;
208
209/// How long the restored peer count holds authority over saving.
210///
211/// It exists to stop a node that is stopped mid-restore from writing the
212/// fragment it has re-dialled so far over a complete snapshot. Once the table
213/// has had this long to converge it is the node's best knowledge, and a smaller
214/// table is a smaller network rather than an unfinished restore, so the floor
215/// stops applying and the file keeps being refreshed.
216const SNAPSHOT_FLOOR_ENFORCED_FOR: Duration = Duration::from_secs(60 * 60);
217
218/// The floor has not been decided yet: bootstrap has not reached the restore
219/// step, or this is a client, which never restores and never writes one.
220const SNAPSHOT_FLOOR_UNRESOLVED: usize = usize::MAX;
221
222/// Addresses tried per snapshot peer.
223///
224/// The budget below stops new peers, so the phase's real bound is the budget
225/// plus the last peer's attempts. Two keeps that tail short while still
226/// covering a peer whose first address has gone stale.
227const MAX_SNAPSHOT_ADDRESSES_DIALLED: usize = 2;
228
229/// Wall-clock budget for the routing-snapshot dial phase.
230///
231/// Startup must not be held hostage to a snapshot full of departed peers. When
232/// the budget expires the remaining candidates are abandoned and the table
233/// refills through ordinary discovery, which is exactly the behaviour of a node
234/// that had no snapshot at all.
235const SNAPSHOT_RESTORE_BUDGET: Duration = Duration::from_secs(20);
236
237/// Serde helper — returns `true`.
238const fn default_true() -> bool {
239    true
240}
241
242/// Configuration for a P2P node
243#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct NodeConfig {
245    /// Bind to loopback only (`127.0.0.1` / `::1`).
246    ///
247    /// When `true`, the node listens on loopback addresses suitable for
248    /// local development and testing. When `false` (the default), the node
249    /// listens on all interfaces (`0.0.0.0` / `::`).
250    #[serde(default)]
251    pub local: bool,
252
253    /// Listen port. `0` means OS-assigned ephemeral port.
254    #[serde(default)]
255    pub port: u16,
256
257    /// Enable IPv6 dual-stack binding.
258    ///
259    /// When `true` (the default), both an IPv4 and an IPv6 address are
260    /// bound. When `false`, only IPv4 is used.
261    #[serde(default = "default_true")]
262    pub ipv6: bool,
263
264    /// Bootstrap peers to connect to on startup.
265    pub bootstrap_peers: Vec<crate::MultiAddr>,
266
267    // MCP removed; will be redesigned later
268    /// Connection timeout duration
269    pub connection_timeout: Duration,
270
271    /// Maximum number of concurrent connections
272    pub max_connections: usize,
273
274    /// DHT configuration
275    pub dht_config: DHTConfig,
276
277    /// Optional IP diversity configuration for Sybil protection tuning.
278    ///
279    /// When set, this configuration is used by diversity-enforcing subsystems.
280    /// If `None`, defaults are used.
281    pub diversity_config: Option<crate::security::IPDiversityConfig>,
282
283    /// Optional override for the maximum application-layer message size.
284    ///
285    /// When `None`, the underlying saorsa-transport default is used.
286    #[serde(default)]
287    pub max_message_size: Option<usize>,
288
289    /// Optional node identity for app-level message signing.
290    ///
291    /// When set, outgoing messages are signed with the node's ML-DSA-65 key
292    /// and incoming signed messages are verified at the transport layer.
293    #[serde(skip)]
294    pub node_identity: Option<Arc<NodeIdentity>>,
295
296    /// Operating mode of this node.
297    ///
298    /// Determines the default user agent and DHT participation:
299    /// - `Node` → user agent `"node/<version>"`, added to DHT routing tables.
300    /// - `Client` → user agent `"client/<version>"`, treated as ephemeral.
301    #[serde(default)]
302    pub mode: NodeMode,
303
304    /// Optional custom user agent override.
305    ///
306    /// When `Some`, this value is used instead of the mode-derived default.
307    /// When `None`, the user agent is derived from [`NodeConfig::mode`].
308    #[serde(default, skip_serializing_if = "Option::is_none")]
309    pub custom_user_agent: Option<String>,
310
311    /// Allow loopback addresses (127.0.0.1, ::1) in the transport layer.
312    ///
313    /// In production, loopback addresses are rejected because they are not
314    /// routable. Enable this for local devnets and testnets where all nodes
315    /// run on the same machine.
316    ///
317    /// Default: `false`
318    #[serde(default)]
319    pub allow_loopback: bool,
320
321    /// Adaptive DHT configuration (trust-based swap-out).
322    ///
323    /// Controls whether peers with low trust scores are eligible for
324    /// swap-out from the routing table when better candidates arrive. Use
325    /// `NodeConfigBuilder::trust_enforcement` for a simple on/off toggle.
326    ///
327    /// Default: enabled with a swap threshold of 0.35.
328    #[serde(default)]
329    pub adaptive_dht_config: AdaptiveDhtConfig,
330
331    /// Optional path for persisting the close group cache.
332    ///
333    /// Directory for persisting the close group cache.
334    ///
335    /// When set, the node saves its close group peers and their trust
336    /// scores to `{dir}/close_group_cache.json` periodically, on shutdown,
337    /// and after bootstrap. On startup, fresh cached peers are loaded and
338    /// contacted first, preserving close group consistency across restarts.
339    ///
340    /// When `None`, no close group cache is used.
341    #[serde(default, skip_serializing_if = "Option::is_none")]
342    pub close_group_cache_dir: Option<PathBuf>,
343
344    /// Maximum age for using a close-group cache snapshot as Priority-0
345    /// bootstrap material. Older snapshots are logged and skipped so the node
346    /// falls through to configured bootstrap peers. `None` disables the age
347    /// check. Default: one hour.
348    #[serde(default = "default_close_group_cache_max_age")]
349    pub close_group_cache_max_age: Option<Duration>,
350}
351
352fn default_close_group_cache_max_age() -> Option<Duration> {
353    Some(Duration::from_secs(DEFAULT_CLOSE_GROUP_CACHE_MAX_AGE_SECS))
354}
355
356/// DHT-specific configuration
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct DHTConfig {
359    /// Kademlia K parameter (bucket size)
360    pub k_value: usize,
361
362    /// Kademlia alpha parameter (parallelism)
363    pub alpha_value: usize,
364
365    /// DHT refresh interval
366    pub refresh_interval: Duration,
367}
368
369// ============================================================================
370// Address Construction Helpers
371// ============================================================================
372
373/// Build QUIC listen addresses based on port, IPv6 preference, and listen mode.
374///
375/// All returned addresses use the QUIC transport — the only transport
376/// currently supported for dialing. When additional transports are added,
377/// extend this function to produce addresses for those transports as well.
378///
379/// `ListenMode::Public` uses unspecified (all-interface) addresses;
380/// `ListenMode::Local` uses loopback addresses.
381#[inline]
382fn build_listen_addrs(port: u16, ipv6_enabled: bool, mode: ListenMode) -> Vec<MultiAddr> {
383    let mut addrs = Vec::with_capacity(if ipv6_enabled { 2 } else { 1 });
384
385    let (v4, v6) = match mode {
386        ListenMode::Public => (
387            std::net::Ipv4Addr::UNSPECIFIED,
388            std::net::Ipv6Addr::UNSPECIFIED,
389        ),
390        ListenMode::Local => (std::net::Ipv4Addr::LOCALHOST, std::net::Ipv6Addr::LOCALHOST),
391    };
392
393    if ipv6_enabled {
394        addrs.push(MultiAddr::quic(std::net::SocketAddr::new(
395            std::net::IpAddr::V6(v6),
396            port,
397        )));
398    }
399
400    addrs.push(MultiAddr::quic(std::net::SocketAddr::new(
401        std::net::IpAddr::V4(v4),
402        port,
403    )));
404
405    addrs
406}
407
408impl NodeConfig {
409    /// Returns the effective user agent string.
410    ///
411    /// If a custom user agent was set, returns that. Otherwise, derives
412    /// the user agent from the node's [`NodeMode`].
413    pub fn user_agent(&self) -> String {
414        self.custom_user_agent
415            .clone()
416            .unwrap_or_else(|| user_agent_for_mode(self.mode))
417    }
418
419    /// Compute the listen addresses from the configuration fields.
420    ///
421    /// The returned addresses are derived from [`local`](Self::local),
422    /// [`port`](Self::port), and [`ipv6`](Self::ipv6).
423    pub fn listen_addrs(&self) -> Vec<MultiAddr> {
424        let mode = if self.local {
425            ListenMode::Local
426        } else {
427            ListenMode::Public
428        };
429        build_listen_addrs(self.port, self.ipv6, mode)
430    }
431
432    /// Create a new NodeConfig with default values
433    ///
434    /// # Errors
435    ///
436    /// Returns an error if default addresses cannot be parsed
437    pub fn new() -> Result<Self> {
438        Ok(Self::default())
439    }
440
441    /// Create a builder for customized NodeConfig construction
442    pub fn builder() -> NodeConfigBuilder {
443        NodeConfigBuilder::default()
444    }
445}
446
447// ============================================================================
448// NodeConfig Builder Pattern
449// ============================================================================
450
451/// Builder for constructing [`NodeConfig`] with a transport-aware fluent API.
452///
453/// Defaults are chosen for quick local development:
454/// - QUIC on a random free port (`0`)
455/// - IPv6 enabled (dual-stack)
456/// - All interfaces (not local-only)
457///
458/// # Examples
459///
460/// ```rust,ignore
461/// // Simplest — QUIC on random port, IPv6 on, all interfaces
462/// let config = NodeConfig::builder().build()?;
463///
464/// // Local dev/test mode (loopback, auto-enables allow_loopback)
465/// let config = NodeConfig::builder()
466///     .local(true)
467///     .build()?;
468/// ```
469#[derive(Debug, Clone)]
470pub struct NodeConfigBuilder {
471    port: u16,
472    ipv6: bool,
473    local: bool,
474    bootstrap_peers: Vec<crate::MultiAddr>,
475    max_connections: Option<usize>,
476    connection_timeout: Option<Duration>,
477    dht_config: Option<DHTConfig>,
478    max_message_size: Option<usize>,
479    mode: NodeMode,
480    custom_user_agent: Option<String>,
481    allow_loopback: Option<bool>,
482    adaptive_dht_config: Option<AdaptiveDhtConfig>,
483    close_group_cache_dir: Option<PathBuf>,
484    /// Outer `None` means the builder setter was not called; inner `None`
485    /// explicitly disables age enforcement.
486    close_group_cache_max_age: Option<Option<Duration>>,
487}
488
489impl Default for NodeConfigBuilder {
490    fn default() -> Self {
491        Self {
492            port: 0,
493            ipv6: true,
494            local: false,
495            bootstrap_peers: Vec::new(),
496            max_connections: None,
497            connection_timeout: None,
498            dht_config: None,
499            max_message_size: None,
500            mode: NodeMode::default(),
501            custom_user_agent: None,
502            allow_loopback: None,
503            adaptive_dht_config: None,
504            close_group_cache_dir: None,
505            close_group_cache_max_age: None,
506        }
507    }
508}
509
510impl NodeConfigBuilder {
511    /// Set the listen port. Default: `0` (random free port).
512    pub fn port(mut self, port: u16) -> Self {
513        self.port = port;
514        self
515    }
516
517    /// Enable or disable IPv6 dual-stack. Default: `true`.
518    pub fn ipv6(mut self, enabled: bool) -> Self {
519        self.ipv6 = enabled;
520        self
521    }
522
523    /// Bind to loopback only (`true`) or all interfaces (`false`).
524    ///
525    /// When `true`, automatically enables `allow_loopback` unless explicitly
526    /// overridden via [`Self::allow_loopback`].
527    ///
528    /// Default: `false` (all interfaces).
529    pub fn local(mut self, local: bool) -> Self {
530        self.local = local;
531        self
532    }
533
534    /// Add a bootstrap peer.
535    pub fn bootstrap_peer(mut self, addr: crate::MultiAddr) -> Self {
536        self.bootstrap_peers.push(addr);
537        self
538    }
539
540    /// Set maximum connections.
541    pub fn max_connections(mut self, max: usize) -> Self {
542        self.max_connections = Some(max);
543        self
544    }
545
546    /// Set connection timeout.
547    pub fn connection_timeout(mut self, timeout: Duration) -> Self {
548        self.connection_timeout = Some(timeout);
549        self
550    }
551
552    /// Set DHT configuration.
553    pub fn dht_config(mut self, config: DHTConfig) -> Self {
554        self.dht_config = Some(config);
555        self
556    }
557
558    /// Set maximum application-layer message size in bytes.
559    ///
560    /// If this method is not called, saorsa-transport's built-in default is used.
561    pub fn max_message_size(mut self, max_message_size: usize) -> Self {
562        self.max_message_size = Some(max_message_size);
563        self
564    }
565
566    /// Set the operating mode (Node or Client).
567    pub fn mode(mut self, mode: NodeMode) -> Self {
568        self.mode = mode;
569        self
570    }
571
572    /// Set a custom user agent string, overriding the mode-derived default.
573    pub fn custom_user_agent(mut self, user_agent: impl Into<String>) -> Self {
574        self.custom_user_agent = Some(user_agent.into());
575        self
576    }
577
578    /// Explicitly control whether loopback addresses are allowed in the
579    /// transport layer. When not called, `local(true)` auto-enables this;
580    /// `local(false)` defaults to `false`.
581    pub fn allow_loopback(mut self, allow: bool) -> Self {
582        self.allow_loopback = Some(allow);
583        self
584    }
585
586    /// Enable or disable trust-based peer swap-out.
587    ///
588    /// When `false`, peers are never swapped out of the routing table
589    /// based on trust scores. Trust scores are still tracked but have
590    /// no enforcement effect.
591    ///
592    /// When `true` (the default), peers whose trust score falls below the
593    /// swap threshold (0.35) become eligible for replacement when a
594    /// better candidate arrives.
595    ///
596    /// For fine-grained control over the threshold, use
597    /// [`adaptive_dht_config`](Self::adaptive_dht_config) instead.
598    pub fn trust_enforcement(mut self, enabled: bool) -> Self {
599        let threshold = if enabled {
600            AdaptiveDhtConfig::default().swap_threshold
601        } else {
602            0.0
603        };
604        self.adaptive_dht_config = Some(AdaptiveDhtConfig {
605            swap_threshold: threshold,
606        });
607        self
608    }
609
610    /// Set the full adaptive DHT configuration.
611    ///
612    /// Overrides any previous call to [`trust_enforcement`](Self::trust_enforcement).
613    pub fn adaptive_dht_config(mut self, config: AdaptiveDhtConfig) -> Self {
614        self.adaptive_dht_config = Some(config);
615        self
616    }
617
618    /// Set the directory for persisting the close group cache.
619    ///
620    /// The node writes `close_group_cache.json` inside this directory on
621    /// shutdown and after bootstrap, and loads it on startup.
622    pub fn close_group_cache_dir(mut self, path: impl Into<PathBuf>) -> Self {
623        self.close_group_cache_dir = Some(path.into());
624        self
625    }
626
627    /// Set the maximum age for using a close-group cache as Priority-0
628    /// bootstrap material. `None` disables the age check.
629    pub fn close_group_cache_max_age(mut self, max_age: Option<Duration>) -> Self {
630        self.close_group_cache_max_age = Some(max_age);
631        self
632    }
633
634    /// Build the [`NodeConfig`].
635    ///
636    /// # Errors
637    ///
638    /// Returns an error if address construction fails.
639    pub fn build(self) -> Result<NodeConfig> {
640        // local mode auto-enables allow_loopback unless explicitly overridden
641        let allow_loopback = self.allow_loopback.unwrap_or(self.local);
642
643        Ok(NodeConfig {
644            local: self.local,
645            port: self.port,
646            ipv6: self.ipv6,
647            bootstrap_peers: self.bootstrap_peers,
648            connection_timeout: self
649                .connection_timeout
650                .unwrap_or(Duration::from_secs(DEFAULT_CONNECTION_TIMEOUT_SECS)),
651            max_connections: self.max_connections.unwrap_or(DEFAULT_MAX_CONNECTIONS),
652            dht_config: self.dht_config.unwrap_or_default(),
653            diversity_config: None,
654            max_message_size: self.max_message_size,
655            node_identity: None,
656            mode: self.mode,
657            custom_user_agent: self.custom_user_agent,
658            allow_loopback,
659            adaptive_dht_config: self.adaptive_dht_config.unwrap_or_default(),
660            close_group_cache_dir: self.close_group_cache_dir,
661            close_group_cache_max_age: self
662                .close_group_cache_max_age
663                .unwrap_or_else(default_close_group_cache_max_age),
664        })
665    }
666}
667
668impl Default for NodeConfig {
669    fn default() -> Self {
670        Self {
671            local: false,
672            port: DEFAULT_LISTEN_PORT,
673            ipv6: true,
674            bootstrap_peers: Vec::new(),
675            connection_timeout: Duration::from_secs(DEFAULT_CONNECTION_TIMEOUT_SECS),
676            max_connections: DEFAULT_MAX_CONNECTIONS,
677            dht_config: DHTConfig::default(),
678            diversity_config: None,
679            max_message_size: None,
680            node_identity: None,
681            mode: NodeMode::default(),
682            custom_user_agent: None,
683            allow_loopback: false,
684            adaptive_dht_config: AdaptiveDhtConfig::default(),
685            close_group_cache_dir: None,
686            close_group_cache_max_age: default_close_group_cache_max_age(),
687        }
688    }
689}
690
691impl DHTConfig {
692    /// Default K value (bucket size) for Kademlia routing.
693    pub const DEFAULT_K_VALUE: usize = crate::dht_lookup::DEFAULT_K_VALUE;
694    const DEFAULT_ALPHA_VALUE: usize = crate::dht_lookup::DEFAULT_ALPHA_VALUE;
695    const DEFAULT_REFRESH_INTERVAL_SECS: u64 = 600;
696    /// Minimum k_value — values below this produce degenerate routing behavior.
697    const MIN_K_VALUE: usize = 4;
698
699    /// Validate parameter safety constraints (Section 4 points 1-13).
700    ///
701    /// Returns `Err` if any constraint is violated.
702    pub fn validate(&self) -> Result<()> {
703        if self.k_value < Self::MIN_K_VALUE {
704            return Err(P2PError::Validation(
705                format!(
706                    "k_value must be >= {} (got {}), values below {} produce degenerate behavior",
707                    Self::MIN_K_VALUE,
708                    self.k_value,
709                    Self::MIN_K_VALUE,
710                )
711                .into(),
712            ));
713        }
714        if self.alpha_value < 1 {
715            return Err(P2PError::Validation(
716                format!("alpha_value must be >= 1 (got {})", self.alpha_value).into(),
717            ));
718        }
719        if self.refresh_interval.is_zero() {
720            return Err(P2PError::Validation("refresh_interval must be > 0".into()));
721        }
722        Ok(())
723    }
724}
725
726impl Default for DHTConfig {
727    fn default() -> Self {
728        Self {
729            k_value: Self::DEFAULT_K_VALUE,
730            alpha_value: Self::DEFAULT_ALPHA_VALUE,
731            refresh_interval: Duration::from_secs(Self::DEFAULT_REFRESH_INTERVAL_SECS),
732        }
733    }
734}
735
736/// Information about a connected peer
737#[derive(Debug, Clone)]
738pub struct PeerInfo {
739    /// Transport-level channel identifier (internal use only).
740    #[allow(dead_code)]
741    pub(crate) channel_id: String,
742
743    /// Peer's addresses
744    pub addresses: Vec<MultiAddr>,
745
746    /// Connection timestamp
747    pub connected_at: Instant,
748
749    /// Last seen timestamp
750    pub last_seen: Instant,
751
752    /// Connection status
753    pub status: ConnectionStatus,
754
755    /// Supported protocols
756    pub protocols: Vec<String>,
757
758    /// Number of heartbeats received
759    pub heartbeat_count: u64,
760}
761
762/// Connection status for a peer
763#[derive(Debug, Clone, PartialEq)]
764pub enum ConnectionStatus {
765    /// Connection is being established
766    Connecting,
767    /// Connection is established and active
768    Connected,
769    /// Connection is being closed
770    Disconnecting,
771    /// Connection is closed
772    Disconnected,
773    /// Connection failed
774    Failed(String),
775}
776
777/// Coarse classification of the route by which a peer's traffic reached us.
778///
779/// Derived by matching the transport source address of an incoming message
780/// against the typed dial addresses the DHT phonebook holds for that peer.
781/// See [`P2PNode::classify_peer_transport_route`].
782///
783/// This is a **provenance label** for logging and metrics. It is not an
784/// identity signal and must not be used for trust scoring or routing
785/// decisions.
786#[derive(Debug, Clone, Copy, PartialEq, Eq)]
787pub enum PeerRouteKind {
788    /// Traffic arrived via the peer's MASQUE relay address.
789    Relay,
790    /// Traffic arrived via the peer's direct public IP address.
791    Direct,
792    /// Traffic arrived via a LAN or other local-scope address.
793    Lan,
794    /// Traffic arrived via an address whose reachability is unverified.
795    Unverified,
796    /// The transport source is absent, non-IP, or does not match any address
797    /// the local phonebook holds for the peer.
798    Unknown,
799}
800
801impl PeerRouteKind {
802    /// Canonical lowercase label for the variant, used by the [`Display`]
803    /// implementation and suitable for log fields / metric tags.
804    #[must_use]
805    pub const fn as_str(self) -> &'static str {
806        match self {
807            Self::Relay => "relay",
808            Self::Direct => "direct",
809            Self::Lan => "lan",
810            Self::Unverified => "unverified",
811            Self::Unknown => "unknown",
812        }
813    }
814}
815
816impl std::fmt::Display for PeerRouteKind {
817    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
818        f.write_str(self.as_str())
819    }
820}
821
822impl From<AddressType> for PeerRouteKind {
823    fn from(ty: AddressType) -> Self {
824        match ty {
825            AddressType::Relay => Self::Relay,
826            AddressType::Direct => Self::Direct,
827            AddressType::Unverified => Self::Unverified,
828            AddressType::Lan => Self::Lan,
829        }
830    }
831}
832
833/// Pure classification of a transport source against a typed address slice.
834///
835/// This is the testable, I/O-free core of
836/// [`P2PNode::classify_peer_transport_route`]; it touches no node state and
837/// can be exercised without constructing a node.
838///
839/// # Classification rules
840/// - `transport_source` `None` or not an IP socket address →
841///   [`PeerRouteKind::Unknown`].
842/// - The transport source's socket address is compared by exact equality
843///   against each `(MultiAddr, AddressType)` entry's socket address
844///   ([`MultiAddr::socket_addr`]).
845/// - A single matching entry yields that entry's [`AddressType`] mapped to
846///   [`PeerRouteKind`].
847/// - Multiple matching entries that **agree** on the [`AddressType`] yield
848///   that type (deduplicated duplicates).
849/// - Multiple matching entries with **conflicting** [`AddressType`]s yield
850///   [`PeerRouteKind::Unknown`]. We deliberately do **not** use
851///   [`AddressType::priority`] to pick a winner here: that ordering governs
852///   *dial preference*, not the provenance of an already-arrived message,
853///   and silently promoting one type over another would mask a phonebook
854///   inconsistency. `Unknown` is the conservative, honest label.
855/// - No match → [`PeerRouteKind::Unknown`].
856///
857/// Address-list order is never used as a classification signal.
858#[must_use]
859pub(crate) fn classify_peer_route_from_typed(
860    transport_source: Option<&MultiAddr>,
861    typed_addrs: &[(MultiAddr, AddressType)],
862) -> PeerRouteKind {
863    let Some(src) = transport_source else {
864        return PeerRouteKind::Unknown;
865    };
866    let Some(src_sa) = src.socket_addr() else {
867        return PeerRouteKind::Unknown;
868    };
869
870    // Walk all entries; remember the first matched type and flag a conflict
871    // if a later match disagrees. We do not short-circuit on first match
872    // because duplicates with conflicting types must surface as Unknown.
873    let mut matched: Option<AddressType> = None;
874    let mut conflict = false;
875    for (addr, ty) in typed_addrs {
876        if addr.socket_addr().is_some_and(|sa| sa == src_sa) {
877            match matched {
878                None => matched = Some(*ty),
879                Some(prev) if prev == *ty => {}
880                Some(_) => {
881                    conflict = true;
882                    break;
883                }
884            }
885        }
886    }
887
888    match (matched, conflict) {
889        (Some(ty), false) => ty.into(),
890        _ => PeerRouteKind::Unknown,
891    }
892}
893
894/// Network events that can occur in the P2P system
895///
896/// Events are broadcast to all listeners and provide real-time
897/// notifications of network state changes and message arrivals.
898#[derive(Debug, Clone)]
899pub enum P2PEvent {
900    /// Message received from a peer on a specific topic
901    Message {
902        /// Topic or channel the message was sent on
903        topic: String,
904        /// For signed messages this is the authenticated app-level [`PeerId`];
905        /// `None` for unsigned messages.
906        source: Option<PeerId>,
907        /// IP transport address that delivered this message, when known.
908        ///
909        /// This is provenance metadata, not an identity signal.
910        transport_source: Option<MultiAddr>,
911        /// Sender-supplied Unix timestamp in seconds.
912        ///
913        /// For signed messages this value is covered by the ML-DSA-65 signature
914        /// alongside the payload, so handlers can use it for application-level
915        /// freshness or replay defense. Wire-level acceptance no longer gates
916        /// on this value; subscribers MUST do their own age/dedup checks when
917        /// the protocol requires them.
918        timestamp: u64,
919        /// Raw message data payload
920        data: Vec<u8>,
921    },
922    /// An authenticated peer has connected (first signed message verified on any channel).
923    /// The `user_agent` identifies the remote software (e.g. `"node/0.12.1"`, `"client/1.0"`).
924    PeerConnected(PeerId, String),
925    /// An authenticated peer has fully disconnected (all channels closed).
926    PeerDisconnected(PeerId),
927}
928
929/// Response from a peer to a request sent via [`P2PNode::send_request`].
930///
931/// Contains the response payload along with metadata about the responder
932/// and round-trip latency.
933#[derive(Debug, Clone)]
934pub struct PeerResponse {
935    /// The peer that sent the response.
936    pub peer_id: PeerId,
937    /// Raw response payload bytes.
938    pub data: Vec<u8>,
939    /// Round-trip latency from request to response.
940    pub latency: Duration,
941}
942
943/// Wire format for request/response correlation.
944///
945/// Wraps application payloads with a message ID and direction flag
946/// so the receive loop can route responses back to waiting callers.
947#[derive(Debug, Clone, Serialize, Deserialize)]
948pub(crate) struct RequestResponseEnvelope {
949    /// Unique identifier to correlate request ↔ response.
950    pub(crate) message_id: String,
951    /// `false` for requests, `true` for responses.
952    pub(crate) is_response: bool,
953    /// Application payload.
954    pub(crate) payload: Vec<u8>,
955}
956
957/// An in-flight request awaiting a response from a specific peer.
958pub(crate) struct PendingRequest {
959    /// Oneshot sender for delivering the response payload.
960    pub(crate) response_tx: tokio::sync::oneshot::Sender<Vec<u8>>,
961    /// The peer we expect the response from (for origin validation).
962    pub(crate) expected_peer: PeerId,
963}
964
965/// Short grace period after closing stale QUIC connections before re-dialing.
966///
967/// `disconnect_channel` is async and waits for the QUIC close, but the
968/// transport endpoint may need a moment to fully release internal state.
969/// Only applied when stale channels were actually disconnected.
970const QUIC_TEARDOWN_GRACE: Duration = Duration::from_millis(100);
971
972/// Main P2P network node that manages connections, routing, and communication
973///
974/// This struct represents a complete P2P network participant that can:
975/// - Connect to other peers via QUIC transport
976/// - Participate in distributed hash table (DHT) operations
977/// - Send and receive messages through various protocols
978/// - Handle network events and peer lifecycle
979///
980/// Transport concerns (connections, messaging, events) are delegated to
981/// `TransportHandle`.
982pub struct P2PNode {
983    /// Node configuration
984    config: NodeConfig,
985
986    /// Our peer ID
987    peer_id: PeerId,
988
989    /// Transport handle owning all QUIC / peer / event state
990    transport: Arc<crate::transport_handle::TransportHandle>,
991
992    /// Node start time
993    start_time: Instant,
994
995    /// Shutdown token — cancelled when the node should stop
996    shutdown: CancellationToken,
997
998    /// Dedicated cancellation token for periodic close-group-cache saves.
999    /// Cancelled and joined before the authoritative shutdown snapshot.
1000    close_group_cache_save_shutdown: CancellationToken,
1001    /// Whether the routing snapshot has already seeded a bootstrap in this
1002    /// process, so a re-bootstrap does not replay it.
1003    routing_snapshot_restored: AtomicBool,
1004    /// How many peers the routing snapshot restored at startup, zero when there
1005    /// was none to restore, and [`SNAPSHOT_FLOOR_UNRESOLVED`] until bootstrap
1006    /// has decided. A save must not write a table smaller than this while the
1007    /// floor still applies. Shared with the periodic save task.
1008    routing_snapshot_floor: Arc<AtomicUsize>,
1009    /// When that floor was decided, in epoch seconds. Zero while unresolved.
1010    routing_snapshot_floor_at: Arc<AtomicU64>,
1011
1012    /// Periodic close-group-cache task, retained so shutdown can prevent a
1013    /// late periodic write from replacing the final snapshot.
1014    close_group_cache_save_handle: TokioMutex<Option<tokio::task::JoinHandle<()>>>,
1015
1016    /// Adaptive DHT layer — owns both the DHT manager and the trust engine.
1017    /// All DHT operations and trust signals go through this component.
1018    adaptive_dht: AdaptiveDHT,
1019
1020    /// Bootstrap state tracking - indicates whether peer discovery has completed
1021    is_bootstrapped: Arc<AtomicBool>,
1022
1023    /// Whether `start()` has been called (and `stop()` has not yet completed)
1024    is_started: Arc<AtomicBool>,
1025
1026    /// Per-peer locks that serialise reconnect attempts so concurrent sends
1027    /// to the same stale peer don't race to dial.  Entries accumulate over
1028    /// the node's lifetime; each is a lightweight `Arc<TokioMutex<()>>`.
1029    reconnect_locks: ParkingMutex<HashMap<PeerId, Arc<TokioMutex<()>>>>,
1030
1031    /// The peer ID of the node currently relaying traffic for us (ADR-014).
1032    ///
1033    /// Set after the reachability classifier acquires a relay in `start()`.
1034    /// The relayer monitor watches this against the K-closest set: if the
1035    /// relayer drops out, it triggers rebinding.
1036    ///
1037    /// `None` when the node is publicly reachable (no relay needed) or
1038    /// before classification has run.
1039    relayer_peer_id: Arc<RwLock<Option<PeerId>>>,
1040
1041    /// The relay-allocated public address (ADR-014).
1042    ///
1043    /// Set after a proactive MASQUE relay is acquired in `start()`. This is
1044    /// the address that external peers must dial to reach this node through
1045    /// the relay. `None` when the node is publicly reachable (no relay) or
1046    /// before classification has run.
1047    relay_address: Arc<RwLock<Option<SocketAddr>>>,
1048}
1049
1050/// Normalize wildcard bind addresses to localhost loopback addresses
1051///
1052/// saorsa-transport correctly rejects "unspecified" addresses (0.0.0.0 and [::]) for remote connections
1053/// because you cannot connect TO an unspecified address - these are only valid for BINDING.
1054///
1055/// This function converts wildcard addresses to appropriate loopback addresses for local connections:
1056/// - IPv6 [::]:port → ::1:port (IPv6 loopback)
1057/// - IPv4 0.0.0.0:port → 127.0.0.1:port (IPv4 loopback)
1058/// - All other addresses pass through unchanged
1059///
1060/// # Arguments
1061/// * `addr` - The SocketAddr to normalize
1062///
1063/// # Returns
1064/// * Normalized SocketAddr suitable for remote connections
1065pub(crate) fn normalize_wildcard_to_loopback(addr: std::net::SocketAddr) -> std::net::SocketAddr {
1066    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
1067
1068    if addr.ip().is_unspecified() {
1069        // Convert unspecified addresses to loopback
1070        let loopback_ip = match addr {
1071            std::net::SocketAddr::V6(_) => IpAddr::V6(Ipv6Addr::LOCALHOST), // ::1
1072            std::net::SocketAddr::V4(_) => IpAddr::V4(Ipv4Addr::LOCALHOST), // 127.0.0.1
1073        };
1074        std::net::SocketAddr::new(loopback_ip, addr.port())
1075    } else {
1076        // Not a wildcard address, pass through unchanged
1077        addr
1078    }
1079}
1080
1081impl P2PNode {
1082    /// Create a new P2P node with the given configuration
1083    pub async fn new(config: NodeConfig) -> Result<Self> {
1084        // Ensure a cryptographic identity exists — generate one if not provided.
1085        let node_identity = match config.node_identity.clone() {
1086            Some(identity) => identity,
1087            None => Arc::new(NodeIdentity::generate()?),
1088        };
1089
1090        // Derive the canonical peer ID from the cryptographic identity.
1091        let peer_id = *node_identity.peer_id();
1092
1093        // Validate parameter safety constraints (Section 4 points 1-13).
1094        // Reject invalid config early, before any resources are allocated.
1095        config.dht_config.validate()?;
1096        if let Some(ref diversity) = config.diversity_config {
1097            diversity
1098                .validate()
1099                .map_err(|e| P2PError::Validation(format!("IP diversity config: {e}").into()))?;
1100        }
1101
1102        // Build transport handle with all transport-level concerns
1103        let transport_config = crate::transport_handle::TransportConfig::from_node_config(
1104            &config,
1105            crate::DEFAULT_EVENT_CHANNEL_CAPACITY,
1106            node_identity.clone(),
1107        );
1108        let transport =
1109            Arc::new(crate::transport_handle::TransportHandle::new(transport_config).await?);
1110
1111        // Initialize AdaptiveDHT — creates the trust engine and DHT manager
1112        let dht_manager_config = DhtNetworkConfig {
1113            peer_id,
1114            node_config: config.clone(),
1115            request_timeout: config.connection_timeout,
1116            max_concurrent_operations: MAX_ACTIVE_REQUESTS,
1117            enable_security: true,
1118            swap_threshold: 0.0, // Set by AdaptiveDHT::new() from AdaptiveDhtConfig
1119        };
1120        let adaptive_dht = AdaptiveDHT::new(
1121            transport.clone(),
1122            dht_manager_config,
1123            config.adaptive_dht_config.clone(),
1124        )
1125        .await?;
1126
1127        let node = Self {
1128            config,
1129            peer_id,
1130            transport,
1131            start_time: Instant::now(),
1132            shutdown: CancellationToken::new(),
1133            close_group_cache_save_shutdown: CancellationToken::new(),
1134            routing_snapshot_restored: AtomicBool::new(false),
1135            routing_snapshot_floor: Arc::new(AtomicUsize::new(SNAPSHOT_FLOOR_UNRESOLVED)),
1136            routing_snapshot_floor_at: Arc::new(AtomicU64::new(0)),
1137            close_group_cache_save_handle: TokioMutex::new(None),
1138            adaptive_dht,
1139            is_bootstrapped: Arc::new(AtomicBool::new(false)),
1140            is_started: Arc::new(AtomicBool::new(false)),
1141            reconnect_locks: ParkingMutex::new(HashMap::new()),
1142            relayer_peer_id: Arc::new(RwLock::new(None)),
1143            relay_address: Arc::new(RwLock::new(None)),
1144        };
1145        info!(
1146            "Created P2P node with peer ID: {} (call start() to begin networking)",
1147            node.peer_id
1148        );
1149
1150        Ok(node)
1151    }
1152
1153    /// Get the peer ID of this node.
1154    pub fn peer_id(&self) -> &PeerId {
1155        &self.peer_id
1156    }
1157
1158    /// Get the transport handle for sharing with other components.
1159    pub fn transport(&self) -> &Arc<crate::transport_handle::TransportHandle> {
1160        &self.transport
1161    }
1162
1163    /// The relay-allocated public address, if this node acquired a MASQUE relay.
1164    ///
1165    /// Returns `Some(addr)` when the node is behind NAT and successfully
1166    /// acquired a proactive relay during `start()`. External peers must dial
1167    /// this address to reach the node through the relay. Returns `None` when
1168    /// the node is publicly reachable or no relay was established.
1169    pub async fn relay_address(&self) -> Option<SocketAddr> {
1170        *self.relay_address.read().await
1171    }
1172
1173    pub fn local_addr(&self) -> Option<MultiAddr> {
1174        self.transport.local_addr()
1175    }
1176
1177    /// Check if the node has completed the initial bootstrap process
1178    ///
1179    /// Returns `true` if the node has successfully connected to at least one
1180    /// bootstrap peer and performed peer discovery (FIND_NODE).
1181    pub fn is_bootstrapped(&self) -> bool {
1182        self.is_bootstrapped.load(Ordering::SeqCst)
1183    }
1184
1185    /// Manually trigger re-bootstrap (useful for recovery or network rejoin)
1186    ///
1187    /// This clears the bootstrapped state and attempts to reconnect to
1188    /// bootstrap peers and discover new peers.
1189    pub async fn re_bootstrap(&self) -> Result<()> {
1190        self.is_bootstrapped.store(false, Ordering::SeqCst);
1191        self.connect_bootstrap_peers(None).await
1192    }
1193
1194    // =========================================================================
1195    // Trust API — delegates to AdaptiveDHT
1196    // =========================================================================
1197
1198    /// Get the trust engine for advanced use cases
1199    pub fn trust_engine(&self) -> Arc<TrustEngine> {
1200        self.adaptive_dht.trust_engine().clone()
1201    }
1202
1203    /// Report a trust event for a peer.
1204    ///
1205    /// Core only records penalties (connection failures). Positive trust
1206    /// signals are the consumer's responsibility via [`TrustEvent::ApplicationSuccess`].
1207    ///
1208    /// # Example
1209    ///
1210    /// ```rust,ignore
1211    /// use saorsa_core::adaptive::TrustEvent;
1212    ///
1213    /// node.report_trust_event(&peer_id, TrustEvent::ApplicationSuccess(1.0)).await;
1214    /// node.report_trust_event(&peer_id, TrustEvent::ConnectionFailed).await;
1215    /// ```
1216    pub async fn report_trust_event(&self, peer_id: &PeerId, event: TrustEvent) {
1217        self.adaptive_dht.report_trust_event(peer_id, event).await;
1218    }
1219
1220    /// Get the current trust score for a peer (0.0 to 1.0).
1221    ///
1222    /// Returns 0.5 (neutral) for unknown peers.
1223    pub fn peer_trust(&self, peer_id: &PeerId) -> f64 {
1224        self.adaptive_dht.peer_trust(peer_id)
1225    }
1226
1227    /// Get the AdaptiveDHT component for direct access
1228    pub fn adaptive_dht(&self) -> &AdaptiveDHT {
1229        &self.adaptive_dht
1230    }
1231
1232    /// Classify the route by which a peer's traffic reached this node.
1233    ///
1234    /// Looks up the typed dial addresses the DHT phonebook holds for `peer_id`
1235    /// and matches `transport_source` against them using the internal pure
1236    /// classifier. `transport_source` must be the address observed by the
1237    /// authenticated transport while delivering the message (currently a
1238    /// QUIC address), not an address asserted by the remote peer. Returns
1239    /// [`PeerRouteKind::Unknown`] when the source is absent, non-IP, or
1240    /// matches no known address.
1241    ///
1242    /// This is a **provenance label** for logging and metrics — not an
1243    /// identity or trust signal. It never infers a route from address-list
1244    /// ordering, and a conflicting phonebook (same socket address tagged
1245    /// with two different [`AddressType`]s) is reported as
1246    /// [`PeerRouteKind::Unknown`] rather than silently picking a winner.
1247    pub async fn classify_peer_transport_route(
1248        &self,
1249        peer_id: &PeerId,
1250        transport_source: Option<&MultiAddr>,
1251    ) -> PeerRouteKind {
1252        let typed = self
1253            .adaptive_dht
1254            .peer_addresses_for_dial_typed(peer_id)
1255            .await;
1256        classify_peer_route_from_typed(transport_source, &typed)
1257    }
1258
1259    /// Return this process's local last-successful-DHT-interaction age for a
1260    /// peer. This is a diagnostic observation, not proof of remote uptime.
1261    pub async fn peer_last_seen_elapsed(&self, peer_id: &PeerId) -> Option<Duration> {
1262        self.adaptive_dht
1263            .dht_manager()
1264            .peer_last_seen_elapsed(peer_id)
1265            .await
1266    }
1267
1268    // =========================================================================
1269    // Request/Response API — Automatic Trust Feedback
1270    // =========================================================================
1271
1272    /// Send a request to a peer and wait for a response with automatic trust penalty reporting.
1273    ///
1274    /// Unlike fire-and-forget `send_message()`, this method:
1275    /// 1. Wraps the payload in a `RequestResponseEnvelope` with a unique message ID
1276    /// 2. Sends it on the `/rr/<protocol>` protocol prefix
1277    /// 3. Waits for a matching response (or timeout)
1278    /// 4. Automatically reports failure to the trust engine (success is the expected baseline)
1279    ///
1280    /// The remote peer's handler should call `send_response()` with the
1281    /// incoming message ID to route the response back.
1282    ///
1283    /// # Arguments
1284    ///
1285    /// * `peer_id` - Target peer
1286    /// * `protocol` - Application protocol name (e.g. `"peer_info"`)
1287    /// * `data` - Request payload bytes
1288    /// * `timeout` - Maximum time to wait for a response
1289    ///
1290    /// # Returns
1291    ///
1292    /// A `PeerResponse` on success, or an error on timeout / connection failure.
1293    ///
1294    /// # Example
1295    ///
1296    /// ```rust,ignore
1297    /// let response = node.send_request(&peer_id, "peer_info", request_data, Duration::from_secs(10)).await?;
1298    /// println!("Got {} bytes from {}", response.data.len(), response.peer_id);
1299    /// ```
1300    pub async fn send_request(
1301        &self,
1302        peer_id: &PeerId,
1303        protocol: &str,
1304        data: Vec<u8>,
1305        timeout: Duration,
1306    ) -> Result<PeerResponse> {
1307        let result = self
1308            .send_request_reconnecting(peer_id, protocol, data, timeout)
1309            .await;
1310        if let Err(ref e) = result {
1311            let event = if matches!(e, P2PError::Timeout(_)) {
1312                TrustEvent::ConnectionTimeout
1313            } else {
1314                TrustEvent::ConnectionFailed
1315            };
1316            self.report_trust_event(peer_id, event).await;
1317        }
1318        result
1319    }
1320
1321    /// Request/response send with reconnect-on-demand.
1322    ///
1323    /// Mirrors [`Self::send_message`]: when there is no live channel to
1324    /// `peer_id` it dials one (serialised per peer via
1325    /// [`Self::reconnect_lock_for`]) before sending, and when an existing
1326    /// channel turns out to be stale it tears it down, reconnects, and retries
1327    /// the request exactly once. The plain transport `send_request` only sends
1328    /// over a pre-existing channel and fails fast with `PeerNotFound`
1329    /// otherwise; routing request/response through this reconnecting path means
1330    /// a request to a peer whose QUIC connection has dropped (e.g. a periodic
1331    /// audit of a close peer that idled out) re-establishes the connection
1332    /// instead of surfacing as a spurious timeout.
1333    ///
1334    /// `timeout` bounds only the response wait inside the transport; the dial
1335    /// is independently bounded by `connect_peer_typed` plus
1336    /// [`crate::dht_network_manager::IDENTITY_EXCHANGE_TIMEOUT`].
1337    async fn send_request_reconnecting(
1338        &self,
1339        peer_id: &PeerId,
1340        protocol: &str,
1341        data: Vec<u8>,
1342        timeout: Duration,
1343    ) -> Result<PeerResponse> {
1344        // Snapshot channel IDs before the send attempt — transport.send_request
1345        // prunes dead channels from bookkeeping but does NOT close the
1346        // underlying QUIC connection. We need the original IDs for
1347        // disconnect_channel later.
1348        let existing_channels = self.transport.channels_for_peer(peer_id).await;
1349
1350        // No live channel — serialise dials so concurrent requests to the same
1351        // unconnected peer don't each open their own QUIC connection.
1352        if existing_channels.is_empty() {
1353            // Hold the per-peer reconnect lock only across the dial so
1354            // concurrent requests to the same cold peer collapse onto one dial —
1355            // not across the response wait, which would serialise every such
1356            // request for the full `timeout`.
1357            {
1358                let lock = self.reconnect_lock_for(peer_id);
1359                let _guard = lock.lock().await;
1360                // Another caller may have connected while we waited for the lock.
1361                if !self.transport.is_peer_connected(peer_id).await {
1362                    self.ensure_channel(peer_id, &[], &[], &[]).await?;
1363                }
1364            }
1365            return self
1366                .transport
1367                .send_request(peer_id, protocol, data, timeout)
1368                .await;
1369        }
1370
1371        // Snapshot addresses before the attempt — transport.send_request prunes
1372        // stale channels, which removes peer_info.
1373        let saved_addrs: Vec<MultiAddr> = self
1374            .transport
1375            .peer_info(peer_id)
1376            .await
1377            .map(|info| info.addresses)
1378            .unwrap_or_default();
1379
1380        // Clone the payload for a possible retry — transport.send_request
1381        // consumes the Vec, and only stale-channel failures are retried.
1382        let retry_data = data.clone();
1383
1384        // Fast path: try the existing connection.
1385        match self
1386            .transport
1387            .send_request(peer_id, protocol, data, timeout)
1388            .await
1389        {
1390            Ok(resp) => return Ok(resp),
1391            Err(e) => {
1392                // A response-deadline timeout means the request WAS delivered
1393                // but went unanswered — reconnecting would not help, so do not
1394                // retry. Only a stale-channel send failure warrants a redial.
1395                if !e.is_stale_channel_send_failure() {
1396                    return Err(e);
1397                }
1398                debug!(
1399                    peer = %peer_id.to_hex(),
1400                    error = %e,
1401                    "stale channel request failed, attempting reconnect",
1402                );
1403            }
1404        }
1405
1406        // Serialise the reconnect (stale-channel teardown + dial) so concurrent
1407        // requests to the same stale peer don't race to dial, but release the
1408        // lock before the response wait so they don't serialise for the full
1409        // `timeout`.
1410        {
1411            let lock = self.reconnect_lock_for(peer_id);
1412            let _guard = lock.lock().await;
1413
1414            // Another caller may have reconnected while we waited for the lock.
1415            if self.transport.is_peer_connected(peer_id).await {
1416                // Close stale QUIC connections that transport.send_request's
1417                // bookkeeping cleanup didn't tear down (it only drops the mapping).
1418                for channel_id in &existing_channels {
1419                    self.transport.disconnect_channel(channel_id).await;
1420                }
1421            } else {
1422                self.ensure_channel(peer_id, &[], &saved_addrs, &existing_channels)
1423                    .await?;
1424            }
1425        }
1426        self.transport
1427            .send_request(peer_id, protocol, retry_data, timeout)
1428            .await
1429    }
1430
1431    pub async fn send_response(
1432        &self,
1433        peer_id: &PeerId,
1434        protocol: &str,
1435        message_id: &str,
1436        data: Vec<u8>,
1437    ) -> Result<()> {
1438        self.transport
1439            .send_response(peer_id, protocol, message_id, data)
1440            .await
1441    }
1442
1443    pub fn parse_request_envelope(data: &[u8]) -> Option<(String, bool, Vec<u8>)> {
1444        crate::transport_handle::TransportHandle::parse_request_envelope(data)
1445    }
1446
1447    pub async fn subscribe(&self, topic: &str) -> Result<()> {
1448        self.transport.subscribe(topic).await
1449    }
1450
1451    pub async fn publish(&self, topic: &str, data: &[u8]) -> Result<()> {
1452        self.transport.publish(topic, data).await
1453    }
1454
1455    /// Get the node configuration
1456    pub fn config(&self) -> &NodeConfig {
1457        &self.config
1458    }
1459
1460    /// Start the P2P node
1461    pub async fn start(&self) -> Result<()> {
1462        info!("Starting P2P node...");
1463
1464        // Start transport listeners and message receiving
1465        self.transport.start_network_listeners().await?;
1466
1467        // Start the adaptive DHT layer (DHT manager + trust engine)
1468        self.adaptive_dht.start().await?;
1469
1470        // Log current listen addresses
1471        let listen_addrs = self.transport.listen_addrs().await;
1472        info!("P2P node started on addresses: {:?}", listen_addrs);
1473
1474        // NOTE: Message receiving is now integrated into the accept loop in start_network_listeners()
1475        // The old start_message_receiving_system() is no longer needed as it competed with the accept
1476        // loop for incoming connections, causing messages to be lost.
1477
1478        // Load close group cache and import trust scores before connecting to peers.
1479        // This ensures trust scores are available when peers are added to the routing table.
1480        let close_group_cache = if let Some(ref dir) = self.config.close_group_cache_dir {
1481            match CloseGroupCache::load_from_dir(dir).await {
1482                Ok(Some(cache)) => {
1483                    let now_epoch = SystemTime::now()
1484                        .duration_since(UNIX_EPOCH)
1485                        .map_or(0, |duration| duration.as_secs());
1486                    if cache.is_stale(now_epoch, self.config.close_group_cache_max_age) {
1487                        warn!(
1488                            cache_age_secs = now_epoch.saturating_sub(cache.saved_at_epoch_secs),
1489                            max_age_secs = self
1490                                .config
1491                                .close_group_cache_max_age
1492                                .map(|age| age.as_secs()),
1493                            "Close group cache is stale; skipping cached trust and Priority-0 peers"
1494                        );
1495                        None
1496                    } else {
1497                        // Filter out peers with non-finite trust scores (NaN/Inf)
1498                        // that could corrupt trust engine state or sort ordering.
1499                        let original_count = cache.peers.len();
1500                        let cache = CloseGroupCache {
1501                            peers: cache
1502                                .peers
1503                                .into_iter()
1504                                .filter(|p| p.trust.score.is_finite())
1505                                .collect(),
1506                            ..cache
1507                        };
1508                        let filtered_count = original_count - cache.peers.len();
1509                        if filtered_count > 0 {
1510                            warn!(
1511                                "Filtered {filtered_count} peers with non-finite trust scores from close group cache"
1512                            );
1513                        }
1514
1515                        let trust_snapshot = TrustSnapshot {
1516                            peers: cache
1517                                .peers
1518                                .iter()
1519                                .map(|p| (p.peer_id, p.trust.clone()))
1520                                .collect(),
1521                        };
1522                        self.adaptive_dht
1523                            .trust_engine()
1524                            .import_snapshot(&trust_snapshot);
1525                        info!(
1526                            cache_age_secs = now_epoch.saturating_sub(cache.saved_at_epoch_secs),
1527                            saved_at_epoch_secs = cache.saved_at_epoch_secs,
1528                            "Loaded {} peers from close group cache (trust scores imported)",
1529                            cache.peers.len()
1530                        );
1531                        Some(cache)
1532                    }
1533                }
1534                Ok(None) => {
1535                    debug!(
1536                        "No close group cache found in {}, fresh start",
1537                        dir.display()
1538                    );
1539                    None
1540                }
1541                Err(e) => {
1542                    warn!(
1543                        "Failed to load close group cache from {}: {e}",
1544                        dir.display()
1545                    );
1546                    None
1547                }
1548            }
1549        } else {
1550            None
1551        };
1552
1553        // Connect to bootstrap peers
1554        self.connect_bootstrap_peers(close_group_cache.as_ref())
1555            .await?;
1556
1557        // Emit BootstrapComplete — the node is connected to the network and
1558        // the DHT routing table is populated; consumers waiting on this
1559        // event can start issuing queries. The relay-acquisition driver
1560        // runs asynchronously after this point, so the node's published
1561        // self-record may be direct-only for a brief window until the
1562        // driver's first acquisition attempt finishes.
1563        {
1564            let dht = self.adaptive_dht.dht_manager();
1565            let rt_size = dht.get_routing_table_size().await;
1566            dht.emit_event(DhtNetworkEvent::BootstrapComplete { num_peers: rt_size });
1567        }
1568
1569        // Spawn the relay-acquisition driver for Node mode.
1570        //
1571        // The driver unconditionally tries to acquire a MASQUE relay from
1572        // an XOR-closest peer right after bootstrap — there is no public/
1573        // private classification. Private candidates are filtered out
1574        // ambiently: their Direct addresses are unreachable from outside
1575        // their NAT, so the QUIC dial fails and the walker advances to
1576        // the next-closest peer.
1577        //
1578        // The driver also owns the relay-lost → republish → reacquire
1579        // state machine (see `reachability::driver` for the full flow).
1580        // Clients (`NodeMode::Client`) do not run the driver at all: they
1581        // are outbound-only and do not need to be reachable.
1582        if self.config.mode != NodeMode::Client {
1583            spawn_acquisition_driver(
1584                self.adaptive_dht.dht_manager().clone(),
1585                Arc::clone(&self.transport),
1586                Arc::clone(&self.relayer_peer_id),
1587                Arc::clone(&self.relay_address),
1588                self.shutdown.clone(),
1589            );
1590        } else {
1591            info!("client mode — skipping relay acquisition driver");
1592        }
1593
1594        if let Some(dir) = self.config.close_group_cache_dir.clone() {
1595            let interval = self
1596                .config
1597                .dht_config
1598                .refresh_interval
1599                .max(MIN_CLOSE_GROUP_CACHE_SAVE_INTERVAL);
1600            let mut task = self.close_group_cache_save_handle.lock().await;
1601            if task.is_none() {
1602                let dht_manager = Arc::clone(self.adaptive_dht.dht_manager());
1603                let trust_engine = Arc::clone(self.adaptive_dht.trust_engine());
1604                let peer_id = self.peer_id;
1605                let k_value = self.config.dht_config.k_value;
1606                let shutdown = self.close_group_cache_save_shutdown.clone();
1607                let snapshot_floor = Arc::clone(&self.routing_snapshot_floor);
1608                let snapshot_floor_at = Arc::clone(&self.routing_snapshot_floor_at);
1609                *task = Some(tokio::spawn(periodic_close_group_cache_save(
1610                    dht_manager,
1611                    trust_engine,
1612                    peer_id,
1613                    k_value,
1614                    dir,
1615                    interval,
1616                    snapshot_floor,
1617                    snapshot_floor_at,
1618                    shutdown,
1619                )));
1620                info!(
1621                    interval_secs = interval.as_secs(),
1622                    "Started periodic close group cache persistence"
1623                );
1624            }
1625        }
1626
1627        self.is_started
1628            .store(true, std::sync::atomic::Ordering::Release);
1629
1630        Ok(())
1631    }
1632
1633    // start_network_listeners and start_message_receiving_system
1634    // are now implemented in TransportHandle
1635
1636    /// Run the P2P node (blocks until shutdown)
1637    pub async fn run(&self) -> Result<()> {
1638        if !self.is_running() {
1639            self.start().await?;
1640        }
1641
1642        info!("P2P node running...");
1643
1644        // Block until shutdown is signalled. All background work (connection
1645        // lifecycle, DHT maintenance, EigenTrust) runs in dedicated tasks.
1646        self.shutdown.cancelled().await;
1647
1648        info!("P2P node stopped");
1649        Ok(())
1650    }
1651
1652    /// Stop the P2P node
1653    pub async fn stop(&self) -> Result<()> {
1654        info!("Stopping P2P node...");
1655
1656        // Stop periodic cache persistence and wait for any in-flight write.
1657        // The final save below is then the authoritative shutdown snapshot.
1658        self.close_group_cache_save_shutdown.cancel();
1659        let cache_task = self.close_group_cache_save_handle.lock().await.take();
1660        if let Some(cache_task) = cache_task
1661            && let Err(error) = cache_task.await
1662        {
1663            warn!("Periodic close group cache task failed during shutdown: {error}");
1664        }
1665
1666        // Save close group cache and routing snapshot before tearing down the
1667        // DHT and transport layers.
1668        if let Some(ref dir) = self.config.close_group_cache_dir {
1669            if let Err(e) = self.save_close_group_cache(dir, "shutdown").await {
1670                warn!("Failed to save close group cache on shutdown: {e}");
1671            }
1672            let now_epoch = SystemTime::now()
1673                .duration_since(UNIX_EPOCH)
1674                .map_or(0, |duration| duration.as_secs());
1675            save_routing_snapshot(
1676                self.dht_manager(),
1677                self.peer_id,
1678                now_epoch,
1679                dir,
1680                &self.routing_snapshot_floor,
1681                &self.routing_snapshot_floor_at,
1682                "shutdown",
1683            )
1684            .await;
1685        }
1686
1687        // Signal the run loop to exit
1688        self.shutdown.cancel();
1689
1690        // Stop DHT layer first so leave messages can be sent while transport is still active.
1691        self.adaptive_dht.stop().await?;
1692
1693        // Stop the transport layer (shutdown endpoints, join tasks, disconnect peers)
1694        self.transport.stop().await?;
1695
1696        self.is_started
1697            .store(false, std::sync::atomic::Ordering::Release);
1698
1699        info!("P2P node stopped");
1700        Ok(())
1701    }
1702
1703    /// Graceful shutdown alias for tests
1704    pub async fn shutdown(&self) -> Result<()> {
1705        self.stop().await
1706    }
1707
1708    /// Check if the node is running
1709    pub fn is_running(&self) -> bool {
1710        self.is_started.load(std::sync::atomic::Ordering::Acquire) && !self.shutdown.is_cancelled()
1711    }
1712
1713    /// Get the current listen addresses
1714    pub async fn listen_addrs(&self) -> Vec<MultiAddr> {
1715        self.transport.listen_addrs().await
1716    }
1717
1718    /// Get connected peers
1719    pub async fn connected_peers(&self) -> Vec<PeerId> {
1720        self.transport.connected_peers().await
1721    }
1722
1723    /// Get peer count
1724    pub async fn peer_count(&self) -> usize {
1725        self.transport.peer_count().await
1726    }
1727
1728    /// Get peer info
1729    pub async fn peer_info(&self, peer_id: &PeerId) -> Option<PeerInfo> {
1730        self.transport.peer_info(peer_id).await
1731    }
1732
1733    /// Get the channel ID for a given address, if connected (internal only).
1734    #[allow(dead_code)]
1735    pub(crate) async fn get_channel_id_by_address(&self, addr: &MultiAddr) -> Option<String> {
1736        self.transport.get_channel_id_by_address(addr).await
1737    }
1738
1739    /// List all active transport-level connections (internal only).
1740    #[allow(dead_code)]
1741    pub(crate) async fn list_active_connections(&self) -> Vec<(String, Vec<MultiAddr>)> {
1742        self.transport.list_active_connections().await
1743    }
1744
1745    /// Remove a channel from the peers map (internal only).
1746    #[allow(dead_code)]
1747    pub(crate) async fn remove_channel(&self, channel_id: &str) -> bool {
1748        self.transport.remove_channel(channel_id).await
1749    }
1750
1751    /// Close a channel's QUIC connection and remove it from all tracking maps.
1752    ///
1753    /// Use when a transport-level connection was established but identity
1754    /// exchange failed, so no [`PeerId`] is available for [`disconnect_peer`].
1755    pub(crate) async fn disconnect_channel(&self, channel_id: &str) {
1756        self.transport.disconnect_channel(channel_id).await;
1757    }
1758
1759    /// Check if an authenticated peer is connected (has at least one active channel).
1760    pub async fn is_peer_connected(&self, peer_id: &PeerId) -> bool {
1761        self.transport.is_peer_connected(peer_id).await
1762    }
1763
1764    /// Connect to a peer, returning the transport-level channel ID.
1765    ///
1766    /// The returned channel ID is **not** the app-level [`PeerId`]. To obtain
1767    /// the authenticated peer identity, call
1768    /// [`wait_for_peer_identity`](Self::wait_for_peer_identity) with the
1769    /// returned channel ID.
1770    ///
1771    /// Callers that already know how the address was classified should
1772    /// prefer [`Self::connect_peer_typed`] so the resulting log line
1773    /// carries an accurate `kind` field instead of `unknown`.
1774    pub async fn connect_peer(&self, address: &MultiAddr) -> Result<String> {
1775        self.transport.connect_peer(address).await
1776    }
1777
1778    /// Connect to a peer at the given typed address.
1779    ///
1780    /// Same as [`Self::connect_peer`] but threads the [`AddressType`]
1781    /// through to the transport-level dial log so an operator can tell,
1782    /// after the fact, whether a failed dial was against a `Direct`,
1783    /// `Relay`, `Unverified`, or `Lan` address.
1784    pub async fn connect_peer_typed(
1785        &self,
1786        address: &MultiAddr,
1787        kind: AddressType,
1788    ) -> Result<String> {
1789        self.transport.connect_peer_typed(address, kind).await
1790    }
1791
1792    /// Wait for the identity exchange on `channel_id` to complete, returning
1793    /// the authenticated [`PeerId`].
1794    ///
1795    /// Use this after [`connect_peer`](Self::connect_peer) to bridge the gap
1796    /// between the transport-level channel ID and the app-level peer identity
1797    /// required by [`send_message`](Self::send_message).
1798    pub async fn wait_for_peer_identity(
1799        &self,
1800        channel_id: &str,
1801        timeout: Duration,
1802    ) -> Result<PeerId> {
1803        self.transport
1804            .wait_for_peer_identity(channel_id, timeout)
1805            .await
1806    }
1807
1808    /// Disconnect from a peer
1809    pub async fn disconnect_peer(&self, peer_id: &PeerId) -> Result<()> {
1810        self.transport.disconnect_peer(peer_id).await
1811    }
1812
1813    /// Check if a connection to a peer is active (internal only).
1814    #[allow(dead_code)]
1815    pub(crate) async fn is_connection_active(&self, channel_id: &str) -> bool {
1816        self.transport.is_connection_active(channel_id).await
1817    }
1818
1819    /// Send a message to an authenticated peer, reconnecting on demand.
1820    ///
1821    /// Tries the existing connection first. If the send fails (stale QUIC
1822    /// session, peer not found, etc.), resolves a dial address from:
1823    ///
1824    /// 1. Caller-provided `addrs` (highest priority)
1825    /// 2. Latest owner-published QUIC addresses in the DHT routing table
1826    /// 3. Addresses cached in the transport layer (snapshotted before the
1827    ///    send attempt, since stale-channel cleanup removes them)
1828    /// 4. Other DHT routing-table or live transport addresses
1829    ///
1830    /// Then dials, waits for identity exchange, and retries the send exactly
1831    /// once on the fresh connection.  Concurrent reconnects to the same peer
1832    /// are serialised so only one dial is attempted at a time.
1833    pub async fn send_message(
1834        &self,
1835        peer_id: &PeerId,
1836        protocol: &str,
1837        data: impl Into<Bytes>,
1838        addrs: &[MultiAddr],
1839    ) -> Result<()> {
1840        // `Bytes` lets retries and the transport share one buffer; a `Vec<u8>`
1841        // converts without copying.
1842        let data: Bytes = data.into();
1843        // Snapshot channel IDs before the send attempt — transport.send_message
1844        // prunes dead channels from bookkeeping but does NOT close the
1845        // underlying QUIC connection.  We need the original IDs for
1846        // disconnect_channel later.
1847        let existing_channels = self.transport.channels_for_peer(peer_id).await;
1848
1849        // No existing connection — serialise so concurrent sends to the same
1850        // unconnected peer don't each open their own QUIC connection.
1851        if existing_channels.is_empty() {
1852            let lock = self.reconnect_lock_for(peer_id);
1853            let _guard = lock.lock().await;
1854
1855            // Another sender may have connected while we waited for the lock.
1856            if self.transport.is_peer_connected(peer_id).await {
1857                return self.transport.send_message(peer_id, protocol, data).await;
1858            }
1859
1860            return self
1861                .reconnect_and_send(peer_id, protocol, data, addrs, &[], &[])
1862                .await;
1863        }
1864
1865        // Snapshot addresses before the send attempt — transport.send_message
1866        // prunes stale channels, which removes peer_info.
1867        let saved_addrs: Vec<MultiAddr> = self
1868            .transport
1869            .peer_info(peer_id)
1870            .await
1871            .map(|info| info.addresses)
1872            .unwrap_or_default();
1873
1874        // Keep a handle for the retry — only stale-channel failures are
1875        // retried; cloning `Bytes` shares the buffer.
1876        let retry_data = data.clone();
1877
1878        // Fast path: try existing connection.
1879        let send_result = self.transport.send_message(peer_id, protocol, data).await;
1880        match send_result {
1881            Ok(()) => return Ok(()),
1882            Err(e) => {
1883                if !e.is_stale_channel_send_failure() {
1884                    debug!(
1885                        peer = %peer_id.to_hex(),
1886                        error = %e,
1887                        "send failed during active channel use, not reconnecting",
1888                    );
1889                    return Err(e);
1890                }
1891
1892                debug!(
1893                    peer = %peer_id.to_hex(),
1894                    error = %e,
1895                    "stale channel send failed, attempting reconnect",
1896                );
1897            }
1898        }
1899
1900        // Serialise reconnect attempts so concurrent sends to the same
1901        // stale peer don't race to dial.
1902        let lock = self.reconnect_lock_for(peer_id);
1903        let _guard = lock.lock().await;
1904
1905        // Another sender may have reconnected while we waited for the lock.
1906        if self.transport.is_peer_connected(peer_id).await {
1907            // Close stale QUIC connections that remove_channel (called inside
1908            // transport.send_message on failure) didn't tear down — it only
1909            // removes bookkeeping, not the underlying QUIC session.
1910            for channel_id in &existing_channels {
1911                self.transport.disconnect_channel(channel_id).await;
1912            }
1913            return self
1914                .transport
1915                .send_message(peer_id, protocol, retry_data)
1916                .await;
1917        }
1918
1919        self.reconnect_and_send(
1920            peer_id,
1921            protocol,
1922            retry_data,
1923            addrs,
1924            &saved_addrs,
1925            &existing_channels,
1926        )
1927        .await
1928    }
1929
1930    /// Ensure an identity-authenticated channel to `peer_id` exists, dialing a
1931    /// fresh connection when necessary.
1932    ///
1933    /// Resolves dial addresses from explicit caller input, the latest owner
1934    /// publication, or saved connection and discovery addresses in that order,
1935    /// tears down any stale channels, dials, waits for the identity exchange,
1936    /// and verifies the authenticated peer matches `peer_id`. On success the
1937    /// transport's `peer_to_channel` map is populated, so a subsequent
1938    /// `send_message` / `send_request` finds the channel instead of failing
1939    /// with `PeerNotFound`. Returns `PeerNotFound` when no dialable address is
1940    /// available.
1941    ///
1942    /// Shared by [`Self::reconnect_and_send`] and
1943    /// [`Self::send_request_reconnecting`] so both gain identical dial
1944    /// behaviour.
1945    async fn ensure_channel(
1946        &self,
1947        peer_id: &PeerId,
1948        addrs: &[MultiAddr],
1949        saved_addrs: &[MultiAddr],
1950        stale_channels: &[String],
1951    ) -> Result<()> {
1952        // Tear down stale QUIC connections using their actual channel IDs.
1953        // transport.send_message only removes bookkeeping (peer_to_channel,
1954        // peers, active_connections) — it does NOT close the underlying QUIC
1955        // connection.  We must use the real channel IDs, not the resolved
1956        // dial address, because NAT / port migration can make them differ.
1957        if !stale_channels.is_empty() {
1958            for channel_id in stale_channels {
1959                self.transport.disconnect_channel(channel_id).await;
1960            }
1961            tokio::time::sleep(QUIC_TEARDOWN_GRACE).await;
1962        }
1963
1964        let candidates = self
1965            .resolve_dial_candidates(peer_id, addrs, saved_addrs)
1966            .await;
1967        if candidates.is_empty() {
1968            return Err(P2PError::Network(NetworkError::PeerNotFound(
1969                peer_id.to_hex().into(),
1970            )));
1971        }
1972        self.adaptive_dht
1973            .ensure_peer_channel(peer_id, &candidates)
1974            .await
1975    }
1976
1977    /// Tear down stale channels, reconnect to a peer, and send a message.
1978    async fn reconnect_and_send(
1979        &self,
1980        peer_id: &PeerId,
1981        protocol: &str,
1982        data: Bytes,
1983        addrs: &[MultiAddr],
1984        saved_addrs: &[MultiAddr],
1985        stale_channels: &[String],
1986    ) -> Result<()> {
1987        self.ensure_channel(peer_id, addrs, saved_addrs, stale_channels)
1988            .await?;
1989        // Send on the fresh connection.
1990        self.transport.send_message(peer_id, protocol, data).await
1991    }
1992
1993    /// Resolve typed dial candidates for `peer_id`. Explicit caller input wins;
1994    /// otherwise an owner-published QUIC set supersedes saved connection addresses.
1995    ///
1996    /// Returns every dialable (QUIC, non-unspecified) address from the chosen
1997    /// source. An authoritative set does not fall back to superseded
1998    /// cached addresses if none of its endpoints are dialable.
1999    /// Caller-provided / saved addresses inherit the
2000    /// [`AddressType`] from the DHT when possible and otherwise fall back to
2001    /// [`AddressType::Unverified`] — the same default the routing table
2002    /// applies to legacy peers that never asserted reachability.
2003    async fn resolve_dial_candidates(
2004        &self,
2005        peer_id: &PeerId,
2006        caller_addrs: &[MultiAddr],
2007        saved_addrs: &[MultiAddr],
2008    ) -> Vec<(MultiAddr, AddressType)> {
2009        if caller_addrs.is_empty()
2010            && let Some(published) = self
2011                .dht_manager()
2012                .published_peer_addresses_for_dial_typed(peer_id)
2013                .await
2014        {
2015            return published;
2016        }
2017        let dht_candidates = self
2018            .adaptive_dht
2019            .peer_addresses_for_dial_typed(peer_id)
2020            .await;
2021        let preferred = if !caller_addrs.is_empty() {
2022            caller_addrs
2023        } else if !saved_addrs.is_empty() {
2024            saved_addrs
2025        } else {
2026            return dht_candidates;
2027        };
2028
2029        preferred
2030            .iter()
2031            .filter(|a| {
2032                let dialable = a
2033                    .dialable_socket_addr()
2034                    .is_some_and(|sa| !sa.ip().is_unspecified());
2035                if !dialable {
2036                    trace!(address = %a, "skipping non-dialable address");
2037                }
2038                dialable
2039            })
2040            .map(|addr| {
2041                let kind = dht_candidates
2042                    .iter()
2043                    .find_map(|(candidate, kind)| (candidate == addr).then_some(*kind))
2044                    .unwrap_or(AddressType::Unverified);
2045                (addr.clone(), kind)
2046            })
2047            .collect()
2048    }
2049
2050    /// Get or create a per-peer reconnect lock.
2051    fn reconnect_lock_for(&self, peer_id: &PeerId) -> Arc<TokioMutex<()>> {
2052        self.reconnect_locks
2053            .lock()
2054            .entry(*peer_id)
2055            .or_insert_with(|| Arc::new(TokioMutex::new(())))
2056            .clone()
2057    }
2058}
2059
2060/// Convenience constructor for `P2PError::Network(NetworkError::ProtocolError(...))`.
2061fn protocol_error(msg: impl std::fmt::Display) -> P2PError {
2062    P2PError::Network(NetworkError::ProtocolError(msg.to_string().into()))
2063}
2064
2065/// Helper to send an event via a broadcast sender, logging at trace level if no receivers.
2066pub(crate) fn broadcast_event(tx: &broadcast::Sender<P2PEvent>, event: P2PEvent) {
2067    if let Err(e) = tx.send(event) {
2068        tracing::trace!("Event broadcast has no receivers: {e}");
2069    }
2070}
2071
2072/// Result of parsing a protocol message, including optional authenticated identity.
2073///
2074/// The signed wire timestamp is carried on the inner [`P2PEvent::Message`]
2075/// (see its `timestamp` field) so subscribers can apply their own freshness
2076/// or replay policy now that the wire-level skew gate is gone.
2077pub(crate) struct ParsedMessage {
2078    /// The P2P event to broadcast.
2079    pub(crate) event: P2PEvent,
2080    /// If the message was signed and verified, the authenticated app-level [`PeerId`].
2081    pub(crate) authenticated_node_id: Option<PeerId>,
2082    /// The sender's user agent string from the wire message.
2083    pub(crate) user_agent: String,
2084    /// Decoded payload length (bytes). Lets the rx choke point compute wire
2085    /// envelope overhead (wire − payload) for V2-623 traffic accounting.
2086    pub(crate) payload_len: usize,
2087}
2088
2089/// Parse a postcard-encoded protocol message into a `P2PEvent::Message`.
2090///
2091/// Returns `None` if the bytes cannot be deserialized as a valid `WireMessage`.
2092///
2093/// The `from` field is a required part of the wire protocol but is **not**
2094/// used as the event source. Instead, `source` — the transport-level peer ID
2095/// derived from the authenticated QUIC connection — is used so that consumers
2096/// can pass it directly to `send_message()`. This eliminates a spoofing
2097/// vector where a peer could claim an arbitrary identity via the payload.
2098pub(crate) fn parse_protocol_message(bytes: &[u8], source: &str) -> Option<ParsedMessage> {
2099    let message: WireMessage = postcard::from_bytes(bytes).ok()?;
2100    let transport_source = source.parse::<SocketAddr>().ok().map(MultiAddr::quic);
2101
2102    // Verify app-level signature if present
2103    let authenticated_node_id = if !message.signature.is_empty() {
2104        match verify_message_signature(&message) {
2105            Ok(peer_id) => {
2106                debug!(
2107                    "Message from {} authenticated as app-level NodeId {}",
2108                    source, peer_id
2109                );
2110                Some(peer_id)
2111            }
2112            Err(e) => {
2113                // `source` (the channel's socket address) is left out: the
2114                // error names the sender's peer ID.
2115                warn!("Rejecting message: signature verification failed: {}", e);
2116                return None;
2117            }
2118        }
2119    } else {
2120        None
2121    };
2122
2123    debug!(
2124        "Parsed P2PEvent::Message - topic: {}, source: {:?} (transport: {}, logical: {}), payload_len: {}",
2125        message.protocol,
2126        authenticated_node_id,
2127        source,
2128        message.from,
2129        message.data.len()
2130    );
2131
2132    let payload_len = message.data.len();
2133    Some(ParsedMessage {
2134        event: P2PEvent::Message {
2135            topic: message.protocol,
2136            source: authenticated_node_id,
2137            transport_source,
2138            timestamp: message.timestamp,
2139            data: message.data,
2140        },
2141        authenticated_node_id,
2142        payload_len,
2143        user_agent: message.user_agent,
2144    })
2145}
2146
2147/// Verify the ML-DSA-65 signature on a WireMessage and return the authenticated [`PeerId`].
2148///
2149/// Besides verifying the cryptographic signature, this also checks that the
2150/// self-asserted `from` field matches the [`PeerId`] derived from the public
2151/// key. This prevents a sender from signing with their real key while
2152/// claiming a different identity in the `from` field.
2153fn verify_message_signature(message: &WireMessage) -> std::result::Result<PeerId, String> {
2154    let pubkey = MlDsaPublicKey::from_bytes(&message.public_key)
2155        .map_err(|e| format!("invalid public key: {e:?}"))?;
2156
2157    let peer_id = peer_id_from_public_key(&pubkey);
2158
2159    // Validate that the self-asserted `from` field matches the public key.
2160    if message.from != peer_id {
2161        return Err(format!(
2162            "from field mismatch: message claims '{}' but public key derives '{}'",
2163            message.from, peer_id
2164        ));
2165    }
2166
2167    let signable = postcard::to_stdvec(&(
2168        &message.protocol,
2169        &message.data as &[u8],
2170        &message.from,
2171        message.timestamp,
2172        &message.user_agent,
2173    ))
2174    .map_err(|e| format!("failed to serialize signable bytes: {e}"))?;
2175
2176    let sig = MlDsaSignature::from_bytes(&message.signature)
2177        .map_err(|e| format!("invalid signature: {e:?}"))?;
2178
2179    let valid = crate::quantum_crypto::ml_dsa_verify(&pubkey, &signable, &sig)
2180        .map_err(|e| format!("verification error: {e}"))?;
2181
2182    if valid {
2183        Ok(peer_id)
2184    } else {
2185        Err("signature is invalid".to_string())
2186    }
2187}
2188
2189impl P2PNode {
2190    /// Subscribe to network events
2191    pub fn subscribe_events(&self) -> broadcast::Receiver<P2PEvent> {
2192        self.transport.subscribe_events()
2193    }
2194
2195    /// Backwards-compat event stream accessor for tests
2196    pub fn events(&self) -> broadcast::Receiver<P2PEvent> {
2197        self.subscribe_events()
2198    }
2199
2200    /// Get node uptime
2201    pub fn uptime(&self) -> Duration {
2202        self.start_time.elapsed()
2203    }
2204
2205    // MCP removed: all MCP tool/service methods removed
2206
2207    // /// Handle MCP remote tool call with network integration
2208
2209    // /// List tools available on a specific remote peer
2210
2211    // /// Get MCP server statistics
2212
2213    // Background tasks (connection_lifecycle_monitor, keepalive, periodic_maintenance)
2214    // are now implemented in TransportHandle.
2215
2216    /// Check system health
2217    pub async fn health_check(&self) -> Result<()> {
2218        let peer_count = self.peer_count().await;
2219        if peer_count > self.config.max_connections {
2220            Err(protocol_error(format!(
2221                "Too many connections: {peer_count}"
2222            )))
2223        } else {
2224            Ok(())
2225        }
2226    }
2227
2228    /// Get the attached DHT manager.
2229    pub fn dht_manager(&self) -> &Arc<DhtNetworkManager> {
2230        self.adaptive_dht.dht_manager()
2231    }
2232
2233    /// Backwards-compatible alias for `dht_manager()`.
2234    pub fn dht(&self) -> &Arc<DhtNetworkManager> {
2235        self.dht_manager()
2236    }
2237
2238    /// Connect to bootstrap peers and perform initial peer discovery.
2239    ///
2240    /// If a `close_group_cache` was loaded on startup, its peers are injected
2241    /// as the highest-priority addresses before configured bootstrap peers.
2242    /// Their trust scores were already imported into the `TrustEngine` before
2243    /// this method is called.
2244    async fn connect_bootstrap_peers(
2245        &self,
2246        close_group_cache: Option<&CloseGroupCache>,
2247    ) -> Result<()> {
2248        // Each entry is a list of addresses for a single peer. Close-group
2249        // peers are dialed serially to preserve trust-priority ordering;
2250        // configured bootstrap peers are dialed concurrently to cut cold-start
2251        // latency when some peers are slow or dead.
2252        let mut serial_addr_sets: Vec<(PeerId, Vec<MultiAddr>)> = Vec::new();
2253        let mut parallel_addr_sets: Vec<Vec<MultiAddr>> = Vec::new();
2254        let mut seen_addresses = std::collections::HashSet::new();
2255
2256        // Priority 0: Cached close group peers (pre-trusted, highest priority).
2257        // These peers had trust scores loaded into the TrustEngine earlier in start(),
2258        // so they are already known-good when added to the routing table.
2259        // Sorted by trust score (highest first), then XOR distance (closest first)
2260        // as tiebreaker so we reconnect to the most trusted, closest peers first.
2261        if let Some(cache) = close_group_cache {
2262            let mut sorted_peers: Vec<&CachedCloseGroupPeer> = cache.peers.iter().collect();
2263            sorted_peers.sort_by(|a, b| {
2264                // NaN-safe comparison: push NaN scores to the back instead
2265                // of treating them as equal (which would silently promote
2266                // corrupted entries to the front of the reconnection queue).
2267                let score_ord = match b.trust.score.partial_cmp(&a.trust.score) {
2268                    Some(ord) => ord,
2269                    None => {
2270                        if a.trust.score.is_nan() {
2271                            std::cmp::Ordering::Greater // a is NaN, push to back
2272                        } else {
2273                            std::cmp::Ordering::Less // b is NaN, push b to back
2274                        }
2275                    }
2276                };
2277                score_ord.then_with(|| {
2278                    let da = self.peer_id.xor_distance(&a.peer_id);
2279                    let db = self.peer_id.xor_distance(&b.peer_id);
2280                    da.cmp(&db)
2281                })
2282            });
2283
2284            let mut added_from_close_group = 0usize;
2285            for peer in &sorted_peers {
2286                let new_addresses: Vec<MultiAddr> = peer
2287                    .addresses
2288                    .iter()
2289                    .filter(|a| {
2290                        a.dialable_socket_addr()
2291                            .is_some_and(|sa| !seen_addresses.contains(&sa))
2292                    })
2293                    .cloned()
2294                    .collect();
2295
2296                if !new_addresses.is_empty() {
2297                    for addr in &new_addresses {
2298                        if let Some(sa) = addr.socket_addr() {
2299                            seen_addresses.insert(sa);
2300                        }
2301                    }
2302                    serial_addr_sets.push((peer.peer_id, new_addresses));
2303                    added_from_close_group += 1;
2304                }
2305            }
2306            if added_from_close_group > 0 {
2307                info!(
2308                    "Added {} close group cache peers (highest trust first)",
2309                    added_from_close_group
2310                );
2311            }
2312        }
2313
2314        // Priority 1: Configured bootstrap peers.
2315        if !self.config.bootstrap_peers.is_empty() {
2316            info!(
2317                "Using {} configured bootstrap peers (priority)",
2318                self.config.bootstrap_peers.len()
2319            );
2320            for multiaddr in &self.config.bootstrap_peers {
2321                let Some(socket_addr) = multiaddr.dialable_socket_addr() else {
2322                    warn!(
2323                        "Skipping non-QUIC bootstrap peer: {}",
2324                        multiaddr.transport()
2325                    );
2326                    continue;
2327                };
2328                seen_addresses.insert(socket_addr);
2329                parallel_addr_sets.push(vec![multiaddr.clone()]);
2330            }
2331        }
2332
2333        // Priority 2: the routing snapshot, if one validated. Addresses already
2334        // queued as close-group or configured candidates are skipped so a peer
2335        // is never dialled twice.
2336        //
2337        // Restored once per process. A client keeps its existing six-peer
2338        // startup bound: it does not serve the DHT, so it never asks whether it
2339        // is responsible for a key, which is the only question this repairs.
2340        // A re-bootstrap of a running node skips it too — the table it would
2341        // restore is the table the node already has.
2342        let snapshot_addr_sets = if self.config.mode == NodeMode::Client
2343            || self.routing_snapshot_restored.swap(true, Ordering::Relaxed)
2344        {
2345            None
2346        } else {
2347            self.routing_snapshot_dial_sets(&mut seen_addresses).await
2348        };
2349
2350        if serial_addr_sets.is_empty()
2351            && parallel_addr_sets.is_empty()
2352            && snapshot_addr_sets.is_none()
2353        {
2354            info!("No bootstrap peers configured");
2355            return Ok(());
2356        }
2357
2358        // Connect to bootstrap peers, wait for identity exchange, then
2359        // perform DHT peer discovery using the real cryptographic PeerIds.
2360        let identity_timeout = Duration::from_secs(BOOTSTRAP_IDENTITY_TIMEOUT_SECS);
2361        let mut successful_connections = 0;
2362        let cache_dial_candidates = serial_addr_sets.len();
2363        let configured_dial_candidates = parallel_addr_sets.len();
2364        let mut cache_dial_successes = 0usize;
2365        let mut configured_dial_successes = 0usize;
2366        let mut connected_peer_ids: Vec<PeerId> = Vec::new();
2367
2368        // Phase A: serial close-group dials to preserve trust-priority ordering.
2369        let client_mode = matches!(self.config.mode, NodeMode::Client);
2370        for (expected_peer_id, addrs) in &serial_addr_sets {
2371            if let Some(peer_id) = self
2372                .dial_bootstrap_addr_set(addrs, identity_timeout, "cache", Some(*expected_peer_id))
2373                .await
2374            {
2375                successful_connections += 1;
2376                cache_dial_successes += 1;
2377                connected_peer_ids.push(peer_id);
2378                if client_mode && successful_connections >= CLIENT_BOOTSTRAP_TARGET {
2379                    debug!(
2380                        "Client bootstrap target reached ({successful_connections} peers) — skipping remaining serial dials"
2381                    );
2382                    break;
2383                }
2384            }
2385        }
2386
2387        // Phase B: concurrent dials of configured bootstrap peers, bounded by
2388        // `MAX_CONCURRENT_BOOTSTRAP_DIALS` to cap simultaneous QUIC+PQC
2389        // handshakes. Skipped entirely when a client has already hit its
2390        // target during Phase A.
2391        if !client_mode || successful_connections < CLIENT_BOOTSTRAP_TARGET {
2392            let mut parallel_stream =
2393                futures::stream::iter(parallel_addr_sets.into_iter().map(|addrs| async move {
2394                    self.dial_bootstrap_addr_set(&addrs, identity_timeout, "configured", None)
2395                        .await
2396                }))
2397                .buffer_unordered(MAX_CONCURRENT_BOOTSTRAP_DIALS);
2398            while let Some(result) = parallel_stream.next().await {
2399                if let Some(peer_id) = result {
2400                    successful_connections += 1;
2401                    configured_dial_successes += 1;
2402                    connected_peer_ids.push(peer_id);
2403                    if client_mode && successful_connections >= CLIENT_BOOTSTRAP_TARGET {
2404                        debug!(
2405                            "Client bootstrap target reached ({successful_connections} peers) — cancelling pending dials"
2406                        );
2407                        break;
2408                    }
2409                }
2410            }
2411            // `parallel_stream` is dropped here when the `if` block exits,
2412            // cancelling any in-flight futures inside `buffer_unordered`
2413            // before we proceed to the DHT discovery phase below.
2414        }
2415
2416        // Phase C: the routing snapshot.
2417        //
2418        // The close group reconnects a neighbourhood; this restores the rest of
2419        // the table, which is what a node consults to decide whether anyone is
2420        // closer to a given key than it is. Dialled last, because the close
2421        // group and the configured peers are what connectivity depends on, and
2422        // concurrently, because the set is an order of magnitude larger.
2423        //
2424        // Two bounds, both deliberate:
2425        //
2426        // - New dials stop once the budget expires; dials already in flight are
2427        //   allowed to finish, each bounded by `identity_timeout`. Cancelling a
2428        //   handshake mid-flight would leave the far side holding a half-open
2429        //   connection, which is a worse outcome than waiting out one timeout.
2430        // - Successes are NOT added to `connected_peer_ids`. A dialled,
2431        //   identity-verified peer is already admitted to the routing table by
2432        //   the connection path, so seeding DHT discovery with all of them would
2433        //   issue a serial FIND_NODE per peer to rediscover the table just
2434        //   restored, and then serially dial everything those queries returned.
2435        //
2436        // Every peer here is dialled and identity-verified through the same path
2437        // as any other candidate. Nothing from the file enters the routing table
2438        // on the file's authority alone.
2439        let mut snapshot_dial_candidates = 0usize;
2440        let mut snapshot_dial_successes = 0usize;
2441        if let Some(snapshot_sets) = snapshot_addr_sets {
2442            snapshot_dial_candidates = snapshot_sets.len();
2443            let deadline = tokio::time::Instant::now() + SNAPSHOT_RESTORE_BUDGET;
2444            let mut snapshot_stream = futures::stream::iter(snapshot_sets.into_iter().map(
2445                |(expected_peer_id, addrs)| async move {
2446                    if tokio::time::Instant::now() >= deadline {
2447                        return None;
2448                    }
2449                    self.dial_bootstrap_addr_set(
2450                        &addrs,
2451                        identity_timeout,
2452                        "routing_snapshot",
2453                        Some(expected_peer_id),
2454                    )
2455                    .await
2456                },
2457            ))
2458            .buffer_unordered(MAX_CONCURRENT_SNAPSHOT_DIALS);
2459
2460            while let Some(outcome) = snapshot_stream.next().await {
2461                if outcome.is_some() {
2462                    snapshot_dial_successes += 1;
2463                    successful_connections += 1;
2464                }
2465            }
2466
2467            if snapshot_dial_successes < snapshot_dial_candidates {
2468                debug!(
2469                    snapshot_dial_successes,
2470                    snapshot_dial_candidates,
2471                    "Some routing-snapshot peers were not restored; the rest is left to \
2472                     ordinary discovery"
2473                );
2474            }
2475        }
2476
2477        info!(
2478            cache_dial_candidates,
2479            cache_dial_successes,
2480            configured_dial_candidates,
2481            configured_dial_successes,
2482            snapshot_dial_candidates,
2483            snapshot_dial_successes,
2484            outbound_bootstrap_successes = successful_connections,
2485            outbound_reachable = successful_connections > 0,
2486            "Bootstrap reachability summary"
2487        );
2488
2489        if successful_connections == 0 {
2490            // Outbound connections failed — but for nodes behind symmetric NAT,
2491            // the bootstrap peer may have already connected INBOUND to us.
2492            // Wait briefly and check if we have any transport-level connections.
2493            tokio::time::sleep(std::time::Duration::from_secs(5)).await;
2494            let transport_peers = self.transport.connected_peers().await;
2495            if !transport_peers.is_empty() {
2496                info!(
2497                    "No outbound bootstrap succeeded, but {} inbound peer(s) connected — proceeding with DHT bootstrap",
2498                    transport_peers.len()
2499                );
2500                connected_peer_ids = transport_peers;
2501                successful_connections = connected_peer_ids.len();
2502            } else {
2503                warn!("Failed to connect to any bootstrap peers");
2504                // Starting a node should not be gated on immediate bootstrap connectivity.
2505                // Keep running and allow background discovery / retries to populate peers later.
2506                return Ok(());
2507            }
2508        }
2509
2510        info!(
2511            "Successfully connected to {} bootstrap peers",
2512            successful_connections
2513        );
2514
2515        // Perform DHT peer discovery from connected bootstrap peers.
2516        match self
2517            .dht_manager()
2518            .bootstrap_from_peers(&connected_peer_ids)
2519            .await
2520        {
2521            Ok(count) => info!("DHT peer discovery found {} peers", count),
2522            Err(e) => warn!("DHT peer discovery failed: {}", e),
2523        }
2524
2525        // Perform two consecutive self-lookups to fully refresh the close
2526        // neighborhood. The second lookup may discover peers that joined or
2527        // became reachable during the first lookup (Section 11.2 step 5).
2528        //
2529        // Client-mode nodes don't serve the DHT, so they don't need an
2530        // accurate close neighborhood — they just need enough peers to route
2531        // lookups for their own requests, which `bootstrap_from_peers` above
2532        // already provides. Skipping the self-lookups here cuts cold-start
2533        // latency by tens of seconds when α-sized batches include dead peers.
2534        if matches!(self.config.mode, NodeMode::Node) {
2535            const SELF_LOOKUP_ROUNDS: u8 = 2;
2536            for i in 1..=SELF_LOOKUP_ROUNDS {
2537                if let Err(e) = self.dht_manager().trigger_self_lookup().await {
2538                    warn!("Post-bootstrap self-lookup {i}/{SELF_LOOKUP_ROUNDS} failed: {e}");
2539                } else {
2540                    debug!("Post-bootstrap self-lookup {i}/{SELF_LOOKUP_ROUNDS} completed");
2541                }
2542            }
2543        } else {
2544            debug!("Skipping post-bootstrap self-lookups (client mode)");
2545        }
2546
2547        // Mark node as bootstrapped - we have connected to bootstrap peers
2548        // and initiated peer discovery
2549        self.is_bootstrapped.store(true, Ordering::SeqCst);
2550        info!(
2551            "Bootstrap complete: connected to {} peers, initiated {} discovery requests",
2552            successful_connections,
2553            connected_peer_ids.len()
2554        );
2555
2556        // Save close group cache after initial bootstrap so a crash before
2557        // graceful shutdown still preserves the newly-discovered close group.
2558        if let Some(ref dir) = self.config.close_group_cache_dir
2559            && let Err(e) = self.save_close_group_cache(dir, "post_bootstrap").await
2560        {
2561            warn!("Failed to save close group cache after bootstrap: {e}");
2562        }
2563
2564        Ok(())
2565    }
2566
2567    /// Dial a single bootstrap peer's address set, stopping at the first
2568    /// address that completes the identity handshake. Returns the remote peer's
2569    /// cryptographic PeerId on success. Safe to call concurrently for different
2570    /// peers.
2571    async fn dial_bootstrap_addr_set(
2572        &self,
2573        addrs: &[MultiAddr],
2574        identity_timeout: Duration,
2575        source: &'static str,
2576        expected_peer_id: Option<PeerId>,
2577    ) -> Option<PeerId> {
2578        for addr in addrs {
2579            // Log the transport part only: a MultiAddr's Display appends
2580            // `/p2p/<peer id>`, and no line here may tie a peer ID to its IP.
2581            let log_addr = addr.transport();
2582            // Bootstrap addresses come from operator-supplied seeds (CLI
2583            // flags or config file). The local reachability classifier hasn't
2584            // proven them yet, so log them as `Unverified` rather than
2585            // `unknown`.
2586            match self
2587                .transport
2588                .connect_peer_typed(addr, AddressType::Unverified)
2589                .await
2590            {
2591                Ok(channel_id) => match self
2592                    .transport
2593                    .wait_for_peer_identity(&channel_id, identity_timeout)
2594                    .await
2595                {
2596                    Ok(real_peer_id) => {
2597                        if !bootstrap_peer_identity_matches(expected_peer_id, real_peer_id) {
2598                            warn!(
2599                                bootstrap_source = source,
2600                                address = %log_addr,
2601                                "Bootstrap cache identity mismatch; rejecting connection"
2602                            );
2603                            self.disconnect_channel(&channel_id).await;
2604                            continue;
2605                        }
2606                        info!(
2607                            bootstrap_source = source,
2608                            outcome = "ok",
2609                            address = %log_addr,
2610                            "Bootstrap dial completed"
2611                        );
2612                        return Some(real_peer_id);
2613                    }
2614                    Err(e) => {
2615                        info!(
2616                            bootstrap_source = source,
2617                            outcome = "identity_timeout",
2618                            address = %log_addr,
2619                            error = %e,
2620                            "Bootstrap dial failed"
2621                        );
2622                        warn!(
2623                            "Timeout waiting for identity from bootstrap peer {}: {}, \
2624                             closing channel {}",
2625                            log_addr, e, channel_id
2626                        );
2627                        self.disconnect_channel(&channel_id).await;
2628                    }
2629                },
2630                Err(e) => {
2631                    info!(
2632                        bootstrap_source = source,
2633                        outcome = "connect_error",
2634                        address = %log_addr,
2635                        error = %e,
2636                        "Bootstrap dial failed"
2637                    );
2638                    warn!("Failed to connect to bootstrap peer {}: {}", log_addr, e);
2639                }
2640            }
2641        }
2642        None
2643    }
2644
2645    /// Record how many peers the restore recovered from, and when.
2646    ///
2647    /// Until this is called a snapshot is never written, so a node stopped
2648    /// before the restore step, and a client that never restores at all, cannot
2649    /// overwrite a good file with an empty or partial table.
2650    fn resolve_snapshot_floor(&self, restored_peers: usize) {
2651        let now_epoch = SystemTime::now()
2652            .duration_since(UNIX_EPOCH)
2653            .map_or(0, |duration| duration.as_secs());
2654        // Publish the timestamp first and the count second with `Release`, so a
2655        // save that sees a resolved floor cannot also see the zero timestamp it
2656        // was published with and conclude the floor has already expired.
2657        self.routing_snapshot_floor_at
2658            .store(now_epoch, Ordering::Relaxed);
2659        self.routing_snapshot_floor
2660            .store(restored_peers, Ordering::Release);
2661    }
2662
2663    /// Load the routing snapshot and turn it into dial candidates.
2664    ///
2665    /// Returns `None` when there is nothing usable — no configured directory,
2666    /// no file, or a rejected one. A rejection is logged with its reason: a node
2667    /// that quietly cold-starts on every restart looks exactly like one that
2668    /// never had a snapshot, and an operator needs to tell those apart.
2669    async fn routing_snapshot_dial_sets(
2670        &self,
2671        seen_addresses: &mut HashSet<SocketAddr>,
2672    ) -> Option<Vec<(PeerId, Vec<MultiAddr>)>> {
2673        let dir = self.config.close_group_cache_dir.as_ref()?;
2674
2675        let snapshot = match RoutingSnapshot::load_from_dir(dir).await {
2676            Ok(Some(snapshot)) => snapshot,
2677            Ok(None) => {
2678                self.resolve_snapshot_floor(0);
2679                return None;
2680            }
2681            Err(rejection) => {
2682                warn!(%rejection, "Discarding routing snapshot");
2683                self.resolve_snapshot_floor(0);
2684                return None;
2685            }
2686        };
2687
2688        let now_epoch = SystemTime::now()
2689            .duration_since(UNIX_EPOCH)
2690            .map_or(0, |duration| duration.as_secs());
2691        let peers = match snapshot.peers_for(&self.peer_id, now_epoch) {
2692            Ok(peers) => peers,
2693            Err(rejection) => {
2694                warn!(%rejection, "Discarding routing snapshot");
2695                self.resolve_snapshot_floor(0);
2696                return None;
2697            }
2698        };
2699
2700        // A later save must not write a table smaller than this one, for as
2701        // long as the floor applies.
2702        self.resolve_snapshot_floor(peers.len());
2703
2704        let sets = snapshot_dial_sets(peers, &self.peer_id, seen_addresses);
2705        if sets.is_empty() {
2706            return None;
2707        }
2708
2709        info!(
2710            snapshot_peers = peers.len(),
2711            new_candidates = sets.len(),
2712            age_secs = now_epoch.saturating_sub(snapshot.saved_at_epoch_secs),
2713            "Restoring routing table from snapshot"
2714        );
2715        Some(sets)
2716    }
2717
2718    /// Persist the current close group peers and their trust scores to disk.
2719    async fn save_close_group_cache(
2720        &self,
2721        dir: &Path,
2722        save_reason: &'static str,
2723    ) -> anyhow::Result<()> {
2724        save_close_group_cache_snapshot(
2725            self.dht_manager(),
2726            self.adaptive_dht.trust_engine(),
2727            self.peer_id,
2728            self.config.dht_config.k_value,
2729            dir,
2730            save_reason,
2731        )
2732        .await
2733    }
2734
2735    // disconnect_all_peers and periodic_tasks are now in TransportHandle
2736}
2737
2738/// Turn snapshot peers into dial candidates, skipping self and anything an
2739/// earlier bootstrap priority already queued.
2740///
2741/// Extends `seen_addresses` with what it returns, so a peer reachable through
2742/// the close-group cache and the snapshot is dialled once, not twice. Only
2743/// dialable (QUIC) addresses survive.
2744fn snapshot_dial_sets(
2745    peers: &[SnapshotPeer],
2746    self_id: &PeerId,
2747    seen_addresses: &mut HashSet<SocketAddr>,
2748) -> Vec<(PeerId, Vec<MultiAddr>)> {
2749    let mut sets: Vec<(PeerId, Vec<MultiAddr>)> = Vec::new();
2750    let mut seen_peers: HashSet<PeerId> = HashSet::new();
2751    for peer in peers {
2752        if peer.peer_id == *self_id || !seen_peers.insert(peer.peer_id) {
2753            continue;
2754        }
2755        let new_addresses: Vec<MultiAddr> = peer
2756            .addresses
2757            .iter()
2758            .filter(|addr| {
2759                addr.dialable_socket_addr()
2760                    .is_some_and(|socket| !seen_addresses.contains(&socket))
2761            })
2762            .take(MAX_SNAPSHOT_ADDRESSES_DIALLED)
2763            .cloned()
2764            .collect();
2765        if new_addresses.is_empty() {
2766            continue;
2767        }
2768        seen_addresses.extend(
2769            new_addresses
2770                .iter()
2771                .filter_map(MultiAddr::dialable_socket_addr),
2772        );
2773        sets.push((peer.peer_id, new_addresses));
2774    }
2775    sets
2776}
2777
2778/// Persist a close-group snapshot using owned subsystem handles.
2779///
2780/// Keeping this separate from `P2PNode` allows the periodic task to own every
2781/// dependency it needs without borrowing the node across a spawned task.
2782async fn save_close_group_cache_snapshot(
2783    dht_manager: &DhtNetworkManager,
2784    trust_engine: &TrustEngine,
2785    peer_id: PeerId,
2786    k_value: usize,
2787    dir: &Path,
2788    save_reason: &'static str,
2789) -> anyhow::Result<()> {
2790    let key: crate::dht::Key = *peer_id.as_bytes();
2791    let close_group = dht_manager.find_closest_nodes_local(&key, k_value).await;
2792
2793    let now_epoch = SystemTime::now()
2794        .duration_since(UNIX_EPOCH)
2795        .map_or(0, |duration| duration.as_secs());
2796    let peers: Vec<CachedCloseGroupPeer> = close_group
2797        .into_iter()
2798        .filter_map(|dht_node| {
2799            let score = trust_engine.score(&dht_node.peer_id);
2800            // Guard against NaN/Infinity — serde_json cannot round-trip
2801            // non-finite f64 values, which would corrupt the cache file.
2802            if !score.is_finite() {
2803                return None;
2804            }
2805            Some(CachedCloseGroupPeer {
2806                peer_id: dht_node.peer_id,
2807                addresses: dht_node.addresses,
2808                trust: TrustRecord {
2809                    score,
2810                    last_updated_epoch_secs: now_epoch,
2811                },
2812            })
2813        })
2814        .collect();
2815
2816    let peer_count = peers.len();
2817    let cache = CloseGroupCache {
2818        peers,
2819        saved_at_epoch_secs: now_epoch,
2820    };
2821
2822    cache.save_to_dir(dir).await?;
2823    info!(
2824        save_reason,
2825        "Saved {} close group peers to cache in {}",
2826        peer_count,
2827        dir.display()
2828    );
2829
2830    Ok(())
2831}
2832
2833/// Persist the whole routing table as a [`RoutingSnapshot`].
2834///
2835/// Best-effort and non-fatal: the close-group cache is the established path and
2836/// must not start failing because a newer, unread file could not be written.
2837///
2838/// Writes nothing until the restore step has decided what the floor is, and
2839/// then nothing smaller than the table it restored while that floor still
2840/// applies. Between opening and finishing its restore a node holds only what it
2841/// has re-dialled so far, and one stopped in that window would otherwise
2842/// replace a complete snapshot with a fragment, shrinking its own file a little
2843/// further on every cycle. A client never resolves a floor and so never writes
2844/// a snapshot it would not read back.
2845async fn save_routing_snapshot(
2846    dht_manager: &DhtNetworkManager,
2847    peer_id: PeerId,
2848    now_epoch: u64,
2849    dir: &Path,
2850    floor: &AtomicUsize,
2851    floor_at: &AtomicU64,
2852    save_reason: &'static str,
2853) {
2854    let floor_peers = floor.load(Ordering::Acquire);
2855    if floor_peers == SNAPSHOT_FLOOR_UNRESOLVED {
2856        debug!(
2857            save_reason,
2858            "Skipping routing snapshot save: the restore step has not run"
2859        );
2860        return;
2861    }
2862
2863    let peers: Vec<SnapshotPeer> = dht_manager
2864        .routing_table_peers()
2865        .await
2866        .into_iter()
2867        .map(|node| SnapshotPeer {
2868            peer_id: node.peer_id,
2869            addresses: node.addresses,
2870        })
2871        .collect();
2872    let peer_count = peers.len();
2873
2874    if peer_count == 0 {
2875        debug!(
2876            save_reason,
2877            "Skipping routing snapshot save: the table is empty"
2878        );
2879        return;
2880    }
2881    let floor_applies = now_epoch.saturating_sub(floor_at.load(Ordering::Relaxed))
2882        < SNAPSHOT_FLOOR_ENFORCED_FOR.as_secs();
2883    if floor_applies && peer_count < floor_peers {
2884        debug!(
2885            save_reason,
2886            peer_count,
2887            floor_peers,
2888            "Skipping routing snapshot save: it would shrink the snapshot while the table is \
2889             still restoring"
2890        );
2891        return;
2892    }
2893
2894    let snapshot = RoutingSnapshot::new(peer_id, now_epoch, peers);
2895    match snapshot.save_to_dir(dir).await {
2896        Ok(()) => debug!(save_reason, peer_count, "Saved routing snapshot"),
2897        Err(error) => warn!(save_reason, %error, "Failed to save routing snapshot"),
2898    }
2899}
2900
2901/// Periodically persist the close group until cancelled.
2902async fn periodic_close_group_cache_save(
2903    dht_manager: Arc<DhtNetworkManager>,
2904    trust_engine: Arc<TrustEngine>,
2905    peer_id: PeerId,
2906    k_value: usize,
2907    dir: PathBuf,
2908    interval: Duration,
2909    snapshot_floor: Arc<AtomicUsize>,
2910    snapshot_floor_at: Arc<AtomicU64>,
2911    shutdown: CancellationToken,
2912) {
2913    let start = tokio::time::Instant::now() + interval;
2914    let mut ticker = tokio::time::interval_at(start, interval);
2915    ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
2916
2917    loop {
2918        tokio::select! {
2919            biased;
2920            () = shutdown.cancelled() => break,
2921            _ = ticker.tick() => {
2922                if let Err(error) = save_close_group_cache_snapshot(
2923                    &dht_manager,
2924                    &trust_engine,
2925                    peer_id,
2926                    k_value,
2927                    &dir,
2928                    "periodic",
2929                ).await {
2930                    warn!("Periodic close group cache save failed: {error}");
2931                }
2932                let now_epoch = SystemTime::now()
2933                    .duration_since(UNIX_EPOCH)
2934                    .map_or(0, |duration| duration.as_secs());
2935                save_routing_snapshot(
2936                    &dht_manager,
2937                    peer_id,
2938                    now_epoch,
2939                    &dir,
2940                    &snapshot_floor,
2941                    &snapshot_floor_at,
2942                    "periodic",
2943                ).await;
2944            }
2945        }
2946    }
2947}
2948
2949/// Network sender trait for sending messages
2950#[async_trait::async_trait]
2951#[allow(dead_code)]
2952pub trait NetworkSender: Send + Sync {
2953    /// Send a message to an authenticated peer.
2954    async fn send_message(&self, peer_id: &PeerId, protocol: &str, data: Vec<u8>) -> Result<()>;
2955
2956    /// Get our local peer ID (cryptographic identity).
2957    fn local_peer_id(&self) -> PeerId;
2958}
2959
2960// P2PNetworkSender removed — NetworkSender is now implemented directly on TransportHandle.
2961// NodeBuilder removed — use NodeConfigBuilder + P2PNode::new() instead.
2962
2963/// Helper function to register a new channel.
2964///
2965/// Sync because the underlying map is a sharded `DashMap` — no `.await` is
2966/// needed to take a write lock. Keeping this sync is what lets the inbound
2967/// accept loop in `TransportHandle` insert without yielding, so it cannot
2968/// stall and back-pressure the upstream handshake channel.
2969pub(crate) fn register_new_channel(
2970    peers: &DashMap<String, PeerInfo>,
2971    channel_id: &str,
2972    remote_addr: &MultiAddr,
2973) {
2974    let peer_info = PeerInfo {
2975        channel_id: channel_id.to_owned(),
2976        addresses: vec![remote_addr.clone()],
2977        connected_at: tokio::time::Instant::now(),
2978        last_seen: tokio::time::Instant::now(),
2979        status: ConnectionStatus::Connected,
2980        protocols: vec!["p2p-core/1.0.0".to_string()],
2981        heartbeat_count: 0,
2982    };
2983    peers.insert(channel_id.to_owned(), peer_info);
2984}
2985
2986#[cfg(test)]
2987mod tests {
2988    use super::*;
2989    // MCP removed from tests
2990    use std::time::Duration;
2991    use tokio::time::timeout;
2992
2993    /// 2 MiB — used in builder tests to verify max_message_size configuration.
2994    const TEST_MAX_MESSAGE_SIZE: usize = 2 * 1024 * 1024;
2995    /// A loopback channel ID with no QUIC connection behind it (discard port).
2996    const UNCONNECTED_CHANNEL_ID: &str = "127.0.0.1:9";
2997
2998    /// The borrowing frame writer must stay byte-for-byte compatible with the
2999    /// owned `WireMessage` that receivers decode.
3000    #[test]
3001    fn wire_message_ref_serializes_like_wire_message() {
3002        let owned = WireMessage {
3003            protocol: "/ant/replication/1".to_string(),
3004            data: (0..=255u8).cycle().take(70_000).collect(),
3005            from: PeerId::from_bytes([7; 32]),
3006            timestamp: 1_790_000_000,
3007            user_agent: "node/0.27.4".to_string(),
3008            public_key: vec![1; 1952],
3009            signature: vec![2; 3309],
3010        };
3011        let borrowed = WireMessageRef {
3012            protocol: &owned.protocol,
3013            data: &owned.data,
3014            from: &owned.from,
3015            timestamp: owned.timestamp,
3016            user_agent: &owned.user_agent,
3017            public_key: &owned.public_key,
3018            signature: &owned.signature,
3019        };
3020
3021        let expected = postcard::to_stdvec(&owned).unwrap();
3022        let actual = postcard::to_stdvec(&borrowed).unwrap();
3023        assert_eq!(actual, expected);
3024        assert_eq!(
3025            postcard::experimental::serialized_size(&borrowed).unwrap(),
3026            actual.len()
3027        );
3028
3029        let decoded: WireMessage = postcard::from_bytes(&actual).unwrap();
3030        assert_eq!(decoded.protocol, owned.protocol);
3031        assert_eq!(decoded.data, owned.data);
3032        assert_eq!(decoded.from, owned.from);
3033        assert_eq!(decoded.timestamp, owned.timestamp);
3034        assert_eq!(decoded.user_agent, owned.user_agent);
3035        assert_eq!(decoded.public_key, owned.public_key);
3036        assert_eq!(decoded.signature, owned.signature);
3037    }
3038
3039    #[test]
3040    fn cached_bootstrap_identity_must_match_handshake_peer() {
3041        let expected = PeerId::from_bytes([1; 32]);
3042        let other = PeerId::from_bytes([2; 32]);
3043
3044        assert!(bootstrap_peer_identity_matches(Some(expected), expected));
3045        assert!(!bootstrap_peer_identity_matches(Some(expected), other));
3046        assert!(bootstrap_peer_identity_matches(None, other));
3047    }
3048
3049    #[test]
3050    fn user_agents_identify_software_without_capability_tokens() {
3051        assert_eq!(
3052            user_agent_for_mode(NodeMode::Node),
3053            format!("node/{}", env!("CARGO_PKG_VERSION"))
3054        );
3055        assert_eq!(
3056            user_agent_for_mode(NodeMode::Client),
3057            format!("client/{}", env!("CARGO_PKG_VERSION"))
3058        );
3059        let mut config = NodeConfig::default();
3060        for custom in ["my-app/1.0", "", "node/custom;custom-feature"] {
3061            config.custom_user_agent = Some(custom.to_owned());
3062            assert_eq!(config.user_agent(), custom);
3063        }
3064    }
3065
3066    // Test tool handler for network tests
3067
3068    // MCP removed
3069
3070    /// Helper function to create a test node configuration
3071    fn create_test_node_config() -> NodeConfig {
3072        NodeConfig {
3073            local: true,
3074            port: 0,
3075            ipv6: true,
3076            bootstrap_peers: vec![],
3077            connection_timeout: Duration::from_secs(2),
3078            max_connections: 100,
3079            dht_config: DHTConfig::default(),
3080            diversity_config: None,
3081            max_message_size: None,
3082            node_identity: None,
3083            mode: NodeMode::default(),
3084            custom_user_agent: None,
3085            allow_loopback: true,
3086            adaptive_dht_config: AdaptiveDhtConfig::default(),
3087            close_group_cache_dir: None,
3088            close_group_cache_max_age: default_close_group_cache_max_age(),
3089        }
3090    }
3091
3092    /// Helper function to create a test tool
3093    // MCP removed: test tool helper deleted
3094
3095    // =========================================================================
3096    // PeerRouteKind / route classification — pure-helper unit tests.
3097    //
3098    // These exercise `classify_peer_route_from_typed` directly so no live
3099    // node/DHT is required. `P2PNode::classify_peer_transport_route` is a
3100    // thin async wrapper over this helper plus `peer_addresses_for_dial_typed`,
3101    // so the classification logic is fully covered here.
3102    // =========================================================================
3103
3104    fn quic_addr(s: &str) -> MultiAddr {
3105        MultiAddr::quic(s.parse().expect("valid socket addr"))
3106    }
3107
3108    #[test]
3109    fn test_peer_route_kind_display_labels() {
3110        assert_eq!(PeerRouteKind::Relay.to_string(), "relay");
3111        assert_eq!(PeerRouteKind::Direct.to_string(), "direct");
3112        assert_eq!(PeerRouteKind::Lan.to_string(), "lan");
3113        assert_eq!(PeerRouteKind::Unverified.to_string(), "unverified");
3114        assert_eq!(PeerRouteKind::Unknown.to_string(), "unknown");
3115        // as_str agrees with Display
3116        assert_eq!(PeerRouteKind::Relay.as_str(), "relay");
3117    }
3118
3119    #[test]
3120    fn test_classify_none_source_is_unknown() {
3121        let typed = vec![(quic_addr("203.0.113.7:9000"), AddressType::Relay)];
3122        assert_eq!(
3123            classify_peer_route_from_typed(None, &typed),
3124            PeerRouteKind::Unknown
3125        );
3126    }
3127
3128    #[test]
3129    fn test_classify_non_ip_source_is_unknown() {
3130        // Bluetooth is a non-IP transport: socket_addr() returns None.
3131        let ble = MultiAddr::new(crate::address::TransportAddr::Ble {
3132            mac: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06],
3133            psm: 128,
3134        });
3135        let typed = vec![(quic_addr("203.0.113.7:9000"), AddressType::Relay)];
3136        assert_eq!(
3137            classify_peer_route_from_typed(Some(&ble), &typed),
3138            PeerRouteKind::Unknown
3139        );
3140    }
3141
3142    #[test]
3143    fn test_classify_each_address_type() {
3144        let src = quic_addr("203.0.113.7:9000");
3145        for (ty, kind) in [
3146            (AddressType::Relay, PeerRouteKind::Relay),
3147            (AddressType::Direct, PeerRouteKind::Direct),
3148            (AddressType::Unverified, PeerRouteKind::Unverified),
3149            (AddressType::Lan, PeerRouteKind::Lan),
3150        ] {
3151            let typed = vec![(src.clone(), ty)];
3152            assert_eq!(
3153                classify_peer_route_from_typed(Some(&src), &typed),
3154                kind,
3155                "mismatch for {ty:?}"
3156            );
3157        }
3158    }
3159
3160    #[test]
3161    fn test_classify_no_match_is_unknown() {
3162        let src = quic_addr("198.51.100.42:7000");
3163        let typed = vec![
3164            (quic_addr("203.0.113.7:9000"), AddressType::Relay),
3165            (quic_addr("203.0.113.8:9000"), AddressType::Direct),
3166        ];
3167        assert_eq!(
3168            classify_peer_route_from_typed(Some(&src), &typed),
3169            PeerRouteKind::Unknown
3170        );
3171    }
3172
3173    #[test]
3174    fn test_classify_empty_typed_is_unknown() {
3175        let src = quic_addr("203.0.113.7:9000");
3176        let typed: Vec<(MultiAddr, AddressType)> = vec![];
3177        assert_eq!(
3178            classify_peer_route_from_typed(Some(&src), &typed),
3179            PeerRouteKind::Unknown
3180        );
3181    }
3182
3183    #[test]
3184    fn test_classify_duplicate_agreeing_type_dedups() {
3185        // Same socket address listed twice with the same type must yield that
3186        // type, not Unknown — duplicates are benign.
3187        let src = quic_addr("203.0.113.7:9000");
3188        let typed = vec![
3189            (quic_addr("203.0.113.7:9000"), AddressType::Relay),
3190            (quic_addr("203.0.113.7:9000"), AddressType::Relay),
3191        ];
3192        assert_eq!(
3193            classify_peer_route_from_typed(Some(&src), &typed),
3194            PeerRouteKind::Relay
3195        );
3196    }
3197
3198    #[test]
3199    fn test_classify_duplicate_conflicting_types_is_unknown() {
3200        // Same socket address tagged with two different AddressTypes is a
3201        // phonebook inconsistency. The classifier refuses to guess and
3202        // reports Unknown — it does NOT use AddressType::priority to pick a
3203        // winner, since that ordering governs dial preference, not the
3204        // provenance of an arrived message.
3205        let src = quic_addr("203.0.113.7:9000");
3206        let typed = vec![
3207            (quic_addr("203.0.113.7:9000"), AddressType::Relay),
3208            (quic_addr("203.0.113.7:9000"), AddressType::Direct),
3209        ];
3210        assert_eq!(
3211            classify_peer_route_from_typed(Some(&src), &typed),
3212            PeerRouteKind::Unknown
3213        );
3214    }
3215
3216    #[test]
3217    fn test_classify_matches_in_any_position() {
3218        // The matching entry need not be first; order is not a signal.
3219        let src = quic_addr("203.0.113.9:9000");
3220        let typed = vec![
3221            (quic_addr("203.0.113.7:9000"), AddressType::Relay),
3222            (quic_addr("203.0.113.8:9000"), AddressType::Direct),
3223            (quic_addr("203.0.113.9:9000"), AddressType::Lan),
3224            (quic_addr("203.0.113.10:9000"), AddressType::Unverified),
3225        ];
3226        assert_eq!(
3227            classify_peer_route_from_typed(Some(&src), &typed),
3228            PeerRouteKind::Lan
3229        );
3230    }
3231
3232    #[test]
3233    fn test_classify_tcp_source_matches_tcp_entry() {
3234        // socket_addr() is shared by Quic/Tcp/Udp, so a TCP source matches a
3235        // TCP phonebook entry with the same socket address.
3236        let src = MultiAddr::tcp("203.0.113.7:9000".parse().unwrap());
3237        let typed = vec![(
3238            MultiAddr::tcp("203.0.113.7:9000".parse().unwrap()),
3239            AddressType::Direct,
3240        )];
3241        assert_eq!(
3242            classify_peer_route_from_typed(Some(&src), &typed),
3243            PeerRouteKind::Direct
3244        );
3245    }
3246
3247    #[tokio::test]
3248    async fn reconnect_uses_latest_v1_and_v2_publications_over_saved_addresses() {
3249        use crate::dht_network_manager::{DhtMessageType, DhtNetworkMessage, DhtNetworkOperation};
3250        use crate::signed_address::SignedAddressRecord;
3251        use crate::{KnownReachability, TransportAddressRecord};
3252
3253        let config = || {
3254            NodeConfig::builder()
3255                .local(true)
3256                .port(0)
3257                .ipv6(false)
3258                .build()
3259                .unwrap()
3260        };
3261        let receiver = P2PNode::new(config()).await.unwrap();
3262        let publisher = P2PNode::new(config()).await.unwrap();
3263        // Keep the publication sequence controlled by this test. Starting the
3264        // publisher's DHT would allow an automatic V2 publication to correctly
3265        // suppress the initial V1 fixture before it can be observed.
3266        receiver.transport.start_network_listeners().await.unwrap();
3267        receiver.dht_manager().start().await.unwrap();
3268        publisher.transport.start_network_listeners().await.unwrap();
3269        let old_address = publisher
3270            .listen_addrs()
3271            .await
3272            .into_iter()
3273            .find(MultiAddr::is_ipv4)
3274            .unwrap();
3275        let channel = receiver.connect_peer(&old_address).await.unwrap();
3276        receiver
3277            .wait_for_peer_identity(&channel, Duration::from_secs(2))
3278            .await
3279            .unwrap();
3280        let owner = *publisher.peer_id();
3281        timeout(Duration::from_secs(2), async {
3282            while !publisher.is_peer_connected(receiver.peer_id()).await
3283                || !receiver.dht_manager().is_in_routing_table(&owner).await
3284            {
3285                tokio::time::sleep(Duration::from_millis(10)).await;
3286            }
3287        })
3288        .await
3289        .unwrap();
3290        let saved = receiver
3291            .transport
3292            .peer_info(&owner)
3293            .await
3294            .unwrap()
3295            .addresses;
3296        let first: MultiAddr = "/ip4/9.9.9.9/udp/9001/quic".parse().unwrap();
3297        let second: MultiAddr = "/ip4/1.1.1.1/udp/9002/quic".parse().unwrap();
3298        let sequence = u64::MAX - 2;
3299        let operations = [
3300            (
3301                "/dht/1.0.0",
3302                first.clone(),
3303                DhtNetworkOperation::PublishAddressSet {
3304                    seq: sequence,
3305                    addresses: vec![(first.clone(), AddressType::Direct)],
3306                },
3307            ),
3308            (
3309                "/dht/address/2.0.0",
3310                second.clone(),
3311                DhtNetworkOperation::PublishAddressSetV2 {
3312                    record: SignedAddressRecord::sign(
3313                        publisher.transport.node_identity(),
3314                        sequence - 1,
3315                        vec![
3316                            TransportAddressRecord::from_multiaddr(
3317                                &second,
3318                                KnownReachability::Direct,
3319                            )
3320                            .unwrap()
3321                            .unwrap(),
3322                        ],
3323                    )
3324                    .unwrap(),
3325                },
3326            ),
3327            (
3328                "/dht/1.0.0",
3329                second.clone(),
3330                DhtNetworkOperation::PublishAddressSet {
3331                    seq: u64::MAX,
3332                    addresses: vec![(first.clone(), AddressType::Direct)],
3333                },
3334            ),
3335        ];
3336        for (topic, expected, operation) in operations {
3337            let message = DhtNetworkMessage {
3338                message_id: "reconnect-publication".into(),
3339                source: owner,
3340                target: None,
3341                message_type: DhtMessageType::Request,
3342                payload: operation,
3343                result: None,
3344                timestamp: 1,
3345                ttl: 10,
3346                hop_count: 0,
3347                signed_records: Vec::new(),
3348            };
3349            publisher
3350                .transport
3351                .send_message(
3352                    receiver.peer_id(),
3353                    topic,
3354                    postcard::to_stdvec(&message).unwrap(),
3355                )
3356                .await
3357                .unwrap();
3358            timeout(Duration::from_secs(2), async {
3359                loop {
3360                    let candidates = receiver.resolve_dial_candidates(&owner, &[], &saved).await;
3361                    if candidates == vec![(expected.clone(), AddressType::Direct)] {
3362                        break;
3363                    }
3364                    tokio::time::sleep(Duration::from_millis(10)).await;
3365                }
3366            })
3367            .await
3368            .expect("reconnect must use the published endpoint without the old cached endpoint");
3369        }
3370        // Explicit caller overrides still take priority.
3371        let explicit = receiver
3372            .resolve_dial_candidates(&owner, std::slice::from_ref(&old_address), &saved)
3373            .await;
3374        assert_eq!(explicit, vec![(old_address, AddressType::Unverified)]);
3375        receiver.stop().await.unwrap();
3376        publisher.stop().await.unwrap();
3377    }
3378
3379    #[tokio::test]
3380    async fn reconnect_uses_saved_addresses_without_an_owner_publication() {
3381        let node = P2PNode::new(create_test_node_config()).await.unwrap();
3382        let peer = PeerId::random();
3383        let saved: MultiAddr = "/ip4/9.9.9.9/udp/9001/quic".parse().unwrap();
3384        assert_eq!(
3385            node.dht_manager()
3386                .published_peer_addresses_for_dial_typed(&peer)
3387                .await,
3388            None
3389        );
3390        assert_eq!(
3391            node.resolve_dial_candidates(&peer, &[], std::slice::from_ref(&saved))
3392                .await,
3393            vec![(saved, AddressType::Unverified)]
3394        );
3395    }
3396
3397    #[tokio::test]
3398    async fn test_node_config_default() {
3399        let config = NodeConfig::default();
3400
3401        assert_eq!(config.listen_addrs().len(), 2); // IPv4 + IPv6
3402        assert_eq!(config.max_connections, 10000);
3403        assert_eq!(config.connection_timeout, Duration::from_secs(25));
3404        assert_eq!(
3405            config.close_group_cache_max_age,
3406            Some(Duration::from_secs(DEFAULT_CLOSE_GROUP_CACHE_MAX_AGE_SECS))
3407        );
3408    }
3409
3410    #[test]
3411    fn close_group_cache_builder_default_and_explicit_disable() {
3412        let defaulted = NodeConfig::builder().build().unwrap();
3413        assert_eq!(
3414            defaulted.close_group_cache_max_age,
3415            Some(Duration::from_secs(DEFAULT_CLOSE_GROUP_CACHE_MAX_AGE_SECS))
3416        );
3417
3418        let disabled = NodeConfig::builder()
3419            .close_group_cache_max_age(None)
3420            .build()
3421            .unwrap();
3422        assert_eq!(disabled.close_group_cache_max_age, None);
3423        let disabled_json = serde_json::to_string(&disabled).unwrap();
3424        let disabled_roundtrip: NodeConfig = serde_json::from_str(&disabled_json).unwrap();
3425        assert_eq!(disabled_roundtrip.close_group_cache_max_age, None);
3426
3427        let custom = NodeConfig::builder()
3428            .close_group_cache_max_age(Some(Duration::from_secs(120)))
3429            .build()
3430            .unwrap();
3431        assert_eq!(
3432            custom.close_group_cache_max_age,
3433            Some(Duration::from_secs(120))
3434        );
3435    }
3436
3437    #[test]
3438    fn old_serialized_config_gets_default_cache_max_age() {
3439        let mut value = serde_json::to_value(NodeConfig::default()).unwrap();
3440        value
3441            .as_object_mut()
3442            .unwrap()
3443            .remove("close_group_cache_max_age");
3444
3445        let decoded: NodeConfig = serde_json::from_value(value).unwrap();
3446        assert_eq!(
3447            decoded.close_group_cache_max_age,
3448            Some(Duration::from_secs(DEFAULT_CLOSE_GROUP_CACHE_MAX_AGE_SECS))
3449        );
3450    }
3451
3452    #[tokio::test]
3453    async fn test_dht_config_default() {
3454        let config = DHTConfig::default();
3455
3456        assert_eq!(config.k_value, 20);
3457        assert_eq!(config.alpha_value, 3);
3458        assert_eq!(config.refresh_interval, Duration::from_secs(600));
3459    }
3460
3461    #[test]
3462    fn test_connection_status_variants() {
3463        let connecting = ConnectionStatus::Connecting;
3464        let connected = ConnectionStatus::Connected;
3465        let disconnecting = ConnectionStatus::Disconnecting;
3466        let disconnected = ConnectionStatus::Disconnected;
3467        let failed = ConnectionStatus::Failed("test error".to_string());
3468
3469        assert_eq!(connecting, ConnectionStatus::Connecting);
3470        assert_eq!(connected, ConnectionStatus::Connected);
3471        assert_eq!(disconnecting, ConnectionStatus::Disconnecting);
3472        assert_eq!(disconnected, ConnectionStatus::Disconnected);
3473        assert_ne!(connecting, connected);
3474
3475        if let ConnectionStatus::Failed(msg) = failed {
3476            assert_eq!(msg, "test error");
3477        } else {
3478            panic!("Expected Failed status");
3479        }
3480    }
3481
3482    #[tokio::test]
3483    async fn test_node_creation() -> Result<()> {
3484        let config = create_test_node_config();
3485        let node = P2PNode::new(config).await?;
3486
3487        // PeerId is derived from the cryptographic identity (32-byte BLAKE3 hash)
3488        assert_eq!(node.peer_id().to_hex().len(), 64);
3489        assert!(!node.is_running());
3490        assert_eq!(node.peer_count().await, 0);
3491        assert!(node.connected_peers().await.is_empty());
3492
3493        Ok(())
3494    }
3495
3496    #[tokio::test]
3497    async fn test_node_lifecycle() -> Result<()> {
3498        let config = create_test_node_config();
3499        let node = P2PNode::new(config).await?;
3500
3501        // Initially not running
3502        assert!(!node.is_running());
3503
3504        // Start the node
3505        node.start().await?;
3506        assert!(node.is_running());
3507
3508        // Check listen addresses were set (at least one)
3509        let listen_addrs = node.listen_addrs().await;
3510        assert!(
3511            !listen_addrs.is_empty(),
3512            "Expected at least one listening address"
3513        );
3514
3515        // Stop the node
3516        node.stop().await?;
3517        assert!(!node.is_running());
3518
3519        Ok(())
3520    }
3521
3522    #[tokio::test]
3523    async fn close_group_cache_task_is_joined_on_stop() -> Result<()> {
3524        let cache_dir = tempfile::tempdir().unwrap();
3525        let mut config = create_test_node_config();
3526        config.close_group_cache_dir = Some(cache_dir.path().to_path_buf());
3527        let node = P2PNode::new(config).await?;
3528
3529        node.start().await?;
3530        assert!(node.close_group_cache_save_handle.lock().await.is_some());
3531
3532        node.stop().await?;
3533        assert!(node.close_group_cache_save_handle.lock().await.is_none());
3534
3535        Ok(())
3536    }
3537
3538    #[tokio::test]
3539    async fn test_peer_connection() -> Result<()> {
3540        let config1 = create_test_node_config();
3541        let config2 = create_test_node_config();
3542
3543        let node1 = P2PNode::new(config1).await?;
3544        let node2 = P2PNode::new(config2).await?;
3545
3546        node1.start().await?;
3547        node2.start().await?;
3548
3549        let node2_addr = node2
3550            .listen_addrs()
3551            .await
3552            .into_iter()
3553            .find(|a| a.is_ipv4())
3554            .ok_or_else(|| {
3555                P2PError::Network(crate::error::NetworkError::InvalidAddress(
3556                    "Node 2 did not expose an IPv4 listen address".into(),
3557                ))
3558            })?;
3559
3560        // Connect to a real peer (unsigned — no node_identity configured).
3561        // connect_peer returns a transport-level channel ID (String), not a PeerId.
3562        let channel_id = node1.connect_peer(&node2_addr).await?;
3563
3564        // Unauthenticated connections don't appear in the app-level peer maps.
3565        // Verify transport-level tracking via is_connection_active / peers map.
3566        assert!(node1.is_connection_active(&channel_id).await);
3567
3568        // Get peer info from the transport-level peers map (keyed by channel ID)
3569        let peer_info = node1.transport.peer_info_by_channel(&channel_id).await;
3570        assert!(peer_info.is_some());
3571        let info = peer_info.expect("Peer info should exist after connect");
3572        assert_eq!(info.channel_id, channel_id);
3573        assert_eq!(info.status, ConnectionStatus::Connected);
3574        assert!(info.protocols.contains(&"p2p-foundation/1.0".to_string()));
3575
3576        // Disconnect the channel
3577        node1.remove_channel(&channel_id).await;
3578        assert!(!node1.is_connection_active(&channel_id).await);
3579
3580        node1.stop().await?;
3581        node2.stop().await?;
3582
3583        Ok(())
3584    }
3585
3586    /// A send failure confined to one stream must not drop a channel whose
3587    /// QUIC connection is still open, or the next send redials a live peer.
3588    /// A stale-channel failure, or a channel with no open connection behind
3589    /// it, still removes the channel.
3590    #[tokio::test]
3591    async fn send_failure_keeps_channel_only_while_connection_is_open() -> Result<()> {
3592        let node1 = P2PNode::new(create_test_node_config()).await?;
3593        let node2 = P2PNode::new(create_test_node_config()).await?;
3594        node1.start().await?;
3595        node2.start().await?;
3596
3597        let node2_addr = node2
3598            .listen_addrs()
3599            .await
3600            .into_iter()
3601            .find(|a| a.is_ipv4())
3602            .ok_or_else(|| {
3603                P2PError::Network(crate::error::NetworkError::InvalidAddress(
3604                    "Node 2 did not expose an IPv4 listen address".into(),
3605                ))
3606            })?;
3607        let channel_id = node1.connect_peer(&node2_addr).await?;
3608
3609        let stalled_write = P2PError::Transport(crate::error::TransportError::SendFailed {
3610            kind: crate::error::SendFailureKind::WriteProgressTimeout,
3611            reason: "stream write made no progress".into(),
3612        });
3613        assert!(
3614            !node1
3615                .transport
3616                .remove_channel_after_send_failure(&channel_id, &stalled_write)
3617                .await,
3618            "a stalled stream on an open connection must keep the channel"
3619        );
3620        assert!(
3621            node1
3622                .transport
3623                .peer_info_by_channel(&channel_id)
3624                .await
3625                .is_some()
3626        );
3627
3628        let stale = P2PError::Network(crate::error::NetworkError::PeerNotFound(
3629            channel_id.clone().into(),
3630        ));
3631        assert!(
3632            node1
3633                .transport
3634                .remove_channel_after_send_failure(&channel_id, &stale)
3635                .await,
3636            "a stale-channel failure must remove the channel"
3637        );
3638        assert!(
3639            node1
3640                .transport
3641                .peer_info_by_channel(&channel_id)
3642                .await
3643                .is_none()
3644        );
3645
3646        assert!(
3647            node1
3648                .transport
3649                .remove_channel_after_send_failure(UNCONNECTED_CHANNEL_ID, &stalled_write)
3650                .await,
3651            "a channel with no open connection must be removed whatever the failure"
3652        );
3653
3654        node1.stop().await?;
3655        node2.stop().await?;
3656        Ok(())
3657    }
3658
3659    #[tokio::test]
3660    async fn test_connect_peer_rejects_tcp_multiaddr() -> Result<()> {
3661        let config = create_test_node_config();
3662        let node = P2PNode::new(config).await?;
3663
3664        let tcp_addr: MultiAddr = "/ip4/127.0.0.1/tcp/1".parse().unwrap();
3665        let result = node.connect_peer(&tcp_addr).await;
3666
3667        assert!(
3668            matches!(
3669                result,
3670                Err(P2PError::Network(
3671                    crate::error::NetworkError::InvalidAddress(_)
3672                ))
3673            ),
3674            "TCP multiaddrs should be rejected before a QUIC dial is attempted, got: {:?}",
3675            result
3676        );
3677
3678        Ok(())
3679    }
3680
3681    // TODO(windows): Investigate QUIC connection issues on Windows CI
3682    // This test consistently fails on Windows GitHub Actions runners with
3683    // "All connect attempts failed" even with IPv4-only config, long delays,
3684    // and multiple retry attempts. The underlying saorsa-transport library may have
3685    // issues on Windows that need investigation.
3686    // See: https://github.com/WithAutonomi/saorsa-core/issues/TBD
3687    #[cfg_attr(target_os = "windows", ignore)]
3688    #[tokio::test]
3689    async fn test_event_subscription() -> Result<()> {
3690        // PeerConnected/PeerDisconnected only fire for authenticated peers
3691        // (nodes with node_identity that send signed messages).
3692        // Configure both nodes with identities so the event subscription test works.
3693        let identity1 =
3694            Arc::new(NodeIdentity::generate().expect("should generate identity for test node1"));
3695        let identity2 =
3696            Arc::new(NodeIdentity::generate().expect("should generate identity for test node2"));
3697
3698        let mut config1 = create_test_node_config();
3699        config1.ipv6 = false;
3700        config1.node_identity = Some(identity1);
3701
3702        let node2_peer_id = *identity2.peer_id();
3703        let mut config2 = create_test_node_config();
3704        config2.ipv6 = false;
3705        config2.node_identity = Some(identity2);
3706
3707        let node1 = P2PNode::new(config1).await?;
3708        let node2 = P2PNode::new(config2).await?;
3709
3710        node1.start().await?;
3711        node2.start().await?;
3712
3713        tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
3714
3715        // Subscribe to node2's events (node2 will receive the signed message)
3716        let mut events = node2.subscribe_events();
3717
3718        let node2_addr = node2.local_addr().ok_or_else(|| {
3719            P2PError::Network(crate::error::NetworkError::ProtocolError(
3720                "No listening address".to_string().into(),
3721            ))
3722        })?;
3723
3724        // Connect node1 → node2
3725        let mut channel_id = None;
3726        for attempt in 0..3 {
3727            if attempt > 0 {
3728                tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
3729            }
3730            match timeout(Duration::from_secs(2), node1.connect_peer(&node2_addr)).await {
3731                Ok(Ok(id)) => {
3732                    channel_id = Some(id);
3733                    break;
3734                }
3735                Ok(Err(_)) | Err(_) => continue,
3736            }
3737        }
3738        let channel_id = channel_id.expect("Failed to connect after 3 attempts");
3739
3740        // Wait for identity exchange to complete via wait_for_peer_identity.
3741        let target_peer_id = node1
3742            .wait_for_peer_identity(&channel_id, Duration::from_secs(2))
3743            .await?;
3744        assert_eq!(target_peer_id, node2_peer_id);
3745
3746        // node1 sends a signed message → node2 authenticates → PeerConnected fires on node2
3747        node1
3748            .send_message(&target_peer_id, "test-topic", b"hello".to_vec(), &[])
3749            .await?;
3750
3751        // Check for PeerConnected event on node2
3752        let event = timeout(Duration::from_secs(2), async {
3753            loop {
3754                match events.recv().await {
3755                    Ok(P2PEvent::PeerConnected(id, _)) => return Ok(id),
3756                    Ok(P2PEvent::Message { .. }) => continue, // skip messages
3757                    Ok(_) => continue,
3758                    Err(e) => return Err(e),
3759                }
3760            }
3761        })
3762        .await;
3763        assert!(event.is_ok(), "Should receive PeerConnected event");
3764        let connected_peer_id = event.expect("Timed out").expect("Channel error");
3765        // The connected peer ID should be node1's app-level ID (a valid PeerId)
3766        assert!(
3767            connected_peer_id.0.iter().any(|&b| b != 0),
3768            "PeerConnected should carry a non-zero peer ID"
3769        );
3770
3771        node1.stop().await?;
3772        node2.stop().await?;
3773
3774        Ok(())
3775    }
3776
3777    // TODO(windows): Same QUIC connection issues as test_event_subscription
3778    #[cfg_attr(target_os = "windows", ignore)]
3779    #[tokio::test]
3780    async fn test_message_sending() -> Result<()> {
3781        // Create two nodes (IPv4-only loopback)
3782        let mut config1 = create_test_node_config();
3783        config1.ipv6 = false;
3784        let node1 = P2PNode::new(config1).await?;
3785        node1.start().await?;
3786
3787        let mut config2 = create_test_node_config();
3788        config2.ipv6 = false;
3789        let node2 = P2PNode::new(config2).await?;
3790        node2.start().await?;
3791
3792        // Wait a bit for nodes to start listening
3793        tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
3794
3795        // Get actual listening address of node2
3796        let node2_addr = node2.local_addr().ok_or_else(|| {
3797            P2PError::Network(crate::error::NetworkError::ProtocolError(
3798                "No listening address".to_string().into(),
3799            ))
3800        })?;
3801
3802        // Connect node1 to node2
3803        let channel_id =
3804            match timeout(Duration::from_millis(500), node1.connect_peer(&node2_addr)).await {
3805                Ok(res) => res?,
3806                Err(_) => return Err(P2PError::Network(NetworkError::Timeout)),
3807            };
3808
3809        // Wait for identity exchange via wait_for_peer_identity.
3810        let target_peer_id = node1
3811            .wait_for_peer_identity(&channel_id, Duration::from_secs(2))
3812            .await?;
3813        assert_eq!(target_peer_id, node2.peer_id().clone());
3814
3815        // Send a message
3816        let message_data = b"Hello, peer!".to_vec();
3817        let result = match timeout(
3818            Duration::from_millis(500),
3819            node1.send_message(&target_peer_id, "test-protocol", message_data, &[]),
3820        )
3821        .await
3822        {
3823            Ok(res) => res,
3824            Err(_) => return Err(P2PError::Network(NetworkError::Timeout)),
3825        };
3826        // For now, we'll just check that we don't get a "not connected" error
3827        // The actual send might fail due to no handler on the other side
3828        if let Err(e) = &result {
3829            assert!(!e.to_string().contains("not connected"), "Got error: {}", e);
3830        }
3831
3832        // Try to send to non-existent peer
3833        let non_existent_peer = PeerId::from_bytes([0xFFu8; 32]);
3834        let result = node1
3835            .send_message(&non_existent_peer, "test-protocol", vec![], &[])
3836            .await;
3837        assert!(result.is_err(), "Sending to non-existent peer should fail");
3838
3839        node1.stop().await?;
3840        node2.stop().await?;
3841
3842        Ok(())
3843    }
3844
3845    #[tokio::test]
3846    async fn test_remote_mcp_operations() -> Result<()> {
3847        let config = create_test_node_config();
3848        let node = P2PNode::new(config).await?;
3849
3850        // MCP removed; test reduced to simple start/stop
3851        node.start().await?;
3852        node.stop().await?;
3853        Ok(())
3854    }
3855
3856    #[tokio::test]
3857    async fn test_health_check() -> Result<()> {
3858        let config = create_test_node_config();
3859        let node = P2PNode::new(config).await?;
3860
3861        // Health check should pass with no connections
3862        let result = node.health_check().await;
3863        assert!(result.is_ok());
3864
3865        // Note: We're not actually connecting to real peers here
3866        // since that would require running bootstrap nodes.
3867        // The health check should still pass with no connections.
3868
3869        Ok(())
3870    }
3871
3872    #[tokio::test]
3873    async fn test_node_uptime() -> Result<()> {
3874        let config = create_test_node_config();
3875        let node = P2PNode::new(config).await?;
3876
3877        let uptime1 = node.uptime();
3878        assert!(uptime1 >= Duration::from_secs(0));
3879
3880        // Wait a bit
3881        tokio::time::sleep(Duration::from_millis(10)).await;
3882
3883        let uptime2 = node.uptime();
3884        assert!(uptime2 > uptime1);
3885
3886        Ok(())
3887    }
3888
3889    #[tokio::test]
3890    async fn test_node_config_access() -> Result<()> {
3891        let config = create_test_node_config();
3892        let node = P2PNode::new(config).await?;
3893
3894        let node_config = node.config();
3895        assert_eq!(node_config.max_connections, 100);
3896        // MCP removed
3897
3898        Ok(())
3899    }
3900
3901    #[tokio::test]
3902    async fn test_mcp_server_access() -> Result<()> {
3903        let config = create_test_node_config();
3904        let _node = P2PNode::new(config).await?;
3905
3906        // MCP removed
3907        Ok(())
3908    }
3909
3910    #[tokio::test]
3911    async fn test_dht_access() -> Result<()> {
3912        let config = create_test_node_config();
3913        let node = P2PNode::new(config).await?;
3914
3915        // DHT is always available
3916        let _dht = node.dht();
3917
3918        Ok(())
3919    }
3920
3921    #[tokio::test]
3922    async fn test_node_config_builder() -> Result<()> {
3923        let bootstrap: MultiAddr = "/ip4/127.0.0.1/udp/9000/quic".parse().unwrap();
3924
3925        let config = NodeConfig::builder()
3926            .local(true)
3927            .ipv6(true)
3928            .bootstrap_peer(bootstrap)
3929            .connection_timeout(Duration::from_secs(15))
3930            .max_connections(200)
3931            .max_message_size(TEST_MAX_MESSAGE_SIZE)
3932            .build()?;
3933
3934        assert_eq!(config.listen_addrs().len(), 2); // IPv4 + IPv6
3935        assert!(config.local);
3936        assert!(config.ipv6);
3937        assert_eq!(config.bootstrap_peers.len(), 1);
3938        assert_eq!(config.connection_timeout, Duration::from_secs(15));
3939        assert_eq!(config.max_connections, 200);
3940        assert_eq!(config.max_message_size, Some(TEST_MAX_MESSAGE_SIZE));
3941        assert!(config.allow_loopback); // auto-enabled by local(true)
3942
3943        Ok(())
3944    }
3945
3946    #[tokio::test]
3947    async fn test_bootstrap_peers() -> Result<()> {
3948        let mut config = create_test_node_config();
3949        config.bootstrap_peers = vec![
3950            crate::MultiAddr::from_ipv4(std::net::Ipv4Addr::LOCALHOST, 9200),
3951            crate::MultiAddr::from_ipv4(std::net::Ipv4Addr::LOCALHOST, 9201),
3952        ];
3953
3954        let node = P2PNode::new(config).await?;
3955
3956        // Start node (which attempts to connect to bootstrap peers)
3957        node.start().await?;
3958
3959        // In a test environment, bootstrap peers may not be available
3960        // The test verifies the node starts correctly with bootstrap configuration
3961        // Peer count may include local/internal tracking, so we just verify it's reasonable
3962        let _peer_count = node.peer_count().await;
3963
3964        node.stop().await?;
3965        Ok(())
3966    }
3967
3968    #[tokio::test]
3969    async fn test_peer_info_structure() {
3970        let peer_info = PeerInfo {
3971            channel_id: "test_peer".to_string(),
3972            addresses: vec!["/ip4/127.0.0.1/tcp/9000".parse::<MultiAddr>().unwrap()],
3973            connected_at: Instant::now(),
3974            last_seen: Instant::now(),
3975            status: ConnectionStatus::Connected,
3976            protocols: vec!["test-protocol".to_string()],
3977            heartbeat_count: 0,
3978        };
3979
3980        assert_eq!(peer_info.channel_id, "test_peer");
3981        assert_eq!(peer_info.addresses.len(), 1);
3982        assert_eq!(peer_info.status, ConnectionStatus::Connected);
3983        assert_eq!(peer_info.protocols.len(), 1);
3984    }
3985
3986    #[tokio::test]
3987    async fn test_serialization() -> Result<()> {
3988        // Test that configs can be serialized/deserialized
3989        let config = create_test_node_config();
3990        let serialized = serde_json::to_string(&config)?;
3991        let deserialized: NodeConfig = serde_json::from_str(&serialized)?;
3992
3993        assert_eq!(config.local, deserialized.local);
3994        assert_eq!(config.port, deserialized.port);
3995        assert_eq!(config.ipv6, deserialized.ipv6);
3996        assert_eq!(config.bootstrap_peers, deserialized.bootstrap_peers);
3997
3998        Ok(())
3999    }
4000
4001    #[tokio::test]
4002    async fn test_get_channel_id_by_address_found() -> Result<()> {
4003        let config = create_test_node_config();
4004        let node = P2PNode::new(config).await?;
4005
4006        // Manually insert a peer for testing
4007        let test_channel_id = "peer_test_123".to_string();
4008        let test_address = "192.168.1.100:9000";
4009        let test_multiaddr = MultiAddr::quic(test_address.parse().unwrap());
4010
4011        let peer_info = PeerInfo {
4012            channel_id: test_channel_id.clone(),
4013            addresses: vec![test_multiaddr],
4014            connected_at: Instant::now(),
4015            last_seen: Instant::now(),
4016            status: ConnectionStatus::Connected,
4017            protocols: vec!["test-protocol".to_string()],
4018            heartbeat_count: 0,
4019        };
4020
4021        node.transport
4022            .inject_peer(test_channel_id.clone(), peer_info)
4023            .await;
4024
4025        // Test: Find channel by address
4026        let lookup_addr = MultiAddr::quic(test_address.parse().unwrap());
4027        let found_channel_id = node.get_channel_id_by_address(&lookup_addr).await;
4028        assert_eq!(found_channel_id, Some(test_channel_id));
4029
4030        Ok(())
4031    }
4032
4033    #[tokio::test]
4034    async fn test_get_channel_id_by_address_not_found() -> Result<()> {
4035        let config = create_test_node_config();
4036        let node = P2PNode::new(config).await?;
4037
4038        // Test: Try to find a channel that doesn't exist
4039        let unknown_addr = MultiAddr::quic("192.168.1.200:9000".parse().unwrap());
4040        let result = node.get_channel_id_by_address(&unknown_addr).await;
4041        assert_eq!(result, None);
4042
4043        Ok(())
4044    }
4045
4046    #[tokio::test]
4047    async fn test_get_channel_id_by_address_invalid_format() -> Result<()> {
4048        let config = create_test_node_config();
4049        let node = P2PNode::new(config).await?;
4050
4051        // Test: Non-IP address should return None (no matching socket addr)
4052        let ble_addr = MultiAddr::new(crate::address::TransportAddr::Ble {
4053            mac: [0x02, 0x00, 0x00, 0x00, 0x00, 0x01],
4054            psm: 0x0025,
4055        });
4056        let result = node.get_channel_id_by_address(&ble_addr).await;
4057        assert_eq!(result, None);
4058
4059        Ok(())
4060    }
4061
4062    #[tokio::test]
4063    async fn test_get_channel_id_by_address_multiple_peers() -> Result<()> {
4064        let config = create_test_node_config();
4065        let node = P2PNode::new(config).await?;
4066
4067        // Add multiple peers with different addresses
4068        let peer1_id = "peer_1".to_string();
4069        let peer1_addr_str = "192.168.1.101:9001";
4070        let peer1_multiaddr = MultiAddr::quic(peer1_addr_str.parse().unwrap());
4071
4072        let peer2_id = "peer_2".to_string();
4073        let peer2_addr_str = "192.168.1.102:9002";
4074        let peer2_multiaddr = MultiAddr::quic(peer2_addr_str.parse().unwrap());
4075
4076        let peer1_info = PeerInfo {
4077            channel_id: peer1_id.clone(),
4078            addresses: vec![peer1_multiaddr],
4079            connected_at: Instant::now(),
4080            last_seen: Instant::now(),
4081            status: ConnectionStatus::Connected,
4082            protocols: vec!["test-protocol".to_string()],
4083            heartbeat_count: 0,
4084        };
4085
4086        let peer2_info = PeerInfo {
4087            channel_id: peer2_id.clone(),
4088            addresses: vec![peer2_multiaddr],
4089            connected_at: Instant::now(),
4090            last_seen: Instant::now(),
4091            status: ConnectionStatus::Connected,
4092            protocols: vec!["test-protocol".to_string()],
4093            heartbeat_count: 0,
4094        };
4095
4096        node.transport
4097            .inject_peer(peer1_id.clone(), peer1_info)
4098            .await;
4099        node.transport
4100            .inject_peer(peer2_id.clone(), peer2_info)
4101            .await;
4102
4103        // Test: Find each channel by their unique address
4104        let found_peer1 = node
4105            .get_channel_id_by_address(&MultiAddr::quic(peer1_addr_str.parse().unwrap()))
4106            .await;
4107        let found_peer2 = node
4108            .get_channel_id_by_address(&MultiAddr::quic(peer2_addr_str.parse().unwrap()))
4109            .await;
4110
4111        assert_eq!(found_peer1, Some(peer1_id));
4112        assert_eq!(found_peer2, Some(peer2_id));
4113
4114        Ok(())
4115    }
4116
4117    #[tokio::test]
4118    async fn test_list_active_connections_empty() -> Result<()> {
4119        let config = create_test_node_config();
4120        let node = P2PNode::new(config).await?;
4121
4122        // Test: No connections initially
4123        let connections = node.list_active_connections().await;
4124        assert!(connections.is_empty());
4125
4126        Ok(())
4127    }
4128
4129    #[tokio::test]
4130    async fn test_list_active_connections_with_peers() -> Result<()> {
4131        let config = create_test_node_config();
4132        let node = P2PNode::new(config).await?;
4133
4134        // Add multiple peers
4135        let peer1_id = "peer_1".to_string();
4136        let peer1_addrs = vec![
4137            MultiAddr::quic("192.168.1.101:9001".parse().unwrap()),
4138            MultiAddr::quic("192.168.1.101:9002".parse().unwrap()),
4139        ];
4140
4141        let peer2_id = "peer_2".to_string();
4142        let peer2_addrs = vec![MultiAddr::quic("192.168.1.102:9003".parse().unwrap())];
4143
4144        let peer1_info = PeerInfo {
4145            channel_id: peer1_id.clone(),
4146            addresses: peer1_addrs.clone(),
4147            connected_at: Instant::now(),
4148            last_seen: Instant::now(),
4149            status: ConnectionStatus::Connected,
4150            protocols: vec!["test-protocol".to_string()],
4151            heartbeat_count: 0,
4152        };
4153
4154        let peer2_info = PeerInfo {
4155            channel_id: peer2_id.clone(),
4156            addresses: peer2_addrs.clone(),
4157            connected_at: Instant::now(),
4158            last_seen: Instant::now(),
4159            status: ConnectionStatus::Connected,
4160            protocols: vec!["test-protocol".to_string()],
4161            heartbeat_count: 0,
4162        };
4163
4164        node.transport
4165            .inject_peer(peer1_id.clone(), peer1_info)
4166            .await;
4167        node.transport
4168            .inject_peer(peer2_id.clone(), peer2_info)
4169            .await;
4170
4171        // Also add to active_connections (list_active_connections iterates over this)
4172        node.transport
4173            .inject_active_connection(peer1_id.clone())
4174            .await;
4175        node.transport
4176            .inject_active_connection(peer2_id.clone())
4177            .await;
4178
4179        // Test: List all active connections
4180        let connections = node.list_active_connections().await;
4181        assert_eq!(connections.len(), 2);
4182
4183        // Verify peer1 and peer2 are in the list
4184        let peer1_conn = connections.iter().find(|(id, _)| id == &peer1_id);
4185        let peer2_conn = connections.iter().find(|(id, _)| id == &peer2_id);
4186
4187        assert!(peer1_conn.is_some());
4188        assert!(peer2_conn.is_some());
4189
4190        // Verify addresses match
4191        assert_eq!(peer1_conn.unwrap().1, peer1_addrs);
4192        assert_eq!(peer2_conn.unwrap().1, peer2_addrs);
4193
4194        Ok(())
4195    }
4196
4197    #[tokio::test]
4198    async fn test_remove_channel_success() -> Result<()> {
4199        let config = create_test_node_config();
4200        let node = P2PNode::new(config).await?;
4201
4202        // Add a peer
4203        let channel_id = "peer_to_remove".to_string();
4204        let channel_peer_id = PeerId::from_name(&channel_id);
4205        let peer_info = PeerInfo {
4206            channel_id: channel_id.clone(),
4207            addresses: vec![MultiAddr::quic("192.168.1.100:9000".parse().unwrap())],
4208            connected_at: Instant::now(),
4209            last_seen: Instant::now(),
4210            status: ConnectionStatus::Connected,
4211            protocols: vec!["test-protocol".to_string()],
4212            heartbeat_count: 0,
4213        };
4214
4215        node.transport
4216            .inject_peer(channel_id.clone(), peer_info)
4217            .await;
4218        node.transport
4219            .inject_peer_to_channel(channel_peer_id, channel_id.clone())
4220            .await;
4221
4222        // Verify peer exists
4223        assert!(node.is_peer_connected(&channel_peer_id).await);
4224
4225        // Remove the channel
4226        let removed = node.remove_channel(&channel_id).await;
4227        assert!(removed);
4228
4229        // Verify peer no longer exists
4230        assert!(!node.is_peer_connected(&channel_peer_id).await);
4231
4232        Ok(())
4233    }
4234
4235    #[tokio::test]
4236    async fn test_remove_channel_nonexistent() -> Result<()> {
4237        let config = create_test_node_config();
4238        let node = P2PNode::new(config).await?;
4239
4240        // Try to remove a channel that doesn't exist
4241        let removed = node.remove_channel("nonexistent_peer").await;
4242        assert!(!removed);
4243
4244        Ok(())
4245    }
4246
4247    #[tokio::test]
4248    async fn test_is_peer_connected() -> Result<()> {
4249        let config = create_test_node_config();
4250        let node = P2PNode::new(config).await?;
4251
4252        let channel_id = "test_peer".to_string();
4253        let channel_peer_id = PeerId::from_name(&channel_id);
4254
4255        // Initially not connected
4256        assert!(!node.is_peer_connected(&channel_peer_id).await);
4257
4258        // Add peer
4259        let peer_info = PeerInfo {
4260            channel_id: channel_id.clone(),
4261            addresses: vec![MultiAddr::quic("192.168.1.100:9000".parse().unwrap())],
4262            connected_at: Instant::now(),
4263            last_seen: Instant::now(),
4264            status: ConnectionStatus::Connected,
4265            protocols: vec!["test-protocol".to_string()],
4266            heartbeat_count: 0,
4267        };
4268
4269        node.transport
4270            .inject_peer(channel_id.clone(), peer_info)
4271            .await;
4272        node.transport
4273            .inject_peer_to_channel(channel_peer_id, channel_id.clone())
4274            .await;
4275
4276        // Now connected
4277        assert!(node.is_peer_connected(&channel_peer_id).await);
4278
4279        // Remove channel
4280        node.remove_channel(&channel_id).await;
4281
4282        // No longer connected
4283        assert!(!node.is_peer_connected(&channel_peer_id).await);
4284
4285        Ok(())
4286    }
4287
4288    #[test]
4289    fn test_normalize_ipv6_wildcard() {
4290        use std::net::{IpAddr, Ipv6Addr, SocketAddr};
4291
4292        let wildcard = SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 8080);
4293        let normalized = normalize_wildcard_to_loopback(wildcard);
4294
4295        assert_eq!(normalized.ip(), IpAddr::V6(Ipv6Addr::LOCALHOST));
4296        assert_eq!(normalized.port(), 8080);
4297    }
4298
4299    #[test]
4300    fn test_normalize_ipv4_wildcard() {
4301        use std::net::{IpAddr, Ipv4Addr, SocketAddr};
4302
4303        let wildcard = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9000);
4304        let normalized = normalize_wildcard_to_loopback(wildcard);
4305
4306        assert_eq!(normalized.ip(), IpAddr::V4(Ipv4Addr::LOCALHOST));
4307        assert_eq!(normalized.port(), 9000);
4308    }
4309
4310    #[test]
4311    fn test_normalize_specific_address_unchanged() {
4312        let specific: std::net::SocketAddr = "192.168.1.100:3000".parse().unwrap();
4313        let normalized = normalize_wildcard_to_loopback(specific);
4314
4315        assert_eq!(normalized, specific);
4316    }
4317
4318    #[test]
4319    fn test_normalize_loopback_unchanged() {
4320        use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
4321
4322        let loopback_v6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 5000);
4323        let normalized_v6 = normalize_wildcard_to_loopback(loopback_v6);
4324        assert_eq!(normalized_v6, loopback_v6);
4325
4326        let loopback_v4 = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5000);
4327        let normalized_v4 = normalize_wildcard_to_loopback(loopback_v4);
4328        assert_eq!(normalized_v4, loopback_v4);
4329    }
4330
4331    // ---- parse_protocol_message regression tests ----
4332
4333    /// Get current Unix timestamp for tests
4334    fn current_timestamp() -> u64 {
4335        std::time::SystemTime::now()
4336            .duration_since(std::time::UNIX_EPOCH)
4337            .map(|d| d.as_secs())
4338            .unwrap_or(0)
4339    }
4340
4341    /// Helper to create a postcard-serialized unsigned WireMessage for tests
4342    fn make_wire_bytes(protocol: &str, data: Vec<u8>, from: &str, timestamp: u64) -> Vec<u8> {
4343        let msg = WireMessage {
4344            protocol: protocol.to_string(),
4345            data,
4346            from: PeerId::from_name(from),
4347            timestamp,
4348            user_agent: String::new(),
4349            public_key: Vec::new(),
4350            signature: Vec::new(),
4351        };
4352        postcard::to_stdvec(&msg).unwrap()
4353    }
4354
4355    /// Helper to create a postcard-serialized signed WireMessage for tests.
4356    fn make_signed_wire_bytes(
4357        identity: &NodeIdentity,
4358        protocol: &str,
4359        data: Vec<u8>,
4360        timestamp: u64,
4361    ) -> Vec<u8> {
4362        let from = *identity.peer_id();
4363        let user_agent = "test/1.0";
4364        let signable =
4365            postcard::to_stdvec(&(protocol, data.as_slice(), &from, timestamp, user_agent))
4366                .unwrap();
4367        let sig = identity.sign(&signable).expect("signing should succeed");
4368        let msg = WireMessage {
4369            protocol: protocol.to_string(),
4370            data,
4371            from,
4372            timestamp,
4373            user_agent: user_agent.to_string(),
4374            public_key: identity.public_key().as_bytes().to_vec(),
4375            signature: sig.as_bytes().to_vec(),
4376        };
4377        postcard::to_stdvec(&msg).unwrap()
4378    }
4379
4380    #[test]
4381    fn test_parse_protocol_message_uses_transport_peer_id_as_source() {
4382        // Regression: For unsigned messages, P2PEvent::Message.source must be the
4383        // transport peer ID, NOT the "from" field from the wire message.
4384        let transport_id = "abcdef0123456789";
4385        let logical_id = "spoofed-logical-id";
4386        let bytes = make_wire_bytes("test/v1", vec![1, 2, 3], logical_id, current_timestamp());
4387
4388        let parsed =
4389            parse_protocol_message(&bytes, transport_id).expect("valid message should parse");
4390
4391        // Unsigned message: no authenticated node ID
4392        assert!(parsed.authenticated_node_id.is_none());
4393
4394        match parsed.event {
4395            P2PEvent::Message {
4396                topic,
4397                source,
4398                transport_source,
4399                timestamp: _,
4400                data,
4401            } => {
4402                assert!(source.is_none(), "unsigned message source must be None");
4403                assert!(
4404                    transport_source.is_none(),
4405                    "non-socket transport source should not produce an IP transport address"
4406                );
4407                assert_eq!(topic, "test/v1");
4408                assert_eq!(data, vec![1u8, 2, 3]);
4409            }
4410            other => panic!("expected P2PEvent::Message, got {:?}", other),
4411        }
4412    }
4413
4414    #[test]
4415    fn test_parse_protocol_message_rejects_invalid_bytes() {
4416        // Random bytes that are not valid bincode should be rejected
4417        assert!(parse_protocol_message(b"not valid bincode", "peer-id").is_none());
4418    }
4419
4420    #[test]
4421    fn test_parse_protocol_message_rejects_truncated_message() {
4422        // A truncated bincode message should fail to deserialize
4423        let full_bytes = make_wire_bytes("test/v1", vec![1, 2, 3], "sender", current_timestamp());
4424        let truncated = &full_bytes[..full_bytes.len() / 2];
4425        assert!(parse_protocol_message(truncated, "peer-id").is_none());
4426    }
4427
4428    #[test]
4429    fn test_parse_protocol_message_empty_payload() {
4430        let bytes = make_wire_bytes("ping", vec![], "sender", current_timestamp());
4431
4432        let parsed = parse_protocol_message(&bytes, "transport-peer")
4433            .expect("valid message with empty data should parse");
4434
4435        match parsed.event {
4436            P2PEvent::Message { data, .. } => assert!(data.is_empty()),
4437            other => panic!("expected P2PEvent::Message, got {:?}", other),
4438        }
4439    }
4440
4441    #[test]
4442    fn test_parse_protocol_message_records_ip_transport_source() {
4443        let bytes = make_wire_bytes("ping", vec![1], "sender", current_timestamp());
4444
4445        let parsed =
4446            parse_protocol_message(&bytes, "192.168.1.2:4567").expect("valid message should parse");
4447
4448        match parsed.event {
4449            P2PEvent::Message {
4450                transport_source, ..
4451            } => {
4452                assert_eq!(
4453                    transport_source,
4454                    Some(MultiAddr::quic("192.168.1.2:4567".parse().unwrap()))
4455                );
4456            }
4457            other => panic!("expected P2PEvent::Message, got {:?}", other),
4458        }
4459    }
4460
4461    #[test]
4462    fn test_parse_protocol_message_preserves_binary_payload() {
4463        // Verify that arbitrary byte values (including 0xFF, 0x00) survive round-trip
4464        let payload: Vec<u8> = (0..=255).collect();
4465        let bytes = make_wire_bytes("binary/v1", payload.clone(), "sender", current_timestamp());
4466
4467        let parsed = parse_protocol_message(&bytes, "peer-id")
4468            .expect("valid message with full byte range should parse");
4469
4470        match parsed.event {
4471            P2PEvent::Message { data, topic, .. } => {
4472                assert_eq!(topic, "binary/v1");
4473                assert_eq!(
4474                    data, payload,
4475                    "payload must survive bincode round-trip exactly"
4476                );
4477            }
4478            other => panic!("expected P2PEvent::Message, got {:?}", other),
4479        }
4480    }
4481
4482    #[test]
4483    fn test_parse_signed_message_verifies_and_uses_node_id() {
4484        let identity = NodeIdentity::generate().expect("should generate identity");
4485        let protocol = "test/signed";
4486        let data: Vec<u8> = vec![10, 20, 30];
4487        // The `from` field must match the PeerId derived from the public key.
4488        let from = *identity.peer_id();
4489        let timestamp = current_timestamp();
4490        let user_agent = "test/1.0";
4491
4492        // Compute signable bytes the same way create_protocol_message does
4493        let signable =
4494            postcard::to_stdvec(&(protocol, data.as_slice(), &from, timestamp, user_agent))
4495                .unwrap();
4496        let sig = identity.sign(&signable).expect("signing should succeed");
4497
4498        let msg = WireMessage {
4499            protocol: protocol.to_string(),
4500            data: data.clone(),
4501            from,
4502            timestamp,
4503            user_agent: user_agent.to_string(),
4504            public_key: identity.public_key().as_bytes().to_vec(),
4505            signature: sig.as_bytes().to_vec(),
4506        };
4507        let bytes = postcard::to_stdvec(&msg).unwrap();
4508
4509        let parsed =
4510            parse_protocol_message(&bytes, "transport-xyz").expect("signed message should parse");
4511
4512        let expected_peer_id = *identity.peer_id();
4513        assert_eq!(
4514            parsed.authenticated_node_id.as_ref(),
4515            Some(&expected_peer_id)
4516        );
4517
4518        match parsed.event {
4519            P2PEvent::Message { source, .. } => {
4520                assert_eq!(
4521                    source.as_ref(),
4522                    Some(&expected_peer_id),
4523                    "source should be the verified PeerId"
4524                );
4525            }
4526            other => panic!("expected P2PEvent::Message, got {:?}", other),
4527        }
4528    }
4529
4530    #[test]
4531    fn test_parse_message_with_bad_signature_is_rejected() {
4532        let identity = NodeIdentity::generate().expect("should generate identity");
4533        let protocol = "test/bad-sig";
4534        let data: Vec<u8> = vec![1, 2, 3];
4535        let from = *identity.peer_id();
4536        let timestamp = current_timestamp();
4537        let user_agent = "test/1.0";
4538
4539        // Sign correct signable bytes
4540        let signable =
4541            postcard::to_stdvec(&(protocol, data.as_slice(), &from, timestamp, user_agent))
4542                .unwrap();
4543        let sig = identity.sign(&signable).expect("signing should succeed");
4544
4545        // Tamper with the data (signature was over [1,2,3], not [99,99,99])
4546        let msg = WireMessage {
4547            protocol: protocol.to_string(),
4548            data: vec![99, 99, 99],
4549            from,
4550            timestamp,
4551            user_agent: user_agent.to_string(),
4552            public_key: identity.public_key().as_bytes().to_vec(),
4553            signature: sig.as_bytes().to_vec(),
4554        };
4555        let bytes = postcard::to_stdvec(&msg).unwrap();
4556
4557        assert!(
4558            parse_protocol_message(&bytes, "transport-xyz").is_none(),
4559            "message with bad signature should be rejected"
4560        );
4561    }
4562
4563    #[test]
4564    fn test_parse_message_with_mismatched_from_is_rejected() {
4565        let identity = NodeIdentity::generate().expect("should generate identity");
4566        let protocol = "test/from-mismatch";
4567        let data: Vec<u8> = vec![1, 2, 3];
4568        // Use a `from` field that does NOT match the public key's PeerId.
4569        let fake_from = PeerId::from_bytes([0xDE; 32]);
4570        let timestamp = current_timestamp();
4571        let user_agent = "test/1.0";
4572
4573        let signable =
4574            postcard::to_stdvec(&(protocol, data.as_slice(), &fake_from, timestamp, user_agent))
4575                .unwrap();
4576        let sig = identity.sign(&signable).expect("signing should succeed");
4577
4578        let msg = WireMessage {
4579            protocol: protocol.to_string(),
4580            data,
4581            from: fake_from,
4582            timestamp,
4583            user_agent: user_agent.to_string(),
4584            public_key: identity.public_key().as_bytes().to_vec(),
4585            signature: sig.as_bytes().to_vec(),
4586        };
4587        let bytes = postcard::to_stdvec(&msg).unwrap();
4588
4589        assert!(
4590            parse_protocol_message(&bytes, "transport-xyz").is_none(),
4591            "message with mismatched from field should be rejected"
4592        );
4593    }
4594
4595    #[test]
4596    fn test_parse_protocol_message_accepts_arbitrary_timestamps() {
4597        // Clock skew between peers must not drop messages.
4598        // Regression: previously ±5 min tolerance silently rejected all
4599        // traffic when client and node clocks differed.
4600        let payload = vec![1, 2, 3];
4601
4602        // 10 hours in the past
4603        let old_ts = current_timestamp().saturating_sub(36_000);
4604        let old_bytes = make_wire_bytes("test/old", payload.clone(), "sender", old_ts);
4605        assert!(
4606            parse_protocol_message(&old_bytes, "peer-id").is_some(),
4607            "should accept unsigned message with timestamp 10h in the past"
4608        );
4609
4610        // 10 hours in the future
4611        let future_ts = current_timestamp().saturating_add(36_000);
4612        let future_bytes = make_wire_bytes("test/future", payload.clone(), "sender", future_ts);
4613        assert!(
4614            parse_protocol_message(&future_bytes, "peer-id").is_some(),
4615            "should accept unsigned message with timestamp 10h in the future"
4616        );
4617
4618        // Signed messages must take the same path: timestamp remains part of the
4619        // signed bytes for integrity, but is not used for wall-clock rejection.
4620        let identity = NodeIdentity::generate().expect("should generate identity");
4621        let signed_old =
4622            make_signed_wire_bytes(&identity, "test/signed-old", payload.clone(), old_ts);
4623        assert!(
4624            parse_protocol_message(&signed_old, "transport-xyz").is_some(),
4625            "should accept signed message with timestamp 10h in the past"
4626        );
4627
4628        let signed_future =
4629            make_signed_wire_bytes(&identity, "test/signed-future", payload, future_ts);
4630        assert!(
4631            parse_protocol_message(&signed_future, "transport-xyz").is_some(),
4632            "should accept signed message with timestamp 10h in the future"
4633        );
4634    }
4635
4636    #[test]
4637    fn test_parse_protocol_message_exposes_timestamp_on_event() {
4638        // After removing the wall-clock skew gate, the signed timestamp must
4639        // remain reachable on `P2PEvent::Message` so application-layer handlers
4640        // can implement freshness / replay defense.
4641        let ts: u64 = 1_234_567_890;
4642        let bytes = make_wire_bytes("test/ts", vec![9, 9, 9], "sender", ts);
4643        let parsed = parse_protocol_message(&bytes, "peer-id").expect("valid message should parse");
4644        match parsed.event {
4645            P2PEvent::Message { timestamp, .. } => {
4646                assert_eq!(timestamp, ts, "P2PEvent::Message.timestamp must round-trip");
4647            }
4648            other => panic!("expected P2PEvent::Message, got {:?}", other),
4649        }
4650    }
4651
4652    #[test]
4653    fn test_signed_message_timestamp_is_signature_covered() {
4654        // Sign once, mutate only the timestamp, assert rejection. This is the
4655        // only timestamp property still enforced by `parse_protocol_message`
4656        // after the wall-clock gate was removed: signature integrity.
4657        let identity = NodeIdentity::generate().expect("should generate identity");
4658        let ts: u64 = 1_700_000_000;
4659        let signed = make_signed_wire_bytes(&identity, "test/sig", vec![1, 2, 3], ts);
4660
4661        // Sanity: unmodified bytes parse and authenticate.
4662        let parsed = parse_protocol_message(&signed, "transport-xyz")
4663            .expect("unmodified signed message should parse");
4664        assert!(parsed.authenticated_node_id.is_some());
4665
4666        // Now tamper with just the timestamp on the wire and re-serialize.
4667        let mut tampered: WireMessage =
4668            postcard::from_bytes(&signed).expect("signed bytes must deserialize");
4669        tampered.timestamp = ts.wrapping_add(1);
4670        let tampered_bytes = postcard::to_stdvec(&tampered).expect("re-serialize");
4671
4672        assert!(
4673            parse_protocol_message(&tampered_bytes, "transport-xyz").is_none(),
4674            "timestamp-only mutation on a signed message must fail signature verification"
4675        );
4676    }
4677
4678    fn snapshot_peer(addr: &str) -> SnapshotPeer {
4679        SnapshotPeer {
4680            peer_id: PeerId::random(),
4681            addresses: vec![addr.parse().expect("valid multiaddr")],
4682        }
4683    }
4684
4685    #[test]
4686    fn snapshot_dial_sets_skips_self() {
4687        let self_id = PeerId::random();
4688        let mut peers = vec![snapshot_peer("/ip4/10.0.0.1/udp/9000/quic")];
4689        peers[0].peer_id = self_id;
4690        let mut seen = HashSet::new();
4691
4692        assert!(snapshot_dial_sets(&peers, &self_id, &mut seen).is_empty());
4693        assert!(seen.is_empty(), "self must not reserve an address");
4694    }
4695
4696    #[test]
4697    fn snapshot_dial_sets_skips_addresses_already_queued() {
4698        let self_id = PeerId::random();
4699        let queued = snapshot_peer("/ip4/10.0.0.2/udp/9000/quic");
4700        let fresh = snapshot_peer("/ip4/10.0.0.3/udp/9000/quic");
4701        let mut seen: HashSet<SocketAddr> = queued
4702            .addresses
4703            .iter()
4704            .filter_map(MultiAddr::dialable_socket_addr)
4705            .collect();
4706
4707        let sets = snapshot_dial_sets(&[queued, fresh.clone()], &self_id, &mut seen);
4708
4709        assert_eq!(
4710            sets.len(),
4711            1,
4712            "the already-queued peer must not be redialled"
4713        );
4714        assert_eq!(sets[0].0, fresh.peer_id);
4715        assert_eq!(
4716            seen.len(),
4717            2,
4718            "the new address is reserved for later phases"
4719        );
4720    }
4721
4722    #[test]
4723    fn snapshot_dial_sets_drops_undialable_addresses() {
4724        let self_id = PeerId::random();
4725        let peers = vec![snapshot_peer("/ip4/10.0.0.4/tcp/9000")];
4726        let mut seen = HashSet::new();
4727
4728        assert!(
4729            snapshot_dial_sets(&peers, &self_id, &mut seen).is_empty(),
4730            "only QUIC addresses are dialable today"
4731        );
4732    }
4733
4734    #[test]
4735    fn snapshot_dial_sets_dedupes_repeated_peers_and_bounds_addresses() {
4736        let self_id = PeerId::random();
4737        let mut peer = snapshot_peer("/ip4/10.0.0.5/udp/9000/quic");
4738        peer.addresses = (0..6)
4739            .map(|i| {
4740                format!("/ip4/10.0.0.5/udp/900{i}/quic")
4741                    .parse()
4742                    .expect("valid multiaddr")
4743            })
4744            .collect();
4745        let mut seen = HashSet::new();
4746
4747        let sets = snapshot_dial_sets(&[peer.clone(), peer], &self_id, &mut seen);
4748
4749        assert_eq!(sets.len(), 1, "the same peer twice is one candidate");
4750        assert_eq!(
4751            sets[0].1.len(),
4752            MAX_SNAPSHOT_ADDRESSES_DIALLED,
4753            "the dial tail past the budget stays bounded"
4754        );
4755    }
4756
4757    #[test]
4758    fn snapshot_dial_sets_returns_every_other_peer_once() {
4759        let self_id = PeerId::random();
4760        let peers: Vec<SnapshotPeer> = (0..130)
4761            .map(|i| snapshot_peer(&format!("/ip4/10.1.{}.{}/udp/9000/quic", i / 256, i % 256)))
4762            .collect();
4763        let mut seen = HashSet::new();
4764
4765        let sets = snapshot_dial_sets(&peers, &self_id, &mut seen);
4766
4767        assert_eq!(sets.len(), 130);
4768        assert_eq!(seen.len(), 130);
4769    }
4770}