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