Skip to main content

consensus_core/
synchronizer.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3use std::{
4    collections::{BTreeMap, BTreeSet},
5    sync::Arc,
6    time::Duration,
7};
8
9use bytes::Bytes;
10use consensus_config::AuthorityIndex;
11use consensus_types::block::{BlockRef, Round, TransactionIndex};
12use futures::{StreamExt as _, stream::FuturesUnordered};
13use itertools::Itertools as _;
14use mysten_common::{ZipDebugEqIteratorExt, debug_fatal};
15use mysten_metrics::{
16    monitored_future,
17    monitored_mpsc::{Receiver, Sender, channel},
18    monitored_scope,
19};
20use parking_lot::{Mutex, RwLock};
21use rand::{prelude::SliceRandom as _, rngs::ThreadRng};
22use sui_macros::fail_point_async;
23use tap::TapFallible;
24use tokio::{
25    sync::{mpsc::error::TrySendError, oneshot},
26    task::JoinSet,
27    time::{Instant, sleep, sleep_until, timeout},
28};
29use tracing::{debug, info, trace, warn};
30
31use crate::{
32    BlockAPI,
33    block::{ExtendedBlock, SignedBlock, VerifiedBlock},
34    block_verifier::BlockVerifier,
35    commit::CommitIndex,
36    commit_vote_monitor::{CommitVoteMonitor, is_commit_lagging},
37    context::Context,
38    dag_state::DagState,
39    error::{ConsensusError, ConsensusResult},
40    network::{ObserverNetworkClient, PeerId, SynchronizerClient, ValidatorNetworkClient},
41    peers_pool::PeersPool,
42    round_tracker::RoundTracker,
43    task::{shutdown_join_set, spawn_blocking},
44};
45use crate::{core_thread::CoreThreadDispatcher, transaction_vote_tracker::TransactionVoteTracker};
46
47/// The number of concurrent fetch blocks requests per authority
48const FETCH_BLOCKS_CONCURRENCY: usize = 5;
49
50/// Timeouts when fetching blocks.
51const FETCH_REQUEST_TIMEOUT: Duration = Duration::from_millis(2_000);
52const FETCH_FROM_PEERS_TIMEOUT: Duration = Duration::from_millis(4_000);
53
54const MAX_AUTHORITIES_TO_FETCH_PER_BLOCK: usize = 2;
55
56// Max number of peers to request missing blocks concurrently in periodic sync.
57const MAX_PERIODIC_SYNC_PEERS: usize = 3;
58
59/// How long commit must be stalled before periodic sync kicks in as fallback.
60const COMMIT_PROGRESS_TIMEOUT: Duration = Duration::from_secs(10);
61
62struct BlocksGuard {
63    map: Arc<InflightBlocksMap>,
64    block_refs: BTreeSet<BlockRef>,
65    peer: PeerId,
66}
67
68impl Drop for BlocksGuard {
69    fn drop(&mut self) {
70        self.map.unlock_blocks(&self.block_refs, self.peer.clone());
71    }
72}
73
74// Keeps a mapping between the missing blocks that have been instructed to be fetched and the peers
75// that are currently fetching them. For a block ref there is a maximum number of peers that can
76// concurrently fetch it. The peer ids that are currently fetching a block are set on the corresponding
77// `BTreeSet` and basically they act as "locks".
78struct InflightBlocksMap {
79    inner: Mutex<BTreeMap<BlockRef, BTreeSet<PeerId>>>,
80}
81
82impl InflightBlocksMap {
83    fn new() -> Arc<Self> {
84        Arc::new(Self {
85            inner: Mutex::new(BTreeMap::new()),
86        })
87    }
88
89    /// Locks the blocks to be fetched for the assigned `peer`. We want to avoid re-fetching the
90    /// missing blocks from too many peers at the same time, thus we limit the concurrency
91    /// per block by attempting to lock per block. If a block is already fetched by the maximum allowed
92    /// number of peers, then the block ref will not be included in the returned set. The method
93    /// returns all the block refs that have been successfully locked and allowed to be fetched.
94    fn lock_blocks(
95        self: &Arc<Self>,
96        missing_block_refs: BTreeSet<BlockRef>,
97        peer: PeerId,
98    ) -> Option<BlocksGuard> {
99        let mut blocks = BTreeSet::new();
100        let mut inner = self.inner.lock();
101
102        for block_ref in missing_block_refs {
103            // check that the number of peers that are already instructed to fetch the block is not
104            // higher than the allowed and the `peer` has not already been instructed to do that.
105            let peers = inner.entry(block_ref).or_default();
106            if peers.len() < MAX_AUTHORITIES_TO_FETCH_PER_BLOCK && peers.get(&peer).is_none() {
107                assert!(peers.insert(peer.clone()));
108                blocks.insert(block_ref);
109            }
110        }
111
112        if blocks.is_empty() {
113            None
114        } else {
115            Some(BlocksGuard {
116                map: self.clone(),
117                block_refs: blocks,
118                peer,
119            })
120        }
121    }
122
123    /// Unlocks the provided block references for the given `peer`. The unlocking is strict, meaning that
124    /// if this method is called for a specific block ref and peer more times than the corresponding lock
125    /// has been called, it will panic.
126    fn unlock_blocks(self: &Arc<Self>, block_refs: &BTreeSet<BlockRef>, peer: PeerId) {
127        // Now mark all the blocks as fetched from the map
128        let mut blocks_to_fetch = self.inner.lock();
129        for block_ref in block_refs {
130            let peers = blocks_to_fetch
131                .get_mut(block_ref)
132                .expect("Should have found a non empty map");
133
134            assert!(peers.remove(&peer), "Peer should be present!");
135
136            // if the last one then just clean up
137            if peers.is_empty() {
138                blocks_to_fetch.remove(block_ref);
139            }
140        }
141    }
142
143    /// Drops the provided `blocks_guard` which will force to unlock the blocks, and lock now again the
144    /// referenced block refs. The swap is best effort and there is no guarantee that the `peer` will
145    /// be able to acquire the new locks.
146    fn swap_locks(
147        self: &Arc<Self>,
148        blocks_guard: BlocksGuard,
149        peer: PeerId,
150    ) -> Option<BlocksGuard> {
151        let block_refs = blocks_guard.block_refs.clone();
152
153        // Explicitly drop the guard
154        drop(blocks_guard);
155
156        // Now create new guard
157        self.lock_blocks(block_refs, peer)
158    }
159
160    #[cfg(test)]
161    fn num_of_locked_blocks(self: &Arc<Self>) -> usize {
162        let inner = self.inner.lock();
163        inner.len()
164    }
165}
166
167enum Command {
168    FetchBlocks {
169        missing_block_refs: BTreeSet<BlockRef>,
170        peer: PeerId,
171        result: oneshot::Sender<Result<(), ConsensusError>>,
172    },
173    FetchOwnLastBlock,
174    KickOffScheduler,
175    Shutdown {
176        result: oneshot::Sender<()>,
177    },
178}
179
180pub(crate) struct SynchronizerHandle {
181    commands_sender: Sender<Command>,
182    tasks: tokio::sync::Mutex<JoinSet<()>>,
183}
184
185impl SynchronizerHandle {
186    /// Explicitly asks from the synchronizer to fetch the blocks - provided the block_refs set - from
187    /// the peer.
188    pub(crate) async fn fetch_blocks(
189        &self,
190        missing_block_refs: BTreeSet<BlockRef>,
191        peer: PeerId,
192    ) -> ConsensusResult<()> {
193        let (sender, receiver) = oneshot::channel();
194        self.commands_sender
195            .send(Command::FetchBlocks {
196                missing_block_refs,
197                peer,
198                result: sender,
199            })
200            .await
201            .map_err(|_err| ConsensusError::Shutdown)?;
202        receiver.await.map_err(|_err| ConsensusError::Shutdown)?
203    }
204
205    pub(crate) async fn stop(&self) {
206        let (shutdown_sender, shutdown_receiver) = oneshot::channel();
207        let _ = self
208            .commands_sender
209            .send(Command::Shutdown {
210                result: shutdown_sender,
211            })
212            .await;
213        let _ = shutdown_receiver.await;
214
215        let mut tasks = self.tasks.lock().await;
216        shutdown_join_set(&mut tasks).await;
217    }
218
219    #[cfg(test)]
220    /// Creates a mock synchronizer handle for testing
221    pub(crate) fn new_for_test() -> Arc<Self> {
222        use tokio::task::JoinSet;
223        let (tx, _rx) = channel("test_synchronizer", 1);
224        Arc::new(Self {
225            commands_sender: tx,
226            tasks: tokio::sync::Mutex::new(JoinSet::new()),
227        })
228    }
229}
230
231/// `Synchronizer` oversees live block synchronization, crucial for node progress. Live synchronization
232/// refers to the process of retrieving missing blocks, particularly those essential for advancing a node
233/// when data from only a few rounds is absent. If a node significantly lags behind the network,
234/// `commit_syncer` handles fetching missing blocks via a more efficient approach. `Synchronizer`
235/// aims for swift catch-up employing two mechanisms:
236///
237/// 1. Explicitly requesting missing blocks from designated authorities via the "block send" path.
238///    This includes attempting to fetch any missing ancestors necessary for processing a received block.
239///    Such requests prioritize the block author, maximizing the chance of prompt retrieval.
240///    A locking mechanism allows concurrent requests for missing blocks from up to two authorities
241///    simultaneously, enhancing the chances of timely retrieval. Notably, if additional missing blocks
242///    arise during block processing, requests to the same authority are deferred to the scheduler.
243///
244/// 2. Periodically requesting missing blocks via a scheduler. This primarily serves to retrieve
245///    missing blocks that were not ancestors of a received block via the "block send" path.
246///    The scheduler operates on either a fixed periodic basis or is triggered immediately
247///    after explicit fetches described in (1), ensuring continued block retrieval if gaps persist.
248///
249/// Additionally to the above, the synchronizer can synchronize and fetch the last own proposed block
250/// from the network peers as best effort approach to recover node from amnesia and avoid making the
251/// node equivocate.
252pub(crate) struct Synchronizer<
253    V: BlockVerifier,
254    D: CoreThreadDispatcher,
255    VC: ValidatorNetworkClient,
256    OC: ObserverNetworkClient,
257> {
258    context: Arc<Context>,
259    commands_receiver: Receiver<Command>,
260    fetch_block_senders: BTreeMap<PeerId, Sender<BlocksGuard>>,
261    core_dispatcher: Arc<D>,
262    commit_vote_monitor: Arc<CommitVoteMonitor>,
263    dag_state: Arc<RwLock<DagState>>,
264    fetch_blocks_scheduler_task: JoinSet<()>,
265    fetch_own_last_block_task: JoinSet<()>,
266    network_client: Arc<SynchronizerClient<VC, OC>>,
267    block_verifier: Arc<V>,
268    transaction_vote_tracker: TransactionVoteTracker,
269    round_tracker: Arc<RwLock<RoundTracker>>,
270    inflight_blocks_map: Arc<InflightBlocksMap>,
271    commands_sender: Sender<Command>,
272    last_changed_commit_index: CommitIndex,
273    last_commit_change_time: Instant,
274    // When commit is not progressing, commit sync fails over to periodic sync for catchup.
275    commit_sync_failover: bool,
276    peers_pool: Arc<PeersPool>,
277}
278
279impl<V, D, VC, OC> Synchronizer<V, D, VC, OC>
280where
281    V: BlockVerifier,
282    D: CoreThreadDispatcher,
283    VC: ValidatorNetworkClient,
284    OC: ObserverNetworkClient,
285{
286    pub(crate) fn start(
287        network_client: Arc<SynchronizerClient<VC, OC>>,
288        context: Arc<Context>,
289        core_dispatcher: Arc<D>,
290        commit_vote_monitor: Arc<CommitVoteMonitor>,
291        block_verifier: Arc<V>,
292        transaction_vote_tracker: TransactionVoteTracker,
293        round_tracker: Arc<RwLock<RoundTracker>>,
294        dag_state: Arc<RwLock<DagState>>,
295        peers_pool: Arc<PeersPool>,
296        sync_last_known_own_block: bool,
297    ) -> Arc<SynchronizerHandle> {
298        let (commands_sender, commands_receiver) =
299            channel("consensus_synchronizer_commands", 1_000);
300        let inflight_blocks_map = InflightBlocksMap::new();
301
302        // Spawn the tasks to fetch the blocks from the others
303        let mut fetch_block_senders = BTreeMap::new();
304        let mut tasks = JoinSet::new();
305
306        // Create fetch tasks for all known peers (validators and observers)
307        // TODO: refactor to update the sender tasks based on the registered/removed pool peers.
308        let known_peers = peers_pool.get_known_peers();
309        for peer in known_peers {
310            let (sender, receiver) =
311                channel("consensus_synchronizer_fetches", FETCH_BLOCKS_CONCURRENCY);
312            let fetch_blocks_from_peer_async = Self::fetch_blocks_from_peer(
313                peer.clone(),
314                network_client.clone(),
315                block_verifier.clone(),
316                transaction_vote_tracker.clone(),
317                commit_vote_monitor.clone(),
318                context.clone(),
319                core_dispatcher.clone(),
320                dag_state.clone(),
321                receiver,
322                commands_sender.clone(),
323                round_tracker.clone(),
324                peers_pool.clone(),
325            );
326            tasks.spawn(monitored_future!(fetch_blocks_from_peer_async));
327            fetch_block_senders.insert(peer, sender);
328        }
329
330        let commands_sender_clone = commands_sender.clone();
331
332        if sync_last_known_own_block {
333            commands_sender
334                .try_send(Command::FetchOwnLastBlock)
335                .expect("Failed to sync our last block");
336        }
337
338        // Spawn the task to listen to the requests & periodic runs
339        tasks.spawn(monitored_future!(async move {
340            let mut s = Self {
341                context,
342                commands_receiver,
343                fetch_block_senders,
344                core_dispatcher,
345                commit_vote_monitor,
346                fetch_blocks_scheduler_task: JoinSet::new(),
347                fetch_own_last_block_task: JoinSet::new(),
348                network_client,
349                block_verifier,
350                transaction_vote_tracker,
351                inflight_blocks_map,
352                commands_sender: commands_sender_clone,
353                dag_state,
354                round_tracker,
355                last_changed_commit_index: 0,
356                last_commit_change_time: Instant::now(),
357                commit_sync_failover: false,
358                peers_pool,
359            };
360            s.run().await;
361        }));
362
363        Arc::new(SynchronizerHandle {
364            commands_sender,
365            tasks: tokio::sync::Mutex::new(tasks),
366        })
367    }
368
369    // The main loop to listen for the submitted commands.
370    async fn run(&mut self) {
371        // We want the synchronizer to run periodically every 200ms to fetch any missing blocks.
372        const PERIODIC_FETCH_INTERVAL: Duration = Duration::from_millis(200);
373        let scheduler_timeout = sleep_until(Instant::now() + PERIODIC_FETCH_INTERVAL);
374
375        tokio::pin!(scheduler_timeout);
376
377        loop {
378            tokio::select! {
379                Some(command) = self.commands_receiver.recv() => {
380                    match command {
381                        Command::FetchBlocks{ missing_block_refs, peer, result } => {
382                            // Check if peer is available. This check also makes sure that we are not trying to fetch from ourselves.
383                            if !self.peers_pool.is_peer_known(&peer) {
384                                result.send(Err(ConsensusError::PeerUnavailable(format!("{:?}", peer)))).ok();
385                                continue;
386                            }
387
388                            // Keep only the max allowed blocks to request. Additional missing blocks
389                            // will be fetched via periodic sync.
390                            // Fetch from the lowest to highest round, to ensure progress.
391                            let missing_block_refs = missing_block_refs
392                                .into_iter()
393                                .take(self.context.parameters.max_blocks_per_sync)
394                                .collect();
395
396                            let blocks_guard = self.inflight_blocks_map.lock_blocks(missing_block_refs, peer.clone());
397                            let Some(blocks_guard) = blocks_guard else {
398                                result.send(Ok(())).ok();
399                                continue;
400                            };
401
402                            // We don't block if the corresponding peer task is saturated - but we rather drop the request. That's ok as the periodic
403                            // synchronization task will handle any still missing blocks in next run.
404                            let r = self
405                                .fetch_block_senders
406                                .get(&peer)
407                                .ok_or(ConsensusError::PeerNotFound(format!("Peer {} not found in fetch_block_senders", peer)))
408                                .and_then(|sender| {
409                                    sender
410                                        .try_send(blocks_guard)
411                                        .map_err(|err| {
412                                            match err {
413                                                TrySendError::Full(_) => {
414                                                    let peer_name = peer.labelname(&self.context);
415                                                    self.context
416                                                        .metrics
417                                                        .node_metrics
418                                                        .synchronizer_skipped_fetch_requests
419                                                        .with_label_values(&[peer_name])
420                                                        .inc();
421                                                    ConsensusError::SynchronizerSaturated(format!("{:?}", peer))
422                                                },
423                                                TrySendError::Closed(_) => ConsensusError::Shutdown
424                                            }
425                                        })
426                                });
427
428                            result.send(r).ok();
429                        }
430                        Command::FetchOwnLastBlock => {
431                            if self.fetch_own_last_block_task.is_empty() {
432                                self.start_fetch_own_last_block_task();
433                            }
434                        }
435                        Command::KickOffScheduler => {
436                            // just reset the scheduler timeout timer to run immediately if not already running.
437                            // If the scheduler is already running then just reduce the remaining time to run.
438                            let timeout = if self.fetch_blocks_scheduler_task.is_empty() {
439                                Instant::now()
440                            } else {
441                                Instant::now() + PERIODIC_FETCH_INTERVAL.checked_div(2).unwrap()
442                            };
443
444                            // only reset if it is earlier than the next deadline
445                            if timeout < scheduler_timeout.deadline() {
446                                scheduler_timeout.as_mut().reset(timeout);
447                            }
448                        }
449                        Command::Shutdown { result } => {
450                            self.shutdown_tasks().await;
451                            self.fetch_block_senders.clear();
452                            let _ = result.send(());
453                            return;
454                        }
455                    }
456                },
457                Some(result) = self.fetch_own_last_block_task.join_next(), if !self.fetch_own_last_block_task.is_empty() => {
458                    match result {
459                        Ok(()) => {},
460                        Err(e) => {
461                            if e.is_cancelled() {
462                            } else if e.is_panic() {
463                                std::panic::resume_unwind(e.into_panic());
464                            } else {
465                                panic!("fetch our last block task failed: {e}");
466                            }
467                        },
468                    };
469                },
470                Some(result) = self.fetch_blocks_scheduler_task.join_next(), if !self.fetch_blocks_scheduler_task.is_empty() => {
471                    match result {
472                        Ok(()) => {},
473                        Err(e) => {
474                            if e.is_cancelled() {
475                            } else if e.is_panic() {
476                                std::panic::resume_unwind(e.into_panic());
477                            } else {
478                                panic!("fetch blocks scheduler task failed: {e}");
479                            }
480                        },
481                    };
482                },
483                () = &mut scheduler_timeout => {
484                    // we want to start a new task only if the previous one has already finished.
485                    // TODO: consider starting backup fetches in parallel, when a fetch takes too long?
486                    if self.fetch_blocks_scheduler_task.is_empty()
487                        && let Err(err) = self.start_fetch_missing_blocks_task().await {
488                            debug!("Core is shutting down, synchronizer is shutting down: {err:?}");
489                            self.shutdown_tasks().await;
490                            return;
491                        };
492
493                    scheduler_timeout
494                        .as_mut()
495                        .reset(Instant::now() + PERIODIC_FETCH_INTERVAL);
496                }
497            }
498        }
499    }
500
501    // Must be called before exiting run(), so no task is dropped without being awaited
502    // and task panics propagate.
503    async fn shutdown_tasks(&mut self) {
504        shutdown_join_set(&mut self.fetch_own_last_block_task).await;
505        shutdown_join_set(&mut self.fetch_blocks_scheduler_task).await;
506    }
507
508    async fn fetch_blocks_from_peer(
509        peer: PeerId,
510        network_client: Arc<SynchronizerClient<VC, OC>>,
511        block_verifier: Arc<V>,
512        transaction_vote_tracker: TransactionVoteTracker,
513        commit_vote_monitor: Arc<CommitVoteMonitor>,
514        context: Arc<Context>,
515        core_dispatcher: Arc<D>,
516        dag_state: Arc<RwLock<DagState>>,
517        mut receiver: Receiver<BlocksGuard>,
518        commands_sender: Sender<Command>,
519        round_tracker: Arc<RwLock<RoundTracker>>,
520        _peers_pool: Arc<PeersPool>,
521    ) {
522        const MAX_RETRIES: u32 = 3;
523        let mut requests = FuturesUnordered::new();
524
525        loop {
526            tokio::select! {
527                Some(blocks_guard) = receiver.recv(), if requests.len() < FETCH_BLOCKS_CONCURRENCY => {
528                    let fetch_after_rounds = Self::get_fetch_after_rounds(&context, dag_state.clone());
529
530                    requests.push(Self::fetch_blocks_request(network_client.clone(), peer.clone(), blocks_guard, fetch_after_rounds, true, FETCH_REQUEST_TIMEOUT, 1))
531                },
532                Some((response, blocks_guard, retries, _peer, fetch_after_rounds)) = requests.next() => {
533                    match response {
534                        Ok(blocks) => {
535                            if let Err(err) = Self::process_fetched_blocks(blocks,
536                                peer.clone(),
537                                blocks_guard,
538                                core_dispatcher.clone(),
539                                block_verifier.clone(),
540                                transaction_vote_tracker.clone(),
541                                commit_vote_monitor.clone(),
542                                context.clone(),
543                                commands_sender.clone(),
544                                round_tracker.clone(),
545                                "live"
546                            ).await {
547                                warn!("Error while processing fetched blocks from peer {}: {err}", peer.hostname(&context));
548                                context.metrics.node_metrics.synchronizer_process_fetched_failures.with_label_values(&[peer.labelname(&context).as_str(), "live"]).inc();
549                            }
550                        },
551                        Err(_) => {
552                            context.metrics.node_metrics.synchronizer_fetch_failures.with_label_values(&[peer.labelname(&context).as_str(), "live"]).inc();
553                            if retries <= MAX_RETRIES {
554                                requests.push(Self::fetch_blocks_request(network_client.clone(), peer.clone(), blocks_guard, fetch_after_rounds, true, FETCH_REQUEST_TIMEOUT, retries))
555                            } else {
556                                warn!("Max retries {retries} reached while trying to fetch blocks from peer {}.", peer.hostname(&context));
557                                // we don't necessarily need to do, but dropping the guard here to unlock the blocks
558                                drop(blocks_guard);
559                            }
560                        }
561                    }
562                },
563                else => {
564                    info!("Fetching blocks from peer {} task will now abort.", peer.hostname(&context));
565                    break;
566                }
567            }
568        }
569    }
570
571    /// Processes the requested raw fetched blocks from peer. If no error is returned then
572    /// the verified blocks are immediately sent to Core for processing.
573    async fn process_fetched_blocks(
574        mut serialized_blocks: Vec<Bytes>,
575        peer: PeerId,
576        requested_blocks_guard: BlocksGuard,
577        core_dispatcher: Arc<D>,
578        block_verifier: Arc<V>,
579        transaction_vote_tracker: TransactionVoteTracker,
580        commit_vote_monitor: Arc<CommitVoteMonitor>,
581        context: Arc<Context>,
582        commands_sender: Sender<Command>,
583        round_tracker: Arc<RwLock<RoundTracker>>,
584        sync_method: &str,
585    ) -> ConsensusResult<()> {
586        if serialized_blocks.is_empty() {
587            return Ok(());
588        }
589
590        // Limit the number of the returned blocks processed.
591        serialized_blocks.truncate(context.parameters.max_blocks_per_sync);
592
593        // Verify all the fetched blocks
594        let (blocks, voted_blocks) = spawn_blocking({
595            let block_verifier = block_verifier.clone();
596            let context = context.clone();
597            let peer = peer.clone();
598            move || Self::verify_blocks(serialized_blocks, block_verifier, &context, peer)
599        })
600        .await??;
601
602        if context.protocol_config.transaction_voting_enabled() {
603            transaction_vote_tracker.add_voted_blocks(voted_blocks);
604        }
605
606        // Record commit votes from the verified blocks.
607        for block in &blocks {
608            commit_vote_monitor.observe_block(block);
609        }
610
611        // Update round tracker from the verified blocks. For fetched blocks,
612        // excluded_ancestors are not available so we use an empty vector.
613        {
614            let mut tracker = round_tracker.write();
615            for block in &blocks {
616                tracker.update_from_verified_block(&ExtendedBlock {
617                    block: block.clone(),
618                    excluded_ancestors: vec![],
619                });
620            }
621        }
622
623        let metrics = &context.metrics.node_metrics;
624        metrics
625            .synchronizer_fetched_blocks_by_peer
626            .with_label_values(&[peer.labelname(&context).as_str(), sync_method])
627            .inc_by(blocks.len() as u64);
628        for block in &blocks {
629            let block_hostname = &context.committee.authority(block.author()).hostname;
630            metrics
631                .synchronizer_fetched_blocks_by_authority
632                .with_label_values(&[block_hostname.as_str(), sync_method])
633                .inc();
634        }
635
636        debug!(
637            "Synced {} missing blocks from peer {:?}: {}",
638            blocks.len(),
639            peer,
640            blocks.iter().map(|b| b.reference().to_string()).join(", "),
641        );
642
643        // Now send them to core for processing. Ignore the returned missing blocks as we don't want
644        // this mechanism to keep feedback looping on fetching more blocks. The periodic synchronization
645        // will take care of that.
646        let missing_blocks = core_dispatcher
647            .add_blocks(blocks)
648            .await
649            .map_err(|_| ConsensusError::Shutdown)?;
650
651        // now release all the locked blocks as they have been fetched, verified & processed
652        drop(requested_blocks_guard);
653
654        // kick off immediately the scheduled synchronizer
655        if !missing_blocks.is_empty() {
656            // do not block here, so we avoid any possible cycles.
657            if let Err(TrySendError::Full(_)) = commands_sender.try_send(Command::KickOffScheduler)
658            {
659                warn!("Commands channel is full")
660            }
661        }
662
663        context
664            .metrics
665            .node_metrics
666            .missing_blocks_after_fetch_total
667            .inc_by(missing_blocks.len() as u64);
668
669        Ok(())
670    }
671
672    fn get_fetch_after_rounds(
673        context: &Arc<Context>,
674        dag_state: Arc<RwLock<DagState>>,
675    ) -> Vec<Round> {
676        let (blocks, gc_round) = {
677            let dag_state = dag_state.read();
678            (
679                dag_state.get_last_cached_block_per_authority(Round::MAX),
680                dag_state.gc_round(),
681            )
682        };
683        assert_eq!(blocks.len(), context.committee.size());
684
685        blocks
686            .into_iter()
687            .map(|(block, _)| block.round().max(gc_round))
688            .collect::<Vec<_>>()
689    }
690
691    fn verify_blocks(
692        serialized_blocks: Vec<Bytes>,
693        block_verifier: Arc<V>,
694        context: &Context,
695        peer: PeerId,
696    ) -> ConsensusResult<(
697        Vec<VerifiedBlock>,
698        Vec<(VerifiedBlock, Vec<TransactionIndex>)>,
699    )> {
700        let mut verified_blocks = Vec::new();
701        let mut voted_blocks = Vec::new();
702        for serialized_block in serialized_blocks {
703            let signed_block: SignedBlock =
704                bcs::from_bytes(&serialized_block).map_err(ConsensusError::MalformedBlock)?;
705
706            // TODO: cache received and verified block refs to avoid duplicated work.
707            let (verified_block, reject_txn_votes) = block_verifier
708                .verify_and_vote(signed_block, serialized_block)
709                .tap_err(|e| {
710                    let peer_label = peer.labelname(context);
711                    context
712                        .metrics
713                        .node_metrics
714                        .invalid_blocks
715                        .with_label_values(&[peer_label.as_str(), "synchronizer", e.clone().name()])
716                        .inc();
717                    info!("Invalid block received from {}: {}", peer, e);
718                })?;
719
720            // TODO: improve efficiency, maybe suspend and continue processing the block asynchronously.
721            let now = context.clock.timestamp_utc_ms();
722            let drift = verified_block.timestamp_ms().saturating_sub(now);
723            if drift > 0 {
724                let peer_hostname = &context
725                    .committee
726                    .authority(verified_block.author())
727                    .hostname;
728                context
729                    .metrics
730                    .node_metrics
731                    .block_timestamp_drift_ms
732                    .with_label_values(&[peer_hostname.as_str(), "synchronizer"])
733                    .inc_by(drift);
734
735                trace!(
736                    "Synced block {} timestamp {} is in the future (now={}).",
737                    verified_block.reference(),
738                    verified_block.timestamp_ms(),
739                    now
740                );
741            }
742
743            verified_blocks.push(verified_block.clone());
744            voted_blocks.push((verified_block, reject_txn_votes));
745        }
746
747        Ok((verified_blocks, voted_blocks))
748    }
749
750    async fn fetch_blocks_request(
751        network_client: Arc<SynchronizerClient<VC, OC>>,
752        peer: PeerId,
753        blocks_guard: BlocksGuard,
754        fetch_after_rounds: Vec<Round>,
755        fetch_missing_ancestors: bool,
756        request_timeout: Duration,
757        mut retries: u32,
758    ) -> (
759        ConsensusResult<Vec<Bytes>>,
760        BlocksGuard,
761        u32,
762        PeerId,
763        Vec<Round>,
764    ) {
765        let start = Instant::now();
766        let resp = timeout(
767            request_timeout,
768            network_client.fetch_blocks(
769                peer.clone(),
770                blocks_guard
771                    .block_refs
772                    .clone()
773                    .into_iter()
774                    .collect::<Vec<_>>(),
775                fetch_after_rounds.clone().into_iter().collect::<Vec<_>>(),
776                fetch_missing_ancestors,
777                request_timeout,
778            ),
779        )
780        .await;
781
782        fail_point_async!("consensus-delay");
783
784        let resp = match resp {
785            Ok(Err(err)) => {
786                // Add a delay before retrying - if that is needed. If request has timed out then eventually
787                // this will be a no-op.
788                sleep_until(start + request_timeout).await;
789                retries += 1;
790                Err(err)
791            } // network error
792            Err(err) => {
793                // timeout
794                sleep_until(start + request_timeout).await;
795                retries += 1;
796                Err(ConsensusError::NetworkRequestTimeout(err.to_string()))
797            }
798            Ok(result) => result,
799        };
800        (resp, blocks_guard, retries, peer, fetch_after_rounds)
801    }
802
803    fn start_fetch_own_last_block_task(&mut self) {
804        const FETCH_OWN_BLOCK_RETRY_DELAY: Duration = Duration::from_millis(1_000);
805        const MAX_RETRY_DELAY_STEP: Duration = Duration::from_millis(4_000);
806
807        let context = self.context.clone();
808        let dag_state = self.dag_state.clone();
809        let network_client = self.network_client.clone();
810        let block_verifier = self.block_verifier.clone();
811        let core_dispatcher = self.core_dispatcher.clone();
812
813        self.fetch_own_last_block_task
814            .spawn(monitored_future!(async move {
815                let _scope = monitored_scope("FetchOwnLastBlockTask");
816
817                let fetch_own_block = |authority_index: AuthorityIndex, fetch_own_block_delay: Duration| {
818                    let network_client_cloned = network_client.clone();
819                    let own_index = context.own_index;
820                    async move {
821                        sleep(fetch_own_block_delay).await;
822                        let r = network_client_cloned.fetch_latest_blocks(authority_index, vec![own_index], FETCH_REQUEST_TIMEOUT).await;
823                        (r, authority_index)
824                    }
825                };
826
827
828                let process_blocks = |blocks: Vec<Bytes>, authority_index: AuthorityIndex| -> ConsensusResult<Vec<VerifiedBlock>> {
829                    let mut result = Vec::new();
830                    for serialized_block in blocks {
831                        let signed_block = bcs::from_bytes(&serialized_block).map_err(ConsensusError::MalformedBlock)?;
832                        let (verified_block, _) = block_verifier.verify_and_vote(signed_block, serialized_block).tap_err(|err|{
833                            let hostname = context.committee.authority(authority_index).hostname.clone();
834                            context
835                                .metrics
836                                .node_metrics
837                                .invalid_blocks
838                                .with_label_values(&[hostname.as_str(), "synchronizer_own_block", err.clone().name()])
839                                .inc();
840                            warn!("Invalid block received from {}: {}", authority_index, err);
841                        })?;
842
843                        if verified_block.author() != context.own_index {
844                            return Err(ConsensusError::UnexpectedLastOwnBlock { index: authority_index, block_ref: verified_block.reference()});
845                        }
846                        result.push(verified_block);
847                    }
848                    Ok(result)
849                };
850
851                // Get the highest of all the results. Retry until at least `f+1` results have been gathered.
852                let mut highest_round;
853                let mut retries = 0;
854                let mut retry_delay_step = Duration::from_millis(500);
855                'main:loop {
856                    if context.committee.size() == 1 {
857                        highest_round = dag_state.read().get_last_proposed_block().expect("Last proposed block should be returned on validators").round();
858                        info!("Only one node in the network, will not try fetching own last block from peers.");
859                        break 'main;
860                    }
861
862                    let mut total_stake = 0;
863                    highest_round = 0;
864
865                    // Ask all the other peers about our last block
866                    let mut results = FuturesUnordered::new();
867
868                    for (authority_index, _authority) in context.committee.authorities() {
869                        if authority_index != context.own_index {
870                            results.push(fetch_own_block(authority_index, Duration::from_millis(0)));
871                        }
872                    }
873
874                    // Gather the results but wait to timeout as well
875                    let timer = sleep_until(Instant::now() + context.parameters.sync_last_known_own_block_timeout);
876                    tokio::pin!(timer);
877
878                    'inner: loop {
879                        tokio::select! {
880                            result = results.next() => {
881                                let Some((result, authority_index)) = result else {
882                                    break 'inner;
883                                };
884                                match result {
885                                    Ok(result) => {
886                                        match process_blocks(result, authority_index) {
887                                            Ok(blocks) => {
888                                                let max_round = blocks.into_iter().map(|b|b.round()).max().unwrap_or(0);
889                                                highest_round = highest_round.max(max_round);
890
891                                                total_stake += context.committee.stake(authority_index);
892                                            },
893                                            Err(err) => {
894                                                warn!("Invalid result returned from {authority_index} while fetching last own block: {err}");
895                                            }
896                                        }
897                                    },
898                                    Err(err) => {
899                                        warn!("Error {err} while fetching our own block from peer {authority_index}. Will retry.");
900                                        results.push(fetch_own_block(authority_index, FETCH_OWN_BLOCK_RETRY_DELAY));
901                                    }
902                                }
903                            },
904                            () = &mut timer => {
905                                info!("Timeout while trying to sync our own last block from peers");
906                                break 'inner;
907                            }
908                        }
909                    }
910
911                    // Request at least f+1 stake to have replied back.
912                    if context.committee.reached_validity(total_stake) {
913                        info!("{} out of {} total stake returned acceptable results for our own last block with highest round {}, with {retries} retries.", total_stake, context.committee.total_stake(), highest_round);
914                        break 'main;
915                    }
916
917                    retries += 1;
918                    context.metrics.node_metrics.sync_last_known_own_block_retries.inc();
919                    warn!("Not enough stake: {} out of {} total stake returned acceptable results for our own last block with highest round {}. Will now retry {retries}.", total_stake, context.committee.total_stake(), highest_round);
920
921                    sleep(retry_delay_step).await;
922
923                    retry_delay_step = Duration::from_secs_f64(retry_delay_step.as_secs_f64() * 1.5);
924                    retry_delay_step = retry_delay_step.min(MAX_RETRY_DELAY_STEP);
925                }
926
927                // Update the Core with the highest detected round
928                context.metrics.node_metrics.last_known_own_block_round.set(highest_round as i64);
929
930                if let Err(err) = core_dispatcher.set_last_known_proposed_round(highest_round) {
931                    warn!("Error received while calling dispatcher, probably dispatcher is shutting down, will now exit: {err:?}");
932                }
933            }));
934    }
935
936    async fn start_fetch_missing_blocks_task(&mut self) -> ConsensusResult<()> {
937        if self.context.committee.size() == 1 {
938            trace!(
939                "Only one node in the network, will not try fetching missing blocks from peers."
940            );
941            return Ok(());
942        }
943
944        // If commit is lagging and commit sync is making progress, skip periodic sync.
945        // Commit syncer fetches certified commits with all necessary causal history.
946        // If commit sync is not making progress, periodic sync resumes as a fallback.
947        if !self.should_run_periodic_sync() {
948            return Ok(());
949        }
950
951        let context = self.context.clone();
952        let network_client = self.network_client.clone();
953        let block_verifier = self.block_verifier.clone();
954        let transaction_vote_tracker = self.transaction_vote_tracker.clone();
955        let commit_vote_monitor = self.commit_vote_monitor.clone();
956        let core_dispatcher = self.core_dispatcher.clone();
957        let blocks_to_fetch = self.inflight_blocks_map.clone();
958        let commands_sender = self.commands_sender.clone();
959        let dag_state = self.dag_state.clone();
960        let round_tracker = self.round_tracker.clone();
961        let peers_pool = self.peers_pool.clone();
962
963        let mut missing_blocks = self
964            .core_dispatcher
965            .get_missing_blocks()
966            .await
967            .map_err(|_err| ConsensusError::Shutdown)?;
968        if self.commit_sync_failover {
969            // Keep missing blocks to those that must be included in fetch request.
970            // Filtered out missing blocks that will eventually be fetched with fetch_after_rounds.
971            let fetch_after_rounds = Self::get_fetch_after_rounds(&context, dag_state.clone());
972            missing_blocks.retain(|block| block.round <= fetch_after_rounds[block.author.value()]);
973        } else if missing_blocks.is_empty() {
974            return Ok(());
975        }
976
977        self.fetch_blocks_scheduler_task
978            .spawn(monitored_future!(async move {
979                let _scope = monitored_scope("FetchMissingBlocksScheduler");
980                context
981                    .metrics
982                    .node_metrics
983                    .fetch_blocks_scheduler_inflight
984                    .inc();
985                let total_requested = missing_blocks.len();
986
987                let results = if missing_blocks.is_empty() {
988                    let _scope = monitored_scope("BlockSync::Periodic::HighestAcceptedRounds");
989                    // Fetch blocks from a random peer using highest accepted rounds (commit sync failover)
990                    Self::fetch_blocks_with_fetch_after_rounds(
991                        context.clone(),
992                        blocks_to_fetch.clone(),
993                        network_client,
994                        dag_state,
995                        peers_pool,
996                    )
997                    .await
998                } else {
999                    let _scope = monitored_scope("BlockSync::Periodic::MissingBlocks");
1000                    // Fetch blocks from 1 to MAX_PERIODIC_SYNC_PEERS peers
1001                    Self::fetch_blocks_from_peers(
1002                        context.clone(),
1003                        blocks_to_fetch.clone(),
1004                        network_client,
1005                        missing_blocks,
1006                        dag_state,
1007                        peers_pool,
1008                    )
1009                    .await
1010                };
1011                context
1012                    .metrics
1013                    .node_metrics
1014                    .fetch_blocks_scheduler_inflight
1015                    .dec();
1016                if results.is_empty() {
1017                    return;
1018                }
1019
1020                fail_point_async!("consensus-delay");
1021
1022                // Now process the returned results
1023                let mut total_fetched = 0;
1024                for (blocks_guard, fetched_blocks, peer) in results {
1025                    total_fetched += fetched_blocks.len();
1026
1027                    if let Err(err) = Self::process_fetched_blocks(
1028                        fetched_blocks,
1029                        peer.clone(),
1030                        blocks_guard,
1031                        core_dispatcher.clone(),
1032                        block_verifier.clone(),
1033                        transaction_vote_tracker.clone(),
1034                        commit_vote_monitor.clone(),
1035                        context.clone(),
1036                        commands_sender.clone(),
1037                        round_tracker.clone(),
1038                        "periodic",
1039                    )
1040                    .await
1041                    {
1042                        warn!(
1043                            "Error occurred while processing fetched blocks from peer {:?}: {err}",
1044                            peer
1045                        );
1046                        let peer_name = peer.labelname(&context);
1047                        context
1048                            .metrics
1049                            .node_metrics
1050                            .synchronizer_process_fetched_failures
1051                            .with_label_values(&[peer_name.as_str(), "periodic"])
1052                            .inc();
1053                    }
1054                }
1055
1056                debug!(
1057                    "Total blocks requested to fetch: {}, total fetched: {}",
1058                    total_requested, total_fetched
1059                );
1060            }));
1061
1062        Ok(())
1063    }
1064
1065    fn should_run_periodic_sync(&mut self) -> bool {
1066        let current_commit_index = self.dag_state.read().last_commit_index();
1067        let quorum_commit_index = self.commit_vote_monitor.quorum_commit_index();
1068        let now = Instant::now();
1069        let metrics = &self.context.metrics.node_metrics;
1070
1071        // Commit is not lagging.
1072        if !is_commit_lagging(
1073            self.context.as_ref(),
1074            current_commit_index,
1075            quorum_commit_index,
1076        ) {
1077            metrics
1078                .synchronizer_periodic_sync_decision
1079                .with_label_values(&["true", "default"])
1080                .inc();
1081            // Reset last commit state.
1082            self.last_changed_commit_index = current_commit_index;
1083            self.last_commit_change_time = now;
1084            self.commit_sync_failover = false;
1085            // Run periodic sync.
1086            return true;
1087        }
1088        // Commit is lagging.
1089
1090        // When in commit sync failover, check if enough progress has been made.
1091        if self.commit_sync_failover {
1092            if current_commit_index
1093                < self.last_changed_commit_index + self.context.parameters.commit_sync_batch_size
1094            {
1095                metrics
1096                    .synchronizer_periodic_sync_decision
1097                    .with_label_values(&["true", "commit_catchup::run"])
1098                    .inc();
1099                // Not enough progress has been made yet. Keep running periodic sync.
1100                // Do not update last commit state yet.
1101                // Run periodic sync.
1102                return true;
1103            } else {
1104                metrics
1105                    .synchronizer_periodic_sync_decision
1106                    .with_label_values(&["false", "commit_catchup::end"])
1107                    .inc();
1108                // Enough progress has been made. Disable commit sync failover and reset last commit state.
1109                self.last_changed_commit_index = current_commit_index;
1110                self.last_commit_change_time = now;
1111                self.commit_sync_failover = false;
1112                // Skip periodic sync because of commit lag.
1113                return false;
1114            }
1115        }
1116
1117        // The node is commit lagging and not in commit sync failover yet.
1118        if current_commit_index == self.last_changed_commit_index {
1119            // Enter commit sync failover if not enough progress has been made.
1120            if now.duration_since(self.last_commit_change_time) >= COMMIT_PROGRESS_TIMEOUT {
1121                metrics
1122                    .synchronizer_periodic_sync_decision
1123                    .with_label_values(&["true", "commit_catchup::start"])
1124                    .inc();
1125                self.commit_sync_failover = true;
1126                // Run periodic sync.
1127                return true;
1128            }
1129        } else {
1130            // IMPORTANT: Only update last commit state when commit index is changing.
1131            self.last_changed_commit_index = current_commit_index;
1132            self.last_commit_change_time = now;
1133        }
1134
1135        metrics
1136            .synchronizer_periodic_sync_decision
1137            .with_label_values(&["false", "commit_lag"])
1138            .inc();
1139        false
1140    }
1141
1142    /// Fetches blocks from a random peer using only fetch_after_rounds (no specific missing blocks).
1143    /// Used during commit sync failover to make progress when commit sync is stalled.
1144    async fn fetch_blocks_with_fetch_after_rounds(
1145        context: Arc<Context>,
1146        inflight_blocks: Arc<InflightBlocksMap>,
1147        network_client: Arc<SynchronizerClient<VC, OC>>,
1148        dag_state: Arc<RwLock<DagState>>,
1149        peers_pool: Arc<PeersPool>,
1150    ) -> Vec<(BlocksGuard, Vec<Bytes>, PeerId)> {
1151        let fetch_after_rounds = Self::get_fetch_after_rounds(&context, dag_state.clone());
1152
1153        // Pick a random peer (excluding self).
1154        // Get available peers from the PeersPool
1155        let mut peers = peers_pool.get_known_peers();
1156
1157        // TODO: in the future it would be possible, temporarily, for an Observer node to not have peers to fetch from.
1158        // We should change this assertion to allow for this case.
1159        assert!(!peers.is_empty(), "No known peers to fetch blocks from");
1160
1161        if cfg!(not(test)) {
1162            peers.shuffle(&mut ThreadRng::default());
1163        }
1164
1165        let peer = peers.first().unwrap().clone();
1166
1167        let response = timeout(
1168            FETCH_REQUEST_TIMEOUT,
1169            network_client.fetch_blocks(
1170                peer.clone(),
1171                vec![],
1172                fetch_after_rounds,
1173                false,
1174                FETCH_REQUEST_TIMEOUT,
1175            ),
1176        )
1177        .await;
1178
1179        let serialized_blocks = match response {
1180            Ok(Ok(blocks)) => blocks,
1181            Ok(Err(err)) => {
1182                debug!("Failed to fetch blocks with fetch_after_rounds from peer {peer}: {err}");
1183                return vec![];
1184            }
1185            Err(_) => {
1186                debug!("Timed out fetching blocks with fetch_after_rounds from peer {peer}");
1187                return vec![];
1188            }
1189        };
1190
1191        let blocks_guard = BlocksGuard {
1192            map: inflight_blocks,
1193            block_refs: BTreeSet::new(),
1194            peer: peer.clone(),
1195        };
1196
1197        vec![(blocks_guard, serialized_blocks, peer)]
1198    }
1199
1200    /// Fetches the `missing_blocks` from peers. Requests the same number of authorities with missing blocks from each peer.
1201    /// Each response from peer can contain the requested blocks, and additional blocks from the last accepted round for
1202    /// authorities with missing blocks.
1203    /// Each element of the vector is a tuple which contains the requested missing block refs, the returned blocks and
1204    /// the peer.
1205    async fn fetch_blocks_from_peers(
1206        context: Arc<Context>,
1207        inflight_blocks: Arc<InflightBlocksMap>,
1208        network_client: Arc<SynchronizerClient<VC, OC>>,
1209        missing_blocks: BTreeSet<BlockRef>,
1210        dag_state: Arc<RwLock<DagState>>,
1211        peers_pool: Arc<PeersPool>,
1212    ) -> Vec<(BlocksGuard, Vec<Bytes>, PeerId)> {
1213        // Preliminary truncation of missing blocks to fetch. Since each peer can have different
1214        // number of missing blocks and the fetching is batched by peer, so keep more than max_blocks_per_fetch
1215        // per peer on average.
1216        let missing_blocks = missing_blocks
1217            .into_iter()
1218            .take(2 * MAX_PERIODIC_SYNC_PEERS * context.parameters.max_blocks_per_fetch)
1219            .collect::<Vec<_>>();
1220
1221        // Maps authorities to the missing blocks they have.
1222        let mut authorities = BTreeMap::<AuthorityIndex, Vec<BlockRef>>::new();
1223        for block_ref in &missing_blocks {
1224            authorities
1225                .entry(block_ref.author)
1226                .or_default()
1227                .push(*block_ref);
1228        }
1229
1230        // Get known peers from the PeersPool
1231        let mut peers = peers_pool.get_known_peers();
1232
1233        // Distribute the same number of authorities into each peer to sync.
1234        // Use the number of known peers from the pool, capped at MAX_PERIODIC_SYNC_PEERS
1235        // TODO: in the future it would be possible, temporarily, for an Observer node to not have peers to fetch from.
1236        // We should change this assertion to allow for this case.
1237        assert!(!peers.is_empty(), "No known peers to fetch blocks from");
1238
1239        let num_authorities_per_peer = authorities
1240            .len()
1241            .div_ceil(peers.len().min(MAX_PERIODIC_SYNC_PEERS));
1242
1243        // Update metrics related to missing blocks.
1244        let mut missing_blocks_per_authority = vec![0; context.committee.size()];
1245        for (authority, blocks) in &authorities {
1246            missing_blocks_per_authority[*authority] += blocks.len();
1247        }
1248        for (missing, (_, authority)) in missing_blocks_per_authority
1249            .into_iter()
1250            .zip_debug_eq(context.committee.authorities())
1251        {
1252            context
1253                .metrics
1254                .node_metrics
1255                .synchronizer_missing_blocks_by_authority
1256                .with_label_values(&[&authority.hostname])
1257                .inc_by(missing as u64);
1258            context
1259                .metrics
1260                .node_metrics
1261                .synchronizer_current_missing_blocks_by_authority
1262                .with_label_values(&[&authority.hostname])
1263                .set(missing as i64);
1264        }
1265
1266        // TODO: probably inject the RNG to allow unit testing - this is a work around for now.
1267        if cfg!(not(test)) {
1268            // Shuffle the peers
1269            peers.shuffle(&mut ThreadRng::default());
1270        }
1271
1272        let mut peers = peers.into_iter();
1273        let mut request_futures = FuturesUnordered::new();
1274
1275        // Shuffle the authorities for each request.
1276        let mut authorities = authorities.into_values().collect::<Vec<_>>();
1277        if cfg!(not(test)) {
1278            // Shuffle the authorities
1279            authorities.shuffle(&mut ThreadRng::default());
1280        }
1281
1282        let fetch_after_rounds = Self::get_fetch_after_rounds(&context, dag_state.clone());
1283
1284        // Send the fetch requests
1285        for batch in authorities.chunks(num_authorities_per_peer) {
1286            let Some(peer) = peers.next() else {
1287                debug_fatal!("No more peers left to fetch blocks!");
1288                break;
1289            };
1290            let peer_name = peer.hostname(&context);
1291            // Fetch from the lowest round missing blocks to ensure progress.
1292            // This may reduce efficiency and increase the chance of duplicated data transfer in edge cases.
1293            let block_refs = batch
1294                .iter()
1295                .flatten()
1296                .cloned()
1297                .collect::<BTreeSet<_>>()
1298                .into_iter()
1299                .take(context.parameters.max_blocks_per_fetch)
1300                .collect::<BTreeSet<_>>();
1301
1302            // lock the blocks to be fetched. If no lock can be acquired for any of the blocks then don't bother
1303            if let Some(blocks_guard) =
1304                inflight_blocks.lock_blocks(block_refs.clone(), peer.clone())
1305            {
1306                info!(
1307                    "Periodic sync of {} missing blocks from peer {} {:?}: {}",
1308                    peer_name.as_str(),
1309                    block_refs.len(),
1310                    peer,
1311                    block_refs
1312                        .iter()
1313                        .map(|b| b.to_string())
1314                        .collect::<Vec<_>>()
1315                        .join(", ")
1316                );
1317                request_futures.push(Self::fetch_blocks_request(
1318                    network_client.clone(),
1319                    peer,
1320                    blocks_guard,
1321                    fetch_after_rounds.clone(),
1322                    false,
1323                    FETCH_REQUEST_TIMEOUT,
1324                    1,
1325                ));
1326            }
1327        }
1328
1329        let mut results = Vec::new();
1330        let fetcher_timeout = sleep(FETCH_FROM_PEERS_TIMEOUT);
1331
1332        tokio::pin!(fetcher_timeout);
1333
1334        loop {
1335            tokio::select! {
1336                Some((response, blocks_guard, _retries, peer, fetch_after_rounds)) = request_futures.next() => {
1337                    match response {
1338                        Ok(fetched_blocks) => {
1339                            results.push((blocks_guard, fetched_blocks, peer));
1340
1341                            // no more pending requests are left, just break the loop
1342                            if request_futures.is_empty() {
1343                                break;
1344                            }
1345                        },
1346                        Err(_) => {
1347                            let peer_name = peer.labelname(&context);
1348                            context.metrics.node_metrics.synchronizer_fetch_failures.with_label_values(&[peer_name.as_str(), "periodic"]).inc();
1349                            // try again if there is any peer left
1350                            if let Some(next_peer) = peers.next() {
1351                                // do best effort to lock guards. If we can't lock then don't bother at this run.
1352                                if let Some(blocks_guard) = inflight_blocks.swap_locks(blocks_guard, next_peer.clone()) {
1353                                    info!(
1354                                        "Retrying syncing {} missing blocks from peer {:?}: {}",
1355                                        blocks_guard.block_refs.len(),
1356                                        next_peer,
1357                                        blocks_guard.block_refs
1358                                            .iter()
1359                                            .map(|b| b.to_string())
1360                                            .collect::<Vec<_>>()
1361                                            .join(", ")
1362                                    );
1363                                    request_futures.push(Self::fetch_blocks_request(
1364                                        network_client.clone(),
1365                                        next_peer,
1366                                        blocks_guard,
1367                                        fetch_after_rounds,
1368                                        false,
1369                                        FETCH_REQUEST_TIMEOUT,
1370                                        1,
1371                                    ));
1372                                } else {
1373                                    debug!("Couldn't acquire locks to fetch blocks from peer {:?}.", next_peer)
1374                                }
1375                            } else {
1376                                debug!("No more peers left to fetch blocks");
1377                            }
1378                        }
1379                    }
1380                },
1381                _ = &mut fetcher_timeout => {
1382                    debug!("Timed out while fetching missing blocks");
1383                    break;
1384                }
1385            }
1386        }
1387
1388        results
1389    }
1390}
1391
1392#[cfg(test)]
1393mod tests {
1394    use std::{
1395        collections::{BTreeMap, BTreeSet},
1396        sync::Arc,
1397        time::Duration,
1398    };
1399
1400    use async_trait::async_trait;
1401    use bytes::Bytes;
1402    use consensus_config::{AuthorityIndex, Parameters};
1403    use consensus_types::block::{BlockDigest, BlockRef, Round};
1404    use mysten_metrics::monitored_mpsc;
1405    use parking_lot::RwLock;
1406    use tokio::{sync::Mutex, time::sleep};
1407
1408    use crate::commit::{CommitVote, TrustedCommit};
1409    use crate::{
1410        CommitDigest, CommitIndex,
1411        block::{TestBlock, VerifiedBlock},
1412        block_verifier::NoopBlockVerifier,
1413        commit_vote_monitor::CommitVoteMonitor,
1414        context::Context,
1415        core_thread::CoreThreadDispatcher,
1416        dag_state::DagState,
1417        error::{ConsensusError, ConsensusResult},
1418        network::{
1419            BlockStream, ObserverNetworkClient, PeerId, SynchronizerClient, ValidatorNetworkClient,
1420        },
1421        storage::mem_store::MemStore,
1422        synchronizer::{
1423            COMMIT_PROGRESS_TIMEOUT, FETCH_BLOCKS_CONCURRENCY, FETCH_REQUEST_TIMEOUT,
1424            InflightBlocksMap, Synchronizer,
1425        },
1426    };
1427    use crate::{
1428        commit_vote_monitor::COMMIT_LAG_MULTIPLIER, core_thread::MockCoreThreadDispatcher,
1429        peers_pool::PeersPool, round_tracker::RoundTracker,
1430        transaction_vote_tracker::TransactionVoteTracker,
1431    };
1432
1433    type FetchRequestKey = (Vec<BlockRef>, AuthorityIndex);
1434    type FetchRequestResponse = (Vec<VerifiedBlock>, Option<Duration>);
1435    type FetchLatestBlockKey = (AuthorityIndex, Vec<AuthorityIndex>);
1436    type FetchLatestBlockResponse = (Vec<VerifiedBlock>, Option<Duration>);
1437
1438    #[derive(Default)]
1439    struct MockNetworkClient {
1440        fetch_blocks_requests: Mutex<BTreeMap<FetchRequestKey, FetchRequestResponse>>,
1441        fetch_latest_blocks_requests:
1442            Mutex<BTreeMap<FetchLatestBlockKey, Vec<FetchLatestBlockResponse>>>,
1443    }
1444
1445    impl MockNetworkClient {
1446        async fn stub_fetch_blocks(
1447            &self,
1448            blocks: Vec<VerifiedBlock>,
1449            peer: AuthorityIndex,
1450            latency: Option<Duration>,
1451        ) {
1452            let mut lock = self.fetch_blocks_requests.lock().await;
1453            let block_refs = blocks
1454                .iter()
1455                .map(|block| block.reference())
1456                .collect::<Vec<_>>();
1457            lock.insert((block_refs, peer), (blocks, latency));
1458        }
1459
1460        async fn stub_fetch_blocks_for_key(
1461            &self,
1462            key_refs: Vec<BlockRef>,
1463            response_blocks: Vec<VerifiedBlock>,
1464            peer: AuthorityIndex,
1465            latency: Option<Duration>,
1466        ) {
1467            let mut lock = self.fetch_blocks_requests.lock().await;
1468            lock.insert((key_refs, peer), (response_blocks, latency));
1469        }
1470
1471        async fn stub_fetch_latest_blocks(
1472            &self,
1473            blocks: Vec<VerifiedBlock>,
1474            peer: AuthorityIndex,
1475            authorities: Vec<AuthorityIndex>,
1476            latency: Option<Duration>,
1477        ) {
1478            let mut lock = self.fetch_latest_blocks_requests.lock().await;
1479            lock.entry((peer, authorities))
1480                .or_default()
1481                .push((blocks, latency));
1482        }
1483
1484        async fn fetch_latest_blocks_pending_calls(&self) -> usize {
1485            let lock = self.fetch_latest_blocks_requests.lock().await;
1486            lock.len()
1487        }
1488    }
1489
1490    #[async_trait]
1491    impl ValidatorNetworkClient for MockNetworkClient {
1492        async fn subscribe_blocks(
1493            &self,
1494            _peer: AuthorityIndex,
1495            _last_received: Round,
1496            _timeout: Duration,
1497        ) -> ConsensusResult<BlockStream> {
1498            unimplemented!("subscribe_blocks not implemented in mock")
1499        }
1500
1501        async fn fetch_blocks(
1502            &self,
1503            peer: AuthorityIndex,
1504            block_refs: Vec<BlockRef>,
1505            _fetch_after_rounds: Vec<Round>,
1506            _fetch_missing_ancestors: bool,
1507            _timeout: Duration,
1508        ) -> ConsensusResult<Vec<Bytes>> {
1509            let mut lock = self.fetch_blocks_requests.lock().await;
1510            let response = lock.remove(&(block_refs.clone(), peer)).unwrap_or_else(|| {
1511                panic!(
1512                    "Unexpected fetch blocks request made: {:?} {}. Current lock: {:?}",
1513                    block_refs, peer, lock
1514                );
1515            });
1516
1517            let serialised = response
1518                .0
1519                .into_iter()
1520                .map(|block| block.serialized().clone())
1521                .collect::<Vec<_>>();
1522
1523            drop(lock);
1524
1525            if let Some(latency) = response.1 {
1526                sleep(latency).await;
1527            }
1528
1529            Ok(serialised)
1530        }
1531
1532        async fn fetch_commits(
1533            &self,
1534            _peer: AuthorityIndex,
1535            _commit_range: crate::commit::CommitRange,
1536            _timeout: Duration,
1537        ) -> ConsensusResult<(Vec<Bytes>, Vec<Bytes>)> {
1538            unimplemented!("fetch_commits not implemented in mock")
1539        }
1540
1541        async fn fetch_latest_blocks(
1542            &self,
1543            peer: AuthorityIndex,
1544            authorities: Vec<AuthorityIndex>,
1545            _timeout: Duration,
1546        ) -> ConsensusResult<Vec<Bytes>> {
1547            let mut lock = self.fetch_latest_blocks_requests.lock().await;
1548            let mut responses = lock
1549                .remove(&(peer, authorities.clone()))
1550                .expect("Unexpected fetch blocks request made");
1551
1552            let response = responses.remove(0);
1553            let serialised = response
1554                .0
1555                .into_iter()
1556                .map(|block| block.serialized().clone())
1557                .collect::<Vec<_>>();
1558
1559            if !responses.is_empty() {
1560                lock.insert((peer, authorities), responses);
1561            }
1562
1563            drop(lock);
1564
1565            if let Some(latency) = response.1 {
1566                sleep(latency).await;
1567            }
1568
1569            Ok(serialised)
1570        }
1571
1572        async fn get_latest_rounds(
1573            &self,
1574            _peer: AuthorityIndex,
1575            _timeout: Duration,
1576        ) -> ConsensusResult<(Vec<Round>, Vec<Round>)> {
1577            unimplemented!("get_latest_rounds not implemented in mock")
1578        }
1579
1580        #[cfg(test)]
1581        async fn send_block(
1582            &self,
1583            _peer: AuthorityIndex,
1584            _block: &VerifiedBlock,
1585            _timeout: Duration,
1586        ) -> ConsensusResult<()> {
1587            unimplemented!("send_block not implemented in mock")
1588        }
1589    }
1590
1591    #[async_trait]
1592    impl ObserverNetworkClient for MockNetworkClient {
1593        async fn stream_blocks(
1594            &self,
1595            _peer: crate::network::PeerId,
1596            _highest_round_per_authority: Vec<Round>,
1597            _filter: crate::network::BlockStreamFilter,
1598            _timeout: Duration,
1599        ) -> ConsensusResult<crate::network::ObserverBlockStream> {
1600            unimplemented!("stream_blocks not implemented in mock")
1601        }
1602
1603        async fn fetch_blocks(
1604            &self,
1605            _peer: crate::network::PeerId,
1606            _block_refs: Vec<BlockRef>,
1607            _fetch_after_rounds: Vec<Round>,
1608            _fetch_missing_ancestors: bool,
1609            _timeout: Duration,
1610        ) -> ConsensusResult<Vec<Bytes>> {
1611            unimplemented!("Observer fetch_blocks not implemented in mock")
1612        }
1613
1614        async fn fetch_commits(
1615            &self,
1616            _peer: crate::network::PeerId,
1617            _commit_range: crate::commit::CommitRange,
1618            _timeout: Duration,
1619        ) -> ConsensusResult<(Vec<Bytes>, Vec<Bytes>)> {
1620            unimplemented!("Observer fetch_commits not implemented in mock")
1621        }
1622    }
1623
1624    #[test]
1625    fn test_inflight_blocks_map() {
1626        // GIVEN
1627        let map = InflightBlocksMap::new();
1628        let some_block_refs = [
1629            BlockRef::new(1, AuthorityIndex::new_for_test(0), BlockDigest::MIN),
1630            BlockRef::new(10, AuthorityIndex::new_for_test(0), BlockDigest::MIN),
1631            BlockRef::new(12, AuthorityIndex::new_for_test(3), BlockDigest::MIN),
1632            BlockRef::new(15, AuthorityIndex::new_for_test(2), BlockDigest::MIN),
1633        ];
1634        let missing_block_refs = some_block_refs.iter().cloned().collect::<BTreeSet<_>>();
1635
1636        // Lock & unlock blocks
1637        {
1638            let mut all_guards = Vec::new();
1639
1640            // Try to acquire the block locks for authorities 1 & 2
1641            for i in 1..=2 {
1642                let authority = AuthorityIndex::new_for_test(i);
1643                let peer = PeerId::Validator(authority);
1644
1645                let guard = map.lock_blocks(missing_block_refs.clone(), peer.clone());
1646                let guard = guard.expect("Guard should be created");
1647                assert_eq!(guard.block_refs.len(), 4);
1648
1649                all_guards.push(guard);
1650
1651                // trying to acquire any of them again will not succeed
1652                let guard = map.lock_blocks(missing_block_refs.clone(), peer);
1653                assert!(guard.is_none());
1654            }
1655
1656            // Trying to acquire for authority 3 it will fail - as we have maxed out the number of allowed peers
1657            let authority_3 = AuthorityIndex::new_for_test(3);
1658            let peer_3 = PeerId::Validator(authority_3);
1659
1660            let guard = map.lock_blocks(missing_block_refs.clone(), peer_3.clone());
1661            assert!(guard.is_none());
1662
1663            // Explicitly drop the guard of authority 1 and try for authority 3 again - it will now succeed
1664            drop(all_guards.remove(0));
1665
1666            let guard = map.lock_blocks(missing_block_refs.clone(), peer_3);
1667            let guard = guard.expect("Guard should be successfully acquired");
1668
1669            assert_eq!(guard.block_refs, missing_block_refs);
1670
1671            // Dropping all guards should unlock on the block refs
1672            drop(guard);
1673            drop(all_guards);
1674
1675            assert_eq!(map.num_of_locked_blocks(), 0);
1676        }
1677
1678        // Swap locks
1679        {
1680            // acquire a lock for authority 1
1681            let authority_1 = AuthorityIndex::new_for_test(1);
1682            let peer_1 = PeerId::Validator(authority_1);
1683            let guard = map.lock_blocks(missing_block_refs.clone(), peer_1).unwrap();
1684
1685            // Now swap the locks for authority 2
1686            let authority_2 = AuthorityIndex::new_for_test(2);
1687            let peer_2 = PeerId::Validator(authority_2);
1688            let guard = map.swap_locks(guard, peer_2);
1689
1690            assert_eq!(guard.unwrap().block_refs, missing_block_refs);
1691        }
1692    }
1693
1694    #[tokio::test]
1695    async fn successful_fetch_blocks_from_peer() {
1696        // GIVEN
1697        let (context, _) = Context::new_for_test(4);
1698        let context = Arc::new(context);
1699        let block_verifier = Arc::new(NoopBlockVerifier {});
1700        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
1701        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
1702        let mock_client = Arc::new(MockNetworkClient::default());
1703        let store = Arc::new(MemStore::new());
1704        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
1705        let transaction_vote_tracker =
1706            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
1707        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1708
1709        let network_client = Arc::new(SynchronizerClient::new(
1710            context.clone(),
1711            Some(mock_client.clone()),
1712            Some(mock_client.clone()),
1713        ));
1714        let peers_pool = Arc::new(PeersPool::new(context.clone()));
1715        let handle = Synchronizer::start(
1716            network_client,
1717            context.clone(),
1718            core_dispatcher.clone(),
1719            commit_vote_monitor,
1720            block_verifier,
1721            transaction_vote_tracker,
1722            round_tracker,
1723            dag_state,
1724            peers_pool.clone(),
1725            false,
1726        );
1727
1728        // Create some test blocks
1729        let expected_blocks = (0..10)
1730            .map(|round| VerifiedBlock::new_for_test(TestBlock::new(round, 0).build()))
1731            .collect::<Vec<_>>();
1732        let missing_blocks = expected_blocks
1733            .iter()
1734            .map(|block| block.reference())
1735            .collect::<BTreeSet<_>>();
1736
1737        // AND stub the fetch_blocks request from peer 1
1738        let peer = AuthorityIndex::new_for_test(1);
1739        mock_client
1740            .stub_fetch_blocks(expected_blocks.clone(), peer, None)
1741            .await;
1742
1743        // WHEN request missing blocks from peer 1
1744        assert!(
1745            handle
1746                .fetch_blocks(missing_blocks, PeerId::Validator(peer))
1747                .await
1748                .is_ok()
1749        );
1750
1751        // Wait a little bit until those have been added in core
1752        sleep(Duration::from_millis(1_000)).await;
1753
1754        // THEN ensure those ended up in Core
1755        let added_blocks = core_dispatcher.get_add_blocks().await;
1756        assert_eq!(added_blocks, expected_blocks);
1757    }
1758
1759    #[tokio::test]
1760    async fn saturate_fetch_blocks_from_peer() {
1761        // GIVEN
1762        let (context, _) = Context::new_for_test(4);
1763        let context = Arc::new(context);
1764        let block_verifier = Arc::new(NoopBlockVerifier {});
1765        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
1766        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
1767        let mock_client = Arc::new(MockNetworkClient::default());
1768        let store = Arc::new(MemStore::new());
1769        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
1770        let transaction_vote_tracker =
1771            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
1772        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1773
1774        let network_client = Arc::new(SynchronizerClient::new(
1775            context.clone(),
1776            Some(mock_client.clone()),
1777            Some(mock_client.clone()),
1778        ));
1779        let peers_pool = Arc::new(PeersPool::new(context.clone()));
1780        let handle = Synchronizer::start(
1781            network_client,
1782            context.clone(),
1783            core_dispatcher.clone(),
1784            commit_vote_monitor,
1785            block_verifier,
1786            transaction_vote_tracker,
1787            round_tracker,
1788            dag_state,
1789            peers_pool.clone(),
1790            false,
1791        );
1792
1793        // Create some test blocks
1794        let expected_blocks = (0..=2 * FETCH_BLOCKS_CONCURRENCY)
1795            .map(|round| VerifiedBlock::new_for_test(TestBlock::new(round as Round, 0).build()))
1796            .collect::<Vec<_>>();
1797
1798        // Now start sending requests to fetch blocks by trying to saturate peer 1 task
1799        let peer = AuthorityIndex::new_for_test(1);
1800        let mut iter = expected_blocks.iter().peekable();
1801        while let Some(block) = iter.next() {
1802            // stub the fetch_blocks request from peer 1 and give some high response latency so requests
1803            // can start blocking the peer task.
1804            mock_client
1805                .stub_fetch_blocks(
1806                    vec![block.clone()],
1807                    peer,
1808                    Some(Duration::from_millis(5_000)),
1809                )
1810                .await;
1811
1812            let mut missing_blocks = BTreeSet::new();
1813            missing_blocks.insert(block.reference());
1814
1815            // WHEN requesting to fetch the blocks, it should not succeed for the last request and get
1816            // an error with "saturated" synchronizer
1817            if iter.peek().is_none() {
1818                match handle
1819                    .fetch_blocks(missing_blocks, PeerId::Validator(peer))
1820                    .await
1821                {
1822                    Err(ConsensusError::SynchronizerSaturated(peer_str)) => {
1823                        assert_eq!(peer_str, format!("{:?}", PeerId::Validator(peer)));
1824                    }
1825                    _ => panic!("A saturated synchronizer error was expected"),
1826                }
1827            } else {
1828                assert!(
1829                    handle
1830                        .fetch_blocks(missing_blocks, PeerId::Validator(peer))
1831                        .await
1832                        .is_ok()
1833                );
1834            }
1835        }
1836    }
1837
1838    #[tokio::test(flavor = "current_thread", start_paused = true)]
1839    async fn synchronizer_periodic_task_fetch_blocks() {
1840        // GIVEN
1841        let (context, _) = Context::new_for_test(4);
1842        let context = Arc::new(context);
1843        let block_verifier = Arc::new(NoopBlockVerifier {});
1844        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
1845        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
1846        let mock_client = Arc::new(MockNetworkClient::default());
1847        let store = Arc::new(MemStore::new());
1848        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
1849        let transaction_vote_tracker =
1850            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
1851        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1852
1853        // Create some test blocks
1854        let expected_blocks = (0..10)
1855            .map(|round| VerifiedBlock::new_for_test(TestBlock::new(round, 0).build()))
1856            .collect::<Vec<_>>();
1857        let missing_blocks = expected_blocks
1858            .iter()
1859            .map(|block| block.reference())
1860            .collect::<BTreeSet<_>>();
1861
1862        // AND stub the missing blocks
1863        core_dispatcher
1864            .stub_missing_blocks(missing_blocks.clone())
1865            .await;
1866
1867        // AND stub the requests for authority 1 & 2
1868        // Make the first authority timeout, so the second will be called. "We" are authority = 0, so
1869        // we are skipped anyways.
1870        mock_client
1871            .stub_fetch_blocks(
1872                expected_blocks.clone(),
1873                AuthorityIndex::new_for_test(1),
1874                Some(FETCH_REQUEST_TIMEOUT),
1875            )
1876            .await;
1877        mock_client
1878            .stub_fetch_blocks(
1879                expected_blocks.clone(),
1880                AuthorityIndex::new_for_test(2),
1881                None,
1882            )
1883            .await;
1884
1885        // WHEN start the synchronizer and wait for a couple of seconds
1886        let network_client = Arc::new(SynchronizerClient::new(
1887            context.clone(),
1888            Some(mock_client.clone()),
1889            Some(mock_client.clone()),
1890        ));
1891        let peers_pool = Arc::new(PeersPool::new(context.clone()));
1892        let _handle = Synchronizer::start(
1893            network_client,
1894            context.clone(),
1895            core_dispatcher.clone(),
1896            commit_vote_monitor,
1897            block_verifier,
1898            transaction_vote_tracker,
1899            round_tracker,
1900            dag_state,
1901            peers_pool.clone(),
1902            false,
1903        );
1904
1905        sleep(2 * FETCH_REQUEST_TIMEOUT).await;
1906
1907        // THEN the missing blocks should now be fetched and added to core
1908        let added_blocks = core_dispatcher.get_add_blocks().await;
1909        assert_eq!(added_blocks, expected_blocks);
1910
1911        // AND missing blocks should have been consumed by the stub
1912        assert!(
1913            core_dispatcher
1914                .get_missing_blocks()
1915                .await
1916                .unwrap()
1917                .is_empty()
1918        );
1919    }
1920
1921    #[tokio::test(flavor = "current_thread", start_paused = true)]
1922    async fn synchronizer_periodic_task_when_commit_lagging_gets_disabled() {
1923        // GIVEN
1924        let (context, _) = Context::new_for_test(4);
1925        let context = Arc::new(context);
1926        let block_verifier = Arc::new(NoopBlockVerifier {});
1927        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
1928        let mock_client = Arc::new(MockNetworkClient::default());
1929        let store = Arc::new(MemStore::new());
1930        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
1931        let transaction_vote_tracker =
1932            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
1933        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
1934        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1935
1936        // AND stub some missing blocks. The highest accepted round is 0. Create blocks that are above the sync threshold.
1937        let sync_missing_block_round_threshold = context.parameters.commit_sync_batch_size;
1938        let stub_blocks = (sync_missing_block_round_threshold * 2
1939            ..sync_missing_block_round_threshold * 3)
1940            .map(|round| VerifiedBlock::new_for_test(TestBlock::new(round, 0).build()))
1941            .collect::<Vec<_>>();
1942        let missing_blocks = stub_blocks
1943            .iter()
1944            .map(|block| block.reference())
1945            .collect::<BTreeSet<_>>();
1946        core_dispatcher
1947            .stub_missing_blocks(missing_blocks.clone())
1948            .await;
1949
1950        // AND stub the requests for authority 1 & 2
1951        // Make the first authority timeout, so the second will be called. "We" are authority = 0, so
1952        // we are skipped anyways. Stub all blocks since the full set is sent in one request.
1953        // Only the first max_blocks_per_sync blocks will be processed by process_fetched_blocks.
1954        mock_client
1955            .stub_fetch_blocks(
1956                stub_blocks.clone(),
1957                AuthorityIndex::new_for_test(1),
1958                Some(FETCH_REQUEST_TIMEOUT),
1959            )
1960            .await;
1961        mock_client
1962            .stub_fetch_blocks(stub_blocks.clone(), AuthorityIndex::new_for_test(2), None)
1963            .await;
1964        let mut expected_blocks = stub_blocks
1965            .iter()
1966            .take(context.parameters.max_blocks_per_sync)
1967            .cloned()
1968            .collect::<Vec<_>>();
1969
1970        // Now create some blocks to simulate a commit lag
1971        let round = context.parameters.commit_sync_batch_size * COMMIT_LAG_MULTIPLIER * 2;
1972        let commit_index: CommitIndex = round - 1;
1973        let blocks = (0..4)
1974            .map(|authority| {
1975                let commit_votes = vec![CommitVote::new(commit_index, CommitDigest::MIN)];
1976                let block = TestBlock::new(round, authority)
1977                    .set_commit_votes(commit_votes)
1978                    .build();
1979
1980                VerifiedBlock::new_for_test(block)
1981            })
1982            .collect::<Vec<_>>();
1983
1984        // Pass them through the commit vote monitor - so now there will be a big commit lag to prevent
1985        // the scheduled synchronizer from running
1986        for block in blocks {
1987            commit_vote_monitor.observe_block(&block);
1988        }
1989
1990        // WHEN start the synchronizer and wait for a couple of seconds where normally the synchronizer should have kicked in.
1991        let network_client = Arc::new(SynchronizerClient::new(
1992            context.clone(),
1993            Some(mock_client.clone()),
1994            Some(mock_client.clone()),
1995        ));
1996        let peers_pool = Arc::new(PeersPool::new(context.clone()));
1997        let _handle = Synchronizer::start(
1998            network_client,
1999            context.clone(),
2000            core_dispatcher.clone(),
2001            commit_vote_monitor.clone(),
2002            block_verifier,
2003            transaction_vote_tracker,
2004            round_tracker,
2005            dag_state.clone(),
2006            peers_pool.clone(),
2007            false,
2008        );
2009
2010        // Wait long enough for periodic sync to have run, but stay under COMMIT_PROGRESS_TIMEOUT
2011        // to avoid triggering commit sync failover.
2012        sleep(COMMIT_PROGRESS_TIMEOUT / 2).await;
2013
2014        // Since we should be in commit lag mode none of the missed blocks should have been fetched - hence nothing should be
2015        // sent to core for processing.
2016        let added_blocks = core_dispatcher.get_add_blocks().await;
2017        assert_eq!(added_blocks, vec![]);
2018
2019        // AND advance now the local commit index by adding a new commit that matches the commit index
2020        // of quorum
2021        {
2022            let mut d = dag_state.write();
2023            for index in 1..=commit_index {
2024                let commit =
2025                    TrustedCommit::new_for_test(index, CommitDigest::MIN, 0, BlockRef::MIN, vec![]);
2026
2027                d.add_commit(commit);
2028            }
2029
2030            assert_eq!(
2031                d.last_commit_index(),
2032                commit_vote_monitor.quorum_commit_index()
2033            );
2034        }
2035
2036        // Now stub again the missing blocks to fetch the exact same ones.
2037        core_dispatcher
2038            .stub_missing_blocks(missing_blocks.clone())
2039            .await;
2040
2041        sleep(2 * FETCH_REQUEST_TIMEOUT).await;
2042
2043        // THEN the missing blocks should now be fetched and added to core
2044        let mut added_blocks = core_dispatcher.get_add_blocks().await;
2045
2046        added_blocks.sort_by_key(|block| block.reference());
2047        expected_blocks.sort_by_key(|block| block.reference());
2048
2049        assert_eq!(added_blocks, expected_blocks);
2050    }
2051
2052    #[tokio::test(flavor = "current_thread", start_paused = true)]
2053    async fn synchronizer_periodic_sync_resumes_when_commit_sync_stalled() {
2054        // GIVEN
2055        let (context, _) = Context::new_for_test(4);
2056        let context = Arc::new(context);
2057        let block_verifier = Arc::new(NoopBlockVerifier {});
2058        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
2059        let mock_client = Arc::new(MockNetworkClient::default());
2060        let store = Arc::new(MemStore::new());
2061        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
2062        let transaction_vote_tracker =
2063            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
2064        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
2065        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
2066
2067        // AND create missing blocks to fetch
2068        let sync_missing_block_round_threshold = context.parameters.commit_sync_batch_size;
2069        let stub_blocks = (sync_missing_block_round_threshold * 2
2070            ..sync_missing_block_round_threshold * 3)
2071            .map(|round| VerifiedBlock::new_for_test(TestBlock::new(round, 0).build()))
2072            .collect::<Vec<_>>();
2073        let missing_blocks = stub_blocks
2074            .iter()
2075            .map(|block| block.reference())
2076            .collect::<BTreeSet<_>>();
2077        core_dispatcher
2078            .stub_missing_blocks(missing_blocks.clone())
2079            .await;
2080
2081        // AND stub the fetch responses for authority 1 & 2.
2082        // Stub all blocks since the full set is sent in one request.
2083        mock_client
2084            .stub_fetch_blocks(
2085                stub_blocks.clone(),
2086                AuthorityIndex::new_for_test(1),
2087                Some(FETCH_REQUEST_TIMEOUT),
2088            )
2089            .await;
2090        mock_client
2091            .stub_fetch_blocks(stub_blocks.clone(), AuthorityIndex::new_for_test(2), None)
2092            .await;
2093        let mut expected_blocks = stub_blocks
2094            .iter()
2095            .take(context.parameters.max_blocks_per_sync)
2096            .cloned()
2097            .collect::<Vec<_>>();
2098
2099        // AND create commit lag by observing high commit votes
2100        let round = context.parameters.commit_sync_batch_size * COMMIT_LAG_MULTIPLIER * 2;
2101        let commit_index: CommitIndex = round - 1;
2102        let blocks = (0..4)
2103            .map(|authority| {
2104                let commit_votes = vec![CommitVote::new(commit_index, CommitDigest::MIN)];
2105                let block = TestBlock::new(round, authority)
2106                    .set_commit_votes(commit_votes)
2107                    .build();
2108                VerifiedBlock::new_for_test(block)
2109            })
2110            .collect::<Vec<_>>();
2111        for block in blocks {
2112            commit_vote_monitor.observe_block(&block);
2113        }
2114
2115        // WHEN start the synchronizer
2116        let network_client = Arc::new(SynchronizerClient::new(
2117            context.clone(),
2118            Some(mock_client.clone()),
2119            Some(mock_client.clone()),
2120        ));
2121        let peers_pool = Arc::new(PeersPool::new(context.clone()));
2122        let _handle = Synchronizer::start(
2123            network_client,
2124            context.clone(),
2125            core_dispatcher.clone(),
2126            commit_vote_monitor.clone(),
2127            block_verifier,
2128            transaction_vote_tracker,
2129            round_tracker,
2130            dag_state.clone(),
2131            peers_pool.clone(),
2132            false,
2133        );
2134
2135        // Wait just under COMMIT_PROGRESS_TIMEOUT — sync should be skipped (commit lagging).
2136        sleep(COMMIT_PROGRESS_TIMEOUT - Duration::from_millis(100)).await;
2137        let added_blocks = core_dispatcher.get_add_blocks().await;
2138        assert_eq!(added_blocks, vec![]);
2139
2140        // Re-stub missing blocks (consumed by earlier get_missing_blocks calls).
2141        core_dispatcher
2142            .stub_missing_blocks(missing_blocks.clone())
2143            .await;
2144        // Stub the failover fetch (empty block_refs key, peer 1) before the stall triggers.
2145        // Use latency to prevent immediate completion and re-triggering.
2146        mock_client
2147            .stub_fetch_blocks_for_key(
2148                vec![],
2149                expected_blocks.clone(),
2150                AuthorityIndex::new_for_test(1),
2151                Some(Duration::from_millis(500)),
2152            )
2153            .await;
2154
2155        // Now sleep past the stall trigger + time for the fetch to complete.
2156        sleep(Duration::from_millis(200) + FETCH_REQUEST_TIMEOUT).await;
2157
2158        let mut added_blocks = core_dispatcher.get_add_blocks().await;
2159        assert!(
2160            !added_blocks.is_empty(),
2161            "Expected periodic sync to resume after commit sync stall"
2162        );
2163        added_blocks.sort_by_key(|block| block.reference());
2164        expected_blocks.sort_by_key(|block| block.reference());
2165        assert_eq!(added_blocks, expected_blocks);
2166
2167        // AND advance commit index enough to reach commit sync batch size from stall start.
2168        // This should resolve the failover and skip periodic sync again.
2169        core_dispatcher.get_add_blocks().await;
2170        {
2171            let current = dag_state.read().last_commit_index();
2172            let mut d = dag_state.write();
2173            // Advance well past the recovery threshold.
2174            for index in (current + 1)..=(current + context.parameters.commit_sync_batch_size) {
2175                let commit =
2176                    TrustedCommit::new_for_test(index, CommitDigest::MIN, 0, BlockRef::MIN, vec![]);
2177                d.add_commit(commit);
2178            }
2179        }
2180
2181        // Re-stub missing blocks and fetch response for another round of checking
2182        core_dispatcher
2183            .stub_missing_blocks(missing_blocks.clone())
2184            .await;
2185        mock_client
2186            .stub_fetch_blocks(
2187                stub_blocks
2188                    .iter()
2189                    .take(context.parameters.max_blocks_per_sync)
2190                    .cloned()
2191                    .collect::<Vec<_>>(),
2192                AuthorityIndex::new_for_test(2),
2193                None,
2194            )
2195            .await;
2196
2197        // Wait for a sync cycle — periodic sync should be skipped again since stall resolved
2198        // but commit is still lagging
2199        sleep(2 * FETCH_REQUEST_TIMEOUT).await;
2200        let added_blocks = core_dispatcher.get_add_blocks().await;
2201        assert_eq!(
2202            added_blocks,
2203            vec![],
2204            "Expected periodic sync to be skipped after stall resolved"
2205        );
2206    }
2207
2208    #[tokio::test(flavor = "current_thread", start_paused = true)]
2209    async fn synchronizer_fetch_own_last_block() {
2210        // GIVEN
2211        let (context, _) = Context::new_for_test(4);
2212        let context = Arc::new(context.with_parameters(Parameters {
2213            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
2214            ..Default::default()
2215        }));
2216        let block_verifier = Arc::new(NoopBlockVerifier {});
2217        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
2218        let mock_client = Arc::new(MockNetworkClient::default());
2219        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
2220        let store = Arc::new(MemStore::new());
2221        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
2222        let transaction_vote_tracker =
2223            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
2224        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
2225        let our_index = AuthorityIndex::new_for_test(0);
2226
2227        // Create some test blocks
2228        let mut expected_blocks = (9..=10)
2229            .map(|round| VerifiedBlock::new_for_test(TestBlock::new(round, 0).build()))
2230            .collect::<Vec<_>>();
2231
2232        // Now set different latest blocks for the peers
2233        // For peer 1 we give the block of round 10 (highest)
2234        let block_1 = expected_blocks.pop().unwrap();
2235        mock_client
2236            .stub_fetch_latest_blocks(
2237                vec![block_1.clone()],
2238                AuthorityIndex::new_for_test(1),
2239                vec![our_index],
2240                None,
2241            )
2242            .await;
2243        mock_client
2244            .stub_fetch_latest_blocks(
2245                vec![block_1],
2246                AuthorityIndex::new_for_test(1),
2247                vec![our_index],
2248                None,
2249            )
2250            .await;
2251
2252        // For peer 2 we give the block of round 9
2253        let block_2 = expected_blocks.pop().unwrap();
2254        mock_client
2255            .stub_fetch_latest_blocks(
2256                vec![block_2.clone()],
2257                AuthorityIndex::new_for_test(2),
2258                vec![our_index],
2259                Some(Duration::from_secs(10)),
2260            )
2261            .await;
2262        mock_client
2263            .stub_fetch_latest_blocks(
2264                vec![block_2],
2265                AuthorityIndex::new_for_test(2),
2266                vec![our_index],
2267                None,
2268            )
2269            .await;
2270
2271        // For peer 3 we don't give any block - and it should return an empty vector
2272        mock_client
2273            .stub_fetch_latest_blocks(
2274                vec![],
2275                AuthorityIndex::new_for_test(3),
2276                vec![our_index],
2277                Some(Duration::from_secs(10)),
2278            )
2279            .await;
2280        mock_client
2281            .stub_fetch_latest_blocks(
2282                vec![],
2283                AuthorityIndex::new_for_test(3),
2284                vec![our_index],
2285                None,
2286            )
2287            .await;
2288
2289        // WHEN start the synchronizer and wait for a couple of seconds
2290        let network_client = Arc::new(SynchronizerClient::new(
2291            context.clone(),
2292            Some(mock_client.clone()),
2293            Some(mock_client.clone()),
2294        ));
2295        let peers_pool = Arc::new(PeersPool::new(context.clone()));
2296        let handle = Synchronizer::start(
2297            network_client,
2298            context.clone(),
2299            core_dispatcher.clone(),
2300            commit_vote_monitor,
2301            block_verifier,
2302            transaction_vote_tracker,
2303            round_tracker,
2304            dag_state,
2305            peers_pool.clone(),
2306            true,
2307        );
2308
2309        // Wait at least for the timeout time
2310        sleep(context.parameters.sync_last_known_own_block_timeout * 2).await;
2311
2312        // Assert that core has been called to set the min propose round
2313        assert_eq!(
2314            core_dispatcher.get_last_own_proposed_round().await,
2315            vec![10]
2316        );
2317
2318        // Ensure that all the requests have been called
2319        assert_eq!(mock_client.fetch_latest_blocks_pending_calls().await, 0);
2320
2321        // And we got one retry
2322        assert_eq!(
2323            context
2324                .metrics
2325                .node_metrics
2326                .sync_last_known_own_block_retries
2327                .get(),
2328            1
2329        );
2330
2331        // Ensure that no panic occurred: stop() propagates panics from synchronizer tasks.
2332        handle.stop().await;
2333    }
2334
2335    #[tokio::test]
2336    async fn test_process_fetched_blocks() {
2337        // GIVEN
2338        let (context, _) = Context::new_for_test(4);
2339        let context = Arc::new(context);
2340        let block_verifier = Arc::new(NoopBlockVerifier {});
2341        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
2342        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
2343        let store = Arc::new(MemStore::new());
2344        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
2345        let transaction_vote_tracker =
2346            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
2347        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
2348        let (commands_sender, _commands_receiver) =
2349            monitored_mpsc::channel("consensus_synchronizer_commands", 1000);
2350
2351        // Create input test blocks:
2352        // - Authority 0 block at round 60.
2353        // - Authority 1 blocks from round 30 to 60.
2354        let mut expected_blocks = vec![VerifiedBlock::new_for_test(TestBlock::new(60, 0).build())];
2355        expected_blocks.extend(
2356            (30..=60).map(|round| VerifiedBlock::new_for_test(TestBlock::new(round, 1).build())),
2357        );
2358        assert_eq!(
2359            expected_blocks.len(),
2360            context.parameters.max_blocks_per_sync
2361        );
2362
2363        let expected_serialized_blocks = expected_blocks
2364            .iter()
2365            .map(|b| b.serialized().clone())
2366            .collect::<Vec<_>>();
2367
2368        let expected_block_refs = expected_blocks
2369            .iter()
2370            .map(|b| b.reference())
2371            .collect::<BTreeSet<_>>();
2372
2373        // GIVEN peer to fetch blocks from
2374        let peer_index = AuthorityIndex::new_for_test(2);
2375        let peer = PeerId::Validator(peer_index);
2376
2377        // Create blocks_guard
2378        let inflight_blocks_map = InflightBlocksMap::new();
2379        let blocks_guard = inflight_blocks_map
2380            .lock_blocks(expected_block_refs.clone(), peer.clone())
2381            .expect("Failed to lock blocks");
2382
2383        assert_eq!(
2384            inflight_blocks_map.num_of_locked_blocks(),
2385            expected_block_refs.len()
2386        );
2387
2388        // Create a Synchronizer
2389        let result = Synchronizer::<
2390            NoopBlockVerifier,
2391            MockCoreThreadDispatcher,
2392            MockNetworkClient,
2393            MockNetworkClient,
2394        >::process_fetched_blocks(
2395            expected_serialized_blocks,
2396            peer,
2397            blocks_guard, // The guard is consumed here
2398            core_dispatcher.clone(),
2399            block_verifier,
2400            transaction_vote_tracker,
2401            commit_vote_monitor,
2402            context.clone(),
2403            commands_sender,
2404            round_tracker,
2405            "test",
2406        )
2407        .await;
2408
2409        // THEN
2410        assert!(result.is_ok());
2411
2412        // Check blocks were sent to core
2413        let added_blocks = core_dispatcher.get_add_blocks().await;
2414        assert_eq!(
2415            added_blocks
2416                .iter()
2417                .map(|b| b.reference())
2418                .collect::<BTreeSet<_>>(),
2419            expected_block_refs,
2420        );
2421
2422        // Check blocks were unlocked
2423        assert_eq!(inflight_blocks_map.num_of_locked_blocks(), 0);
2424    }
2425}