Skip to main content

p2panda_net/
addrs.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Types representing node information and transport addresses.
4//!
5//! ## Example
6//!
7//! Create new bootstrap node with attached transport information for iroh:
8//!
9//! ```rust
10//! use p2panda_net::addrs::NodeInfo;
11//!
12//! let node_id = "c0f3ce745cee96e1e9c01a20746cd503bb2199c2459d8ff8697f5edb30569101"
13//!     .parse()
14//!     .expect("valid hex-encoded Ed25519 public key");
15//! let relay_url = "https://my.relay.org".parse().expect("valid relay url");
16//!
17//! let endpoint_addr = iroh_base::EndpointAddr::new(node_id)
18//!    .with_relay_url(relay_url);
19//! let bootstrap_node = NodeInfo::from(endpoint_addr).bootstrap();
20//! ```
21use std::fmt::Display;
22use std::hash::Hash as StdHash;
23use std::mem;
24#[cfg(any(test, feature = "test_utils"))]
25use std::net::SocketAddr;
26
27use p2panda_core::cbor::encode_cbor;
28use p2panda_core::timestamp::{HybridTimestamp, Timestamp};
29use p2panda_core::{Signature, SigningKey};
30use serde::{Deserialize, Serialize};
31use thiserror::Error;
32
33use crate::NodeId;
34use crate::utils::to_verifying_key;
35
36/// Record of a node we store locally in the address book.
37///
38/// Node information associates configuration, metrics and transport information with a node id.
39/// Since the associated information is mostly for our own local use, we can consider `NodeInfo` to
40/// be private data.
41#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
42pub struct NodeInfo {
43    /// Unique identifier (Ed25519 public key) of this node.
44    pub node_id: NodeId,
45
46    /// Use node as a "bootstrap".
47    ///
48    /// Bootstraps are prioritized during discovery as they are considered "more reliable" and
49    /// faster to reach than other nodes. Usually they are behind a static IP address and are
50    /// always online.
51    ///
52    /// This is a local configuration and is not exchanged during discovery. Every node can decide
53    /// themselves which other node they consider a bootstrap or not.
54    pub bootstrap: bool,
55
56    /// Records of successful or failed connection attempts with this node.
57    ///
58    /// This is useful to understand if we can consider this node as "stale" or not.
59    pub metrics: NodeMetrics,
60
61    /// Transport protocols we can use to connect to this node.
62    ///
63    /// If `None` then no information was received and we can't connect yet.
64    pub transports: Option<TransportInfo>,
65}
66
67impl NodeInfo {
68    /// Returns new `NodeInfo` with default values.
69    pub fn new(node_id: NodeId) -> Self {
70        Self {
71            node_id,
72            bootstrap: false,
73            transports: None,
74            metrics: NodeMetrics::default(),
75        }
76    }
77
78    /// Use this node as a "bootstrap".
79    pub fn bootstrap(mut self) -> Self {
80        self.bootstrap = true;
81        self
82    }
83
84    /// Updates transport info for a node if it is newer ("last-write wins" principle).
85    ///
86    /// Returns true if given transport info is newer than the current one.
87    pub fn update_transports(&mut self, other: TransportInfo) -> Result<bool, NodeInfoError> {
88        other.verify(&self.node_id)?;
89
90        // Choose "latest" info by checking timestamp if given.
91        let mut is_newer = false;
92        match self.transports.as_ref() {
93            None => {
94                is_newer = true;
95                self.transports = Some(other)
96            }
97            Some(current) => {
98                if other.timestamp() > current.timestamp() {
99                    self.transports = Some(other);
100                    is_newer = true;
101                }
102            }
103        }
104
105        Ok(is_newer)
106    }
107
108    /// Checks authenticity of associated transport information.
109    pub fn verify(&self) -> Result<(), NodeInfoError> {
110        match self.transports {
111            Some(ref transports) => transports.verify(&self.node_id),
112            None => Ok(()),
113        }
114    }
115}
116
117impl TryFrom<NodeInfo> for iroh_base::EndpointAddr {
118    type Error = NodeInfoError;
119
120    fn try_from(node_info: NodeInfo) -> Result<Self, Self::Error> {
121        let Some(transports) = node_info.transports else {
122            return Err(NodeInfoError::MissingTransportAddresses);
123        };
124
125        transports
126            .addresses()
127            .iter()
128            .find_map(|address| match address {
129                TransportAddress::Iroh(endpoint_addr) => Some(endpoint_addr),
130                #[allow(unreachable_patterns)]
131                _ => None,
132            })
133            .cloned()
134            .ok_or(NodeInfoError::MissingTransportAddresses)
135    }
136}
137
138impl From<iroh_base::EndpointAddr> for NodeInfo {
139    fn from(addr: iroh_base::EndpointAddr) -> Self {
140        let node_id = to_verifying_key(addr.id);
141        let transports = TransportInfo::from(TrustedTransportInfo::from(addr));
142
143        Self {
144            node_id,
145            bootstrap: false,
146            transports: Some(transports),
147            metrics: NodeMetrics::default(),
148        }
149    }
150}
151
152impl p2panda_store::address_book::NodeInfo<NodeId> for NodeInfo {
153    type Transports = AuthenticatedTransportInfo;
154
155    fn id(&self) -> NodeId {
156        self.node_id
157    }
158
159    fn is_bootstrap(&self) -> bool {
160        self.bootstrap
161    }
162
163    fn is_stale(&self) -> bool {
164        if self.bootstrap {
165            // Bootstrap nodes can never be marked as stale.
166            false
167        } else {
168            self.metrics.is_stale()
169        }
170    }
171
172    fn transports(&self) -> Option<Self::Transports> {
173        match &self.transports {
174            Some(TransportInfo::Authenticated(info)) => Some(info.clone()),
175            Some(TransportInfo::Trusted(_)) => {
176                // "Trusted" information is _not_ authenticated and can not be used for discovery
177                // services as the origin of the data can't be verified by other parties.
178                None
179            }
180            None => None,
181        }
182    }
183}
184
185pub trait NodeTransportInfo {
186    /// Returns logical timestamp from when this information was created.
187    fn timestamp(&self) -> HybridTimestamp;
188
189    /// Returns all associated transport addresses.
190    fn addresses(&self) -> Vec<TransportAddress>;
191
192    /// Returns number of associated transports for this node.
193    fn len(&self) -> usize;
194
195    /// Returns `false` if no transports are given.
196    fn is_empty(&self) -> bool;
197
198    /// Check authenticity integrity of this information when possible.
199    fn verify(&self, node_id: &NodeId) -> Result<(), NodeInfoError>;
200}
201
202/// Transport protocols information we can use to connect to a node.
203#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
204pub enum TransportInfo {
205    /// Unauthenticated transport info we "trust" to be correct since it came to us via an verified
206    /// side-channel (scanning QR code, sharing in a trusted chat group etc.).
207    ///
208    /// This info is never shared across the network services and is only used _locally_ by our own
209    /// node. See `AuthenticatedTransportInfo` for an alternative which can be automatically
210    /// distributed.
211    Trusted(TrustedTransportInfo),
212
213    /// Signed transport info which can be automatically shared across the network by discovery
214    /// services and "untrusted" intermediaries since the original author is verifiable.
215    Authenticated(AuthenticatedTransportInfo),
216}
217
218impl TransportInfo {
219    pub fn new_trusted() -> TrustedTransportInfo {
220        TrustedTransportInfo::new()
221    }
222
223    pub fn new_unsigned() -> UnsignedTransportInfo {
224        UnsignedTransportInfo::new()
225    }
226}
227
228impl NodeTransportInfo for TransportInfo {
229    fn timestamp(&self) -> HybridTimestamp {
230        match self {
231            TransportInfo::Trusted(info) => info.timestamp(),
232            TransportInfo::Authenticated(info) => info.timestamp(),
233        }
234    }
235
236    fn addresses(&self) -> Vec<TransportAddress> {
237        match self {
238            TransportInfo::Trusted(info) => info.addresses(),
239            TransportInfo::Authenticated(info) => info.addresses(),
240        }
241    }
242
243    fn len(&self) -> usize {
244        match self {
245            TransportInfo::Trusted(info) => info.addresses.len(),
246            TransportInfo::Authenticated(info) => info.addresses.len(),
247        }
248    }
249
250    fn is_empty(&self) -> bool {
251        match self {
252            TransportInfo::Trusted(info) => info.addresses.is_empty(),
253            TransportInfo::Authenticated(info) => info.addresses.is_empty(),
254        }
255    }
256
257    fn verify(&self, node_id: &NodeId) -> Result<(), NodeInfoError> {
258        match self {
259            TransportInfo::Trusted(info) => info.verify(node_id),
260            TransportInfo::Authenticated(info) => info.verify(node_id),
261        }
262    }
263}
264
265impl Display for TransportInfo {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        match self {
268            TransportInfo::Trusted(info) => write!(f, "{info}"),
269            TransportInfo::Authenticated(info) => write!(f, "{info}"),
270        }
271    }
272}
273
274impl From<iroh_base::EndpointAddr> for TransportInfo {
275    fn from(addr: iroh_base::EndpointAddr) -> Self {
276        Self::from(TrustedTransportInfo::from(addr))
277    }
278}
279
280impl From<AuthenticatedTransportInfo> for TransportInfo {
281    fn from(value: AuthenticatedTransportInfo) -> Self {
282        Self::Authenticated(value)
283    }
284}
285
286impl From<TrustedTransportInfo> for TransportInfo {
287    fn from(value: TrustedTransportInfo) -> Self {
288        Self::Trusted(value)
289    }
290}
291
292/// Signed transport info which can be automatically shared across the network by discovery
293/// services and "untrusted" intermediaries since the original author is verifiable.
294#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
295pub struct AuthenticatedTransportInfo {
296    /// Logical timestamp from when this transport information was published.
297    ///
298    /// This can be used to find out which information is the "latest".
299    pub timestamp: HybridTimestamp,
300
301    /// Signature to prove authenticity of this transport information.
302    ///
303    /// Other nodes can validate the authenticity by checking this signature against the associated
304    /// node id and info.
305    ///
306    /// This protects against attacks where nodes maliciously publish wrong information about other
307    /// nodes, for example to make them unreachable due to invalid addresses.
308    pub signature: Signature,
309
310    /// Associated transport addresses to aid establishing a connection to this node.
311    pub addresses: Vec<TransportAddress>,
312}
313
314impl AuthenticatedTransportInfo {
315    pub fn new_unsigned() -> UnsignedTransportInfo {
316        UnsignedTransportInfo::new()
317    }
318
319    fn to_unsigned(&self) -> UnsignedTransportInfo {
320        UnsignedTransportInfo {
321            timestamp: self.timestamp,
322            addresses: self.addresses.clone(),
323        }
324    }
325}
326
327impl NodeTransportInfo for AuthenticatedTransportInfo {
328    fn timestamp(&self) -> HybridTimestamp {
329        self.timestamp
330    }
331
332    fn addresses(&self) -> Vec<TransportAddress> {
333        self.addresses.clone()
334    }
335
336    fn len(&self) -> usize {
337        self.addresses.len()
338    }
339
340    fn is_empty(&self) -> bool {
341        self.addresses.is_empty()
342    }
343
344    fn verify(&self, node_id: &NodeId) -> Result<(), NodeInfoError> {
345        let bytes = self.to_unsigned().to_bytes()?;
346
347        if !node_id.verify(&bytes, &self.signature) {
348            Err(NodeInfoError::InvalidSignature)
349        } else {
350            Ok(())
351        }
352    }
353}
354
355impl Display for AuthenticatedTransportInfo {
356    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357        let addresses = if self.addresses.is_empty() {
358            "[]".to_string()
359        } else {
360            self.addresses.iter().map(|addr| addr.to_string()).collect()
361        };
362
363        write!(
364            f,
365            "[authenticated] timestamp={}, addresses={}",
366            self.timestamp, addresses
367        )
368    }
369}
370
371#[derive(Debug, Serialize, Deserialize)]
372pub struct UnsignedTransportInfo {
373    /// Logical timestamp from when this transport information was published.
374    ///
375    /// This can be used to find out which information is the "latest".
376    pub timestamp: HybridTimestamp,
377
378    /// Associated transport addresses to aid establishing a connection to this node.
379    pub addresses: Vec<TransportAddress>,
380}
381
382impl Default for UnsignedTransportInfo {
383    fn default() -> Self {
384        Self::new()
385    }
386}
387
388impl UnsignedTransportInfo {
389    pub fn new() -> Self {
390        Self {
391            timestamp: HybridTimestamp::now(),
392            addresses: vec![],
393        }
394    }
395
396    pub fn from_addrs(addrs: impl IntoIterator<Item = TransportAddress>) -> Self {
397        let mut info = Self::new();
398        for addr in addrs {
399            info.add_addr(addr);
400        }
401        info
402    }
403
404    /// Add transport address for this node.
405    ///
406    /// This method automatically de-duplicates transports per type and chooses the last-inserted
407    /// one.
408    pub fn add_addr(&mut self, addr: TransportAddress) {
409        let existing_transport_index =
410            self.addresses
411                .iter()
412                .enumerate()
413                .find_map(|(index, existing_addr)| {
414                    if mem::discriminant(&addr) == mem::discriminant(existing_addr) {
415                        Some(index)
416                    } else {
417                        None
418                    }
419                });
420
421        if let Some(index) = existing_transport_index {
422            self.addresses.remove(index);
423        }
424
425        self.addresses.push(addr);
426    }
427
428    fn to_bytes(&self) -> Result<Vec<u8>, NodeInfoError> {
429        let bytes = encode_cbor(&self)?;
430        Ok(bytes)
431    }
432
433    /// Returns number of associated transports for this node.
434    pub fn len(&self) -> usize {
435        self.addresses.len()
436    }
437
438    pub fn is_empty(&self) -> bool {
439        self.addresses.is_empty()
440    }
441
442    /// Increment logical timestamp based on previous clock state (if given).
443    pub fn increment_timestamp(mut self, previous: Option<&AuthenticatedTransportInfo>) -> Self {
444        match previous {
445            Some(previous) => {
446                // The underlying "hybrid" timestamp implementation guarantees to _always_ be
447                // larger than the previous one.
448                //
449                // This is a locally created event and we want to make sure it is _after_ anything
450                // which happened before.
451                self.timestamp = previous.timestamp.increment();
452                self
453            }
454            None => self,
455        }
456    }
457
458    /// Authenticate transport info by signining it with our secret key.
459    pub fn sign(
460        self,
461        signing_key: &SigningKey,
462    ) -> Result<AuthenticatedTransportInfo, NodeInfoError> {
463        Ok(AuthenticatedTransportInfo {
464            timestamp: self.timestamp,
465            signature: {
466                let bytes = self.to_bytes()?;
467                signing_key.sign(&bytes)
468            },
469            addresses: self.addresses,
470        })
471    }
472}
473
474impl From<iroh_base::EndpointAddr> for UnsignedTransportInfo {
475    fn from(addr: iroh_base::EndpointAddr) -> Self {
476        Self::from_addrs([addr.into()])
477    }
478}
479
480/// Unauthenticated transport info we "trust" to be correct since it came to us via an verified
481/// side-channel (scanning QR code, sharing in a trusted chat group etc.).
482///
483/// This info is never shared across the network services and is only used _locally_ by our own
484/// node. See `AuthenticatedTransportInfo` for an alternative which can be automatically
485/// distributed.
486#[derive(Clone, Debug, PartialEq, Eq, StdHash, Serialize, Deserialize)]
487pub struct TrustedTransportInfo {
488    /// Logical timestamp from when this transport information was published.
489    ///
490    /// This can be used to find out which information is the "latest".
491    pub timestamp: HybridTimestamp,
492
493    /// Associated transport addresses to aid establishing a connection to this node.
494    pub addresses: Vec<TransportAddress>,
495}
496
497impl Default for TrustedTransportInfo {
498    fn default() -> Self {
499        Self::new()
500    }
501}
502
503impl TrustedTransportInfo {
504    pub fn new() -> Self {
505        Self {
506            timestamp: HybridTimestamp::now(),
507            addresses: vec![],
508        }
509    }
510
511    pub fn from_addrs(addrs: impl IntoIterator<Item = TransportAddress>) -> Self {
512        let mut info = Self::new();
513        for addr in addrs {
514            info.add_addr(addr);
515        }
516        info
517    }
518
519    /// Add transport address for this node.
520    ///
521    /// This method automatically de-duplicates transports per type and chooses the last-inserted
522    /// one.
523    pub fn add_addr(&mut self, addr: TransportAddress) {
524        let existing_transport_index =
525            self.addresses
526                .iter()
527                .enumerate()
528                .find_map(|(index, existing_addr)| {
529                    if mem::discriminant(&addr) == mem::discriminant(existing_addr) {
530                        Some(index)
531                    } else {
532                        None
533                    }
534                });
535
536        if let Some(index) = existing_transport_index {
537            self.addresses.remove(index);
538        }
539
540        self.addresses.push(addr);
541    }
542}
543
544impl NodeTransportInfo for TrustedTransportInfo {
545    fn timestamp(&self) -> HybridTimestamp {
546        self.timestamp
547    }
548
549    fn addresses(&self) -> Vec<TransportAddress> {
550        self.addresses.clone()
551    }
552
553    fn len(&self) -> usize {
554        self.addresses.len()
555    }
556
557    fn is_empty(&self) -> bool {
558        self.addresses.is_empty()
559    }
560
561    fn verify(&self, node_id: &NodeId) -> Result<(), NodeInfoError> {
562        for address in &self.addresses {
563            address.verify(node_id)?;
564        }
565
566        Ok(())
567    }
568}
569
570impl From<iroh_base::EndpointAddr> for TrustedTransportInfo {
571    fn from(addr: iroh_base::EndpointAddr) -> Self {
572        Self::from_addrs([addr.into()])
573    }
574}
575
576impl Display for TrustedTransportInfo {
577    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
578        let addresses = if self.addresses.is_empty() {
579            "[]".to_string()
580        } else {
581            self.addresses.iter().map(|addr| addr.to_string()).collect()
582        };
583
584        write!(
585            f,
586            "[trusted] timestamp={}, addresses={}",
587            self.timestamp, addresses
588        )
589    }
590}
591
592/// Associated transport addresses to aid establishing a connection to this node.
593///
594/// Currently this only supports using iroh (Internet Protocol) to connect.
595#[derive(Clone, Debug, PartialEq, Eq, StdHash, Serialize, Deserialize)]
596pub enum TransportAddress {
597    /// Information to connect to another node via QUIC / UDP / IP using iroh for holepunching and
598    /// relayed connections as a fallback.
599    ///
600    /// To connect to another node either their "home relay" URL needs to be known (to coordinate
601    /// holepunching or relayed connection fallback) or at least one reachable "direct address"
602    /// (IPv4 or IPv6). If none of these are given, establishing a connection is not possible.
603    Iroh(iroh_base::EndpointAddr),
604}
605
606impl TransportAddress {
607    #[cfg(any(test, feature = "test_utils"))]
608    pub fn from_iroh(
609        node_id: NodeId,
610        relay_url: Option<iroh_base::RelayUrl>,
611        direct_addresses: impl IntoIterator<Item = SocketAddr>,
612    ) -> Self {
613        let transport_addrs = direct_addresses
614            .into_iter()
615            .map(iroh_base::TransportAddr::Ip);
616
617        let mut endpoint_addr =
618            iroh_base::EndpointAddr::new(crate::utils::from_verifying_key(node_id))
619                .with_addrs(transport_addrs);
620
621        if let Some(url) = relay_url {
622            endpoint_addr = endpoint_addr.with_relay_url(url);
623        }
624
625        Self::Iroh(endpoint_addr)
626    }
627
628    pub fn verify(&self, node_id: &NodeId) -> Result<(), NodeInfoError> {
629        #[allow(irrefutable_let_patterns)]
630        // Make sure the given address matches the node id.
631        if let TransportAddress::Iroh(endpoint_addr) = self
632            && &to_verifying_key(endpoint_addr.id) != node_id
633        {
634            return Err(NodeInfoError::NodeIdMismatch);
635        }
636
637        Ok(())
638    }
639}
640
641impl From<iroh_base::EndpointAddr> for TransportAddress {
642    fn from(addr: iroh_base::EndpointAddr) -> Self {
643        Self::Iroh(addr)
644    }
645}
646
647impl Display for TransportAddress {
648    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
649        match self {
650            TransportAddress::Iroh(endpoint_addr) => {
651                write!(f, "[iroh] {:?}", endpoint_addr.addrs)
652            }
653        }
654    }
655}
656
657/// Metrics which are locally recorded for a node.
658///
659/// The recorded information can be used to indicate if a node is "stale" or not.
660#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
661pub struct NodeMetrics {
662    failed_connections: usize,
663    successful_connections: usize,
664    last_failed_at: Option<Timestamp>,
665    last_succeeded_at: Option<Timestamp>,
666}
667
668impl NodeMetrics {
669    /// Records failed connection attempt (both incoming or outgoing).
670    pub fn report_failed_connection(&mut self) {
671        self.failed_connections += 1;
672        self.last_failed_at = Some(Timestamp::now());
673    }
674
675    /// Records successful connection attempt (both incoming or outgoing).
676    pub fn report_successful_connection(&mut self) {
677        self.successful_connections += 1;
678        self.last_succeeded_at = Some(Timestamp::now());
679    }
680
681    /// Returns true if last known connection attempt failed.
682    pub fn is_stale(&self) -> bool {
683        match (self.last_succeeded_at, self.last_failed_at) {
684            (None, None) => false,
685            (None, Some(_)) => true,
686            (Some(_), None) => false,
687            (Some(succeeded_at), Some(failed_at)) => succeeded_at < failed_at,
688        }
689    }
690}
691
692#[derive(Debug, Error)]
693pub enum NodeInfoError {
694    #[error("missing or invalid signature")]
695    InvalidSignature,
696
697    #[error("no addresses given for this transport")]
698    MissingTransportAddresses,
699
700    #[error("node id of given transport info does not match")]
701    NodeIdMismatch,
702
703    #[error(transparent)]
704    Encode(#[from] p2panda_core::cbor::EncodeError),
705}
706
707#[cfg(test)]
708mod tests {
709    use std::time::Duration;
710
711    use mock_instant::thread_local::MockClock;
712    use p2panda_core::SigningKey;
713
714    use crate::addrs::NodeTransportInfo;
715
716    use super::{
717        AuthenticatedTransportInfo, NodeInfo, NodeMetrics, TransportAddress, UnsignedTransportInfo,
718    };
719
720    #[test]
721    fn deduplicate_transport_address() {
722        let signing_key_1 = SigningKey::generate();
723        let node_id_1 = signing_key_1.verifying_key();
724
725        // De-duplicate addresses when transport is the same.
726        let mut info = AuthenticatedTransportInfo::new_unsigned();
727        info.add_addr(TransportAddress::from_iroh(node_id_1, None, []));
728        info.add_addr(TransportAddress::from_iroh(
729            node_id_1,
730            Some("https://my.relay.net".parse().unwrap()),
731            [],
732        ));
733
734        assert_eq!(info.len(), 1);
735    }
736
737    #[test]
738    fn authenticate_address_infos() {
739        let signing_key_1 = SigningKey::generate();
740        let node_id_1 = signing_key_1.verifying_key();
741
742        let mut unsigned = UnsignedTransportInfo::new();
743        unsigned.add_addr(TransportAddress::from_iroh(
744            node_id_1,
745            Some("https://my.relay.net".parse().unwrap()),
746            [],
747        ));
748
749        let info = unsigned.sign(&signing_key_1).unwrap();
750        assert!(info.verify(&node_id_1).is_ok());
751
752        // Fails when node id does not match.
753        let signing_key_2 = SigningKey::generate();
754        let node_id_2 = signing_key_2.verifying_key();
755        assert!(info.verify(&node_id_2).is_err());
756
757        // Fails when information got changed.
758        let mut info = info;
759        info.addresses.pop().unwrap();
760        assert!(info.verify(&node_id_1).is_err());
761    }
762
763    #[test]
764    fn node_id_mismatch() {
765        let signing_key_1 = SigningKey::generate();
766        let node_id_1 = signing_key_1.verifying_key();
767
768        let signing_key_2 = SigningKey::generate();
769        let node_id_2 = signing_key_2.verifying_key();
770
771        // Create transport info for node 1.
772        let mut unsigned = UnsignedTransportInfo::new();
773        unsigned.add_addr(TransportAddress::from_iroh(
774            node_id_1,
775            Some("https://my.relay.net".parse().unwrap()),
776            [],
777        ));
778        let transport_info = unsigned.sign(&signing_key_1).unwrap();
779
780        // Create info for node 2 and try to add unrelated transport info.
781        let mut node_info = NodeInfo {
782            node_id: node_id_2,
783            bootstrap: false,
784            transports: None,
785            metrics: NodeMetrics::default(),
786        };
787        assert!(node_info.verify().is_ok());
788        assert!(node_info.update_transports(transport_info.into()).is_err());
789    }
790
791    #[test]
792    fn latest_transport_info_wins() {
793        let signing_key_1 = SigningKey::generate();
794        let node_id_1 = signing_key_1.verifying_key();
795
796        // Create "newer" transport info.
797        let transport_info_1 = {
798            let mut unsigned = UnsignedTransportInfo::new();
799            unsigned.add_addr(TransportAddress::from_iroh(
800                node_id_1,
801                Some("https://my.relay.net".parse().unwrap()),
802                [],
803            ));
804            unsigned.timestamp = 2.into(); // Force "newer" timestamp.
805            unsigned.sign(&signing_key_1).unwrap()
806        };
807
808        // Create "older" transport info.
809        let transport_info_2 = {
810            let mut unsigned = UnsignedTransportInfo::new();
811            unsigned.add_addr(TransportAddress::from_iroh(
812                node_id_1,
813                Some("https://my.relay.net".parse().unwrap()),
814                [],
815            ));
816            unsigned.timestamp = 1.into(); // Force "older" timestamp.
817            unsigned.sign(&signing_key_1).unwrap()
818        };
819
820        // Register both transport infos with node.
821        let mut node_info = NodeInfo {
822            node_id: node_id_1,
823            bootstrap: true,
824            transports: None,
825            metrics: NodeMetrics::default(),
826        };
827        assert!(node_info.verify().is_ok());
828        assert!(node_info.update_transports(transport_info_1.into()).is_ok());
829        assert!(node_info.update_transports(transport_info_2.into()).is_ok());
830
831        // The "newer" transport info is the only one registered.
832        assert_eq!(node_info.transports.as_ref().unwrap().len(), 1);
833        assert_eq!(node_info.transports.unwrap().timestamp(), 2.into());
834    }
835
836    #[test]
837    fn stale_nodes() {
838        let signing_key = SigningKey::generate();
839        let node_id = signing_key.verifying_key();
840
841        let mut node_info = NodeInfo {
842            node_id,
843            bootstrap: true,
844            transports: None,
845            metrics: NodeMetrics::default(),
846        };
847
848        // Node is not stale by default.
849        assert!(!node_info.metrics.is_stale());
850
851        // Node is not stale after reporting successful connection attempt.
852        node_info.metrics.report_successful_connection();
853        assert!(!node_info.metrics.is_stale());
854
855        MockClock::advance_system_time(Duration::from_secs(1));
856
857        // Node is stale after reporting failed connection attempt.
858        node_info.metrics.report_failed_connection();
859        assert!(node_info.metrics.is_stale());
860
861        MockClock::advance_system_time(Duration::from_secs(1));
862
863        // After a successful connection was reported, it is not stale again.
864        node_info.metrics.report_successful_connection();
865        assert!(!node_info.metrics.is_stale());
866    }
867}