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}