Skip to main content

forest/chain_sync/
network_context.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use 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
39/// Timeout milliseconds for response from an RPC request
40// This value is automatically adapted in the range of [5, 60] for different network conditions,
41// being decreased on success and increased on failure
42static CHAIN_EXCHANGE_TIMEOUT_MILLIS: LazyLock<ExponentialAdaptiveValueProvider<u64>> =
43    LazyLock::new(|| ExponentialAdaptiveValueProvider::new(5000, 2000, 60000, false));
44
45/// Maximum number of concurrent chain exchange request being sent to the
46/// network.
47static 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/// Context used in chain sync to handle network requests.
58/// This contains the peer manager, P2P service interface, and [`Blockstore`]
59/// required to make network requests.
60#[derive(derive_more::Constructor)]
61pub struct SyncNetworkContext {
62    /// Channel to send network messages through P2P service
63    network_send: flume::Sender<NetworkMessage>,
64    /// Manages peers to send requests to and updates request stats for the
65    /// respective peers.
66    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
80/// Race tasks to completion while limiting the number of tasks that may execute concurrently.
81/// Once a task finishes without error, the rest of the tasks are canceled.
82struct 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    /// Return first finishing `Ok` future that passes validation else return `None` if all jobs failed
112    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        // So far every task have failed
124        None
125    }
126}
127
128impl SyncNetworkContext {
129    /// Returns a reference to the peer manager of the network context.
130    pub fn peer_manager(&self) -> &PeerManager {
131        self.peer_manager.as_ref()
132    }
133
134    /// Returns a reference to the channel for sending network messages through P2P service.
135    pub fn network_send(&self) -> &flume::Sender<NetworkMessage> {
136        &self.network_send
137    }
138
139    /// Send a `chain_exchange` request for only block headers (ignore
140    /// messages). If `peer_id` is `None`, requests will be sent to a set of
141    /// shuffled peers.
142    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    /// Send a `chain_exchange` request for messages to assemble a full tipset with a local tipset,
155    /// If `peer_id` is `None`, requests will be sent to a set of shuffled peers.
156    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    /// Send a `chain_exchange` request for a single full tipset (includes
177    /// messages) If `peer_id` is `None`, requests will be sent to a set of
178    /// shuffled peers.
179    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    /// Helper function to handle the peer retrieval if no peer supplied as well
219    /// as the logging and updating of the peer info in the `PeerManager`.
220    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            // Specific peer is given to send request, send specifically to that peer.
244            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                // No specific peer set, send requests to a shuffled set of top peers until
254                // a request succeeds.
255                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        // Log success for the global request with the latency from before sending.
344        self.peer_manager
345            .log_global_success(Instant::now().duration_since(global_pre_time));
346
347        Ok(chain_exchange_result)
348    }
349
350    /// Send a `chain_exchange` request to the network and await response.
351    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        // Add timeout to receiving response from p2p service to avoid stalling.
375        // There is also a timeout inside the request-response calls, but this ensures
376        // this. Awaited rather than `spawn_blocking`d so a slow peer doesn't pin a
377        // blocking-pool thread for the whole timeout.
378        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                // Successful response
387                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                // Internal libp2p error, score failure for peer and potentially disconnect
393                match e {
394                    RequestResponseError::UnsupportedProtocols => {
395                        // refactor this into Networkevent if user agent logging is critical here
396                        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                    // Ignore dropping peer on timeout for now. Can't be confident yet that the
408                    // specified timeout is adequate time.
409                    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                // Sender channel internally dropped or timeout, both should log failure which
418                // will negatively score the peer, but not drop yet.
419                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    /// Send a hello request to the network (does not immediately await
427    /// response).
428    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        // Create oneshot channel for receiving response from sent hello.
436        let (tx, rx) = flume::bounded(1);
437
438        // Send request into libp2p service
439        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
458/// Validates network tipsets that are sorted by epoch in descending order with the below checks
459/// 1. The latest(first) tipset has the desired tipset key
460/// 2. The sorted tipsets are chained by their tipset keys
461fn 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        // We add one more job to exceed the limit
560        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}