1use 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#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct NodeInfo {
47 pub node_id: NodeId,
49
50 pub bootstrap: bool,
59
60 pub metrics: NodeMetrics,
64
65 pub transports: Option<TransportInfo>,
69}
70
71impl NodeInfo {
72 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 pub fn bootstrap(mut self) -> Self {
84 self.bootstrap = true;
85 self
86 }
87
88 pub fn update_transports(&mut self, other: TransportInfo) -> Result<bool, NodeInfoError> {
92 other.verify(&self.node_id)?;
93
94 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 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 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 None
185 }
186 None => None,
187 }
188 }
189}
190
191pub trait NodeTransportInfo {
192 fn timestamp(&self) -> HybridTimestamp;
194
195 fn addresses(&self) -> Vec<TransportAddress>;
197
198 fn len(&self) -> usize;
200
201 fn is_empty(&self) -> bool;
203
204 fn verify(&self, node_id: &NodeId) -> Result<(), NodeInfoError>;
206}
207
208#[derive(Clone, Debug, PartialEq, Eq)]
210pub enum TransportInfo {
211 Trusted(TrustedTransportInfo),
218
219 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
301pub struct AuthenticatedTransportInfo {
302 pub timestamp: HybridTimestamp,
306
307 pub signature: Signature,
315
316 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 pub timestamp: HybridTimestamp,
383
384 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 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 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 pub fn increment_timestamp(mut self, previous: Option<&AuthenticatedTransportInfo>) -> Self {
450 match previous {
451 Some(previous) => {
452 self.timestamp = previous.timestamp.increment();
458 self
459 }
460 None => self,
461 }
462 }
463
464 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#[derive(Clone, Debug, PartialEq, Eq, StdHash, Serialize, Deserialize)]
494pub struct TrustedTransportInfo {
495 pub timestamp: HybridTimestamp,
499
500 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 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#[derive(Clone, Debug, PartialEq, Eq, StdHash, Serialize, Deserialize)]
604pub enum TransportAddress {
605 #[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 #[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#[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 pub fn report_failed_connection(&mut self) {
680 self.failed_connections += 1;
681 self.last_failed_at = Some(Timestamp::now());
682 }
683
684 pub fn report_successful_connection(&mut self) {
686 self.successful_connections += 1;
687 self.last_succeeded_at = Some(Timestamp::now());
688 }
689
690 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 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 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 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 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 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 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(); unsigned.sign(&signing_key_1).unwrap()
815 };
816
817 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(); unsigned.sign(&signing_key_1).unwrap()
827 };
828
829 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 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 assert!(!node_info.metrics.is_stale());
859
860 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_info.metrics.report_failed_connection();
868 assert!(node_info.metrics.is_stale());
869
870 MockClock::advance_system_time(Duration::from_secs(1));
871
872 node_info.metrics.report_successful_connection();
874 assert!(!node_info.metrics.is_stale());
875 }
876}