Skip to main content

urma_node/
peer_sync.rs

1//! Fast inventory synchronization from configured URMA peers. A peer page is an
2//! untrusted claim: every fetched package is proof-verified by `admit_record`
3//! against the independently selected header chain, and coverage is recorded
4//! only after a complete range has every advertised record present locally.
5//! Full-block audit (`indexed_through`) remains the separate, stronger claim.
6//! A peer that answers with an invalid page, package or reply is disconnected
7//! and backed off; one that stays Busy for many turns is disconnected so the
8//! connection slot it holds frees up for others. So is one that answers turn
9//! after turn without anything we can use (stale listings, unscanned pages),
10//! and a peer refused by the local connection limit gets the slot of the
11//! connected peer that progressed longest ago.
12use crate::{
13    DialLimit, Error,
14    engine::Engine,
15    model::{OverlayPeerStatus, OverlayStatus},
16    peer::{PeerSource, SyncPeers},
17    peer_inventory::{placed, selected},
18    peer_protocol::Placement,
19    recorded::{RecordedError, unix_seconds},
20};
21use libp2p::{Multiaddr, PeerId};
22use range::{Range, accept, open};
23use std::{
24    sync::{
25        Arc, Mutex,
26        atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering},
27    },
28    time::{Duration, Instant},
29};
30
31mod range;
32
33pub type ConnectedPeers = Arc<dyn Fn() -> u64 + Send + Sync>;
34pub type Listeners = Arc<dyn Fn() -> Vec<Multiaddr> + Send + Sync>;
35
36/// Public overlay identity facts reported in status; never secrets.
37pub struct OverlayInfo {
38    pub peer_id: String,
39    pub listen: Listeners,
40    pub announce: Vec<String>,
41    pub peer_book: u64,
42    pub connected: ConnectedPeers,
43}
44
45/// Inventory peers, re-read every step: configured ones, then live ones.
46pub struct PeerSync {
47    pub peers: SyncPeers,
48    pub info: OverlayInfo,
49}
50
51struct Peer {
52    label: String,
53    id: String,
54    peer_id: PeerId,
55    source: PeerSource,
56    range: Mutex<Option<Range>>,
57    last_request: Mutex<Option<Instant>>,
58    last_error: Mutex<Option<RecordedError>>,
59    backoff: Mutex<Backoff>,
60    busy_turns: AtomicU32,
61    idle_turns: AtomicU32,
62    last_progress: Mutex<Option<u64>>,
63    /// Scheduler step at which this peer was first seen connected, if it is.
64    connected_since: Mutex<Option<u64>>,
65}
66
67/// Sync backoff after peer errors: consecutive blamed turns and the Unix
68/// second before which the peer is skipped.
69struct Backoff {
70    failures: u32,
71    until: Option<u64>,
72}
73
74impl Peer {
75    fn backoff_until(&self, now: u64) -> Result<Option<u64>, Error> {
76        let backoff = self.backoff.lock().map_err(|_| poisoned())?;
77        Ok(backoff.until.filter(|until| *until > now))
78    }
79
80    fn last_progress(&self) -> Result<Option<u64>, Error> {
81        Ok(*self.last_progress.lock().map_err(|_| poisoned())?)
82    }
83
84    /// Skip this peer until at least `now + seconds`, unblamed: failures and
85    /// the book are untouched, and a longer backoff is never shortened.
86    fn hold_off(&self, now: u64, seconds: u64) -> Result<(), Error> {
87        let until = now.checked_add(seconds).unwrap_or(u64::MAX);
88        let mut backoff = self.backoff.lock().map_err(|_| poisoned())?;
89        backoff.until = Some(backoff.until.map_or(until, |current| current.max(until)));
90        Ok(())
91    }
92
93    /// Wait for an evicted connection to close so its slot is really free,
94    /// bounded by `EVICT_WAIT`, the step deadline and cancellation.
95    fn wait_closed(&self, stop: &AtomicBool, deadline: Instant) -> bool {
96        let until = deadline.min(Instant::now() + Scheduler::EVICT_WAIT);
97        while self.source.connected() {
98            if stop.load(Ordering::Acquire) || Instant::now() >= until {
99                return false;
100            }
101            std::thread::sleep(Duration::from_millis(25));
102        }
103        true
104    }
105}
106
107/// Why a step stopped early. `Blame` is a protocol violation by the peer;
108/// `Error` is ours, transport or retryable.
109enum Pause {
110    Done,
111    Abandon,
112    Error(Error),
113    Blame(Error),
114}
115
116/// How one step ended for the peer it ran with: remote Busy builds a streak,
117/// local Busy and transport failures retry quietly.
118enum Turn {
119    Quiet,
120    Busy(Error),
121    Retry(Error),
122    Blamed(Error),
123}
124
125/// A failed request blames the peer only for a protocol violation: never for
126/// capacity (ours or theirs), transport failure or our reorg.
127fn blame(error: Error) -> Pause {
128    match error {
129        Error::Busy(_)
130        | Error::PeerBusy
131        | Error::Transport(_)
132        | Error::DialDenied(_)
133        | Error::Stale => Pause::Error(error),
134        other => Pause::Blame(other),
135    }
136}
137
138impl From<Error> for Pause {
139    fn from(error: Error) -> Self {
140        Pause::Error(error)
141    }
142}
143
144/// Per-round budgets for one peer: request spacing, fetch count, wall clock;
145/// whether the peer was asked anything and whether an admission or coverage
146/// came of it.
147struct Round<'a> {
148    stop: &'a AtomicBool,
149    deadline: Instant,
150    fetched: usize,
151    requested_page: bool,
152    contacted: bool,
153    progressed: bool,
154}
155
156impl Round<'_> {
157    fn open(&self) -> Result<(), Pause> {
158        if self.stop.load(Ordering::Acquire) || Instant::now() >= self.deadline {
159            return Err(Pause::Done);
160        }
161        Ok(())
162    }
163}
164
165pub(crate) struct Scheduler {
166    offered: SyncPeers,
167    peers: Mutex<Vec<Arc<Peer>>>,
168    turn: AtomicUsize,
169    configured_turn: AtomicBool,
170    configured_next: AtomicUsize,
171    info: OverlayInfo,
172    admitted: AtomicU64,
173    rejected: AtomicU64,
174    penalized: Mutex<Vec<PeerId>>,
175    steps: AtomicU64,
176}
177
178fn poisoned() -> Error {
179    Error::Invalid("peer synchronization state poisoned".into())
180}
181
182/// The next non-omitting, non-backed-off peer after `turn`.
183fn rotate(
184    peers: &[Arc<Peer>],
185    turn: &AtomicUsize,
186    omitting: &[String],
187    now: u64,
188) -> Result<Option<Arc<Peer>>, Error> {
189    for _ in 0..peers.len() {
190        let peer = &peers[turn.fetch_add(1, Ordering::Relaxed) % peers.len()];
191        if !omitting.contains(&peer.id) && peer.backoff_until(now)?.is_none() {
192            return Ok(Some(Arc::clone(peer)));
193        }
194    }
195    Ok(None)
196}
197
198fn configured(peer: &Arc<Peer>) -> bool {
199    peer.source.configured()
200}
201
202/// A peer we dialed that has held its outbound slot for more than one step
203/// without progress for `IDLE_BACKOFF` seconds, or ever. Evicting an inbound
204/// peer would free nothing for our outbound dial.
205fn evictable(peer: &Peer, step: u64, now: u64) -> Result<bool, Error> {
206    let settled = peer
207        .connected_since
208        .lock()
209        .map_err(|_| poisoned())?
210        .is_some_and(|since| step >= since.saturating_add(2));
211    let idle = peer
212        .last_progress()?
213        .is_none_or(|at| now >= at.saturating_add(Scheduler::IDLE_BACKOFF));
214    Ok(settled && idle && peer.source.outbound())
215}
216
217fn inventory_peer(label: String, source: PeerSource) -> Result<Peer, Error> {
218    let peer_id = match label.parse::<PeerId>() {
219        Ok(id) => id,
220        Err(error) => {
221            return Err(Error::Invalid(format!("inventory peer label: {error}")));
222        }
223    };
224    Ok(Peer {
225        label,
226        id: peer_id.to_string(),
227        peer_id,
228        source,
229        range: Mutex::new(None),
230        last_request: Mutex::new(None),
231        last_error: Mutex::new(None),
232        backoff: Mutex::new(Backoff {
233            failures: 0,
234            until: None,
235        }),
236        busy_turns: AtomicU32::new(0),
237        idle_turns: AtomicU32::new(0),
238        last_progress: Mutex::new(None),
239        connected_since: Mutex::new(None),
240    })
241}
242
243impl Scheduler {
244    /// Heights per claimed range, inclusive of both ends.
245    pub const RANGE_HEIGHTS: u64 = 4096;
246    /// Entries requested per inventory page.
247    pub const PAGE_ENTRIES: u32 = 512;
248    /// Package fetches per worker round.
249    pub const ROUND_FETCHES: usize = 16;
250    /// Wall-clock budget of one step, checked between requests.
251    pub const ROUND_TIME: Duration = Duration::from_secs(20);
252    /// Minimum spacing between requests to one peer (its 4/s token bucket).
253    pub const REQUEST_SPACING: Duration = Duration::from_millis(250);
254    /// Sync backoff after a peer error: 30 s, doubling per repeat, capped at a day.
255    pub const BACKOFF_SECONDS: u64 = 30;
256    pub const BACKOFF_CAP: u64 = 86_400;
257    /// Consecutive Busy turns after which a peer is disconnected.
258    pub const BUSY_TURNS: u32 = 10;
259    /// Consecutive Missing answers for advertised records that blame a peer.
260    pub const MISSING_ANSWERS: u32 = 2;
261    /// Consecutive answered turns without progress after which a peer's slot
262    /// is released for `IDLE_BACKOFF` seconds, unblamed. The same span guards
263    /// eviction: only a peer without progress for that long can be evicted,
264    /// and an evicted peer is held off for it too.
265    pub const IDLE_TURNS: u32 = 3;
266    pub const IDLE_BACKOFF: u64 = 60;
267    /// Longest wait for an evicted connection to close before the retry.
268    pub const EVICT_WAIT: Duration = Duration::from_secs(2);
269    const MAX_PEERS: usize = 1024;
270
271    /// Status lists the configured peers before the first step.
272    pub(crate) fn new(sync: PeerSync) -> Result<Self, Error> {
273        let scheduler = Self {
274            offered: sync.peers,
275            peers: Mutex::new(Vec::new()),
276            turn: AtomicUsize::new(0),
277            configured_turn: AtomicBool::new(false),
278            configured_next: AtomicUsize::new(0),
279            info: sync.info,
280            admitted: AtomicU64::new(0),
281            rejected: AtomicU64::new(0),
282            penalized: Mutex::new(Vec::new()),
283            steps: AtomicU64::new(0),
284        };
285        scheduler.refresh(scheduler.offered.sources(&[]), unix_seconds()?)?;
286        Ok(scheduler)
287    }
288
289    /// One bounded step with the next non-omitting, non-backed-off peer in
290    /// rotation. Busy, deadline and cancellation keep the range; a step
291    /// interrupted by `stop` records nothing about the peer.
292    pub(crate) fn step(&self, engine: &Engine, stop: &AtomicBool) -> Result<(), Error> {
293        let omitting = engine.omitting_peers()?;
294        let now = unix_seconds()?;
295        self.refresh(self.offered.sources(&omitting), now)?;
296        let step = self.steps.fetch_add(1, Ordering::Relaxed);
297        self.observe_connections(step)?;
298        let Some(peer) = self.next_peer(&omitting, now)? else {
299            return Ok(());
300        };
301        let deadline = Instant::now() + Self::ROUND_TIME;
302        let denied = self.turn(&peer, engine, stop, deadline, now)?;
303        if denied && self.free_slot_for(&peer, step, stop, deadline, now)? {
304            self.turn(&peer, engine, stop, deadline, now)?;
305        }
306        Ok(())
307    }
308
309    /// One turn with `peer`. Returns whether our established-connection
310    /// limit refused its dial, the only refusal an eviction can cure.
311    fn turn(
312        &self,
313        peer: &Peer,
314        engine: &Engine,
315        stop: &AtomicBool,
316        deadline: Instant,
317        now: u64,
318    ) -> Result<bool, Error> {
319        let mut round = Round {
320            stop,
321            deadline,
322            fetched: 0,
323            requested_page: false,
324            contacted: false,
325            progressed: false,
326        };
327        let outcome = self.advance(peer, engine, &mut round);
328        if stop.load(Ordering::Acquire) {
329            return Ok(false);
330        }
331        self.track_progress(peer, &outcome, &round, now)?;
332        let denied = matches!(
333            &outcome,
334            Ok(Turn::Retry(Error::DialDenied(DialLimit::Established)))
335        );
336        let result = self.judge(peer, outcome, now)?;
337        *peer.last_error.lock().map_err(|_| poisoned())? = RecordedError::from_result(&result);
338        Ok(denied)
339    }
340
341    /// Note at which step each inventory peer was first seen connected.
342    fn observe_connections(&self, step: u64) -> Result<(), Error> {
343        for peer in self.snapshot()? {
344            let mut since = peer.connected_since.lock().map_err(|_| poisoned())?;
345            *since = match (peer.source.connected(), *since) {
346                (true, Some(first)) => Some(first),
347                (true, None) => Some(step),
348                (false, _gone) => None,
349            };
350        }
351        Ok(())
352    }
353
354    /// Peers sync is currently skipping, for discovery not to redial them.
355    pub(crate) fn backed_off(&self, now: u64) -> Result<Vec<String>, Error> {
356        let mut skipped = Vec::new();
357        for peer in self.snapshot()? {
358            if peer.backoff_until(now)?.is_some() {
359                skipped.push(peer.id.clone());
360            }
361        }
362        Ok(skipped)
363    }
364
365    /// An admission, coverage or an already-held listed record is progress
366    /// and resets the idle streak. `IDLE_TURNS` quiet turns in a row that
367    /// asked the peer and got nothing usable release its slot for
368    /// `IDLE_BACKOFF`: no blame, no book penalty, failures untouched.
369    fn track_progress(
370        &self,
371        peer: &Peer,
372        outcome: &Result<Turn, Error>,
373        round: &Round,
374        now: u64,
375    ) -> Result<(), Error> {
376        if round.progressed {
377            peer.idle_turns.store(0, Ordering::Relaxed);
378            *peer.last_progress.lock().map_err(|_| poisoned())? = Some(now);
379            return Ok(());
380        }
381        if !round.contacted || !matches!(outcome, Ok(Turn::Quiet)) {
382            return Ok(());
383        }
384        let idle = peer.idle_turns.fetch_add(1, Ordering::Relaxed) + 1;
385        if idle < Self::IDLE_TURNS {
386            return Ok(());
387        }
388        peer.idle_turns.store(0, Ordering::Relaxed);
389        peer.source.disconnect();
390        peer.hold_off(now, Self::IDLE_BACKOFF)
391    }
392
393    /// Our established-connection limit refused `starved`: evict the
394    /// connected inventory peer that progressed longest ago, but only one
395    /// without progress for `IDLE_BACKOFF` (or ever) that has stayed connected
396    /// for more than one step. The victim is held off for `IDLE_BACKOFF`, in
397    /// sync and in discovery, so nothing redials it into the freed slot.
398    /// Returns whether its connection closed in time for `starved` to retry
399    /// in this step. One eviction per step, and no second one for `starved`
400    /// before that retry; the scheduler runs one request at a time, so no
401    /// other inventory peer is mid-request.
402    fn free_slot_for(
403        &self,
404        starved: &Peer,
405        step: u64,
406        stop: &AtomicBool,
407        deadline: Instant,
408        now: u64,
409    ) -> Result<bool, Error> {
410        let mut oldest: Option<(Arc<Peer>, Option<u64>)> = None;
411        for peer in self.snapshot()? {
412            if peer.id == starved.id || !evictable(&peer, step, now)? {
413                continue;
414            }
415            let progress = peer.last_progress()?;
416            if oldest.as_ref().is_none_or(|(_, best)| progress < *best) {
417                oldest = Some((peer, progress));
418            }
419        }
420        let Some((victim, _progress)) = oldest else {
421            return Ok(false);
422        };
423        victim.source.disconnect();
424        victim.hold_off(now, Self::IDLE_BACKOFF)?;
425        Ok(victim.wait_closed(stop, deadline))
426    }
427
428    /// A blamed turn disconnects the peer and backs it off; remote Busy turns
429    /// build a streak that disconnects the peer at `BUSY_TURNS`; a quiet turn
430    /// resets both. Retries and our own failures say nothing about the peer.
431    fn judge(
432        &self,
433        peer: &Peer,
434        outcome: Result<Turn, Error>,
435        now: u64,
436    ) -> Result<Result<(), Error>, Error> {
437        match outcome {
438            Ok(Turn::Quiet) => {
439                peer.busy_turns.store(0, Ordering::Relaxed);
440                peer.backoff.lock().map_err(|_| poisoned())?.failures = 0;
441                Ok(Ok(()))
442            }
443            Ok(Turn::Busy(error)) => {
444                let streak = peer.busy_turns.fetch_add(1, Ordering::Relaxed) + 1;
445                if streak < Self::BUSY_TURNS {
446                    return Ok(Err(error));
447                }
448                peer.busy_turns.store(0, Ordering::Relaxed);
449                peer.source.disconnect();
450                Ok(Err(Error::Busy(format!(
451                    "{error}; busy for {streak} turns, disconnected"
452                ))))
453            }
454            Ok(Turn::Blamed(error)) => {
455                peer.busy_turns.store(0, Ordering::Relaxed);
456                self.penalize(peer, now)?;
457                Ok(Err(error))
458            }
459            Ok(Turn::Retry(error)) | Err(error) => Ok(Err(error)),
460        }
461    }
462
463    fn penalize(&self, peer: &Peer, now: u64) -> Result<(), Error> {
464        peer.source.disconnect();
465        let mut backoff = peer.backoff.lock().map_err(|_| poisoned())?;
466        backoff.failures = backoff.failures.checked_add(1).unwrap_or(u32::MAX);
467        let delay = 2u64
468            .checked_pow(backoff.failures - 1)
469            .and_then(|factor| factor.checked_mul(Self::BACKOFF_SECONDS))
470            .map_or(Self::BACKOFF_CAP, |delay| delay.min(Self::BACKOFF_CAP));
471        backoff.until = Some(now.checked_add(delay).unwrap_or(u64::MAX));
472        drop(backoff);
473        let mut penalized = self.penalized.lock().map_err(|_| poisoned())?;
474        if !penalized.contains(&peer.peer_id) {
475            penalized.push(peer.peer_id);
476        }
477        Ok(())
478    }
479
480    /// Peers backed off since the last call, for the peer book to record.
481    pub(crate) fn drain_penalized(&self) -> Result<Vec<PeerId>, Error> {
482        let mut penalized = self.penalized.lock().map_err(|_| poisoned())?;
483        Ok(std::mem::take(&mut *penalized))
484    }
485
486    /// Reconcile with the offered peers: known identities keep their peer
487    /// state and in-flight range, new ones join. One no longer offered stays
488    /// while backed off, so a hold-off survives the disconnect it caused and
489    /// discovery keeps avoiding it; otherwise it is dropped.
490    fn refresh(&self, offered: Vec<(String, PeerSource)>, now: u64) -> Result<(), Error> {
491        if offered.len() > Self::MAX_PEERS {
492            return Err(Error::Invalid("too many inventory peers".into()));
493        }
494        let mut peers = self.peers.lock().map_err(|_| poisoned())?;
495        let mut next: Vec<Arc<Peer>> = Vec::with_capacity(offered.len());
496        for (label, source) in offered {
497            let candidate = inventory_peer(label, source)?;
498            match peers.iter().find(|known| known.id == candidate.id) {
499                Some(known) => next.push(Arc::clone(known)),
500                None => next.push(Arc::new(candidate)),
501            }
502        }
503        let mut held = Vec::new();
504        for known in peers.iter() {
505            if !next.iter().any(|kept| kept.id == known.id) && known.backoff_until(now)?.is_some() {
506                held.push(Arc::clone(known));
507            }
508        }
509        // Rotating inbound identities must not pile up: the oldest go first.
510        let room = Self::MAX_PEERS.saturating_sub(next.len());
511        next.extend(held.split_off(held.len().saturating_sub(room)));
512        *peers = next;
513        Ok(())
514    }
515
516    fn snapshot(&self) -> Result<Vec<Arc<Peer>>, Error> {
517        Ok(self.peers.lock().map_err(|_| poisoned())?.clone())
518    }
519
520    /// Configured and live-only peers alternate turns, each group rotating on
521    /// its own; a group with nobody ready passes its turn to the other.
522    fn next_peer(&self, omitting: &[String], now: u64) -> Result<Option<Arc<Peer>>, Error> {
523        let peers = self.snapshot()?;
524        let (configured, live): (Vec<_>, Vec<_>) = peers.into_iter().partition(configured);
525        let mut groups = [(&configured, &self.configured_next), (&live, &self.turn)];
526        if self.configured_turn.fetch_xor(true, Ordering::Relaxed) {
527            groups.reverse();
528        }
529        for (peers, turn) in groups {
530            if let Some(peer) = rotate(peers, turn, omitting, now)? {
531                return Ok(Some(peer));
532            }
533        }
534        Ok(None)
535    }
536
537    fn advance(&self, peer: &Peer, engine: &Engine, round: &mut Round) -> Result<Turn, Error> {
538        let mut slot = peer.range.lock().map_err(|_| poisoned())?;
539        if slot.is_none() {
540            *slot = open(&peer.id, engine)?;
541            // Nothing left to ask a fully synced peer: as fresh as progress,
542            // so surplus synced peers are not evicted every IDLE_BACKOFF.
543            round.progressed |= slot.is_none();
544        }
545        // A peer error or our own failure abandons the range; it is requested
546        // again from the persisted cursor on this peer's next turn.
547        match self.walk(peer, engine, &mut slot, round) {
548            Ok(()) | Err(Pause::Done) => Ok(Turn::Quiet),
549            Err(Pause::Abandon) | Err(Pause::Error(Error::Stale)) => {
550                *slot = None;
551                Ok(Turn::Quiet)
552            }
553            Err(Pause::Error(Error::PeerBusy)) => Ok(Turn::Busy(Error::PeerBusy)),
554            Err(Pause::Error(
555                retry @ (Error::Busy(_) | Error::Transport(_) | Error::DialDenied(_)),
556            )) => Ok(Turn::Retry(retry)),
557            Err(Pause::Blame(error)) => {
558                *slot = None;
559                Ok(Turn::Blamed(error))
560            }
561            Err(Pause::Error(error)) => {
562                *slot = None;
563                Err(error)
564            }
565        }
566    }
567
568    fn walk(
569        &self,
570        peer: &Peer,
571        engine: &Engine,
572        slot: &mut Option<Range>,
573        round: &mut Round,
574    ) -> Result<(), Pause> {
575        while let Some(range) = slot.as_mut() {
576            round.open()?;
577            if let Some(&entry) = range.entries.get(range.next) {
578                if !selected(engine, &entry)? {
579                    return Err(Pause::Abandon);
580                }
581                if engine.record(entry.txid)?.is_none() {
582                    if round.fetched == Self::ROUND_FETCHES {
583                        return Err(Pause::Done);
584                    }
585                    round.fetched += 1;
586                    pace(peer, round)?;
587                    let missing = &mut range.missing;
588                    round.progressed |=
589                        self.acquire(peer, engine, &entry, missing, round.deadline)?;
590                } else {
591                    round.progressed = true;
592                }
593                range.next += 1;
594            } else if range.more {
595                if round.requested_page {
596                    return Err(Pause::Done);
597                }
598                round.requested_page = true;
599                pace(peer, round)?;
600                let Some(page) = peer
601                    .source
602                    .inventory_until(
603                        (range.from, range.to),
604                        range.after,
605                        Self::PAGE_ENTRIES,
606                        round.stop,
607                        round.deadline,
608                    )
609                    .map_err(blame)?
610                else {
611                    return Err(Pause::Done);
612                };
613                if !accept(range, page).map_err(Pause::Blame)? {
614                    *slot = None;
615                }
616            } else {
617                let through = range.to.min(range.scanned_through);
618                engine.record_peer_coverage(&peer.id, range.from, through, range.generation)?;
619                round.progressed = true;
620                *slot = None;
621            }
622        }
623        Ok(())
624    }
625
626    /// Fetch one advertised record from the advertising peer and admit it. A
627    /// peer that answers Missing leaves the entry listed-only: it neither blocks
628    /// coverage nor is retried within this range. `MISSING_ANSWERS` Missing
629    /// answers in a row within one range blame the peer; an invalid package
630    /// always does. Returns whether a record was admitted.
631    fn acquire(
632        &self,
633        peer: &Peer,
634        engine: &Engine,
635        entry: &Placement,
636        missing: &mut u32,
637        deadline: Instant,
638    ) -> Result<bool, Pause> {
639        let fetched = peer
640            .source
641            .fetch_until(entry.txid, deadline)
642            .map_err(blame)?;
643        let Some(package) = fetched else {
644            *missing += 1;
645            if *missing < Self::MISSING_ANSWERS {
646                return Ok(false);
647            }
648            return Err(Pause::Blame(Error::Invalid(
649                "peer reported advertised records missing".into(),
650            )));
651        };
652        *missing = 0;
653        let receipt = match engine.admit_record(package) {
654            Ok(receipt) => receipt,
655            Err(invalid @ Error::Invalid(_)) => {
656                self.rejected.fetch_add(1, Ordering::Relaxed);
657                return Err(Pause::Blame(invalid));
658            }
659            Err(local) => return Err(Pause::Error(local)),
660        };
661        self.admitted.fetch_add(1, Ordering::Relaxed);
662        if !placed(&receipt, entry) {
663            return Err(Pause::Blame(Error::Invalid(
664                "peer inventory placement disagrees with its proven record".into(),
665            )));
666        }
667        Ok(true)
668    }
669
670    pub(crate) fn status(&self, engine: &Engine) -> Result<OverlayStatus, Error> {
671        let start = engine.status()?.application_start.height;
672        let now = unix_seconds()?;
673        let known = self.snapshot()?;
674        let mut peers = Vec::with_capacity(known.len());
675        for peer in &known {
676            let cursor = engine.peer_cursor(&peer.id)?;
677            let synced_through = cursor.checked_sub(1).filter(|height| *height >= start);
678            let recorded = peer.last_error.lock().map_err(|_| poisoned())?.clone();
679            let (last_error, last_error_at) = RecordedError::split(recorded);
680            let mut status = OverlayPeerStatus::default();
681            status.label = peer.label.clone();
682            status.cursor = cursor;
683            status.synced_through = synced_through;
684            status.last_error = last_error;
685            status.last_error_at = last_error_at;
686            status.backoff_until = peer.backoff_until(now)?;
687            status.last_progress_at = peer.last_progress()?;
688            peers.push(status);
689        }
690        let mut status = OverlayStatus::default();
691        status.peer_id = self.info.peer_id.clone();
692        status.listen = (self.info.listen)()
693            .iter()
694            .map(Multiaddr::to_string)
695            .collect();
696        status.announce = self.info.announce.clone();
697        status.peer_book = self.info.peer_book;
698        status.connected = (self.info.connected)();
699        status.admitted = self.admitted.load(Ordering::Relaxed);
700        status.rejected = self.rejected.load(Ordering::Relaxed);
701        status.peers = peers;
702        Ok(status)
703    }
704}
705
706/// Keep requests to one peer at least `REQUEST_SPACING` apart, waiting in
707/// short slices so cancellation and the round deadline stay responsive. The
708/// round counts as having asked the peer once pacing lets a request through.
709fn pace(peer: &Peer, round: &mut Round) -> Result<(), Pause> {
710    let mut last = peer.last_request.lock().map_err(|_| poisoned())?;
711    if let Some(previous) = *last {
712        let ready = previous + Scheduler::REQUEST_SPACING;
713        while Instant::now() < ready {
714            round.open()?;
715            let wait = ready.saturating_duration_since(Instant::now());
716            std::thread::sleep(wait.min(Duration::from_millis(50)));
717        }
718    }
719    round.open()?;
720    *last = Some(Instant::now());
721    round.contacted = true;
722    Ok(())
723}