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            _timeout: Duration,
1598        ) -> ConsensusResult<crate::network::ObserverBlockStream> {
1599            unimplemented!("stream_blocks not implemented in mock")
1600        }
1601
1602        async fn fetch_blocks(
1603            &self,
1604            _peer: crate::network::PeerId,
1605            _block_refs: Vec<BlockRef>,
1606            _fetch_after_rounds: Vec<Round>,
1607            _fetch_missing_ancestors: bool,
1608            _timeout: Duration,
1609        ) -> ConsensusResult<Vec<Bytes>> {
1610            unimplemented!("Observer fetch_blocks not implemented in mock")
1611        }
1612
1613        async fn fetch_commits(
1614            &self,
1615            _peer: crate::network::PeerId,
1616            _commit_range: crate::commit::CommitRange,
1617            _timeout: Duration,
1618        ) -> ConsensusResult<(Vec<Bytes>, Vec<Bytes>)> {
1619            unimplemented!("Observer fetch_commits not implemented in mock")
1620        }
1621    }
1622
1623    #[test]
1624    fn test_inflight_blocks_map() {
1625        // GIVEN
1626        let map = InflightBlocksMap::new();
1627        let some_block_refs = [
1628            BlockRef::new(1, AuthorityIndex::new_for_test(0), BlockDigest::MIN),
1629            BlockRef::new(10, AuthorityIndex::new_for_test(0), BlockDigest::MIN),
1630            BlockRef::new(12, AuthorityIndex::new_for_test(3), BlockDigest::MIN),
1631            BlockRef::new(15, AuthorityIndex::new_for_test(2), BlockDigest::MIN),
1632        ];
1633        let missing_block_refs = some_block_refs.iter().cloned().collect::<BTreeSet<_>>();
1634
1635        // Lock & unlock blocks
1636        {
1637            let mut all_guards = Vec::new();
1638
1639            // Try to acquire the block locks for authorities 1 & 2
1640            for i in 1..=2 {
1641                let authority = AuthorityIndex::new_for_test(i);
1642                let peer = PeerId::Validator(authority);
1643
1644                let guard = map.lock_blocks(missing_block_refs.clone(), peer.clone());
1645                let guard = guard.expect("Guard should be created");
1646                assert_eq!(guard.block_refs.len(), 4);
1647
1648                all_guards.push(guard);
1649
1650                // trying to acquire any of them again will not succeed
1651                let guard = map.lock_blocks(missing_block_refs.clone(), peer);
1652                assert!(guard.is_none());
1653            }
1654
1655            // Trying to acquire for authority 3 it will fail - as we have maxed out the number of allowed peers
1656            let authority_3 = AuthorityIndex::new_for_test(3);
1657            let peer_3 = PeerId::Validator(authority_3);
1658
1659            let guard = map.lock_blocks(missing_block_refs.clone(), peer_3.clone());
1660            assert!(guard.is_none());
1661
1662            // Explicitly drop the guard of authority 1 and try for authority 3 again - it will now succeed
1663            drop(all_guards.remove(0));
1664
1665            let guard = map.lock_blocks(missing_block_refs.clone(), peer_3);
1666            let guard = guard.expect("Guard should be successfully acquired");
1667
1668            assert_eq!(guard.block_refs, missing_block_refs);
1669
1670            // Dropping all guards should unlock on the block refs
1671            drop(guard);
1672            drop(all_guards);
1673
1674            assert_eq!(map.num_of_locked_blocks(), 0);
1675        }
1676
1677        // Swap locks
1678        {
1679            // acquire a lock for authority 1
1680            let authority_1 = AuthorityIndex::new_for_test(1);
1681            let peer_1 = PeerId::Validator(authority_1);
1682            let guard = map.lock_blocks(missing_block_refs.clone(), peer_1).unwrap();
1683
1684            // Now swap the locks for authority 2
1685            let authority_2 = AuthorityIndex::new_for_test(2);
1686            let peer_2 = PeerId::Validator(authority_2);
1687            let guard = map.swap_locks(guard, peer_2);
1688
1689            assert_eq!(guard.unwrap().block_refs, missing_block_refs);
1690        }
1691    }
1692
1693    #[tokio::test]
1694    async fn successful_fetch_blocks_from_peer() {
1695        // GIVEN
1696        let (context, _) = Context::new_for_test(4);
1697        let context = Arc::new(context);
1698        let block_verifier = Arc::new(NoopBlockVerifier {});
1699        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
1700        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
1701        let mock_client = Arc::new(MockNetworkClient::default());
1702        let store = Arc::new(MemStore::new());
1703        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
1704        let transaction_vote_tracker =
1705            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
1706        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1707
1708        let network_client = Arc::new(SynchronizerClient::new(
1709            context.clone(),
1710            Some(mock_client.clone()),
1711            Some(mock_client.clone()),
1712        ));
1713        let peers_pool = Arc::new(PeersPool::new(context.clone()));
1714        let handle = Synchronizer::start(
1715            network_client,
1716            context.clone(),
1717            core_dispatcher.clone(),
1718            commit_vote_monitor,
1719            block_verifier,
1720            transaction_vote_tracker,
1721            round_tracker,
1722            dag_state,
1723            peers_pool.clone(),
1724            false,
1725        );
1726
1727        // Create some test blocks
1728        let expected_blocks = (0..10)
1729            .map(|round| VerifiedBlock::new_for_test(TestBlock::new(round, 0).build()))
1730            .collect::<Vec<_>>();
1731        let missing_blocks = expected_blocks
1732            .iter()
1733            .map(|block| block.reference())
1734            .collect::<BTreeSet<_>>();
1735
1736        // AND stub the fetch_blocks request from peer 1
1737        let peer = AuthorityIndex::new_for_test(1);
1738        mock_client
1739            .stub_fetch_blocks(expected_blocks.clone(), peer, None)
1740            .await;
1741
1742        // WHEN request missing blocks from peer 1
1743        assert!(
1744            handle
1745                .fetch_blocks(missing_blocks, PeerId::Validator(peer))
1746                .await
1747                .is_ok()
1748        );
1749
1750        // Wait a little bit until those have been added in core
1751        sleep(Duration::from_millis(1_000)).await;
1752
1753        // THEN ensure those ended up in Core
1754        let added_blocks = core_dispatcher.get_add_blocks().await;
1755        assert_eq!(added_blocks, expected_blocks);
1756    }
1757
1758    #[tokio::test]
1759    async fn saturate_fetch_blocks_from_peer() {
1760        // GIVEN
1761        let (context, _) = Context::new_for_test(4);
1762        let context = Arc::new(context);
1763        let block_verifier = Arc::new(NoopBlockVerifier {});
1764        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
1765        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
1766        let mock_client = Arc::new(MockNetworkClient::default());
1767        let store = Arc::new(MemStore::new());
1768        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
1769        let transaction_vote_tracker =
1770            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
1771        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1772
1773        let network_client = Arc::new(SynchronizerClient::new(
1774            context.clone(),
1775            Some(mock_client.clone()),
1776            Some(mock_client.clone()),
1777        ));
1778        let peers_pool = Arc::new(PeersPool::new(context.clone()));
1779        let handle = Synchronizer::start(
1780            network_client,
1781            context.clone(),
1782            core_dispatcher.clone(),
1783            commit_vote_monitor,
1784            block_verifier,
1785            transaction_vote_tracker,
1786            round_tracker,
1787            dag_state,
1788            peers_pool.clone(),
1789            false,
1790        );
1791
1792        // Create some test blocks
1793        let expected_blocks = (0..=2 * FETCH_BLOCKS_CONCURRENCY)
1794            .map(|round| VerifiedBlock::new_for_test(TestBlock::new(round as Round, 0).build()))
1795            .collect::<Vec<_>>();
1796
1797        // Now start sending requests to fetch blocks by trying to saturate peer 1 task
1798        let peer = AuthorityIndex::new_for_test(1);
1799        let mut iter = expected_blocks.iter().peekable();
1800        while let Some(block) = iter.next() {
1801            // stub the fetch_blocks request from peer 1 and give some high response latency so requests
1802            // can start blocking the peer task.
1803            mock_client
1804                .stub_fetch_blocks(
1805                    vec![block.clone()],
1806                    peer,
1807                    Some(Duration::from_millis(5_000)),
1808                )
1809                .await;
1810
1811            let mut missing_blocks = BTreeSet::new();
1812            missing_blocks.insert(block.reference());
1813
1814            // WHEN requesting to fetch the blocks, it should not succeed for the last request and get
1815            // an error with "saturated" synchronizer
1816            if iter.peek().is_none() {
1817                match handle
1818                    .fetch_blocks(missing_blocks, PeerId::Validator(peer))
1819                    .await
1820                {
1821                    Err(ConsensusError::SynchronizerSaturated(peer_str)) => {
1822                        assert_eq!(peer_str, format!("{:?}", PeerId::Validator(peer)));
1823                    }
1824                    _ => panic!("A saturated synchronizer error was expected"),
1825                }
1826            } else {
1827                assert!(
1828                    handle
1829                        .fetch_blocks(missing_blocks, PeerId::Validator(peer))
1830                        .await
1831                        .is_ok()
1832                );
1833            }
1834        }
1835    }
1836
1837    #[tokio::test(flavor = "current_thread", start_paused = true)]
1838    async fn synchronizer_periodic_task_fetch_blocks() {
1839        // GIVEN
1840        let (context, _) = Context::new_for_test(4);
1841        let context = Arc::new(context);
1842        let block_verifier = Arc::new(NoopBlockVerifier {});
1843        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
1844        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
1845        let mock_client = Arc::new(MockNetworkClient::default());
1846        let store = Arc::new(MemStore::new());
1847        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
1848        let transaction_vote_tracker =
1849            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
1850        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1851
1852        // Create some test blocks
1853        let expected_blocks = (0..10)
1854            .map(|round| VerifiedBlock::new_for_test(TestBlock::new(round, 0).build()))
1855            .collect::<Vec<_>>();
1856        let missing_blocks = expected_blocks
1857            .iter()
1858            .map(|block| block.reference())
1859            .collect::<BTreeSet<_>>();
1860
1861        // AND stub the missing blocks
1862        core_dispatcher
1863            .stub_missing_blocks(missing_blocks.clone())
1864            .await;
1865
1866        // AND stub the requests for authority 1 & 2
1867        // Make the first authority timeout, so the second will be called. "We" are authority = 0, so
1868        // we are skipped anyways.
1869        mock_client
1870            .stub_fetch_blocks(
1871                expected_blocks.clone(),
1872                AuthorityIndex::new_for_test(1),
1873                Some(FETCH_REQUEST_TIMEOUT),
1874            )
1875            .await;
1876        mock_client
1877            .stub_fetch_blocks(
1878                expected_blocks.clone(),
1879                AuthorityIndex::new_for_test(2),
1880                None,
1881            )
1882            .await;
1883
1884        // WHEN start the synchronizer and wait for a couple of seconds
1885        let network_client = Arc::new(SynchronizerClient::new(
1886            context.clone(),
1887            Some(mock_client.clone()),
1888            Some(mock_client.clone()),
1889        ));
1890        let peers_pool = Arc::new(PeersPool::new(context.clone()));
1891        let _handle = Synchronizer::start(
1892            network_client,
1893            context.clone(),
1894            core_dispatcher.clone(),
1895            commit_vote_monitor,
1896            block_verifier,
1897            transaction_vote_tracker,
1898            round_tracker,
1899            dag_state,
1900            peers_pool.clone(),
1901            false,
1902        );
1903
1904        sleep(2 * FETCH_REQUEST_TIMEOUT).await;
1905
1906        // THEN the missing blocks should now be fetched and added to core
1907        let added_blocks = core_dispatcher.get_add_blocks().await;
1908        assert_eq!(added_blocks, expected_blocks);
1909
1910        // AND missing blocks should have been consumed by the stub
1911        assert!(
1912            core_dispatcher
1913                .get_missing_blocks()
1914                .await
1915                .unwrap()
1916                .is_empty()
1917        );
1918    }
1919
1920    #[tokio::test(flavor = "current_thread", start_paused = true)]
1921    async fn synchronizer_periodic_task_when_commit_lagging_gets_disabled() {
1922        // GIVEN
1923        let (context, _) = Context::new_for_test(4);
1924        let context = Arc::new(context);
1925        let block_verifier = Arc::new(NoopBlockVerifier {});
1926        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
1927        let mock_client = Arc::new(MockNetworkClient::default());
1928        let store = Arc::new(MemStore::new());
1929        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
1930        let transaction_vote_tracker =
1931            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
1932        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
1933        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1934
1935        // AND stub some missing blocks. The highest accepted round is 0. Create blocks that are above the sync threshold.
1936        let sync_missing_block_round_threshold = context.parameters.commit_sync_batch_size;
1937        let stub_blocks = (sync_missing_block_round_threshold * 2
1938            ..sync_missing_block_round_threshold * 3)
1939            .map(|round| VerifiedBlock::new_for_test(TestBlock::new(round, 0).build()))
1940            .collect::<Vec<_>>();
1941        let missing_blocks = stub_blocks
1942            .iter()
1943            .map(|block| block.reference())
1944            .collect::<BTreeSet<_>>();
1945        core_dispatcher
1946            .stub_missing_blocks(missing_blocks.clone())
1947            .await;
1948
1949        // AND stub the requests for authority 1 & 2
1950        // Make the first authority timeout, so the second will be called. "We" are authority = 0, so
1951        // we are skipped anyways. Stub all blocks since the full set is sent in one request.
1952        // Only the first max_blocks_per_sync blocks will be processed by process_fetched_blocks.
1953        mock_client
1954            .stub_fetch_blocks(
1955                stub_blocks.clone(),
1956                AuthorityIndex::new_for_test(1),
1957                Some(FETCH_REQUEST_TIMEOUT),
1958            )
1959            .await;
1960        mock_client
1961            .stub_fetch_blocks(stub_blocks.clone(), AuthorityIndex::new_for_test(2), None)
1962            .await;
1963        let mut expected_blocks = stub_blocks
1964            .iter()
1965            .take(context.parameters.max_blocks_per_sync)
1966            .cloned()
1967            .collect::<Vec<_>>();
1968
1969        // Now create some blocks to simulate a commit lag
1970        let round = context.parameters.commit_sync_batch_size * COMMIT_LAG_MULTIPLIER * 2;
1971        let commit_index: CommitIndex = round - 1;
1972        let blocks = (0..4)
1973            .map(|authority| {
1974                let commit_votes = vec![CommitVote::new(commit_index, CommitDigest::MIN)];
1975                let block = TestBlock::new(round, authority)
1976                    .set_commit_votes(commit_votes)
1977                    .build();
1978
1979                VerifiedBlock::new_for_test(block)
1980            })
1981            .collect::<Vec<_>>();
1982
1983        // Pass them through the commit vote monitor - so now there will be a big commit lag to prevent
1984        // the scheduled synchronizer from running
1985        for block in blocks {
1986            commit_vote_monitor.observe_block(&block);
1987        }
1988
1989        // WHEN start the synchronizer and wait for a couple of seconds where normally the synchronizer should have kicked in.
1990        let network_client = Arc::new(SynchronizerClient::new(
1991            context.clone(),
1992            Some(mock_client.clone()),
1993            Some(mock_client.clone()),
1994        ));
1995        let peers_pool = Arc::new(PeersPool::new(context.clone()));
1996        let _handle = Synchronizer::start(
1997            network_client,
1998            context.clone(),
1999            core_dispatcher.clone(),
2000            commit_vote_monitor.clone(),
2001            block_verifier,
2002            transaction_vote_tracker,
2003            round_tracker,
2004            dag_state.clone(),
2005            peers_pool.clone(),
2006            false,
2007        );
2008
2009        // Wait long enough for periodic sync to have run, but stay under COMMIT_PROGRESS_TIMEOUT
2010        // to avoid triggering commit sync failover.
2011        sleep(COMMIT_PROGRESS_TIMEOUT / 2).await;
2012
2013        // Since we should be in commit lag mode none of the missed blocks should have been fetched - hence nothing should be
2014        // sent to core for processing.
2015        let added_blocks = core_dispatcher.get_add_blocks().await;
2016        assert_eq!(added_blocks, vec![]);
2017
2018        // AND advance now the local commit index by adding a new commit that matches the commit index
2019        // of quorum
2020        {
2021            let mut d = dag_state.write();
2022            for index in 1..=commit_index {
2023                let commit =
2024                    TrustedCommit::new_for_test(index, CommitDigest::MIN, 0, BlockRef::MIN, vec![]);
2025
2026                d.add_commit(commit);
2027            }
2028
2029            assert_eq!(
2030                d.last_commit_index(),
2031                commit_vote_monitor.quorum_commit_index()
2032            );
2033        }
2034
2035        // Now stub again the missing blocks to fetch the exact same ones.
2036        core_dispatcher
2037            .stub_missing_blocks(missing_blocks.clone())
2038            .await;
2039
2040        sleep(2 * FETCH_REQUEST_TIMEOUT).await;
2041
2042        // THEN the missing blocks should now be fetched and added to core
2043        let mut added_blocks = core_dispatcher.get_add_blocks().await;
2044
2045        added_blocks.sort_by_key(|block| block.reference());
2046        expected_blocks.sort_by_key(|block| block.reference());
2047
2048        assert_eq!(added_blocks, expected_blocks);
2049    }
2050
2051    #[tokio::test(flavor = "current_thread", start_paused = true)]
2052    async fn synchronizer_periodic_sync_resumes_when_commit_sync_stalled() {
2053        // GIVEN
2054        let (context, _) = Context::new_for_test(4);
2055        let context = Arc::new(context);
2056        let block_verifier = Arc::new(NoopBlockVerifier {});
2057        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
2058        let mock_client = Arc::new(MockNetworkClient::default());
2059        let store = Arc::new(MemStore::new());
2060        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
2061        let transaction_vote_tracker =
2062            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
2063        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
2064        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
2065
2066        // AND create missing blocks to fetch
2067        let sync_missing_block_round_threshold = context.parameters.commit_sync_batch_size;
2068        let stub_blocks = (sync_missing_block_round_threshold * 2
2069            ..sync_missing_block_round_threshold * 3)
2070            .map(|round| VerifiedBlock::new_for_test(TestBlock::new(round, 0).build()))
2071            .collect::<Vec<_>>();
2072        let missing_blocks = stub_blocks
2073            .iter()
2074            .map(|block| block.reference())
2075            .collect::<BTreeSet<_>>();
2076        core_dispatcher
2077            .stub_missing_blocks(missing_blocks.clone())
2078            .await;
2079
2080        // AND stub the fetch responses for authority 1 & 2.
2081        // Stub all blocks since the full set is sent in one request.
2082        mock_client
2083            .stub_fetch_blocks(
2084                stub_blocks.clone(),
2085                AuthorityIndex::new_for_test(1),
2086                Some(FETCH_REQUEST_TIMEOUT),
2087            )
2088            .await;
2089        mock_client
2090            .stub_fetch_blocks(stub_blocks.clone(), AuthorityIndex::new_for_test(2), None)
2091            .await;
2092        let mut expected_blocks = stub_blocks
2093            .iter()
2094            .take(context.parameters.max_blocks_per_sync)
2095            .cloned()
2096            .collect::<Vec<_>>();
2097
2098        // AND create commit lag by observing high commit votes
2099        let round = context.parameters.commit_sync_batch_size * COMMIT_LAG_MULTIPLIER * 2;
2100        let commit_index: CommitIndex = round - 1;
2101        let blocks = (0..4)
2102            .map(|authority| {
2103                let commit_votes = vec![CommitVote::new(commit_index, CommitDigest::MIN)];
2104                let block = TestBlock::new(round, authority)
2105                    .set_commit_votes(commit_votes)
2106                    .build();
2107                VerifiedBlock::new_for_test(block)
2108            })
2109            .collect::<Vec<_>>();
2110        for block in blocks {
2111            commit_vote_monitor.observe_block(&block);
2112        }
2113
2114        // WHEN start the synchronizer
2115        let network_client = Arc::new(SynchronizerClient::new(
2116            context.clone(),
2117            Some(mock_client.clone()),
2118            Some(mock_client.clone()),
2119        ));
2120        let peers_pool = Arc::new(PeersPool::new(context.clone()));
2121        let _handle = Synchronizer::start(
2122            network_client,
2123            context.clone(),
2124            core_dispatcher.clone(),
2125            commit_vote_monitor.clone(),
2126            block_verifier,
2127            transaction_vote_tracker,
2128            round_tracker,
2129            dag_state.clone(),
2130            peers_pool.clone(),
2131            false,
2132        );
2133
2134        // Wait just under COMMIT_PROGRESS_TIMEOUT — sync should be skipped (commit lagging).
2135        sleep(COMMIT_PROGRESS_TIMEOUT - Duration::from_millis(100)).await;
2136        let added_blocks = core_dispatcher.get_add_blocks().await;
2137        assert_eq!(added_blocks, vec![]);
2138
2139        // Re-stub missing blocks (consumed by earlier get_missing_blocks calls).
2140        core_dispatcher
2141            .stub_missing_blocks(missing_blocks.clone())
2142            .await;
2143        // Stub the failover fetch (empty block_refs key, peer 1) before the stall triggers.
2144        // Use latency to prevent immediate completion and re-triggering.
2145        mock_client
2146            .stub_fetch_blocks_for_key(
2147                vec![],
2148                expected_blocks.clone(),
2149                AuthorityIndex::new_for_test(1),
2150                Some(Duration::from_millis(500)),
2151            )
2152            .await;
2153
2154        // Now sleep past the stall trigger + time for the fetch to complete.
2155        sleep(Duration::from_millis(200) + FETCH_REQUEST_TIMEOUT).await;
2156
2157        let mut added_blocks = core_dispatcher.get_add_blocks().await;
2158        assert!(
2159            !added_blocks.is_empty(),
2160            "Expected periodic sync to resume after commit sync stall"
2161        );
2162        added_blocks.sort_by_key(|block| block.reference());
2163        expected_blocks.sort_by_key(|block| block.reference());
2164        assert_eq!(added_blocks, expected_blocks);
2165
2166        // AND advance commit index enough to reach commit sync batch size from stall start.
2167        // This should resolve the failover and skip periodic sync again.
2168        core_dispatcher.get_add_blocks().await;
2169        {
2170            let current = dag_state.read().last_commit_index();
2171            let mut d = dag_state.write();
2172            // Advance well past the recovery threshold.
2173            for index in (current + 1)..=(current + context.parameters.commit_sync_batch_size) {
2174                let commit =
2175                    TrustedCommit::new_for_test(index, CommitDigest::MIN, 0, BlockRef::MIN, vec![]);
2176                d.add_commit(commit);
2177            }
2178        }
2179
2180        // Re-stub missing blocks and fetch response for another round of checking
2181        core_dispatcher
2182            .stub_missing_blocks(missing_blocks.clone())
2183            .await;
2184        mock_client
2185            .stub_fetch_blocks(
2186                stub_blocks
2187                    .iter()
2188                    .take(context.parameters.max_blocks_per_sync)
2189                    .cloned()
2190                    .collect::<Vec<_>>(),
2191                AuthorityIndex::new_for_test(2),
2192                None,
2193            )
2194            .await;
2195
2196        // Wait for a sync cycle — periodic sync should be skipped again since stall resolved
2197        // but commit is still lagging
2198        sleep(2 * FETCH_REQUEST_TIMEOUT).await;
2199        let added_blocks = core_dispatcher.get_add_blocks().await;
2200        assert_eq!(
2201            added_blocks,
2202            vec![],
2203            "Expected periodic sync to be skipped after stall resolved"
2204        );
2205    }
2206
2207    #[tokio::test(flavor = "current_thread", start_paused = true)]
2208    async fn synchronizer_fetch_own_last_block() {
2209        // GIVEN
2210        let (context, _) = Context::new_for_test(4);
2211        let context = Arc::new(context.with_parameters(Parameters {
2212            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
2213            ..Default::default()
2214        }));
2215        let block_verifier = Arc::new(NoopBlockVerifier {});
2216        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
2217        let mock_client = Arc::new(MockNetworkClient::default());
2218        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
2219        let store = Arc::new(MemStore::new());
2220        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
2221        let transaction_vote_tracker =
2222            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
2223        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
2224        let our_index = AuthorityIndex::new_for_test(0);
2225
2226        // Create some test blocks
2227        let mut expected_blocks = (9..=10)
2228            .map(|round| VerifiedBlock::new_for_test(TestBlock::new(round, 0).build()))
2229            .collect::<Vec<_>>();
2230
2231        // Now set different latest blocks for the peers
2232        // For peer 1 we give the block of round 10 (highest)
2233        let block_1 = expected_blocks.pop().unwrap();
2234        mock_client
2235            .stub_fetch_latest_blocks(
2236                vec![block_1.clone()],
2237                AuthorityIndex::new_for_test(1),
2238                vec![our_index],
2239                None,
2240            )
2241            .await;
2242        mock_client
2243            .stub_fetch_latest_blocks(
2244                vec![block_1],
2245                AuthorityIndex::new_for_test(1),
2246                vec![our_index],
2247                None,
2248            )
2249            .await;
2250
2251        // For peer 2 we give the block of round 9
2252        let block_2 = expected_blocks.pop().unwrap();
2253        mock_client
2254            .stub_fetch_latest_blocks(
2255                vec![block_2.clone()],
2256                AuthorityIndex::new_for_test(2),
2257                vec![our_index],
2258                Some(Duration::from_secs(10)),
2259            )
2260            .await;
2261        mock_client
2262            .stub_fetch_latest_blocks(
2263                vec![block_2],
2264                AuthorityIndex::new_for_test(2),
2265                vec![our_index],
2266                None,
2267            )
2268            .await;
2269
2270        // For peer 3 we don't give any block - and it should return an empty vector
2271        mock_client
2272            .stub_fetch_latest_blocks(
2273                vec![],
2274                AuthorityIndex::new_for_test(3),
2275                vec![our_index],
2276                Some(Duration::from_secs(10)),
2277            )
2278            .await;
2279        mock_client
2280            .stub_fetch_latest_blocks(
2281                vec![],
2282                AuthorityIndex::new_for_test(3),
2283                vec![our_index],
2284                None,
2285            )
2286            .await;
2287
2288        // WHEN start the synchronizer and wait for a couple of seconds
2289        let network_client = Arc::new(SynchronizerClient::new(
2290            context.clone(),
2291            Some(mock_client.clone()),
2292            Some(mock_client.clone()),
2293        ));
2294        let peers_pool = Arc::new(PeersPool::new(context.clone()));
2295        let handle = Synchronizer::start(
2296            network_client,
2297            context.clone(),
2298            core_dispatcher.clone(),
2299            commit_vote_monitor,
2300            block_verifier,
2301            transaction_vote_tracker,
2302            round_tracker,
2303            dag_state,
2304            peers_pool.clone(),
2305            true,
2306        );
2307
2308        // Wait at least for the timeout time
2309        sleep(context.parameters.sync_last_known_own_block_timeout * 2).await;
2310
2311        // Assert that core has been called to set the min propose round
2312        assert_eq!(
2313            core_dispatcher.get_last_own_proposed_round().await,
2314            vec![10]
2315        );
2316
2317        // Ensure that all the requests have been called
2318        assert_eq!(mock_client.fetch_latest_blocks_pending_calls().await, 0);
2319
2320        // And we got one retry
2321        assert_eq!(
2322            context
2323                .metrics
2324                .node_metrics
2325                .sync_last_known_own_block_retries
2326                .get(),
2327            1
2328        );
2329
2330        // Ensure that no panic occurred: stop() propagates panics from synchronizer tasks.
2331        handle.stop().await;
2332    }
2333
2334    #[tokio::test]
2335    async fn test_process_fetched_blocks() {
2336        // GIVEN
2337        let (context, _) = Context::new_for_test(4);
2338        let context = Arc::new(context);
2339        let block_verifier = Arc::new(NoopBlockVerifier {});
2340        let core_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
2341        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
2342        let store = Arc::new(MemStore::new());
2343        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
2344        let transaction_vote_tracker =
2345            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
2346        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
2347        let (commands_sender, _commands_receiver) =
2348            monitored_mpsc::channel("consensus_synchronizer_commands", 1000);
2349
2350        // Create input test blocks:
2351        // - Authority 0 block at round 60.
2352        // - Authority 1 blocks from round 30 to 60.
2353        let mut expected_blocks = vec![VerifiedBlock::new_for_test(TestBlock::new(60, 0).build())];
2354        expected_blocks.extend(
2355            (30..=60).map(|round| VerifiedBlock::new_for_test(TestBlock::new(round, 1).build())),
2356        );
2357        assert_eq!(
2358            expected_blocks.len(),
2359            context.parameters.max_blocks_per_sync
2360        );
2361
2362        let expected_serialized_blocks = expected_blocks
2363            .iter()
2364            .map(|b| b.serialized().clone())
2365            .collect::<Vec<_>>();
2366
2367        let expected_block_refs = expected_blocks
2368            .iter()
2369            .map(|b| b.reference())
2370            .collect::<BTreeSet<_>>();
2371
2372        // GIVEN peer to fetch blocks from
2373        let peer_index = AuthorityIndex::new_for_test(2);
2374        let peer = PeerId::Validator(peer_index);
2375
2376        // Create blocks_guard
2377        let inflight_blocks_map = InflightBlocksMap::new();
2378        let blocks_guard = inflight_blocks_map
2379            .lock_blocks(expected_block_refs.clone(), peer.clone())
2380            .expect("Failed to lock blocks");
2381
2382        assert_eq!(
2383            inflight_blocks_map.num_of_locked_blocks(),
2384            expected_block_refs.len()
2385        );
2386
2387        // Create a Synchronizer
2388        let result = Synchronizer::<
2389            NoopBlockVerifier,
2390            MockCoreThreadDispatcher,
2391            MockNetworkClient,
2392            MockNetworkClient,
2393        >::process_fetched_blocks(
2394            expected_serialized_blocks,
2395            peer,
2396            blocks_guard, // The guard is consumed here
2397            core_dispatcher.clone(),
2398            block_verifier,
2399            transaction_vote_tracker,
2400            commit_vote_monitor,
2401            context.clone(),
2402            commands_sender,
2403            round_tracker,
2404            "test",
2405        )
2406        .await;
2407
2408        // THEN
2409        assert!(result.is_ok());
2410
2411        // Check blocks were sent to core
2412        let added_blocks = core_dispatcher.get_add_blocks().await;
2413        assert_eq!(
2414            added_blocks
2415                .iter()
2416                .map(|b| b.reference())
2417                .collect::<BTreeSet<_>>(),
2418            expected_block_refs,
2419        );
2420
2421        // Check blocks were unlocked
2422        assert_eq!(inflight_blocks_map.num_of_locked_blocks(), 0);
2423    }
2424}