1use crate::{
7 Error,
8 engine::Engine,
9 model::{BookEntry, DiscoveryStatus},
10 peer::{PeerNetwork, PeerSource},
11 peer_protocol::GossipPeer,
12 peerbook::PeerBook,
13 recorded::{RecordedError, read, unix_seconds as now},
14};
15use libp2p::{Multiaddr, PeerId};
16use rand::seq::SliceRandom;
17use std::{
18 path::PathBuf,
19 sync::{
20 Arc, Mutex, MutexGuard,
21 atomic::{AtomicBool, AtomicU64, Ordering},
22 },
23 time::{Duration, Instant},
24};
25
26pub struct Discovery {
27 network: Arc<PeerNetwork>,
28 book: Arc<Mutex<PeerBook>>,
29 path: PathBuf,
30 round: AtomicU64,
31 seed_error: Mutex<Option<RecordedError>>,
32 gossip_error: Mutex<Option<RecordedError>>,
33 sync_backoff: Mutex<Vec<String>>,
34}
35
36struct Budget<'a> {
38 left: usize,
39 stop: &'a AtomicBool,
40}
41
42impl Budget<'_> {
43 fn take(&mut self) -> bool {
44 if self.left == 0 || self.stop.load(Ordering::Acquire) {
45 return false;
46 }
47 self.left -= 1;
48 true
49 }
50}
51
52fn poisoned() -> Error {
53 Error::Invalid("peer discovery state poisoned".into())
54}
55
56impl Discovery {
57 pub const TARGET: usize = 8;
59 pub const DIALS_PER_ROUND: usize = 2;
60 pub const OPERATIONS_PER_ROUND: usize = 3;
61 pub const GOSSIP_EVERY: u64 = 30;
63 pub const GOSSIP_LIMIT: u32 = 32;
64 pub const SEED_EVERY: u64 = 600;
66 pub const BOOTSTRAP_REACHED: u64 = 4;
68 pub const OPERATION_TIME: Duration = Duration::from_secs(10);
70
71 pub fn new(network: Arc<PeerNetwork>, book: Arc<Mutex<PeerBook>>, path: PathBuf) -> Self {
73 Self {
74 network,
75 book,
76 path,
77 round: AtomicU64::new(0),
78 seed_error: Mutex::new(None),
79 gossip_error: Mutex::new(None),
80 sync_backoff: Mutex::new(Vec::new()),
81 }
82 }
83
84 pub fn hold_off(&self, peers: Vec<String>) -> Result<(), Error> {
87 *self.sync_backoff.lock().map_err(|_| poisoned())? = peers;
88 Ok(())
89 }
90
91 fn lock(&self) -> Result<MutexGuard<'_, PeerBook>, Error> {
92 self.book.lock().map_err(|_| poisoned())
93 }
94
95 pub fn sync_sources(&self, omitting: &[String]) -> Result<Vec<(String, PeerSource)>, Error> {
97 let addresses = self.lock()?.sync_addresses(omitting);
98 Ok(addresses
99 .into_iter()
100 .map(|address| (address.peer_id().to_string(), self.network.source(address)))
101 .collect())
102 }
103
104 pub fn step(&self, engine: &Engine, stop: &AtomicBool) -> Result<(), Error> {
108 let round = self.round.fetch_add(1, Ordering::Relaxed);
109 let omitting = engine.omitting_peers()?;
110 let mut changed = self.merge_identified(&omitting)?;
111 self.publish(&omitting)?;
112 let mut budget = Budget {
113 left: Self::OPERATIONS_PER_ROUND,
114 stop,
115 };
116 let mut avoid = omitting.clone();
117 avoid.extend(
118 self.sync_backoff
119 .lock()
120 .map_err(|_| poisoned())?
121 .iter()
122 .cloned(),
123 );
124 if self.seeds_due(round)? {
125 changed |= self.dial_seeds(&avoid, &mut budget, round == 0)?;
126 }
127 changed |= self.dial_candidates(&avoid, &mut budget)?;
128 if round % Self::GOSSIP_EVERY == Self::GOSSIP_EVERY - 1 {
129 changed |= self.ask_for_peers(&omitting, &mut budget)?;
130 }
131 if changed {
132 self.lock()?.save(&self.path)?;
133 }
134 Ok(())
135 }
136
137 fn merge_identified(&self, omitting: &[String]) -> Result<bool, Error> {
140 let at = now()?;
141 let peers: Vec<GossipPeer> = self
142 .network
143 .drain_identified()
144 .into_iter()
145 .map(|(peer_id, addrs)| GossipPeer {
146 peer_id,
147 addrs,
148 last_seen: at,
149 })
150 .collect();
151 if peers.is_empty() {
152 return Ok(false);
153 }
154 let local = self.network.peer_id();
155 Ok(self.lock()?.merge_identified(&peers, local, omitting) > 0)
156 }
157
158 fn publish(&self, omitting: &[String]) -> Result<(), Error> {
160 let limit = usize::try_from(Self::GOSSIP_LIMIT)
161 .map_err(|error| Error::Invalid(format!("gossip limit: {error}")))?;
162 let at = now()?;
163 let mut peers = Vec::new();
164 for mut peer in self.lock()?.gossip(limit, omitting) {
165 peer.last_seen = peer.last_seen.min(at);
166 peers.push(peer);
167 }
168 self.network.set_gossip(peers);
169 Ok(())
170 }
171
172 fn seeds_due(&self, round: u64) -> Result<bool, Error> {
174 let connected = self.network.connected_peer_ids().len();
175 if connected >= Self::TARGET {
176 return Ok(false);
177 }
178 if connected == 0 {
179 return Ok(true);
180 }
181 let reached_ever = self.lock()?.stats().reached_ever;
182 Ok((round == 0 && reached_ever < Self::BOOTSTRAP_REACHED)
183 || (round > 0 && round % Self::SEED_EVERY == 0))
184 }
185
186 fn dial(&self, address: &Multiaddr, stop: &AtomicBool) -> Result<Result<PeerId, Error>, Error> {
188 let deadline = Instant::now() + Self::OPERATION_TIME;
189 let outcome = self.network.dial(address.clone(), stop, deadline);
190 if stop.load(Ordering::Acquire) {
191 return Ok(outcome);
192 }
193 let at = now()?;
194 let mut book = self.lock()?;
195 match &outcome {
196 Ok(peer) => book.succeeded(address, *peer, self.network.peer_id(), at),
197 Err(Error::DialDenied(_) | Error::Busy(_) | Error::PeerBusy) => (),
199 Err(_failure) => book.failed(address, at),
200 }
201 Ok(outcome)
202 }
203
204 fn dial_seeds(
206 &self,
207 omitting: &[String],
208 budget: &mut Budget,
209 startup: bool,
210 ) -> Result<bool, Error> {
211 let backoff_at = if startup { None } else { Some(now()?) };
212 let seeds = self.lock()?.seeds(omitting, backoff_at);
213 let mut dialed = false;
214 let mut error = None;
215 let mut reached = false;
216 for seed in seeds {
217 if !budget.take() {
218 break;
219 }
220 dialed = true;
221 match self.dial(&seed, budget.stop)? {
222 Ok(_peer) => reached = true,
223 Err(failure) => error = Some(format!("{seed}: {failure}")),
224 }
225 }
226 if dialed {
227 let recorded = if reached {
228 None
229 } else {
230 error.map(RecordedError::now)
231 };
232 *self.seed_error.lock().map_err(|_| poisoned())? = recorded;
233 }
234 Ok(dialed)
235 }
236
237 fn dial_candidates(&self, omitting: &[String], budget: &mut Budget) -> Result<bool, Error> {
238 let connected = self.network.connected_peer_ids();
239 if connected.len() >= Self::TARGET {
240 return Ok(false);
241 }
242 let candidates =
243 self.lock()?
244 .candidates(now()?, &connected, omitting, Self::DIALS_PER_ROUND);
245 let mut dialed = false;
246 for candidate in candidates {
247 if !budget.take() {
248 break;
249 }
250 dialed = true;
251 match self.dial(&candidate, budget.stop)? {
253 Ok(_reached) => (),
254 Err(_backed_off) => (),
255 }
256 }
257 Ok(dialed)
258 }
259
260 fn ask_for_peers(&self, omitting: &[String], budget: &mut Budget) -> Result<bool, Error> {
264 let connected: Vec<PeerId> = self
265 .network
266 .connected_peer_ids()
267 .into_iter()
268 .filter(|peer| !omitting.contains(&peer.to_string()))
269 .collect();
270 let Some(peer) = connected.choose(&mut rand::thread_rng()).copied() else {
271 return Ok(false);
272 };
273 if !budget.take() {
274 return Ok(false);
275 }
276 let deadline = Instant::now() + Self::OPERATION_TIME;
277 let outcome = self
278 .network
279 .peers_of(peer, Self::GOSSIP_LIMIT, budget.stop, deadline);
280 let (changed, recorded) = match outcome {
281 Ok(peers) => {
282 let local = self.network.peer_id();
283 (self.lock()?.merge(&peers, local, omitting) > 0, None)
284 }
285 Err(failure) => (
286 false,
287 Some(RecordedError::now(format!("{peer}: {failure}"))),
288 ),
289 };
290 *self.gossip_error.lock().map_err(|_| poisoned())? = recorded;
291 Ok(changed)
292 }
293
294 pub fn status(&self) -> Result<DiscoveryStatus, Error> {
295 let stats = self.lock()?.stats();
296 let (last_seed_round_error, last_seed_round_error_at) =
297 RecordedError::split(read(&self.seed_error, "seed round")?);
298 let (last_gossip_error, last_gossip_error_at) =
299 RecordedError::split(read(&self.gossip_error, "gossip")?);
300 Ok(DiscoveryStatus {
301 seeds: stats.seeds,
302 book: stats.book,
303 reached: stats.reached,
304 reached_ever: stats.reached_ever,
305 gossip: stats.gossip,
306 last_seed_round_error,
307 last_seed_round_error_at,
308 last_gossip_error,
309 last_gossip_error_at,
310 })
311 }
312
313 pub fn penalize(&self, peers: &[PeerId]) -> Result<(), Error> {
316 if peers.is_empty() {
317 return Ok(());
318 }
319 let at = now()?;
320 let mut book = self.lock()?;
321 let mut changed = false;
322 for peer in peers {
323 changed |= book.failed_peer(*peer, at);
324 }
325 if changed {
326 book.save(&self.path)?;
327 }
328 Ok(())
329 }
330
331 pub fn book(&self) -> Result<Vec<BookEntry>, Error> {
332 Ok(self.lock()?.listing())
333 }
334}