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