Skip to main content

bb_runtime/framework/
address_book.rs

1//! `AddressBook` - global `PeerId → (Vec<Address>, ref_count)`
2//! registry per `ENGINE.md` §10.5 + `docs/ADDRESSING.md`.
3//!
4//! Real-world peers expose multiple reachable endpoints
5//! (`/ip4/.../tcp/...`, `/ip6/.../quic/...`, `/relay/...`); the
6//! AddressBook holds the ordered list per peer (insertion order =
7//! peer-stated preference) and the host transport adapter picks
8//! one based on its networking capabilities.
9//!
10//! Entries are **reference-counted**: multiple overlay protocols
11//! can independently "own" the same peer without duplicating
12//! address records. `add_peer` increments the count;
13//! `drop_peer` decrements; the entry is removed when the count
14//! hits zero. Components that need peer metadata (gossip
15//! overlays, peer-sampling views) store their own metadata in
16//! their own state and call `lookup` here for addresses -
17//! addresses live in exactly one place.
18//!
19//! The wire syscall (`src/syscall/wire.rs`) consults this on
20//! every `wire::Send` to populate
21//! `WireEnvelope.dest_peer_addresses` with the resolved list;
22//! a lookup miss surfaces as `EngineStep::PeerResolveFailed`.
23//!
24//! Addresses in bytes-and-brains are **multiaddrs**. The framework
25//! only carries the segments it routes on internally - transport
26//! segments (Ip4 / Tcp / etc.) and any other host-managed routing
27//! data live in the host adapter. The framework's `Protocol` enum
28//! is the four BB-internal variants only:
29//!
30//! - `P2p(PeerId)` - peer identity, consulted by the AddressBook.
31//! - `Site(NodeSiteId)` - data-plane slot fill target.
32//! - `Component(ComponentRef)` - control-plane component identity.
33//! - `Op(String)` - control-plane op name for `dispatch_atomic`.
34//!
35//! Receivers route directly by parsing the multiaddr suffix - no
36//! per-message-type subscription tables, no endpoint id lookups.
37//! Hosts that need IP/port/sim-channel identity prepend whatever
38//! bytes they like; the framework treats those bytes as opaque.
39
40use std::collections::HashMap;
41use std::fmt;
42
43use bb_ir::types::{Storage, TypeNode, TYPE_MULTIADDRESS};
44
45use crate::ids::{ComponentRef, NodeSiteId, PeerId};
46
47/// Public DSL alias - the chosen name for `Address` on the graph
48/// surface. The internal type is `framework::Address`; the alias
49/// keeps user-facing code (`Output<Multiaddress>`, `Multiaddress`
50/// constants) reading naturally without introducing a second type.
51pub type Multiaddress = Address;
52
53/// One typed protocol segment in an [`Address`] multiaddr. Each
54/// variant maps to a stable BB-specific multiaddr code in the
55/// 0xE1-0xEF range (`P2p` reuses the libp2p code 0x55 since it
56/// carries the BB `PeerId` identity that crosses transport
57/// adapters). The binary encoding is `code (u8) || payload` where
58/// the payload's length is fixed for every variant except `Op`
59/// (which is length-prefixed).
60#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
61pub enum Protocol {
62    /// Peer identity (BB `PeerId`).
63    P2p(PeerId),
64    /// Data-plane slot fill target - the receiver writes the payload
65    /// to the slot at this `NodeSiteId` and pushes downstream
66    /// consumers.
67    Site(NodeSiteId),
68    /// Control-plane component identity. Combined with an `Op`
69    /// segment, identifies the receiver's `dispatch_atomic` target.
70    Component(ComponentRef),
71    /// Op name for control-plane dispatch (e.g. `"FindNode"`). The
72    /// receiver calls `component[cref].dispatch_atomic(op_name,
73    /// payload, ctx)`.
74    Op(String),
75}
76
77/// Standard multiaddr protocol code for `/p2p/<peerid>`. Encoded
78/// as an unsigned LEB128 varint; the value is a length-prefixed
79/// multihash. Matches libp2p's wire format byte-for-byte.
80const CODE_P2P: u64 = 421;
81
82/// Framework-internal slot-routing segment. Value is a fixed
83/// 8-byte big-endian `NodeSiteId`. Code lives outside the libp2p
84/// standard range; values 0xE0-0xEF are unassigned upstream.
85const CODE_SITE: u64 = 0xE2;
86
87/// Framework-internal component-routing segment. Value is a
88/// fixed 4-byte big-endian `ComponentRef`.
89const CODE_COMPONENT: u64 = 0xE3;
90
91/// Framework-internal control-plane op name. Value is a
92/// length-prefixed UTF-8 string (varint-prefix matches `/p2p/`).
93const CODE_OP: u64 = 0xE4;
94
95impl Protocol {
96    /// Multiaddr protocol code. Encoded as an unsigned LEB128
97    /// varint on the wire. `P2p` uses the standard libp2p code
98    /// 421; framework-internal codes (Site / Component / Op) live
99    /// in the 0xE0-range.
100    pub const fn code(&self) -> u64 {
101        match self {
102            Protocol::P2p(_) => CODE_P2P,
103            Protocol::Site(_) => CODE_SITE,
104            Protocol::Component(_) => CODE_COMPONENT,
105            Protocol::Op(_) => CODE_OP,
106        }
107    }
108
109    fn write_to(&self, out: &mut Vec<u8>) {
110        let mut code_buf = unsigned_varint::encode::u64_buffer();
111        out.extend_from_slice(unsigned_varint::encode::u64(self.code(), &mut code_buf));
112        match self {
113            Protocol::P2p(p) => {
114                // `/p2p/<peerid>`: value is length-prefixed
115                // multihash bytes - matches libp2p exactly.
116                let mh_bytes = p.to_bytes();
117                let mut len_buf = unsigned_varint::encode::usize_buffer();
118                out.extend_from_slice(unsigned_varint::encode::usize(mh_bytes.len(), &mut len_buf));
119                out.extend_from_slice(&mh_bytes);
120            }
121            Protocol::Site(s) => out.extend_from_slice(&s.as_u64().to_be_bytes()),
122            Protocol::Component(c) => out.extend_from_slice(&c.as_u32().to_be_bytes()),
123            Protocol::Op(name) => {
124                let bytes = name.as_bytes();
125                let mut len_buf = unsigned_varint::encode::usize_buffer();
126                out.extend_from_slice(unsigned_varint::encode::usize(bytes.len(), &mut len_buf));
127                out.extend_from_slice(bytes);
128            }
129        }
130    }
131
132    /// Parse one segment from the head of `buf`. On success returns
133    /// `(protocol, bytes_consumed)`. The caller advances by
134    /// `bytes_consumed` for the next segment.
135    fn read_from(buf: &[u8]) -> Result<(Self, usize), AddressError> {
136        let (code, rest) =
137            unsigned_varint::decode::u64(buf).map_err(|_| AddressError::Truncated)?;
138        let code_len = buf.len() - rest.len();
139        let (prot, payload_len) = match code {
140            CODE_P2P => {
141                let (mh_len, after_len) =
142                    unsigned_varint::decode::usize(rest).map_err(|_| AddressError::Truncated)?;
143                let mh_len_bytes = rest.len() - after_len.len();
144                let mh_bytes = after_len.get(0..mh_len).ok_or(AddressError::Truncated)?;
145                let peer =
146                    PeerId::from_bytes(mh_bytes).map_err(|_| AddressError::InvalidValue {
147                        protocol: "p2p".into(),
148                        value: format!("malformed multihash ({mh_len} bytes)"),
149                    })?;
150                (Protocol::P2p(peer), mh_len_bytes + mh_len)
151            }
152            CODE_SITE => {
153                let bytes: [u8; 8] = rest
154                    .get(0..8)
155                    .ok_or(AddressError::Truncated)?
156                    .try_into()
157                    .expect("8-byte slice");
158                (
159                    Protocol::Site(NodeSiteId::from(u64::from_be_bytes(bytes))),
160                    8,
161                )
162            }
163            CODE_COMPONENT => {
164                let bytes: [u8; 4] = rest
165                    .get(0..4)
166                    .ok_or(AddressError::Truncated)?
167                    .try_into()
168                    .expect("4-byte slice");
169                (
170                    Protocol::Component(ComponentRef::from(u32::from_be_bytes(bytes))),
171                    4,
172                )
173            }
174            CODE_OP => {
175                let (str_len, after_len) =
176                    unsigned_varint::decode::usize(rest).map_err(|_| AddressError::Truncated)?;
177                let len_bytes = rest.len() - after_len.len();
178                let str_bytes = after_len.get(0..str_len).ok_or(AddressError::Truncated)?;
179                let name = std::str::from_utf8(str_bytes)
180                    .map_err(|_| AddressError::InvalidValue {
181                        protocol: "op".into(),
182                        value: format!("non-utf8 {str_len} bytes"),
183                    })?
184                    .to_string();
185                (Protocol::Op(name), len_bytes + str_len)
186            }
187            other => {
188                // `UnknownCode` only carries a u8 today - narrow
189                // unrecognized varints into that range for the
190                // existing error variant.
191                return Err(AddressError::UnknownCode((other & 0xFF) as u8));
192            }
193        };
194        Ok((prot, code_len + payload_len))
195    }
196}
197
198/// Multiaddr - a sequence of typed [`Protocol`] segments describing
199/// a delivery path. Per `docs/ADDRESSING.md`, this is BB's canonical
200/// address type: the suffix segments tell the receiver where to
201/// route inside its own node, so no per-message-type or
202/// per-endpoint-id lookup tables are needed.
203///
204/// Construct via the builder chain
205/// (`Address::empty().p2p(...).site(...).component(...).op(...)`),
206/// [`Address::from_bytes`], or [`Address::parse_str`].
207#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
208pub struct Address {
209    segments: Vec<Protocol>,
210}
211
212/// Decode errors surfaced by [`Address::from_bytes`] and
213/// [`Address::parse_str`].
214#[derive(Debug, Clone, Eq, PartialEq)]
215pub enum AddressError {
216    /// Buffer ended before a segment finished decoding.
217    Truncated,
218    /// First byte of a segment didn't match any known protocol code.
219    UnknownCode(u8),
220    /// String form didn't match `/protocol/value/...`.
221    MalformedString(String),
222    /// String value couldn't be parsed as the named protocol's type.
223    InvalidValue {
224        /// Protocol whose value failed to parse.
225        protocol: String,
226        /// Original raw value.
227        value: String,
228    },
229}
230
231impl fmt::Display for AddressError {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        match self {
234            AddressError::Truncated => write!(f, "Address: buffer truncated"),
235            AddressError::UnknownCode(c) => write!(f, "Address: unknown protocol code 0x{c:x}"),
236            AddressError::MalformedString(s) => write!(f, "Address: malformed string `{s}`"),
237            AddressError::InvalidValue { protocol, value } => {
238                write!(
239                    f,
240                    "Address: invalid value `{value}` for protocol `{protocol}`"
241                )
242            }
243        }
244    }
245}
246
247impl std::error::Error for AddressError {}
248
249impl Address {
250    /// Empty Address - useful as a "no destination" sentinel.
251    pub fn empty() -> Self {
252        Self::default()
253    }
254
255    /// Append a segment. Internal - external callers use the typed
256    /// `.p2p()` / `.site()` / `.component()` / `.op()` builders so the
257    /// allowed segment shapes can't drift outside this module.
258    fn with(mut self, segment: Protocol) -> Self {
259        self.segments.push(segment);
260        self
261    }
262
263    /// Builder - append P2P peer id.
264    pub fn p2p(self, peer: PeerId) -> Self {
265        self.with(Protocol::P2p(peer))
266    }
267    /// Builder - append data-plane Site segment.
268    pub fn site(self, site: NodeSiteId) -> Self {
269        self.with(Protocol::Site(site))
270    }
271    /// Builder - append control-plane Component segment.
272    pub fn component(self, c: ComponentRef) -> Self {
273        self.with(Protocol::Component(c))
274    }
275    /// Builder - append control-plane Op segment.
276    pub fn op(self, name: impl Into<String>) -> Self {
277        self.with(Protocol::Op(name.into()))
278    }
279
280    /// Read-only view of the segments.
281    pub fn segments(&self) -> &[Protocol] {
282        &self.segments
283    }
284
285    /// First `P2p` segment's peer id, if any.
286    pub fn peer_id(&self) -> Option<PeerId> {
287        self.segments.iter().find_map(|p| match p {
288            Protocol::P2p(id) => Some(*id),
289            _ => None,
290        })
291    }
292
293    /// First `Site` segment, if any.
294    pub fn site_id(&self) -> Option<NodeSiteId> {
295        self.segments.iter().find_map(|p| match p {
296            Protocol::Site(id) => Some(*id),
297            _ => None,
298        })
299    }
300
301    /// First `Component` segment, if any.
302    pub fn component_ref(&self) -> Option<ComponentRef> {
303        self.segments.iter().find_map(|p| match p {
304            Protocol::Component(c) => Some(*c),
305            _ => None,
306        })
307    }
308
309    /// First `Op` segment, if any (control-plane op name).
310    pub fn op_name(&self) -> Option<&str> {
311        self.segments.iter().find_map(|p| match p {
312            Protocol::Op(name) => Some(name.as_str()),
313            _ => None,
314        })
315    }
316
317    /// Encode to canonical binary form.
318    pub fn to_bytes(&self) -> Vec<u8> {
319        let mut buf = Vec::new();
320        for seg in &self.segments {
321            seg.write_to(&mut buf);
322        }
323        buf
324    }
325
326    /// Decode from canonical binary form.
327    pub fn from_bytes(mut bytes: &[u8]) -> Result<Self, AddressError> {
328        let mut segments: Vec<Protocol> = Vec::new();
329        while !bytes.is_empty() {
330            let (seg, consumed) = Protocol::read_from(bytes)?;
331            segments.push(seg);
332            bytes = &bytes[consumed..];
333        }
334        Ok(Address { segments })
335    }
336
337    /// Parse a `/protocol/value/...` string form. Only the four
338    /// BB-internal protocols (`p2p`, `site`, `component`, `op`) are
339    /// recognized - transport-layer segments (ip4/tcp/udp/etc.)
340    /// belong to host adapters and never reach this parser.
341    ///
342    /// Example: `/p2p/12/site/17` or `/p2p/12/component/5/op/FindNode`.
343    pub fn parse_str(s: &str) -> Result<Self, AddressError> {
344        let trimmed = s.trim();
345        if trimmed.is_empty() {
346            return Ok(Self::empty());
347        }
348        if !trimmed.starts_with('/') {
349            return Err(AddressError::MalformedString(s.to_string()));
350        }
351        let mut parts = trimmed[1..].split('/').peekable();
352        let mut segments: Vec<Protocol> = Vec::new();
353        while parts.peek().is_some() {
354            let protocol = parts
355                .next()
356                .ok_or_else(|| AddressError::MalformedString(s.to_string()))?;
357            let value = parts
358                .next()
359                .ok_or_else(|| AddressError::MalformedString(s.to_string()))?;
360            let seg = match protocol {
361                "p2p" => {
362                    // Value is base58btc-encoded multihash bytes,
363                    // matches libp2p's `/p2p/Qm…` string form.
364                    let mh_bytes = bs58::decode(value)
365                        .into_vec()
366                        .map_err(|_| invalid(protocol, value))?;
367                    Protocol::P2p(
368                        PeerId::from_bytes(&mh_bytes).map_err(|_| invalid(protocol, value))?,
369                    )
370                }
371                "site" => Protocol::Site(NodeSiteId::from(
372                    value.parse::<u64>().map_err(|_| invalid(protocol, value))?,
373                )),
374                "component" => Protocol::Component(ComponentRef::from(
375                    value.parse::<u32>().map_err(|_| invalid(protocol, value))?,
376                )),
377                "op" => Protocol::Op(value.to_string()),
378                other => return Err(invalid(other, value)),
379            };
380            segments.push(seg);
381        }
382        Ok(Address { segments })
383    }
384}
385
386fn invalid(protocol: &str, value: &str) -> AddressError {
387    AddressError::InvalidValue {
388        protocol: protocol.into(),
389        value: value.into(),
390    }
391}
392
393impl fmt::Display for Address {
394    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
395        for seg in &self.segments {
396            match seg {
397                Protocol::P2p(id) => write!(f, "/p2p/{id}")?,
398                Protocol::Site(id) => write!(f, "/site/{}", id.as_u64())?,
399                Protocol::Component(c) => write!(f, "/component/{}", c.as_u32())?,
400                Protocol::Op(name) => write!(f, "/op/{}", name)?,
401            }
402        }
403        Ok(())
404    }
405}
406
407impl std::str::FromStr for Address {
408    type Err = AddressError;
409    fn from_str(s: &str) -> Result<Self, Self::Err> {
410        Self::parse_str(s)
411    }
412}
413
414// `Address` is `Sized` (it owns `Vec<Protocol>`), so the `Storage`
415// impl applies to the owned type — distinct from the tensor leaves
416// in `bb-ir`, which impl `Storage` for `?Sized` slice types. The
417// associated `Storage::TYPE` lets custom ops declare `Multiaddress`
418// ports and have the type solver narrow on them.
419impl Storage for Address {
420    const TYPE: &'static TypeNode = &TYPE_MULTIADDRESS;
421}
422
423// `Address` itself is never a slot carrier in production code —
424// the typed wrapper `crate::syscall::values::AddressValue` is what
425// flows through the slot table, and that's where the
426// `register_type_node!(AddressValue, &TYPE_MULTIADDRESS)` binding
427// lives. The `Storage for Address` impl above is what custom ops
428// reach for at compile time when declaring `Multiaddress` ports.
429
430/// Errors surfaced by `AddressBook` mutation methods.
431#[derive(Clone, Debug, Eq, PartialEq)]
432pub enum AddressBookError {
433    /// `register_address` / `forget_address` / `drop_peer` called
434    /// for a `PeerId` with no entry in the book.
435    UnknownPeer(PeerId),
436    /// `add_peer` called with an empty address vector. An entry
437    /// with zero addresses can't be looked up successfully, so
438    /// creating one is meaningless - reject up front.
439    EmptyAddressList,
440    /// `add_peer` for a NEW peer when the book is already at its
441    /// configured cap. Adversarial peer-discovery floods can't
442    /// grow the book without bound.
443    Full {
444        /// Current cap.
445        cap: usize,
446    },
447    /// `add_peer`'s internal dedup buffer could not be reserved.
448    /// `Vec::try_reserve_exact` returned `TryReserveError` - the
449    /// host's allocator has no headroom for `requested` addresses.
450    /// The engine maps this to `WireReceiveErrorKind::AllocationFailed`
451    /// so the receiver-side address-book hint is best-effort under
452    /// allocator pressure (the envelope still routes).
453    AllocationFailed {
454        /// Address count the dedup buffer attempted to reserve.
455        requested: usize,
456    },
457}
458
459impl fmt::Display for AddressBookError {
460    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461        match self {
462            AddressBookError::UnknownPeer(p) => {
463                write!(f, "AddressBook: peer {p} not registered")
464            }
465            AddressBookError::EmptyAddressList => {
466                write!(f, "AddressBook: add_peer requires a non-empty address list")
467            }
468            AddressBookError::Full { cap } => {
469                write!(f, "AddressBook: at cap {cap}, new peer rejected")
470            }
471            AddressBookError::AllocationFailed { requested } => {
472                write!(
473                    f,
474                    "AddressBook: dedup reservation for {requested} addresses failed"
475                )
476            }
477        }
478    }
479}
480
481impl std::error::Error for AddressBookError {}
482
483/// Per-peer storage: ordered address list + reference count.
484struct AddressEntry {
485    addresses: Vec<Address>,
486    ref_count: u64,
487}
488
489/// Default cap on tracked peer entries. Adversarial peer-discovery
490/// floods (gossip announcements, peer-list responses, multi-address
491/// chatter) would otherwise grow `entries` without bound.
492pub const DEFAULT_ADDRESS_BOOK_CAP: usize = 16_384;
493
494/// Ref-counted `PeerId → Vec<Address>` registry. Single source of
495/// truth for "where can I reach this peer."
496pub struct AddressBook {
497    entries: HashMap<PeerId, AddressEntry>,
498    /// Maximum permitted entry count. `add_peer` for a NEW peer
499    /// fails with `AddressBookError::Full` once `entries.len() >=
500    /// cap`. Tunable via [`Self::set_cap`].
501    cap: usize,
502}
503
504impl Default for AddressBook {
505    fn default() -> Self {
506        Self {
507            entries: HashMap::new(),
508            cap: DEFAULT_ADDRESS_BOOK_CAP,
509        }
510    }
511}
512
513impl AddressBook {
514    /// Fresh, empty address book.
515    pub fn new() -> Self {
516        Self::default()
517    }
518
519    /// Override the entry cap. Production hosts can match the cap
520    /// to their topology size (a small cluster + 2× headroom).
521    pub fn set_cap(&mut self, cap: usize) {
522        self.cap = cap.max(1);
523    }
524
525    /// Announce a peer with one or more addresses.
526    ///
527    /// - **New peer** → entry created with `ref_count = 1`,
528    ///   addresses inserted in the given order.
529    /// - **Known peer** → `ref_count += 1`. New addresses appended;
530    ///   duplicates dropped, existing entries preserve their
531    ///   position (first caller's preference wins).
532    ///
533    /// Errors with [`AddressBookError::EmptyAddressList`] if
534    /// `addresses` is empty.
535    ///
536    /// Components MUST pair every `add_peer` with an eventual
537    /// [`Self::drop_peer`] so `ref_count` converges to zero.
538    pub fn add_peer(
539        &mut self,
540        peer: PeerId,
541        addresses: Vec<Address>,
542    ) -> Result<(), AddressBookError> {
543        if addresses.is_empty() {
544            return Err(AddressBookError::EmptyAddressList);
545        }
546        match self.entries.get_mut(&peer) {
547            Some(entry) => {
548                entry.ref_count = entry.ref_count.saturating_add(1);
549                for addr in addresses {
550                    if !entry.addresses.contains(&addr) {
551                        entry.addresses.push(addr);
552                    }
553                }
554            }
555            None => {
556                if self.entries.len() >= self.cap {
557                    return Err(AddressBookError::Full { cap: self.cap });
558                }
559                let mut dedup: Vec<Address> = Vec::new();
560                crate::fallible::try_reserve_exact(&mut dedup, addresses.len()).map_err(|_| {
561                    AddressBookError::AllocationFailed {
562                        requested: addresses.len(),
563                    }
564                })?;
565                for addr in addresses {
566                    if !dedup.contains(&addr) {
567                        dedup.push(addr);
568                    }
569                }
570                self.entries.insert(
571                    peer,
572                    AddressEntry {
573                        addresses: dedup,
574                        ref_count: 1,
575                    },
576                );
577            }
578        }
579        Ok(())
580    }
581
582    /// Release one reference to `peer`. Decrements `ref_count`;
583    /// removes the entry (and all addresses) when the count
584    /// reaches zero. Errors with
585    /// [`AddressBookError::UnknownPeer`] if no entry exists.
586    pub fn drop_peer(&mut self, peer: PeerId) -> Result<(), AddressBookError> {
587        let Some(entry) = self.entries.get_mut(&peer) else {
588            return Err(AddressBookError::UnknownPeer(peer));
589        };
590        entry.ref_count = entry.ref_count.saturating_sub(1);
591        if entry.ref_count == 0 {
592            self.entries.remove(&peer);
593        }
594        Ok(())
595    }
596
597    /// Append `address` to the peer's list. Idempotent - duplicates
598    /// are dropped. Does NOT change `ref_count`. Errors with
599    /// [`AddressBookError::UnknownPeer`] if the peer has no entry.
600    pub fn register_address(
601        &mut self,
602        peer: PeerId,
603        address: Address,
604    ) -> Result<(), AddressBookError> {
605        let Some(entry) = self.entries.get_mut(&peer) else {
606            return Err(AddressBookError::UnknownPeer(peer));
607        };
608        if !entry.addresses.contains(&address) {
609            entry.addresses.push(address);
610        }
611        Ok(())
612    }
613
614    /// Prune one unreachable `address` from the peer's list.
615    /// Transport adapters call this after observing a
616    /// transport-level failure on a specific address. Does NOT
617    /// change `ref_count` and does NOT remove the entry even if
618    /// pruning leaves the address list empty - [`Self::drop_peer`]
619    /// is the only path that removes entries. Errors with
620    /// [`AddressBookError::UnknownPeer`] if no entry exists.
621    pub fn forget_address(
622        &mut self,
623        peer: PeerId,
624        address: &Address,
625    ) -> Result<(), AddressBookError> {
626        let Some(entry) = self.entries.get_mut(&peer) else {
627            return Err(AddressBookError::UnknownPeer(peer));
628        };
629        entry.addresses.retain(|a| a != address);
630        Ok(())
631    }
632
633    /// Ordered slice of every address bound to `peer`. Returns
634    /// `None` for an unknown peer OR a peer whose address list is
635    /// empty (e.g. all addresses pruned via `forget_address` and
636    /// nothing re-added). Both cases mean "can't route" to the
637    /// wire syscall.
638    pub fn lookup(&self, peer: PeerId) -> Option<&[Address]> {
639        let entry = self.entries.get(&peer)?;
640        if entry.addresses.is_empty() {
641            None
642        } else {
643            Some(entry.addresses.as_slice())
644        }
645    }
646
647    /// Convenience: the first (highest-preference) address.
648    pub fn lookup_first(&self, peer: PeerId) -> Option<&Address> {
649        self.lookup(peer).and_then(|addrs| addrs.first())
650    }
651
652    /// Current reference count for `peer`, or `0` if unregistered.
653    pub fn ref_count(&self, peer: PeerId) -> u64 {
654        self.entries.get(&peer).map(|e| e.ref_count).unwrap_or(0)
655    }
656
657    /// Number of registered peers (regardless of ref_count).
658    pub fn len(&self) -> usize {
659        self.entries.len()
660    }
661
662    /// `true` when no peers are registered.
663    pub fn is_empty(&self) -> bool {
664        self.entries.is_empty()
665    }
666
667    /// Iterate `(peer, &[Address], ref_count)` triples for every
668    /// registered peer. Used by snapshot capture.
669    pub fn iter(&self) -> impl Iterator<Item = (PeerId, &[Address], u64)> {
670        self.entries
671            .iter()
672            .map(|(p, e)| (*p, e.addresses.as_slice(), e.ref_count))
673    }
674
675    /// Snapshot-restore setter - reconstructs an entry with the
676    /// recorded addresses + ref_count without going through
677    /// [`Self::add_peer`] (which would force `ref_count = 1`).
678    /// Used exclusively by `Node::restore`.
679    pub fn restore_entry(&mut self, peer: PeerId, addresses: Vec<Address>, ref_count: u64) {
680        self.entries.insert(
681            peer,
682            AddressEntry {
683                addresses,
684                ref_count,
685            },
686        );
687    }
688}
689