Skip to main content

consensus_core/
commit_finalizer.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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
29/// For transaction T committed at leader round R, when a new leader at round >= R + INDIRECT_REJECT_DEPTH
30/// commits and T is still not finalized, T is rejected.
31/// NOTE: 3 round is the minimum depth possible for indirect finalization and rejection.
32pub(crate) const INDIRECT_REJECT_DEPTH: Round = 3;
33
34/// Handle to CommitFinalizer, for sending CommittedSubDag.
35pub(crate) struct CommitFinalizerHandle {
36    sender: Option<UnboundedSender<CommittedSubDag>>,
37    task: Option<tokio::task::JoinHandle<()>>,
38}
39
40impl CommitFinalizerHandle {
41    // Sends a CommittedSubDag to CommitFinalizer, which will finalize it before sending it to execution.
42    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        // Closing the channel allows CommitFinalizer to drain accepted commits before exiting.
57        self.sender.take();
58        if let Some(task) = self.task.take() {
59            join_and_propagate_panic(task).await;
60        }
61    }
62}
63
64/// CommitFinalizer accepts a continuous stream of CommittedSubDag and outputs
65/// them when they are finalized.
66/// In finalized commits, every transaction is either finalized or rejected.
67/// It runs in a separate thread, to reduce the load on the core thread.
68///
69/// Life of a finalized commit:
70///
71/// For efficiency, finalization happens first for transactions without reject votes (common case).
72/// The pending undecided transactions with reject votes are individually finalized or rejected.
73/// When there is no more pending transactions, the commit is finalized.
74///
75/// This is correct because regardless if a commit leader was directly or indirectly committed,
76/// every committed block can be considered finalized, because at least one leader certificate of the commit
77/// will be committed, which can also serve as a certificate for the block and its transactions.
78///
79/// From the earliest buffered commit, pending blocks are checked to see if they are now finalized.
80/// New finalized blocks are removed from the pending blocks, and its transactions are moved to the
81/// finalized, rejected or pending state. If the commit now has no pending blocks or transactions,
82/// the commit is finalized and popped from the buffer. The next earliest commit is then processed
83/// similarly, until either the buffer becomes empty or a commit with pending blocks or transactions
84/// is encountered.
85pub 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 commit index processed by CommitFinalizer.
92    last_processed_commit: Option<CommitIndex>,
93    // Commits pending finalization.
94    pending_commits: VecDeque<CommitState>,
95    // Blocks in the pending commits.
96    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                // Transaction certifier state should be GC'ed as soon as new commits are finalized.
144                // But this is done outside of process_commit(), because during recovery process_commit()
145                // is not called to finalize commits, but GC still needs to run.
146                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                    // Records rejected transactions in newly finalized commits.
150                    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                // Commits and committed blocks must be persisted to storage before sending them to Sui
158                // to execute their finalized transactions.
159                // Commit metadata and uncommitted blocks can be persisted more lazily because they are recoverable.
160                // But for simplicity, all unpersisted commits and blocks are flushed to storage.
161                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        // The prerequisite for running direct finalization on a commit is that the commit must
194        // have either a quorum of leader certificates in the local DAG, or a committed leader certificate.
195        //
196        // A leader certificate is a finalization certificate for every block in the commit.
197        // When the prerequisite holds, all blocks in the current commit can be considered finalized.
198        // And any transaction in the current commit that has not observed reject votes will never be rejected.
199        // So these transactions are directly finalized.
200        //
201        // When a commit is direct, there are a quorum of its leader certificates in the local DAG.
202        //
203        // When a commit is indirect, it implies one of its leader certificates is in the committed blocks.
204        // So a leader certificate must exist in the local DAG as well.
205        //
206        // When a commit is received through commit sync and processed as certified commit, the commit might
207        // not have a leader certificate in the local DAG. So a committed transaction might not observe any reject
208        // vote from local DAG, although it will eventually get rejected. To finalize blocks in this commit,
209        // there must be another commit with leader round >= 3 (WAVE_LENGTH) rounds above the commit leader.
210        // From the indirect commit rule, a leader certificate must exist in committed blocks for the earliest commit.
211        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                // The commit has already been processed through direct finalization.
215                continue;
216            }
217            // Direct finalization cannot happen when
218            // -  This commit is remote.
219            // -  And the latest commit is less than 3 (WAVE_LENGTH) rounds above this commit.
220            // In this case, this commit's leader certificate is not guaranteed to be in local DAG.
221            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        // Indirect finalization: one or more commits cannot be directly finalized.
241        // So the pending transactions need to be checked for indirect finalization.
242        if !self.pending_commits.is_empty() {
243            // Initialize the state of the last added commit for computing indirect finalization.
244            //
245            // As long as there are remaining commits, even if the last commit has been directly finalized,
246            // its state still needs to be initialized here to help indirectly finalize previous commits.
247            // This is because the last commit may have been directly finalized, but its previous commits
248            // may not have been directly finalized.
249            self.link_blocks_in_last_commit();
250            self.append_origin_descendants_from_last_commit();
251            // Try to indirectly finalize a prefix of the buffered commits.
252            // If only one commit remains, it cannot be indirectly finalized because there is no commit afterwards,
253            // so it is excluded.
254            while self.pending_commits.len() > 1 {
255                // Stop indirect finalization when the earliest commit has not been processed
256                // through direct finalization.
257                if !self.pending_commits[0].pending_blocks.is_empty() {
258                    break;
259                }
260                // Otherwise, try to indirectly finalize the earliest commit.
261                self.try_indirect_finalize_first_commit().await;
262                let indirect_finalized_commits = self.pop_finalized_commits();
263                if indirect_finalized_commits.is_empty() {
264                    // No additional commits can be indirectly finalized.
265                    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    // Tries directly finalizing transactions in the commit.
303    // Direct commit means every transaction in the commit can be considered to have a quorum of post-commit certificates,
304    // unless (1) the transaction has reject votes that do not reach quorum, or
305    // (2) the block containing the transaction is outside the GC bound of the commit's leader.
306    // In the 2nd case, when the blocks voting and certifying this commit's leader were proposed, there is a chance
307    // that some of these voting and certifying blocks do not include votes for the transactions below the leader's GC bound.
308    // So conservatively, these transactions are not directly finalized. The logic here matches the GC logic in
309    // try_indirect_finalize_pending_transactions_in_block().
310    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        // Estimate conservatively the GC round of the blocks voting and certifying this commit's leader of round (R).
319        //
320        // The key question we try to answer: "Could the voting blocks (R+1) and certifying blocks (R+2) have seen all blocks in this commit at the time they were proposed by their respective nodes, or
321        // could they have discarded them due to their local GC round already advanced by being ahead in their commit round?"
322        //
323        // It's not possible to know the exact GC round that was used when the voting and certifying blocks were proposed, so we use the most conservative threshold.
324        // We assume that the nodes proposing those blocks had already committed the `commit_state.commit.leader.round + INDIRECT_REJECT_DEPTH` - (R+3) and we calculate the gc_round of it as the cut off.
325        //
326        // Why we use the (R+3) leader round though?
327        //
328        // According to the protocol (R+3) is the min num of rounds needed for the leader's certificate to appear in a commit. Since we can't know which exact blocks form the certificate, we assume (R+3) being this minimum
329        // limit which also aligns with the assumptions made in the indirect finalization logic.
330        //
331        // So we assume as if the blocks that certify the leader (R) had advanced to the commit (R+3), and thus any block below the `vote_gc_round`, the gc round after the (R+3) leader was committed, could have been locally gced
332        // and never been voted by the certifying blocks.
333        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        // Each commit can only try direct finalization once.
344        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                // The block is outside of GC bound.
350                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            // If a transaction_index does not exist in reject_votes, the transaction has no reject votes.
382            // So it is finalized and does not need to be added to pending_transactions.
383            for (transaction_index, stake) in reject_votes {
384                // If the transaction has > 0 but < 2f+1 reject votes, it is still pending.
385                // Otherwise, it is rejected.
386                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    // Creates an entry in the blocks map for each block in the commit,
407    // and have its ancestors link to the block.
408    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        // Link blocks in ascending order of round, to ensure ancestor block states are created
415        // before they are linked from.
416        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            // Link ancestors to the block.
423            for ancestor in block.ancestors() {
424                // Ancestor may not exist in the blocks map if it has been finalized or gc'ed.
425                // So skip linking if the ancestor does not exist.
426                if let Some(ancestor_block) = blocks_map.get(ancestor) {
427                    ancestor_block.write().children.insert(block_ref);
428                }
429            }
430            // Initialize the block state.
431            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    /// Updates the set of origin descendants, by appending blocks from the last commit to
438    /// origin descendants of previous linked blocks from the same origin.
439    ///
440    /// The purpose of maintaining the origin descendants per block is to save bandwidth by avoiding to explicitly
441    /// list all accept votes on transactions in blocks.
442    /// Instead when an ancestor block Ba is first included by a proposed block Bp, reject votes for transactions in Ba
443    /// are explicitly listed (if they exist). The rest of non-rejected transactions in Ba are assumed to be accepted by Bp.
444    /// This vote compression rule must be applied during vote aggregation as well.
445    ///
446    /// The above rule is equivalent to saying that transactions in a block can only be voted on by its immediate descendants.
447    /// A block Bp is an **immediate descendant** of Ba, if any directed path from Bp to Ba does not contain a block from Bp's own authority.
448    ///
449    /// This rule implies the following optimization is possible: after collecting votes for Ba from block Bp,
450    /// we can skip collecting votes from Bp's **origin descendants** (descendant blocks from the
451    /// same authority), because they cannot vote on Ba anyway.
452    ///
453    /// This vote compression rule is easy to implement when proposing blocks. Reject votes can be gathered against
454    /// all the newly included ancestors of the proposed block. But vote decompression is trickier to get right.
455    /// One edge case is when a block may not be an immediate descendant, because of GC. In this case votes from the
456    /// block should not be counted.
457    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            // Each block must have at least one ancestor.
468            // Block verification ensures the first ancestor is from the block's own authority.
469            // Also, block verification ensures each authority appears at most once among ancestors.
470            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    // Tries indirectly finalizing the buffered commits at the given index.
497    async fn try_indirect_finalize_first_commit(&mut self) {
498        // Ensure direct finalization has been attempted for the commit.
499        assert!(!self.pending_commits.is_empty());
500        assert!(self.pending_commits[0].pending_blocks.is_empty());
501
502        // Optional optimization: re-check pending transactions to see if they are rejected by a quorum now.
503        self.check_pending_transactions_in_first_commit();
504
505        // Check if remaining pending transactions can be finalized.
506        self.try_indirect_finalize_pending_transactions_in_first_commit()
507            .await;
508
509        // Check if remaining pending transactions can be indirectly rejected.
510        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        // Collect all rejected transactions without modifying state
517        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                // Pending transactions do not have reject votes when the block is outside of GC bound from the commit leader's round.
527                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                    // The transaction cannot be rejected yet.
533                    continue;
534                }
535                // Otherwise, mark the transaction for rejection.
536                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        // Move rejected transactions from pending_transactions.
544        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        // Number of blocks to process in each task.
590        const BLOCKS_PER_INDIRECT_COMMIT_TASK: usize = 8;
591
592        // Process chunks in parallel.
593        let mut all_finalized_transactions = vec![];
594        let mut handles = Vec::new();
595        // TODO(fastpath): investigate using a cost based batching,
596        // for example each block has cost num authorities + pending_transactions.len().
597        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        // Collect results from all chunks
627        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            // Ok to return. No potential inconsistency in state.
640            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            // Remove finalized transactions from pending transactions.
651            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            // This function is called after trying to indirectly finalize pending blocks.
662            // When last commit leader round is INDIRECT_REJECT_DEPTH rounds higher or more,
663            // all pending blocks should have been finalized.
664            assert!(curr_commit_state.pending_blocks.is_empty());
665            // This function is called after trying to indirectly finalize pending transactions.
666            // All remaining pending transactions, since they are not finalized, should now be
667            // indirectly rejected.
668            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    // Returns the indices of the requested pending transactions that are indirectly finalized.
686    // This function is used for checking finalization of transactions, so it must traverse
687    // all blocks which can contribute to the requested transactions' finalizations.
688    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        // Use BTreeSet for to_visit_blocks, to visit blocks in the earliest round first.
706        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        // Blocks that have been visited.
711        let mut visited = BTreeSet::new();
712        // Blocks where votes and origin descendants should be ignored for processing.
713        let mut ignored = BTreeSet::new();
714        // Traverse children blocks breadth-first and accumulate accept votes for pending transactions.
715        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            // Check if transaction votes for the pending block are potentially not carried by the
721            // current block, because of GC at the current block's proposer.
722            // See comment above gced_transaction_votes_for_pending_block() for more details.
723            //
724            // Implicit transaction votes should only be considered in commit finalizer if they are definitely
725            // part of the transactions votes from the current block when it is proposed.
726            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            // Skip counting votes from the block if it has been marked to be ignored.
733            if ignored.insert(curr_block_ref) {
734                // Skip collecting votes from origin descendants of current block.
735                // Votes from origin descendants of current block do not count for these transactions.
736                // Consider this case: block B is an origin descendant of block A (from the same authority),
737                // and both blocks A and B link to another block C.
738                // Only B's implicit and explicit transaction votes on C are considered.
739                // None of A's implicit or explicit transaction votes on C should be considered.
740                //
741                // See append_origin_descendants_from_last_commit() for more details.
742                ignored.extend(curr_block_state.origin_descendants.iter());
743                // Skip counting votes from current block if the votes on pending block could have been
744                // casted by an earlier block from the same origin, or the votes might not be proposed due to GC.
745                // Note: if the current block casts reject votes on transactions in the pending block,
746                // it can be assumed that accept votes are also casted to other transactions in the pending block.
747                // But we choose to skip counting the accept votes in this edge case for simplicity.
748                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                // Get reject votes from current block to the pending block.
767                let curr_block_reject_votes = curr_block_state
768                    .reject_votes
769                    .get(&pending_block_ref)
770                    .cloned()
771                    .unwrap_or_default();
772                // Because of lifetime, first collect finalized transactions, and then remove them from accept_votes.
773                let mut newly_finalized = vec![];
774                for (index, stake) in &mut accept_votes {
775                    // Skip if the transaction has been rejected by the current block.
776                    if curr_block_reject_votes.contains(index) {
777                        continue;
778                    }
779                    // Skip if the total stake has not reached quorum.
780                    if !stake.add(curr_block_ref.author, &context.committee) {
781                        continue;
782                    }
783                    newly_finalized.push(*index);
784                    finalized_transactions.push(*index);
785                }
786                // There is no need to aggregate additional votes for already finalized transactions.
787                for index in newly_finalized {
788                    accept_votes.remove(&index);
789                }
790                // End traversal if all blocks and requested transactions have reached quorum.
791                if accept_votes.is_empty() {
792                    break;
793                }
794            }
795            // Add additional children blocks to visit.
796            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    /// Returns true if transaction votes from the current block to the pending block
807    /// could have been be GC'ed. If this is the case, the current block cannot be assumed
808    /// to have implicitly voted to accept transactions in the pending block.
809    ///
810    /// When collecting transaction votes during proposal of the current block
811    /// (via DagState::link_causal_history()), votes against blocks in the DAG
812    /// below the proposer's GC round are skipped. Implicit accept votes cannot be assumed
813    /// for these GC'ed blocks. However, blocks do not carry the GC round when they are proposed.
814    /// So this function computes the highest possible GC round when the current block was proposed,
815    /// and use it as the minimum round threshold for implicit accept votes. Even if the computed
816    /// GC round here is higher than the actual GC round used by the current block, it is still
817    /// correct although less efficient.
818    ///
819    /// gc_rounds is a list of cached commit indices and the GC rounds resulting from the commits.
820    /// It must be a superset of commits in the range [pending_commit_index, current_commit_index].
821    /// The first element should have pending_commit_index, because pending commit should be the
822    /// first commit buffered in CommitFinalizer.
823    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        // current_commit_index is the commit index which includes the current / voting block.
837        // When the current block was proposed, the latest/highest possible GC round that could have been used is the GC round computed
838        // from the leader of the previous commit (current_commit_index - 1). This acts as the most conservative threshold to make sure
839        // that the current block had actually "seen" the blocks within it's local GC bound.
840        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                // The commit is not finalized yet.
859                break;
860            }
861
862            // Pop the finalized commit and set its rejected transactions.
863            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            // Clean up committed blocks.
872            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        // GC TransactionVoteTracker state only with finalized commits, to ensure unfinalized transactions
896        // can access their reject votes from TransactionVoteTracker.
897        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    // Blocks pending finalization, mapped to the number of transactions in the block.
913    // This field is populated by all blocks in the commit, before direct finalization.
914    // After direct finalization, this field becomes empty.
915    pending_blocks: BTreeMap<BlockRef, usize>,
916    // Transactions pending indirect finalization.
917    // This field is populated after direct finalization, if pending transactions exist.
918    // Values in this field are removed as transactions are indirectly finalized or directly rejected.
919    // When both pending_blocks and pending_transactions are empty, the commit is finalized.
920    pending_transactions: BTreeMap<BlockRef, BTreeSet<TransactionIndex>>,
921    // Transactions rejected by a quorum or indirectly, per block.
922    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    // Content of the block.
960    block: VerifiedBlock,
961    // Blocks which has an explicit ancestor linking to this block.
962    children: BTreeSet<BlockRef>,
963    // Reject votes casted by this block, and by linked ancestors from the same authority.
964    reject_votes: BTreeMap<BlockRef, BTreeSet<TransactionIndex>>,
965    // Other committed blocks that are origin descendants of this block.
966    // See the comment above append_origin_descendants_from_last_commit() for more details.
967    origin_descendants: Vec<BlockRef>,
968    // Commit which contains this block.
969    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        // With at most 4 pending commits and assume 2 origin descendants per commit,
980        // there will be at most 8 origin descendants.
981        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        // Move own authority ancestor to the front of the ancestors.
1013        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        // Create test block.
1022        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        // Create round 1-4 blocks with 10 transactions each. Add these blocks to the transaction vote tracker.
1035        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        // Select a round 2 block as the leader and create CommittedSubDag.
1041        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        // This committed sub-dag can be directly finalized.
1047        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        // CommitFinalizer should be empty.
1056        assert!(fixture.commit_finalizer.is_empty());
1057    }
1058
1059    // Commits can be directly finalized if when they are added to commit finalizer,
1060    // the rejected votes reach quorum if they exist on any transaction.
1061    #[tokio::test]
1062    async fn test_direct_finalize_with_reject_votes() {
1063        let mut fixture = create_commit_finalizer_fixture();
1064
1065        // Create round 1 blocks with 10 transactions each.
1066        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        // Select the block with rejected transaction.
1084        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        // Create round 2 blocks without authority 3's block from round 1.
1091        let ancestors: Vec<BlockRef> = round_1_blocks[0..3].iter().map(|b| b.reference()).collect();
1092        // Leader links to block_with_rejected_txn, but other blocks do not.
1093        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        // Select round 2 authority 0 block as the leader and create CommittedSubDag.
1107        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        // Create round 3 blocks voting on the leader.
1114        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        // Create round 4 blocks certifying the leader.
1132        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        // This committed sub-dag can be directly finalized because the rejected transactions
1142        // have a quorum of votes.
1143        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        // CommitFinalizer should be empty.
1162        assert!(fixture.commit_finalizer.is_empty());
1163    }
1164
1165    // Test indirect finalization when:
1166    // 1. Reject votes on transaction does not reach quorum initially, but reach quorum later.
1167    // 2. Transaction is indirectly rejected.
1168    // 3. Transaction is indirectly finalized.
1169    #[tokio::test]
1170    async fn test_indirect_finalize_with_reject_votes() {
1171        let mut fixture = create_commit_finalizer_fixture();
1172
1173        // Create round 1 blocks with 10 transactions each.
1174        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        // Select the block with rejected transaction.
1192        let block_with_rejected_txn = round_1_blocks[3].clone();
1193        // How transactions in this block will be voted:
1194        // Txn 1 (quorum reject): 1 reject vote at round 2, 1 reject vote at round 3, and 1 at round 4.
1195        // Txn 4 (indirect reject): 1 reject vote at round 3, and 1 at round 4.
1196        // Txn 7 (indirect finalize): 1 reject vote at round 3.
1197
1198        // Create round 2 blocks without authority 3.
1199        let ancestors: Vec<BlockRef> = round_1_blocks[0..3].iter().map(|b| b.reference()).collect();
1200        // Leader links to block_with_rejected_txn, but other blocks do not.
1201        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            // Use ancestors without authority 3 to avoid voting on its transactions.
1213            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        // Select round 2 authority 0 block as the a leader.
1219        let mut leaders = vec![round_2_blocks[0].clone()];
1220
1221        // Create round 3 blocks voting on the leader and casting reject votes.
1222        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        // Create round 4 blocks certifying the leader and casting reject votes.
1249        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        // Create round 5-7 blocks without casting reject votes.
1271        // Select the last leader from round 5. It is necessary to have round 5 leader to indirectly finalize
1272        // transactions committed by round 2 leader.
1273        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        // Create CommittedSubDag from leaders.
1288        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        // Buffering the initial 3 commits should not finalize.
1293        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        // Buffering the 4th commit should finalize all commits.
1302        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        // Check rejected transactions.
1309        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        // Other commits should have no rejected transactions.
1319        for commit in finalized_commits.iter().skip(1) {
1320            assert!(commit.rejected_transactions_by_block.is_empty());
1321        }
1322
1323        // CommitFinalizer should be empty.
1324        assert!(fixture.commit_finalizer.is_empty());
1325    }
1326
1327    // Test direct finalization when a block is at or below GC round from the block's own leader.
1328    #[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        // Create round 1 blocks with 10 transactions each.
1334        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        // Select B1(3) to be rejected due to GC.
1340        let block_rejected = round_1_blocks[3].clone();
1341
1342        // Create round 2-5 blocks without creating or linking to an authority 4 block.
1343        // The goal is to GC B1(3).
1344        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        // Create round 6-9 blocks without authority 3 blocks.
1365        // And add a leader from authority 0 of each round. Only authority 0 blocks can link to B1(3).
1366        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                        // Link to the GC'ed block B1(3).
1375                        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        // Create CommittedSubDag from leaders.
1385        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        // Ensure B1(3) is included in commit 0.
1390        assert!(committed_sub_dags[0].blocks.contains(&block_rejected));
1391
1392        // Buffering the initial 3 commits should not finalize.
1393        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        // Buffering the 4th commit should finalize all commits.
1403        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        // Check rejected transactions.
1410        // B1(3) all transactions get rejected as it is effectively voted only by one block from authority 3 (the block it self) and authority 0. Due to vote compression only the first block of authority 0 is counted.
1411        // The block is out of GC bound for the B7(1) and B7(2) blocks which are committed by the leader of round 8. Thus no accept votes are counted from authorities 1 & 2.
1412        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        // Other commits should have no rejected transactions.
1422        for commit in finalized_commits.iter().skip(1) {
1423            assert!(commit.rejected_transactions_by_block.is_empty());
1424        }
1425
1426        // CommitFinalizer should be empty.
1427        assert!(fixture.commit_finalizer.is_empty());
1428    }
1429
1430    // Test indirect finalization when transaction is rejected due to GC.
1431    #[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        // Create round 1 blocks with 10 transactions each.
1437        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        // Select B1(3) to have a rejected transaction.
1444        let block_with_rejected_txn = round_1_blocks[3].clone();
1445        // How transactions in this block will be voted:
1446        // Txn 1 (GC reject): 1 reject vote at round 2. But the txn will get rejected because there are only
1447        // 2 accept votes.
1448
1449        // Create round 2 blocks, with B2(1) rejecting transaction 1 from B1(3).
1450        // Note that 3 blocks link to B1(3) without rejecting transaction 1.
1451        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        // Create round 3-6 blocks without creating or linking to an authority 2 block.
1470        // The goal is to GC B2(2).
1471        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        // Create round 7-10 blocks and add a leader from authority 0 of each round.
1486        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                        // Link to the GC'ed block B2(2).
1495                        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        // Create CommittedSubDag from leaders.
1505        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        // Ensure 1 reject vote is contained in B2(1) in commit 0.
1510        assert!(committed_sub_dags[0].blocks.contains(&round_2_blocks[1]));
1511        // Ensure B2(2) is GC'ed.
1512        for commit in committed_sub_dags.iter() {
1513            assert!(!commit.blocks.contains(&round_2_blocks[2]));
1514        }
1515
1516        // Buffering the initial 3 commits should not finalize.
1517        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        // Buffering the 4th commit should finalize all commits.
1527        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        // Check rejected transactions.
1534        // B1(3) txn 1 gets rejected, even though there are has 3 blocks links to B1(3) without rejecting txn 1.
1535        // This is because there are only 2 accept votes for this transaction, which is less than the quorum threshold.
1536        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        // Other commits should have no rejected transactions.
1546        for commit in finalized_commits.iter().skip(1) {
1547            assert!(commit.rejected_transactions_by_block.is_empty());
1548        }
1549
1550        // CommitFinalizer should be empty.
1551        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        // Create round 1 blocks with 10 transactions each.
1560        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        // Collect leaders from round 1.
1566        let mut leaders = vec![round_1_blocks[0].clone()];
1567
1568        // Create round 2-9 blocks and set leaders until round 7.
1569        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        // Leader rounds: 1, 2, 3, 4, 6, 7.
1584        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            // Add blocks related to the commit to DagState and TransactionVoteTracker.
1595            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            // Generate remote commit from leader.
1605            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            // Process the remote commit.
1610            fixture
1611                .commit_finalizer
1612                .process_commit(remote_commit.clone())
1613                .await
1614        }
1615
1616        // Add commit 1-3 as remote commits. There should be no finalized commits.
1617        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        // Buffer round 4 commit as a remote commit. This should finalize the 1st commit at round 1.
1624        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        // Buffer round 6 (5th) commit as local commit. This should help finalize the commits at round 2 and 3.
1631        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        // Buffer round 7 (6th) commit as local commit. This should help finalize the commits at round 4, 6 and 7 (itself).
1640        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        // CommitFinalizer should be empty.
1651        assert!(fixture.commit_finalizer.is_empty());
1652    }
1653}