//! Transport layer construction for ChronoGrid P2P. //! //! Builds a libp2p transport stack with: //! //! - **QUIC over UDP** (primary) — low-latency, multiplexed, with built-in TLS //! 1.3 equivalent security via the QUIC protocol. //! - **TCP + Noise XK** (fallback) — for environments where UDP/QUIC is blocked. //! - **Yamux** stream multiplexer over TCP. //! - **SwarmKey authentication** — a 256-bit pre-shared key that gates cluster //! membership. The key fingerprint is embedded in the `UserAgent` string //! exchanged during the `identify` handshake; peers with a mismatched //! fingerprint are disconnected immediately after identification. //! //! # Architecture note //! //! libp2p 0.54 uses the `SwarmBuilder` API which handles transport construction //! internally. This module exposes a `TransportConfig` struct that captures all //! parameters needed by `SwarmBuilder`, and a `build_transport` function that //! constructs a `Boxed<(PeerId, StreamMuxerBox)>` for use in lower-level swarm //! construction or testing. use libp2p::{ PeerId, Transport, core::{muxing::StreamMuxerBox, transport::Boxed, upgrade}, identity::Keypair, noise, tcp, yamux, }; use crate::{ error::{NetError, NetResult}, identity::NodeIdentity, swarm_key::SwarmKey, }; // --------------------------------------------------------------------------- // TransportConfig // --------------------------------------------------------------------------- /// Configuration parameters for the ChronoGrid transport stack. /// /// Passed to [`build_transport`] to construct the libp2p transport. /// Also consumed by `SwarmManager` (Task 2.5) when building the full swarm. #[derive(Debug, Clone)] pub struct TransportConfig { /// The swarm key fingerprint embedded in the identify user-agent string. /// Peers that present a different fingerprint are rejected post-handshake. pub swarm_key_fingerprint: String, /// Maximum number of simultaneous inbound connections. pub max_connections_in: u32, /// Maximum number of simultaneous outbound connections. pub max_connections_out: u32, /// Connection keep-alive interval in seconds. pub keep_alive_secs: u64, } impl TransportConfig { /// Create a `TransportConfig` from a `SwarmKey`, using sensible defaults. pub fn from_swarm_key(swarm_key: &SwarmKey) -> Self { TransportConfig { swarm_key_fingerprint: swarm_key.fingerprint(), max_connections_in: 50, max_connections_out: 50, keep_alive_secs: 20, } } /// Return the user-agent string that encodes the swarm key fingerprint. /// /// Format: `chronogrid//swarm:` /// /// This string is exchanged during the `identify` protocol handshake. /// Peers validate that the fingerprint matches their own swarm key before /// accepting the connection into the cluster. pub fn user_agent(&self) -> String { format!( "chronogrid/{}/swarm:{}", env!("CARGO_PKG_VERSION"), self.swarm_key_fingerprint ) } /// Extract the swarm key fingerprint from a user-agent string produced by /// [`user_agent`](Self::user_agent). /// /// Returns `None` if the string does not match the expected format. pub fn extract_fingerprint(user_agent: &str) -> Option<&str> { // Format: "chronogrid//swarm:" let swarm_part = user_agent.split('/').find(|s| s.starts_with("swarm:"))?; Some(swarm_part.trim_start_matches("swarm:")) } /// Validate that a remote peer's user-agent string carries the correct /// swarm key fingerprint. /// /// Returns `true` if the fingerprint matches, `false` otherwise. pub fn validate_peer_user_agent(&self, remote_user_agent: &str) -> bool { match Self::extract_fingerprint(remote_user_agent) { Some(fp) => fp == self.swarm_key_fingerprint, None => false, } } } // --------------------------------------------------------------------------- // build_transport // --------------------------------------------------------------------------- /// Build a libp2p TCP + Noise XK + Yamux transport stack. /// /// This function constructs the **TCP fallback transport** used when QUIC is /// unavailable. The primary QUIC transport is configured directly via /// `SwarmBuilder::with_quic()` in `SwarmManager` (Task 2.5). /// /// # Transport stack /// /// ```text /// TCP (tokio) /// └─ Noise XK (Ed25519 keypair from NodeIdentity) /// └─ Yamux stream multiplexer /// ``` /// /// # Parameters /// /// - `identity` — The node's Ed25519 keypair, used for Noise authentication. /// - `_swarm_key` — The cluster pre-shared key. Its fingerprint is validated /// at the application layer via the `identify` protocol (see /// [`TransportConfig::validate_peer_user_agent`]). The raw key bytes are /// reserved for a future libp2p private-network transport layer. /// /// # Errors /// /// Returns [`NetError::Transport`] if the Noise keypair cannot be constructed /// from the provided identity. pub fn build_transport( identity: &NodeIdentity, _swarm_key: &SwarmKey, ) -> NetResult> { // Convert the ed25519-dalek SigningKey to a libp2p Keypair. let libp2p_keypair = dalek_to_libp2p_keypair(identity)?; // Noise authentication configuration (XX pattern for TCP fallback; // QUIC uses its own built-in security layer). let noise_config = noise::Config::new(&libp2p_keypair) .map_err(|e| NetError::Transport(format!("noise config error: {e}")))?; // Yamux multiplexer with default configuration. let yamux_config = yamux::Config::default(); // Build the TCP transport with Noise + Yamux. let transport = tcp::tokio::Transport::new(tcp::Config::default().nodelay(true)) .upgrade(upgrade::Version::V1Lazy) .authenticate(noise_config) .multiplex(yamux_config) .boxed(); Ok(transport) } // --------------------------------------------------------------------------- // Keypair conversion helper // --------------------------------------------------------------------------- /// Convert an `ed25519-dalek` `SigningKey` to a `libp2p` `Keypair`. /// /// libp2p uses its own `identity::Keypair` type internally. We extract the /// raw 32-byte seed from the dalek key and re-import it into libp2p's Ed25519 /// key type to ensure compatibility. pub(crate) fn dalek_to_libp2p_keypair(identity: &NodeIdentity) -> NetResult { // Extract the 32-byte secret seed from the dalek SigningKey. let secret_bytes = identity.signing_key().to_bytes(); // libp2p's Ed25519 SecretKey is constructed from the 32-byte seed. let secret = libp2p::identity::ed25519::SecretKey::try_from_bytes(secret_bytes) .map_err(|e| NetError::Transport(format!("ed25519 secret key conversion failed: {e}")))?; let kp = libp2p::identity::ed25519::Keypair::from(secret); Ok(Keypair::from(kp)) } /// Derive the libp2p `PeerId` from a `NodeIdentity`. /// /// The `PeerId` is the libp2p-canonical identifier derived from the Ed25519 /// public key. It differs from `NodeId` (which is SHA-256 of the public key) /// but both are stable identifiers for the same node. pub fn peer_id_from_identity(identity: &NodeIdentity) -> NetResult { let keypair = dalek_to_libp2p_keypair(identity)?; Ok(PeerId::from(keypair.public())) } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; use crate::identity::generate_node_identity; // ── TransportConfig tests ──────────────────────────────────────────────── #[test] fn test_transport_config_from_swarm_key() { let key = SwarmKey::generate(); let config = TransportConfig::from_swarm_key(&key); assert_eq!(config.swarm_key_fingerprint, key.fingerprint()); assert_eq!(config.max_connections_in, 50); assert_eq!(config.max_connections_out, 50); assert_eq!(config.keep_alive_secs, 20); } #[test] fn test_user_agent_contains_fingerprint() { let key = SwarmKey::generate(); let config = TransportConfig::from_swarm_key(&key); let ua = config.user_agent(); assert!(ua.contains(&key.fingerprint()), "user_agent must contain fingerprint"); assert!(ua.starts_with("chronogrid/"), "user_agent must start with 'chronogrid/'"); assert!(ua.contains("swarm:"), "user_agent must contain 'swarm:' prefix"); } #[test] fn test_extract_fingerprint_valid() { let key = SwarmKey::generate(); let config = TransportConfig::from_swarm_key(&key); let ua = config.user_agent(); let extracted = TransportConfig::extract_fingerprint(&ua); assert_eq!(extracted, Some(key.fingerprint().as_str())); } #[test] fn test_extract_fingerprint_invalid_format() { let result = TransportConfig::extract_fingerprint("some-other-agent/1.0"); assert_eq!(result, None); } #[test] fn test_validate_peer_user_agent_matching_key() { let key = SwarmKey::generate(); let config = TransportConfig::from_swarm_key(&key); let ua = config.user_agent(); assert!(config.validate_peer_user_agent(&ua)); } #[test] fn test_validate_peer_user_agent_wrong_key() { let key1 = SwarmKey::generate(); let key2 = SwarmKey::generate(); let config1 = TransportConfig::from_swarm_key(&key1); let config2 = TransportConfig::from_swarm_key(&key2); // config1 should reject config2's user-agent (different fingerprint). let ua2 = config2.user_agent(); assert!(!config1.validate_peer_user_agent(&ua2)); } #[test] fn test_validate_peer_user_agent_empty_string() { let key = SwarmKey::generate(); let config = TransportConfig::from_swarm_key(&key); assert!(!config.validate_peer_user_agent("")); } // ── Keypair conversion tests ───────────────────────────────────────────── #[test] fn test_dalek_to_libp2p_keypair_succeeds() { let identity = generate_node_identity(); let result = dalek_to_libp2p_keypair(&identity); assert!(result.is_ok(), "keypair conversion must succeed: {:?}", result); } #[test] fn test_dalek_to_libp2p_keypair_is_deterministic() { let identity = generate_node_identity(); let kp1 = dalek_to_libp2p_keypair(&identity).unwrap(); let kp2 = dalek_to_libp2p_keypair(&identity).unwrap(); // Both keypairs must produce the same PeerId. assert_eq!( PeerId::from(kp1.public()), PeerId::from(kp2.public()), "same identity must produce same PeerId" ); } #[test] fn test_peer_id_from_identity_is_stable() { let identity = generate_node_identity(); let pid1 = peer_id_from_identity(&identity).unwrap(); let pid2 = peer_id_from_identity(&identity).unwrap(); assert_eq!(pid1, pid2); } #[test] fn test_different_identities_produce_different_peer_ids() { let id1 = generate_node_identity(); let id2 = generate_node_identity(); let pid1 = peer_id_from_identity(&id1).unwrap(); let pid2 = peer_id_from_identity(&id2).unwrap(); assert_ne!(pid1, pid2); } // ── Transport construction test ────────────────────────────────────────── #[test] fn test_build_transport_succeeds() { let identity = generate_node_identity(); let swarm_key = SwarmKey::generate(); let result = build_transport(&identity, &swarm_key); assert!( result.is_ok(), "build_transport must succeed: {:?}", result.err() ); } #[test] fn test_build_transport_different_identities() { // Two different identities must each produce a valid transport. let id1 = generate_node_identity(); let id2 = generate_node_identity(); let key = SwarmKey::generate(); assert!(build_transport(&id1, &key).is_ok()); assert!(build_transport(&id2, &key).is_ok()); } }