Skip to main content

ant_quic/
node_config.rs

1// Copyright 2024 Saorsa Labs Ltd.
2//
3// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
4// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
5//
6// Full details available at https://saorsalabs.com/licenses
7
8//! Minimal configuration for zero-config P2P nodes
9//!
10//! This module provides [`NodeConfig`] - a simple configuration struct
11//! with only 3 optional fields. Most applications need zero configuration.
12//!
13//! # Zero Configuration
14//!
15//! ```rust,ignore
16//! use ant_quic::Node;
17//!
18//! // No configuration needed - just create a node
19//! let node = Node::new().await?;
20//! ```
21//!
22//! # Optional Configuration
23//!
24//! ```rust,ignore
25//! use ant_quic::{Node, NodeConfig};
26//!
27//! // Only configure what you need
28//! let config = NodeConfig::builder()
29//!     .known_peer("quic.saorsalabs.com:9000".parse()?)
30//!     .build();
31//!
32//! let node = Node::with_config(config).await?;
33//! ```
34
35use std::path::Path;
36use std::sync::Arc;
37
38use crate::bootstrap_cache::BootstrapCacheConfig;
39use crate::crypto::pqc::types::{MlDsaPublicKey, MlDsaSecretKey};
40use crate::host_identity::HostIdentity;
41use crate::transport::{TransportAddr, TransportProvider, TransportRegistry};
42use crate::unified_config::load_or_generate_endpoint_keypair;
43
44/// Minimal configuration for P2P nodes
45///
46/// All fields are optional - the node will auto-configure everything.
47/// - `bind_addr`: Defaults to `0.0.0.0:0` (random port)
48/// - `known_peers`: Defaults to empty (node can still accept connections)
49/// - `keypair`: Defaults to fresh generated keypair
50/// - `transport_providers`: Defaults to UDP plus best-effort constrained transports
51///   such as BLE when available
52///
53/// # Example
54///
55/// ```rust,ignore
56/// // Zero configuration
57/// let config = NodeConfig::default();
58///
59/// // Or with known peers
60/// let config = NodeConfig::builder()
61///     .known_peer("peer1.example.com:9000".parse()?)
62///     .build();
63///
64/// // Or with additional transport providers
65/// #[cfg(feature = "ble")]
66/// let config = NodeConfig::builder()
67///     .transport_provider(Arc::new(BleTransport::new().await?))
68///     .build();
69/// ```
70#[derive(Clone, Default)]
71pub struct NodeConfig {
72    /// Bind address. Default: 0.0.0.0:0 (random port)
73    pub bind_addr: Option<TransportAddr>,
74
75    /// Known peers for initial discovery. Default: empty
76    /// When empty, node can still accept incoming connections.
77    pub known_peers: Vec<TransportAddr>,
78
79    /// Identity keypair (ML-DSA-65). Default: fresh generated
80    /// Provide for persistent identity across restarts.
81    pub keypair: Option<(MlDsaPublicKey, MlDsaSecretKey)>,
82
83    /// Additional transport providers beyond the default transport set.
84    ///
85    /// UDP is always included, and constrained transports such as BLE are
86    /// registered automatically when compiled in and available at runtime.
87    /// Use this to add additional transports like LoRa, serial, etc.
88    ///
89    /// Transport capabilities are propagated to peer advertisements and
90    /// used for routing decisions.
91    pub transport_providers: Vec<Arc<dyn TransportProvider>>,
92
93    /// Data channel capacity (bounded mpsc between reader tasks and recv).
94    /// Default: 256.
95    pub data_channel_capacity: Option<usize>,
96
97    /// Maximum concurrent unidirectional QUIC streams per connection.
98    /// Default: 100.
99    pub max_concurrent_uni_streams: Option<u32>,
100
101    /// Maximum bytes accepted for a single message read from a stream.
102    /// Default: [`crate::unified_config::P2pConfig::DEFAULT_MAX_MESSAGE_SIZE`].
103    pub max_message_size: Option<usize>,
104
105    /// Reviewer P2 #2: surface ant-quic's best-effort UPnP IGD port-mapping
106    /// toggle at the simpler `NodeConfig` builder layer (the existing knob
107    /// is only on `P2pConfigBuilder`). When `Some(false)`, the UPnP
108    /// discovery + port-mapping task is skipped. When `None`, the ant-quic
109    /// default applies (currently enabled).
110    pub port_mapping_enabled: Option<bool>,
111
112    /// Enable or disable first-party mDNS discovery for this node.
113    ///
114    /// When `Some(false)`, mDNS browsing and advertising are disabled so
115    /// co-located daemons on the same LAN cannot discover and auto-connect
116    /// to this node via mDNS. When `None`, the ant-quic default applies
117    /// (currently enabled with auto-connect).
118    pub mdns_enabled: Option<bool>,
119
120    /// mDNS namespace isolating this node from other logical planes.
121    ///
122    /// When set, the namespace is advertised in the mDNS TXT record and
123    /// discovered services advertising a different (or no) namespace are
124    /// rejected as dial candidates. Callers running multiple logical
125    /// planes on one host (e.g. prod + testnet daemons) MUST set a
126    /// distinct namespace per plane, otherwise the daemons discover each
127    /// other via the shared `ant-quic` service and auto-connect, bridging
128    /// gossip across planes. When `None`, the ant-quic default applies
129    /// (no namespace; every `ant-quic` service on the LAN is eligible).
130    pub mdns_namespace: Option<String>,
131
132    /// Bootstrap peer cache configuration for the node's endpoint.
133    ///
134    /// The endpoint always owns exactly one [`BootstrapCacheConfig`]-backed
135    /// cache used for quality-scored reconnection, coordinator selection and
136    /// bootstrap tokens. When `None`, the ant-quic default applies — a
137    /// **host-shared** directory (`$TMPDIR/ant-quic-cache` or the platform
138    /// cache dir). Embedders running multiple nodes per host (or wanting
139    /// per-instance persistence) should set an explicit per-instance
140    /// `cache_dir`, or `persist(false)` for an in-memory-only cache.
141    /// Access the resulting shared cache via [`crate::Node::bootstrap_cache`].
142    pub bootstrap_cache: Option<BootstrapCacheConfig>,
143}
144
145impl std::fmt::Debug for NodeConfig {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_struct("NodeConfig")
148            .field("bind_addr", &self.bind_addr)
149            .field("known_peers", &self.known_peers)
150            .field("keypair", &self.keypair.as_ref().map(|_| "[REDACTED]"))
151            .field("transport_providers", &self.transport_providers.len())
152            .finish()
153    }
154}
155
156impl NodeConfig {
157    /// Create a new config with defaults
158    pub fn new() -> Self {
159        Self::default()
160    }
161
162    /// Create a builder for fluent construction
163    pub fn builder() -> NodeConfigBuilder {
164        NodeConfigBuilder::default()
165    }
166
167    /// Create config with a specific bind address
168    pub fn with_bind_addr(addr: impl Into<TransportAddr>) -> Self {
169        Self {
170            bind_addr: Some(addr.into()),
171            ..Default::default()
172        }
173    }
174
175    /// Create config with known peers
176    pub fn with_known_peers(peers: impl IntoIterator<Item = impl Into<TransportAddr>>) -> Self {
177        Self {
178            known_peers: peers.into_iter().map(|p| p.into()).collect(),
179            ..Default::default()
180        }
181    }
182
183    /// Create config with a specific ML-DSA-65 keypair
184    pub fn with_keypair(public_key: MlDsaPublicKey, secret_key: MlDsaSecretKey) -> Self {
185        Self {
186            keypair: Some((public_key, secret_key)),
187            ..Default::default()
188        }
189    }
190}
191
192/// Builder for [`NodeConfig`]
193#[derive(Default)]
194pub struct NodeConfigBuilder {
195    bind_addr: Option<TransportAddr>,
196    known_peers: Vec<TransportAddr>,
197    keypair: Option<(MlDsaPublicKey, MlDsaSecretKey)>,
198    transport_providers: Vec<Arc<dyn TransportProvider>>,
199    data_channel_capacity: Option<usize>,
200    max_concurrent_uni_streams: Option<u32>,
201    max_message_size: Option<usize>,
202    port_mapping_enabled: Option<bool>,
203    mdns_enabled: Option<bool>,
204    mdns_namespace: Option<String>,
205    bootstrap_cache: Option<BootstrapCacheConfig>,
206}
207
208impl NodeConfigBuilder {
209    /// Set the local address to bind to
210    ///
211    /// Accepts any type implementing `Into<TransportAddr>`:
212    /// - `SocketAddr` - Auto-converts to `TransportAddr::Udp` (backward compatible)
213    /// - `TransportAddr` - Enables multi-transport support (BLE, LoRa, etc.)
214    ///
215    /// If not specified, defaults to `0.0.0.0:0` (random ephemeral port).
216    ///
217    /// # Examples
218    ///
219    /// ```rust,ignore
220    /// use ant_quic::NodeConfig;
221    /// use std::net::SocketAddr;
222    ///
223    /// // Backward compatible: SocketAddr
224    /// let config = NodeConfig::builder()
225    ///     .bind_addr("0.0.0.0:9000".parse::<SocketAddr>().unwrap())
226    ///     .build();
227    ///
228    /// // Multi-transport: Explicit TransportAddr
229    /// use ant_quic::transport::TransportAddr;
230    /// let config = NodeConfig::builder()
231    ///     .bind_addr(TransportAddr::Udp("0.0.0.0:0".parse().unwrap()))
232    ///     .build();
233    /// ```
234    pub fn bind_addr(mut self, addr: impl Into<TransportAddr>) -> Self {
235        self.bind_addr = Some(addr.into());
236        self
237    }
238
239    /// Add a known peer for initial network connectivity
240    ///
241    /// Known peers are used for initial discovery and connection establishment.
242    /// The node will learn about additional peers through the network.
243    ///
244    /// Accepts any type implementing `Into<TransportAddr>`:
245    /// - `SocketAddr` - Auto-converts to `TransportAddr::Udp`
246    /// - `TransportAddr` - Supports multiple transport types
247    ///
248    /// # Examples
249    ///
250    /// ```rust,ignore
251    /// use ant_quic::NodeConfig;
252    /// use std::net::SocketAddr;
253    ///
254    /// // Backward compatible: SocketAddr
255    /// let config = NodeConfig::builder()
256    ///     .known_peer("peer.example.com:9000".parse::<SocketAddr>().unwrap())
257    ///     .build();
258    ///
259    /// // Multi-transport: Mix different transport types
260    /// use ant_quic::transport::TransportAddr;
261    /// let config = NodeConfig::builder()
262    ///     .known_peer(TransportAddr::Udp("192.168.1.1:9000".parse().unwrap()))
263    ///     .known_peer(TransportAddr::ble([0x11, 0x22, 0x33, 0x44, 0x55, 0x66], None))
264    ///     .build();
265    /// ```
266    pub fn known_peer(mut self, addr: impl Into<TransportAddr>) -> Self {
267        self.known_peers.push(addr.into());
268        self
269    }
270
271    /// Add multiple known peers at once
272    ///
273    /// Convenient method to add a collection of peers. Each item is automatically
274    /// converted via `Into<TransportAddr>`, supporting both `SocketAddr` and
275    /// `TransportAddr` for backward compatibility and multi-transport scenarios.
276    ///
277    /// # Examples
278    ///
279    /// ```rust,ignore
280    /// use ant_quic::NodeConfig;
281    /// use std::net::SocketAddr;
282    ///
283    /// // Backward compatible: Vec<SocketAddr>
284    /// let peers: Vec<SocketAddr> = vec![
285    ///     "peer1.example.com:9000".parse().unwrap(),
286    ///     "peer2.example.com:9000".parse().unwrap(),
287    /// ];
288    /// let config = NodeConfig::builder()
289    ///     .known_peers(peers)
290    ///     .build();
291    ///
292    /// // Multi-transport: Heterogeneous transport list
293    /// use ant_quic::transport::TransportAddr;
294    /// let mixed = vec![
295    ///     TransportAddr::Udp("192.168.1.1:9000".parse().unwrap()),
296    ///     TransportAddr::ble([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], None),
297    ///     TransportAddr::serial("/dev/ttyUSB0"),
298    /// ];
299    /// let config = NodeConfig::builder()
300    ///     .known_peers(mixed)
301    ///     .build();
302    /// ```
303    pub fn known_peers(
304        mut self,
305        addrs: impl IntoIterator<Item = impl Into<TransportAddr>>,
306    ) -> Self {
307        self.known_peers.extend(addrs.into_iter().map(|a| a.into()));
308        self
309    }
310
311    /// Set the identity keypair (ML-DSA-65)
312    pub fn keypair(mut self, public_key: MlDsaPublicKey, secret_key: MlDsaSecretKey) -> Self {
313        self.keypair = Some((public_key, secret_key));
314        self
315    }
316
317    /// Set the identity from a HostIdentity with encrypted storage
318    ///
319    /// This loads or generates a keypair using the HostIdentity for encryption.
320    /// The keypair is stored encrypted at rest in the specified directory.
321    ///
322    /// # Arguments
323    ///
324    /// * `host` - The HostIdentity for key derivation
325    /// * `network_id` - Network identifier for per-network keypair isolation
326    /// * `storage_dir` - Directory to store the encrypted keypair
327    ///
328    /// # Errors
329    ///
330    /// Returns an error if the keypair cannot be loaded or generated.
331    pub fn with_host_identity(
332        mut self,
333        host: &HostIdentity,
334        network_id: &[u8],
335        storage_dir: &Path,
336    ) -> Result<Self, String> {
337        let (public_key, secret_key) =
338            load_or_generate_endpoint_keypair(host, network_id, storage_dir)
339                .map_err(|e| format!("Failed to load/generate keypair: {e}"))?;
340        self.keypair = Some((public_key, secret_key));
341        Ok(self)
342    }
343
344    /// Add a transport provider
345    ///
346    /// Transport providers are used for multi-transport P2P networking.
347    /// UDP is always included, and constrained transports such as BLE are
348    /// registered automatically when compiled in and available at runtime.
349    ///
350    /// # Example
351    ///
352    /// ```rust,ignore
353    /// #[cfg(feature = "ble")]
354    /// let config = NodeConfig::builder()
355    ///     .transport_provider(Arc::new(BleTransport::new().await?))
356    ///     .build();
357    /// ```
358    pub fn transport_provider(mut self, provider: Arc<dyn TransportProvider>) -> Self {
359        self.transport_providers.push(provider);
360        self
361    }
362
363    /// Add multiple transport providers
364    pub fn transport_providers(
365        mut self,
366        providers: impl IntoIterator<Item = Arc<dyn TransportProvider>>,
367    ) -> Self {
368        self.transport_providers.extend(providers);
369        self
370    }
371
372    /// Set the data channel capacity (bounded mpsc between reader tasks and recv).
373    ///
374    /// Higher values reduce backpressure on reader tasks. Default: 256.
375    pub fn data_channel_capacity(mut self, capacity: usize) -> Self {
376        self.data_channel_capacity = Some(capacity);
377        self
378    }
379
380    /// Set the maximum concurrent unidirectional QUIC streams per connection.
381    ///
382    /// Each `send()` call opens a new unidirectional stream. Applications with
383    /// high message throughput should increase this. Default: 100.
384    pub fn max_concurrent_uni_streams(mut self, count: u32) -> Self {
385        self.max_concurrent_uni_streams = Some(count);
386        self
387    }
388
389    /// Set the maximum bytes accepted for a single message read from a stream.
390    ///
391    /// This mirrors [`crate::unified_config::P2pConfigBuilder::max_message_size`]
392    /// for applications using the simpler [`NodeConfig`] builder. Values must be
393    /// at least 1; [`crate::Node::with_config`] rejects zero with a configuration
394    /// error before constructing the endpoint.
395    pub fn max_message_size(mut self, bytes: usize) -> Self {
396        self.max_message_size = Some(bytes);
397        self
398    }
399
400    /// Reviewer P2 #2: enable or disable the best-effort UPnP IGD
401    /// port-mapping task. Default (when not called) follows the global
402    /// ant-quic default — currently enabled. Use `false` on networks
403    /// without IGD support, or where operator policy prohibits unsolicited
404    /// router port mappings.
405    pub fn port_mapping_enabled(mut self, enabled: bool) -> Self {
406        self.port_mapping_enabled = Some(enabled);
407        self
408    }
409
410    /// Enable or disable first-party mDNS discovery (see
411    /// [`NodeConfig::mdns_enabled`]). Use `false` to keep co-located
412    /// daemons on other planes from discovering this node via mDNS.
413    pub fn mdns_enabled(mut self, enabled: bool) -> Self {
414        self.mdns_enabled = Some(enabled);
415        self
416    }
417
418    /// Set the mDNS namespace isolating this node's logical plane (see
419    /// [`NodeConfig::mdns_namespace`]). Callers running multiple planes on
420    /// one host MUST set a distinct namespace per plane.
421    pub fn mdns_namespace(mut self, namespace: impl Into<String>) -> Self {
422        self.mdns_namespace = Some(namespace.into());
423        self
424    }
425
426    /// Configure the endpoint's bootstrap peer cache (see
427    /// [`NodeConfig::bootstrap_cache`])
428    pub fn bootstrap_cache(mut self, config: BootstrapCacheConfig) -> Self {
429        self.bootstrap_cache = Some(config);
430        self
431    }
432
433    /// Build the configuration
434    pub fn build(self) -> NodeConfig {
435        NodeConfig {
436            bind_addr: self.bind_addr,
437            known_peers: self.known_peers,
438            keypair: self.keypair,
439            transport_providers: self.transport_providers,
440            data_channel_capacity: self.data_channel_capacity,
441            max_concurrent_uni_streams: self.max_concurrent_uni_streams,
442            max_message_size: self.max_message_size,
443            port_mapping_enabled: self.port_mapping_enabled,
444            mdns_enabled: self.mdns_enabled,
445            mdns_namespace: self.mdns_namespace,
446            bootstrap_cache: self.bootstrap_cache,
447        }
448    }
449}
450
451impl NodeConfig {
452    /// Build a transport registry from this configuration
453    ///
454    /// Creates a registry containing all configured transport providers.
455    /// If no providers are configured, returns an empty registry (UDP
456    /// should be added by the caller based on bind_addr).
457    pub fn build_transport_registry(&self) -> TransportRegistry {
458        let mut registry = TransportRegistry::new();
459        for provider in &self.transport_providers {
460            registry.register(provider.clone());
461        }
462        registry
463    }
464
465    /// Check if this configuration has any non-UDP transport providers
466    pub fn has_constrained_transports(&self) -> bool {
467        use crate::transport::TransportType;
468        self.transport_providers
469            .iter()
470            .any(|p| p.transport_type() != TransportType::Udp)
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477    use std::net::SocketAddr;
478
479    #[test]
480    fn test_default_config() {
481        let config = NodeConfig::default();
482        assert!(config.bind_addr.is_none());
483        assert!(config.known_peers.is_empty());
484        assert!(config.keypair.is_none());
485        assert!(config.transport_providers.is_empty());
486        assert!(config.max_message_size.is_none());
487    }
488
489    /// The endpoint always opens exactly one bootstrap cache from
490    /// `NodeConfig::bootstrap_cache`; without this plumbing embedders were
491    /// forced to open a *second* cache instance (host-shared default dir)
492    /// that diverged from the endpoint's own.
493    #[test]
494    fn builder_plumbs_bootstrap_cache_config() {
495        let cache_config = BootstrapCacheConfig::builder()
496            .cache_dir("/tmp/per-instance-peers")
497            .persist(false)
498            .build();
499        let config = NodeConfig::builder().bootstrap_cache(cache_config).build();
500
501        let plumbed = config.bootstrap_cache.expect("bootstrap cache config set");
502        assert_eq!(
503            plumbed.cache_dir,
504            std::path::PathBuf::from("/tmp/per-instance-peers")
505        );
506        assert!(!plumbed.persist);
507        // Default stays None so existing embedders keep ant-quic's default.
508        assert!(NodeConfig::default().bootstrap_cache.is_none());
509    }
510
511    /// Issue #206: NodeConfig must expose mDNS plane-isolation knobs so
512    /// co-located daemons on different logical planes do not discover and
513    /// auto-connect to each other via the default `ant-quic` service.
514    #[test]
515    fn builder_plumbs_mdns_plane_isolation_config() {
516        let config = NodeConfig::builder()
517            .mdns_enabled(false)
518            .mdns_namespace("testnet")
519            .build();
520        assert_eq!(config.mdns_enabled, Some(false));
521        assert_eq!(config.mdns_namespace.as_deref(), Some("testnet"));
522
523        // Defaults stay None so existing embedders keep ant-quic's default.
524        let default = NodeConfig::default();
525        assert!(default.mdns_enabled.is_none());
526        assert!(default.mdns_namespace.is_none());
527    }
528
529    #[test]
530    fn test_builder_with_max_message_size() {
531        let config = NodeConfig::builder()
532            .max_message_size(10 * 1024 * 1024)
533            .build();
534        assert_eq!(config.max_message_size, Some(10 * 1024 * 1024));
535    }
536
537    #[test]
538    fn test_builder_records_zero_max_message_size_for_node_validation() {
539        let config = NodeConfig::builder().max_message_size(0).build();
540        assert_eq!(config.max_message_size, Some(0));
541    }
542
543    #[test]
544    fn test_builder_with_bind_addr() {
545        let addr: SocketAddr = "0.0.0.0:9000".parse().unwrap();
546        let config = NodeConfig::builder().bind_addr(addr).build();
547        assert_eq!(config.bind_addr, Some(TransportAddr::from(addr)));
548    }
549
550    #[test]
551    fn test_builder_with_known_peers() {
552        let peer1: SocketAddr = "127.0.0.1:9000".parse().unwrap();
553        let peer2: SocketAddr = "127.0.0.1:9001".parse().unwrap();
554
555        let config = NodeConfig::builder()
556            .known_peer(peer1)
557            .known_peer(peer2)
558            .build();
559
560        assert_eq!(config.known_peers.len(), 2);
561        assert!(config.known_peers.contains(&TransportAddr::from(peer1)));
562        assert!(config.known_peers.contains(&TransportAddr::from(peer2)));
563    }
564
565    #[test]
566    fn test_builder_with_multiple_peers() {
567        let peers: Vec<SocketAddr> = vec![
568            "127.0.0.1:9000".parse().unwrap(),
569            "127.0.0.1:9001".parse().unwrap(),
570        ];
571
572        let config = NodeConfig::builder().known_peers(peers.clone()).build();
573
574        assert_eq!(config.known_peers.len(), 2);
575        assert_eq!(
576            config.known_peers,
577            peers
578                .into_iter()
579                .map(TransportAddr::from)
580                .collect::<Vec<_>>()
581        );
582    }
583
584    #[test]
585    fn test_with_bind_addr() {
586        let addr: SocketAddr = "0.0.0.0:9000".parse().unwrap();
587        let config = NodeConfig::with_bind_addr(addr);
588        assert_eq!(config.bind_addr, Some(TransportAddr::from(addr)));
589        assert!(config.known_peers.is_empty());
590        assert!(config.keypair.is_none());
591    }
592
593    #[test]
594    fn test_with_known_peers() {
595        let peers: Vec<SocketAddr> = vec![
596            "127.0.0.1:9000".parse().unwrap(),
597            "127.0.0.1:9001".parse().unwrap(),
598        ];
599
600        let config = NodeConfig::with_known_peers(peers.clone());
601        assert!(config.bind_addr.is_none());
602        assert_eq!(
603            config.known_peers,
604            peers
605                .into_iter()
606                .map(TransportAddr::from)
607                .collect::<Vec<_>>()
608        );
609        assert!(config.keypair.is_none());
610    }
611
612    #[test]
613    fn test_debug_redacts_keypair() {
614        use crate::crypto::raw_public_keys::key_utils::generate_ml_dsa_keypair;
615        let (public_key, secret_key) = generate_ml_dsa_keypair().unwrap();
616        let config = NodeConfig::with_keypair(public_key, secret_key);
617        let debug_str = format!("{:?}", config);
618        assert!(debug_str.contains("[REDACTED]"));
619        assert!(!debug_str.contains(&format!("{:?}", config.keypair)));
620    }
621
622    #[test]
623    fn test_config_is_clone() {
624        let addr: SocketAddr = "0.0.0.0:9000".parse().unwrap();
625        let peer: SocketAddr = "127.0.0.1:9001".parse().unwrap();
626        let config = NodeConfig::builder()
627            .bind_addr(addr)
628            .known_peer(peer)
629            .build();
630
631        let cloned = config.clone();
632        assert_eq!(config.bind_addr, cloned.bind_addr);
633        assert_eq!(config.known_peers, cloned.known_peers);
634    }
635
636    #[test]
637    fn test_build_transport_registry() {
638        let config = NodeConfig::default();
639        let registry = config.build_transport_registry();
640        assert!(registry.is_empty());
641    }
642
643    #[test]
644    fn test_has_constrained_transports_default() {
645        let config = NodeConfig::default();
646        assert!(!config.has_constrained_transports());
647    }
648
649    #[test]
650    fn test_debug_shows_transport_count() {
651        let config = NodeConfig::default();
652        let debug_str = format!("{:?}", config);
653        assert!(debug_str.contains("transport_providers: 0"));
654    }
655
656    #[test]
657    fn test_node_config_with_transport_addr() {
658        // Create NodeConfig with TransportAddr bind and peers
659        let bind_addr = TransportAddr::from("0.0.0.0:9000".parse::<SocketAddr>().unwrap());
660        let peer1 = TransportAddr::from("127.0.0.1:9001".parse::<SocketAddr>().unwrap());
661        let peer2 = TransportAddr::from("127.0.0.1:9002".parse::<SocketAddr>().unwrap());
662
663        let config = NodeConfig::builder()
664            .bind_addr(bind_addr.clone())
665            .known_peer(peer1.clone())
666            .known_peer(peer2.clone())
667            .build();
668
669        // Verify fields set correctly
670        assert_eq!(config.bind_addr, Some(bind_addr));
671        assert_eq!(config.known_peers.len(), 2);
672        assert!(config.known_peers.contains(&peer1));
673        assert!(config.known_peers.contains(&peer2));
674    }
675
676    #[test]
677    fn test_node_config_builder_backward_compat() {
678        // Use builder with SocketAddr (should auto-convert via Into trait)
679        let bind_socket: SocketAddr = "0.0.0.0:9000".parse().unwrap();
680        let peer_socket: SocketAddr = "127.0.0.1:9001".parse().unwrap();
681
682        let config = NodeConfig::builder()
683            .bind_addr(bind_socket)
684            .known_peer(peer_socket)
685            .build();
686
687        // Verify Into trait conversion works
688        assert_eq!(config.bind_addr, Some(TransportAddr::from(bind_socket)));
689        assert_eq!(config.known_peers.len(), 1);
690        assert_eq!(config.known_peers[0], TransportAddr::from(peer_socket));
691
692        // Verify it's the same as explicit TransportAddr usage
693        let explicit_config = NodeConfig::builder()
694            .bind_addr(TransportAddr::from(bind_socket))
695            .known_peer(TransportAddr::from(peer_socket))
696            .build();
697
698        assert_eq!(config.bind_addr, explicit_config.bind_addr);
699        assert_eq!(config.known_peers, explicit_config.known_peers);
700    }
701
702    #[test]
703    fn test_node_config_transport_addr_preservation() {
704        // Create NodeConfig with various TransportAddr types
705        let udp_bind = TransportAddr::from("0.0.0.0:0".parse::<SocketAddr>().unwrap());
706        let udp_peer = TransportAddr::from("127.0.0.1:9000".parse::<SocketAddr>().unwrap());
707        let ipv6_peer = TransportAddr::from("[::1]:9001".parse::<SocketAddr>().unwrap());
708
709        let config = NodeConfig::builder()
710            .bind_addr(udp_bind.clone())
711            .known_peer(udp_peer.clone())
712            .known_peer(ipv6_peer.clone())
713            .build();
714
715        // Verify address types preserved
716        assert_eq!(config.bind_addr, Some(udp_bind));
717        assert_eq!(config.known_peers.len(), 2);
718
719        // Check that TransportAddr types are maintained
720        assert!(matches!(config.known_peers[0], TransportAddr::Udp(_)));
721        assert!(matches!(config.known_peers[1], TransportAddr::Udp(_)));
722
723        // Verify actual addresses match
724        assert_eq!(config.known_peers[0], udp_peer);
725        assert_eq!(config.known_peers[1], ipv6_peer);
726    }
727}