1use std::{
4 collections::HashMap,
5 sync::{Arc, Mutex},
6 time::{Duration, Instant},
7};
8
9use crate::{
10 peer::ConnectionInfo,
11 protocol::external::types::Version,
12 zakura::{ZakuraConnId, ZakuraPeerId},
13 PeerSocketAddr,
14};
15
16#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct ConnectedPeer {
19 pub addr: PeerSocketAddr,
21 pub user_agent: Arc<str>,
23 pub version: Version,
25 pub is_inbound: bool,
27 pub rtt: Option<Duration>,
29 pub ping_sent_at: Option<Instant>,
31}
32
33impl ConnectedPeer {
34 pub(crate) fn from_connection_info(connection_info: &ConnectionInfo) -> Option<Self> {
36 Some(Self {
37 addr: connection_info.connected_addr.diagnostic_remote_addr()?,
38 user_agent: sanitize_subversion(&connection_info.remote.user_agent).into(),
39 version: connection_info.remote.version,
40 is_inbound: connection_info.connected_addr.is_inbound(),
41 rtt: None,
42 ping_sent_at: None,
43 })
44 }
45}
46
47#[derive(Clone, Debug, Default)]
49pub(crate) struct PeerRegistry {
50 inner: Arc<Mutex<RegistryState>>,
51}
52
53#[derive(Debug, Default)]
54struct RegistryState {
55 next_legacy_generation: u64,
56 active_connections: HashMap<ConnectionKey, ConnectedPeer>,
57 native_conn_ids: HashMap<ZakuraPeerId, ZakuraConnId>,
58 retained_native_metadata: HashMap<ZakuraPeerId, ConnectedPeer>,
59}
60
61#[derive(Clone, Debug, Eq, Hash, PartialEq)]
62enum ConnectionKey {
63 Legacy(u64),
64 Native(ZakuraPeerId, ZakuraConnId),
65}
66
67impl PeerRegistry {
68 pub(crate) fn connected_peers(&self) -> Vec<ConnectedPeer> {
70 let mut connected_peers: Vec<_> = self
71 .inner
72 .lock()
73 .expect("peer registry mutex is never poisoned")
74 .active_connections
75 .values()
76 .cloned()
77 .collect();
78 connected_peers.sort_by(|left, right| {
79 left.addr
80 .cmp(&right.addr)
81 .then(left.is_inbound.cmp(&right.is_inbound))
82 .then(left.version.0.cmp(&right.version.0))
83 .then(left.user_agent.cmp(&right.user_agent))
84 });
85 connected_peers
86 }
87
88 pub(crate) fn register_legacy(
90 &self,
91 connected_peer: ConnectedPeer,
92 ) -> (PeerRegistryGuard, PeerRegistryUpdater) {
93 let mut state = self
94 .inner
95 .lock()
96 .expect("peer registry mutex is never poisoned");
97 let generation = state.next_legacy_generation;
98 state.next_legacy_generation = state
99 .next_legacy_generation
100 .checked_add(1)
101 .expect("legacy connection generation cannot overflow in one process");
102 let key = ConnectionKey::Legacy(generation);
103 state.active_connections.insert(key.clone(), connected_peer);
104 drop(state);
105
106 (
107 PeerRegistryGuard {
108 registry: self.clone(),
109 key: key.clone(),
110 },
111 PeerRegistryUpdater {
112 registry: self.clone(),
113 key,
114 },
115 )
116 }
117
118 #[must_use]
123 pub(crate) fn attach_native_metadata(
124 &self,
125 peer_id: ZakuraPeerId,
126 conn_id: ZakuraConnId,
127 connection_metadata: ConnectedPeer,
128 retain_for_redial: bool,
129 ) -> bool {
130 let mut state = self
131 .inner
132 .lock()
133 .expect("peer registry mutex is never poisoned");
134 if state.native_conn_ids.get(&peer_id) != Some(&conn_id) {
135 return false;
136 }
137 if retain_for_redial {
138 state
139 .retained_native_metadata
140 .insert(peer_id.clone(), connection_metadata.clone());
141 }
142 state
143 .active_connections
144 .insert(ConnectionKey::Native(peer_id, conn_id), connection_metadata);
145 true
146 }
147
148 pub(crate) fn register_native_connection(&self, peer_id: ZakuraPeerId, conn_id: ZakuraConnId) {
152 let mut state = self
153 .inner
154 .lock()
155 .expect("peer registry mutex is never poisoned");
156 if let Some(previous) = state.native_conn_ids.insert(peer_id.clone(), conn_id) {
157 state
158 .active_connections
159 .remove(&ConnectionKey::Native(peer_id.clone(), previous));
160 }
161 if let Some(connection_metadata) = state.retained_native_metadata.get(&peer_id).cloned() {
162 state
163 .active_connections
164 .insert(ConnectionKey::Native(peer_id, conn_id), connection_metadata);
165 }
166 }
167
168 pub(crate) fn deregister_native_connection(
170 &self,
171 peer_id: &ZakuraPeerId,
172 conn_id: ZakuraConnId,
173 ) {
174 let mut state = self
175 .inner
176 .lock()
177 .expect("peer registry mutex is never poisoned");
178 if state.native_conn_ids.get(peer_id) == Some(&conn_id) {
179 state.native_conn_ids.remove(peer_id);
180 state
181 .active_connections
182 .remove(&ConnectionKey::Native(peer_id.clone(), conn_id));
183 }
184 }
185
186 pub(crate) fn forget_retained_native_metadata(&self, peer_id: &ZakuraPeerId) {
188 self.inner
189 .lock()
190 .expect("peer registry mutex is never poisoned")
191 .retained_native_metadata
192 .remove(peer_id);
193 }
194
195 fn update_connection(&self, key: &ConnectionKey, update: impl FnOnce(&mut ConnectedPeer)) {
196 if let Some(connected_peer) = self
197 .inner
198 .lock()
199 .expect("peer registry mutex is never poisoned")
200 .active_connections
201 .get_mut(key)
202 {
203 update(connected_peer);
204 }
205 }
206
207 fn remove_connection(&self, key: &ConnectionKey) {
208 self.inner
209 .lock()
210 .expect("peer registry mutex is never poisoned")
211 .active_connections
212 .remove(key);
213 }
214}
215
216#[derive(Clone, Debug)]
218pub(crate) struct PeerRegistryUpdater {
219 registry: PeerRegistry,
220 key: ConnectionKey,
221}
222
223impl PeerRegistryUpdater {
224 pub(crate) fn record_ping_sent(&self, now: Instant) {
225 self.registry
226 .update_connection(&self.key, |connected_peer| {
227 connected_peer.ping_sent_at = Some(now);
228 });
229 }
230
231 pub(crate) fn record_response(&self, rtt: Duration) {
232 self.registry
233 .update_connection(&self.key, |connected_peer| {
234 connected_peer.rtt = Some(rtt);
235 connected_peer.ping_sent_at = None;
236 });
237 }
238}
239
240#[derive(Debug)]
242pub(crate) struct PeerRegistryGuard {
243 registry: PeerRegistry,
244 key: ConnectionKey,
245}
246
247impl Drop for PeerRegistryGuard {
248 fn drop(&mut self) {
249 self.registry.remove_connection(&self.key);
250 }
251}
252
253fn sanitize_subversion(user_agent: &str) -> String {
254 user_agent
255 .chars()
256 .filter(|character| {
257 character.is_ascii_alphanumeric() || matches!(character, ' ' | '.' | '-' | '/' | ':')
258 })
259 .collect()
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 fn peer(port: u16) -> ConnectedPeer {
267 ConnectedPeer {
268 addr: format!("127.0.0.1:{port}")
269 .parse()
270 .expect("test address parses"),
271 user_agent: Arc::from("/Zakura:1.0.0/"),
272 version: Version(1),
273 is_inbound: false,
274 rtt: None,
275 ping_sent_at: None,
276 }
277 }
278
279 fn peer_id(byte: u8) -> ZakuraPeerId {
280 ZakuraPeerId::new(vec![byte; 32]).expect("test peer id is valid")
281 }
282
283 #[test]
284 fn legacy_guard_removes_only_its_generation() {
285 let registry = PeerRegistry::default();
286 let (first, _) = registry.register_legacy(peer(1));
287 let (_second, _) = registry.register_legacy(peer(2));
288
289 drop(first);
290
291 assert_eq!(registry.connected_peers(), vec![peer(2)]);
292 }
293
294 #[test]
295 fn native_replacement_ignores_stale_disconnect() {
296 let registry = PeerRegistry::default();
297 let peer_id = peer_id(1);
298 registry.register_native_connection(peer_id.clone(), 1);
299 assert!(registry.attach_native_metadata(peer_id.clone(), 1, peer(1), true));
300 registry.register_native_connection(peer_id.clone(), 2);
301
302 registry.deregister_native_connection(&peer_id, 1);
303 assert_eq!(registry.connected_peers(), vec![peer(1)]);
304
305 registry.deregister_native_connection(&peer_id, 2);
306 assert!(registry.connected_peers().is_empty());
307 }
308
309 #[test]
310 fn native_metadata_can_arrive_after_registration() {
311 let registry = PeerRegistry::default();
312 let peer_id = peer_id(2);
313 registry.register_native_connection(peer_id.clone(), 1);
314 assert!(registry.connected_peers().is_empty());
315
316 assert!(registry.attach_native_metadata(peer_id, 1, peer(1), true));
317 assert_eq!(registry.connected_peers(), vec![peer(1)]);
318 }
319
320 #[test]
321 fn stale_native_metadata_cannot_replace_active_or_retained_metadata() {
322 let registry = PeerRegistry::default();
323 let peer_id = peer_id(3);
324 registry.register_native_connection(peer_id.clone(), 1);
325 assert!(registry.attach_native_metadata(peer_id.clone(), 1, peer(1), true));
326 registry.register_native_connection(peer_id.clone(), 2);
327
328 assert!(!registry.attach_native_metadata(peer_id.clone(), 1, peer(2), true));
329 assert_eq!(registry.connected_peers(), vec![peer(1)]);
330
331 registry.deregister_native_connection(&peer_id, 2);
332 registry.register_native_connection(peer_id, 3);
333 assert_eq!(registry.connected_peers(), vec![peer(1)]);
334 }
335
336 #[test]
337 fn transient_native_metadata_does_not_survive_reconnect() {
338 let registry = PeerRegistry::default();
339 let peer_id = peer_id(4);
340 registry.register_native_connection(peer_id.clone(), 1);
341 assert!(registry.attach_native_metadata(peer_id.clone(), 1, peer(1), false));
342 assert_eq!(registry.connected_peers(), vec![peer(1)]);
343
344 registry.deregister_native_connection(&peer_id, 1);
345 registry.register_native_connection(peer_id, 2);
346 assert!(registry.connected_peers().is_empty());
347 }
348
349 #[test]
350 fn heartbeat_updates_do_not_revive_dropped_generation() {
351 let registry = PeerRegistry::default();
352 let (guard, updater) = registry.register_legacy(peer(1));
353 updater.record_ping_sent(Instant::now());
354 updater.record_response(Duration::from_millis(5));
355 assert_eq!(
356 registry.connected_peers()[0].rtt,
357 Some(Duration::from_millis(5)),
358 );
359
360 drop(guard);
361 updater.record_response(Duration::from_millis(10));
362 assert!(registry.connected_peers().is_empty());
363 }
364
365 #[test]
366 fn subversion_sanitizer_matches_zcashd_allowed_characters() {
367 assert_eq!(
368 sanitize_subversion("/Magic Bean:2.1.1-1/\u{1b}[31m\n"),
369 "/Magic Bean:2.1.1-1/31m"
370 );
371 }
372}