1use 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
69pub(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 join_and_propagate_panic(self.schedule_task).await;
80 }
81}
82
83pub(crate) struct CommitSyncer<VC: ValidatorNetworkClient, OC: ObserverNetworkClient> {
84 inner: Arc<Inner<VC, OC>>,
88
89 inflight_fetches: JoinSet<(u32, CertifiedCommits)>,
93 pending_fetches: BTreeSet<CommitRange>,
95 fetched_ranges: BTreeMap<CommitRange, CertifiedCommits>,
97 highest_scheduled_index: Option<CommitIndex>,
100 highest_fetched_commit_index: CommitIndex,
103 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 _ = interval.tick() => {
166 self.try_schedule_once();
167 }
168 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 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 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 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 let fetch_after_index = self
221 .synced_commit_index
222 .max(self.highest_scheduled_index.unwrap_or(0));
223 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 let range_start = prev_end + 1;
229 let range_end = prev_end + self.inner.context.parameters.commit_sync_batch_size;
230 if quorum_commit_index < range_end {
234 break;
235 }
236 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 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 if commit_end < target_end {
295 self.pending_fetches
296 .insert((commit_end + 1..=target_end).into());
297 }
298 self.synced_commit_index = self
300 .synced_commit_index
301 .max(self.inner.dag_state.read().last_commit_index());
302 if self.synced_commit_index < commit_end {
304 self.fetched_ranges
305 .insert((commit_start..=commit_end).into(), certified_commits);
306 }
307 while let Some((fetched_commit_range, _commits)) = self.fetched_ranges.first_key_value() {
309 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 metrics.commit_sync_gap_on_processing.inc();
318 break;
319 };
320 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 match self
339 .inner
340 .core_thread_dispatcher
341 .add_certified_commits(commits)
342 .await
343 {
344 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 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 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 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 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 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 const MAX_TIMEOUT_MULTIPLIER: u32 = 12;
460 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 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 timeout_multiplier = (timeout_multiplier + 1).min(MAX_TIMEOUT_MULTIPLIER);
478 let request_timeout = base_timeout * timeout_multiplier;
479 let fetch_timeout = request_timeout * 4;
485 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 sleep(base_timeout).await;
536 }
537 }
538
539 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 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 let (serialized_commits, serialized_blocks) = inner
565 .network_client
566 .fetch_commits(target_peer.clone(), commit_range.clone(), timeout)
567 .await?;
568
569 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 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 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 sleep(timeout * i as u32 / num_chunks).await;
617 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 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 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 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 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 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 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 for commit in &certified_commits {
720 for block in commit.blocks() {
721 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 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 {
745 let mut tracker = inner.round_tracker.write();
746 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 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 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 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 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 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 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 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 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 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 let (mut context, _) = Context::new_for_test(4);
1036 context.own_index = AuthorityIndex::MAX; 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 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 let peers_pool = Arc::new(PeersPool::new(context.clone()));
1065 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 assert!(!context.is_validator(), "Should be an observer node");
1088
1089 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 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 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 let (mut context, _) = Context::new_for_test(4);
1118 context.own_index = AuthorityIndex::MAX; context.parameters = Parameters {
1120 commit_sync_batch_size: 5,
1121 commit_sync_batches_ahead: 10,
1122 commit_sync_parallel_fetches: 8, max_blocks_per_fetch: 5,
1124 ..context.parameters
1125 };
1126 let context = Arc::new(context);
1127
1128 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 let peers_pool = Arc::new(PeersPool::new(context.clone()));
1146 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 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 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 let pending_fetches = commit_syncer.pending_fetches().len();
1196 assert!(pending_fetches > 0, "Should schedule fetches");
1197
1198 commit_syncer.try_start_fetches();
1200
1201 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 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 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 let (context, _) = Context::new_for_test(4);
1238 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 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 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 commit_syncer.try_schedule_once();
1298
1299 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 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 commit_syncer.try_schedule_once();
1317
1318 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 commit_consumer_monitor.set_highest_handled_commit(25);
1326 commit_syncer.try_schedule_once();
1327
1328 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 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}