Skip to main content

fips_core/config/
peer.rs

1//! Peer configuration types.
2//!
3//! Known peer definitions with transport addresses and connection policies.
4
5use serde::{Deserialize, Serialize};
6
7/// Connection policy for a peer.
8///
9/// Determines when and how to establish a connection to a peer.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ConnectPolicy {
13    /// Connect to this peer automatically on node startup.
14    /// This is the only policy supported in the initial implementation.
15    #[default]
16    AutoConnect,
17
18    /// Connect only when traffic needs to be routed through this peer (future).
19    OnDemand,
20
21    /// Wait for explicit API call to connect (future).
22    Manual,
23}
24
25/// A transport-specific address for reaching a peer.
26///
27/// Each peer can have multiple addresses across different transports,
28/// allowing fallback if one transport is unavailable.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct PeerAddress {
32    /// Transport type (e.g., "udp", "tor", "ethernet").
33    pub transport: String,
34
35    /// Transport-specific address string.
36    ///
37    /// Format depends on transport type:
38    /// - UDP/TCP: "host:port" — IP address or DNS hostname
39    ///   (e.g., "192.168.1.1:2121" or "peer1.example.com:2121")
40    /// - Ethernet: "interface/mac" (e.g., "eth0/aa:bb:cc:dd:ee:ff")
41    pub addr: String,
42
43    /// Priority for address selection (lower = preferred).
44    /// When multiple addresses are available, lower priority addresses
45    /// are tried first.
46    #[serde(default = "default_priority")]
47    pub priority: u8,
48
49    /// Wall-clock observation timestamp (Unix ms) for ranking by recency.
50    ///
51    /// `None` means "no freshness signal" — typically an operator-edited
52    /// static config. The dialer sorts candidates by this field descending
53    /// (most recent first) and tries every address in one pass; `None`
54    /// values sort last. Skipped from serde so that round-tripping a
55    /// config file doesn't produce noisy empty fields.
56    ///
57    /// Excluded from `PartialEq`: refreshing the timestamp on a peer that's
58    /// otherwise unchanged should not flag it as "updated" in
59    /// [`crate::endpoint::FipsEndpoint::update_peers`]'s diff.
60    #[serde(default, skip_serializing_if = "Option::is_none", skip_deserializing)]
61    pub seen_at_ms: Option<u64>,
62}
63
64impl PartialEq for PeerAddress {
65    fn eq(&self, other: &Self) -> bool {
66        self.transport == other.transport
67            && self.addr == other.addr
68            && self.priority == other.priority
69    }
70}
71
72impl Eq for PeerAddress {}
73
74fn default_priority() -> u8 {
75    100
76}
77
78fn default_auto_reconnect() -> bool {
79    true
80}
81
82impl PeerAddress {
83    /// Create a new peer address.
84    pub fn new(transport: impl Into<String>, addr: impl Into<String>) -> Self {
85        Self {
86            transport: transport.into(),
87            addr: addr.into(),
88            priority: default_priority(),
89            seen_at_ms: None,
90        }
91    }
92
93    /// Create a new peer address with priority.
94    pub fn with_priority(
95        transport: impl Into<String>,
96        addr: impl Into<String>,
97        priority: u8,
98    ) -> Self {
99        Self {
100            transport: transport.into(),
101            addr: addr.into(),
102            priority,
103            seen_at_ms: None,
104        }
105    }
106
107    /// Tag this address with a freshness timestamp. Used by the dialer to
108    /// rank candidates from multiple sources (overlay advert, recent-peers
109    /// cache, operator hints) by recency without caring where they came
110    /// from. See [`crate::config::PeerAddress::seen_at_ms`].
111    pub fn with_seen_at_ms(mut self, seen_at_ms: u64) -> Self {
112        self.seen_at_ms = Some(seen_at_ms);
113        self
114    }
115}
116
117/// Configuration for a known peer.
118///
119/// Peers are identified by their Nostr public key (npub) and can have
120/// multiple transport addresses for reaching them.
121#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(deny_unknown_fields)]
123pub struct PeerConfig {
124    /// The peer's Nostr public key in npub (bech32) or hex format.
125    pub npub: String,
126
127    /// Human-readable alias for the peer (optional).
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub alias: Option<String>,
130
131    /// Transport addresses for reaching this peer.
132    ///
133    /// At least one address is required unless Nostr discovery is enabled,
134    /// in which case the address list may be empty and endpoints are
135    /// resolved from the peer's Nostr advert at dial time.
136    #[serde(default)]
137    pub addresses: Vec<PeerAddress>,
138
139    /// Connection policy for this peer.
140    #[serde(default)]
141    pub connect_policy: ConnectPolicy,
142
143    /// Whether to automatically reconnect after link-dead removal.
144    /// When true (default), the node will retry connecting with exponential
145    /// backoff after MMP removes this peer due to liveness timeout.
146    #[serde(default = "default_auto_reconnect")]
147    pub auto_reconnect: bool,
148}
149
150impl Default for PeerConfig {
151    fn default() -> Self {
152        Self {
153            npub: String::new(),
154            alias: None,
155            addresses: Vec::new(),
156            connect_policy: ConnectPolicy::default(),
157            auto_reconnect: default_auto_reconnect(),
158        }
159    }
160}
161
162impl PeerConfig {
163    /// Create a new peer config with a single address.
164    pub fn new(
165        npub: impl Into<String>,
166        transport: impl Into<String>,
167        addr: impl Into<String>,
168    ) -> Self {
169        Self {
170            npub: npub.into(),
171            alias: None,
172            addresses: vec![PeerAddress::new(transport, addr)],
173            connect_policy: ConnectPolicy::default(),
174            auto_reconnect: default_auto_reconnect(),
175        }
176    }
177
178    /// Set an alias for the peer.
179    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
180        self.alias = Some(alias.into());
181        self
182    }
183
184    /// Add an additional address for the peer.
185    pub fn with_address(mut self, addr: PeerAddress) -> Self {
186        self.addresses.push(addr);
187        self
188    }
189
190    /// Get addresses sorted by priority (lowest first).
191    pub fn addresses_by_priority(&self) -> Vec<&PeerAddress> {
192        let mut addrs: Vec<_> = self.addresses.iter().collect();
193        addrs.sort_by_key(|a| a.priority);
194        addrs
195    }
196
197    /// Check if this peer should auto-connect on startup.
198    pub fn is_auto_connect(&self) -> bool {
199        matches!(self.connect_policy, ConnectPolicy::AutoConnect)
200    }
201}