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::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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
42pub struct NodeInfo {
43 pub node_id: NodeId,
45
46 pub bootstrap: bool,
55
56 pub metrics: NodeMetrics,
60
61 pub transports: Option<TransportInfo>,
65}
66
67impl NodeInfo {
68 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 pub fn bootstrap(mut self) -> Self {
80 self.bootstrap = true;
81 self
82 }
83
84 pub fn update_transports(&mut self, other: TransportInfo) -> Result<bool, NodeInfoError> {
88 other.verify(&self.node_id)?;
89
90 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 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 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 None
179 }
180 None => None,
181 }
182 }
183}
184
185pub trait NodeTransportInfo {
186 fn timestamp(&self) -> HybridTimestamp;
188
189 fn addresses(&self) -> Vec<TransportAddress>;
191
192 fn len(&self) -> usize;
194
195 fn is_empty(&self) -> bool;
197
198 fn verify(&self, node_id: &NodeId) -> Result<(), NodeInfoError>;
200}
201
202#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
204pub enum TransportInfo {
205 Trusted(TrustedTransportInfo),
212
213 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
295pub struct AuthenticatedTransportInfo {
296 pub timestamp: HybridTimestamp,
300
301 pub signature: Signature,
309
310 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 pub timestamp: HybridTimestamp,
377
378 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 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 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 pub fn increment_timestamp(mut self, previous: Option<&AuthenticatedTransportInfo>) -> Self {
444 match previous {
445 Some(previous) => {
446 self.timestamp = previous.timestamp.increment();
452 self
453 }
454 None => self,
455 }
456 }
457
458 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#[derive(Clone, Debug, PartialEq, Eq, StdHash, Serialize, Deserialize)]
487pub struct TrustedTransportInfo {
488 pub timestamp: HybridTimestamp,
492
493 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 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#[derive(Clone, Debug, PartialEq, Eq, StdHash, Serialize, Deserialize)]
596pub enum TransportAddress {
597 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 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#[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 pub fn report_failed_connection(&mut self) {
671 self.failed_connections += 1;
672 self.last_failed_at = Some(Timestamp::now());
673 }
674
675 pub fn report_successful_connection(&mut self) {
677 self.successful_connections += 1;
678 self.last_succeeded_at = Some(Timestamp::now());
679 }
680
681 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 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 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 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 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 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 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(); unsigned.sign(&signing_key_1).unwrap()
806 };
807
808 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(); unsigned.sign(&signing_key_1).unwrap()
818 };
819
820 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 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 assert!(!node_info.metrics.is_stale());
850
851 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_info.metrics.report_failed_connection();
859 assert!(node_info.metrics.is_stale());
860
861 MockClock::advance_system_time(Duration::from_secs(1));
862
863 node_info.metrics.report_successful_connection();
865 assert!(!node_info.metrics.is_stale());
866 }
867}