Skip to main content

urma_node/
peerbook.rs

1//! Bounded peer book: explicit routing hints, configured seeds and addresses
2//! learned from peers. Every entry is an untrusted hint, never chain evidence
3//! or a statement of record availability. Nothing here resolves names, dials
4//! or starts networking; the caller owns the directory, its lock and saves.
5
6use crate::{
7    Error,
8    peer_address::{is_circuit, parse, relay},
9    peer_gossip::{is_public, validate_gossip_address},
10    peer_protocol::GossipPeer,
11};
12use bitcoin::BlockHash;
13use libp2p::{Multiaddr, PeerId, multiaddr::Protocol};
14use rand::seq::SliceRandom;
15use serde::{Deserialize, Serialize};
16use std::{cmp::Reverse, fs, io::ErrorKind, net::IpAddr, path::Path};
17use urma_node_client::model::BookEntry as ListedEntry;
18
19pub use crate::peer_address::PeerAddress;
20pub use urma_node_client::model::BookOrigin as Origin;
21
22/// One peer (or one not yet identified seed address). Addresses are stored
23/// without a terminal `/p2p`; the identity lives in `peer_id` once learned.
24/// Circuit addresses are learned only from peers and always name the entry.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct BookEntry {
27    pub peer_id: Option<PeerId>,
28    pub addrs: Vec<Multiaddr>,
29    pub origin: Origin,
30    pub last_success: Option<u64>,
31    pub failures: u32,
32    pub banned_until: Option<u64>,
33}
34
35impl BookEntry {
36    /// Reachable only through relays: every address is a circuit.
37    pub fn circuit_only(&self) -> bool {
38        self.addrs.iter().all(is_circuit)
39    }
40}
41
42/// Counts reported in overlay status. `reached` entries succeeded and have
43/// not failed since; `reached_ever` entries succeeded at least once.
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
45pub struct BookStats {
46    pub seeds: u64,
47    pub book: u64,
48    pub reached: u64,
49    pub reached_ever: u64,
50    pub gossip: u64,
51}
52
53/// Who supplied an address: the operator, a peer's gossip, or a peer
54/// identifying itself over a connection.
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56enum Learned {
57    Operator,
58    Gossip,
59    Identify,
60}
61
62pub struct PeerBook {
63    chain_genesis: BlockHash,
64    entries: Vec<BookEntry>,
65}
66
67#[derive(Serialize, Deserialize)]
68#[serde(deny_unknown_fields)]
69struct SavedBook {
70    version: u32,
71    chain_genesis: String,
72    entries: Vec<SavedEntry>,
73}
74
75/// The persisted entry: the served `ListedEntry` shape, but strict on load.
76#[derive(Serialize, Deserialize)]
77#[serde(deny_unknown_fields)]
78struct SavedEntry {
79    peer_id: Option<String>,
80    addrs: Vec<String>,
81    origin: Origin,
82    last_success: Option<u64>,
83    failures: u32,
84    banned_until: Option<u64>,
85}
86
87impl From<SavedEntry> for ListedEntry {
88    fn from(saved: SavedEntry) -> Self {
89        let SavedEntry {
90            peer_id,
91            addrs,
92            origin,
93            last_success,
94            failures,
95            banned_until,
96        } = saved;
97        Self {
98            peer_id,
99            addrs,
100            origin,
101            last_success,
102            failures,
103            banned_until,
104        }
105    }
106}
107
108/// Where an address points: a literal IP or a name resolved at dial time.
109enum Host {
110    Ip(IpAddr),
111    Name,
112}
113
114impl PeerBook {
115    pub const CAPACITY: usize = 1024;
116    /// Entries reachable only through relays, out of `CAPACITY`.
117    pub const CIRCUIT_CAPACITY: usize = 256;
118    pub const ADDRESSES: usize = 4;
119    /// Gossip entries per IPv4 /24 or IPv6 /48.
120    pub const GROUP_LIMIT: usize = 2;
121    const FILE_LIMIT: usize = 1024 * 1024;
122    const VERSION: u32 = 2;
123    const BACKOFF_SECONDS: u64 = 60;
124    const BACKOFF_CAP: u64 = 86_400;
125    /// Seeds are operator configuration, never banned like gossip: their
126    /// backoff stops growing at ten minutes.
127    pub const SEED_BACKOFF_CAP: u64 = 600;
128
129    pub fn empty(chain_genesis: BlockHash) -> Self {
130        Self {
131            chain_genesis,
132            entries: Vec::new(),
133        }
134    }
135
136    /// Load a bounded regular JSON file for the caller's explicit network.
137    /// Only `NotFound` yields an empty book; any invalid file, including an
138    /// older format version, is refused whole and preserved untouched.
139    pub fn load(path: &Path, chain_genesis: BlockHash) -> Result<Self, Error> {
140        let mut book = Self::empty(chain_genesis);
141        let metadata = match fs::symlink_metadata(path) {
142            Ok(metadata) => metadata,
143            Err(error) if error.kind() == ErrorKind::NotFound => return Ok(book),
144            Err(error) => return Err(error.into()),
145        };
146        let limit = u64::try_from(Self::FILE_LIMIT).map_err(|e| Error::Invalid(e.to_string()))?;
147        if !metadata.is_file() || metadata.len() > limit {
148            return Err(Error::Invalid(
149                "peer book must be a regular file of at most 1 MiB".into(),
150            ));
151        }
152        let bytes = urma_io::read_regular(path, Self::FILE_LIMIT).map_err(io_source)?;
153        let saved: SavedBook = serde_json::from_slice(&bytes)?;
154        if saved.version != Self::VERSION {
155            return Err(Error::Invalid("unsupported peer book version".into()));
156        }
157        let saved_genesis: BlockHash = saved
158            .chain_genesis
159            .parse()
160            .map_err(|_| Error::Invalid("invalid peer book chain genesis".into()))?;
161        if saved_genesis != chain_genesis {
162            return Err(Error::Invalid("peer book belongs to another chain".into()));
163        }
164        if saved.entries.len() > Self::CAPACITY {
165            return Err(Error::Busy("peer book has too many entries".into()));
166        }
167        for saved in saved.entries {
168            let entry = restore(saved)?;
169            if book.overlaps(&entry) {
170                return Err(Error::Invalid("duplicate saved peer or address".into()));
171            }
172            book.entries.push(entry);
173        }
174        book.trim_circuits();
175        Ok(book)
176    }
177
178    /// Drop the worst circuit-only entries (most failures, then oldest
179    /// success) beyond the circuit share.
180    fn trim_circuits(&mut self) {
181        while self.circuit_entries() > Self::CIRCUIT_CAPACITY {
182            let Some(worst) = self
183                .entries
184                .iter()
185                .enumerate()
186                .filter(|(_, entry)| entry.circuit_only())
187                .max_by_key(|(_, entry)| (entry.failures, Reverse(entry.last_success)))
188                .map(|(index, _)| index)
189            else {
190                return;
191            };
192            self.entries.remove(worst);
193        }
194    }
195
196    /// Atomically replace a bounded snapshot sorted by identity then address.
197    pub fn save(&self, path: &Path) -> Result<(), Error> {
198        let mut entries: Vec<SavedEntry> = self.entries.iter().map(persist).collect();
199        entries.sort_by(|a, b| (&a.peer_id, &a.addrs).cmp(&(&b.peer_id, &b.addrs)));
200        let saved = SavedBook {
201            version: Self::VERSION,
202            chain_genesis: self.chain_genesis.to_string(),
203            entries,
204        };
205        let bytes = serde_json::to_vec(&saved)?;
206        if bytes.len() > Self::FILE_LIMIT {
207            return Err(Error::Invalid("peer book exceeds 1 MiB".into()));
208        }
209        urma_io::write_replace(path, &bytes).map_err(io_source)
210    }
211
212    pub fn entries(&self) -> &[BookEntry] {
213        &self.entries
214    }
215
216    /// Add one address. A terminal `/p2p` names the peer; without it the
217    /// entry stays unidentified until a dial succeeds. Gossip only accepts
218    /// public literal IPs under the group cap, and is the only origin of
219    /// circuit addresses; manual and seed accept any usable direct address
220    /// including loopback and names. Returns whether memory changed; capacity
221    /// is made by evicting the worst gossip entry only.
222    pub fn add(&mut self, address: Multiaddr, origin: Origin) -> Result<bool, Error> {
223        let learned = match origin {
224            Origin::Gossip => Learned::Gossip,
225            Origin::Seed | Origin::Manual => Learned::Operator,
226        };
227        self.insert(address, origin, learned)
228    }
229
230    /// Learned addresses never relabel an existing entry: an address already
231    /// in the book is left alone, and only identify (not gossip) may add an
232    /// address to a known peer. Identities are learned only by `succeeded`.
233    fn insert(
234        &mut self,
235        address: Multiaddr,
236        origin: Origin,
237        learned: Learned,
238    ) -> Result<bool, Error> {
239        let (address, peer) = split(address)?;
240        let circuit = is_circuit(&address);
241        if circuit && (origin != Origin::Gossip || peer.is_none()) {
242            return Err(Error::Invalid(
243                "circuit addresses come only from gossip and name their peer".into(),
244            ));
245        }
246        if origin == Origin::Gossip {
247            self.admit_gossip(&address)?;
248        }
249        if let Some(index) = self.position_of_address(&address, peer) {
250            if learned != Learned::Operator {
251                return Ok(false);
252            }
253            let entry = &mut self.entries[index];
254            if peer.is_some() && entry.peer_id.is_some() && entry.peer_id != peer {
255                return Err(Error::Invalid("address already names another peer".into()));
256            }
257            let upgraded = origin > entry.origin || (entry.peer_id.is_none() && peer.is_some());
258            entry.origin = entry.origin.max(origin);
259            entry.peer_id = entry.peer_id.or(peer);
260            return Ok(upgraded);
261        }
262        if let Some(index) = peer.and_then(|peer| self.position_of_peer(peer)) {
263            if learned == Learned::Gossip {
264                return Ok(false);
265            }
266            let entry = &mut self.entries[index];
267            if entry.addrs.len() == Self::ADDRESSES {
268                return Err(Error::Busy("peer already has four addresses".into()));
269            }
270            entry.addrs.push(address);
271            entry.origin = entry.origin.max(origin);
272            return Ok(true);
273        }
274        self.make_room(origin, circuit)?;
275        self.entries.push(BookEntry {
276            peer_id: peer,
277            addrs: vec![address],
278            origin,
279            last_success: None,
280            failures: 0,
281            banned_until: None,
282        });
283        Ok(true)
284    }
285
286    /// Merge one peer's gossip under the caps. The local identity and peers
287    /// this archive found omitting records are never stored from gossip.
288    /// Returns how many addresses changed memory; refusals are skipped.
289    pub fn merge(&mut self, peers: &[GossipPeer], local: PeerId, omitting: &[String]) -> usize {
290        self.learn(peers, local, omitting, Learned::Gossip)
291    }
292
293    /// Peers that identified themselves over a connection: like gossip, but a
294    /// known peer may gain the addresses it reported for itself.
295    pub fn merge_identified(
296        &mut self,
297        peers: &[GossipPeer],
298        local: PeerId,
299        omitting: &[String],
300    ) -> usize {
301        self.learn(peers, local, omitting, Learned::Identify)
302    }
303
304    fn learn(
305        &mut self,
306        peers: &[GossipPeer],
307        local: PeerId,
308        omitting: &[String],
309        learned: Learned,
310    ) -> usize {
311        let mut changed = 0;
312        for peer in peers {
313            let id = peer.peer_id;
314            if id == local || omitting.contains(&id.to_string()) {
315                continue;
316            }
317            let addresses = own_addresses(peer, local);
318            if self.position_of_peer(id).is_none() {
319                changed += self.insert_learned_peer(id, addresses);
320                continue;
321            }
322            if learned != Learned::Identify {
323                continue;
324            }
325            for address in addresses {
326                let address = address.with(Protocol::P2p(id));
327                if let Ok(true) = self.insert(address, Origin::Gossip, learned) {
328                    changed += 1;
329                }
330            }
331        }
332        changed
333    }
334
335    /// A peer new to the book enters once with every learned address that
336    /// passes the gossip filter and group cap and is not already in the book.
337    fn insert_learned_peer(&mut self, id: PeerId, addresses: Vec<Multiaddr>) -> usize {
338        let mut admitted = Vec::new();
339        for address in addresses {
340            let known = self.position_of_address(&address, Some(id)).is_some();
341            if !known && self.admit_gossip(&address).is_ok() {
342                admitted.push(address);
343            }
344        }
345        let circuit = admitted.iter().all(is_circuit);
346        if admitted.is_empty() || self.make_room(Origin::Gossip, circuit).is_err() {
347            return 0;
348        }
349        let count = admitted.len();
350        self.entries.push(BookEntry {
351            peer_id: Some(id),
352            addrs: admitted,
353            origin: Origin::Gossip,
354            last_success: None,
355            failures: 0,
356            banned_until: None,
357        });
358        count
359    }
360
361    /// A dial through `address` reached `peer`. Resets failures and records
362    /// the success; an unidentified literal-IP entry learns the identity. An
363    /// entry that turns out to be this node is dropped: the book never holds
364    /// ourselves.
365    pub fn succeeded(&mut self, address: &Multiaddr, peer: PeerId, local: PeerId, now: u64) {
366        let Ok((address, named)) = split(address.clone()) else {
367            return;
368        };
369        let Some(index) = self.position_of_address(&address, named) else {
370            return;
371        };
372        if peer == local {
373            self.entries.remove(index);
374            return;
375        }
376        let learnable = matches!(host(&address), Some(Host::Ip(_)));
377        let known_elsewhere = self
378            .position_of_peer(peer)
379            .is_some_and(|other| other != index);
380        let entry = &mut self.entries[index];
381        entry.last_success = Some(now);
382        entry.failures = 0;
383        entry.banned_until = None;
384        if entry.peer_id.is_none() && learnable && !known_elsewhere {
385            entry.peer_id = Some(peer);
386        }
387    }
388
389    /// A dial through `address` failed: back off 60 s × 2^(failures−1), so the
390    /// first failure waits 60 s, capped at one day (ten minutes for seeds).
391    /// The address moves to the back, so the next dial tries another one.
392    pub fn failed(&mut self, address: &Multiaddr, now: u64) {
393        let Ok((address, named)) = split(address.clone()) else {
394            return;
395        };
396        let Some(index) = self.position_of_address(&address, named) else {
397            return;
398        };
399        let addrs = &mut self.entries[index].addrs;
400        addrs.retain(|known| *known != address);
401        addrs.push(address);
402        self.fail_entry(index, now);
403    }
404
405    /// A peer misbehaved during sync: back its entry off like a failed dial.
406    /// Returns whether the book holds that peer.
407    pub fn failed_peer(&mut self, peer: PeerId, now: u64) -> bool {
408        let Some(index) = self.position_of_peer(peer) else {
409            return false;
410        };
411        self.fail_entry(index, now);
412        true
413    }
414
415    fn fail_entry(&mut self, index: usize, now: u64) {
416        let entry = &mut self.entries[index];
417        entry.failures = entry.failures.checked_add(1).unwrap_or(u32::MAX);
418        let cap = match entry.origin {
419            Origin::Seed => Self::SEED_BACKOFF_CAP,
420            Origin::Gossip | Origin::Manual => Self::BACKOFF_CAP,
421        };
422        let delay = 2u64
423            .checked_pow(entry.failures - 1)
424            .and_then(|factor| factor.checked_mul(Self::BACKOFF_SECONDS))
425            .map_or(cap, |delay| delay.min(cap));
426        entry.banned_until = Some(now.checked_add(delay).unwrap_or(u64::MAX));
427    }
428
429    /// Up to `count` dial addresses for non-seed entries that are not banned,
430    /// connected or omitting. Entries with a direct address come before
431    /// circuit-only ones. Within each, never-tried entries (picked at random)
432    /// alternate with tried ones (oldest backoff expiry first), so fresh
433    /// gossip cannot starve peers that were reached before.
434    pub fn candidates(
435        &self,
436        now: u64,
437        connected: &[PeerId],
438        omitting: &[String],
439        count: usize,
440    ) -> Vec<Multiaddr> {
441        let eligible = |entry: &&BookEntry| {
442            entry.origin != Origin::Seed
443                && entry.banned_until.is_none_or(|until| until <= now)
444                && entry.peer_id.is_none_or(|peer| {
445                    !connected.contains(&peer) && !omitting.contains(&peer.to_string())
446                })
447        };
448        let (circuit, direct): (Vec<&BookEntry>, Vec<&BookEntry>) = self
449            .entries
450            .iter()
451            .filter(eligible)
452            .partition(|entry| entry.circuit_only());
453        let mut picked = alternate(direct);
454        picked.extend(alternate(circuit));
455        picked.truncate(count);
456        picked.into_iter().filter_map(dial_address).collect()
457    }
458
459    /// Seed dial addresses in random order, skipping seeds known to be this
460    /// node's omitters and, given `now`, seeds still backing off. A ban beyond
461    /// the seed cap (persisted under an older policy) counts as expired.
462    pub fn seeds(&self, omitting: &[String], now: Option<u64>) -> Vec<Multiaddr> {
463        let ready = |until: u64, now: u64| until <= now || until - now > Self::SEED_BACKOFF_CAP;
464        let mut seeds: Vec<Multiaddr> = self
465            .entries
466            .iter()
467            .filter(|entry| entry.origin == Origin::Seed)
468            .filter(|entry| {
469                now.is_none_or(|now| entry.banned_until.is_none_or(|until| ready(until, now)))
470            })
471            .filter(|entry| {
472                entry
473                    .peer_id
474                    .is_none_or(|peer| !omitting.contains(&peer.to_string()))
475            })
476            .filter_map(dial_address)
477            .collect();
478        seeds.shuffle(&mut rand::thread_rng());
479        seeds
480    }
481
482    /// Identified direct IP addresses for record, commit and inventory
483    /// sources, one per peer, excluding peers this archive found omitting
484    /// records. Circuit addresses never become sync sources.
485    pub fn sync_addresses(&self, omitting: &[String]) -> Vec<PeerAddress> {
486        self.entries
487            .iter()
488            .filter_map(|entry| {
489                let peer = entry.peer_id?;
490                if omitting.contains(&peer.to_string()) {
491                    return None;
492                }
493                entry.addrs.iter().find_map(|address| {
494                    PeerAddress::new(peer, address.clone().with(Protocol::P2p(peer))).ok()
495                })
496            })
497            .collect()
498    }
499
500    /// Verified, identified peers with public literal-IP addresses (circuits
501    /// through public relays included), most recently successful first, for
502    /// answering a Peers request.
503    pub fn gossip(&self, limit: usize, omitting: &[String]) -> Vec<GossipPeer> {
504        let mut verified: Vec<(&BookEntry, PeerId, u64)> = self
505            .entries
506            .iter()
507            .filter_map(|entry| Some((entry, entry.peer_id?, entry.last_success?)))
508            .filter(|(_, peer, _)| !omitting.contains(&peer.to_string()))
509            .collect();
510        verified.sort_by_key(|(_, _, seen)| std::cmp::Reverse(*seen));
511        verified
512            .into_iter()
513            .filter_map(|(entry, peer_id, last_seen)| {
514                let addrs: Vec<Multiaddr> =
515                    entry.addrs.iter().filter(|a| public(a)).cloned().collect();
516                (!addrs.is_empty()).then_some(GossipPeer {
517                    peer_id,
518                    addrs,
519                    last_seen,
520                })
521            })
522            .take(limit)
523            .collect()
524    }
525
526    pub fn stats(&self) -> BookStats {
527        let count = |keep: &dyn Fn(&BookEntry) -> bool| {
528            u64::try_from(self.entries.iter().filter(|entry| keep(entry)).count())
529                .unwrap_or(u64::MAX)
530        };
531        BookStats {
532            seeds: count(&|entry| entry.origin == Origin::Seed),
533            book: count(&|_| true),
534            reached: count(&|entry| entry.failures == 0 && entry.last_success.is_some()),
535            reached_ever: count(&|entry| entry.last_success.is_some()),
536            gossip: count(&|entry| entry.origin == Origin::Gossip),
537        }
538    }
539
540    /// Bounded JSON snapshot for the local control API.
541    pub fn listing(&self) -> Vec<ListedEntry> {
542        let mut entries: Vec<ListedEntry> = self
543            .entries
544            .iter()
545            .map(|entry| persist(entry).into())
546            .collect();
547        entries.sort_by(|a, b| (&a.peer_id, &a.addrs).cmp(&(&b.peer_id, &b.addrs)));
548        entries.truncate(Self::CAPACITY);
549        entries
550    }
551
552    /// Remove a peer's entry from memory without persisting.
553    pub fn remove(&mut self, peer: PeerId) -> bool {
554        let before = self.entries.len();
555        self.entries.retain(|entry| entry.peer_id != Some(peer));
556        self.entries.len() != before
557    }
558
559    /// A circuit is stored without its target, so every peer behind one relay
560    /// shares the bare address: it matches only the entry of `peer`.
561    fn position_of_address(&self, address: &Multiaddr, peer: Option<PeerId>) -> Option<usize> {
562        let circuit = is_circuit(address);
563        self.entries
564            .iter()
565            .position(|entry| entry.addrs.contains(address) && (!circuit || entry.peer_id == peer))
566    }
567
568    fn position_of_peer(&self, peer: PeerId) -> Option<usize> {
569        self.entries
570            .iter()
571            .position(|entry| entry.peer_id == Some(peer))
572    }
573
574    fn circuit_entries(&self) -> usize {
575        self.entries
576            .iter()
577            .filter(|entry| entry.circuit_only())
578            .count()
579    }
580
581    fn overlaps(&self, candidate: &BookEntry) -> bool {
582        candidate
583            .peer_id
584            .is_some_and(|peer| self.position_of_peer(peer).is_some())
585            || candidate.addrs.iter().any(|address| {
586                self.position_of_address(address, candidate.peer_id)
587                    .is_some()
588            })
589    }
590
591    /// Gossip may only name public literal IPs, two direct entries per
592    /// network group. Circuits share their relay's group, so they are bounded
593    /// by the circuit-only capacity instead.
594    fn admit_gossip(&self, address: &Multiaddr) -> Result<(), Error> {
595        let Some(Host::Ip(ip)) = host(address) else {
596            return Err(Error::Invalid(
597                "gossip addresses must be literal IPs".into(),
598            ));
599        };
600        validate_gossip_address(address)?;
601        if !is_public(address) {
602            return Err(Error::Invalid("gossip named a non-public address".into()));
603        }
604        if is_circuit(address) {
605            return Ok(());
606        }
607        let neighbours = self
608            .entries
609            .iter()
610            .filter(|entry| entry.origin == Origin::Gossip)
611            .filter(|entry| entry.addrs.iter().any(|known| same_group(known, ip)))
612            .count();
613        if neighbours >= Self::GROUP_LIMIT && self.position_of_address(address, None).is_none() {
614            return Err(Error::Busy("gossip network group is full".into()));
615        }
616        Ok(())
617    }
618
619    /// Make room for a new entry (`circuit`: reachable only through relays).
620    /// When the circuit-only share is full, evict among circuit-only gossip;
621    /// otherwise at capacity among all gossip: most failures first, then the
622    /// oldest success (never-successful first). Manual and seed are never
623    /// evicted, and a new gossip entry never evicts a healthy one.
624    fn make_room(&mut self, origin: Origin, circuit: bool) -> Result<(), Error> {
625        let crowded = circuit && self.circuit_entries() >= Self::CIRCUIT_CAPACITY;
626        if !crowded && self.entries.len() < Self::CAPACITY {
627            return Ok(());
628        }
629        let worst = self
630            .entries
631            .iter()
632            .enumerate()
633            .filter(|(_, entry)| entry.origin == Origin::Gossip)
634            .filter(|(_, entry)| !crowded || entry.circuit_only())
635            .max_by_key(|(_, entry)| {
636                (
637                    entry.failures,
638                    std::cmp::Reverse(entry.last_success.unwrap_or(0)),
639                )
640            })
641            .map(|(index, _)| index);
642        match worst {
643            Some(index)
644                if origin != Origin::Gossip
645                    || self.entries[index].failures > 0
646                    || self.entries[index].last_success.is_none() =>
647            {
648                self.entries.remove(index);
649                Ok(())
650            }
651            _ => Err(Error::Busy("peer book is full".into())),
652        }
653    }
654}
655
656/// A gossiped peer's distinct addresses naming it (or nothing), skipping
657/// circuits through itself or through this node.
658fn own_addresses(peer: &GossipPeer, local: PeerId) -> Vec<Multiaddr> {
659    let id = peer.peer_id;
660    let mut addresses = Vec::new();
661    for address in peer.addrs.iter().take(PeerBook::ADDRESSES) {
662        if relay(address).is_some_and(|relay| relay == id || relay == local) {
663            continue;
664        }
665        let Ok((address, named)) = split(address.clone()) else {
666            continue;
667        };
668        if named.is_none_or(|named| named == id) && !addresses.contains(&address) {
669            addresses.push(address);
670        }
671    }
672    addresses
673}
674
675/// Never-tried entries in random order alternating with tried ones, oldest
676/// backoff expiry first.
677fn alternate(entries: Vec<&BookEntry>) -> Vec<&BookEntry> {
678    let (mut fresh, mut tried): (Vec<&BookEntry>, Vec<&BookEntry>) = entries
679        .into_iter()
680        .partition(|entry| entry.last_success.is_none() && entry.failures == 0);
681    fresh.shuffle(&mut rand::thread_rng());
682    tried.sort_by_key(|entry| (entry.banned_until, entry.failures));
683    let mut fresh = fresh.into_iter();
684    let mut tried = tried.into_iter();
685    let mut picked = Vec::new();
686    loop {
687        let (next_fresh, next_tried) = (fresh.next(), tried.next());
688        if next_fresh.is_none() && next_tried.is_none() {
689            return picked;
690        }
691        picked.extend(next_fresh.into_iter().chain(next_tried));
692    }
693}
694
695/// Validate a dialable address and split off a terminal `/p2p`.
696fn split(address: Multiaddr) -> Result<(Multiaddr, Option<PeerId>), Error> {
697    let shape = parse(&address)?;
698    Ok((shape.bare, shape.peer))
699}
700
701fn host(address: &Multiaddr) -> Option<Host> {
702    match address.iter().next()? {
703        Protocol::Ip4(ip) => Some(Host::Ip(IpAddr::V4(ip))),
704        Protocol::Ip6(ip) => Some(Host::Ip(IpAddr::V6(ip))),
705        Protocol::Dns(_) | Protocol::Dns4(_) | Protocol::Dns6(_) | Protocol::Dnsaddr(_) => {
706            Some(Host::Name)
707        }
708        _ => None,
709    }
710}
711
712/// A globally routable literal IP: the only address gossip may carry. Names
713/// and non-public ranges come only from an operator (manual or seed).
714fn public(address: &Multiaddr) -> bool {
715    matches!(host(address), Some(Host::Ip(_))) && is_public(address)
716}
717
718/// IPv4 /24 or IPv6 /48 equality.
719fn same_group(address: &Multiaddr, ip: IpAddr) -> bool {
720    match (host(address), ip) {
721        (Some(Host::Ip(IpAddr::V4(known))), IpAddr::V4(ip)) => {
722            known.octets()[..3] == ip.octets()[..3]
723        }
724        (Some(Host::Ip(IpAddr::V6(known))), IpAddr::V6(ip)) => {
725            known.segments()[..3] == ip.segments()[..3]
726        }
727        _ => false,
728    }
729}
730
731/// The first direct address, else the first circuit, with the identity.
732fn dial_address(entry: &BookEntry) -> Option<Multiaddr> {
733    let address = entry
734        .addrs
735        .iter()
736        .find(|address| !is_circuit(address))
737        .or(entry.addrs.first())?
738        .clone();
739    Some(match entry.peer_id {
740        Some(peer) => address.with(Protocol::P2p(peer)),
741        None => address,
742    })
743}
744
745fn persist(entry: &BookEntry) -> SavedEntry {
746    SavedEntry {
747        peer_id: entry.peer_id.map(|peer| peer.to_string()),
748        addrs: entry.addrs.iter().map(Multiaddr::to_string).collect(),
749        origin: entry.origin,
750        last_success: entry.last_success,
751        failures: entry.failures,
752        banned_until: entry.banned_until,
753    }
754}
755
756fn restore(saved: SavedEntry) -> Result<BookEntry, Error> {
757    let peer_id = saved
758        .peer_id
759        .map(|peer| peer.parse::<PeerId>())
760        .transpose()
761        .map_err(|_| Error::Invalid("invalid saved peer ID".into()))?;
762    if saved.addrs.is_empty() || saved.addrs.len() > PeerBook::ADDRESSES {
763        return Err(Error::Invalid(
764            "saved peer needs one to four addresses".into(),
765        ));
766    }
767    let mut addrs = Vec::new();
768    for address in saved.addrs {
769        let parsed: Multiaddr = address
770            .parse()
771            .map_err(|_| Error::Invalid("invalid saved peer address".into()))?;
772        let (bare, named) = split(parsed)?;
773        if named.is_some() || addrs.contains(&bare) {
774            return Err(Error::Invalid("saved peer address is not canonical".into()));
775        }
776        if saved.origin == Origin::Gossip && !public(&bare) {
777            return Err(Error::Invalid("saved gossip address is not public".into()));
778        }
779        if is_circuit(&bare) && (saved.origin != Origin::Gossip || peer_id.is_none()) {
780            return Err(Error::Invalid(
781                "saved circuit address needs a gossip entry with its peer".into(),
782            ));
783        }
784        addrs.push(bare);
785    }
786    Ok(BookEntry {
787        peer_id,
788        addrs,
789        origin: saved.origin,
790        last_success: saved.last_success,
791        failures: saved.failures,
792        banned_until: saved.banned_until,
793    })
794}
795
796fn io_source(error: urma_io::Error) -> Error {
797    Error::Source(Box::new(error))
798}