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    /// Bootstrap peer cache configuration for the node's endpoint.
113    ///
114    /// The endpoint always owns exactly one [`BootstrapCacheConfig`]-backed
115    /// cache used for quality-scored reconnection, coordinator selection and
116    /// bootstrap tokens. When `None`, the ant-quic default applies — a
117    /// **host-shared** directory (`$TMPDIR/ant-quic-cache` or the platform
118    /// cache dir). Embedders running multiple nodes per host (or wanting
119    /// per-instance persistence) should set an explicit per-instance
120    /// `cache_dir`, or `persist(false)` for an in-memory-only cache.
121    /// Access the resulting shared cache via [`crate::Node::bootstrap_cache`].
122    pub bootstrap_cache: Option<BootstrapCacheConfig>,
123}
124
125impl std::fmt::Debug for NodeConfig {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        f.debug_struct("NodeConfig")
128            .field("bind_addr", &self.bind_addr)
129            .field("known_peers", &self.known_peers)
130            .field("keypair", &self.keypair.as_ref().map(|_| "[REDACTED]"))
131            .field("transport_providers", &self.transport_providers.len())
132            .finish()
133    }
134}
135
136impl NodeConfig {
137    /// Create a new config with defaults
138    pub fn new() -> Self {
139        Self::default()
140    }
141
142    /// Create a builder for fluent construction
143    pub fn builder() -> NodeConfigBuilder {
144        NodeConfigBuilder::default()
145    }
146
147    /// Create config with a specific bind address
148    pub fn with_bind_addr(addr: impl Into<TransportAddr>) -> Self {
149        Self {
150            bind_addr: Some(addr.into()),
151            ..Default::default()
152        }
153    }
154
155    /// Create config with known peers
156    pub fn with_known_peers(peers: impl IntoIterator<Item = impl Into<TransportAddr>>) -> Self {
157        Self {
158            known_peers: peers.into_iter().map(|p| p.into()).collect(),
159            ..Default::default()
160        }
161    }
162
163    /// Create config with a specific ML-DSA-65 keypair
164    pub fn with_keypair(public_key: MlDsaPublicKey, secret_key: MlDsaSecretKey) -> Self {
165        Self {
166            keypair: Some((public_key, secret_key)),
167            ..Default::default()
168        }
169    }
170}
171
172/// Builder for [`NodeConfig`]
173#[derive(Default)]
174pub struct NodeConfigBuilder {
175    bind_addr: Option<TransportAddr>,
176    known_peers: Vec<TransportAddr>,
177    keypair: Option<(MlDsaPublicKey, MlDsaSecretKey)>,
178    transport_providers: Vec<Arc<dyn TransportProvider>>,
179    data_channel_capacity: Option<usize>,
180    max_concurrent_uni_streams: Option<u32>,
181    max_message_size: Option<usize>,
182    port_mapping_enabled: Option<bool>,
183    bootstrap_cache: Option<BootstrapCacheConfig>,
184}
185
186impl NodeConfigBuilder {
187    /// Set the local address to bind to
188    ///
189    /// Accepts any type implementing `Into<TransportAddr>`:
190    /// - `SocketAddr` - Auto-converts to `TransportAddr::Udp` (backward compatible)
191    /// - `TransportAddr` - Enables multi-transport support (BLE, LoRa, etc.)
192    ///
193    /// If not specified, defaults to `0.0.0.0:0` (random ephemeral port).
194    ///
195    /// # Examples
196    ///
197    /// ```rust,ignore
198    /// use ant_quic::NodeConfig;
199    /// use std::net::SocketAddr;
200    ///
201    /// // Backward compatible: SocketAddr
202    /// let config = NodeConfig::builder()
203    ///     .bind_addr("0.0.0.0:9000".parse::<SocketAddr>().unwrap())
204    ///     .build();
205    ///
206    /// // Multi-transport: Explicit TransportAddr
207    /// use ant_quic::transport::TransportAddr;
208    /// let config = NodeConfig::builder()
209    ///     .bind_addr(TransportAddr::Udp("0.0.0.0:0".parse().unwrap()))
210    ///     .build();
211    /// ```
212    pub fn bind_addr(mut self, addr: impl Into<TransportAddr>) -> Self {
213        self.bind_addr = Some(addr.into());
214        self
215    }
216
217    /// Add a known peer for initial network connectivity
218    ///
219    /// Known peers are used for initial discovery and connection establishment.
220    /// The node will learn about additional peers through the network.
221    ///
222    /// Accepts any type implementing `Into<TransportAddr>`:
223    /// - `SocketAddr` - Auto-converts to `TransportAddr::Udp`
224    /// - `TransportAddr` - Supports multiple transport types
225    ///
226    /// # Examples
227    ///
228    /// ```rust,ignore
229    /// use ant_quic::NodeConfig;
230    /// use std::net::SocketAddr;
231    ///
232    /// // Backward compatible: SocketAddr
233    /// let config = NodeConfig::builder()
234    ///     .known_peer("peer.example.com:9000".parse::<SocketAddr>().unwrap())
235    ///     .build();
236    ///
237    /// // Multi-transport: Mix different transport types
238    /// use ant_quic::transport::TransportAddr;
239    /// let config = NodeConfig::builder()
240    ///     .known_peer(TransportAddr::Udp("192.168.1.1:9000".parse().unwrap()))
241    ///     .known_peer(TransportAddr::ble([0x11, 0x22, 0x33, 0x44, 0x55, 0x66], None))
242    ///     .build();
243    /// ```
244    pub fn known_peer(mut self, addr: impl Into<TransportAddr>) -> Self {
245        self.known_peers.push(addr.into());
246        self
247    }
248
249    /// Add multiple known peers at once
250    ///
251    /// Convenient method to add a collection of peers. Each item is automatically
252    /// converted via `Into<TransportAddr>`, supporting both `SocketAddr` and
253    /// `TransportAddr` for backward compatibility and multi-transport scenarios.
254    ///
255    /// # Examples
256    ///
257    /// ```rust,ignore
258    /// use ant_quic::NodeConfig;
259    /// use std::net::SocketAddr;
260    ///
261    /// // Backward compatible: Vec<SocketAddr>
262    /// let peers: Vec<SocketAddr> = vec![
263    ///     "peer1.example.com:9000".parse().unwrap(),
264    ///     "peer2.example.com:9000".parse().unwrap(),
265    /// ];
266    /// let config = NodeConfig::builder()
267    ///     .known_peers(peers)
268    ///     .build();
269    ///
270    /// // Multi-transport: Heterogeneous transport list
271    /// use ant_quic::transport::TransportAddr;
272    /// let mixed = vec![
273    ///     TransportAddr::Udp("192.168.1.1:9000".parse().unwrap()),
274    ///     TransportAddr::ble([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], None),
275    ///     TransportAddr::serial("/dev/ttyUSB0"),
276    /// ];
277    /// let config = NodeConfig::builder()
278    ///     .known_peers(mixed)
279    ///     .build();
280    /// ```
281    pub fn known_peers(
282        mut self,
283        addrs: impl IntoIterator<Item = impl Into<TransportAddr>>,
284    ) -> Self {
285        self.known_peers.extend(addrs.into_iter().map(|a| a.into()));
286        self
287    }
288
289    /// Set the identity keypair (ML-DSA-65)
290    pub fn keypair(mut self, public_key: MlDsaPublicKey, secret_key: MlDsaSecretKey) -> Self {
291        self.keypair = Some((public_key, secret_key));
292        self
293    }
294
295    /// Set the identity from a HostIdentity with encrypted storage
296    ///
297    /// This loads or generates a keypair using the HostIdentity for encryption.
298    /// The keypair is stored encrypted at rest in the specified directory.
299    ///
300    /// # Arguments
301    ///
302    /// * `host` - The HostIdentity for key derivation
303    /// * `network_id` - Network identifier for per-network keypair isolation
304    /// * `storage_dir` - Directory to store the encrypted keypair
305    ///
306    /// # Errors
307    ///
308    /// Returns an error if the keypair cannot be loaded or generated.
309    pub fn with_host_identity(
310        mut self,
311        host: &HostIdentity,
312        network_id: &[u8],
313        storage_dir: &Path,
314    ) -> Result<Self, String> {
315        let (public_key, secret_key) =
316            load_or_generate_endpoint_keypair(host, network_id, storage_dir)
317                .map_err(|e| format!("Failed to load/generate keypair: {e}"))?;
318        self.keypair = Some((public_key, secret_key));
319        Ok(self)
320    }
321
322    /// Add a transport provider
323    ///
324    /// Transport providers are used for multi-transport P2P networking.
325    /// UDP is always included, and constrained transports such as BLE are
326    /// registered automatically when compiled in and available at runtime.
327    ///
328    /// # Example
329    ///
330    /// ```rust,ignore
331    /// #[cfg(feature = "ble")]
332    /// let config = NodeConfig::builder()
333    ///     .transport_provider(Arc::new(BleTransport::new().await?))
334    ///     .build();
335    /// ```
336    pub fn transport_provider(mut self, provider: Arc<dyn TransportProvider>) -> Self {
337        self.transport_providers.push(provider);
338        self
339    }
340
341    /// Add multiple transport providers
342    pub fn transport_providers(
343        mut self,
344        providers: impl IntoIterator<Item = Arc<dyn TransportProvider>>,
345    ) -> Self {
346        self.transport_providers.extend(providers);
347        self
348    }
349
350    /// Set the data channel capacity (bounded mpsc between reader tasks and recv).
351    ///
352    /// Higher values reduce backpressure on reader tasks. Default: 256.
353    pub fn data_channel_capacity(mut self, capacity: usize) -> Self {
354        self.data_channel_capacity = Some(capacity);
355        self
356    }
357
358    /// Set the maximum concurrent unidirectional QUIC streams per connection.
359    ///
360    /// Each `send()` call opens a new unidirectional stream. Applications with
361    /// high message throughput should increase this. Default: 100.
362    pub fn max_concurrent_uni_streams(mut self, count: u32) -> Self {
363        self.max_concurrent_uni_streams = Some(count);
364        self
365    }
366
367    /// Set the maximum bytes accepted for a single message read from a stream.
368    ///
369    /// This mirrors [`crate::unified_config::P2pConfigBuilder::max_message_size`]
370    /// for applications using the simpler [`NodeConfig`] builder. Values must be
371    /// at least 1; [`crate::Node::with_config`] rejects zero with a configuration
372    /// error before constructing the endpoint.
373    pub fn max_message_size(mut self, bytes: usize) -> Self {
374        self.max_message_size = Some(bytes);
375        self
376    }
377
378    /// Reviewer P2 #2: enable or disable the best-effort UPnP IGD
379    /// port-mapping task. Default (when not called) follows the global
380    /// ant-quic default — currently enabled. Use `false` on networks
381    /// without IGD support, or where operator policy prohibits unsolicited
382    /// router port mappings.
383    pub fn port_mapping_enabled(mut self, enabled: bool) -> Self {
384        self.port_mapping_enabled = Some(enabled);
385        self
386    }
387
388    /// Configure the endpoint's bootstrap peer cache (see
389    /// [`NodeConfig::bootstrap_cache`])
390    pub fn bootstrap_cache(mut self, config: BootstrapCacheConfig) -> Self {
391        self.bootstrap_cache = Some(config);
392        self
393    }
394
395    /// Build the configuration
396    pub fn build(self) -> NodeConfig {
397        NodeConfig {
398            bind_addr: self.bind_addr,
399            known_peers: self.known_peers,
400            keypair: self.keypair,
401            transport_providers: self.transport_providers,
402            data_channel_capacity: self.data_channel_capacity,
403            max_concurrent_uni_streams: self.max_concurrent_uni_streams,
404            max_message_size: self.max_message_size,
405            port_mapping_enabled: self.port_mapping_enabled,
406            bootstrap_cache: self.bootstrap_cache,
407        }
408    }
409}
410
411impl NodeConfig {
412    /// Build a transport registry from this configuration
413    ///
414    /// Creates a registry containing all configured transport providers.
415    /// If no providers are configured, returns an empty registry (UDP
416    /// should be added by the caller based on bind_addr).
417    pub fn build_transport_registry(&self) -> TransportRegistry {
418        let mut registry = TransportRegistry::new();
419        for provider in &self.transport_providers {
420            registry.register(provider.clone());
421        }
422        registry
423    }
424
425    /// Check if this configuration has any non-UDP transport providers
426    pub fn has_constrained_transports(&self) -> bool {
427        use crate::transport::TransportType;
428        self.transport_providers
429            .iter()
430            .any(|p| p.transport_type() != TransportType::Udp)
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437    use std::net::SocketAddr;
438
439    #[test]
440    fn test_default_config() {
441        let config = NodeConfig::default();
442        assert!(config.bind_addr.is_none());
443        assert!(config.known_peers.is_empty());
444        assert!(config.keypair.is_none());
445        assert!(config.transport_providers.is_empty());
446        assert!(config.max_message_size.is_none());
447    }
448
449    /// The endpoint always opens exactly one bootstrap cache from
450    /// `NodeConfig::bootstrap_cache`; without this plumbing embedders were
451    /// forced to open a *second* cache instance (host-shared default dir)
452    /// that diverged from the endpoint's own.
453    #[test]
454    fn builder_plumbs_bootstrap_cache_config() {
455        let cache_config = BootstrapCacheConfig::builder()
456            .cache_dir("/tmp/per-instance-peers")
457            .persist(false)
458            .build();
459        let config = NodeConfig::builder().bootstrap_cache(cache_config).build();
460
461        let plumbed = config.bootstrap_cache.expect("bootstrap cache config set");
462        assert_eq!(
463            plumbed.cache_dir,
464            std::path::PathBuf::from("/tmp/per-instance-peers")
465        );
466        assert!(!plumbed.persist);
467        // Default stays None so existing embedders keep ant-quic's default.
468        assert!(NodeConfig::default().bootstrap_cache.is_none());
469    }
470
471    #[test]
472    fn test_builder_with_max_message_size() {
473        let config = NodeConfig::builder()
474            .max_message_size(10 * 1024 * 1024)
475            .build();
476        assert_eq!(config.max_message_size, Some(10 * 1024 * 1024));
477    }
478
479    #[test]
480    fn test_builder_records_zero_max_message_size_for_node_validation() {
481        let config = NodeConfig::builder().max_message_size(0).build();
482        assert_eq!(config.max_message_size, Some(0));
483    }
484
485    #[test]
486    fn test_builder_with_bind_addr() {
487        let addr: SocketAddr = "0.0.0.0:9000".parse().unwrap();
488        let config = NodeConfig::builder().bind_addr(addr).build();
489        assert_eq!(config.bind_addr, Some(TransportAddr::from(addr)));
490    }
491
492    #[test]
493    fn test_builder_with_known_peers() {
494        let peer1: SocketAddr = "127.0.0.1:9000".parse().unwrap();
495        let peer2: SocketAddr = "127.0.0.1:9001".parse().unwrap();
496
497        let config = NodeConfig::builder()
498            .known_peer(peer1)
499            .known_peer(peer2)
500            .build();
501
502        assert_eq!(config.known_peers.len(), 2);
503        assert!(config.known_peers.contains(&TransportAddr::from(peer1)));
504        assert!(config.known_peers.contains(&TransportAddr::from(peer2)));
505    }
506
507    #[test]
508    fn test_builder_with_multiple_peers() {
509        let peers: Vec<SocketAddr> = vec![
510            "127.0.0.1:9000".parse().unwrap(),
511            "127.0.0.1:9001".parse().unwrap(),
512        ];
513
514        let config = NodeConfig::builder().known_peers(peers.clone()).build();
515
516        assert_eq!(config.known_peers.len(), 2);
517        assert_eq!(
518            config.known_peers,
519            peers
520                .into_iter()
521                .map(TransportAddr::from)
522                .collect::<Vec<_>>()
523        );
524    }
525
526    #[test]
527    fn test_with_bind_addr() {
528        let addr: SocketAddr = "0.0.0.0:9000".parse().unwrap();
529        let config = NodeConfig::with_bind_addr(addr);
530        assert_eq!(config.bind_addr, Some(TransportAddr::from(addr)));
531        assert!(config.known_peers.is_empty());
532        assert!(config.keypair.is_none());
533    }
534
535    #[test]
536    fn test_with_known_peers() {
537        let peers: Vec<SocketAddr> = vec![
538            "127.0.0.1:9000".parse().unwrap(),
539            "127.0.0.1:9001".parse().unwrap(),
540        ];
541
542        let config = NodeConfig::with_known_peers(peers.clone());
543        assert!(config.bind_addr.is_none());
544        assert_eq!(
545            config.known_peers,
546            peers
547                .into_iter()
548                .map(TransportAddr::from)
549                .collect::<Vec<_>>()
550        );
551        assert!(config.keypair.is_none());
552    }
553
554    #[test]
555    fn test_debug_redacts_keypair() {
556        use crate::crypto::raw_public_keys::key_utils::generate_ml_dsa_keypair;
557        let (public_key, secret_key) = generate_ml_dsa_keypair().unwrap();
558        let config = NodeConfig::with_keypair(public_key, secret_key);
559        let debug_str = format!("{:?}", config);
560        assert!(debug_str.contains("[REDACTED]"));
561        assert!(!debug_str.contains(&format!("{:?}", config.keypair)));
562    }
563
564    #[test]
565    fn test_config_is_clone() {
566        let addr: SocketAddr = "0.0.0.0:9000".parse().unwrap();
567        let peer: SocketAddr = "127.0.0.1:9001".parse().unwrap();
568        let config = NodeConfig::builder()
569            .bind_addr(addr)
570            .known_peer(peer)
571            .build();
572
573        let cloned = config.clone();
574        assert_eq!(config.bind_addr, cloned.bind_addr);
575        assert_eq!(config.known_peers, cloned.known_peers);
576    }
577
578    #[test]
579    fn test_build_transport_registry() {
580        let config = NodeConfig::default();
581        let registry = config.build_transport_registry();
582        assert!(registry.is_empty());
583    }
584
585    #[test]
586    fn test_has_constrained_transports_default() {
587        let config = NodeConfig::default();
588        assert!(!config.has_constrained_transports());
589    }
590
591    #[test]
592    fn test_debug_shows_transport_count() {
593        let config = NodeConfig::default();
594        let debug_str = format!("{:?}", config);
595        assert!(debug_str.contains("transport_providers: 0"));
596    }
597
598    #[test]
599    fn test_node_config_with_transport_addr() {
600        // Create NodeConfig with TransportAddr bind and peers
601        let bind_addr = TransportAddr::from("0.0.0.0:9000".parse::<SocketAddr>().unwrap());
602        let peer1 = TransportAddr::from("127.0.0.1:9001".parse::<SocketAddr>().unwrap());
603        let peer2 = TransportAddr::from("127.0.0.1:9002".parse::<SocketAddr>().unwrap());
604
605        let config = NodeConfig::builder()
606            .bind_addr(bind_addr.clone())
607            .known_peer(peer1.clone())
608            .known_peer(peer2.clone())
609            .build();
610
611        // Verify fields set correctly
612        assert_eq!(config.bind_addr, Some(bind_addr));
613        assert_eq!(config.known_peers.len(), 2);
614        assert!(config.known_peers.contains(&peer1));
615        assert!(config.known_peers.contains(&peer2));
616    }
617
618    #[test]
619    fn test_node_config_builder_backward_compat() {
620        // Use builder with SocketAddr (should auto-convert via Into trait)
621        let bind_socket: SocketAddr = "0.0.0.0:9000".parse().unwrap();
622        let peer_socket: SocketAddr = "127.0.0.1:9001".parse().unwrap();
623
624        let config = NodeConfig::builder()
625            .bind_addr(bind_socket)
626            .known_peer(peer_socket)
627            .build();
628
629        // Verify Into trait conversion works
630        assert_eq!(config.bind_addr, Some(TransportAddr::from(bind_socket)));
631        assert_eq!(config.known_peers.len(), 1);
632        assert_eq!(config.known_peers[0], TransportAddr::from(peer_socket));
633
634        // Verify it's the same as explicit TransportAddr usage
635        let explicit_config = NodeConfig::builder()
636            .bind_addr(TransportAddr::from(bind_socket))
637            .known_peer(TransportAddr::from(peer_socket))
638            .build();
639
640        assert_eq!(config.bind_addr, explicit_config.bind_addr);
641        assert_eq!(config.known_peers, explicit_config.known_peers);
642    }
643
644    #[test]
645    fn test_node_config_transport_addr_preservation() {
646        // Create NodeConfig with various TransportAddr types
647        let udp_bind = TransportAddr::from("0.0.0.0:0".parse::<SocketAddr>().unwrap());
648        let udp_peer = TransportAddr::from("127.0.0.1:9000".parse::<SocketAddr>().unwrap());
649        let ipv6_peer = TransportAddr::from("[::1]:9001".parse::<SocketAddr>().unwrap());
650
651        let config = NodeConfig::builder()
652            .bind_addr(udp_bind.clone())
653            .known_peer(udp_peer.clone())
654            .known_peer(ipv6_peer.clone())
655            .build();
656
657        // Verify address types preserved
658        assert_eq!(config.bind_addr, Some(udp_bind));
659        assert_eq!(config.known_peers.len(), 2);
660
661        // Check that TransportAddr types are maintained
662        assert!(matches!(config.known_peers[0], TransportAddr::Udp(_)));
663        assert!(matches!(config.known_peers[1], TransportAddr::Udp(_)));
664
665        // Verify actual addresses match
666        assert_eq!(config.known_peers[0], udp_peer);
667        assert_eq!(config.known_peers[1], ipv6_peer);
668    }
669}