use std::fmt::Display;
use std::hash::Hash as StdHash;
use std::mem;
#[cfg(any(test, feature = "test_utils"))]
use std::net::SocketAddr;
use p2panda_core::cbor::encode_cbor;
use p2panda_core::timestamp::{HybridTimestamp, Timestamp};
use p2panda_core::{Signature, SigningKey};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::NodeId;
use crate::utils::to_verifying_key;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeInfo {
pub node_id: NodeId,
pub bootstrap: bool,
pub metrics: NodeMetrics,
pub transports: Option<TransportInfo>,
}
impl NodeInfo {
pub fn new(node_id: NodeId) -> Self {
Self {
node_id,
bootstrap: false,
transports: None,
metrics: NodeMetrics::default(),
}
}
pub fn bootstrap(mut self) -> Self {
self.bootstrap = true;
self
}
pub fn update_transports(&mut self, other: TransportInfo) -> Result<bool, NodeInfoError> {
other.verify(&self.node_id)?;
let mut is_newer = false;
match self.transports.as_ref() {
None => {
is_newer = true;
self.transports = Some(other)
}
Some(current) => {
if other.timestamp() > current.timestamp() {
self.transports = Some(other);
is_newer = true;
}
}
}
Ok(is_newer)
}
pub fn verify(&self) -> Result<(), NodeInfoError> {
match self.transports {
Some(ref transports) => transports.verify(&self.node_id),
None => Ok(()),
}
}
}
impl TryFrom<NodeInfo> for iroh_base::EndpointAddr {
type Error = NodeInfoError;
fn try_from(node_info: NodeInfo) -> Result<Self, Self::Error> {
let Some(transports) = node_info.transports else {
return Err(NodeInfoError::MissingTransportAddresses);
};
transports
.addresses()
.iter()
.find_map(|address| match address {
TransportAddress::Iroh(endpoint_addr) => Some(endpoint_addr),
#[allow(unreachable_patterns)]
_ => None,
})
.cloned()
.ok_or(NodeInfoError::MissingTransportAddresses)
}
}
impl From<iroh_base::EndpointAddr> for NodeInfo {
fn from(addr: iroh_base::EndpointAddr) -> Self {
let node_id = to_verifying_key(addr.id);
let transports = TransportInfo::from(TrustedTransportInfo::from(addr));
Self {
node_id,
bootstrap: false,
transports: Some(transports),
metrics: NodeMetrics::default(),
}
}
}
impl p2panda_store::address_book::NodeInfo<NodeId> for NodeInfo {
type Transports = AuthenticatedTransportInfo;
fn id(&self) -> NodeId {
self.node_id
}
fn is_bootstrap(&self) -> bool {
self.bootstrap
}
fn is_stale(&self) -> bool {
if self.bootstrap {
false
} else {
self.metrics.is_stale()
}
}
fn transports(&self) -> Option<Self::Transports> {
match &self.transports {
Some(TransportInfo::Authenticated(info)) => Some(info.clone()),
Some(TransportInfo::Trusted(_)) => {
None
}
None => None,
}
}
}
pub trait NodeTransportInfo {
fn timestamp(&self) -> HybridTimestamp;
fn addresses(&self) -> Vec<TransportAddress>;
fn len(&self) -> usize;
fn is_empty(&self) -> bool;
fn verify(&self, node_id: &NodeId) -> Result<(), NodeInfoError>;
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum TransportInfo {
Trusted(TrustedTransportInfo),
Authenticated(AuthenticatedTransportInfo),
}
impl TransportInfo {
pub fn new_trusted() -> TrustedTransportInfo {
TrustedTransportInfo::new()
}
pub fn new_unsigned() -> UnsignedTransportInfo {
UnsignedTransportInfo::new()
}
}
impl NodeTransportInfo for TransportInfo {
fn timestamp(&self) -> HybridTimestamp {
match self {
TransportInfo::Trusted(info) => info.timestamp(),
TransportInfo::Authenticated(info) => info.timestamp(),
}
}
fn addresses(&self) -> Vec<TransportAddress> {
match self {
TransportInfo::Trusted(info) => info.addresses(),
TransportInfo::Authenticated(info) => info.addresses(),
}
}
fn len(&self) -> usize {
match self {
TransportInfo::Trusted(info) => info.addresses.len(),
TransportInfo::Authenticated(info) => info.addresses.len(),
}
}
fn is_empty(&self) -> bool {
match self {
TransportInfo::Trusted(info) => info.addresses.is_empty(),
TransportInfo::Authenticated(info) => info.addresses.is_empty(),
}
}
fn verify(&self, node_id: &NodeId) -> Result<(), NodeInfoError> {
match self {
TransportInfo::Trusted(info) => info.verify(node_id),
TransportInfo::Authenticated(info) => info.verify(node_id),
}
}
}
impl Display for TransportInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransportInfo::Trusted(info) => write!(f, "{info}"),
TransportInfo::Authenticated(info) => write!(f, "{info}"),
}
}
}
impl From<iroh_base::EndpointAddr> for TransportInfo {
fn from(addr: iroh_base::EndpointAddr) -> Self {
Self::from(TrustedTransportInfo::from(addr))
}
}
impl From<AuthenticatedTransportInfo> for TransportInfo {
fn from(value: AuthenticatedTransportInfo) -> Self {
Self::Authenticated(value)
}
}
impl From<TrustedTransportInfo> for TransportInfo {
fn from(value: TrustedTransportInfo) -> Self {
Self::Trusted(value)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthenticatedTransportInfo {
pub timestamp: HybridTimestamp,
pub signature: Signature,
pub addresses: Vec<TransportAddress>,
}
impl AuthenticatedTransportInfo {
pub fn new_unsigned() -> UnsignedTransportInfo {
UnsignedTransportInfo::new()
}
fn to_unsigned(&self) -> UnsignedTransportInfo {
UnsignedTransportInfo {
timestamp: self.timestamp,
addresses: self.addresses.clone(),
}
}
}
impl NodeTransportInfo for AuthenticatedTransportInfo {
fn timestamp(&self) -> HybridTimestamp {
self.timestamp
}
fn addresses(&self) -> Vec<TransportAddress> {
self.addresses.clone()
}
fn len(&self) -> usize {
self.addresses.len()
}
fn is_empty(&self) -> bool {
self.addresses.is_empty()
}
fn verify(&self, node_id: &NodeId) -> Result<(), NodeInfoError> {
let bytes = self.to_unsigned().to_bytes()?;
if !node_id.verify(&bytes, &self.signature) {
Err(NodeInfoError::InvalidSignature)
} else {
Ok(())
}
}
}
impl Display for AuthenticatedTransportInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let addresses = if self.addresses.is_empty() {
"[]".to_string()
} else {
self.addresses.iter().map(|addr| addr.to_string()).collect()
};
write!(
f,
"[authenticated] timestamp={}, addresses={}",
self.timestamp, addresses
)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UnsignedTransportInfo {
pub timestamp: HybridTimestamp,
pub addresses: Vec<TransportAddress>,
}
impl Default for UnsignedTransportInfo {
fn default() -> Self {
Self::new()
}
}
impl UnsignedTransportInfo {
pub fn new() -> Self {
Self {
timestamp: HybridTimestamp::now(),
addresses: vec![],
}
}
pub fn from_addrs(addrs: impl IntoIterator<Item = TransportAddress>) -> Self {
let mut info = Self::new();
for addr in addrs {
info.add_addr(addr);
}
info
}
pub fn add_addr(&mut self, addr: TransportAddress) {
let existing_transport_index =
self.addresses
.iter()
.enumerate()
.find_map(|(index, existing_addr)| {
if mem::discriminant(&addr) == mem::discriminant(existing_addr) {
Some(index)
} else {
None
}
});
if let Some(index) = existing_transport_index {
self.addresses.remove(index);
}
self.addresses.push(addr);
}
fn to_bytes(&self) -> Result<Vec<u8>, NodeInfoError> {
let bytes = encode_cbor(&self)?;
Ok(bytes)
}
pub fn len(&self) -> usize {
self.addresses.len()
}
pub fn is_empty(&self) -> bool {
self.addresses.is_empty()
}
pub fn increment_timestamp(mut self, previous: Option<&AuthenticatedTransportInfo>) -> Self {
match previous {
Some(previous) => {
self.timestamp = previous.timestamp.increment();
self
}
None => self,
}
}
pub fn sign(
self,
signing_key: &SigningKey,
) -> Result<AuthenticatedTransportInfo, NodeInfoError> {
Ok(AuthenticatedTransportInfo {
timestamp: self.timestamp,
signature: {
let bytes = self.to_bytes()?;
signing_key.sign(&bytes)
},
addresses: self.addresses,
})
}
}
impl From<iroh_base::EndpointAddr> for UnsignedTransportInfo {
fn from(addr: iroh_base::EndpointAddr) -> Self {
Self::from_addrs([addr.into()])
}
}
#[derive(Clone, Debug, PartialEq, Eq, StdHash, Serialize, Deserialize)]
pub struct TrustedTransportInfo {
pub timestamp: HybridTimestamp,
pub addresses: Vec<TransportAddress>,
}
impl Default for TrustedTransportInfo {
fn default() -> Self {
Self::new()
}
}
impl TrustedTransportInfo {
pub fn new() -> Self {
Self {
timestamp: HybridTimestamp::now(),
addresses: vec![],
}
}
pub fn from_addrs(addrs: impl IntoIterator<Item = TransportAddress>) -> Self {
let mut info = Self::new();
for addr in addrs {
info.add_addr(addr);
}
info
}
pub fn add_addr(&mut self, addr: TransportAddress) {
let existing_transport_index =
self.addresses
.iter()
.enumerate()
.find_map(|(index, existing_addr)| {
if mem::discriminant(&addr) == mem::discriminant(existing_addr) {
Some(index)
} else {
None
}
});
if let Some(index) = existing_transport_index {
self.addresses.remove(index);
}
self.addresses.push(addr);
}
}
impl NodeTransportInfo for TrustedTransportInfo {
fn timestamp(&self) -> HybridTimestamp {
self.timestamp
}
fn addresses(&self) -> Vec<TransportAddress> {
self.addresses.clone()
}
fn len(&self) -> usize {
self.addresses.len()
}
fn is_empty(&self) -> bool {
self.addresses.is_empty()
}
fn verify(&self, node_id: &NodeId) -> Result<(), NodeInfoError> {
for address in &self.addresses {
address.verify(node_id)?;
}
Ok(())
}
}
impl From<iroh_base::EndpointAddr> for TrustedTransportInfo {
fn from(addr: iroh_base::EndpointAddr) -> Self {
Self::from_addrs([addr.into()])
}
}
impl Display for TrustedTransportInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let addresses = if self.addresses.is_empty() {
"[]".to_string()
} else {
self.addresses.iter().map(|addr| addr.to_string()).collect()
};
write!(
f,
"[trusted] timestamp={}, addresses={}",
self.timestamp, addresses
)
}
}
#[derive(Clone, Debug, PartialEq, Eq, StdHash, Serialize, Deserialize)]
pub enum TransportAddress {
Iroh(iroh_base::EndpointAddr),
}
impl TransportAddress {
#[cfg(any(test, feature = "test_utils"))]
pub fn from_iroh(
node_id: NodeId,
relay_url: Option<iroh_base::RelayUrl>,
direct_addresses: impl IntoIterator<Item = SocketAddr>,
) -> Self {
let transport_addrs = direct_addresses
.into_iter()
.map(iroh_base::TransportAddr::Ip);
let mut endpoint_addr =
iroh_base::EndpointAddr::new(crate::utils::from_verifying_key(node_id))
.with_addrs(transport_addrs);
if let Some(url) = relay_url {
endpoint_addr = endpoint_addr.with_relay_url(url);
}
Self::Iroh(endpoint_addr)
}
pub fn verify(&self, node_id: &NodeId) -> Result<(), NodeInfoError> {
#[allow(irrefutable_let_patterns)]
if let TransportAddress::Iroh(endpoint_addr) = self
&& &to_verifying_key(endpoint_addr.id) != node_id
{
return Err(NodeInfoError::NodeIdMismatch);
}
Ok(())
}
}
impl From<iroh_base::EndpointAddr> for TransportAddress {
fn from(addr: iroh_base::EndpointAddr) -> Self {
Self::Iroh(addr)
}
}
impl Display for TransportAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransportAddress::Iroh(endpoint_addr) => {
write!(f, "[iroh] {:?}", endpoint_addr.addrs)
}
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct NodeMetrics {
failed_connections: usize,
successful_connections: usize,
last_failed_at: Option<Timestamp>,
last_succeeded_at: Option<Timestamp>,
}
impl NodeMetrics {
pub fn report_failed_connection(&mut self) {
self.failed_connections += 1;
self.last_failed_at = Some(Timestamp::now());
}
pub fn report_successful_connection(&mut self) {
self.successful_connections += 1;
self.last_succeeded_at = Some(Timestamp::now());
}
pub fn is_stale(&self) -> bool {
match (self.last_succeeded_at, self.last_failed_at) {
(None, None) => false,
(None, Some(_)) => true,
(Some(_), None) => false,
(Some(succeeded_at), Some(failed_at)) => succeeded_at < failed_at,
}
}
}
#[derive(Debug, Error)]
pub enum NodeInfoError {
#[error("missing or invalid signature")]
InvalidSignature,
#[error("no addresses given for this transport")]
MissingTransportAddresses,
#[error("node id of given transport info does not match")]
NodeIdMismatch,
#[error(transparent)]
Encode(#[from] p2panda_core::cbor::EncodeError),
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use mock_instant::thread_local::MockClock;
use p2panda_core::SigningKey;
use crate::addrs::NodeTransportInfo;
use super::{
AuthenticatedTransportInfo, NodeInfo, NodeMetrics, TransportAddress, UnsignedTransportInfo,
};
#[test]
fn deduplicate_transport_address() {
let signing_key_1 = SigningKey::generate();
let node_id_1 = signing_key_1.verifying_key();
let mut info = AuthenticatedTransportInfo::new_unsigned();
info.add_addr(TransportAddress::from_iroh(node_id_1, None, []));
info.add_addr(TransportAddress::from_iroh(
node_id_1,
Some("https://my.relay.net".parse().unwrap()),
[],
));
assert_eq!(info.len(), 1);
}
#[test]
fn authenticate_address_infos() {
let signing_key_1 = SigningKey::generate();
let node_id_1 = signing_key_1.verifying_key();
let mut unsigned = UnsignedTransportInfo::new();
unsigned.add_addr(TransportAddress::from_iroh(
node_id_1,
Some("https://my.relay.net".parse().unwrap()),
[],
));
let info = unsigned.sign(&signing_key_1).unwrap();
assert!(info.verify(&node_id_1).is_ok());
let signing_key_2 = SigningKey::generate();
let node_id_2 = signing_key_2.verifying_key();
assert!(info.verify(&node_id_2).is_err());
let mut info = info;
info.addresses.pop().unwrap();
assert!(info.verify(&node_id_1).is_err());
}
#[test]
fn node_id_mismatch() {
let signing_key_1 = SigningKey::generate();
let node_id_1 = signing_key_1.verifying_key();
let signing_key_2 = SigningKey::generate();
let node_id_2 = signing_key_2.verifying_key();
let mut unsigned = UnsignedTransportInfo::new();
unsigned.add_addr(TransportAddress::from_iroh(
node_id_1,
Some("https://my.relay.net".parse().unwrap()),
[],
));
let transport_info = unsigned.sign(&signing_key_1).unwrap();
let mut node_info = NodeInfo {
node_id: node_id_2,
bootstrap: false,
transports: None,
metrics: NodeMetrics::default(),
};
assert!(node_info.verify().is_ok());
assert!(node_info.update_transports(transport_info.into()).is_err());
}
#[test]
fn latest_transport_info_wins() {
let signing_key_1 = SigningKey::generate();
let node_id_1 = signing_key_1.verifying_key();
let transport_info_1 = {
let mut unsigned = UnsignedTransportInfo::new();
unsigned.add_addr(TransportAddress::from_iroh(
node_id_1,
Some("https://my.relay.net".parse().unwrap()),
[],
));
unsigned.timestamp = 2.into(); unsigned.sign(&signing_key_1).unwrap()
};
let transport_info_2 = {
let mut unsigned = UnsignedTransportInfo::new();
unsigned.add_addr(TransportAddress::from_iroh(
node_id_1,
Some("https://my.relay.net".parse().unwrap()),
[],
));
unsigned.timestamp = 1.into(); unsigned.sign(&signing_key_1).unwrap()
};
let mut node_info = NodeInfo {
node_id: node_id_1,
bootstrap: true,
transports: None,
metrics: NodeMetrics::default(),
};
assert!(node_info.verify().is_ok());
assert!(node_info.update_transports(transport_info_1.into()).is_ok());
assert!(node_info.update_transports(transport_info_2.into()).is_ok());
assert_eq!(node_info.transports.as_ref().unwrap().len(), 1);
assert_eq!(node_info.transports.unwrap().timestamp(), 2.into());
}
#[test]
fn stale_nodes() {
let signing_key = SigningKey::generate();
let node_id = signing_key.verifying_key();
let mut node_info = NodeInfo {
node_id,
bootstrap: true,
transports: None,
metrics: NodeMetrics::default(),
};
assert!(!node_info.metrics.is_stale());
node_info.metrics.report_successful_connection();
assert!(!node_info.metrics.is_stale());
MockClock::advance_system_time(Duration::from_secs(1));
node_info.metrics.report_failed_connection();
assert!(node_info.metrics.is_stale());
MockClock::advance_system_time(Duration::from_secs(1));
node_info.metrics.report_successful_connection();
assert!(!node_info.metrics.is_stale());
}
}