1use std::{
5 collections::{BTreeMap, BTreeSet, VecDeque},
6 sync::Arc,
7 time::Duration,
8};
9
10use consensus_config::Stake;
11use consensus_types::block::{BlockRef, Round, TransactionIndex};
12use mysten_metrics::{
13 monitored_mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
14 monitored_scope, spawn_logged_monitored_task,
15};
16use parking_lot::RwLock;
17
18use crate::{
19 BlockAPI, CommitIndex, CommittedSubDag, VerifiedBlock,
20 commit::DEFAULT_WAVE_LENGTH,
21 context::Context,
22 dag_state::DagState,
23 error::{ConsensusError, ConsensusResult},
24 stake_aggregator::{QuorumThreshold, StakeAggregator},
25 task::join_and_propagate_panic,
26 transaction_vote_tracker::TransactionVoteTracker,
27};
28
29pub(crate) const INDIRECT_REJECT_DEPTH: Round = 3;
33
34pub(crate) struct CommitFinalizerHandle {
36 sender: Option<UnboundedSender<CommittedSubDag>>,
37 task: Option<tokio::task::JoinHandle<()>>,
38}
39
40impl CommitFinalizerHandle {
41 pub(crate) fn send(&self, commit: CommittedSubDag) -> ConsensusResult<()> {
43 self.sender
44 .as_ref()
45 .ok_or(ConsensusError::Shutdown)?
46 .send(commit)
47 .map_err(|e| {
48 tracing::warn!(
49 "Failed to send to commit finalizer, probably due to shutdown: {e:?}"
50 );
51 ConsensusError::Shutdown
52 })
53 }
54
55 pub(crate) async fn stop(&mut self) {
56 self.sender.take();
58 if let Some(task) = self.task.take() {
59 join_and_propagate_panic(task).await;
60 }
61 }
62}
63
64pub struct CommitFinalizer {
86 context: Arc<Context>,
87 dag_state: Arc<RwLock<DagState>>,
88 transaction_vote_tracker: TransactionVoteTracker,
89 commit_sender: UnboundedSender<CommittedSubDag>,
90
91 last_processed_commit: Option<CommitIndex>,
93 pending_commits: VecDeque<CommitState>,
95 blocks: Arc<RwLock<BTreeMap<BlockRef, RwLock<BlockState>>>>,
97}
98
99impl CommitFinalizer {
100 pub fn new(
101 context: Arc<Context>,
102 dag_state: Arc<RwLock<DagState>>,
103 transaction_vote_tracker: TransactionVoteTracker,
104 commit_sender: UnboundedSender<CommittedSubDag>,
105 ) -> Self {
106 Self {
107 context,
108 dag_state,
109 transaction_vote_tracker,
110 commit_sender,
111 last_processed_commit: None,
112 pending_commits: VecDeque::new(),
113 blocks: Arc::new(RwLock::new(BTreeMap::new())),
114 }
115 }
116
117 pub(crate) fn start(
118 context: Arc<Context>,
119 dag_state: Arc<RwLock<DagState>>,
120 transaction_vote_tracker: TransactionVoteTracker,
121 commit_sender: UnboundedSender<CommittedSubDag>,
122 ) -> CommitFinalizerHandle {
123 let processor = Self::new(context, dag_state, transaction_vote_tracker, commit_sender);
124 let (sender, receiver) = unbounded_channel("consensus_commit_finalizer");
125 let task =
126 spawn_logged_monitored_task!(processor.run(receiver), "consensus_commit_finalizer");
127 CommitFinalizerHandle {
128 sender: Some(sender),
129 task: Some(task),
130 }
131 }
132
133 async fn run(mut self, mut receiver: UnboundedReceiver<CommittedSubDag>) {
134 while let Some(committed_sub_dag) = receiver.recv().await {
135 let already_finalized = !self.context.protocol_config.transaction_voting_enabled()
136 || committed_sub_dag.recovered_rejected_transactions;
137 let finalized_commits = if !already_finalized {
138 self.process_commit(committed_sub_dag).await
139 } else {
140 vec![committed_sub_dag]
141 };
142 if !finalized_commits.is_empty() {
143 self.try_update_gc_round(finalized_commits.last().unwrap().leader.round);
147 let mut dag_state = self.dag_state.write();
148 if !already_finalized {
149 for commit in &finalized_commits {
151 dag_state.add_finalized_commit(
152 commit.commit_ref,
153 commit.rejected_transactions_by_block.clone(),
154 );
155 }
156 }
157 dag_state.flush();
162 }
163 for commit in finalized_commits {
164 if let Err(e) = self.commit_sender.send(commit) {
165 tracing::warn!(
166 "Failed to send to commit handler, probably due to shutdown: {e:?}"
167 );
168 return;
169 }
170 }
171 }
172 }
173
174 pub async fn process_commit(
175 &mut self,
176 committed_sub_dag: CommittedSubDag,
177 ) -> Vec<CommittedSubDag> {
178 let _scope = monitored_scope("CommitFinalizer::process_commit");
179
180 if let Some(last_processed_commit) = self.last_processed_commit {
181 assert_eq!(
182 last_processed_commit + 1,
183 committed_sub_dag.commit_ref.index
184 );
185 }
186 self.last_processed_commit = Some(committed_sub_dag.commit_ref.index);
187
188 self.pending_commits
189 .push_back(CommitState::new(committed_sub_dag));
190
191 let mut finalized_commits = vec![];
192
193 for i in 0..self.pending_commits.len() {
212 let commit_state = &self.pending_commits[i];
213 if commit_state.pending_blocks.is_empty() {
214 continue;
216 }
217 if !commit_state.commit.decided_with_local_blocks {
222 let last_commit_state = self.pending_commits.back().unwrap();
223 if commit_state.commit.leader.round + DEFAULT_WAVE_LENGTH
224 > last_commit_state.commit.leader.round
225 {
226 break;
227 }
228 }
229 self.try_direct_finalize_commit(i);
230 }
231 let direct_finalized_commits = self.pop_finalized_commits();
232 self.context
233 .metrics
234 .node_metrics
235 .finalizer_output_commits
236 .with_label_values(&["direct"])
237 .inc_by(direct_finalized_commits.len() as u64);
238 finalized_commits.extend(direct_finalized_commits);
239
240 if !self.pending_commits.is_empty() {
243 self.link_blocks_in_last_commit();
250 self.append_origin_descendants_from_last_commit();
251 while self.pending_commits.len() > 1 {
255 if !self.pending_commits[0].pending_blocks.is_empty() {
258 break;
259 }
260 self.try_indirect_finalize_first_commit().await;
262 let indirect_finalized_commits = self.pop_finalized_commits();
263 if indirect_finalized_commits.is_empty() {
264 break;
266 }
267 self.context
268 .metrics
269 .node_metrics
270 .finalizer_output_commits
271 .with_label_values(&["indirect"])
272 .inc_by(indirect_finalized_commits.len() as u64);
273 finalized_commits.extend(indirect_finalized_commits);
274 }
275 }
276
277 let utc_now = self.context.clock.timestamp_utc_ms();
278 for commit in &finalized_commits {
279 for block in commit
280 .blocks
281 .iter()
282 .filter(|block| block.author() == self.context.own_index)
283 {
284 let latency_ms = utc_now.saturating_sub(block.timestamp_ms());
285 self.context
286 .metrics
287 .node_metrics
288 .proposed_block_finalization_latency
289 .observe(Duration::from_millis(latency_ms).as_secs_f64());
290 }
291 }
292
293 self.context
294 .metrics
295 .node_metrics
296 .finalizer_buffered_commits
297 .set(self.pending_commits.len() as i64);
298
299 finalized_commits
300 }
301
302 fn try_direct_finalize_commit(&mut self, index: usize) {
311 let metrics = &self.context.metrics.node_metrics;
312 let num_commits = self.pending_commits.len();
313 let commit_state = self
314 .pending_commits
315 .get_mut(index)
316 .unwrap_or_else(|| panic!("Commit {} does not exist. len = {}", index, num_commits));
317
318 let vote_gc_round = self
334 .dag_state
335 .read()
336 .calculate_gc_round(commit_state.commit.leader.round + INDIRECT_REJECT_DEPTH);
337 tracing::debug!(
338 "Trying to direct finalize commit {} using vote GC round {}",
339 commit_state.commit.commit_ref,
340 vote_gc_round,
341 );
342
343 assert!(!commit_state.pending_blocks.is_empty());
345 let pending_blocks = std::mem::take(&mut commit_state.pending_blocks);
346
347 for (block_ref, num_transactions) in pending_blocks {
348 if block_ref.round <= vote_gc_round && num_transactions > 0 {
349 let transactions =
351 (0..(num_transactions as TransactionIndex)).collect::<BTreeSet<_>>();
352 commit_state
353 .pending_transactions
354 .entry(block_ref)
355 .or_default()
356 .extend(transactions);
357 let hostname = &self.context.committee.authority(block_ref.author).hostname;
358 metrics
359 .finalizer_skipped_voting_blocks
360 .with_label_values(&[hostname.as_str(), "direct"])
361 .inc();
362 tracing::debug!(
363 "Block {} is potentially outside of GC bound from its leader {} in commit {}. Skipping direct finalization.",
364 block_ref,
365 commit_state.commit.leader,
366 commit_state.commit.commit_ref
367 );
368 continue;
369 }
370 let reject_votes = self.transaction_vote_tracker.get_reject_votes(&block_ref)
371 .unwrap_or_else(|| panic!("No vote info found for {block_ref}. It is either incorrectly gc'ed or failed to be recovered after crash."));
372 metrics
373 .finalizer_transaction_status
374 .with_label_values(&["direct_finalize"])
375 .inc_by((num_transactions - reject_votes.len()) as u64);
376 let hostname = &self.context.committee.authority(block_ref.author).hostname;
377 metrics
378 .finalizer_reject_votes
379 .with_label_values(&[hostname])
380 .inc_by(reject_votes.len() as u64);
381 for (transaction_index, stake) in reject_votes {
384 let entry = if stake < self.context.committee.quorum_threshold() {
387 commit_state
388 .pending_transactions
389 .entry(block_ref)
390 .or_default()
391 } else {
392 metrics
393 .finalizer_transaction_status
394 .with_label_values(&["direct_reject"])
395 .inc();
396 commit_state
397 .rejected_transactions
398 .entry(block_ref)
399 .or_default()
400 };
401 entry.insert(transaction_index);
402 }
403 }
404 }
405
406 fn link_blocks_in_last_commit(&mut self) {
409 let commit_state = self
410 .pending_commits
411 .back_mut()
412 .unwrap_or_else(|| panic!("No pending commit."));
413
414 let mut blocks = commit_state.commit.blocks.clone();
417 blocks.sort_by_key(|b| b.round());
418
419 let mut blocks_map = self.blocks.write();
420 for block in blocks {
421 let block_ref = block.reference();
422 for ancestor in block.ancestors() {
424 if let Some(ancestor_block) = blocks_map.get(ancestor) {
427 ancestor_block.write().children.insert(block_ref);
428 }
429 }
430 blocks_map.entry(block_ref).or_insert_with(|| {
432 RwLock::new(BlockState::new(block, commit_state.commit.commit_ref.index))
433 });
434 }
435 }
436
437 fn append_origin_descendants_from_last_commit(&mut self) {
458 let commit_state = self
459 .pending_commits
460 .back_mut()
461 .unwrap_or_else(|| panic!("No pending commit."));
462 let mut committed_blocks = commit_state.commit.blocks.clone();
463 committed_blocks.sort_by_key(|b| b.round());
464 let blocks_map = self.blocks.read();
465 for committed_block in committed_blocks {
466 let committed_block_ref = committed_block.reference();
467 let mut origin_ancestor_ref = *blocks_map
471 .get(&committed_block_ref)
472 .unwrap()
473 .read()
474 .block
475 .ancestors()
476 .first()
477 .unwrap();
478 while origin_ancestor_ref.author == committed_block_ref.author {
479 let Some(origin_ancestor_block) = blocks_map.get(&origin_ancestor_ref) else {
480 break;
481 };
482 origin_ancestor_block
483 .write()
484 .origin_descendants
485 .push(committed_block_ref);
486 origin_ancestor_ref = *origin_ancestor_block
487 .read()
488 .block
489 .ancestors()
490 .first()
491 .unwrap();
492 }
493 }
494 }
495
496 async fn try_indirect_finalize_first_commit(&mut self) {
498 assert!(!self.pending_commits.is_empty());
500 assert!(self.pending_commits[0].pending_blocks.is_empty());
501
502 self.check_pending_transactions_in_first_commit();
504
505 self.try_indirect_finalize_pending_transactions_in_first_commit()
507 .await;
508
509 self.try_indirect_reject_pending_transactions_in_first_commit();
511 }
512
513 fn check_pending_transactions_in_first_commit(&mut self) {
514 let mut all_rejected_transactions: Vec<(BlockRef, Vec<TransactionIndex>)> = vec![];
515
516 for (block_ref, pending_transactions) in &self.pending_commits[0].pending_transactions {
518 let reject_votes: BTreeMap<TransactionIndex, Stake> = self
519 .transaction_vote_tracker
520 .get_reject_votes(block_ref)
521 .unwrap_or_else(|| panic!("No vote info found for {block_ref}. It is incorrectly gc'ed or failed to be recovered after crash."))
522 .into_iter()
523 .collect();
524 let mut rejected_transactions = vec![];
525 for &transaction_index in pending_transactions {
526 let reject_stake = reject_votes
528 .get(&transaction_index)
529 .copied()
530 .unwrap_or_default();
531 if reject_stake < self.context.committee.quorum_threshold() {
532 continue;
534 }
535 rejected_transactions.push(transaction_index);
537 }
538 if !rejected_transactions.is_empty() {
539 all_rejected_transactions.push((*block_ref, rejected_transactions));
540 }
541 }
542
543 for (block_ref, rejected_transactions) in all_rejected_transactions {
545 self.context
546 .metrics
547 .node_metrics
548 .finalizer_transaction_status
549 .with_label_values(&["direct_late_reject"])
550 .inc_by(rejected_transactions.len() as u64);
551 let curr_commit_state = &mut self.pending_commits[0];
552 curr_commit_state.remove_pending_transactions(&block_ref, &rejected_transactions);
553 curr_commit_state
554 .rejected_transactions
555 .entry(block_ref)
556 .or_default()
557 .extend(rejected_transactions);
558 }
559 }
560
561 async fn try_indirect_finalize_pending_transactions_in_first_commit(&mut self) {
562 tracing::debug!(
563 "Trying to indirectly finalize pending transactions in first commit {}",
564 self.pending_commits[0].commit.commit_ref,
565 );
566 let _scope = monitored_scope(
567 "CommitFinalizer::try_indirect_finalize_pending_transactions_in_first_commit",
568 );
569
570 let pending_blocks: Vec<_> = self.pending_commits[0]
571 .pending_transactions
572 .iter()
573 .map(|(k, v)| (*k, v.clone()))
574 .collect();
575
576 let gc_rounds = self
577 .pending_commits
578 .iter()
579 .map(|c| {
580 (
581 c.commit.commit_ref.index,
582 self.dag_state
583 .read()
584 .calculate_gc_round(c.commit.leader.round),
585 )
586 })
587 .collect::<Vec<_>>();
588
589 const BLOCKS_PER_INDIRECT_COMMIT_TASK: usize = 8;
591
592 let mut all_finalized_transactions = vec![];
594 let mut handles = Vec::new();
595 for chunk in pending_blocks.chunks(BLOCKS_PER_INDIRECT_COMMIT_TASK) {
598 let context = self.context.clone();
599 let blocks = self.blocks.clone();
600 let gc_rounds = gc_rounds.clone();
601 let chunk: Vec<(BlockRef, BTreeSet<TransactionIndex>)> = chunk.to_vec();
602
603 let handle = tokio::task::spawn_blocking(move || {
604 let mut chunk_results = Vec::new();
605
606 for (block_ref, pending_transactions) in chunk {
607 let finalized = Self::try_indirect_finalize_pending_transactions_in_block(
608 &context,
609 &blocks,
610 &gc_rounds,
611 block_ref,
612 pending_transactions,
613 );
614
615 if !finalized.is_empty() {
616 chunk_results.push((block_ref, finalized));
617 }
618 }
619
620 chunk_results
621 });
622
623 handles.push(handle);
624 }
625
626 for handle in handles {
628 let result = match handle.await {
629 Ok(chunk_results) => {
630 all_finalized_transactions.extend(chunk_results);
631 continue;
632 }
633 Err(e) => e,
634 };
635 if result.is_panic() {
636 std::panic::resume_unwind(result.into_panic());
637 }
638 tracing::info!("Process likely shutting down: {:?}", result);
639 return;
641 }
642
643 for (block_ref, finalized_transactions) in all_finalized_transactions {
644 self.context
645 .metrics
646 .node_metrics
647 .finalizer_transaction_status
648 .with_label_values(&["indirect_finalize"])
649 .inc_by(finalized_transactions.len() as u64);
650 self.pending_commits[0]
652 .remove_pending_transactions(&block_ref, &finalized_transactions);
653 }
654 }
655
656 fn try_indirect_reject_pending_transactions_in_first_commit(&mut self) {
657 let curr_leader_round = self.pending_commits[0].commit.leader.round;
658 let last_commit_leader_round = self.pending_commits.back().unwrap().commit.leader.round;
659 if curr_leader_round + INDIRECT_REJECT_DEPTH <= last_commit_leader_round {
660 let curr_commit_state = &mut self.pending_commits[0];
661 assert!(curr_commit_state.pending_blocks.is_empty());
665 let pending_transactions = std::mem::take(&mut curr_commit_state.pending_transactions);
669 for (block_ref, pending_transactions) in pending_transactions {
670 self.context
671 .metrics
672 .node_metrics
673 .finalizer_transaction_status
674 .with_label_values(&["indirect_reject"])
675 .inc_by(pending_transactions.len() as u64);
676 curr_commit_state
677 .rejected_transactions
678 .entry(block_ref)
679 .or_default()
680 .extend(pending_transactions);
681 }
682 }
683 }
684
685 fn try_indirect_finalize_pending_transactions_in_block(
689 context: &Arc<Context>,
690 blocks: &Arc<RwLock<BTreeMap<BlockRef, RwLock<BlockState>>>>,
691 gc_rounds: &[(CommitIndex, Round)],
692 pending_block_ref: BlockRef,
693 pending_transactions: BTreeSet<TransactionIndex>,
694 ) -> Vec<TransactionIndex> {
695 if pending_transactions.is_empty() {
696 return vec![];
697 }
698 let mut accept_votes: BTreeMap<TransactionIndex, StakeAggregator<QuorumThreshold>> =
699 pending_transactions
700 .into_iter()
701 .map(|transaction_index| (transaction_index, StakeAggregator::new()))
702 .collect();
703 let mut finalized_transactions = vec![];
704 let blocks_map = blocks.read();
705 let (pending_commit_index, mut to_visit_blocks) = {
707 let block_state = blocks_map.get(&pending_block_ref).unwrap().read();
708 (block_state.commit_index, block_state.children.clone())
709 };
710 let mut visited = BTreeSet::new();
712 let mut ignored = BTreeSet::new();
714 while let Some(curr_block_ref) = to_visit_blocks.pop_first() {
716 if !visited.insert(curr_block_ref) {
717 continue;
718 }
719 let curr_block_state = blocks_map.get(&curr_block_ref).unwrap_or_else(|| panic!("Block {curr_block_ref} is either incorrectly gc'ed or failed to be recovered after crash.")).read();
720 let votes_gced = Self::gced_transaction_votes_for_pending_block(
727 gc_rounds,
728 pending_block_ref.round,
729 pending_commit_index,
730 curr_block_state.commit_index,
731 );
732 if ignored.insert(curr_block_ref) {
734 ignored.extend(curr_block_state.origin_descendants.iter());
743 if votes_gced {
749 let hostname = &context
750 .committee
751 .authority(pending_block_ref.author)
752 .hostname;
753 context
754 .metrics
755 .node_metrics
756 .finalizer_skipped_voting_blocks
757 .with_label_values(&[hostname.as_str(), "indirect"])
758 .inc();
759 tracing::debug!(
760 "Block {} is potentially outside of GC bound from current block {}. Skipping indirect finalization.",
761 pending_block_ref,
762 curr_block_ref,
763 );
764 continue;
765 }
766 let curr_block_reject_votes = curr_block_state
768 .reject_votes
769 .get(&pending_block_ref)
770 .cloned()
771 .unwrap_or_default();
772 let mut newly_finalized = vec![];
774 for (index, stake) in &mut accept_votes {
775 if curr_block_reject_votes.contains(index) {
777 continue;
778 }
779 if !stake.add(curr_block_ref.author, &context.committee) {
781 continue;
782 }
783 newly_finalized.push(*index);
784 finalized_transactions.push(*index);
785 }
786 for index in newly_finalized {
788 accept_votes.remove(&index);
789 }
790 if accept_votes.is_empty() {
792 break;
793 }
794 }
795 to_visit_blocks.extend(
797 curr_block_state
798 .children
799 .iter()
800 .filter(|b| !visited.contains(*b)),
801 );
802 }
803 finalized_transactions
804 }
805
806 fn gced_transaction_votes_for_pending_block(
824 gc_rounds: &[(CommitIndex, Round)],
825 pending_block_round: Round,
826 pending_commit_index: CommitIndex,
827 current_commit_index: CommitIndex,
828 ) -> bool {
829 assert!(
830 pending_commit_index <= current_commit_index,
831 "Pending {pending_commit_index} should be <= current {current_commit_index}"
832 );
833 if pending_commit_index == current_commit_index {
834 return false;
835 }
836 let (commit_index, gc_round) = *gc_rounds
841 .get((current_commit_index - 1 - pending_commit_index) as usize)
842 .unwrap();
843 assert_eq!(
844 commit_index,
845 current_commit_index - 1,
846 "Commit index mismatch {commit_index} != {current_commit_index}"
847 );
848 pending_block_round <= gc_round
849 }
850
851 fn pop_finalized_commits(&mut self) -> Vec<CommittedSubDag> {
852 let mut finalized_commits = vec![];
853
854 while let Some(commit_state) = self.pending_commits.front() {
855 if !commit_state.pending_blocks.is_empty()
856 || !commit_state.pending_transactions.is_empty()
857 {
858 break;
860 }
861
862 let commit_state = self.pending_commits.pop_front().unwrap();
864 let mut commit = commit_state.commit;
865 for (block_ref, rejected_transactions) in commit_state.rejected_transactions {
866 commit
867 .rejected_transactions_by_block
868 .insert(block_ref, rejected_transactions.into_iter().collect());
869 }
870
871 let mut blocks_map = self.blocks.write();
873 for block in commit.blocks.iter() {
874 blocks_map.remove(&block.reference());
875 }
876
877 let round_delay = if let Some(last_commit_state) = self.pending_commits.back() {
878 last_commit_state.commit.leader.round - commit.leader.round
879 } else {
880 0
881 };
882 self.context
883 .metrics
884 .node_metrics
885 .finalizer_round_delay
886 .observe(round_delay as f64);
887
888 finalized_commits.push(commit);
889 }
890
891 finalized_commits
892 }
893
894 fn try_update_gc_round(&mut self, last_finalized_commit_round: Round) {
895 let gc_round = self
898 .dag_state
899 .read()
900 .calculate_gc_round(last_finalized_commit_round);
901 self.transaction_vote_tracker.run_gc(gc_round);
902 }
903
904 #[cfg(test)]
905 fn is_empty(&self) -> bool {
906 self.pending_commits.is_empty() && self.blocks.read().is_empty()
907 }
908}
909
910struct CommitState {
911 commit: CommittedSubDag,
912 pending_blocks: BTreeMap<BlockRef, usize>,
916 pending_transactions: BTreeMap<BlockRef, BTreeSet<TransactionIndex>>,
921 rejected_transactions: BTreeMap<BlockRef, BTreeSet<TransactionIndex>>,
923}
924
925impl CommitState {
926 fn new(commit: CommittedSubDag) -> Self {
927 let pending_blocks: BTreeMap<_, _> = commit
928 .blocks
929 .iter()
930 .map(|b| (b.reference(), b.transactions().len()))
931 .collect();
932 assert!(!pending_blocks.is_empty());
933 Self {
934 commit,
935 pending_blocks,
936 pending_transactions: BTreeMap::new(),
937 rejected_transactions: BTreeMap::new(),
938 }
939 }
940
941 fn remove_pending_transactions(
942 &mut self,
943 block_ref: &BlockRef,
944 transactions: &[TransactionIndex],
945 ) {
946 let Some(block_pending_txns) = self.pending_transactions.get_mut(block_ref) else {
947 return;
948 };
949 for t in transactions {
950 block_pending_txns.remove(t);
951 }
952 if block_pending_txns.is_empty() {
953 self.pending_transactions.remove(block_ref);
954 }
955 }
956}
957
958struct BlockState {
959 block: VerifiedBlock,
961 children: BTreeSet<BlockRef>,
963 reject_votes: BTreeMap<BlockRef, BTreeSet<TransactionIndex>>,
965 origin_descendants: Vec<BlockRef>,
968 commit_index: CommitIndex,
970}
971
972impl BlockState {
973 fn new(block: VerifiedBlock, commit_index: CommitIndex) -> Self {
974 let reject_votes: BTreeMap<_, _> = block
975 .transaction_votes()
976 .iter()
977 .map(|v| (v.block_ref, v.rejects.clone().into_iter().collect()))
978 .collect();
979 let origin_descendants = Vec::with_capacity(8);
982 Self {
983 block,
984 children: BTreeSet::new(),
985 reject_votes,
986 origin_descendants,
987 commit_index,
988 }
989 }
990}
991
992#[cfg(test)]
993mod tests {
994 use crate::{
995 TestBlock, VerifiedBlock, block::BlockTransactionVotes,
996 commit_test_fixture::CommitTestFixture, test_dag_builder::DagBuilder,
997 };
998
999 use super::*;
1000
1001 fn create_commit_finalizer_fixture() -> CommitTestFixture {
1002 CommitTestFixture::with_options(4, 0, Some(5))
1003 }
1004
1005 fn create_block(
1006 round: Round,
1007 authority: u32,
1008 mut ancestors: Vec<BlockRef>,
1009 num_transactions: usize,
1010 reject_votes: Vec<BlockTransactionVotes>,
1011 ) -> VerifiedBlock {
1012 let i = ancestors
1014 .iter()
1015 .position(|b| b.author.value() == authority as usize)
1016 .unwrap_or_else(|| {
1017 panic!("Authority {authority} (round {round}) not found in {ancestors:?}")
1018 });
1019 let b = ancestors.remove(i);
1020 ancestors.insert(0, b);
1021 let block = TestBlock::new(round, authority)
1023 .set_ancestors(ancestors)
1024 .set_transactions(vec![crate::Transaction::new(vec![1; 16]); num_transactions])
1025 .set_transaction_votes(reject_votes)
1026 .build();
1027 VerifiedBlock::new_for_test(block)
1028 }
1029
1030 #[tokio::test]
1031 async fn test_direct_finalize_no_reject_votes() {
1032 let mut fixture = create_commit_finalizer_fixture();
1033
1034 let mut dag_builder = DagBuilder::new(fixture.context.clone());
1036 dag_builder.layers(1..=4).num_transactions(10).build();
1037 let blocks = dag_builder.all_blocks();
1038 fixture.add_blocks(blocks.clone());
1039
1040 let leader = blocks.iter().find(|b| b.round() == 2).unwrap();
1042 let committed_sub_dags = fixture.linearizer.handle_commit(vec![leader.clone()]);
1043 assert_eq!(committed_sub_dags.len(), 1);
1044 let committed_sub_dag = &committed_sub_dags[0];
1045
1046 let finalized_commits = fixture
1048 .commit_finalizer
1049 .process_commit(committed_sub_dag.clone())
1050 .await;
1051 assert_eq!(finalized_commits.len(), 1);
1052 let finalized_commit = &finalized_commits[0];
1053 assert_eq!(committed_sub_dag, finalized_commit);
1054
1055 assert!(fixture.commit_finalizer.is_empty());
1057 }
1058
1059 #[tokio::test]
1062 async fn test_direct_finalize_with_reject_votes() {
1063 let mut fixture = create_commit_finalizer_fixture();
1064
1065 let mut dag_builder = DagBuilder::new(fixture.context.clone());
1067 dag_builder.layer(1).num_transactions(10).build();
1068
1069 let round_1_blocks = dag_builder.all_blocks();
1070 fixture.add_blocks_with_own_votes(
1071 round_1_blocks
1072 .iter()
1073 .map(|b| {
1074 if b.author().value() != 3 {
1075 (b.clone(), vec![])
1076 } else {
1077 (b.clone(), vec![0, 3])
1078 }
1079 })
1080 .collect(),
1081 );
1082
1083 let block_with_rejected_txn = round_1_blocks[3].clone();
1085 let reject_vote = BlockTransactionVotes {
1086 block_ref: block_with_rejected_txn.reference(),
1087 rejects: vec![0, 3],
1088 };
1089
1090 let ancestors: Vec<BlockRef> = round_1_blocks[0..3].iter().map(|b| b.reference()).collect();
1092 let round_2_blocks = vec![
1094 create_block(
1095 2,
1096 0,
1097 round_1_blocks.iter().map(|b| b.reference()).collect(),
1098 10,
1099 vec![reject_vote.clone()],
1100 ),
1101 create_block(2, 1, ancestors.clone(), 10, vec![]),
1102 create_block(2, 2, ancestors.clone(), 10, vec![]),
1103 ];
1104 fixture.add_blocks(round_2_blocks.clone());
1105
1106 let leader = round_2_blocks[0].clone();
1108 let committed_sub_dags = fixture.linearizer.handle_commit(vec![leader.clone()]);
1109 assert_eq!(committed_sub_dags.len(), 1);
1110 let committed_sub_dag = &committed_sub_dags[0];
1111 assert_eq!(committed_sub_dag.blocks.len(), 5);
1112
1113 let ancestors: Vec<BlockRef> = round_2_blocks.iter().map(|b| b.reference()).collect();
1115 let round_3_blocks = vec![
1116 create_block(3, 0, ancestors.clone(), 0, vec![]),
1117 create_block(3, 1, ancestors.clone(), 0, vec![reject_vote.clone()]),
1118 create_block(3, 2, ancestors.clone(), 0, vec![reject_vote.clone()]),
1119 create_block(
1120 3,
1121 3,
1122 std::iter::once(round_1_blocks[3].reference())
1123 .chain(ancestors.clone())
1124 .collect(),
1125 0,
1126 vec![reject_vote.clone()],
1127 ),
1128 ];
1129 fixture.add_blocks(round_3_blocks.clone());
1130
1131 let ancestors: Vec<BlockRef> = round_3_blocks.iter().map(|b| b.reference()).collect();
1133 let round_4_blocks = vec![
1134 create_block(4, 0, ancestors.clone(), 0, vec![]),
1135 create_block(4, 1, ancestors.clone(), 0, vec![]),
1136 create_block(4, 2, ancestors.clone(), 0, vec![]),
1137 create_block(4, 3, ancestors.clone(), 0, vec![]),
1138 ];
1139 fixture.add_blocks(round_4_blocks.clone());
1140
1141 let finalized_commits = fixture
1144 .commit_finalizer
1145 .process_commit(committed_sub_dag.clone())
1146 .await;
1147 assert_eq!(finalized_commits.len(), 1);
1148 let finalized_commit = &finalized_commits[0];
1149 assert_eq!(committed_sub_dag.commit_ref, finalized_commit.commit_ref);
1150 assert_eq!(committed_sub_dag.blocks, finalized_commit.blocks);
1151 assert_eq!(finalized_commit.rejected_transactions_by_block.len(), 1);
1152 assert_eq!(
1153 finalized_commit
1154 .rejected_transactions_by_block
1155 .get(&block_with_rejected_txn.reference())
1156 .unwrap()
1157 .clone(),
1158 vec![0, 3],
1159 );
1160
1161 assert!(fixture.commit_finalizer.is_empty());
1163 }
1164
1165 #[tokio::test]
1170 async fn test_indirect_finalize_with_reject_votes() {
1171 let mut fixture = create_commit_finalizer_fixture();
1172
1173 let mut dag_builder = DagBuilder::new(fixture.context.clone());
1175 dag_builder.layer(1).num_transactions(10).build();
1176
1177 let round_1_blocks = dag_builder.all_blocks();
1178 fixture.add_blocks_with_own_votes(
1179 round_1_blocks
1180 .iter()
1181 .map(|b| {
1182 if b.author().value() != 3 {
1183 (b.clone(), vec![])
1184 } else {
1185 (b.clone(), vec![0, 3])
1186 }
1187 })
1188 .collect(),
1189 );
1190
1191 let block_with_rejected_txn = round_1_blocks[3].clone();
1193 let ancestors: Vec<BlockRef> = round_1_blocks[0..3].iter().map(|b| b.reference()).collect();
1200 let round_2_blocks = vec![
1202 create_block(
1203 2,
1204 0,
1205 round_1_blocks.iter().map(|b| b.reference()).collect(),
1206 10,
1207 vec![BlockTransactionVotes {
1208 block_ref: block_with_rejected_txn.reference(),
1209 rejects: vec![1, 4],
1210 }],
1211 ),
1212 create_block(2, 1, ancestors.clone(), 10, vec![]),
1214 create_block(2, 2, ancestors.clone(), 10, vec![]),
1215 ];
1216 fixture.add_blocks(round_2_blocks.clone());
1217
1218 let mut leaders = vec![round_2_blocks[0].clone()];
1220
1221 let ancestors: Vec<BlockRef> = round_2_blocks.iter().map(|b| b.reference()).collect();
1223 let round_3_blocks = vec![
1224 create_block(3, 0, ancestors.clone(), 0, vec![]),
1225 create_block(
1226 3,
1227 1,
1228 ancestors.clone(),
1229 0,
1230 vec![BlockTransactionVotes {
1231 block_ref: block_with_rejected_txn.reference(),
1232 rejects: vec![1, 4, 7],
1233 }],
1234 ),
1235 create_block(
1236 3,
1237 3,
1238 std::iter::once(round_1_blocks[3].reference())
1239 .chain(ancestors.clone())
1240 .collect(),
1241 0,
1242 vec![],
1243 ),
1244 ];
1245 fixture.add_blocks(round_3_blocks.clone());
1246 leaders.push(round_3_blocks[2].clone());
1247
1248 let ancestors: Vec<BlockRef> = round_3_blocks.iter().map(|b| b.reference()).collect();
1250 let round_4_blocks = vec![
1251 create_block(4, 0, ancestors.clone(), 0, vec![]),
1252 create_block(4, 1, ancestors.clone(), 0, vec![]),
1253 create_block(
1254 4,
1255 2,
1256 std::iter::once(round_2_blocks[2].reference())
1257 .chain(ancestors.clone())
1258 .collect(),
1259 0,
1260 vec![BlockTransactionVotes {
1261 block_ref: block_with_rejected_txn.reference(),
1262 rejects: vec![1],
1263 }],
1264 ),
1265 create_block(4, 3, ancestors.clone(), 0, vec![]),
1266 ];
1267 fixture.add_blocks(round_4_blocks.clone());
1268 leaders.push(round_4_blocks[1].clone());
1269
1270 let mut last_round_blocks = round_4_blocks.clone();
1274 for r in 5..=7 {
1275 let ancestors: Vec<BlockRef> =
1276 last_round_blocks.iter().map(|b| b.reference()).collect();
1277 let round_blocks: Vec<_> = (0..4)
1278 .map(|i| create_block(r, i, ancestors.clone(), 0, vec![]))
1279 .collect();
1280 fixture.add_blocks(round_blocks.clone());
1281 if r == 5 {
1282 leaders.push(round_blocks[0].clone());
1283 }
1284 last_round_blocks = round_blocks;
1285 }
1286
1287 assert_eq!(leaders.len(), 4);
1289 let committed_sub_dags = fixture.linearizer.handle_commit(leaders);
1290 assert_eq!(committed_sub_dags.len(), 4);
1291
1292 for commit in committed_sub_dags.iter().take(3) {
1294 let finalized_commits = fixture
1295 .commit_finalizer
1296 .process_commit(commit.clone())
1297 .await;
1298 assert_eq!(finalized_commits.len(), 0);
1299 }
1300
1301 let finalized_commits = fixture
1303 .commit_finalizer
1304 .process_commit(committed_sub_dags[3].clone())
1305 .await;
1306 assert_eq!(finalized_commits.len(), 4);
1307
1308 let rejected_transactions = finalized_commits[0].rejected_transactions_by_block.clone();
1310 assert_eq!(rejected_transactions.len(), 1);
1311 assert_eq!(
1312 rejected_transactions
1313 .get(&block_with_rejected_txn.reference())
1314 .unwrap(),
1315 &vec![1, 4]
1316 );
1317
1318 for commit in finalized_commits.iter().skip(1) {
1320 assert!(commit.rejected_transactions_by_block.is_empty());
1321 }
1322
1323 assert!(fixture.commit_finalizer.is_empty());
1325 }
1326
1327 #[tokio::test]
1329 async fn test_direct_finalize_with_gc() {
1330 let mut fixture = create_commit_finalizer_fixture();
1331 assert_eq!(fixture.context.protocol_config.gc_depth(), 5);
1332
1333 let mut dag_builder = DagBuilder::new(fixture.context.clone());
1335 dag_builder.layer(1).num_transactions(10).build();
1336 let round_1_blocks = dag_builder.all_blocks();
1337 fixture.add_blocks(round_1_blocks.clone());
1338
1339 let block_rejected = round_1_blocks[3].clone();
1341
1342 let mut last_round_blocks: Vec<VerifiedBlock> = round_1_blocks
1345 .iter()
1346 .enumerate()
1347 .filter_map(|(i, b)| {
1348 if i != block_rejected.author().value() {
1349 Some(b.clone())
1350 } else {
1351 None
1352 }
1353 })
1354 .collect();
1355 for r in 2..=5 {
1356 let ancestors: Vec<BlockRef> =
1357 last_round_blocks.iter().map(|b| b.reference()).collect();
1358 last_round_blocks = [0, 1, 2]
1359 .map(|i| create_block(r, i, ancestors.clone(), 0, vec![]))
1360 .to_vec();
1361 fixture.add_blocks(last_round_blocks.clone());
1362 }
1363
1364 let mut leaders = vec![];
1367 for r in 6..=9 {
1368 let ancestors: Vec<BlockRef> =
1369 last_round_blocks.iter().map(|b| b.reference()).collect();
1370 last_round_blocks = [0, 1, 2]
1371 .map(|i| {
1372 let mut ancestors = ancestors.clone();
1373 if i == 0 {
1374 ancestors.push(block_rejected.reference());
1376 }
1377 create_block(r, i, ancestors, 0, vec![])
1378 })
1379 .to_vec();
1380 leaders.push(last_round_blocks[0].clone());
1381 fixture.add_blocks(last_round_blocks.clone());
1382 }
1383
1384 assert_eq!(leaders.len(), 4);
1386 let committed_sub_dags = fixture.linearizer.handle_commit(leaders);
1387 assert_eq!(committed_sub_dags.len(), 4);
1388
1389 assert!(committed_sub_dags[0].blocks.contains(&block_rejected));
1391
1392 for commit in committed_sub_dags.iter().take(3) {
1394 assert!(commit.decided_with_local_blocks);
1395 let finalized_commits = fixture
1396 .commit_finalizer
1397 .process_commit(commit.clone())
1398 .await;
1399 assert_eq!(finalized_commits.len(), 0);
1400 }
1401
1402 let finalized_commits = fixture
1404 .commit_finalizer
1405 .process_commit(committed_sub_dags[3].clone())
1406 .await;
1407 assert_eq!(finalized_commits.len(), 4);
1408
1409 let rejected_transactions = finalized_commits[0].rejected_transactions_by_block.clone();
1413 assert_eq!(rejected_transactions.len(), 1);
1414 assert_eq!(
1415 rejected_transactions
1416 .get(&block_rejected.reference())
1417 .unwrap(),
1418 &vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1419 );
1420
1421 for commit in finalized_commits.iter().skip(1) {
1423 assert!(commit.rejected_transactions_by_block.is_empty());
1424 }
1425
1426 assert!(fixture.commit_finalizer.is_empty());
1428 }
1429
1430 #[tokio::test]
1432 async fn test_indirect_reject_with_gc() {
1433 let mut fixture = create_commit_finalizer_fixture();
1434 assert_eq!(fixture.context.protocol_config.gc_depth(), 5);
1435
1436 let mut dag_builder = DagBuilder::new(fixture.context.clone());
1438 dag_builder.layer(1).num_transactions(10).build();
1439
1440 let round_1_blocks = dag_builder.all_blocks();
1441 fixture.add_blocks(round_1_blocks.clone());
1442
1443 let block_with_rejected_txn = round_1_blocks[3].clone();
1445 let ancestors: Vec<BlockRef> = round_1_blocks.iter().map(|b| b.reference()).collect();
1452 let round_2_blocks = vec![
1453 create_block(2, 0, ancestors.clone(), 0, vec![]),
1454 create_block(
1455 2,
1456 1,
1457 ancestors.clone(),
1458 0,
1459 vec![BlockTransactionVotes {
1460 block_ref: block_with_rejected_txn.reference(),
1461 rejects: vec![1],
1462 }],
1463 ),
1464 create_block(2, 2, ancestors.clone(), 0, vec![]),
1465 create_block(2, 3, ancestors.clone(), 0, vec![]),
1466 ];
1467 fixture.add_blocks(round_2_blocks.clone());
1468
1469 let mut last_round_blocks: Vec<VerifiedBlock> = round_2_blocks
1472 .iter()
1473 .enumerate()
1474 .filter_map(|(i, b)| if i != 2 { Some(b.clone()) } else { None })
1475 .collect();
1476 for r in 3..=6 {
1477 let ancestors: Vec<BlockRef> =
1478 last_round_blocks.iter().map(|b| b.reference()).collect();
1479 last_round_blocks = [0, 1, 3]
1480 .map(|i| create_block(r, i, ancestors.clone(), 0, vec![]))
1481 .to_vec();
1482 fixture.add_blocks(last_round_blocks.clone());
1483 }
1484
1485 let mut leaders = vec![];
1487 for r in 7..=10 {
1488 let ancestors: Vec<BlockRef> =
1489 last_round_blocks.iter().map(|b| b.reference()).collect();
1490 last_round_blocks = (0..4)
1491 .map(|i| {
1492 let mut ancestors = ancestors.clone();
1493 if r == 7 && i == 2 {
1494 ancestors.push(round_2_blocks[2].reference());
1496 }
1497 create_block(r, i, ancestors, 0, vec![])
1498 })
1499 .collect();
1500 leaders.push(last_round_blocks[0].clone());
1501 fixture.add_blocks(last_round_blocks.clone());
1502 }
1503
1504 assert_eq!(leaders.len(), 4);
1506 let committed_sub_dags = fixture.linearizer.handle_commit(leaders);
1507 assert_eq!(committed_sub_dags.len(), 4);
1508
1509 assert!(committed_sub_dags[0].blocks.contains(&round_2_blocks[1]));
1511 for commit in committed_sub_dags.iter() {
1513 assert!(!commit.blocks.contains(&round_2_blocks[2]));
1514 }
1515
1516 for commit in committed_sub_dags.iter().take(3) {
1518 assert!(commit.decided_with_local_blocks);
1519 let finalized_commits = fixture
1520 .commit_finalizer
1521 .process_commit(commit.clone())
1522 .await;
1523 assert_eq!(finalized_commits.len(), 0);
1524 }
1525
1526 let finalized_commits = fixture
1528 .commit_finalizer
1529 .process_commit(committed_sub_dags[3].clone())
1530 .await;
1531 assert_eq!(finalized_commits.len(), 4);
1532
1533 let rejected_transactions = finalized_commits[0].rejected_transactions_by_block.clone();
1537 assert_eq!(rejected_transactions.len(), 1);
1538 assert_eq!(
1539 rejected_transactions
1540 .get(&block_with_rejected_txn.reference())
1541 .unwrap(),
1542 &vec![1]
1543 );
1544
1545 for commit in finalized_commits.iter().skip(1) {
1547 assert!(commit.rejected_transactions_by_block.is_empty());
1548 }
1549
1550 assert!(fixture.commit_finalizer.is_empty());
1552 }
1553
1554 #[tokio::test]
1555 async fn test_finalize_remote_commits_with_reject_votes() {
1556 let mut fixture: CommitTestFixture = create_commit_finalizer_fixture();
1557 let mut all_blocks = vec![];
1558
1559 let mut dag_builder = DagBuilder::new(fixture.context.clone());
1561 dag_builder.layer(1).num_transactions(10).build();
1562 let round_1_blocks = dag_builder.all_blocks();
1563 all_blocks.push(round_1_blocks.clone());
1564
1565 let mut leaders = vec![round_1_blocks[0].clone()];
1567
1568 let mut last_round_blocks = round_1_blocks.clone();
1570 for r in 2..=9 {
1571 let ancestors: Vec<BlockRef> =
1572 last_round_blocks.iter().map(|b| b.reference()).collect();
1573 let round_blocks: Vec<_> = (0..4)
1574 .map(|i| create_block(r, i, ancestors.clone(), 0, vec![]))
1575 .collect();
1576 all_blocks.push(round_blocks.clone());
1577 if r <= 7 && r != 5 {
1578 leaders.push(round_blocks[r as usize % 4].clone());
1579 }
1580 last_round_blocks = round_blocks;
1581 }
1582
1583 assert_eq!(leaders.len(), 6);
1585
1586 async fn add_blocks_and_process_commit(
1587 fixture: &mut CommitTestFixture,
1588 leaders: &[VerifiedBlock],
1589 all_blocks: &[Vec<VerifiedBlock>],
1590 index: usize,
1591 local: bool,
1592 ) -> Vec<CommittedSubDag> {
1593 let leader = leaders[index].clone();
1594 if local {
1596 for round_blocks in all_blocks.iter().take(leader.round() as usize + 2) {
1597 fixture.add_blocks(round_blocks.clone());
1598 }
1599 } else {
1600 for round_blocks in all_blocks.iter().take(leader.round() as usize) {
1601 fixture.add_blocks(round_blocks.clone());
1602 }
1603 };
1604 let mut committed_sub_dags = fixture.linearizer.handle_commit(vec![leader]);
1606 assert_eq!(committed_sub_dags.len(), 1);
1607 let mut remote_commit = committed_sub_dags.pop().unwrap();
1608 remote_commit.decided_with_local_blocks = local;
1609 fixture
1611 .commit_finalizer
1612 .process_commit(remote_commit.clone())
1613 .await
1614 }
1615
1616 for i in 0..3 {
1618 let finalized_commits =
1619 add_blocks_and_process_commit(&mut fixture, &leaders, &all_blocks, i, false).await;
1620 assert!(finalized_commits.is_empty());
1621 }
1622
1623 let finalized_commits =
1625 add_blocks_and_process_commit(&mut fixture, &leaders, &all_blocks, 3, false).await;
1626 assert_eq!(finalized_commits.len(), 1);
1627 assert_eq!(finalized_commits[0].commit_ref.index, 1);
1628 assert_eq!(finalized_commits[0].leader.round, 1);
1629
1630 let finalized_commits =
1632 add_blocks_and_process_commit(&mut fixture, &leaders, &all_blocks, 4, true).await;
1633 assert_eq!(finalized_commits.len(), 2);
1634 assert_eq!(finalized_commits[0].commit_ref.index, 2);
1635 assert_eq!(finalized_commits[0].leader.round, 2);
1636 assert_eq!(finalized_commits[1].commit_ref.index, 3);
1637 assert_eq!(finalized_commits[1].leader.round, 3);
1638
1639 let finalized_commits =
1641 add_blocks_and_process_commit(&mut fixture, &leaders, &all_blocks, 5, true).await;
1642 assert_eq!(finalized_commits.len(), 3);
1643 assert_eq!(finalized_commits[0].commit_ref.index, 4);
1644 assert_eq!(finalized_commits[0].leader.round, 4);
1645 assert_eq!(finalized_commits[1].commit_ref.index, 5);
1646 assert_eq!(finalized_commits[1].leader.round, 6);
1647 assert_eq!(finalized_commits[2].commit_ref.index, 6);
1648 assert_eq!(finalized_commits[2].leader.round, 7);
1649
1650 assert!(fixture.commit_finalizer.is_empty());
1652 }
1653}