Skip to main content

consensus_core/
commit_syncer.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! CommitSyncer implements efficient synchronization of committed data.
5//!
6//! During the operation of a committee of authorities for consensus, one or more authorities
7//! can fall behind the quorum in their received and accepted blocks. This can happen due to
8//! network disruptions, host crash, or other reasons. Authorities fell behind need to catch up to
9//! the quorum to be able to vote on the latest leaders. So efficient synchronization is necessary
10//! to minimize the impact of temporary disruptions and maintain smooth operations of the network.
11//!  
12//! CommitSyncer achieves efficient synchronization by relying on the following: when blocks
13//! are included in commits with >= 2f+1 certifiers by stake, these blocks must have passed
14//! verifications on some honest validators, so re-verifying them is unnecessary. In fact, the
15//! quorum certified commits themselves can be trusted to be sent to Sui directly, but for
16//! simplicity this is not done. Blocks from trusted commits still go through Core and committer.
17//!
18//! Another way CommitSyncer improves the efficiency of synchronization is parallel fetching:
19//! commits have a simple dependency graph (linear), so it is easy to fetch ranges of commits
20//! in parallel.
21//!
22//! Commit synchronization is an expensive operation, involving transferring large amount of data via
23//! the network. And it is not on the critical path of block processing. So the heuristics for
24//! synchronization, including triggers and retries, should be chosen to favor throughput and
25//! efficient resource usage, over faster reactions.
26
27use std::{
28    collections::{BTreeMap, BTreeSet},
29    sync::Arc,
30    time::Duration,
31};
32
33use bytes::Bytes;
34use consensus_types::block::{BlockRef, TransactionIndex};
35use futures::{StreamExt as _, stream::FuturesOrdered};
36use itertools::Itertools as _;
37use mysten_common::ZipDebugEqIteratorExt;
38use mysten_metrics::spawn_logged_monitored_task;
39use parking_lot::RwLock;
40use rand::{prelude::SliceRandom as _, rngs::ThreadRng};
41use tokio::{
42    sync::oneshot,
43    task::{JoinHandle, JoinSet},
44    time::{MissedTickBehavior, sleep},
45};
46use tracing::{debug, info, warn};
47
48use crate::{
49    CommitConsumerMonitor, CommitIndex,
50    block::{BlockAPI, ExtendedBlock, SignedBlock, VerifiedBlock},
51    block_verifier::BlockVerifier,
52    commit::{
53        CertifiedCommit, CertifiedCommits, Commit, CommitAPI as _, CommitDigest, CommitRange,
54        CommitRef, TrustedCommit,
55    },
56    commit_vote_monitor::CommitVoteMonitor,
57    context::Context,
58    core_thread::CoreThreadDispatcher,
59    dag_state::DagState,
60    error::{ConsensusError, ConsensusResult},
61    network::{CommitSyncerClient, ObserverNetworkClient, PeerId, ValidatorNetworkClient},
62    peers_pool::PeersPool,
63    round_tracker::RoundTracker,
64    stake_aggregator::{QuorumThreshold, StakeAggregator},
65    task::{join_and_propagate_panic, shutdown_join_set, spawn_blocking},
66    transaction_vote_tracker::TransactionVoteTracker,
67};
68
69// Handle to stop the CommitSyncer loop.
70pub(crate) struct CommitSyncerHandle {
71    schedule_task: JoinHandle<()>,
72    tx_shutdown: oneshot::Sender<()>,
73}
74
75impl CommitSyncerHandle {
76    pub(crate) async fn stop(self) {
77        let _ = self.tx_shutdown.send(());
78        // Do not abort schedule task, which waits for fetches to shut down.
79        join_and_propagate_panic(self.schedule_task).await;
80    }
81}
82
83pub(crate) struct CommitSyncer<VC: ValidatorNetworkClient, OC: ObserverNetworkClient> {
84    // States shared by scheduler and fetch tasks.
85
86    // Shared components wrapper.
87    inner: Arc<Inner<VC, OC>>,
88
89    // States only used by the scheduler.
90
91    // Inflight requests to fetch commits from different authorities.
92    inflight_fetches: JoinSet<(u32, CertifiedCommits)>,
93    // Additional ranges of commits to fetch.
94    pending_fetches: BTreeSet<CommitRange>,
95    // Fetched commits and blocks by commit range.
96    fetched_ranges: BTreeMap<CommitRange, CertifiedCommits>,
97    // Highest commit index among inflight and pending fetches.
98    // Used to determine the start of new ranges to be fetched.
99    highest_scheduled_index: Option<CommitIndex>,
100    // Highest index among fetched commits, after commits and blocks are verified.
101    // Used for metrics.
102    highest_fetched_commit_index: CommitIndex,
103    // The commit index that is the max of highest local commit index and commit index inflight to Core.
104    // Used to determine if fetched blocks can be sent to Core without gaps.
105    synced_commit_index: CommitIndex,
106}
107
108impl<VC, OC> CommitSyncer<VC, OC>
109where
110    VC: ValidatorNetworkClient,
111    OC: ObserverNetworkClient,
112{
113    pub(crate) fn new(
114        context: Arc<Context>,
115        core_thread_dispatcher: Arc<dyn CoreThreadDispatcher>,
116        commit_vote_monitor: Arc<CommitVoteMonitor>,
117        commit_consumer_monitor: Arc<CommitConsumerMonitor>,
118        block_verifier: Arc<dyn BlockVerifier>,
119        transaction_vote_tracker: TransactionVoteTracker,
120        round_tracker: Arc<RwLock<RoundTracker>>,
121        network_client: Arc<CommitSyncerClient<VC, OC>>,
122        dag_state: Arc<RwLock<DagState>>,
123        peers_pool: Arc<PeersPool>,
124    ) -> Self {
125        let inner = Arc::new(Inner {
126            context,
127            core_thread_dispatcher,
128            commit_vote_monitor,
129            commit_consumer_monitor,
130            block_verifier,
131            transaction_vote_tracker,
132            round_tracker,
133            network_client,
134            dag_state,
135            peers_pool,
136        });
137        let synced_commit_index = inner.dag_state.read().last_commit_index();
138        CommitSyncer {
139            inner,
140            inflight_fetches: JoinSet::new(),
141            pending_fetches: BTreeSet::new(),
142            fetched_ranges: BTreeMap::new(),
143            highest_scheduled_index: None,
144            highest_fetched_commit_index: 0,
145            synced_commit_index,
146        }
147    }
148
149    pub(crate) fn start(self) -> CommitSyncerHandle {
150        let (tx_shutdown, rx_shutdown) = oneshot::channel();
151        let schedule_task = spawn_logged_monitored_task!(self.schedule_loop(rx_shutdown,));
152        CommitSyncerHandle {
153            schedule_task,
154            tx_shutdown,
155        }
156    }
157
158    async fn schedule_loop(mut self, mut rx_shutdown: oneshot::Receiver<()>) {
159        let mut interval = tokio::time::interval(Duration::from_secs(2));
160        interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
161
162        loop {
163            tokio::select! {
164                // Periodically, schedule new fetches if the node is falling behind.
165                _ = interval.tick() => {
166                    self.try_schedule_once();
167                }
168                // Handles results from fetch tasks.
169                Some(result) = self.inflight_fetches.join_next(), if !self.inflight_fetches.is_empty() => {
170                    if let Err(e) = result {
171                        if e.is_panic() {
172                            std::panic::resume_unwind(e.into_panic());
173                        }
174                        warn!("Fetch cancelled. CommitSyncer shutting down: {}", e);
175                        // If any fetch is cancelled or panicked, try to shutdown and exit the loop.
176                        shutdown_join_set(&mut self.inflight_fetches).await;
177                        return;
178                    }
179                    let (target_end, commits) = result.unwrap();
180                    self.handle_fetch_result(target_end, commits).await;
181                }
182                _ = &mut rx_shutdown => {
183                    // Shutdown requested.
184                    info!("CommitSyncer shutting down ...");
185                    shutdown_join_set(&mut self.inflight_fetches).await;
186                    return;
187                }
188            }
189
190            self.try_start_fetches();
191        }
192    }
193
194    fn try_schedule_once(&mut self) {
195        let quorum_commit_index = self.inner.commit_vote_monitor.quorum_commit_index();
196        let local_commit_index = self.inner.dag_state.read().last_commit_index();
197        let metrics = &self.inner.context.metrics.node_metrics;
198        metrics
199            .commit_sync_quorum_index
200            .set(quorum_commit_index as i64);
201        metrics
202            .commit_sync_local_index
203            .set(local_commit_index as i64);
204        let highest_handled_index = self.inner.commit_consumer_monitor.highest_handled_commit();
205        let highest_scheduled_index = self.highest_scheduled_index.unwrap_or(0);
206        // Update synced_commit_index periodically to make sure it is no smaller than
207        // local commit index.
208        self.synced_commit_index = self.synced_commit_index.max(local_commit_index);
209        let unhandled_commits_threshold = self.unhandled_commits_threshold();
210        info!(
211            "Checking to schedule fetches: synced_commit_index={}, highest_handled_index={}, highest_scheduled_index={}, quorum_commit_index={}, unhandled_commits_threshold={}",
212            self.synced_commit_index,
213            highest_handled_index,
214            highest_scheduled_index,
215            quorum_commit_index,
216            unhandled_commits_threshold,
217        );
218
219        // TODO: cleanup inflight fetches that are no longer needed.
220        let fetch_after_index = self
221            .synced_commit_index
222            .max(self.highest_scheduled_index.unwrap_or(0));
223        // When the node is falling behind, schedule pending fetches which will be executed on later.
224        for prev_end in (fetch_after_index..=quorum_commit_index)
225            .step_by(self.inner.context.parameters.commit_sync_batch_size as usize)
226        {
227            // Create range with inclusive start and end.
228            let range_start = prev_end + 1;
229            let range_end = prev_end + self.inner.context.parameters.commit_sync_batch_size;
230            // Commit range is not fetched when [range_start, range_end] contains less number of commits
231            // than the target batch size. This is to avoid the cost of processing more and smaller batches.
232            // Block broadcast, subscription and synchronization will help the node catchup.
233            if quorum_commit_index < range_end {
234                break;
235            }
236            // Pause scheduling new fetches when handling of commits is lagging.
237            if highest_handled_index + unhandled_commits_threshold < range_end {
238                warn!(
239                    "Skip scheduling new commit fetches: consensus handler is lagging. highest_handled_index={}, highest_scheduled_index={}",
240                    highest_handled_index, highest_scheduled_index
241                );
242                break;
243            }
244            self.pending_fetches
245                .insert((range_start..=range_end).into());
246            // quorum_commit_index should be non-decreasing, so highest_scheduled_index should not
247            // decrease either.
248            self.highest_scheduled_index = Some(range_end);
249        }
250    }
251
252    async fn handle_fetch_result(
253        &mut self,
254        target_end: CommitIndex,
255        certified_commits: CertifiedCommits,
256    ) {
257        assert!(!certified_commits.commits().is_empty());
258
259        let (total_blocks_fetched, total_blocks_size_bytes) = certified_commits
260            .commits()
261            .iter()
262            .fold((0, 0), |(blocks, bytes), c| {
263                (
264                    blocks + c.blocks().len(),
265                    bytes
266                        + c.blocks()
267                            .iter()
268                            .map(|b| b.serialized().len())
269                            .sum::<usize>() as u64,
270                )
271            });
272
273        let metrics = &self.inner.context.metrics.node_metrics;
274        metrics
275            .commit_sync_fetched_commits
276            .inc_by(certified_commits.commits().len() as u64);
277        metrics
278            .commit_sync_fetched_blocks
279            .inc_by(total_blocks_fetched as u64);
280        metrics
281            .commit_sync_total_fetched_blocks_size
282            .inc_by(total_blocks_size_bytes);
283
284        let (commit_start, commit_end) = (
285            certified_commits.commits().first().unwrap().index(),
286            certified_commits.commits().last().unwrap().index(),
287        );
288        self.highest_fetched_commit_index = self.highest_fetched_commit_index.max(commit_end);
289        metrics
290            .commit_sync_highest_fetched_index
291            .set(self.highest_fetched_commit_index as i64);
292
293        // Allow returning partial results, and try fetching the rest separately.
294        if commit_end < target_end {
295            self.pending_fetches
296                .insert((commit_end + 1..=target_end).into());
297        }
298        // Make sure synced_commit_index is up to date.
299        self.synced_commit_index = self
300            .synced_commit_index
301            .max(self.inner.dag_state.read().last_commit_index());
302        // Only add new blocks if at least some of them are not already synced.
303        if self.synced_commit_index < commit_end {
304            self.fetched_ranges
305                .insert((commit_start..=commit_end).into(), certified_commits);
306        }
307        // Try to process as many fetched blocks as possible.
308        while let Some((fetched_commit_range, _commits)) = self.fetched_ranges.first_key_value() {
309            // Only pop fetched_ranges if there is no gap with blocks already synced.
310            // Note: start, end and synced_commit_index are all inclusive.
311            let (fetched_commit_range, commits) =
312                if fetched_commit_range.start() <= self.synced_commit_index + 1 {
313                    self.fetched_ranges.pop_first().unwrap()
314                } else {
315                    // Found gap between earliest fetched block and latest synced block,
316                    // so not sending additional blocks to Core.
317                    metrics.commit_sync_gap_on_processing.inc();
318                    break;
319                };
320            // Avoid sending to Core a whole batch of already synced blocks.
321            if fetched_commit_range.end() <= self.synced_commit_index {
322                continue;
323            }
324
325            debug!(
326                "Fetched blocks for commit range {:?}: {}",
327                fetched_commit_range,
328                commits
329                    .commits()
330                    .iter()
331                    .flat_map(|c| c.blocks())
332                    .map(|b| b.reference().to_string())
333                    .join(","),
334            );
335
336            // If core thread cannot handle the incoming blocks, it is ok to block here
337            // to slow down the commit syncer.
338            match self
339                .inner
340                .core_thread_dispatcher
341                .add_certified_commits(commits)
342                .await
343            {
344                // Missing ancestors are possible from certification blocks, but
345                // it is unnecessary to try to sync their causal history. If they are required
346                // for the progress of the DAG, they will be included in a future commit.
347                Ok(missing) => {
348                    if !missing.is_empty() {
349                        info!(
350                            "Certification blocks have missing ancestors: {} for commit range {:?}",
351                            missing.iter().map(|b| b.to_string()).join(","),
352                            fetched_commit_range,
353                        );
354                    }
355                    for block_ref in missing {
356                        let hostname = &self
357                            .inner
358                            .context
359                            .committee
360                            .authority(block_ref.author)
361                            .hostname;
362                        metrics
363                            .commit_sync_fetch_missing_blocks
364                            .with_label_values(&[hostname])
365                            .inc();
366                    }
367                }
368                Err(e) => {
369                    info!("Failed to add blocks, shutting down: {}", e);
370                    return;
371                }
372            };
373
374            // Once commits and blocks are sent to Core, ratchet up synced_commit_index
375            self.synced_commit_index = self.synced_commit_index.max(fetched_commit_range.end());
376        }
377
378        metrics
379            .commit_sync_inflight_fetches
380            .set(self.inflight_fetches.len() as i64);
381        metrics
382            .commit_sync_pending_fetches
383            .set(self.pending_fetches.len() as i64);
384        metrics
385            .commit_sync_highest_synced_index
386            .set(self.synced_commit_index as i64);
387    }
388
389    fn try_start_fetches(&mut self) {
390        // Cap parallel fetches based on configured limit and known peers, to avoid overloading the network.
391        // Also when there are too many fetched blocks that cannot be sent to Core before an earlier fetch
392        // has not finished, reduce parallelism so the earlier fetch can retry on a better host and succeed.
393        // For validators, use committee size for the calculation. For observers, don't apply the 2/3 limit.
394        let known_peers_count = self.inner.peers_pool.get_known_peers().len();
395        let target_parallel_fetches = if self.inner.context.is_validator() {
396            self.inner
397                .context
398                .parameters
399                .commit_sync_parallel_fetches
400                .min(known_peers_count * 2 / 3)
401                .min(
402                    self.inner
403                        .context
404                        .parameters
405                        .commit_sync_batches_ahead
406                        .saturating_sub(self.fetched_ranges.len()),
407                )
408                .max(1)
409        } else {
410            // For observers, currently the node is probably connected only to another peer. In the future probably more observer peers might be available
411            // to sync from. That's why we do not cap the number of parallel fetches by the number of known peers.
412            self.inner
413                .context
414                .parameters
415                .commit_sync_parallel_fetches
416                .min(
417                    self.inner
418                        .context
419                        .parameters
420                        .commit_sync_batches_ahead
421                        .saturating_sub(self.fetched_ranges.len()),
422                )
423                .max(1)
424        };
425        // Start new fetches if there are pending batches and available slots.
426        loop {
427            if self.inflight_fetches.len() >= target_parallel_fetches {
428                break;
429            }
430            let Some(commit_range) = self.pending_fetches.pop_first() else {
431                break;
432            };
433            self.inflight_fetches
434                .spawn(Self::fetch_loop(self.inner.clone(), commit_range));
435        }
436
437        let metrics = &self.inner.context.metrics.node_metrics;
438        metrics
439            .commit_sync_inflight_fetches
440            .set(self.inflight_fetches.len() as i64);
441        metrics
442            .commit_sync_pending_fetches
443            .set(self.pending_fetches.len() as i64);
444        metrics
445            .commit_sync_highest_synced_index
446            .set(self.synced_commit_index as i64);
447    }
448
449    // Retries fetching commits and blocks from available authorities, until a request succeeds
450    // where at least a prefix of the commit range is fetched.
451    // Returns the fetched commits and blocks referenced by the commits.
452    async fn fetch_loop(
453        inner: Arc<Inner<VC, OC>>,
454        commit_range: CommitRange,
455    ) -> (CommitIndex, CertifiedCommits) {
456        let base_timeout = inner.context.parameters.commit_sync_request_timeout;
457        // Max per-request timeout will be base timeout times a multiplier.
458        // At the extreme, this means there will be 120s timeout to fetch max_blocks_per_fetch blocks.
459        const MAX_TIMEOUT_MULTIPLIER: u32 = 12;
460        // timeout * max number of targets should be reasonably small, so the
461        // system can adjust to slow network or large data sizes quickly.
462        const MAX_NUM_TARGETS: usize = 24;
463        let mut timeout_multiplier = 0;
464        let _timer = inner
465            .context
466            .metrics
467            .node_metrics
468            .commit_sync_fetch_loop_latency
469            .start_timer();
470        info!("Starting to fetch commits in {commit_range:?} ...",);
471        loop {
472            // Attempt to fetch commits and blocks through min(available peers count, MAX_NUM_TARGETS) peers.
473            let mut target_peers = inner.peers_pool.get_known_peers();
474            target_peers.shuffle(&mut ThreadRng::default());
475            target_peers.truncate(MAX_NUM_TARGETS);
476            // Increase timeout multiplier for each loop until MAX_TIMEOUT_MULTIPLIER.
477            timeout_multiplier = (timeout_multiplier + 1).min(MAX_TIMEOUT_MULTIPLIER);
478            let request_timeout = base_timeout * timeout_multiplier;
479            // Give enough overall timeout for fetching commits and blocks.
480            // - Timeout for fetching commits and commit certifying blocks.
481            // - Timeout for fetching blocks referenced by the commits.
482            // - Time spent on pipelining requests to fetch blocks.
483            // - Another headroom to allow fetch_once() to timeout gracefully if possible.
484            let fetch_timeout = request_timeout * 4;
485            // Try fetching from selected target peers.
486            for peer in target_peers {
487                match tokio::time::timeout(
488                    fetch_timeout,
489                    Self::fetch_once(
490                        inner.clone(),
491                        peer.clone(),
492                        commit_range.clone(),
493                        request_timeout,
494                    ),
495                )
496                .await
497                {
498                    Ok(Ok(commits)) => {
499                        info!("Finished fetching commits in {commit_range:?}",);
500                        return (commit_range.end(), commits);
501                    }
502                    Ok(Err(e)) => {
503                        warn!(
504                            "Failed to fetch {commit_range:?} from {}: {}",
505                            peer.hostname(&inner.context),
506                            e
507                        );
508                        inner
509                            .context
510                            .metrics
511                            .node_metrics
512                            .commit_sync_fetch_once_errors
513                            .with_label_values(&[peer.labelname(&inner.context).as_str(), e.name()])
514                            .inc();
515                    }
516                    Err(_) => {
517                        warn!(
518                            "Timed out fetching {commit_range:?} from {}",
519                            peer.hostname(&inner.context)
520                        );
521                        inner
522                            .context
523                            .metrics
524                            .node_metrics
525                            .commit_sync_fetch_once_errors
526                            .with_label_values(&[
527                                peer.labelname(&inner.context).as_str(),
528                                "FetchTimeout",
529                            ])
530                            .inc();
531                    }
532                }
533            }
534            // Avoid busy looping, by waiting for a while before retrying.
535            sleep(base_timeout).await;
536        }
537    }
538
539    // Fetches commits and blocks from a single peer. At a high level, first the commits are
540    // fetched and verified. After that, blocks referenced in the certified commits are fetched
541    // and sent to Core for processing.
542    async fn fetch_once(
543        inner: Arc<Inner<VC, OC>>,
544        target_peer: PeerId,
545        commit_range: CommitRange,
546        timeout: Duration,
547    ) -> ConsensusResult<CertifiedCommits> {
548        let _timer = inner
549            .context
550            .metrics
551            .node_metrics
552            .commit_sync_fetch_once_latency
553            .start_timer();
554
555        // 0. Probe the target to check reachability before committing to the full fetch.
556        // This skips unreachable and slow peers quickly.
557        let probe_timeout = inner.context.parameters.commit_sync_probe_timeout;
558        inner
559            .network_client
560            .probe_connectivity(target_peer.clone(), probe_timeout)
561            .await?;
562
563        // 1. Fetch commits in the commit range from the target authority.
564        let (serialized_commits, serialized_blocks) = inner
565            .network_client
566            .fetch_commits(target_peer.clone(), commit_range.clone(), timeout)
567            .await?;
568
569        // 2. Verify the response contains blocks that can certify the last returned commit,
570        // and the returned commits are chained by digests, so earlier commits are certified
571        // as well.
572        let (commits, commit_certifying_blocks_and_votes) = spawn_blocking({
573            let context = inner.context.clone();
574            let block_verifier = inner.block_verifier.clone();
575            let peer = target_peer.clone();
576            move || {
577                Inner::<VC, OC>::verify_commits(
578                    &context,
579                    block_verifier.as_ref(),
580                    peer,
581                    commit_range,
582                    serialized_commits,
583                    serialized_blocks,
584                )
585            }
586        })
587        .await??;
588
589        // Only the vote tracker needs the reject votes, so move them into it without cloning.
590        // Cheap clones of the blocks are enough for the rest of the fetch handling.
591        let commit_certifying_blocks: Vec<_> = commit_certifying_blocks_and_votes
592            .iter()
593            .map(|(block, _)| block.clone())
594            .collect();
595        if inner.context.protocol_config.transaction_voting_enabled() {
596            inner
597                .transaction_vote_tracker
598                .add_voted_blocks(commit_certifying_blocks_and_votes);
599        }
600
601        // 3. Fetch blocks referenced by the commits, from the same peer where commits are fetched.
602        let mut block_refs: Vec<_> = commits.iter().flat_map(|c| c.blocks()).cloned().collect();
603        block_refs.sort();
604        let num_chunks = block_refs
605            .len()
606            .div_ceil(inner.context.parameters.max_blocks_per_fetch)
607            as u32;
608        let mut requests: FuturesOrdered<_> = block_refs
609            .chunks(inner.context.parameters.max_blocks_per_fetch)
610            .enumerate()
611            .map(|(i, request_block_refs)| {
612                let inner = inner.clone();
613                let peer = target_peer.clone();
614                async move {
615                    // 4. Send out pipelined fetch requests to avoid overloading the target authority.
616                    sleep(timeout * i as u32 / num_chunks).await;
617                    // TODO: add some retries.
618                    let serialized_blocks = inner
619                        .network_client
620                        .fetch_blocks(
621                            peer.clone(),
622                            request_block_refs.to_vec(),
623                            vec![],
624                            false,
625                            timeout,
626                        )
627                        .await?;
628                    // 5. Verify the same number of blocks are returned as requested.
629                    if request_block_refs.len() != serialized_blocks.len() {
630                        return Err(ConsensusError::UnexpectedNumberOfBlocksFetched {
631                            peer,
632                            requested: request_block_refs.len(),
633                            received: serialized_blocks.len(),
634                        });
635                    }
636                    // 6. Verify returned blocks have valid formats.
637                    let signed_blocks = serialized_blocks
638                        .iter()
639                        .map(|serialized| {
640                            let block: SignedBlock = bcs::from_bytes(serialized)
641                                .map_err(ConsensusError::MalformedBlock)?;
642                            Ok(block)
643                        })
644                        .collect::<ConsensusResult<Vec<_>>>()?;
645                    // 7. Verify the returned blocks match the requested block refs.
646                    // If they do match, the returned blocks can be considered verified as well.
647                    let mut blocks = Vec::new();
648                    for ((requested_block_ref, signed_block), serialized) in request_block_refs
649                        .iter()
650                        .zip_debug_eq(signed_blocks)
651                        .zip_debug_eq(serialized_blocks)
652                    {
653                        let signed_block_digest = VerifiedBlock::compute_digest(&serialized);
654                        let received_block_ref = BlockRef::new(
655                            signed_block.round(),
656                            signed_block.author(),
657                            signed_block_digest,
658                        );
659                        if *requested_block_ref != received_block_ref {
660                            return Err(ConsensusError::UnexpectedBlockForCommit {
661                                peer,
662                                requested: *requested_block_ref,
663                                received: received_block_ref,
664                            });
665                        }
666                        blocks.push(VerifiedBlock::new_verified(signed_block, serialized));
667                    }
668                    Ok(blocks)
669                }
670            })
671            .collect();
672
673        let mut fetched_blocks = BTreeMap::new();
674        while let Some(result) = requests.next().await {
675            for block in result? {
676                fetched_blocks.insert(block.reference(), block);
677            }
678        }
679
680        // 8. Check if the block timestamps are lower than current time - this is for metrics only.
681        for block in fetched_blocks
682            .values()
683            .chain(commit_certifying_blocks.iter())
684        {
685            let now_ms = inner.context.clock.timestamp_utc_ms();
686            let forward_drift = block.timestamp_ms().saturating_sub(now_ms);
687            if forward_drift == 0 {
688                continue;
689            };
690            // Extract hostname based on peer type
691            inner
692                .context
693                .metrics
694                .node_metrics
695                .block_timestamp_drift_ms
696                .with_label_values(&[
697                    target_peer.labelname(&inner.context).as_str(),
698                    "commit_syncer",
699                ])
700                .inc_by(forward_drift);
701        }
702
703        // 9. Now create certified commits by assigning the blocks to each commit.
704        let mut certified_commits = Vec::new();
705        for commit in &commits {
706            let blocks = commit
707                .blocks()
708                .iter()
709                .map(|block_ref| {
710                    fetched_blocks
711                        .remove(block_ref)
712                        .expect("Block should exist")
713                })
714                .collect::<Vec<_>>();
715            certified_commits.push(CertifiedCommit::new_certified(commit.clone(), blocks));
716        }
717
718        // 10. Add blocks in certified commits to the transaction vote tracker.
719        for commit in &certified_commits {
720            for block in commit.blocks() {
721                // Only account for reject votes in the block, since they may vote on uncommitted
722                // blocks or transactions. It is unnecessary to vote on the committed blocks
723                // themselves.
724                if inner.context.protocol_config.transaction_voting_enabled() {
725                    inner
726                        .transaction_vote_tracker
727                        .add_voted_blocks(vec![(block.clone(), vec![])]);
728                }
729            }
730        }
731
732        // 11. Record commit votes from the fetched blocks.
733        for commit in &certified_commits {
734            for block in commit.blocks() {
735                inner.commit_vote_monitor.observe_block(block);
736            }
737        }
738        for block in &commit_certifying_blocks {
739            inner.commit_vote_monitor.observe_block(block);
740        }
741
742        // 12. Update round tracker from the fetched blocks. For fetched blocks,
743        // excluded_ancestors are not available so we use an empty vector.
744        {
745            let mut tracker = inner.round_tracker.write();
746            // Update from commit blocks
747            for commit in &certified_commits {
748                for block in commit.blocks() {
749                    tracker.update_from_verified_block(&ExtendedBlock {
750                        block: block.clone(),
751                        excluded_ancestors: vec![],
752                    });
753                }
754            }
755            // Update from vote blocks
756            for block in &commit_certifying_blocks {
757                tracker.update_from_verified_block(&ExtendedBlock {
758                    block: block.clone(),
759                    excluded_ancestors: vec![],
760                });
761            }
762        }
763
764        Ok(CertifiedCommits::new(
765            certified_commits,
766            commit_certifying_blocks,
767        ))
768    }
769
770    fn unhandled_commits_threshold(&self) -> CommitIndex {
771        self.inner.context.parameters.commit_sync_batch_size
772            * (self.inner.context.parameters.commit_sync_batches_ahead as u32)
773    }
774
775    #[cfg(test)]
776    fn pending_fetches(&self) -> BTreeSet<CommitRange> {
777        self.pending_fetches.clone()
778    }
779
780    #[cfg(test)]
781    fn fetched_ranges(&self) -> BTreeMap<CommitRange, CertifiedCommits> {
782        self.fetched_ranges.clone()
783    }
784
785    #[cfg(test)]
786    fn highest_scheduled_index(&self) -> Option<CommitIndex> {
787        self.highest_scheduled_index
788    }
789
790    #[cfg(test)]
791    fn highest_fetched_commit_index(&self) -> CommitIndex {
792        self.highest_fetched_commit_index
793    }
794
795    #[cfg(test)]
796    fn synced_commit_index(&self) -> CommitIndex {
797        self.synced_commit_index
798    }
799}
800
801struct Inner<VC: ValidatorNetworkClient, OC: ObserverNetworkClient> {
802    context: Arc<Context>,
803    core_thread_dispatcher: Arc<dyn CoreThreadDispatcher>,
804    commit_vote_monitor: Arc<CommitVoteMonitor>,
805    commit_consumer_monitor: Arc<CommitConsumerMonitor>,
806    block_verifier: Arc<dyn BlockVerifier>,
807    transaction_vote_tracker: TransactionVoteTracker,
808    round_tracker: Arc<RwLock<RoundTracker>>,
809    network_client: Arc<CommitSyncerClient<VC, OC>>,
810    dag_state: Arc<RwLock<DagState>>,
811    peers_pool: Arc<PeersPool>,
812}
813
814impl<VC: ValidatorNetworkClient, OC: ObserverNetworkClient> Inner<VC, OC> {
815    /// Verifies the commits and certifies them using the provided vote blocks for the last commit.
816    /// Returns, in order:
817    /// - the verified commit chain as trusted commits;
818    /// - the verified blocks that certify the last commit, paired with locally rejected
819    ///   transaction indices for transaction vote tracking.
820    fn verify_commits(
821        context: &Context,
822        block_verifier: &dyn BlockVerifier,
823        peer: PeerId,
824        commit_range: CommitRange,
825        serialized_commits: Vec<Bytes>,
826        serialized_vote_blocks: Vec<Bytes>,
827    ) -> ConsensusResult<(
828        Vec<TrustedCommit>,
829        Vec<(VerifiedBlock, Vec<TransactionIndex>)>,
830    )> {
831        // Parse and verify commits.
832        let mut commits = Vec::new();
833        for serialized in &serialized_commits {
834            let commit: Commit =
835                bcs::from_bytes(serialized).map_err(ConsensusError::MalformedCommit)?;
836            let digest = TrustedCommit::compute_digest(serialized);
837            if commits.is_empty() {
838                // start is inclusive, so first commit must be at the start index.
839                if commit.index() != commit_range.start() {
840                    return Err(ConsensusError::UnexpectedStartCommit {
841                        peer,
842                        start: commit_range.start(),
843                        commit: Box::new(commit),
844                    });
845                }
846            } else {
847                // Verify next commit increments index and references the previous digest.
848                let (last_commit_digest, last_commit): &(CommitDigest, Commit) =
849                    commits.last().unwrap();
850                if commit.index() != last_commit.index() + 1
851                    || &commit.previous_digest() != last_commit_digest
852                {
853                    return Err(ConsensusError::UnexpectedCommitSequence {
854                        peer,
855                        prev_commit: Box::new(last_commit.clone()),
856                        curr_commit: Box::new(commit),
857                    });
858                }
859            }
860            // Do not process more commits past the end index.
861            if commit.index() > commit_range.end() {
862                break;
863            }
864            commits.push((digest, commit));
865        }
866        let Some((end_commit_digest, end_commit)) = commits.last() else {
867            return Err(ConsensusError::NoCommitReceived { peer });
868        };
869
870        // Parse and verify blocks. Then accumulate votes on the end commit.
871        let end_commit_ref = CommitRef::new(end_commit.index(), *end_commit_digest);
872        let mut stake_aggregator = StakeAggregator::<QuorumThreshold>::new();
873        let mut commit_certifying_blocks = Vec::new();
874        for serialized in serialized_vote_blocks {
875            let block: SignedBlock =
876                bcs::from_bytes(&serialized).map_err(ConsensusError::MalformedBlock)?;
877            // Only block signatures need to be verified, to verify commit votes.
878            // But the blocks will be sent to Core, so they need to be fully verified.
879            let (block, reject_transaction_votes) =
880                block_verifier.verify_and_vote(block, serialized)?;
881            for vote in block.commit_votes() {
882                if *vote == end_commit_ref {
883                    stake_aggregator.add(block.author(), &context.committee);
884                }
885            }
886            commit_certifying_blocks.push((block, reject_transaction_votes));
887        }
888
889        // Check if the end commit has enough votes.
890        if !stake_aggregator.reached_threshold(&context.committee) {
891            return Err(ConsensusError::NotEnoughCommitVotes {
892                stake: stake_aggregator.stake(),
893                peer,
894                commit: Box::new(end_commit.clone()),
895            });
896        }
897
898        let trusted_commits = commits
899            .into_iter()
900            .zip_debug_eq(serialized_commits)
901            .map(|((_d, c), s)| TrustedCommit::new_trusted(c, s))
902            .collect();
903        Ok((trusted_commits, commit_certifying_blocks))
904    }
905}
906
907#[cfg(test)]
908mod tests {
909    use std::{sync::Arc, time::Duration};
910
911    use bytes::Bytes;
912    use consensus_config::{AuthorityIndex, NetworkKeyPair, Parameters};
913    use consensus_types::block::{BlockRef, Round};
914    use mysten_common::ZipDebugEqIteratorExt;
915    use parking_lot::RwLock;
916
917    use crate::{
918        CommitConsumerMonitor, CommitDigest, CommitRef,
919        block::{TestBlock, VerifiedBlock},
920        block_verifier::NoopBlockVerifier,
921        commit::CommitRange,
922        commit_syncer::CommitSyncer,
923        commit_vote_monitor::CommitVoteMonitor,
924        context::Context,
925        core_thread::MockCoreThreadDispatcher,
926        dag_state::DagState,
927        error::ConsensusResult,
928        network::{BlockStream, CommitSyncerClient, ObserverNetworkClient, ValidatorNetworkClient},
929        peers_pool::{PeerService, PeersPool},
930        round_tracker::RoundTracker,
931        storage::mem_store::MemStore,
932        transaction_vote_tracker::TransactionVoteTracker,
933    };
934
935    #[derive(Default)]
936    struct FakeNetworkClient {}
937
938    #[async_trait::async_trait]
939    impl ValidatorNetworkClient for FakeNetworkClient {
940        async fn subscribe_blocks(
941            &self,
942            _peer: AuthorityIndex,
943            _last_received: Round,
944            _timeout: Duration,
945        ) -> ConsensusResult<BlockStream> {
946            unimplemented!("Unimplemented")
947        }
948
949        async fn fetch_blocks(
950            &self,
951            _peer: AuthorityIndex,
952            _block_refs: Vec<BlockRef>,
953            _fetch_after_rounds: Vec<Round>,
954            _fetch_missing_ancestors: bool,
955            _timeout: Duration,
956        ) -> ConsensusResult<Vec<Bytes>> {
957            unimplemented!("Unimplemented")
958        }
959
960        async fn fetch_commits(
961            &self,
962            _peer: AuthorityIndex,
963            _commit_range: CommitRange,
964            _timeout: Duration,
965        ) -> ConsensusResult<(Vec<Bytes>, Vec<Bytes>)> {
966            unimplemented!("Unimplemented")
967        }
968
969        async fn fetch_latest_blocks(
970            &self,
971            _peer: AuthorityIndex,
972            _authorities: Vec<AuthorityIndex>,
973            _timeout: Duration,
974        ) -> ConsensusResult<Vec<Bytes>> {
975            unimplemented!("Unimplemented")
976        }
977
978        async fn get_latest_rounds(
979            &self,
980            _peer: AuthorityIndex,
981            _timeout: Duration,
982        ) -> ConsensusResult<(Vec<Round>, Vec<Round>)> {
983            unimplemented!("Unimplemented")
984        }
985
986        #[cfg(test)]
987        async fn send_block(
988            &self,
989            _peer: AuthorityIndex,
990            _block: &VerifiedBlock,
991            _timeout: Duration,
992        ) -> ConsensusResult<()> {
993            unimplemented!("Unimplemented")
994        }
995    }
996
997    #[async_trait::async_trait]
998    impl ObserverNetworkClient for FakeNetworkClient {
999        async fn stream_blocks(
1000            &self,
1001            _peer: crate::network::PeerId,
1002            _highest_round_per_authority: Vec<Round>,
1003            _timeout: Duration,
1004        ) -> ConsensusResult<crate::network::ObserverBlockStream> {
1005            unimplemented!("Unimplemented")
1006        }
1007
1008        async fn fetch_blocks(
1009            &self,
1010            _peer: crate::network::PeerId,
1011            _block_refs: Vec<BlockRef>,
1012            _highest_accepted_rounds: Vec<Round>,
1013            _breadth_first: bool,
1014            _timeout: Duration,
1015        ) -> ConsensusResult<Vec<Bytes>> {
1016            unimplemented!("Unimplemented")
1017        }
1018
1019        async fn fetch_commits(
1020            &self,
1021            _peer: crate::network::PeerId,
1022            _commit_range: CommitRange,
1023            _timeout: Duration,
1024        ) -> ConsensusResult<(Vec<Bytes>, Vec<Bytes>)> {
1025            unimplemented!("Unimplemented")
1026        }
1027    }
1028
1029    #[tokio::test(flavor = "current_thread", start_paused = true)]
1030    async fn commit_syncer_observer_node_basic() {
1031        // Test basic Observer node behavior for commit syncing
1032        // An Observer node should be able to sync from both validator and observer peers
1033
1034        // Create an Observer node context (own_index = MAX)
1035        let (mut context, _) = Context::new_for_test(4);
1036        context.own_index = AuthorityIndex::MAX; // Mark this as an observer node
1037        context.parameters = Parameters {
1038            commit_sync_batch_size: 5,
1039            commit_sync_batches_ahead: 10,
1040            commit_sync_parallel_fetches: 5,
1041            max_blocks_per_fetch: 5,
1042            ..context.parameters
1043        };
1044        let context = Arc::new(context);
1045
1046        // Setup observer node commit syncer
1047        let block_verifier = Arc::new(NoopBlockVerifier {});
1048        let core_thread_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
1049        let mock_client = Arc::new(FakeNetworkClient::default());
1050        let network_client = Arc::new(CommitSyncerClient::new(
1051            context.clone(),
1052            Some(mock_client.clone()),
1053            Some(mock_client.clone()),
1054        ));
1055        let store = Arc::new(MemStore::new());
1056        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
1057        let transaction_vote_tracker =
1058            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
1059        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
1060        let commit_consumer_monitor = Arc::new(CommitConsumerMonitor::new(0, 0));
1061        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1062
1063        // Create PeersPool - Observer typically connects to one validator
1064        let peers_pool = Arc::new(PeersPool::new(context.clone()));
1065        // Register the validator peer that the observer connects to
1066        peers_pool
1067            .register_validator(
1068                AuthorityIndex::new_for_test(0),
1069                vec![PeerService::Validator, PeerService::Observer],
1070            )
1071            .unwrap();
1072
1073        let mut commit_syncer = CommitSyncer::new(
1074            context.clone(),
1075            core_thread_dispatcher,
1076            commit_vote_monitor.clone(),
1077            commit_consumer_monitor.clone(),
1078            block_verifier,
1079            transaction_vote_tracker,
1080            round_tracker,
1081            network_client,
1082            dag_state,
1083            peers_pool.clone(),
1084        );
1085
1086        // Verify this is recognized as an observer
1087        assert!(!context.is_validator(), "Should be an observer node");
1088
1089        // Simulate the observer seeing commits from its connected validator
1090        for i in 0..3 {
1091            let test_block = TestBlock::new(10, i)
1092                .set_commit_votes(vec![CommitRef::new(5, CommitDigest::MIN)])
1093                .build();
1094            let block = VerifiedBlock::new_for_test(test_block);
1095            commit_vote_monitor.observe_block(&block);
1096        }
1097
1098        // Observer should be able to schedule commit fetches
1099        commit_syncer.try_schedule_once();
1100        assert_eq!(commit_syncer.pending_fetches().len(), 1);
1101        assert_eq!(commit_syncer.highest_scheduled_index(), Some(5));
1102
1103        // Start fetches - observer should be able to fetch from its single peer
1104        commit_syncer.try_start_fetches();
1105        assert_eq!(
1106            commit_syncer.inflight_fetches.len(),
1107            1,
1108            "Should start fetch from single peer"
1109        );
1110    }
1111
1112    #[tokio::test(flavor = "current_thread", start_paused = true)]
1113    async fn commit_syncer_observer_with_multiple_peers() {
1114        // Test Observer node behavior when connected to multiple peers
1115        // This simulates an observer that can sync from multiple sources
1116
1117        let (mut context, _) = Context::new_for_test(4);
1118        context.own_index = AuthorityIndex::MAX; // Observer node
1119        context.parameters = Parameters {
1120            commit_sync_batch_size: 5,
1121            commit_sync_batches_ahead: 10,
1122            commit_sync_parallel_fetches: 8, // Allow more parallelism
1123            max_blocks_per_fetch: 5,
1124            ..context.parameters
1125        };
1126        let context = Arc::new(context);
1127
1128        // Setup
1129        let block_verifier = Arc::new(NoopBlockVerifier {});
1130        let core_thread_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
1131        let network_client = Arc::new(CommitSyncerClient::new(
1132            context.clone(),
1133            Some(Arc::new(FakeNetworkClient::default())),
1134            Some(Arc::new(FakeNetworkClient::default())),
1135        ));
1136        let store = Arc::new(MemStore::new());
1137        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
1138        let transaction_vote_tracker =
1139            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
1140        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
1141        let commit_consumer_monitor = Arc::new(CommitConsumerMonitor::new(0, 0));
1142        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1143
1144        // Create PeersPool with multiple peers (validators and another observer)
1145        let peers_pool = Arc::new(PeersPool::new(context.clone()));
1146        // Register multiple validator peers
1147        peers_pool
1148            .register_validator(
1149                AuthorityIndex::new_for_test(0),
1150                vec![PeerService::Validator, PeerService::Observer],
1151            )
1152            .unwrap();
1153        peers_pool
1154            .register_validator(
1155                AuthorityIndex::new_for_test(1),
1156                vec![PeerService::Validator, PeerService::Observer],
1157            )
1158            .unwrap();
1159        peers_pool
1160            .register_validator(
1161                AuthorityIndex::new_for_test(2),
1162                vec![PeerService::Validator, PeerService::Observer],
1163            )
1164            .unwrap();
1165
1166        // Now register another observer peer (simulating observer-to-observer sync)
1167        let observer_peer = NetworkKeyPair::generate(&mut rand::thread_rng()).public();
1168        peers_pool.register_observer(observer_peer);
1169
1170        let mut commit_syncer = CommitSyncer::new(
1171            context.clone(),
1172            core_thread_dispatcher,
1173            commit_vote_monitor.clone(),
1174            commit_consumer_monitor.clone(),
1175            block_verifier,
1176            transaction_vote_tracker,
1177            round_tracker,
1178            network_client,
1179            dag_state,
1180            peers_pool.clone(),
1181        );
1182
1183        // Simulate heavy commit load that requires parallel fetching
1184        for i in 0..3 {
1185            let test_block = TestBlock::new(100, i)
1186                .set_commit_votes(vec![CommitRef::new(50, CommitDigest::MIN)])
1187                .build();
1188            let block = VerifiedBlock::new_for_test(test_block);
1189            commit_vote_monitor.observe_block(&block);
1190        }
1191
1192        commit_syncer.try_schedule_once();
1193
1194        // Should schedule multiple batches
1195        let pending_fetches = commit_syncer.pending_fetches().len();
1196        assert!(pending_fetches > 0, "Should schedule fetches");
1197
1198        // Start fetches - observer should utilize multiple peers in parallel
1199        commit_syncer.try_start_fetches();
1200
1201        // Observer with 4 peers (3 validators + 1 observer) should be able to
1202        // fetch from multiple peers in parallel, not limited by 2/3 rule
1203        let inflight = commit_syncer.inflight_fetches.len();
1204        let known_peers = peers_pool.get_known_peers().len();
1205
1206        assert_eq!(known_peers, 4, "Should have 3 validators + 1 observer peer");
1207
1208        // Observer should be able to use full parallelism up to configured limit
1209        // Not restricted by the validator's 2/3 limitation
1210        let max_parallel = context
1211            .parameters
1212            .commit_sync_parallel_fetches
1213            .min(pending_fetches)
1214            .min(context.parameters.commit_sync_batches_ahead);
1215
1216        assert!(
1217            inflight <= max_parallel,
1218            "Observer should respect configured parallelism limit: {} <= {}",
1219            inflight,
1220            max_parallel
1221        );
1222
1223        // Verify observer can potentially use more parallelism than a validator would
1224        // A validator with 4 peers would be limited to 4 * 2/3 = 2 parallel fetches
1225        // But an observer can use more
1226        if pending_fetches >= 3 {
1227            assert!(
1228                max_parallel > 2,
1229                "Observer should be able to use more parallelism than validator's 2/3 limit"
1230            );
1231        }
1232    }
1233
1234    #[tokio::test(flavor = "current_thread", start_paused = true)]
1235    async fn commit_syncer_start_and_pause_scheduling() {
1236        // SETUP
1237        let (context, _) = Context::new_for_test(4);
1238        // Use smaller batches and fetch limits for testing.
1239        let context = Context {
1240            own_index: AuthorityIndex::new_for_test(3),
1241            parameters: Parameters {
1242                commit_sync_batch_size: 5,
1243                commit_sync_batches_ahead: 5,
1244                commit_sync_parallel_fetches: 5,
1245                max_blocks_per_fetch: 5,
1246                ..context.parameters
1247            },
1248            ..context
1249        };
1250        let context = Arc::new(context);
1251        let block_verifier = Arc::new(NoopBlockVerifier {});
1252        let core_thread_dispatcher = Arc::new(MockCoreThreadDispatcher::default());
1253        let mock_client = Arc::new(FakeNetworkClient::default());
1254        let network_client = Arc::new(CommitSyncerClient::new(
1255            context.clone(),
1256            Some(mock_client.clone()),
1257            Some(mock_client.clone()),
1258        ));
1259        let store = Arc::new(MemStore::new());
1260        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
1261        let transaction_vote_tracker =
1262            TransactionVoteTracker::new(context.clone(), block_verifier.clone(), dag_state.clone());
1263        let commit_vote_monitor = Arc::new(CommitVoteMonitor::new(context.clone()));
1264        let commit_consumer_monitor = Arc::new(CommitConsumerMonitor::new(0, 0));
1265        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1266        let peers_pool = Arc::new(PeersPool::new(context.clone()));
1267        let mut commit_syncer = CommitSyncer::new(
1268            context,
1269            core_thread_dispatcher,
1270            commit_vote_monitor.clone(),
1271            commit_consumer_monitor.clone(),
1272            block_verifier,
1273            transaction_vote_tracker,
1274            round_tracker,
1275            network_client,
1276            dag_state,
1277            peers_pool,
1278        );
1279
1280        // Check initial state.
1281        assert!(commit_syncer.pending_fetches().is_empty());
1282        assert!(commit_syncer.fetched_ranges().is_empty());
1283        assert!(commit_syncer.highest_scheduled_index().is_none());
1284        assert_eq!(commit_syncer.highest_fetched_commit_index(), 0);
1285        assert_eq!(commit_syncer.synced_commit_index(), 0);
1286
1287        // Observe round 15 blocks voting for commit 10 from authorities 0 to 2 in CommitVoteMonitor
1288        for i in 0..3 {
1289            let test_block = TestBlock::new(15, i)
1290                .set_commit_votes(vec![CommitRef::new(10, CommitDigest::MIN)])
1291                .build();
1292            let block = VerifiedBlock::new_for_test(test_block);
1293            commit_vote_monitor.observe_block(&block);
1294        }
1295
1296        // Fetches should be scheduled after seeing progress of other validators.
1297        commit_syncer.try_schedule_once();
1298
1299        // Verify state.
1300        assert_eq!(commit_syncer.pending_fetches().len(), 2);
1301        assert!(commit_syncer.fetched_ranges().is_empty());
1302        assert_eq!(commit_syncer.highest_scheduled_index(), Some(10));
1303        assert_eq!(commit_syncer.highest_fetched_commit_index(), 0);
1304        assert_eq!(commit_syncer.synced_commit_index(), 0);
1305
1306        // Observe round 40 blocks voting for commit 35 from authorities 0 to 2 in CommitVoteMonitor
1307        for i in 0..3 {
1308            let test_block = TestBlock::new(40, i)
1309                .set_commit_votes(vec![CommitRef::new(35, CommitDigest::MIN)])
1310                .build();
1311            let block = VerifiedBlock::new_for_test(test_block);
1312            commit_vote_monitor.observe_block(&block);
1313        }
1314
1315        // Fetches should be scheduled until the unhandled commits threshold.
1316        commit_syncer.try_schedule_once();
1317
1318        // Verify commit syncer is paused after scheduling 15 commits to index 25.
1319        assert_eq!(commit_syncer.unhandled_commits_threshold(), 25);
1320        assert_eq!(commit_syncer.highest_scheduled_index(), Some(25));
1321        let pending_fetches = commit_syncer.pending_fetches();
1322        assert_eq!(pending_fetches.len(), 5);
1323
1324        // Indicate commit index 25 is consumed, and try to schedule again.
1325        commit_consumer_monitor.set_highest_handled_commit(25);
1326        commit_syncer.try_schedule_once();
1327
1328        // Verify commit syncer schedules fetches up to index 35.
1329        assert_eq!(commit_syncer.highest_scheduled_index(), Some(35));
1330        let pending_fetches = commit_syncer.pending_fetches();
1331        assert_eq!(pending_fetches.len(), 7);
1332
1333        // Verify contiguous ranges are scheduled.
1334        for (range, start) in pending_fetches.iter().zip_debug_eq((1..35).step_by(5)) {
1335            assert_eq!(range.start(), start);
1336            assert_eq!(range.end(), start + 4);
1337        }
1338    }
1339}