3939//! automatically enables saorsa-transport's prometheus metrics collection.
4040
4141use crate :: error:: { GeoRejectionError , GeographicConfig } ;
42+ use crate :: quantum_crypto:: saorsa_transport_integration:: { MlDsaPublicKey , MlDsaSecretKey } ;
4243use crate :: transport:: observed_address_cache:: ObservedAddressCache ;
4344use anyhow:: { Context , Result } ;
4445use std:: collections:: HashMap ;
@@ -188,16 +189,30 @@ impl P2PNetworkNode<P2pLinkTransport> {
188189 max_connections : usize ,
189190 max_msg_size : Option < usize > ,
190191 ) -> Result < Self > {
191- Self :: new_with_options ( bind_addr, max_connections, max_msg_size, false ) . await
192+ Self :: new_with_options ( bind_addr, max_connections, max_msg_size, false , None ) . await
192193 }
193194
194195 /// Create a new P2P network node with full control over connection
195- /// limits, message size, and loopback address acceptance.
196+ /// limits, message size, loopback acceptance, and TLS keypair injection.
197+ ///
198+ /// When `keypair` is `Some`, the supplied ML-DSA-65 keypair is installed
199+ /// as the transport's TLS identity, so the SPKI carried in every QUIC
200+ /// handshake authenticates the same peer ID that signs application
201+ /// messages. saorsa-core threads its `NodeIdentity` keys through here so
202+ /// the lifecycle monitor can derive the app-level peer ID directly from
203+ /// the TLS-handshake bytes — no separate identity-announce protocol is
204+ /// required.
205+ ///
206+ /// When `keypair` is `None`, saorsa-transport generates a fresh keypair
207+ /// internally; the resulting peer ID will not match anything stored in
208+ /// saorsa-core, so this branch is only suitable for tests that don't
209+ /// cross the identity boundary.
196210 pub async fn new_with_options (
197211 bind_addr : SocketAddr ,
198212 max_connections : usize ,
199213 max_msg_size : Option < usize > ,
200214 allow_loopback : bool ,
215+ keypair : Option < ( MlDsaPublicKey , MlDsaSecretKey ) > ,
201216 ) -> Result < Self > {
202217 let mut builder = P2pConfig :: builder ( )
203218 . bind_addr ( bind_addr)
@@ -213,6 +228,9 @@ impl P2PNetworkNode<P2pLinkTransport> {
213228 ..NatConfig :: default ( )
214229 } ) ;
215230 }
231+ if let Some ( ( public_key, secret_key) ) = keypair {
232+ builder = builder. keypair ( public_key, secret_key) ;
233+ }
216234 let config = builder
217235 . build ( )
218236 . map_err ( |e| anyhow:: anyhow!( "Failed to build P2P config: {}" , e) ) ?;
@@ -856,18 +874,30 @@ impl DualStackNetworkNode<P2pLinkTransport> {
856874 }
857875 }
858876
859- /// Set a preferred coordinator for hole-punching to a specific target.
860- /// The preferred coordinator is a peer that referred us to the target
861- /// during a DHT lookup, so it has a connection to the target.
862- pub async fn set_hole_punch_preferred_coordinator (
877+ /// Set an ordered list of preferred coordinators for hole-punching to a
878+ /// specific target.
879+ ///
880+ /// The list is iterated front to back at hole-punch time: every
881+ /// coordinator except the last gets a short per-attempt timeout
882+ /// (~1.5s) so a busy or unreachable referrer is abandoned quickly,
883+ /// and the final coordinator gets the strategy's full hole-punch
884+ /// timeout to give it time to actually complete the punch.
885+ ///
886+ /// The caller (`DhtNetworkManager::dial_candidate`) is expected to
887+ /// rank the list best-first using DHT signals — round observed,
888+ /// trust score, etc. — via [`DhtNetworkManager::rank_referrers`].
889+ ///
890+ /// Empty `coordinators` removes any preferred coordinators for
891+ /// `target`.
892+ pub async fn set_hole_punch_preferred_coordinators (
863893 & self ,
864894 target : SocketAddr ,
865- coordinator : SocketAddr ,
895+ coordinators : Vec < SocketAddr > ,
866896 ) {
867897 for node in [ & self . v6 , & self . v4 ] . into_iter ( ) . flatten ( ) {
868898 node. transport
869899 . endpoint ( )
870- . set_hole_punch_preferred_coordinator ( target, coordinator )
900+ . set_hole_punch_preferred_coordinators ( target, coordinators . clone ( ) )
871901 . await ;
872902 }
873903 }
@@ -1063,17 +1093,22 @@ impl DualStackNetworkNode<P2pLinkTransport> {
10631093 max_connections : usize ,
10641094 max_msg_size : Option < usize > ,
10651095 ) -> Result < Self > {
1066- Self :: new_with_options ( v6_addr, v4_addr, max_connections, max_msg_size, false ) . await
1096+ Self :: new_with_options ( v6_addr, v4_addr, max_connections, max_msg_size, false , None ) . await
10671097 }
10681098
10691099 /// Create dual nodes with full control over connection limits, message
1070- /// size, and loopback address acceptance.
1100+ /// size, loopback acceptance, and TLS keypair injection.
1101+ ///
1102+ /// When `keypair` is `Some`, both stacks share the same ML-DSA-65
1103+ /// identity, so the SPKI carried in every QUIC handshake authenticates
1104+ /// the same peer ID regardless of which stack the connection arrived on.
10711105 pub async fn new_with_options (
10721106 v6_addr : Option < SocketAddr > ,
10731107 v4_addr : Option < SocketAddr > ,
10741108 max_connections : usize ,
10751109 max_msg_size : Option < usize > ,
10761110 allow_loopback : bool ,
1111+ keypair : Option < ( MlDsaPublicKey , MlDsaSecretKey ) > ,
10771112 ) -> Result < Self > {
10781113 let v6 = if let Some ( addr) = v6_addr {
10791114 Some (
@@ -1082,6 +1117,7 @@ impl DualStackNetworkNode<P2pLinkTransport> {
10821117 max_connections,
10831118 max_msg_size,
10841119 allow_loopback,
1120+ keypair. clone ( ) ,
10851121 )
10861122 . await ?,
10871123 )
@@ -1094,6 +1130,7 @@ impl DualStackNetworkNode<P2pLinkTransport> {
10941130 max_connections,
10951131 max_msg_size,
10961132 allow_loopback,
1133+ keypair. clone ( ) ,
10971134 )
10981135 . await
10991136 {
0 commit comments