Skip to main content

urma_node/
peer_gossip.rs

1//! Peer gossip and explicit dialing. Gossiped addresses are untrusted routing
2//! hints; a dial proves only the remote transport identity, and the Peers probe
3//! that follows it only that the peer speaks this protocol on the same chain.
4use crate::{
5    Error,
6    peer::{
7        Answer, Behaviour, CALL_TIMEOUT, Command, PeerNetwork, Pending, bind_response, request,
8    },
9    peer_limits::LimitDenied,
10    peer_protocol::{
11        GossipPeer, Kind, PEER_ADDRESSES_MAX, PEERS_LIMIT, PEERS_MAX, Reply, Response,
12    },
13    source::check_cancelled,
14};
15use bitcoin::BlockHash;
16use libp2p::{
17    Multiaddr, PeerId, Swarm,
18    multiaddr::Protocol,
19    swarm::{ConnectionId, DialError, dial_opts::DialOpts},
20};
21use std::{
22    collections::{BTreeMap, BTreeSet, HashMap, VecDeque},
23    net::{Ipv4Addr, Ipv6Addr},
24    sync::{
25        Mutex, MutexGuard,
26        atomic::{AtomicBool, Ordering},
27        mpsc::{self, RecvTimeoutError},
28    },
29    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
30};
31
32/// State shared between the network thread and the owner: the gossip snapshot
33/// the scheduler hands over, public addresses peers observed for us, the live
34/// connections and our bound listeners.
35pub(crate) struct Shared {
36    local: PeerId,
37    gossip: Mutex<Vec<GossipPeer>>,
38    observed: Mutex<ObservedAddrs>,
39    connected: Mutex<BTreeMap<PeerId, Links>>,
40    identified: Mutex<VecDeque<(PeerId, Vec<Multiaddr>)>>,
41    listening: Mutex<Vec<Multiaddr>>,
42}
43
44/// Established connections of one peer by direction, and whether it has
45/// spoken this protocol version on our chain over any of them.
46#[derive(Default)]
47struct Links {
48    inbound: u32,
49    outbound: u32,
50    on_chain: bool,
51}
52
53fn guard<T>(lock: &Mutex<T>) -> MutexGuard<'_, T> {
54    match lock.lock() {
55        Ok(guard) => guard,
56        Err(poisoned) => poisoned.into_inner(),
57    }
58}
59
60/// Public addresses outbound peers reported observing for this node. A
61/// candidate is confirmed only once WITNESSES distinct peers reported it.
62#[derive(Default)]
63pub struct ObservedAddrs {
64    candidates: Vec<(Multiaddr, BTreeSet<PeerId>)>,
65    confirmed: Vec<Multiaddr>,
66}
67
68impl ObservedAddrs {
69    pub const WITNESSES: usize = 2;
70    const CANDIDATES_MAX: usize = 32;
71    const CONFIRMED_MAX: usize = 8;
72
73    /// Record one report; non-public or non-literal addresses are ignored.
74    /// Returns the address when this report is the one that confirms it.
75    pub fn observe(&mut self, address: Multiaddr, reporter: PeerId) -> Option<Multiaddr> {
76        if !is_public(&address)
77            || validate_gossip_address(&address).is_err()
78            || self.confirmed.contains(&address)
79        {
80            return None;
81        }
82        let index = match self
83            .candidates
84            .iter()
85            .position(|(known, _)| *known == address)
86        {
87            Some(index) => index,
88            None => {
89                if self.candidates.len() == Self::CANDIDATES_MAX {
90                    self.candidates.remove(0);
91                }
92                self.candidates.push((address, BTreeSet::new()));
93                self.candidates.len() - 1
94            }
95        };
96        self.candidates[index].1.insert(reporter);
97        if self.candidates[index].1.len() < Self::WITNESSES {
98            return None;
99        }
100        let (address, _) = self.candidates.remove(index);
101        if self.confirmed.len() == Self::CONFIRMED_MAX {
102            self.confirmed.remove(0);
103        }
104        self.confirmed.push(address.clone());
105        Some(address)
106    }
107
108    pub fn confirmed(&self) -> Vec<Multiaddr> {
109        self.confirmed.clone()
110    }
111}
112
113impl Shared {
114    const GOSSIP_MAX: usize = 256;
115    const IDENTIFIED_MAX: usize = 64;
116    const ENVELOPE: usize = 1024;
117
118    pub(crate) fn new(local: PeerId) -> Self {
119        Self {
120            local,
121            gossip: Mutex::new(Vec::new()),
122            observed: Mutex::new(ObservedAddrs::default()),
123            connected: Mutex::new(BTreeMap::new()),
124            identified: Mutex::new(VecDeque::new()),
125            listening: Mutex::new(Vec::new()),
126        }
127    }
128
129    /// Queue a connected peer's own literal public addresses (newest 64 peers).
130    pub(crate) fn identified(&self, peer: PeerId, advertised: Vec<Multiaddr>) {
131        let addresses: Vec<Multiaddr> = advertised
132            .into_iter()
133            .filter(|address| validate_gossip_address(address).is_ok() && is_public(address))
134            .take(PEER_ADDRESSES_MAX)
135            .collect();
136        if addresses.is_empty() || peer == self.local {
137            return;
138        }
139        let mut queue = match self.identified.lock() {
140            Ok(queue) => queue,
141            Err(poisoned) => poisoned.into_inner(),
142        };
143        queue.retain(|(known, _)| *known != peer);
144        if queue.len() == Self::IDENTIFIED_MAX {
145            queue.pop_front();
146        }
147        queue.push_back((peer, addresses));
148    }
149
150    fn drain_identified(&self) -> Vec<(PeerId, Vec<Multiaddr>)> {
151        match self.identified.lock() {
152            Ok(mut queue) => queue.drain(..).collect(),
153            Err(poisoned) => poisoned.into_inner().drain(..).collect(),
154        }
155    }
156
157    fn set_gossip(&self, mut peers: Vec<GossipPeer>) {
158        peers.truncate(Self::GOSSIP_MAX);
159        if let Ok(mut gossip) = self.gossip.lock() {
160            *gossip = peers;
161        }
162    }
163
164    fn observed(&self) -> Vec<Multiaddr> {
165        match self.observed.lock() {
166            Ok(observed) => observed.confirmed(),
167            Err(poisoned) => poisoned.into_inner().confirmed(),
168        }
169    }
170
171    pub(crate) fn is_connected(&self, peer: PeerId) -> bool {
172        guard(&self.connected).contains_key(&peer)
173    }
174
175    /// Whether the peer holds a connection we dialed.
176    pub(crate) fn is_outbound(&self, peer: PeerId) -> bool {
177        guard(&self.connected)
178            .get(&peer)
179            .is_some_and(|links| links.outbound > 0)
180    }
181
182    fn connected(&self) -> Vec<PeerId> {
183        guard(&self.connected).keys().copied().collect()
184    }
185
186    /// Connected peers that spoke this protocol version on our chain, those
187    /// we dialed before those that only dialed us.
188    pub(crate) fn live(&self) -> Vec<PeerId> {
189        let connected = guard(&self.connected);
190        let (outbound, inbound): (Vec<_>, Vec<_>) = connected
191            .iter()
192            .filter(|(_, links)| links.on_chain)
193            .partition(|(_, links)| links.outbound > 0);
194        outbound
195            .into_iter()
196            .chain(inbound)
197            .map(|(peer, _)| *peer)
198            .collect()
199    }
200
201    pub(crate) fn opened(&self, peer: PeerId, outbound: bool) {
202        let mut connected = guard(&self.connected);
203        let links = connected.entry(peer).or_default();
204        match outbound {
205            true => links.outbound += 1,
206            false => links.inbound += 1,
207        }
208    }
209
210    /// The peer leaves with its last connection, chain proof included.
211    pub(crate) fn closed(&self, peer: PeerId, outbound: bool) {
212        let mut connected = guard(&self.connected);
213        let Some(links) = connected.get_mut(&peer) else {
214            return;
215        };
216        match outbound {
217            true => links.outbound = links.outbound.saturating_sub(1),
218            false => links.inbound = links.inbound.saturating_sub(1),
219        }
220        if links.inbound == 0 && links.outbound == 0 {
221            connected.remove(&peer);
222        }
223    }
224
225    /// A message carrying this protocol version and our genesis came from or
226    /// was answered by the peer over a live connection.
227    pub(crate) fn on_chain(&self, peer: PeerId) {
228        if let Some(links) = guard(&self.connected).get_mut(&peer) {
229            links.on_chain = true;
230        }
231    }
232
233    pub(crate) fn listening(&self, address: Multiaddr, bound: bool) {
234        let mut listening = guard(&self.listening);
235        listening.retain(|known| *known != address);
236        if bound {
237            listening.push(address);
238        }
239    }
240
241    pub(crate) fn listeners(&self) -> Vec<Multiaddr> {
242        guard(&self.listening).clone()
243    }
244
245    /// One identify report, counted only from outbound connections. Returns
246    /// the address once this report confirms it.
247    pub(crate) fn observe(&self, address: Multiaddr, reporter: PeerId) -> Option<Multiaddr> {
248        match self.observed.lock() {
249            Ok(mut observed) => observed.observe(address, reporter),
250            Err(poisoned) => poisoned.into_inner().observe(address, reporter),
251        }
252    }
253
254    /// Answer a Peers request from the snapshot. Only IP-literal addresses are
255    /// forwarded, private and loopback ones only to loopback requesters. Neither
256    /// the requester nor this node is listed; future `last_seen` is clamped.
257    pub(crate) fn serve(&self, requester: PeerId, loopback: bool, limit: u32) -> Reply {
258        let Ok(limit) = usize::try_from(limit) else {
259            return Reply::Refused;
260        };
261        if limit > usize::try_from(PEERS_MAX).unwrap_or(0) {
262            return Reply::Refused;
263        }
264        let Ok(snapshot) = self.gossip.lock() else {
265            return Reply::Busy;
266        };
267        let now = unix_now();
268        let mut peers = Vec::new();
269        let listed = snapshot
270            .iter()
271            .filter(|entry| entry.peer_id != requester && entry.peer_id != self.local);
272        for entry in listed {
273            if peers.len() == limit {
274                break;
275            }
276            let addrs: Vec<Multiaddr> = entry
277                .addrs
278                .iter()
279                .filter(|address| validate_gossip_address(address).is_ok())
280                .filter(|address| loopback || is_public(address))
281                .take(PEER_ADDRESSES_MAX)
282                .cloned()
283                .collect();
284            if !addrs.is_empty() {
285                peers.push(GossipPeer {
286                    peer_id: entry.peer_id,
287                    addrs,
288                    last_seen: entry.last_seen.min(now),
289                });
290            }
291        }
292        loop {
293            match serde_json::to_vec(&peers) {
294                Ok(bytes) if bytes.len() + Self::ENVELOPE <= PEERS_LIMIT => break,
295                Ok(_) => {
296                    peers.pop();
297                }
298                Err(_) => return Reply::Busy,
299            }
300        }
301        Reply::Peers(peers)
302    }
303}
304
305/// Globally routable IP, or a DNS name other than localhost. IPv4-mapped IPv6
306/// addresses are judged as the IPv4 address they carry.
307pub fn is_public(address: &Multiaddr) -> bool {
308    match address.iter().next() {
309        Some(Protocol::Ip4(ip)) => public_v4(ip),
310        Some(Protocol::Ip6(ip)) => match ip.to_ipv4_mapped() {
311            Some(mapped) => public_v4(mapped),
312            None => public_v6(ip),
313        },
314        Some(
315            Protocol::Dns(name)
316            | Protocol::Dns4(name)
317            | Protocol::Dns6(name)
318            | Protocol::Dnsaddr(name),
319        ) => name != "localhost" && !name.ends_with(".localhost"),
320        _ => false,
321    }
322}
323
324fn public_v4(ip: Ipv4Addr) -> bool {
325    let [first, second, third, _] = ip.octets();
326    let reserved = first == 0
327        || (first == 100 && (second & 0xc0) == 64)
328        || (first == 192 && second == 0 && third == 0)
329        || (first == 198 && (second & 0xfe) == 18)
330        || first >= 240;
331    !(reserved
332        || ip.is_loopback()
333        || ip.is_private()
334        || ip.is_link_local()
335        || ip.is_multicast()
336        || ip.is_documentation())
337}
338
339fn public_v6(ip: Ipv6Addr) -> bool {
340    let documentation = ip.segments()[..2] == [0x2001, 0x0db8];
341    !(documentation
342        || ip.is_loopback()
343        || ip.is_unspecified()
344        || ip.is_multicast()
345        || ip.is_unique_local()
346        || ip.is_unicast_link_local())
347}
348
349fn unix_now() -> u64 {
350    match SystemTime::now().duration_since(UNIX_EPOCH) {
351        Ok(elapsed) => elapsed.as_secs(),
352        Err(_clock_before_epoch) => 0,
353    }
354}
355
356pub(crate) fn is_loopback(address: &Multiaddr) -> bool {
357    match address.iter().next() {
358        Some(Protocol::Ip4(ip)) => ip.is_loopback(),
359        Some(Protocol::Ip6(ip)) => ip.is_loopback(),
360        _ => false,
361    }
362}
363
364/// Gossip carries only `/ip4|ip6/<literal>/tcp/<port>[/p2p/<id>]`: names come
365/// from operators (seeds, manual entries), never from peers.
366pub fn validate_gossip_address(address: &Multiaddr) -> Result<Option<PeerId>, Error> {
367    match address.iter().next() {
368        Some(Protocol::Ip4(_) | Protocol::Ip6(_)) => validate_dial(address),
369        _ => Err(Error::Invalid(
370            "gossiped addresses must be IP literals with TCP".into(),
371        )),
372    }
373}
374
375/// An operator announce address: a public IP literal with TCP, without a /p2p
376/// of another node. Returned without /p2p, as identify advertises it.
377pub fn validate_announce(address: &Multiaddr, local: PeerId) -> Result<Multiaddr, Error> {
378    if validate_gossip_address(address)?.is_some_and(|named| named != local) {
379        return Err(Error::Invalid("announce address names another peer".into()));
380    }
381    if !is_public(address) {
382        return Err(Error::Invalid("announce address must be public".into()));
383    }
384    Ok(address
385        .iter()
386        .filter(|part| !matches!(part, Protocol::P2p(_)))
387        .collect())
388}
389
390/// Operator-supplied dial targets: `/ip4|ip6|dns|dns4|dns6/<host>/tcp/<port>`
391/// or `/dnsaddr/<name>`, each with optional `/p2p`. Returns the named identity.
392pub fn validate_dial(address: &Multiaddr) -> Result<Option<PeerId>, Error> {
393    let invalid = || Error::Invalid("peer dial requires host, TCP port and optional /p2p".into());
394    let mut parts = address.iter();
395    match parts.next() {
396        Some(Protocol::Dnsaddr(_)) => (),
397        Some(
398            Protocol::Ip4(_)
399            | Protocol::Ip6(_)
400            | Protocol::Dns(_)
401            | Protocol::Dns4(_)
402            | Protocol::Dns6(_),
403        ) => match parts.next() {
404            Some(Protocol::Tcp(port)) if port != 0 => (),
405            _ => return Err(invalid()),
406        },
407        _ => return Err(invalid()),
408    }
409    let peer = match parts.next() {
410        None => None,
411        Some(Protocol::P2p(peer)) => Some(peer),
412        Some(_) => return Err(invalid()),
413    };
414    if parts.next().is_some() {
415        return Err(invalid());
416    }
417    Ok(peer)
418}
419
420/// Bind the reply to its request, then check every gossiped hint's shape.
421pub(crate) fn validate_peers(
422    genesis: BlockHash,
423    peer: PeerId,
424    pending: &Pending,
425    response: Response,
426) -> Result<Vec<GossipPeer>, Error> {
427    bind_response(genesis, peer, pending, &response)?;
428    let Kind::Peers { limit } = pending.kind else {
429        return Err(Error::Invalid("peers reply to another request".into()));
430    };
431    match response.result {
432        Reply::Peers(peers) => {
433            let limit = usize::try_from(limit).map_err(|e| Error::Invalid(e.to_string()))?;
434            if peers.len() > limit {
435                return Err(Error::Invalid(
436                    "peer gossip exceeds the requested limit".into(),
437                ));
438            }
439            for entry in &peers {
440                check_gossip(entry)?;
441            }
442            Ok(peers)
443        }
444        Reply::Busy => Err(Error::PeerBusy),
445        _ => Err(Error::Invalid(
446            "peer refused the peers request or answered another kind".into(),
447        )),
448    }
449}
450
451fn check_gossip(entry: &GossipPeer) -> Result<(), Error> {
452    let peer = entry.peer_id;
453    if entry.addrs.is_empty() || entry.addrs.len() > PEER_ADDRESSES_MAX {
454        return Err(Error::Invalid(
455            "peer gossip address count out of range".into(),
456        ));
457    }
458    for address in &entry.addrs {
459        if validate_gossip_address(address)?.is_some_and(|named| named != peer) {
460            return Err(Error::Invalid(
461                "peer gossip address names another peer".into(),
462            ));
463        }
464    }
465    Ok(())
466}
467
468pub(crate) struct Dial {
469    address: Multiaddr,
470    deadline: Instant,
471    answer: mpsc::SyncSender<Result<PeerId, Error>>,
472}
473
474impl Dial {
475    fn answer(&self, result: Result<PeerId, Error>) {
476        let _ = self.answer.try_send(result);
477    }
478}
479
480/// Outstanding explicit dials on the network thread, keyed by connection.
481#[derive(Default)]
482pub(crate) struct Dials(HashMap<ConnectionId, (Option<PeerId>, Dial)>);
483
484impl Dials {
485    const MAX: usize = 8;
486
487    pub(crate) fn start(&mut self, swarm: &mut Swarm<Behaviour>, dial: Dial) {
488        let expected = match validate_dial(&dial.address) {
489            Ok(expected) => expected,
490            Err(error) => return dial.answer(Err(error)),
491        };
492        if Instant::now() >= dial.deadline || self.0.len() >= Self::MAX {
493            return dial.answer(Err(Error::Busy("peer dial capacity or deadline".into())));
494        }
495        if let Some(peer) = expected.filter(|peer| swarm.is_connected(peer)) {
496            return dial.answer(Ok(peer));
497        }
498        let options = match expected {
499            Some(peer) => DialOpts::peer_id(peer)
500                .addresses(vec![dial.address.clone()])
501                .build(),
502            None => DialOpts::unknown_peer_id()
503                .address(dial.address.clone())
504                .build(),
505        };
506        let connection = options.connection_id();
507        match swarm.dial(options) {
508            Ok(()) => {
509                self.0.insert(connection, (expected, dial));
510            }
511            Err(error) => dial.answer(Err(dial_error(&error))),
512        }
513    }
514
515    pub(crate) fn established(&mut self, connection: ConnectionId, peer: PeerId) {
516        if let Some((expected, dial)) = self.0.remove(&connection) {
517            if expected.is_some_and(|expected| expected != peer) {
518                return dial.answer(Err(Error::Invalid(
519                    "dialed peer authenticated as another identity".into(),
520                )));
521            }
522            dial.answer(Ok(peer));
523        }
524    }
525
526    pub(crate) fn failed(&mut self, connection: ConnectionId, error: &DialError) {
527        if let Some((_, dial)) = self.0.remove(&connection) {
528            dial.answer(Err(dial_error(error)));
529        }
530    }
531
532    /// A dial past its deadline is answered now; a late connection idles out.
533    pub(crate) fn expire(&mut self) {
534        let now = Instant::now();
535        self.0.retain(|_, (_, dial)| {
536            if now < dial.deadline {
537                return true;
538            }
539            dial.answer(Err(Error::Busy("peer dial deadline".into())));
540            false
541        });
542    }
543
544    pub(crate) fn close(&mut self) {
545        for (_, (_, dial)) in self.0.drain() {
546            dial.answer(Err(Error::Busy("peer network stopped".into())));
547        }
548    }
549}
550
551fn dial_error(error: &DialError) -> Error {
552    match error {
553        DialError::Denied { cause } => match cause.downcast_ref::<LimitDenied>() {
554            Some(LimitDenied(limit)) => Error::DialDenied(*limit),
555            None => Error::Invalid(format!("peer dial: {error}")),
556        },
557        DialError::LocalPeerId { .. } => Error::LocalPeer,
558        DialError::DialPeerConditionFalse(condition) => {
559            Error::Busy(format!("peer dial already pending ({condition:?})"))
560        }
561        other => Error::Invalid(format!("peer dial: {other}")),
562    }
563}
564
565impl PeerNetwork {
566    const DIAL_POLL: Duration = Duration::from_millis(50);
567
568    /// Replace the snapshot served to Peers requests. Order is preserved; the
569    /// first entries are the ones a bounded request receives.
570    pub fn set_gossip(&self, peers: Vec<GossipPeer>) {
571        self.shared.set_gossip(peers);
572    }
573
574    /// Public addresses connected peers reported observing for this node.
575    pub fn observed_addrs(&self) -> Vec<Multiaddr> {
576        self.shared.observed()
577    }
578
579    /// Peers with at least one established connection right now, sorted.
580    pub fn connected_peer_ids(&self) -> Vec<PeerId> {
581        self.shared.connected()
582    }
583
584    /// Connected peers' own literal public addresses learned through identify
585    /// since the last call, newest 64 peers; the queue is emptied.
586    pub fn drain_identified(&self) -> Vec<(PeerId, Vec<Multiaddr>)> {
587        self.shared.drain_identified()
588    }
589
590    /// Dial an address (with or without `/p2p`), then probe the peer with a
591    /// zero-limit Peers request. Returns the authenticated identity; the
592    /// connection stays for later requests. A Busy probe is an error that keeps
593    /// the connection; any other failed probe closes it: the peer is on another
594    /// chain or does not speak this protocol.
595    pub fn dial(
596        &self,
597        address: Multiaddr,
598        cancelled: &AtomicBool,
599        deadline: Instant,
600    ) -> Result<PeerId, Error> {
601        validate_dial(&address)?;
602        check_cancelled(cancelled)?;
603        if self.stop.load(Ordering::Acquire) {
604            return Err(Error::Busy("peer network stopped".into()));
605        }
606        let deadline = deadline.min(Instant::now() + CALL_TIMEOUT);
607        let (answer, receiver) = mpsc::sync_channel(1);
608        self.commands
609            .try_send(Command::Dial(Dial {
610                address,
611                deadline,
612                answer,
613            }))
614            .map_err(|_| Error::Busy("peer command queue unavailable".into()))?;
615        let peer = loop {
616            check_cancelled(cancelled)?;
617            let left = deadline.saturating_duration_since(Instant::now());
618            if left.is_zero() {
619                return Err(Error::Busy("peer dial deadline".into()));
620            }
621            match receiver.recv_timeout(left.min(Self::DIAL_POLL)) {
622                Ok(result) => break result?,
623                Err(RecvTimeoutError::Timeout) => (),
624                Err(RecvTimeoutError::Disconnected) => {
625                    return Err(Error::Busy("peer network stopped".into()));
626                }
627            }
628        };
629        match self.peers_of(peer, 0, cancelled, deadline) {
630            Ok(_) => Ok(peer),
631            Err(busy @ (Error::Busy(_) | Error::PeerBusy)) => Err(busy),
632            Err(error) => {
633                let _ = self.commands.try_send(Command::Disconnect(peer));
634                Err(error)
635            }
636        }
637    }
638
639    /// One bounded Peers request over an existing connection (or addresses the
640    /// swarm already knows). `limit` is at most PEERS_MAX; 0 is a bare probe.
641    pub fn peers_of(
642        &self,
643        peer: PeerId,
644        limit: u32,
645        cancelled: &AtomicBool,
646        deadline: Instant,
647    ) -> Result<Vec<GossipPeer>, Error> {
648        if limit > PEERS_MAX {
649            return Err(Error::Invalid("peers request limit exceeds 32".into()));
650        }
651        check_cancelled(cancelled)?;
652        let kind = Kind::Peers { limit };
653        request(
654            &self.commands,
655            &self.stop,
656            (peer, Vec::new()),
657            kind,
658            deadline,
659            Answer::Peers,
660        )
661    }
662}