Skip to main content

consensus_core/
linearizer.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::sync::Arc;
5
6use consensus_config::Stake;
7use consensus_types::block::{BlockRef, BlockTimestampMs, Round};
8use itertools::Itertools;
9use parking_lot::RwLock;
10
11use crate::{
12    block::{BlockAPI, VerifiedBlock},
13    commit::{
14        Commit, CommittedSubDag, TrustedCommit, compute_sort_seed, sort_committed_blocks,
15        sort_sub_dag_blocks,
16    },
17    context::Context,
18    dag_state::DagState,
19};
20
21/// The `StorageAPI` trait provides an interface for the block store and has been
22/// mostly introduced for allowing to inject the test store in `DagBuilder`.
23pub(crate) trait BlockStoreAPI {
24    fn get_blocks(&self, refs: &[BlockRef]) -> Vec<Option<VerifiedBlock>>;
25
26    fn gc_round(&self) -> Round;
27
28    fn set_committed(&mut self, block_ref: &BlockRef) -> bool;
29
30    fn is_committed(&self, block_ref: &BlockRef) -> bool;
31}
32
33impl BlockStoreAPI
34    for parking_lot::lock_api::RwLockWriteGuard<'_, parking_lot::RawRwLock, DagState>
35{
36    fn get_blocks(&self, refs: &[BlockRef]) -> Vec<Option<VerifiedBlock>> {
37        DagState::get_blocks(self, refs)
38    }
39
40    fn gc_round(&self) -> Round {
41        DagState::gc_round(self)
42    }
43
44    fn set_committed(&mut self, block_ref: &BlockRef) -> bool {
45        DagState::set_committed(self, block_ref)
46    }
47
48    fn is_committed(&self, block_ref: &BlockRef) -> bool {
49        DagState::is_committed(self, block_ref)
50    }
51}
52
53/// Expand a committed sequence of leader into a sequence of sub-dags.
54#[derive(Clone)]
55pub struct Linearizer {
56    /// In memory block store representing the dag state
57    context: Arc<Context>,
58    dag_state: Arc<RwLock<DagState>>,
59}
60
61impl Linearizer {
62    pub fn new(context: Arc<Context>, dag_state: Arc<RwLock<DagState>>) -> Self {
63        Self { context, dag_state }
64    }
65
66    /// Collect the sub-dag and the corresponding commit from a specific leader excluding any duplicates or
67    /// blocks that have already been committed (within previous sub-dags).
68    fn collect_sub_dag_and_commit(
69        &mut self,
70        leader_block: VerifiedBlock,
71    ) -> (CommittedSubDag, TrustedCommit) {
72        let _s = self
73            .context
74            .metrics
75            .node_metrics
76            .scope_processing_time
77            .with_label_values(&["Linearizer::collect_sub_dag_and_commit"])
78            .start_timer();
79
80        // Grab latest commit state from dag state
81        let mut dag_state = self.dag_state.write();
82        let last_commit_index = dag_state.last_commit_index();
83        let last_commit_digest = dag_state.last_commit_digest();
84        let last_commit_timestamp_ms = dag_state.last_commit_timestamp_ms();
85
86        // Now linearize the sub-dag starting from the leader block
87        let to_commit = Self::linearize_sub_dag(
88            leader_block.clone(),
89            &mut dag_state,
90            self.context.protocol_config.shuffle_commit_blocks(),
91        );
92
93        let timestamp_ms = Self::calculate_commit_timestamp(
94            &self.context,
95            &mut dag_state,
96            &leader_block,
97            last_commit_timestamp_ms,
98        );
99
100        drop(dag_state);
101
102        // Create the Commit.
103        let commit = Commit::new(
104            last_commit_index + 1,
105            last_commit_digest,
106            timestamp_ms,
107            leader_block.reference(),
108            to_commit
109                .iter()
110                .map(|block| block.reference())
111                .collect::<Vec<_>>(),
112        );
113        let serialized = commit
114            .serialize()
115            .unwrap_or_else(|e| panic!("Failed to serialize commit: {}", e));
116        let commit = TrustedCommit::new_trusted(commit, serialized);
117
118        // Create the corresponding committed sub dag
119        let sub_dag = CommittedSubDag::new(
120            leader_block.reference(),
121            to_commit,
122            timestamp_ms,
123            commit.reference(),
124        );
125
126        (sub_dag, commit)
127    }
128
129    /// Calculates the commit's timestamp. The timestamp will be calculated as the median of leader's parents (leader.round - 1)
130    /// timestamps by stake. To ensure that commit timestamp monotonicity is respected it is compared against the `last_commit_timestamp_ms`
131    /// and the maximum of the two is returned.
132    pub(crate) fn calculate_commit_timestamp(
133        context: &Context,
134        dag_state: &mut impl BlockStoreAPI,
135        leader_block: &VerifiedBlock,
136        last_commit_timestamp_ms: BlockTimestampMs,
137    ) -> BlockTimestampMs {
138        let timestamp_ms = {
139            // Select leaders' parent blocks.
140            let block_refs = leader_block
141                .ancestors()
142                .iter()
143                .filter(|block_ref| block_ref.round == leader_block.round() - 1)
144                .cloned()
145                .collect::<Vec<_>>();
146            // Get the blocks from dag state which should not fail.
147            let block_opts = dag_state.get_blocks(&block_refs);
148            let blocks = block_opts.iter().map(|block_opt| {
149                block_opt
150                    .as_ref()
151                    .expect("We should have all blocks in dag state.")
152            });
153            median_timestamp_by_stake(context, blocks).unwrap_or_else(|e| {
154                panic!(
155                    "Cannot compute median timestamp for leader block {:?} ancestors: {}",
156                    leader_block, e
157                )
158            })
159        };
160
161        // Always make sure that commit timestamps are monotonic, so override if necessary.
162        timestamp_ms.max(last_commit_timestamp_ms)
163    }
164
165    /// Returns the blocks of the sub-dag committed by `leader_block`, ordered by round. Within a
166    /// round, blocks are ordered by a hash seeded from the leader's digest if `shuffle_blocks`,
167    /// and by authority index otherwise. Either way the leader, the only block of its round, is last.
168    pub(crate) fn linearize_sub_dag(
169        leader_block: VerifiedBlock,
170        dag_state: &mut impl BlockStoreAPI,
171        shuffle_blocks: bool,
172    ) -> Vec<VerifiedBlock> {
173        // The GC round here is calculated based on the last committed round of the leader block. The algorithm will attempt to
174        // commit blocks up to this GC round. Once this commit has been processed and written to DagState, then gc round will update
175        // and on the processing of the next commit we'll have it already updated, so no need to do any gc_round recalculations here.
176        // We just use whatever is currently in DagState.
177        let gc_round: Round = dag_state.gc_round();
178        let leader_block_ref = leader_block.reference();
179        let seed = shuffle_blocks.then(|| compute_sort_seed(std::slice::from_ref(&leader_block)));
180        let mut buffer = vec![leader_block];
181        let mut to_commit = Vec::new();
182
183        // Perform the recursion without stopping at the highest round round that has been committed per authority. Instead it will
184        // allow to commit blocks that are lower than the highest committed round for an authority but higher than gc_round.
185        assert!(
186            dag_state.set_committed(&leader_block_ref),
187            "Leader block with reference {:?} attempted to be committed twice",
188            leader_block_ref
189        );
190
191        while let Some(x) = buffer.pop() {
192            to_commit.push(x.clone());
193
194            let ancestors: Vec<VerifiedBlock> = dag_state
195                .get_blocks(
196                    &x.ancestors()
197                        .iter()
198                        .copied()
199                        .filter(|ancestor| {
200                            ancestor.round > gc_round && !dag_state.is_committed(ancestor)
201                        })
202                        .collect::<Vec<_>>(),
203                )
204                .into_iter()
205                .map(|ancestor_opt| {
206                    ancestor_opt.expect("We should have all uncommitted blocks in dag state.")
207                })
208                .collect();
209
210            for ancestor in ancestors {
211                buffer.push(ancestor.clone());
212                assert!(
213                    dag_state.set_committed(&ancestor.reference()),
214                    "Block with reference {:?} attempted to be committed twice",
215                    ancestor.reference()
216                );
217            }
218        }
219
220        // The above code should have not yielded any blocks that are <= gc_round, but just to make sure that we'll never
221        // commit anything that should be garbage collected we attempt to prune here as well.
222        assert!(
223            to_commit.iter().all(|block| block.round() > gc_round),
224            "No blocks <= {gc_round} should be committed. Leader round {}, blocks {to_commit:?}.",
225            leader_block_ref
226        );
227
228        match seed {
229            Some(seed) => sort_committed_blocks(&mut to_commit, &seed),
230            None => sort_sub_dag_blocks(&mut to_commit),
231        }
232
233        to_commit
234    }
235
236    // This function should be called whenever a new commit is observed. This will
237    // iterate over the sequence of committed leaders and produce a list of committed
238    // sub-dags.
239    pub fn handle_commit(&mut self, committed_leaders: Vec<VerifiedBlock>) -> Vec<CommittedSubDag> {
240        if committed_leaders.is_empty() {
241            return vec![];
242        }
243
244        let mut committed_sub_dags = vec![];
245        for leader_block in committed_leaders {
246            // Collect the sub-dag generated using each of these leaders and the corresponding commit.
247            let (sub_dag, commit) = self.collect_sub_dag_and_commit(leader_block);
248
249            self.update_blocks_pruned_metric(&sub_dag);
250
251            // Buffer commit in dag state for persistence later.
252            // This also updates the last committed rounds.
253            self.dag_state.write().add_commit(commit.clone());
254
255            committed_sub_dags.push(sub_dag);
256        }
257
258        committed_sub_dags
259    }
260
261    // Try to measure the number of blocks that get pruned due to GC. This is not very accurate, but it can give us a good enough idea.
262    // We consider a block as pruned when it is an ancestor of a block that has been committed as part of the provided `sub_dag`, but
263    // it has not been committed as part of previous commits. Right now we measure this via checking that highest committed round for the authority
264    // as we don't an efficient look up functionality to check if a block has been committed or not.
265    fn update_blocks_pruned_metric(&self, sub_dag: &CommittedSubDag) {
266        let (last_committed_rounds, gc_round) = {
267            let dag_state = self.dag_state.read();
268            (dag_state.last_committed_rounds(), dag_state.gc_round())
269        };
270
271        for block_ref in sub_dag
272            .blocks
273            .iter()
274            .flat_map(|block| block.ancestors())
275            .filter(
276                |ancestor_ref| {
277                    ancestor_ref.round <= gc_round
278                        && last_committed_rounds[ancestor_ref.author] != ancestor_ref.round
279                }, // If the last committed round is the same as the pruned block's round, then we know for sure that it has been committed and it doesn't count here
280                   // as pruned block.
281            )
282            .unique()
283        {
284            let hostname = &self.context.committee.authority(block_ref.author).hostname;
285
286            // If the last committed round from this authority is lower than the pruned ancestor in question, then we know for sure that it has not been committed.
287            let label_values = if last_committed_rounds[block_ref.author] < block_ref.round {
288                &[hostname, "uncommitted"]
289            } else {
290                // If last committed round is higher for this authority, then we don't really know it's status, but we know that there is a higher committed block from this authority.
291                &[hostname, "higher_committed"]
292            };
293
294            self.context
295                .metrics
296                .node_metrics
297                .blocks_pruned_on_commit
298                .with_label_values(label_values)
299                .inc();
300        }
301    }
302}
303
304/// Computes the median timestamp of the blocks weighted by the stake of their authorities.
305/// This function assumes each block comes from a different authority of the same round.
306/// Error is returned if no blocks are provided or total stake is less than quorum threshold.
307pub(crate) fn median_timestamp_by_stake<'a>(
308    context: &Context,
309    blocks: impl IntoIterator<Item = &'a VerifiedBlock>,
310) -> Result<BlockTimestampMs, String> {
311    let mut total_stake = 0;
312    let mut timestamps = vec![];
313    for block in blocks {
314        let stake = context.committee.authority(block.author()).stake;
315        timestamps.push((block.timestamp_ms(), stake));
316        total_stake += stake;
317    }
318
319    if timestamps.is_empty() {
320        return Err("No blocks provided".to_string());
321    }
322    if total_stake < context.committee.quorum_threshold() {
323        return Err(format!(
324            "Total stake {} < quorum threshold {}",
325            total_stake,
326            context.committee.quorum_threshold()
327        ));
328    }
329
330    Ok(median_timestamps_by_stake_inner(timestamps, total_stake))
331}
332
333fn median_timestamps_by_stake_inner(
334    mut timestamps: Vec<(BlockTimestampMs, Stake)>,
335    total_stake: Stake,
336) -> BlockTimestampMs {
337    timestamps.sort_by_key(|(ts, _)| *ts);
338
339    let mut cumulative_stake = 0;
340    for (ts, stake) in &timestamps {
341        cumulative_stake += stake;
342        if cumulative_stake > total_stake / 2 {
343            return *ts;
344        }
345    }
346
347    timestamps.last().unwrap().0
348}
349
350#[cfg(test)]
351mod tests {
352    use consensus_config::AuthorityIndex;
353    use mysten_common::ZipDebugEqIteratorExt as _;
354    use rstest::rstest;
355
356    use super::*;
357    use crate::{
358        CommitIndex, TestBlock,
359        commit::{CommitAPI as _, CommitDigest, DEFAULT_WAVE_LENGTH},
360        context::Context,
361        leader_schedule::{LeaderSchedule, LeaderSwapTable},
362        storage::mem_store::MemStore,
363        test_dag_builder::DagBuilder,
364        test_dag_parser::parse_dag,
365    };
366
367    #[rstest]
368    #[tokio::test]
369    async fn test_handle_commit() {
370        telemetry_subscribers::init_for_testing();
371        let num_authorities = 4;
372        let (context, _keys) = Context::new_for_test(num_authorities);
373        let context = Arc::new(context);
374
375        let dag_state = Arc::new(RwLock::new(DagState::new(
376            context.clone(),
377            Arc::new(MemStore::new()),
378        )));
379        let mut linearizer = Linearizer::new(context.clone(), dag_state.clone());
380
381        // Populate fully connected test blocks for round 0 ~ 10, authorities 0 ~ 3.
382        let num_rounds: u32 = 10;
383        let mut dag_builder = DagBuilder::new(context.clone());
384        dag_builder
385            .layers(1..=num_rounds)
386            .build()
387            .persist_layers(dag_state.clone());
388
389        let leaders = dag_builder
390            .leader_blocks(1..=num_rounds)
391            .into_iter()
392            .map(Option::unwrap)
393            .collect::<Vec<_>>();
394
395        let commits = linearizer.handle_commit(leaders.clone());
396        for (idx, subdag) in commits.into_iter().enumerate() {
397            tracing::info!("{subdag:?}");
398            assert_eq!(subdag.leader, leaders[idx].reference());
399
400            let expected_ts = {
401                let block_refs = leaders[idx]
402                    .ancestors()
403                    .iter()
404                    .filter(|block_ref| block_ref.round == leaders[idx].round() - 1)
405                    .cloned()
406                    .collect::<Vec<_>>();
407                let block_opts = dag_state.read().get_blocks(&block_refs);
408                let blocks = block_opts.iter().map(|block_opt| {
409                    block_opt
410                        .as_ref()
411                        .expect("We should have all blocks in dag state.")
412                });
413
414                median_timestamp_by_stake(&context, blocks).unwrap()
415            };
416            assert_eq!(subdag.timestamp_ms, expected_ts);
417
418            if idx == 0 {
419                // First subdag includes the leader block only
420                assert_eq!(subdag.blocks.len(), 1);
421            } else {
422                // Every subdag after will be missing the leader block from the previous
423                // committed subdag
424                assert_eq!(subdag.blocks.len(), num_authorities);
425            }
426            for block in subdag.blocks.iter() {
427                assert!(block.round() <= leaders[idx].round());
428            }
429            assert_eq!(subdag.commit_ref.index, idx as CommitIndex + 1);
430        }
431    }
432
433    #[rstest]
434    #[tokio::test]
435    async fn test_handle_commit_shuffles_blocks_within_round() {
436        telemetry_subscribers::init_for_testing();
437        let (context, _keys) = Context::new_for_test(7);
438        let num_rounds: u32 = 10;
439        let mut dag_builder = DagBuilder::new(Arc::new(context.clone()));
440        dag_builder.layers(1..=num_rounds).build();
441        let leaders: Vec<_> = dag_builder
442            .leader_blocks(1..=num_rounds)
443            .into_iter()
444            .map(Option::unwrap)
445            .collect();
446
447        let commit_all = |shuffle: bool| {
448            let mut context = context.clone();
449            context
450                .protocol_config
451                .set_shuffle_commit_blocks_for_testing(shuffle);
452            let context = Arc::new(context);
453            let dag_state = Arc::new(RwLock::new(DagState::new(
454                context.clone(),
455                Arc::new(MemStore::new()),
456            )));
457            dag_builder.persist_all_blocks(dag_state.clone());
458            Linearizer::new(context, dag_state)
459                .handle_commit(leaders.clone())
460                .into_iter()
461                .map(|subdag| {
462                    subdag
463                        .blocks
464                        .iter()
465                        .map(|b| b.reference())
466                        .collect::<Vec<_>>()
467                })
468                .collect::<Vec<_>>()
469        };
470
471        let unshuffled = commit_all(false);
472        let shuffled = commit_all(true);
473        assert_eq!(shuffled, commit_all(true));
474        assert_eq!(shuffled.len(), unshuffled.len());
475
476        let mut any_reordered = false;
477        for ((shuffled, unshuffled), leader) in shuffled
478            .iter()
479            .zip_debug_eq(&unshuffled)
480            .zip_debug_eq(&leaders)
481        {
482            assert!(unshuffled.is_sorted_by_key(|b| (b.round, b.author)));
483            assert!(shuffled.is_sorted_by_key(|b| b.round));
484            assert_eq!(shuffled.last(), Some(&leader.reference()));
485
486            let mut sorted = shuffled.clone();
487            sorted.sort_by_key(|b| (b.round, b.author));
488            assert_eq!(&sorted, unshuffled);
489            any_reordered |= shuffled != unshuffled;
490        }
491        assert!(any_reordered);
492    }
493
494    #[tokio::test]
495    async fn test_handle_already_committed() {
496        telemetry_subscribers::init_for_testing();
497        let num_authorities = 4;
498        let (context, _) = Context::new_for_test(num_authorities);
499        let context = Arc::new(context);
500
501        let dag_state = Arc::new(RwLock::new(DagState::new(
502            context.clone(),
503            Arc::new(MemStore::new()),
504        )));
505        let leader_schedule = Arc::new(LeaderSchedule::new(
506            context.clone(),
507            LeaderSwapTable::default(),
508        ));
509        let mut linearizer = Linearizer::new(context.clone(), dag_state.clone());
510        let wave_length = DEFAULT_WAVE_LENGTH;
511
512        let leader_round_wave_1 = 3;
513        let leader_round_wave_2 = leader_round_wave_1 + wave_length;
514
515        // Build a Dag from round 1..=6
516        let mut dag_builder = DagBuilder::new(context.clone());
517        dag_builder.layers(1..=leader_round_wave_2).build();
518
519        // Now retrieve all the blocks up to round leader_round_wave_1 - 1
520        // And then only the leader of round leader_round_wave_1
521        // Also store those to DagState
522        let mut blocks = dag_builder.blocks(0..=leader_round_wave_1 - 1);
523        blocks.push(
524            dag_builder
525                .leader_block(leader_round_wave_1)
526                .expect("Leader block should have been found"),
527        );
528        dag_state.write().accept_blocks(blocks.clone());
529
530        let first_leader = dag_builder
531            .leader_block(leader_round_wave_1)
532            .expect("Wave 1 leader round block should exist");
533        let mut last_commit_index = 1;
534        let first_commit_data = TrustedCommit::new_for_test(
535            last_commit_index,
536            CommitDigest::MIN,
537            0,
538            first_leader.reference(),
539            blocks.iter().map(|block| block.reference()).collect(),
540        );
541        dag_state.write().add_commit(first_commit_data);
542
543        // Mark the blocks as committed in DagState. This will allow to correctly detect the committed blocks when the new linearizer logic is enabled.
544        for block in blocks.iter() {
545            dag_state.write().set_committed(&block.reference());
546        }
547
548        // Now take all the blocks from round `leader_round_wave_1` up to round `leader_round_wave_2-1`
549        let mut blocks = dag_builder.blocks(leader_round_wave_1..=leader_round_wave_2 - 1);
550        // Filter out leader block of round `leader_round_wave_1`
551        blocks.retain(|block| {
552            !(block.round() == leader_round_wave_1
553                && block.author() == leader_schedule.elect_leader(leader_round_wave_1, 0))
554        });
555        // Add the leader block of round `leader_round_wave_2`
556        blocks.push(
557            dag_builder
558                .leader_block(leader_round_wave_2)
559                .expect("Leader block should have been found"),
560        );
561        // Write them in dag state
562        dag_state.write().accept_blocks(blocks.clone());
563
564        let mut blocks: Vec<_> = blocks.into_iter().map(|block| block.reference()).collect();
565
566        // Now get the latest leader which is the leader round of wave 2
567        let leader = dag_builder
568            .leader_block(leader_round_wave_2)
569            .expect("Leader block should exist");
570
571        last_commit_index += 1;
572        let expected_second_commit = TrustedCommit::new_for_test(
573            last_commit_index,
574            CommitDigest::MIN,
575            0,
576            leader.reference(),
577            blocks.clone(),
578        );
579
580        let commit = linearizer.handle_commit(vec![leader.clone()]);
581        assert_eq!(commit.len(), 1);
582
583        let subdag = &commit[0];
584        tracing::info!("{subdag:?}");
585        assert_eq!(subdag.leader, leader.reference());
586        assert_eq!(subdag.commit_ref.index, expected_second_commit.index());
587
588        let expected_ts = median_timestamp_by_stake(
589            &context,
590            subdag
591                .blocks
592                .iter()
593                .filter(|block| block.round() == subdag.leader.round - 1),
594        )
595        .unwrap();
596        assert_eq!(subdag.timestamp_ms, expected_ts);
597
598        let mut committed: Vec<_> = subdag.blocks.iter().map(|b| b.reference()).collect();
599        committed.sort();
600        blocks.sort();
601        assert_eq!(committed, blocks);
602        for block in subdag.blocks.iter() {
603            assert!(block.round() <= expected_second_commit.leader().round);
604        }
605    }
606
607    /// This test will run the linearizer with gc_depth = 3 and make
608    /// sure that for the exact same DAG the linearizer will commit different blocks according to the rules.
609    #[tokio::test]
610    async fn test_handle_commit_with_gc_simple() {
611        telemetry_subscribers::init_for_testing();
612
613        const GC_DEPTH: u32 = 3;
614
615        let num_authorities = 4;
616        let (mut context, _keys) = Context::new_for_test(num_authorities);
617        context.protocol_config.set_gc_depth_for_testing(GC_DEPTH);
618
619        let context = Arc::new(context);
620        let dag_state = Arc::new(RwLock::new(DagState::new(
621            context.clone(),
622            Arc::new(MemStore::new()),
623        )));
624        let mut linearizer = Linearizer::new(context.clone(), dag_state.clone());
625
626        // Authorities of index 0->2 will always creates blocks that see each other, but until round 5 they won't see the blocks of authority 3.
627        // For authority 3 we create blocks that connect to all the other authorities.
628        // On round 5 we finally make the other authorities see the blocks of authority 3.
629        // Practically we "simulate" here a long chain created by authority 3 that is visible in round 5, but due to GC blocks of only round >=2 will
630        // be committed, when GC is enabled. When GC is disabled all blocks will be committed for rounds >= 1.
631        let dag_str = "DAG {
632                Round 0 : { 4 },
633                Round 1 : { * },
634                Round 2 : {
635                    A -> [-D1],
636                    B -> [-D1],
637                    C -> [-D1],
638                    D -> [*],
639                },
640                Round 3 : {
641                    A -> [-D2],
642                    B -> [-D2],
643                    C -> [-D2],
644                },
645                Round 4 : { 
646                    A -> [-D3],
647                    B -> [-D3],
648                    C -> [-D3],
649                    D -> [A3, B3, C3, D2],
650                },
651                Round 5 : { * },
652            }";
653
654        let (_, dag_builder) = parse_dag(dag_str).expect("Invalid dag");
655        dag_builder.print();
656        dag_builder.persist_all_blocks(dag_state.clone());
657
658        let leaders = dag_builder
659            .leader_blocks(1..=6)
660            .into_iter()
661            .flatten()
662            .collect::<Vec<_>>();
663
664        let commits = linearizer.handle_commit(leaders.clone());
665        for (idx, subdag) in commits.into_iter().enumerate() {
666            tracing::info!("{subdag:?}");
667            assert_eq!(subdag.leader, leaders[idx].reference());
668
669            let expected_ts = {
670                let block_refs = leaders[idx]
671                    .ancestors()
672                    .iter()
673                    .filter(|block_ref| block_ref.round == leaders[idx].round() - 1)
674                    .cloned()
675                    .collect::<Vec<_>>();
676                let block_opts = dag_state.read().get_blocks(&block_refs);
677                let blocks = block_opts.iter().map(|block_opt| {
678                    block_opt
679                        .as_ref()
680                        .expect("We should have all blocks in dag state.")
681                });
682
683                median_timestamp_by_stake(&context, blocks).unwrap()
684            };
685            assert_eq!(subdag.timestamp_ms, expected_ts);
686
687            if idx == 0 {
688                // First subdag includes the leader block only
689                assert_eq!(subdag.blocks.len(), 1);
690            } else if idx == 1 {
691                assert_eq!(subdag.blocks.len(), 3);
692            } else if idx == 2 {
693                // We commit:
694                // * 1 block on round 4, the leader block
695                // * 3 blocks on round 3, as no commit happened on round 3 since the leader was missing
696                // * 2 blocks on round 2, again as no commit happened on round 3, we commit the "sub dag" of leader of round 3, which will be another 2 blocks
697                assert_eq!(subdag.blocks.len(), 6);
698            } else {
699                // Now it's going to be the first time that a leader will see the blocks of authority 3 and will attempt to commit
700                // the long chain. However, due to GC it will only commit blocks of round > 1. That's because it will commit blocks
701                // up to previous leader's round (round = 4) minus the gc_depth = 3, so that will be gc_round = 4 - 3 = 1. So we expect
702                // to see on the sub dag committed blocks of round >= 2.
703                assert_eq!(subdag.blocks.len(), 5);
704
705                assert!(
706                    subdag.blocks.iter().all(|block| block.round() >= 2),
707                    "Found blocks that are of round < 2."
708                );
709
710                // Also ensure that gc_round has advanced with the latest committed leader
711                assert_eq!(dag_state.read().gc_round(), subdag.leader.round - GC_DEPTH);
712            }
713            for block in subdag.blocks.iter() {
714                assert!(block.round() <= leaders[idx].round());
715            }
716            assert_eq!(subdag.commit_ref.index, idx as CommitIndex + 1);
717        }
718    }
719
720    #[tokio::test]
721    async fn test_handle_commit_below_highest_committed_round() {
722        telemetry_subscribers::init_for_testing();
723
724        const GC_DEPTH: u32 = 3;
725
726        let num_authorities = 4;
727        let (mut context, _keys) = Context::new_for_test(num_authorities);
728        context.protocol_config.set_gc_depth_for_testing(GC_DEPTH);
729
730        let context = Arc::new(context);
731        let dag_state = Arc::new(RwLock::new(DagState::new(
732            context.clone(),
733            Arc::new(MemStore::new()),
734        )));
735        let mut linearizer = Linearizer::new(context.clone(), dag_state.clone());
736
737        // Authority D will create an "orphaned" block on round 1 as it won't reference to it on the block of round 2. Similar, no other authority will reference to it on round 2.
738        // Then on round 3 the authorities A, B & C will link to block D1. Once the DAG gets committed we should see the block D1 getting committed as well. Normally ,as block D2 would
739        // have been committed first block D1 should be ommitted. With the new logic this is no longer true.
740        let dag_str = "DAG {
741                Round 0 : { 4 },
742                Round 1 : { * },
743                Round 2 : {
744                    A -> [-D1],
745                    B -> [-D1],
746                    C -> [-D1],
747                    D -> [-D1],
748                },
749                Round 3 : {
750                    A -> [A2, B2, C2, D1],
751                    B -> [A2, B2, C2, D1],
752                    C -> [A2, B2, C2, D1],
753                    D -> [A2, B2, C2, D2]
754                },
755                Round 4 : { * },
756            }";
757
758        let (_, dag_builder) = parse_dag(dag_str).expect("Invalid dag");
759        dag_builder.print();
760        dag_builder.persist_all_blocks(dag_state.clone());
761
762        let leaders = dag_builder
763            .leader_blocks(1..=4)
764            .into_iter()
765            .flatten()
766            .collect::<Vec<_>>();
767
768        let commits = linearizer.handle_commit(leaders.clone());
769        for (idx, subdag) in commits.into_iter().enumerate() {
770            tracing::info!("{subdag:?}");
771            assert_eq!(subdag.leader, leaders[idx].reference());
772
773            let expected_ts = {
774                let block_refs = leaders[idx]
775                    .ancestors()
776                    .iter()
777                    .filter(|block_ref| block_ref.round == leaders[idx].round() - 1)
778                    .cloned()
779                    .collect::<Vec<_>>();
780                let block_opts = dag_state.read().get_blocks(&block_refs);
781                let blocks = block_opts.iter().map(|block_opt| {
782                    block_opt
783                        .as_ref()
784                        .expect("We should have all blocks in dag state.")
785                });
786
787                median_timestamp_by_stake(&context, blocks).unwrap()
788            };
789            assert_eq!(subdag.timestamp_ms, expected_ts);
790
791            if idx == 0 {
792                // First subdag includes the leader block only B1
793                assert_eq!(subdag.blocks.len(), 1);
794            } else if idx == 1 {
795                // We commit:
796                // * 1 block on round 2, the leader block C2
797                // * 2 blocks on round 1, A1, C1
798                assert_eq!(subdag.blocks.len(), 3);
799            } else if idx == 2 {
800                // We commit:
801                // * 1 block on round 3, the leader block D3
802                // * 3 blocks on round 2, A2, B2, D2
803                assert_eq!(subdag.blocks.len(), 4);
804
805                assert!(
806                    subdag.blocks.iter().any(|block| block.round() == 2
807                        && block.author() == AuthorityIndex::new_for_test(3)),
808                    "Block D2 should have been committed."
809                );
810            } else if idx == 3 {
811                // We commit:
812                // * 1 block on round 4, the leader block A4
813                // * 3 blocks on round 3, A3, B3, C3
814                // * 1 block of round 1, D1
815                assert_eq!(subdag.blocks.len(), 5);
816                assert!(
817                    subdag.blocks.iter().any(|block| block.round() == 1
818                        && block.author() == AuthorityIndex::new_for_test(3)),
819                    "Block D1 should have been committed."
820                );
821            } else {
822                panic!("Unexpected subdag with index {:?}", idx);
823            }
824
825            for block in subdag.blocks.iter() {
826                assert!(block.round() <= leaders[idx].round());
827            }
828            assert_eq!(subdag.commit_ref.index, idx as CommitIndex + 1);
829        }
830    }
831
832    #[rstest]
833    #[case(3_000, 3_000, 6_000)]
834    #[tokio::test]
835    async fn test_calculate_commit_timestamp(
836        #[case] timestamp_1: u64,
837        #[case] timestamp_2: u64,
838        #[case] timestamp_3: u64,
839    ) {
840        // GIVEN
841        telemetry_subscribers::init_for_testing();
842
843        let num_authorities = 4;
844        let (context, _keys) = Context::new_for_test(num_authorities);
845
846        let context = Arc::new(context);
847        let store = Arc::new(MemStore::new());
848        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
849        let mut dag_state = dag_state.write();
850
851        let ancestors = vec![
852            VerifiedBlock::new_for_test(TestBlock::new(4, 0).set_timestamp_ms(1_000).build()),
853            VerifiedBlock::new_for_test(TestBlock::new(4, 1).set_timestamp_ms(2_000).build()),
854            VerifiedBlock::new_for_test(TestBlock::new(4, 2).set_timestamp_ms(3_000).build()),
855            VerifiedBlock::new_for_test(TestBlock::new(4, 3).set_timestamp_ms(4_000).build()),
856        ];
857
858        let leader_block = VerifiedBlock::new_for_test(
859            TestBlock::new(5, 0)
860                .set_timestamp_ms(5_000)
861                .set_ancestors(
862                    ancestors
863                        .iter()
864                        .map(|block| block.reference())
865                        .collect::<Vec<_>>(),
866                )
867                .build(),
868        );
869
870        for block in &ancestors {
871            dag_state.accept_block(block.clone());
872        }
873
874        let last_commit_timestamp_ms = 0;
875
876        // WHEN
877        let timestamp = Linearizer::calculate_commit_timestamp(
878            &context,
879            &mut dag_state,
880            &leader_block,
881            last_commit_timestamp_ms,
882        );
883        assert_eq!(timestamp, timestamp_1);
884
885        // AND skip the block of authority 0 and round 4.
886        let leader_block = VerifiedBlock::new_for_test(
887            TestBlock::new(5, 0)
888                .set_timestamp_ms(5_000)
889                .set_ancestors(
890                    ancestors
891                        .iter()
892                        .skip(1)
893                        .map(|block| block.reference())
894                        .collect::<Vec<_>>(),
895                )
896                .build(),
897        );
898
899        let timestamp = Linearizer::calculate_commit_timestamp(
900            &context,
901            &mut dag_state,
902            &leader_block,
903            last_commit_timestamp_ms,
904        );
905        assert_eq!(timestamp, timestamp_2);
906
907        // AND set the `last_commit_timestamp_ms` to 6_000
908        let last_commit_timestamp_ms = 6_000;
909        let timestamp = Linearizer::calculate_commit_timestamp(
910            &context,
911            &mut dag_state,
912            &leader_block,
913            last_commit_timestamp_ms,
914        );
915        assert_eq!(timestamp, timestamp_3);
916
917        // AND there is only one ancestor block to commit
918        let (context, _) = Context::new_for_test(1);
919        let leader_block = VerifiedBlock::new_for_test(
920            TestBlock::new(5, 0)
921                .set_timestamp_ms(5_000)
922                .set_ancestors(
923                    ancestors
924                        .iter()
925                        .take(1)
926                        .map(|block| block.reference())
927                        .collect::<Vec<_>>(),
928                )
929                .build(),
930        );
931        let last_commit_timestamp_ms = 0;
932        let timestamp = Linearizer::calculate_commit_timestamp(
933            &context,
934            &mut dag_state,
935            &leader_block,
936            last_commit_timestamp_ms,
937        );
938        assert_eq!(timestamp, 1_000);
939    }
940
941    #[test]
942    fn test_median_timestamps_by_stake() {
943        // One total stake.
944        let timestamps = vec![(1_000, 1)];
945        assert_eq!(median_timestamps_by_stake_inner(timestamps, 1), 1_000);
946
947        // Odd number of total stakes.
948        let timestamps = vec![(1_000, 1), (2_000, 1), (3_000, 1)];
949        assert_eq!(median_timestamps_by_stake_inner(timestamps, 3), 2_000);
950
951        // Even number of total stakes.
952        let timestamps = vec![(1_000, 1), (2_000, 1), (3_000, 1), (4_000, 1)];
953        assert_eq!(median_timestamps_by_stake_inner(timestamps, 4), 3_000);
954
955        // Even number of total stakes, different order.
956        let timestamps = vec![(4_000, 1), (3_000, 1), (1_000, 1), (2_000, 1)];
957        assert_eq!(median_timestamps_by_stake_inner(timestamps, 4), 3_000);
958
959        // Unequal stakes.
960        let timestamps = vec![(2_000, 2), (4_000, 2), (1_000, 3), (3_000, 3)];
961        assert_eq!(median_timestamps_by_stake_inner(timestamps, 10), 3_000);
962
963        // Unequal stakes.
964        let timestamps = vec![
965            (500, 2),
966            (4_000, 2),
967            (2_500, 3),
968            (1_000, 5),
969            (3_000, 3),
970            (2_000, 4),
971        ];
972        assert_eq!(median_timestamps_by_stake_inner(timestamps, 19), 2_000);
973
974        // One authority dominates.
975        let timestamps = vec![(1_000, 1), (2_000, 1), (3_000, 1), (4_000, 1), (5_000, 10)];
976        assert_eq!(median_timestamps_by_stake_inner(timestamps, 14), 5_000);
977    }
978
979    #[tokio::test]
980    async fn test_median_timestamps_by_stake_errors() {
981        let num_authorities = 4;
982        let (context, _keys) = Context::new_for_test(num_authorities);
983        let context = Arc::new(context);
984
985        // No blocks provided
986        let err = median_timestamp_by_stake(&context, &[] as &[VerifiedBlock]).unwrap_err();
987        assert_eq!(err, "No blocks provided");
988
989        // Blocks provided but total stake is less than quorum threshold
990        let block = VerifiedBlock::new_for_test(TestBlock::new(5, 0).build());
991        let err = median_timestamp_by_stake(&context, &[block]).unwrap_err();
992        assert_eq!(err, "Total stake 1 < quorum threshold 3");
993    }
994}