1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
//! Circuit Relay v2 reservation management
//!
//! This module provides [`RelayManager`] which tracks active Circuit Relay v2
//! reservations, enforces a maximum-reservation cap, and evicts stale/expired
//! entries so the node always has a fresh set of relay contacts for NAT
//! traversal.
use parking_lot::RwLock;
use std::collections::HashMap;
// ─── Data types ──────────────────────────────────────────────────────────────
/// State captured when a circuit relay v2 reservation is accepted.
#[derive(Debug, Clone)]
pub struct RelayReservation {
/// Peer ID of the relay (string form of [`libp2p::PeerId`]).
pub relay_peer_id: String,
/// Multiaddr string of the relay.
pub relay_addr: String,
/// Unix timestamp (ms) when the reservation was created.
pub reserved_at_ms: u64,
/// Unix timestamp (ms) when the reservation expires (TTL from relay).
pub expires_at_ms: u64,
/// Optional relay voucher bytes (`None` when the relay did not provide one).
pub voucher: Option<Vec<u8>>,
}
impl RelayReservation {
/// Returns `true` when the reservation has passed its expiry time.
#[inline]
pub fn is_expired(&self, now_ms: u64) -> bool {
now_ms >= self.expires_at_ms
}
/// Remaining lifetime in milliseconds, or `0` if already expired.
#[inline]
pub fn ttl_remaining_ms(&self, now_ms: u64) -> u64 {
self.expires_at_ms.saturating_sub(now_ms)
}
}
// ─── Error type ──────────────────────────────────────────────────────────────
/// Errors returned by [`RelayManager`] operations.
#[derive(Debug)]
pub enum RelayError {
/// The manager has reached its reservation cap.
AtCapacity {
/// Maximum allowed reservations.
max: usize,
},
/// A reservation for this relay peer already exists.
AlreadyExists {
/// The peer that was already registered.
relay_peer_id: String,
},
}
impl std::fmt::Display for RelayError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AtCapacity { max } => {
write!(f, "relay manager at capacity (max {max} reservations)")
}
Self::AlreadyExists { relay_peer_id } => {
write!(
f,
"relay reservation already exists for peer {relay_peer_id}"
)
}
}
}
}
impl std::error::Error for RelayError {}
// ─── RelayManager ────────────────────────────────────────────────────────────
/// Manages circuit relay v2 reservations.
///
/// All public methods are safe to call from multiple threads concurrently; the
/// internal [`RwLock`] is a `parking_lot` lock and never blocks async tasks
/// for long.
pub struct RelayManager {
/// Current reservations, keyed by relay peer ID.
reservations: RwLock<HashMap<String, RelayReservation>>,
/// Maximum number of simultaneously held reservations.
max_reservations: usize,
}
impl RelayManager {
/// Create a new manager with the given capacity limit.
pub fn new(max_reservations: usize) -> Self {
Self {
reservations: RwLock::new(HashMap::new()),
max_reservations,
}
}
/// Insert a new reservation.
///
/// Fails with:
/// - [`RelayError::AlreadyExists`] if `reservation.relay_peer_id` is
/// already tracked.
/// - [`RelayError::AtCapacity`] if the cap would be exceeded.
pub fn add_reservation(&self, reservation: RelayReservation) -> Result<(), RelayError> {
let mut map = self.reservations.write();
if map.contains_key(&reservation.relay_peer_id) {
return Err(RelayError::AlreadyExists {
relay_peer_id: reservation.relay_peer_id.clone(),
});
}
if map.len() >= self.max_reservations {
return Err(RelayError::AtCapacity {
max: self.max_reservations,
});
}
map.insert(reservation.relay_peer_id.clone(), reservation);
Ok(())
}
/// Remove the reservation for `relay_peer_id`.
///
/// Returns `true` if a reservation existed and was removed.
pub fn remove_reservation(&self, relay_peer_id: &str) -> bool {
self.reservations.write().remove(relay_peer_id).is_some()
}
/// Look up a reservation by relay peer ID (cloned snapshot).
pub fn get_reservation(&self, relay_peer_id: &str) -> Option<RelayReservation> {
self.reservations.read().get(relay_peer_id).cloned()
}
/// Return all non-expired reservations as of `now_ms`.
pub fn active_reservations(&self, now_ms: u64) -> Vec<RelayReservation> {
self.reservations
.read()
.values()
.filter(|r| !r.is_expired(now_ms))
.cloned()
.collect()
}
/// Return the peer IDs of all expired reservations.
pub fn expired_reservations(&self, now_ms: u64) -> Vec<String> {
self.reservations
.read()
.values()
.filter(|r| r.is_expired(now_ms))
.map(|r| r.relay_peer_id.clone())
.collect()
}
/// Remove every expired reservation and return the count of removed entries.
pub fn prune_expired(&self, now_ms: u64) -> usize {
let mut map = self.reservations.write();
let before = map.len();
map.retain(|_, r| !r.is_expired(now_ms));
before - map.len()
}
/// Total number of tracked reservations (active + expired).
pub fn reservation_count(&self) -> usize {
self.reservations.read().len()
}
/// Returns `true` when the manager cannot accept any more reservations.
pub fn is_at_capacity(&self) -> bool {
self.reservations.read().len() >= self.max_reservations
}
}
// ─── Tests ───────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
fn make_reservation(peer_id: &str, reserved_at: u64, expires_at: u64) -> RelayReservation {
RelayReservation {
relay_peer_id: peer_id.to_string(),
relay_addr: format!("/ip4/127.0.0.1/tcp/4001/p2p/{peer_id}"),
reserved_at_ms: reserved_at,
expires_at_ms: expires_at,
voucher: None,
}
}
#[test]
fn test_add_and_get_reservation() {
let mgr = RelayManager::new(4);
let res = make_reservation("peer1", 1000, 5000);
mgr.add_reservation(res).expect("should succeed");
let got = mgr.get_reservation("peer1").expect("should be present");
assert_eq!(got.relay_peer_id, "peer1");
assert_eq!(got.reserved_at_ms, 1000);
assert_eq!(got.expires_at_ms, 5000);
}
#[test]
fn test_remove_reservation() {
let mgr = RelayManager::new(4);
mgr.add_reservation(make_reservation("peer1", 0, 9999))
.expect("add ok");
assert!(mgr.remove_reservation("peer1"));
assert!(!mgr.remove_reservation("peer1")); // second remove returns false
assert!(mgr.get_reservation("peer1").is_none());
}
#[test]
fn test_prune_expired() {
let mgr = RelayManager::new(10);
mgr.add_reservation(make_reservation("alive", 0, 10_000))
.expect("ok");
mgr.add_reservation(make_reservation("dead1", 0, 100))
.expect("ok");
mgr.add_reservation(make_reservation("dead2", 0, 200))
.expect("ok");
let removed = mgr.prune_expired(500);
assert_eq!(removed, 2);
assert_eq!(mgr.reservation_count(), 1);
assert!(mgr.get_reservation("alive").is_some());
}
#[test]
fn test_active_reservations_filters_expired() {
let mgr = RelayManager::new(10);
mgr.add_reservation(make_reservation("alive", 0, 10_000))
.expect("ok");
mgr.add_reservation(make_reservation("expired", 0, 50))
.expect("ok");
let active = mgr.active_reservations(100);
assert_eq!(active.len(), 1);
assert_eq!(active[0].relay_peer_id, "alive");
}
#[test]
fn test_capacity_limit() {
let mgr = RelayManager::new(2);
mgr.add_reservation(make_reservation("p1", 0, 9999))
.expect("ok");
mgr.add_reservation(make_reservation("p2", 0, 9999))
.expect("ok");
let err = mgr
.add_reservation(make_reservation("p3", 0, 9999))
.expect_err("should fail at capacity");
assert!(
matches!(err, RelayError::AtCapacity { max: 2 }),
"expected AtCapacity, got {err}"
);
}
#[test]
fn test_duplicate_reservation_error() {
let mgr = RelayManager::new(4);
mgr.add_reservation(make_reservation("peer1", 0, 9999))
.expect("first add ok");
let err = mgr
.add_reservation(make_reservation("peer1", 100, 9999))
.expect_err("duplicate should fail");
assert!(
matches!(err, RelayError::AlreadyExists { .. }),
"expected AlreadyExists, got {err}"
);
}
#[test]
fn test_reservation_ttl_remaining() {
let res = make_reservation("p", 0, 5000);
assert_eq!(res.ttl_remaining_ms(3000), 2000);
assert_eq!(res.ttl_remaining_ms(5000), 0); // exactly at expiry
assert_eq!(res.ttl_remaining_ms(6000), 0); // saturating_sub
}
#[test]
fn test_reservation_is_expired() {
let res = make_reservation("p", 0, 5000);
assert!(!res.is_expired(4999));
assert!(res.is_expired(5000));
assert!(res.is_expired(9999));
}
}