1use std::{
5 convert::TryFrom,
6 num::{NonZeroU64, NonZeroUsize},
7 sync::{
8 Arc, LazyLock,
9 atomic::{AtomicU64, Ordering},
10 },
11 time::{Duration, Instant},
12};
13
14use crate::{
15 blocks::{FullTipset, Tipset, TipsetKey, TipsetLike},
16 db::DbImpl,
17 libp2p::{
18 NetworkMessage, PeerId, PeerManager,
19 chain_exchange::{
20 ChainExchangeRequest, ChainExchangeResponse, HEADERS, MESSAGES, TipsetBundle,
21 },
22 hello::{HelloRequest, HelloResponse},
23 rpc::RequestResponseError,
24 },
25 prelude::*,
26 utils::{
27 misc::{AdaptiveValueProvider, ExponentialAdaptiveValueProvider},
28 stats::Stats,
29 },
30};
31use anyhow::Context as _;
32use nonzero_ext::nonzero;
33use parking_lot::Mutex;
34use std::future::Future;
35use tokio::sync::Semaphore;
36use tokio::task::JoinSet;
37use tracing::{debug, trace};
38
39static CHAIN_EXCHANGE_TIMEOUT_MILLIS: LazyLock<ExponentialAdaptiveValueProvider<u64>> =
43 LazyLock::new(|| ExponentialAdaptiveValueProvider::new(5000, 2000, 60000, false));
44
45static MAX_CONCURRENT_CHAIN_EXCHANGE_REQUESTS: LazyLock<NonZeroUsize> = LazyLock::new(|| {
48 std::env::var("FOREST_MAX_CONCURRENT_CHAIN_EXCHANGE_REQUESTS")
49 .ok()
50 .and_then(|i| {
51 i.parse().ok().inspect(|i| {
52 tracing::info!("max concurrent chain exchange requests set to {i} from `FOREST_MAX_CONCURRENT_CHAIN_EXCHANGE_REQUESTS`");
53 })
54 }).unwrap_or(nonzero!(3_usize))
55});
56
57#[derive(derive_more::Constructor)]
61pub struct SyncNetworkContext {
62 network_send: flume::Sender<NetworkMessage>,
64 peer_manager: Arc<PeerManager>,
67 db: DbImpl,
68}
69
70impl ShallowClone for SyncNetworkContext {
71 fn shallow_clone(&self) -> Self {
72 Self {
73 network_send: self.network_send.clone(),
74 peer_manager: self.peer_manager.shallow_clone(),
75 db: self.db.shallow_clone(),
76 }
77 }
78}
79
80struct RaceBatch<T> {
83 tasks: JoinSet<anyhow::Result<T>>,
84 semaphore: Arc<Semaphore>,
85}
86
87impl<T> RaceBatch<T>
88where
89 T: Send + 'static,
90{
91 pub fn new(max_concurrent_jobs: NonZeroUsize) -> Self {
92 RaceBatch {
93 tasks: JoinSet::new(),
94 semaphore: Arc::new(Semaphore::new(max_concurrent_jobs.get())),
95 }
96 }
97
98 pub fn add(&mut self, future: impl Future<Output = anyhow::Result<T>> + Send + 'static) {
99 let sem = self.semaphore.clone();
100 self.tasks.spawn(async move {
101 let permit = sem
102 .acquire_owned()
103 .await
104 .context("Semaphore unexpectedly closed")?;
105 let result = future.await;
106 drop(permit);
107 result
108 });
109 }
110
111 pub async fn get_ok_validated<F>(mut self, validate: F) -> Option<T>
113 where
114 F: Fn(&T) -> bool,
115 {
116 while let Some(result) = self.tasks.join_next().await {
117 if let Ok(Ok(value)) = result
118 && validate(&value)
119 {
120 return Some(value);
121 }
122 }
123 None
125 }
126}
127
128impl SyncNetworkContext {
129 pub fn peer_manager(&self) -> &PeerManager {
131 self.peer_manager.as_ref()
132 }
133
134 pub fn network_send(&self) -> &flume::Sender<NetworkMessage> {
136 &self.network_send
137 }
138
139 pub async fn chain_exchange_headers(
143 &self,
144 peer_id: Option<PeerId>,
145 tsk: &TipsetKey,
146 count: NonZeroU64,
147 ) -> anyhow::Result<Vec<Tipset>> {
148 self.handle_chain_exchange_request(peer_id, tsk, count, HEADERS, |tipsets| {
149 validate_network_tipsets(tipsets, tsk)
150 })
151 .await
152 }
153
154 pub async fn chain_exchange_messages(
157 &self,
158 peer_id: Option<PeerId>,
159 ts: &Tipset,
160 ) -> anyhow::Result<FullTipset> {
161 let mut bundles: Vec<TipsetBundle> = self
162 .handle_chain_exchange_request(peer_id, ts.key(), nonzero!(1_u64), MESSAGES, |_| true)
163 .await?;
164
165 if bundles.len() != 1 {
166 anyhow::bail!(
167 "chain exchange request returned {} tipsets, 1 expected.",
168 bundles.len()
169 );
170 }
171 let mut bundle = bundles.remove(0);
172 bundle.blocks = ts.block_headers().to_vec();
173 bundle.try_into()
174 }
175
176 pub async fn chain_exchange_full_tipset(
180 &self,
181 peer_id: Option<PeerId>,
182 tsk: &TipsetKey,
183 ) -> anyhow::Result<FullTipset> {
184 let mut fts = self
185 .handle_chain_exchange_request(
186 peer_id,
187 tsk,
188 nonzero!(1_u64),
189 HEADERS | MESSAGES,
190 |tipsets| validate_network_tipsets(tipsets, tsk),
191 )
192 .await?;
193
194 anyhow::ensure!(
195 fts.len() == 1,
196 "Full tipset request returned {} tipsets, 1 expected.",
197 fts.len()
198 );
199
200 Ok(fts.remove(0))
201 }
202
203 pub async fn chain_exchange_full_tipsets(
204 &self,
205 peer_id: Option<PeerId>,
206 tsk: &TipsetKey,
207 ) -> anyhow::Result<Vec<FullTipset>> {
208 self.handle_chain_exchange_request(
209 peer_id,
210 tsk,
211 nonzero!(16_u64),
212 HEADERS | MESSAGES,
213 |_| true,
214 )
215 .await
216 }
217
218 pub async fn handle_chain_exchange_request<T, F>(
221 &self,
222 peer_id: Option<PeerId>,
223 tsk: &TipsetKey,
224 request_len: NonZeroU64,
225 options: u64,
226 validate: F,
227 ) -> anyhow::Result<Vec<T>>
228 where
229 T: TryFrom<TipsetBundle> + Send + Sync + 'static,
230 <T as TryFrom<TipsetBundle>>::Error: Into<anyhow::Error>,
231 F: Fn(&Vec<T>) -> bool,
232 {
233 let request = ChainExchangeRequest {
234 start: tsk.to_cids(),
235 request_len: request_len.get(),
236 options,
237 };
238
239 let global_pre_time = Instant::now();
240 let network_failures = Arc::new(AtomicU64::new(0));
241 let lookup_failures = Arc::new(AtomicU64::new(0));
242 let chain_exchange_result = match peer_id {
243 Some(id) => Self::chain_exchange_request(
245 self.peer_manager.clone(),
246 self.network_send.clone(),
247 id,
248 request,
249 )
250 .await?
251 .into_result()?,
252 None => {
253 let peers = self.peer_manager.top_peers_shuffled();
256 anyhow::ensure!(
257 !peers.is_empty(),
258 "chain exchange failed: no peers are available"
259 );
260
261 let n_peers = peers.len();
262 let mut batch = RaceBatch::new(*MAX_CONCURRENT_CHAIN_EXCHANGE_REQUESTS);
263 let success_time_cost_millis_stats = Arc::new(Mutex::new(Stats::new()));
264 for peer_id in peers.into_iter() {
265 let peer_manager = self.peer_manager.clone();
266 let network_send = self.network_send.clone();
267 let request = request.clone();
268 let network_failures = network_failures.clone();
269 let lookup_failures = lookup_failures.clone();
270 let success_time_cost_millis_stats = success_time_cost_millis_stats.clone();
271 batch.add(async move {
272 let start = Instant::now();
273 match Self::chain_exchange_request(
274 peer_manager,
275 network_send,
276 peer_id,
277 request,
278 )
279 .await
280 {
281 Ok(chain_exchange_result) => {
282 match chain_exchange_result.into_result::<T>() {
283 Ok(r) => {
284 success_time_cost_millis_stats.lock().update(
285 start.elapsed().as_millis()
286 );
287 Ok(r)
288 }
289 Err(error) => {
290 lookup_failures.fetch_add(1, Ordering::Relaxed);
291 debug!(%peer_id, %request_len, %options, %n_peers, %error, "Failed chain_exchange response");
292 Err(error)
293 }
294 }
295 }
296 Err(error) => {
297 network_failures.fetch_add(1, Ordering::Relaxed);
298 debug!(%peer_id, %request_len, %options, %n_peers, %error, "Failed chain_exchange request to peer");
299 Err(error)
300 }
301 }
302 });
303 }
304
305 let make_failure_message = || {
306 CHAIN_EXCHANGE_TIMEOUT_MILLIS.adapt_on_failure();
307 tracing::debug!(
308 "Increased chain exchange timeout to {}ms",
309 CHAIN_EXCHANGE_TIMEOUT_MILLIS.get()
310 );
311 let mut message = String::new();
312 message.push_str("ChainExchange request failed for all top peers. ");
313 message.push_str(&format!(
314 "{} network failures, ",
315 network_failures.load(Ordering::Relaxed)
316 ));
317 message.push_str(&format!(
318 "{} lookup failures, ",
319 lookup_failures.load(Ordering::Relaxed)
320 ));
321 message.push_str(&format!("request:\n{request:?}"));
322 anyhow::anyhow!(message)
323 };
324
325 let v = batch
326 .get_ok_validated(validate)
327 .await
328 .ok_or_else(make_failure_message)?;
329 if let Ok(mean) = success_time_cost_millis_stats.lock().mean()
330 && CHAIN_EXCHANGE_TIMEOUT_MILLIS.adapt_on_success(mean as _)
331 {
332 tracing::debug!(
333 "Decreased chain exchange timeout to {}ms. Current average: {}ms",
334 CHAIN_EXCHANGE_TIMEOUT_MILLIS.get(),
335 mean,
336 );
337 }
338 trace!("Succeed: handle_chain_exchange_request");
339 v
340 }
341 };
342
343 self.peer_manager
345 .log_global_success(Instant::now().duration_since(global_pre_time));
346
347 Ok(chain_exchange_result)
348 }
349
350 async fn chain_exchange_request(
352 peer_manager: Arc<PeerManager>,
353 network_send: flume::Sender<NetworkMessage>,
354 peer_id: PeerId,
355 request: ChainExchangeRequest,
356 ) -> anyhow::Result<ChainExchangeResponse> {
357 trace!("Sending ChainExchange Request to {peer_id}");
358
359 let req_pre_time = Instant::now();
360
361 let (tx, rx) = flume::bounded(1);
362 if network_send
363 .send_async(NetworkMessage::ChainExchangeRequest {
364 peer_id,
365 request,
366 response_channel: tx,
367 })
368 .await
369 .is_err()
370 {
371 anyhow::bail!("Failed to send chain exchange request to network");
372 };
373
374 let res = tokio::time::timeout(
379 Duration::from_millis(CHAIN_EXCHANGE_TIMEOUT_MILLIS.get()),
380 rx.recv_async(),
381 )
382 .await;
383 let res_duration = Instant::now().duration_since(req_pre_time);
384 match res {
385 Ok(Ok(Ok(bs_res))) => {
386 peer_manager.log_success(&peer_id, res_duration);
388 trace!("Succeeded: ChainExchange Request to {peer_id}");
389 Ok(bs_res)
390 }
391 Ok(Ok(Err(e))) => {
392 match e {
394 RequestResponseError::UnsupportedProtocols => {
395 peer_manager
397 .ban_peer_with_default_duration(
398 peer_id,
399 "ChainExchange protocol unsupported",
400 |_| None,
401 )
402 .await;
403 }
404 RequestResponseError::ConnectionClosed | RequestResponseError::DialFailure => {
405 peer_manager.mark_peer_bad(peer_id, format!("chain exchange error {e:?}"));
406 }
407 RequestResponseError::Timeout | RequestResponseError::Io(_) => {
410 peer_manager.log_failure(&peer_id, res_duration);
411 }
412 }
413 debug!("Failed: ChainExchange Request to {peer_id}");
414 anyhow::bail!("Internal libp2p error: {e:?}");
415 }
416 Ok(Err(_)) | Err(_) => {
417 peer_manager.log_failure(&peer_id, res_duration);
420 debug!("Timeout: ChainExchange Request to {peer_id}");
421 anyhow::bail!("Chain exchange request to {peer_id} timed out");
422 }
423 }
424 }
425
426 pub async fn hello_request(
429 &self,
430 peer_id: PeerId,
431 request: HelloRequest,
432 ) -> anyhow::Result<(PeerId, Instant, Option<HelloResponse>)> {
433 trace!("Sending Hello Message to {}", peer_id);
434
435 let (tx, rx) = flume::bounded(1);
437
438 self.network_send
440 .send_async(NetworkMessage::HelloRequest {
441 peer_id,
442 request,
443 response_channel: tx,
444 })
445 .await
446 .context("Failed to send hello request: receiver dropped")?;
447
448 const HELLO_TIMEOUT: Duration = Duration::from_secs(30);
449 let sent = Instant::now();
450 let res = tokio::time::timeout(HELLO_TIMEOUT, rx.recv_async())
451 .await
452 .ok()
453 .and_then(Result::ok);
454 Ok((peer_id, sent, res))
455 }
456}
457
458fn validate_network_tipsets<T: TipsetLike>(tipsets: &[T], start_tipset_key: &TipsetKey) -> bool {
462 if let Some(start) = tipsets.first() {
463 if start.key() != start_tipset_key {
464 tracing::warn!(epoch=%start.epoch(), expected=%start_tipset_key, actual=%start.key(), "start tipset key mismatch");
465 return false;
466 }
467 for (ts, pts) in tipsets.iter().zip(tipsets.iter().skip(1)) {
468 if ts.parents() != pts.key() {
469 tracing::warn!(epoch=%ts.epoch(), expected_parent=%pts.key(), actual_parent=%ts.parents(), "invalid chain");
470 return false;
471 }
472 }
473 true
474 } else {
475 tracing::warn!("invalid empty chain_exchange_headers response");
476 false
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 use std::sync::atomic::{AtomicBool, AtomicUsize};
485
486 impl<T> RaceBatch<T>
487 where
488 T: Send + 'static,
489 {
490 pub async fn get_ok(self) -> Option<T> {
491 self.get_ok_validated(|_| true).await
492 }
493 }
494
495 #[tokio::test]
496 async fn race_batch_ok() {
497 let mut batch = RaceBatch::new(nonzero!(3_usize));
498 batch.add(async move { Ok(1) });
499 batch.add(async move { anyhow::bail!("kaboom") });
500
501 assert_eq!(batch.get_ok().await, Some(1));
502 }
503
504 #[tokio::test]
505 async fn race_batch_ok_faster() {
506 let mut batch = RaceBatch::new(nonzero!(3_usize));
507 batch.add(async move {
508 tokio::time::sleep(Duration::from_secs(100)).await;
509 Ok(1)
510 });
511 batch.add(async move { Ok(2) });
512 batch.add(async move { anyhow::bail!("kaboom") });
513
514 assert_eq!(batch.get_ok().await, Some(2));
515 }
516
517 #[tokio::test]
518 async fn race_batch_none() {
519 let mut batch: RaceBatch<i32> = RaceBatch::new(nonzero!(3_usize));
520 batch.add(async move { anyhow::bail!("kaboom") });
521 batch.add(async move { anyhow::bail!("banana") });
522
523 assert_eq!(batch.get_ok().await, None);
524 }
525
526 #[tokio::test]
527 async fn race_batch_semaphore() {
528 const MAX_JOBS: NonZeroUsize = nonzero!(30_usize);
529 let counter = Arc::new(AtomicUsize::new(0));
530 let exceeded = Arc::new(AtomicBool::new(false));
531
532 let mut batch: RaceBatch<i32> = RaceBatch::new(MAX_JOBS);
533 for _ in 0..10000 {
534 let c = counter.clone();
535 let e = exceeded.clone();
536 batch.add(async move {
537 let prev = c.fetch_add(1, Ordering::Relaxed);
538 if prev >= MAX_JOBS.get() {
539 e.fetch_or(true, Ordering::Relaxed);
540 }
541
542 tokio::task::yield_now().await;
543 c.fetch_sub(1, Ordering::Relaxed);
544
545 anyhow::bail!("banana")
546 });
547 }
548
549 assert_eq!(batch.get_ok().await, None);
550 assert!(!exceeded.load(Ordering::Relaxed));
551 }
552
553 #[tokio::test]
554 async fn race_batch_semaphore_exceeded() {
555 const MAX_JOBS: NonZeroUsize = nonzero!(30_usize);
556 let counter = Arc::new(AtomicUsize::new(0));
557 let exceeded = Arc::new(AtomicBool::new(false));
558
559 let mut batch: RaceBatch<i32> = RaceBatch::new(MAX_JOBS.checked_add(1).unwrap());
561 for _ in 0..10000 {
562 let c = counter.clone();
563 let e = exceeded.clone();
564 batch.add(async move {
565 let prev = c.fetch_add(1, Ordering::Relaxed);
566 if prev >= MAX_JOBS.get() {
567 e.fetch_or(true, Ordering::Relaxed);
568 }
569
570 tokio::task::yield_now().await;
571 c.fetch_sub(1, Ordering::Relaxed);
572
573 anyhow::bail!("banana")
574 });
575 }
576
577 assert_eq!(batch.get_ok().await, None);
578 assert!(exceeded.load(Ordering::Relaxed));
579 }
580
581 #[test]
582 #[allow(unused_variables)]
583 fn validate_network_tipsets_tests() {
584 use crate::blocks::{Chain4U, chain4u};
585
586 let c4u = Chain4U::new();
587 chain4u! {
588 in c4u;
589 t0 @ [genesis_header]
590 -> t1 @ [first_header]
591 -> t2 @ [second_left, second_right]
592 -> t3 @ [third]
593 -> t4 @ [fourth]
594 };
595 assert!(validate_network_tipsets(
596 &[t4.clone(), t3.clone(), t2.clone(), t1.clone(), t0.clone()],
597 t4.key()
598 ));
599 assert!(!validate_network_tipsets(
600 &[t4.clone(), t3.clone(), t2.clone(), t1.clone(), t0.clone()],
601 t3.key()
602 ));
603 assert!(!validate_network_tipsets(
604 &[t4.clone(), t2.clone(), t1.clone(), t0.clone()],
605 t4.key()
606 ));
607 }
608}