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