1use std::{sync::Arc, time::Duration};
5
6use parking_lot::RwLock;
7use tokio::time::Instant;
8use tracing::info;
9
10use crate::{
11 CommitConsumerArgs, CommitConsumerMonitor, CommittedSubDag,
12 block::{BlockAPI, VerifiedBlock},
13 commit::{CommitAPI, load_committed_subdag_from_store},
14 commit_consumer::ReplayMode,
15 commit_finalizer::{CommitFinalizer, CommitFinalizerHandle},
16 context::Context,
17 dag_state::DagState,
18 error::ConsensusResult,
19 linearizer::Linearizer,
20 storage::Store,
21 task::spawn_blocking,
22 transaction_vote_tracker::TransactionVoteTracker,
23};
24
25pub(crate) struct CommitObserver {
39 context: Arc<Context>,
40 dag_state: Arc<RwLock<DagState>>,
41 store: Arc<dyn Store>,
43 transaction_vote_tracker: TransactionVoteTracker,
44 commit_interpreter: Linearizer,
46 commit_finalizer_handle: CommitFinalizerHandle,
48 consumer_monitor: Arc<CommitConsumerMonitor>,
49}
50
51impl CommitObserver {
52 pub(crate) async fn new(
53 context: Arc<Context>,
54 commit_consumer: CommitConsumerArgs,
55 dag_state: Arc<RwLock<DagState>>,
56 transaction_vote_tracker: TransactionVoteTracker,
57 ) -> Self {
58 let store = dag_state.read().store();
59 let commit_interpreter = Linearizer::new(context.clone(), dag_state.clone());
60 let commit_finalizer_handle = CommitFinalizer::start(
61 context.clone(),
62 dag_state.clone(),
63 transaction_vote_tracker.clone(),
64 commit_consumer.commit_sender.clone(),
65 );
66
67 let mut observer = Self {
68 context,
69 dag_state,
70 store,
71 transaction_vote_tracker,
72 commit_interpreter,
73 commit_finalizer_handle,
74 consumer_monitor: commit_consumer.monitor(),
75 };
76 observer.recover_and_send_commits(&commit_consumer).await;
77
78 if let Err(e) = spawn_blocking({
82 let transaction_vote_tracker = observer.transaction_vote_tracker.clone();
83 let gc_round = observer.dag_state.read().gc_round();
84 move || {
85 transaction_vote_tracker.recover_blocks_after_round(gc_round);
86 }
87 })
88 .await
89 {
90 info!("Skipping block recovery for transaction voting: {e}");
91 }
92
93 observer
94 }
95
96 pub(crate) async fn stop(&mut self) {
97 self.commit_finalizer_handle.stop().await;
98 }
99
100 pub(crate) fn handle_committed_leaders(
105 &mut self,
106 committed_leaders: Vec<VerifiedBlock>,
107 local: bool,
108 ) -> ConsensusResult<Vec<CommittedSubDag>> {
109 let _s = self
110 .context
111 .metrics
112 .node_metrics
113 .scope_processing_time
114 .with_label_values(&["CommitObserver::handle_committed_leaders"])
115 .start_timer();
116
117 let mut committed_sub_dags = self.commit_interpreter.handle_commit(committed_leaders);
118 self.report_metrics(&committed_sub_dags);
119
120 for subdag in committed_sub_dags.iter_mut() {
122 subdag.decided_with_local_blocks = local;
123 }
124
125 for commit in committed_sub_dags.iter() {
126 tracing::debug!(
127 "Sending commit {} leader {} to finalization and execution.",
128 commit.commit_ref,
129 commit.leader
130 );
131 tracing::trace!("Committed subdag: {:#?}", commit);
132 self.send_to_finalizer(commit.clone())?;
134 }
135
136 self.dag_state
137 .write()
138 .add_scoring_subdags(committed_sub_dags.clone());
139
140 Ok(committed_sub_dags)
141 }
142
143 pub(crate) fn send_to_finalizer(&self, subdag: CommittedSubDag) -> ConsensusResult<()> {
145 self.consumer_monitor
147 .report_committed(subdag.commit_ref.index, subdag.leader.round);
148 self.commit_finalizer_handle.send(subdag)
149 }
150
151 async fn recover_and_send_commits(&mut self, commit_consumer: &CommitConsumerArgs) {
152 let now = Instant::now();
153
154 let replay_after_commit_index = match commit_consumer.replay_mode {
155 ReplayMode::FromWatermark {
156 replay_after_commit_index,
157 ..
158 } => replay_after_commit_index,
159 ReplayMode::FullEpochPaced => 0,
160 };
161
162 let last_commit = self
163 .store
164 .read_last_commit()
165 .expect("Reading the last commit should not fail");
166 if let Some(commit) = &last_commit {
167 self.consumer_monitor
168 .report_committed(commit.index(), commit.leader().round);
169 }
170 let replay_target = if matches!(commit_consumer.replay_mode, ReplayMode::FullEpochPaced) {
173 let target = if self.context.protocol_config.transaction_voting_enabled() {
174 self.store
175 .read_last_finalized_commit()
176 .expect("Reading the last finalized commit should not fail")
177 .map_or(0, |commit| commit.index)
178 } else {
179 last_commit.as_ref().map_or(0, |commit| commit.index())
180 };
181 assert!(
182 target <= last_commit.as_ref().map_or(0, |commit| commit.index()),
183 "finalized replay target cannot exceed the consensus store head"
184 );
185 self.consumer_monitor.set_replay_target(target);
186 target
187 } else {
188 0
189 };
190 let Some(last_commit) = &last_commit else {
191 assert_eq!(
192 replay_after_commit_index, 0,
193 "Commit replay should start at the beginning if there is no commit history"
194 );
195 info!("Nothing to recover for commit observer - starting new epoch");
196 return;
197 };
198
199 let last_commit_index = last_commit.index();
200 if last_commit_index == replay_after_commit_index {
201 info!(
202 "Nothing to recover for commit observer - replay is requested immediately after last commit index {last_commit_index}"
203 );
204 return;
205 }
206 assert!(last_commit_index > replay_after_commit_index);
207
208 info!(
209 "Recovering commit observer in the range [{}..={last_commit_index}]",
210 replay_after_commit_index + 1,
211 );
212
213 const COMMIT_RECOVERY_BATCH_SIZE: u32 = if cfg!(test) { 3 } else { 250 };
216
217 let mut last_sent_commit_index = replay_after_commit_index;
218
219 self.dag_state.read().ensure_commits_to_write_is_empty();
221
222 let mut seen_unfinalized_commit = false;
223 for start_index in (replay_after_commit_index + 1..=last_commit_index)
224 .step_by(COMMIT_RECOVERY_BATCH_SIZE as usize)
225 {
226 let end_index = start_index
227 .saturating_add(COMMIT_RECOVERY_BATCH_SIZE - 1)
228 .min(last_commit_index);
229
230 let unsent_commits = self
231 .store
232 .scan_commits((start_index..=end_index).into())
233 .expect("Scanning commits should not fail");
234 assert_eq!(
235 unsent_commits.len() as u32,
236 end_index.checked_sub(start_index).unwrap() + 1,
237 "Gap in scanned commits: start index: {start_index}, end index: {end_index}, commits: {:?}",
238 unsent_commits,
239 );
240
241 self.dag_state
244 .write()
245 .recover_commits_to_write(unsent_commits.clone());
246
247 info!(
248 "Recovering {} unsent commits in range [{start_index}..={end_index}]",
249 unsent_commits.len()
250 );
251
252 for commit in unsent_commits.into_iter() {
255 last_sent_commit_index += 1;
257 assert_eq!(commit.index(), last_sent_commit_index);
258
259 let committed_sub_dag =
260 load_committed_subdag_from_store(self.store.as_ref(), commit);
261
262 if matches!(commit_consumer.replay_mode, ReplayMode::FullEpochPaced)
263 && self.context.protocol_config.transaction_voting_enabled()
264 && committed_sub_dag.commit_ref.index <= replay_target
265 {
266 assert!(
270 committed_sub_dag.recovered_rejected_transactions,
271 "missing rejected transactions below the finalized replay target"
272 );
273 }
274
275 if !committed_sub_dag.recovered_rejected_transactions && !seen_unfinalized_commit {
276 info!(
277 "Starting to recover unfinalized commit from {}",
278 committed_sub_dag.commit_ref
279 );
280 seen_unfinalized_commit = true;
283 }
284
285 if seen_unfinalized_commit {
286 assert!(!committed_sub_dag.recovered_rejected_transactions);
288 assert!(!committed_sub_dag.decided_with_local_blocks);
291 self.transaction_vote_tracker
294 .recover_and_vote_on_blocks(committed_sub_dag.blocks.clone());
295 }
296
297 self.commit_finalizer_handle
298 .send(committed_sub_dag)
299 .unwrap();
300
301 self.context
302 .metrics
303 .node_metrics
304 .commit_observer_last_recovered_commit_index
305 .set(last_sent_commit_index as i64);
306
307 tokio::task::yield_now().await;
308 }
309 if matches!(commit_consumer.replay_mode, ReplayMode::FullEpochPaced) {
310 tokio::select! {
315 biased;
316 () = self.consumer_monitor.wait_for_handled(end_index.min(replay_target)) => {},
317 () = commit_consumer.commit_sender.closed() => {
318 panic!("consensus commit consumer closed during startup replay");
319 }
320 }
321 }
322 }
323
324 assert_eq!(
325 last_sent_commit_index, last_commit_index,
326 "We should have sent all commits up to the last commit {}",
327 last_commit_index
328 );
329
330 info!(
331 "Commit observer recovery [{}..={}] completed, took {:?}",
332 replay_after_commit_index + 1,
333 last_commit_index,
334 now.elapsed()
335 );
336 }
337
338 pub(crate) fn report_commit_metrics(&self, commit: &CommittedSubDag) {
341 let metrics = &self.context.metrics.node_metrics;
342 let utc_now = self.context.clock.timestamp_utc_ms();
343
344 info!(
345 "Consensus commit {} with leader {} has {} blocks",
346 commit.commit_ref,
347 commit.leader,
348 commit.blocks.len()
349 );
350
351 metrics
352 .last_committed_leader_round
353 .set(commit.leader.round as i64);
354 metrics
355 .last_commit_index
356 .set(commit.commit_ref.index as i64);
357 metrics
358 .blocks_per_commit_count
359 .observe(commit.blocks.len() as f64);
360
361 for block in &commit.blocks {
362 let latency_ms = utc_now.saturating_sub(block.timestamp_ms());
363 metrics
364 .block_commit_latency
365 .observe(Duration::from_millis(latency_ms).as_secs_f64());
366 if block.author() == self.context.own_index {
367 metrics
368 .proposed_block_commit_latency
369 .observe(Duration::from_millis(latency_ms).as_secs_f64());
370 }
371 }
372 }
373
374 fn report_metrics(&self, committed: &[CommittedSubDag]) {
375 for commit in committed {
376 self.report_commit_metrics(commit);
377 }
378 self.context
382 .metrics
383 .node_metrics
384 .sub_dags_per_commit_count
385 .observe(committed.len() as f64);
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use consensus_types::block::BlockRef;
392 use mysten_metrics::monitored_mpsc::UnboundedReceiver;
393 use parking_lot::RwLock;
394 use rstest::rstest;
395 use tokio::time::timeout;
396
397 use super::*;
398 use crate::{
399 CommitIndex, block_verifier::NoopBlockVerifier, context::Context, dag_state::DagState,
400 linearizer::median_timestamp_by_stake, storage::mem_store::MemStore,
401 test_dag_builder::DagBuilder,
402 };
403
404 fn full_replay_fixture(
405 commits: u32,
406 finalized: u32,
407 voting: bool,
408 ) -> (Arc<Context>, Arc<RwLock<DagState>>, TransactionVoteTracker) {
409 let mut config = consensus_config::ConsensusProtocolConfig::for_testing();
410 config.set_transaction_voting_enabled_for_testing(voting);
411 let context = Arc::new(Context::new_for_test(4).0.with_protocol_config(config));
412 let store = Arc::new(MemStore::new());
413 if commits > 0 {
414 let mut builder = DagBuilder::new(context.clone());
415 builder.layers(1..=(commits * 2 + 2)).build();
416 let produced = builder.get_sub_dag_and_commits(1..=commits);
417 assert!(produced.len() >= commits as usize);
418 for (index, (subdag, commit)) in produced.into_iter().take(commits as usize).enumerate()
419 {
420 let finalized_rows = if (index as u32) < finalized {
421 vec![(commit.reference(), std::collections::BTreeMap::new())]
422 } else {
423 Vec::new()
424 };
425 store
426 .write(crate::storage::WriteBatch::new(
427 subdag.blocks,
428 vec![commit],
429 Vec::new(),
430 finalized_rows,
431 ))
432 .unwrap();
433 }
434 }
435 let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
436 let tracker = TransactionVoteTracker::new(
437 context.clone(),
438 Arc::new(NoopBlockVerifier {}),
439 dag_state.clone(),
440 );
441 (context, dag_state, tracker)
442 }
443
444 #[tokio::test]
445 async fn supplied_watermark_replay_keeps_its_target_without_waiting_for_acknowledgements() {
446 let (context, dag_state, tracker) = full_replay_fixture(7, 7, true);
447 let (args, mut receiver) = CommitConsumerArgs::new(2, 4);
448 let monitor = args.monitor();
449 assert_eq!(monitor.replay_target(), Some(4));
450 assert_eq!(monitor.highest_handled_commit(), 2);
451 let mut observer = timeout(
452 Duration::from_secs(5),
453 CommitObserver::new(context, args, dag_state, tracker),
454 )
455 .await
456 .expect("legacy replay must not require application acknowledgements during startup");
457 assert_eq!(monitor.replay_target(), Some(4));
458 assert_eq!(monitor.progress().highest_committed_index, 7);
459 assert_eq!(monitor.highest_handled_commit(), 2);
460 for index in 3..=7 {
461 let commit = timeout(Duration::from_secs(5), receiver.recv())
462 .await
463 .unwrap()
464 .unwrap();
465 assert_eq!(commit.commit_ref.index, index);
466 }
467 monitor.set_highest_handled_commit(3);
468 assert!(
469 timeout(
470 Duration::from_millis(20),
471 monitor.replay_to_consumer_last_processed_commit_complete(),
472 )
473 .await
474 .is_err()
475 );
476 monitor.set_highest_handled_commit(4);
477 timeout(
478 Duration::from_secs(1),
479 monitor.replay_to_consumer_last_processed_commit_complete(),
480 )
481 .await
482 .expect("legacy readiness uses the supplied watermark, not the stored head");
483 observer.stop().await;
484 }
485
486 #[tokio::test]
487 async fn full_replay_is_paced_by_applied_commits_even_without_finalized_rows() {
488 let (context, dag_state, tracker) = full_replay_fixture(7, 0, false);
489 let stored_head_round = dag_state.read().last_commit_round();
490 let (args, mut receiver) = CommitConsumerArgs::new_with_full_replay();
491 let monitor = args.monitor();
492 let recovery = tokio::spawn(CommitObserver::new(context, args, dag_state, tracker));
493 for index in 1..=3 {
494 let commit = timeout(Duration::from_secs(5), receiver.recv())
495 .await
496 .unwrap()
497 .unwrap();
498 assert_eq!(commit.commit_ref.index, index);
499 assert_eq!(monitor.replay_target(), Some(7));
500 assert_eq!(monitor.progress().highest_committed_index, 7);
501 assert_eq!(
502 monitor.progress().highest_committed_round,
503 stored_head_round
504 );
505 if index < 3 {
506 monitor.set_highest_handled_commit(index);
507 }
508 }
509 assert!(
512 timeout(Duration::from_millis(100), receiver.recv())
513 .await
514 .is_err(),
515 "recovery must wait for the consumer to apply the current batch"
516 );
517 assert!(!recovery.is_finished());
518 monitor.set_highest_handled_commit(3);
519 for index in 4..=7 {
520 let commit = timeout(Duration::from_secs(5), receiver.recv())
521 .await
522 .unwrap()
523 .unwrap();
524 assert_eq!(commit.commit_ref.index, index);
525 monitor.set_highest_handled_commit(index);
526 }
527 let mut observer = timeout(Duration::from_secs(5), recovery)
528 .await
529 .unwrap()
530 .unwrap();
531 monitor
532 .replay_to_consumer_last_processed_commit_complete()
533 .await;
534 observer.stop().await;
535 }
536
537 #[rstest]
538 #[case(0)]
539 #[case(3)]
540 #[tokio::test]
541 async fn full_replay_does_not_wait_for_an_unfinalized_tail(#[case] finalized: u32) {
542 let (context, dag_state, tracker) = full_replay_fixture(7, finalized, true);
543 let (args, mut receiver) = CommitConsumerArgs::new_with_full_replay();
544 let monitor = args.monitor();
545 let recovery = tokio::spawn(CommitObserver::new(context, args, dag_state, tracker));
546 for index in 1..=finalized {
547 let commit = timeout(Duration::from_secs(5), receiver.recv())
548 .await
549 .unwrap()
550 .unwrap();
551 assert_eq!(commit.commit_ref.index, index);
552 monitor.set_highest_handled_commit(index);
553 }
554 let mut observer = timeout(Duration::from_secs(5), recovery)
555 .await
556 .expect("unfinalized commits must not prevent live consensus startup")
557 .unwrap();
558 assert_eq!(monitor.replay_target(), Some(finalized));
559 assert_eq!(monitor.highest_handled_commit(), finalized);
560 monitor
561 .replay_to_consumer_last_processed_commit_complete()
562 .await;
563 observer.stop().await;
564 }
565
566 #[tokio::test]
567 async fn full_replay_of_an_empty_store_publishes_an_empty_target() {
568 let (context, dag_state, tracker) = full_replay_fixture(0, 0, false);
569 let (args, _receiver) = CommitConsumerArgs::new_with_full_replay();
570 let monitor = args.monitor();
571 assert_eq!(monitor.replay_target(), None);
572 let mut observer = CommitObserver::new(context, args, dag_state, tracker).await;
573 assert_eq!(monitor.replay_target(), Some(0));
574 timeout(
575 Duration::from_secs(1),
576 monitor.replay_to_consumer_last_processed_commit_complete(),
577 )
578 .await
579 .unwrap();
580 observer.stop().await;
581 }
582
583 #[tokio::test]
584 async fn a_closed_full_replay_consumer_does_not_leave_startup_waiting() {
585 let (context, dag_state, tracker) = full_replay_fixture(7, 0, false);
586 let (args, mut receiver) = CommitConsumerArgs::new_with_full_replay();
587 let recovery = tokio::spawn(CommitObserver::new(context, args, dag_state, tracker));
588 for _ in 0..3 {
589 timeout(Duration::from_secs(5), receiver.recv())
590 .await
591 .unwrap()
592 .unwrap();
593 }
594 drop(receiver);
595 let error = timeout(Duration::from_secs(5), recovery)
596 .await
597 .unwrap()
598 .err()
599 .expect(
600 "a vanished consumer must fail startup instead of waiting for its acknowledgement",
601 );
602 assert!(error.is_panic());
603 assert!(
604 error
605 .to_string()
606 .contains("consensus commit consumer closed during startup replay")
607 );
608 }
609
610 #[tokio::test]
611 #[should_panic(expected = "finalized replay target cannot exceed the consensus store head")]
612 async fn a_finalized_target_beyond_stored_history_fails_replay() {
613 let (context, dag_state, tracker) = full_replay_fixture(3, 3, true);
614 let store = dag_state.read().store();
615 let mut missing = store.read_last_commit().unwrap().unwrap().reference();
616 missing.index += 1;
617 store
618 .write(crate::storage::WriteBatch::new(
619 Vec::new(),
620 Vec::new(),
621 Vec::new(),
622 vec![(missing, std::collections::BTreeMap::new())],
623 ))
624 .unwrap();
625 let (args, _receiver) = CommitConsumerArgs::new_with_full_replay();
626 CommitObserver::new(context, args, dag_state, tracker).await;
627 }
628
629 #[tokio::test]
630 async fn a_finalization_hole_below_the_replay_target_fails_startup() {
631 let (context, dag_state, tracker) = full_replay_fixture(5, 0, true);
632 let store = dag_state.read().store();
633 let finalized = store
634 .scan_commits((1..=5).into())
635 .unwrap()
636 .into_iter()
637 .filter(|commit| commit.index() != 3)
638 .map(|commit| (commit.reference(), std::collections::BTreeMap::new()))
639 .collect();
640 store
641 .write(crate::storage::WriteBatch::new(
642 Vec::new(),
643 Vec::new(),
644 Vec::new(),
645 finalized,
646 ))
647 .unwrap();
648 let (args, mut receiver) = CommitConsumerArgs::new_with_full_replay();
649 let monitor = args.monitor();
650 let consume = tokio::spawn(async move {
651 while let Some(commit) = receiver.recv().await {
652 monitor.set_highest_handled_commit(commit.commit_ref.index);
653 }
654 });
655 let recovery = tokio::spawn(CommitObserver::new(context, args, dag_state, tracker));
656 let error = timeout(Duration::from_secs(5), recovery)
657 .await
658 .unwrap()
659 .err()
660 .expect("a hole in finalized history must fail startup");
661 assert!(error.is_panic());
662 assert!(
663 error
664 .to_string()
665 .contains("missing rejected transactions below the finalized replay target")
666 );
667 consume.abort();
668 }
669
670 #[rstest]
671 #[tokio::test]
672 async fn test_handle_commit() {
673 telemetry_subscribers::init_for_testing();
674 let num_authorities = 4;
675 let (context, _keys) = Context::new_for_test(num_authorities);
676 let context = Arc::new(context);
677
678 let mem_store = Arc::new(MemStore::new());
679 let dag_state = Arc::new(RwLock::new(DagState::new(
680 context.clone(),
681 mem_store.clone(),
682 )));
683 let last_processed_commit_index = 0;
684 let (commit_consumer, mut commit_receiver) =
685 CommitConsumerArgs::new(0, last_processed_commit_index);
686 let transaction_vote_tracker = TransactionVoteTracker::new(
687 context.clone(),
688 Arc::new(NoopBlockVerifier {}),
689 dag_state.clone(),
690 );
691
692 let mut observer = CommitObserver::new(
693 context.clone(),
694 commit_consumer,
695 dag_state.clone(),
696 transaction_vote_tracker.clone(),
697 )
698 .await;
699
700 let num_rounds = 10;
702 let mut builder = DagBuilder::new(context.clone());
703 builder
704 .layers(1..=num_rounds)
705 .build()
706 .persist_layers(dag_state.clone());
707 transaction_vote_tracker.add_voted_blocks(
708 builder
709 .all_blocks()
710 .iter()
711 .map(|b| (b.clone(), vec![]))
712 .collect(),
713 );
714
715 let leaders = builder
716 .leader_blocks(1..=num_rounds)
717 .into_iter()
718 .map(Option::unwrap)
719 .collect::<Vec<_>>();
720
721 let commits = observer
722 .handle_committed_leaders(leaders.clone(), true)
723 .unwrap();
724 let head = commits.last().unwrap();
725 assert_eq!(
726 observer.consumer_monitor.progress().highest_committed_index,
727 head.commit_ref.index
728 );
729 assert_eq!(
730 observer.consumer_monitor.progress().highest_committed_round,
731 head.leader.round
732 );
733 assert_eq!(observer.consumer_monitor.highest_handled_commit(), 0);
734
735 let mut expected_stored_refs: Vec<BlockRef> = vec![];
737 for (idx, subdag) in commits.iter().enumerate() {
738 tracing::info!("{subdag:?}");
739 assert_eq!(subdag.leader, leaders[idx].reference());
740
741 let expected_ts = {
742 let block_refs = leaders[idx]
743 .ancestors()
744 .iter()
745 .filter(|block_ref| block_ref.round == leaders[idx].round() - 1)
746 .cloned()
747 .collect::<Vec<_>>();
748 let block_opts = dag_state.read().get_blocks(&block_refs);
749 let blocks = block_opts.iter().map(|block_opt| {
750 block_opt
751 .as_ref()
752 .expect("We should have all blocks in dag state.")
753 });
754 median_timestamp_by_stake(&context, blocks).unwrap()
755 };
756
757 let expected_ts = if idx == 0 {
758 expected_ts
759 } else {
760 expected_ts.max(commits[idx - 1].timestamp_ms)
761 };
762
763 assert_eq!(expected_ts, subdag.timestamp_ms);
764
765 if idx == 0 {
766 assert_eq!(subdag.blocks.len(), 1);
769 } else {
770 assert_eq!(subdag.blocks.len(), num_authorities);
773 }
774 for block in subdag.blocks.iter() {
775 expected_stored_refs.push(block.reference());
776 assert!(block.round() <= leaders[idx].round());
777 }
778 assert_eq!(subdag.commit_ref.index, idx as CommitIndex + 1);
779 }
780
781 let mut processed_subdag_index = 0;
783 while let Ok(Some(subdag)) = timeout(Duration::from_secs(1), commit_receiver.recv()).await {
784 assert_eq!(subdag, commits[processed_subdag_index]);
785 processed_subdag_index = subdag.commit_ref.index as usize;
786 if processed_subdag_index == leaders.len() {
787 break;
788 }
789 }
790 assert_eq!(processed_subdag_index, leaders.len());
791
792 let own_committed_blocks = commits
795 .iter()
796 .flat_map(|commit| commit.blocks.iter())
797 .filter(|block| block.author() == context.own_index)
798 .count() as u64;
799 assert!(own_committed_blocks > 0);
800 assert_eq!(
801 context
802 .metrics
803 .node_metrics
804 .proposed_block_commit_latency
805 .get_sample_count(),
806 own_committed_blocks
807 );
808 assert_eq!(
809 context
810 .metrics
811 .node_metrics
812 .proposed_block_finalization_latency
813 .get_sample_count(),
814 own_committed_blocks
815 );
816
817 verify_channel_empty(&mut commit_receiver).await;
818
819 let last_commit = mem_store.read_last_commit().unwrap().unwrap();
821 assert_eq!(
822 last_commit.index(),
823 commits.last().unwrap().commit_ref.index
824 );
825 let all_stored_commits = mem_store
826 .scan_commits((0..=CommitIndex::MAX).into())
827 .unwrap();
828 assert_eq!(all_stored_commits.len(), leaders.len());
829 let blocks_existence = mem_store.contains_blocks(&expected_stored_refs).unwrap();
830 assert!(blocks_existence.iter().all(|exists| *exists));
831 }
832
833 #[tokio::test]
834 async fn test_recover_and_send_commits() {
835 telemetry_subscribers::init_for_testing();
836 let num_authorities = 4;
837 let context = Arc::new(Context::new_for_test(num_authorities).0);
838 let mem_store = Arc::new(MemStore::new());
839 let dag_state = Arc::new(RwLock::new(DagState::new(
840 context.clone(),
841 mem_store.clone(),
842 )));
843 let transaction_vote_tracker = TransactionVoteTracker::new(
844 context.clone(),
845 Arc::new(NoopBlockVerifier {}),
846 dag_state.clone(),
847 );
848 let last_processed_commit_index = 0;
849 let (commit_consumer, mut commit_receiver) =
850 CommitConsumerArgs::new(0, last_processed_commit_index);
851
852 let mut observer = CommitObserver::new(
853 context.clone(),
854 commit_consumer,
855 dag_state.clone(),
856 transaction_vote_tracker.clone(),
857 )
858 .await;
859
860 let num_rounds = 10;
862 let mut builder = DagBuilder::new(context.clone());
863 builder
864 .layers(1..=num_rounds)
865 .build()
866 .persist_layers(dag_state.clone());
867 transaction_vote_tracker.add_voted_blocks(
868 builder
869 .all_blocks()
870 .iter()
871 .map(|b| (b.clone(), vec![]))
872 .collect(),
873 );
874
875 let leaders = builder
876 .leader_blocks(1..=num_rounds)
877 .into_iter()
878 .map(Option::unwrap)
879 .collect::<Vec<_>>();
880
881 let expected_last_processed_index: usize = 2;
884 let mut commits = observer
885 .handle_committed_leaders(leaders[..expected_last_processed_index].to_vec(), true)
886 .unwrap();
887
888 let mut processed_subdag_index = 0;
890 while let Ok(Some(subdag)) = timeout(Duration::from_secs(1), commit_receiver.recv()).await {
891 tracing::info!("Processed {subdag}");
892 assert_eq!(subdag, commits[processed_subdag_index]);
893 processed_subdag_index = subdag.commit_ref.index as usize;
894 if processed_subdag_index == expected_last_processed_index {
895 break;
896 }
897 }
898 assert_eq!(processed_subdag_index, expected_last_processed_index);
899
900 verify_channel_empty(&mut commit_receiver).await;
901
902 let last_commit = mem_store.read_last_commit().unwrap().unwrap();
904 assert_eq!(
905 last_commit.index(),
906 expected_last_processed_index as CommitIndex
907 );
908
909 commits.append(
913 &mut observer
914 .handle_committed_leaders(leaders[expected_last_processed_index..].to_vec(), true)
915 .unwrap(),
916 );
917
918 let expected_last_sent_index = num_rounds as usize;
919 while let Ok(Some(subdag)) = timeout(Duration::from_secs(1), commit_receiver.recv()).await {
920 tracing::info!("{subdag} was sent but not processed by consumer");
921 assert_eq!(subdag, commits[processed_subdag_index]);
922 assert!(subdag.decided_with_local_blocks);
923 processed_subdag_index = subdag.commit_ref.index as usize;
924 if processed_subdag_index == expected_last_sent_index {
925 break;
926 }
927 }
928 assert_eq!(processed_subdag_index, expected_last_sent_index);
929
930 verify_channel_empty(&mut commit_receiver).await;
931
932 let last_commit = mem_store.read_last_commit().unwrap().unwrap();
936 assert_eq!(last_commit.index(), expected_last_sent_index as CommitIndex);
937
938 {
940 let replay_after_commit_index = 2;
941 let consumer_last_processed_commit_index = 10;
942 let dag_state = Arc::new(RwLock::new(DagState::new(
943 context.clone(),
944 mem_store.clone(),
945 )));
946 let (commit_consumer, mut commit_receiver) = CommitConsumerArgs::new(
947 replay_after_commit_index,
948 consumer_last_processed_commit_index,
949 );
950 let _observer = CommitObserver::new(
951 context.clone(),
952 commit_consumer,
953 dag_state.clone(),
954 transaction_vote_tracker.clone(),
955 )
956 .await;
957
958 let mut processed_subdag_index = replay_after_commit_index;
959 while let Ok(Some(mut subdag)) =
960 timeout(Duration::from_secs(1), commit_receiver.recv()).await
961 {
962 tracing::info!("Received {subdag} on recovery");
963 assert_eq!(subdag.commit_ref.index, processed_subdag_index + 1);
964 assert!(subdag.recovered_rejected_transactions);
965
966 subdag.recovered_rejected_transactions = false;
968 assert_eq!(subdag, commits[processed_subdag_index as usize]);
969
970 assert!(subdag.decided_with_local_blocks);
971 processed_subdag_index = subdag.commit_ref.index;
972 if processed_subdag_index == consumer_last_processed_commit_index {
973 break;
974 }
975 }
976 assert_eq!(processed_subdag_index, consumer_last_processed_commit_index);
977
978 verify_channel_empty(&mut commit_receiver).await;
979 }
980
981 {
983 let replay_after_commit_index = 10;
984 let consumer_last_processed_commit_index = 10;
985 let dag_state = Arc::new(RwLock::new(DagState::new(
986 context.clone(),
987 mem_store.clone(),
988 )));
989 let (commit_consumer, mut commit_receiver) = CommitConsumerArgs::new(
992 replay_after_commit_index,
993 consumer_last_processed_commit_index,
994 );
995 let _observer = CommitObserver::new(
996 context.clone(),
997 commit_consumer,
998 dag_state.clone(),
999 transaction_vote_tracker.clone(),
1000 )
1001 .await;
1002
1003 verify_channel_empty(&mut commit_receiver).await;
1006 }
1007
1008 {
1010 let replay_after_commit_index = 2;
1011 let consumer_last_processed_commit_index = 4;
1012 let dag_state = Arc::new(RwLock::new(DagState::new(
1013 context.clone(),
1014 mem_store.clone(),
1015 )));
1016 let (commit_consumer, mut commit_receiver) = CommitConsumerArgs::new(
1017 replay_after_commit_index,
1018 consumer_last_processed_commit_index,
1019 );
1020 let _observer = CommitObserver::new(
1021 context.clone(),
1022 commit_consumer,
1023 dag_state.clone(),
1024 transaction_vote_tracker.clone(),
1025 )
1026 .await;
1027
1028 let mut processed_subdag_index = replay_after_commit_index;
1031 while let Ok(Some(subdag)) =
1032 timeout(Duration::from_secs(1), commit_receiver.recv()).await
1033 {
1034 tracing::info!("Received {subdag} on recovery");
1035 assert_eq!(subdag.commit_ref.index, processed_subdag_index + 1);
1036 assert!(subdag.decided_with_local_blocks);
1037 processed_subdag_index = subdag.commit_ref.index;
1038 if processed_subdag_index == expected_last_sent_index as CommitIndex {
1039 break;
1040 }
1041 }
1042 assert_eq!(
1043 processed_subdag_index,
1044 expected_last_sent_index as CommitIndex
1045 );
1046
1047 verify_channel_empty(&mut commit_receiver).await;
1048 }
1049
1050 {
1054 let replay_after_commit_index = 2;
1055 let consumer_last_processed_commit_index = 20;
1056 let dag_state = Arc::new(RwLock::new(DagState::new(
1057 context.clone(),
1058 mem_store.clone(),
1059 )));
1060 let (commit_consumer, mut commit_receiver) = CommitConsumerArgs::new(
1061 replay_after_commit_index,
1062 consumer_last_processed_commit_index,
1063 );
1064 let _observer = CommitObserver::new(
1065 context.clone(),
1066 commit_consumer,
1067 dag_state.clone(),
1068 transaction_vote_tracker.clone(),
1069 )
1070 .await;
1071
1072 let mut processed_subdag_index = replay_after_commit_index;
1075 while let Ok(Some(mut subdag)) =
1076 timeout(Duration::from_secs(1), commit_receiver.recv()).await
1077 {
1078 tracing::info!("Received {subdag} on recovery");
1079 assert_eq!(subdag.commit_ref.index, processed_subdag_index + 1);
1080 assert!(subdag.recovered_rejected_transactions);
1081
1082 subdag.recovered_rejected_transactions = false;
1084 assert_eq!(subdag, commits[processed_subdag_index as usize]);
1085
1086 assert!(subdag.decided_with_local_blocks);
1087 processed_subdag_index = subdag.commit_ref.index;
1088 if processed_subdag_index == expected_last_sent_index as CommitIndex {
1089 break;
1090 }
1091 }
1092 assert_eq!(
1093 processed_subdag_index,
1094 expected_last_sent_index as CommitIndex
1095 );
1096 assert_eq!(10, expected_last_sent_index);
1097
1098 verify_channel_empty(&mut commit_receiver).await;
1099 }
1100 }
1101
1102 async fn verify_channel_empty(receiver: &mut UnboundedReceiver<CommittedSubDag>) {
1104 if let Ok(Some(_)) = timeout(Duration::from_secs(1), receiver.recv()).await {
1105 panic!("Expected the consensus output channel to be empty, but found more subdags.")
1106 }
1107 }
1108}