Skip to main content

consensus_core/
dag_state.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    cmp::max,
6    collections::{BTreeMap, BTreeSet, VecDeque},
7    ops::Bound::{Excluded, Included, Unbounded},
8    panic,
9    sync::Arc,
10    time::Duration,
11    vec,
12};
13
14use consensus_config::AuthorityIndex;
15use consensus_types::block::{BlockDigest, BlockRef, BlockTimestampMs, Round, TransactionIndex};
16use itertools::Itertools as _;
17use mysten_common::ZipDebugEqIteratorExt;
18use tokio::time::Instant;
19use tracing::{debug, error, info, trace};
20
21use crate::{
22    CommittedSubDag,
23    block::{BlockAPI, GENESIS_ROUND, Slot, VerifiedBlock, genesis_blocks},
24    commit::{
25        CommitAPI as _, CommitDigest, CommitIndex, CommitInfo, CommitRef, CommitVote,
26        GENESIS_COMMIT_INDEX, TrustedCommit, load_committed_subdag_from_store,
27    },
28    context::Context,
29    leader_scoring::{ReputationScores, ScoringSubdag},
30    stake_aggregator::{QuorumThreshold, StakeAggregator},
31    storage::{Store, WriteBatch},
32    threshold_clock::ThresholdClock,
33};
34
35/// DagState provides the API to write and read accepted blocks from the DAG.
36/// Only uncommitted and last committed blocks are cached in memory.
37/// The rest of blocks are stored on disk.
38/// Refs to cached blocks and additional refs are cached as well, to speed up existence checks.
39///
40/// Note: DagState should be wrapped with Arc<parking_lot::RwLock<_>>, to allow
41/// concurrent access from multiple components.
42pub struct DagState {
43    context: Arc<Context>,
44
45    // The genesis blocks
46    genesis: BTreeMap<BlockRef, VerifiedBlock>,
47
48    // Contains recent blocks within CACHED_ROUNDS from the last committed round per authority.
49    // Note: all uncommitted blocks are kept in memory.
50    //
51    // When GC is enabled, this map has a different semantic. It holds all the recent data for each authority making sure that it always have available
52    // CACHED_ROUNDS worth of data. The entries are evicted based on the latest GC round, however the eviction process will respect the CACHED_ROUNDS.
53    // For each authority, blocks are only evicted when their round is less than or equal to both `gc_round`, and `highest authority round - cached rounds`.
54    // This ensures that the GC requirements are respected (we never clean up any block above `gc_round`), and there are enough blocks cached.
55    recent_blocks: BTreeMap<BlockRef, BlockInfo>,
56
57    // Indexes recent block refs by their authorities.
58    // Vec position corresponds to the authority index.
59    recent_refs_by_authority: Vec<BTreeSet<BlockRef>>,
60
61    // Per-round aggregation of accepted blocks. Front holds the oldest retained
62    // round (`gc_round + 1` after eviction); back holds `highest_accepted_round`.
63    // Rounds between front and back are always contiguous in a valid DagState.
64    round_info: VecDeque<RoundInfo>,
65
66    // Keeps track of the threshold clock for proposing blocks.
67    threshold_clock: ThresholdClock,
68
69    // Keeps track of the highest round that has been evicted for each authority. Any blocks that are of round <= evict_round
70    // should be considered evicted, and if any exist we should not consider the causauly complete in the order they appear.
71    // The `evicted_rounds` size should be the same as the committee size.
72    evicted_rounds: Vec<Round>,
73
74    // Highest round of blocks accepted.
75    highest_accepted_round: Round,
76
77    // Last consensus commit of the dag.
78    last_commit: Option<TrustedCommit>,
79
80    // Last wall time when commit round advanced. Does not persist across restarts.
81    last_commit_round_advancement_time: Option<std::time::Instant>,
82
83    // Last committed rounds per authority.
84    last_committed_rounds: Vec<Round>,
85
86    /// The committed subdags that have been scored but scores have not been used
87    /// for leader schedule yet.
88    scoring_subdag: ScoringSubdag,
89
90    // Commit votes pending to be included in new blocks.
91    // TODO: limit to 1st commit per round with multi-leader.
92    // TODO: recover unproposed pending commit votes at startup.
93    pending_commit_votes: VecDeque<CommitVote>,
94
95    // Blocks and commits must be buffered for persistence before they can be
96    // inserted into the local DAG or sent to output.
97    blocks_to_write: Vec<VerifiedBlock>,
98    commits_to_write: Vec<TrustedCommit>,
99
100    // Buffers the reputation scores & last_committed_rounds to be flushed with the
101    // next dag state flush. Not writing eagerly is okay because we can recover reputation scores
102    // & last_committed_rounds from the commits as needed.
103    commit_info_to_write: Vec<(CommitRef, CommitInfo)>,
104
105    // Buffers finalized commits and their rejected transactions to be written to storage.
106    finalized_commits_to_write: Vec<(CommitRef, BTreeMap<BlockRef, Vec<TransactionIndex>>)>,
107
108    // Persistent storage for blocks, commits and other consensus data.
109    store: Arc<dyn Store>,
110
111    // The number of cached rounds
112    cached_rounds: Round,
113}
114
115impl DagState {
116    /// Initializes DagState from storage.
117    pub fn new(context: Arc<Context>, store: Arc<dyn Store>) -> Self {
118        let cached_rounds = context.parameters.dag_state_cached_rounds as Round;
119        let num_authorities = context.committee.size();
120
121        let genesis = genesis_blocks(context.as_ref())
122            .into_iter()
123            .map(|block| (block.reference(), block))
124            .collect();
125
126        let threshold_clock = ThresholdClock::new(1, context.clone());
127
128        let last_commit = store
129            .read_last_commit()
130            .unwrap_or_else(|e| panic!("Failed to read from storage: {:?}", e));
131
132        let commit_info = store
133            .read_last_commit_info()
134            .unwrap_or_else(|e| panic!("Failed to read from storage: {:?}", e));
135        let (mut last_committed_rounds, commit_recovery_start_index) =
136            if let Some((commit_ref, commit_info)) = commit_info {
137                tracing::info!("Recovering committed state from {commit_ref} {commit_info:?}");
138                (commit_info.committed_rounds, commit_ref.index + 1)
139            } else {
140                tracing::info!("Found no stored CommitInfo to recover from");
141                (vec![0; num_authorities], GENESIS_COMMIT_INDEX + 1)
142            };
143
144        let mut unscored_committed_subdags = Vec::new();
145        let mut scoring_subdag = ScoringSubdag::new(context.clone());
146
147        if let Some(last_commit) = last_commit.as_ref() {
148            store
149                .scan_commits((commit_recovery_start_index..=last_commit.index()).into())
150                .unwrap_or_else(|e| panic!("Failed to read from storage: {:?}", e))
151                .iter()
152                .for_each(|commit| {
153                    for block_ref in commit.blocks() {
154                        last_committed_rounds[block_ref.author] =
155                            max(last_committed_rounds[block_ref.author], block_ref.round);
156                    }
157                    let committed_subdag =
158                        load_committed_subdag_from_store(store.as_ref(), commit.clone());
159                    unscored_committed_subdags.push(committed_subdag);
160                });
161        }
162
163        tracing::info!(
164            "DagState was initialized with the following state: \
165            {last_commit:?}; {last_committed_rounds:?}; {} unscored committed subdags;",
166            unscored_committed_subdags.len()
167        );
168
169        scoring_subdag.add_subdags(std::mem::take(&mut unscored_committed_subdags));
170
171        let mut state = Self {
172            context: context.clone(),
173            genesis,
174            recent_blocks: BTreeMap::new(),
175            recent_refs_by_authority: vec![BTreeSet::new(); num_authorities],
176            round_info: VecDeque::new(),
177            threshold_clock,
178            highest_accepted_round: 0,
179            last_commit: last_commit.clone(),
180            last_commit_round_advancement_time: None,
181            last_committed_rounds: last_committed_rounds.clone(),
182            pending_commit_votes: VecDeque::new(),
183            blocks_to_write: vec![],
184            commits_to_write: vec![],
185            commit_info_to_write: vec![],
186            finalized_commits_to_write: vec![],
187            scoring_subdag,
188            store: store.clone(),
189            cached_rounds,
190            evicted_rounds: vec![0; num_authorities],
191        };
192
193        let mut recovered_blocks = Vec::new();
194        for (authority_index, _) in context.committee.authorities() {
195            let (blocks, eviction_round) = {
196                // Find the latest block for the authority to calculate the eviction round. Then we want to scan and load the blocks from the eviction round and onwards only.
197                // As reminder, the eviction round is taking into account the gc_round.
198                let last_block = state
199                    .store
200                    .scan_last_blocks_by_author(authority_index, 1, None)
201                    .expect("Database error");
202                let last_block_round = last_block
203                    .last()
204                    .map(|b| b.round())
205                    .unwrap_or(GENESIS_ROUND);
206
207                let eviction_round =
208                    Self::eviction_round(last_block_round, state.gc_round(), state.cached_rounds);
209                let blocks = state
210                    .store
211                    .scan_blocks_by_author(authority_index, eviction_round + 1)
212                    .expect("Database error");
213
214                (blocks, eviction_round)
215            };
216
217            debug!(
218                "Recovered blocks {}: {:?}",
219                authority_index,
220                blocks
221                    .iter()
222                    .map(|b| b.reference())
223                    .collect::<Vec<BlockRef>>()
224            );
225            recovered_blocks.extend(blocks);
226
227            state.evicted_rounds[authority_index] = eviction_round;
228        }
229
230        // Update the block metadata across all recovered blocks from lowest round to highest round.
231        recovered_blocks.sort_by_key(|b| b.reference());
232        for block in &recovered_blocks {
233            state.update_block_metadata(block);
234        }
235
236        if let Some(last_commit) = last_commit {
237            let mut index = last_commit.index();
238            let gc_round = state.gc_round();
239            info!(
240                "Recovering block commit statuses from commit index {} and backwards until leader of round <= gc_round {:?}",
241                index, gc_round
242            );
243
244            loop {
245                let commits = store
246                    .scan_commits((index..=index).into())
247                    .unwrap_or_else(|e| panic!("Failed to read from storage: {:?}", e));
248                let Some(commit) = commits.first() else {
249                    info!("Recovering finished up to index {index}, no more commits to recover");
250                    break;
251                };
252
253                // Check the commit leader round to see if it is within the gc_round. If it is not then we can stop the recovery process.
254                if gc_round > 0 && commit.leader().round <= gc_round {
255                    info!(
256                        "Recovering finished, reached commit leader round {} <= gc_round {}",
257                        commit.leader().round,
258                        gc_round
259                    );
260                    break;
261                }
262
263                commit.blocks().iter().filter(|b| b.round > gc_round).for_each(|block_ref|{
264                    debug!(
265                        "Setting block {:?} as committed based on commit {:?}",
266                        block_ref,
267                        commit.index()
268                    );
269                    assert!(state.set_committed(block_ref), "Attempted to set again a block {:?} as committed when recovering commit {:?}", block_ref, commit);
270                });
271
272                // All commits are indexed starting from 1, so one reach zero exit.
273                index = index.saturating_sub(1);
274                if index == 0 {
275                    break;
276                }
277            }
278        }
279
280        // Recover hard linked statuses for blocks within GC round.
281        let proposed_blocks = store
282            .scan_blocks_by_author(context.own_index, state.gc_round() + 1)
283            .expect("Database error");
284        for block in proposed_blocks {
285            state.link_causal_history(block.reference());
286        }
287
288        state
289    }
290
291    /// Accepts a block into DagState and keeps it in memory.
292    pub(crate) fn accept_block(&mut self, block: VerifiedBlock) {
293        assert_ne!(
294            block.round(),
295            0,
296            "Genesis block should not be accepted into DAG."
297        );
298
299        let block_ref = block.reference();
300        if self.contains_block(&block_ref) {
301            return;
302        }
303
304        let now = self.context.clock.timestamp_utc_ms();
305        if block.timestamp_ms() > now {
306            trace!(
307                "Block {:?} with timestamp {} is greater than local timestamp {}.",
308                block,
309                block.timestamp_ms(),
310                now,
311            );
312        }
313        let hostname = &self.context.committee.authority(block_ref.author).hostname;
314        self.context
315            .metrics
316            .node_metrics
317            .accepted_block_time_drift_ms
318            .with_label_values(&[hostname])
319            .inc_by(block.timestamp_ms().saturating_sub(now));
320
321        // TODO: Move this check to core
322        // Ensure we don't write multiple blocks per slot for our own index
323        if block_ref.author == self.context.own_index {
324            let existing_blocks = self.get_uncommitted_blocks_at_slot(block_ref.into());
325            if !self
326                .context
327                .parameters
328                .internal
329                .skip_equivocation_validation
330            {
331                assert!(
332                    existing_blocks.is_empty(),
333                    "Block Rejected! Attempted to add block {block:#?} to own slot where \
334                    block(s) {existing_blocks:#?} already exists."
335                );
336            }
337        }
338        self.update_block_metadata(&block);
339        self.blocks_to_write.push(block);
340        let source = if self.context.own_index == block_ref.author {
341            "own"
342        } else {
343            "others"
344        };
345        self.context
346            .metrics
347            .node_metrics
348            .accepted_blocks
349            .with_label_values(&[source])
350            .inc();
351    }
352
353    /// Updates internal metadata for a block.
354    fn update_block_metadata(&mut self, block: &VerifiedBlock) {
355        let block_ref = block.reference();
356        self.recent_blocks
357            .insert(block_ref, BlockInfo::new(block.clone()));
358        self.recent_refs_by_authority[block_ref.author].insert(block_ref);
359        if self.context.protocol_config.enable_v3() {
360            // Update votes accounting in BlockInfo.
361            for ancestor in block.ancestors() {
362                // Only update children info when the link is potentially a leader vote.
363                if ancestor.round + 1 != block_ref.round || ancestor.round <= self.gc_round() {
364                    continue;
365                }
366                let block_info = self.recent_blocks.get_mut(ancestor).unwrap_or_else(|| {
367                    panic!(
368                        "Parent block {} of block {} does not exist",
369                        ancestor, block_ref
370                    )
371                });
372                block_info.children.insert(block_ref);
373                block_info
374                    .children_stake
375                    .add_unique(block_ref.author, &self.context.committee);
376            }
377            self.update_round_info(block);
378        }
379        if self.threshold_clock.add_block(block_ref) {
380            // Do not measure quorum delay when no local block is proposed in the round.
381            if let Some(last_proposed_block) = self.get_last_proposed_block()
382                && last_proposed_block.round() == block_ref.round
383            {
384                let quorum_delay_ms = self
385                    .context
386                    .clock
387                    .timestamp_utc_ms()
388                    .saturating_sub(last_proposed_block.timestamp_ms());
389                self.context
390                    .metrics
391                    .node_metrics
392                    .quorum_receive_latency
393                    .observe(Duration::from_millis(quorum_delay_ms).as_secs_f64());
394            }
395        }
396
397        self.highest_accepted_round = max(self.highest_accepted_round, block.round());
398        self.context
399            .metrics
400            .node_metrics
401            .highest_accepted_round
402            .set(self.highest_accepted_round as i64);
403
404        let highest_accepted_round_for_author = self.recent_refs_by_authority[block_ref.author]
405            .last()
406            .map(|block_ref| block_ref.round)
407            .expect("There should be by now at least one block ref");
408        let hostname = &self.context.committee.authority(block_ref.author).hostname;
409        self.context
410            .metrics
411            .node_metrics
412            .highest_accepted_authority_round
413            .with_label_values(&[hostname])
414            .set(highest_accepted_round_for_author as i64);
415    }
416
417    /// Accepts a blocks into DagState and keeps it in memory.
418    pub(crate) fn accept_blocks(&mut self, blocks: Vec<VerifiedBlock>) {
419        debug!(
420            "Accepting blocks: {}",
421            blocks.iter().map(|b| b.reference().to_string()).join(",")
422        );
423        for block in blocks {
424            self.accept_block(block);
425        }
426    }
427
428    /// Gets a block by checking cached recent blocks then storage.
429    /// Returns None when the block is not found.
430    pub(crate) fn get_block(&self, reference: &BlockRef) -> Option<VerifiedBlock> {
431        self.get_blocks(&[*reference])
432            .pop()
433            .expect("Exactly one element should be returned")
434    }
435
436    /// Gets blocks by checking genesis, cached recent blocks in memory, then storage.
437    /// An element is None when the corresponding block is not found.
438    pub(crate) fn get_blocks(&self, block_refs: &[BlockRef]) -> Vec<Option<VerifiedBlock>> {
439        if block_refs.is_empty() {
440            return vec![];
441        }
442
443        let mut blocks = vec![None; block_refs.len()];
444        let mut missing = Vec::new();
445
446        for (index, block_ref) in block_refs.iter().enumerate() {
447            if block_ref.round == GENESIS_ROUND {
448                // Allow the caller to handle the invalid genesis ancestor error.
449                if let Some(block) = self.genesis.get(block_ref) {
450                    blocks[index] = Some(block.clone());
451                }
452                continue;
453            }
454            if let Some(block_info) = self.recent_blocks.get(block_ref) {
455                blocks[index] = Some(block_info.block.clone());
456                continue;
457            }
458            missing.push((index, block_ref));
459        }
460
461        if missing.is_empty() {
462            return blocks;
463        }
464
465        let missing_refs = missing
466            .iter()
467            .map(|(_, block_ref)| **block_ref)
468            .collect::<Vec<_>>();
469        let store_results = self
470            .store
471            .read_blocks(&missing_refs)
472            .unwrap_or_else(|e| panic!("Failed to read from storage: {:?}", e));
473        self.context
474            .metrics
475            .node_metrics
476            .dag_state_store_read_count
477            .with_label_values(&["get_blocks"])
478            .inc();
479
480        for ((index, _), result) in missing.into_iter().zip_debug_eq(store_results) {
481            blocks[index] = result;
482        }
483
484        blocks
485    }
486
487    /// Gets all block infos in the slot.
488    /// Must be called on slots above gc_round, otherwise the result may be misleading due to GC eviction.
489    pub(crate) fn get_block_info_at_slot(&self, slot: Slot) -> Vec<BlockInfo> {
490        assert!(
491            slot.round > self.gc_round(),
492            "get_block_info_at_slot() should only be called for slots above gc_round: slot {}, gc_round {}",
493            slot,
494            self.gc_round()
495        );
496        let mut results = vec![];
497        for (_block_ref, block_info) in self.recent_blocks.range((
498            Included(BlockRef::new(slot.round, slot.authority, BlockDigest::MIN)),
499            Included(BlockRef::new(slot.round, slot.authority, BlockDigest::MAX)),
500        )) {
501            results.push(block_info.clone());
502        }
503        results
504    }
505
506    /// Gets all uncommitted blocks in a slot.
507    /// Uncommitted blocks must exist in memory, so only in-memory blocks are checked.
508    pub(crate) fn get_uncommitted_blocks_at_slot(&self, slot: Slot) -> Vec<VerifiedBlock> {
509        // TODO: either panic below when the slot is at or below the last committed round,
510        // or support reading from storage while limiting storage reads to edge cases.
511
512        let mut blocks = vec![];
513        for (_block_ref, block_info) in self.recent_blocks.range((
514            Included(BlockRef::new(slot.round, slot.authority, BlockDigest::MIN)),
515            Included(BlockRef::new(slot.round, slot.authority, BlockDigest::MAX)),
516        )) {
517            blocks.push(block_info.block.clone())
518        }
519        blocks
520    }
521
522    /// Gets all uncommitted blocks in a round.
523    /// Uncommitted blocks must exist in memory, so only in-memory blocks are checked.
524    pub(crate) fn get_uncommitted_blocks_at_round(&self, round: Round) -> Vec<VerifiedBlock> {
525        if round <= self.last_commit_round() {
526            panic!("Round {} have committed blocks!", round);
527        }
528
529        let mut blocks = vec![];
530        for (_block_ref, block_info) in self.recent_blocks.range((
531            Included(BlockRef::new(round, AuthorityIndex::ZERO, BlockDigest::MIN)),
532            Excluded(BlockRef::new(
533                round + 1,
534                AuthorityIndex::ZERO,
535                BlockDigest::MIN,
536            )),
537        )) {
538            blocks.push(block_info.block.clone())
539        }
540        blocks
541    }
542
543    #[cfg(test)]
544    pub(crate) fn get_block_children(&self, block_ref: &BlockRef) -> Option<Vec<BlockRef>> {
545        if block_ref.round <= self.gc_round() {
546            return None;
547        }
548        self.recent_blocks
549            .get(block_ref)
550            .map(|block_info| block_info.children.iter().cloned().collect())
551    }
552
553    #[cfg(test)]
554    pub(crate) fn get_block_children_authorities(
555        &self,
556        block_ref: &BlockRef,
557    ) -> Option<BTreeSet<AuthorityIndex>> {
558        if block_ref.round <= self.gc_round() {
559            return None;
560        }
561        self.recent_blocks
562            .get(block_ref)
563            .map(|block_info| block_info.children_stake.authorities().clone())
564    }
565
566    #[cfg(test)]
567    pub(crate) fn get_block_total_children_stake(
568        &self,
569        block_ref: &BlockRef,
570    ) -> Option<consensus_config::Stake> {
571        if block_ref.round <= self.gc_round() {
572            return None;
573        }
574        self.recent_blocks
575            .get(block_ref)
576            .map(|block_info| block_info.children_stake.stake())
577    }
578
579    /// Gets all ancestors in the history of a block at a certain round.
580    pub(crate) fn ancestors_at_round(
581        &self,
582        later_block: &VerifiedBlock,
583        earlier_round: Round,
584    ) -> Vec<VerifiedBlock> {
585        // Iterate through ancestors of later_block in round descending order.
586        let mut linked: BTreeSet<BlockRef> = later_block.ancestors().iter().cloned().collect();
587        while !linked.is_empty() {
588            let round = linked.last().unwrap().round;
589            // Stop after finishing traversal for ancestors above earlier_round.
590            if round <= earlier_round {
591                break;
592            }
593            let block_ref = linked.pop_last().unwrap();
594            let Some(block) = self.get_block(&block_ref) else {
595                panic!("Block {:?} should exist in DAG!", block_ref);
596            };
597            linked.extend(block.ancestors().iter().cloned());
598        }
599        linked
600            .range((
601                Included(BlockRef::new(
602                    earlier_round,
603                    AuthorityIndex::ZERO,
604                    BlockDigest::MIN,
605                )),
606                Unbounded,
607            ))
608            .map(|r| {
609                self.get_block(r)
610                    .unwrap_or_else(|| panic!("Block {:?} should exist in DAG!", r))
611                    .clone()
612            })
613            .collect()
614    }
615
616    /// Gets the last proposed block from this authority.
617    /// If no block is proposed yet, returns the genesis block.
618    /// If the node is an observer, returns None.
619    pub(crate) fn get_last_proposed_block(&self) -> Option<VerifiedBlock> {
620        if self.context.is_validator() {
621            Some(self.get_last_block_for_authority(self.context.own_index))
622        } else {
623            None
624        }
625    }
626
627    /// Retrieves the last accepted block from the specified `authority`. If no block is found in cache
628    /// then the genesis block is returned as no other block has been received from that authority.
629    pub(crate) fn get_last_block_for_authority(&self, authority: AuthorityIndex) -> VerifiedBlock {
630        if let Some(last) = self.recent_refs_by_authority[authority].last() {
631            return self
632                .recent_blocks
633                .get(last)
634                .expect("Block should be found in recent blocks")
635                .block
636                .clone();
637        }
638
639        // if none exists, then fallback to genesis
640        let (_, genesis_block) = self
641            .genesis
642            .iter()
643            .find(|(block_ref, _)| block_ref.author == authority)
644            .expect("Genesis should be found for authority {authority_index}");
645        genesis_block.clone()
646    }
647
648    /// Returns cached recent blocks from the specified authority.
649    /// Blocks returned are limited to round >= `start`, and cached.
650    /// NOTE: caller should not assume returned blocks are always chained.
651    /// "Disconnected" blocks can be returned when there are byzantine blocks,
652    /// or a previously evicted block is accepted again.
653    pub(crate) fn get_cached_blocks(
654        &self,
655        authority: AuthorityIndex,
656        start: Round,
657    ) -> Vec<VerifiedBlock> {
658        self.get_cached_blocks_in_range(authority, start, Round::MAX, usize::MAX)
659    }
660
661    // Retrieves the cached block within the range [start_round, end_round) from a given authority,
662    // limited in total number of blocks.
663    pub(crate) fn get_cached_blocks_in_range(
664        &self,
665        authority: AuthorityIndex,
666        start_round: Round,
667        end_round: Round,
668        limit: usize,
669    ) -> Vec<VerifiedBlock> {
670        if start_round >= end_round || limit == 0 {
671            return vec![];
672        }
673
674        let mut blocks = vec![];
675        for block_ref in self.recent_refs_by_authority[authority].range((
676            Included(BlockRef::new(start_round, authority, BlockDigest::MIN)),
677            Excluded(BlockRef::new(
678                end_round,
679                AuthorityIndex::MIN,
680                BlockDigest::MIN,
681            )),
682        )) {
683            let block_info = self
684                .recent_blocks
685                .get(block_ref)
686                .expect("Block should exist in recent blocks");
687            blocks.push(block_info.block.clone());
688            if blocks.len() >= limit {
689                break;
690            }
691        }
692        blocks
693    }
694
695    // Retrieves the last cached block within the range [start_round, end_round) from a given authority.
696    pub(crate) fn get_last_cached_block_in_range(
697        &self,
698        authority: AuthorityIndex,
699        start_round: Round,
700        end_round: Round,
701    ) -> Option<VerifiedBlock> {
702        if start_round >= end_round {
703            return None;
704        }
705
706        let block_ref = self.recent_refs_by_authority[authority]
707            .range((
708                Included(BlockRef::new(start_round, authority, BlockDigest::MIN)),
709                Excluded(BlockRef::new(
710                    end_round,
711                    AuthorityIndex::MIN,
712                    BlockDigest::MIN,
713                )),
714            ))
715            .last()?;
716
717        self.recent_blocks
718            .get(block_ref)
719            .map(|block_info| block_info.block.clone())
720    }
721
722    /// Returns the last block proposed per authority with `evicted round < round < end_round`.
723    /// The method is guaranteed to return results only when the `end_round` is not earlier of the
724    /// available cached data for each authority (evicted round + 1), otherwise the method will panic.
725    /// It's the caller's responsibility to ensure that is not requesting for earlier rounds.
726    /// In case of equivocation for an authority's last slot, one block will be returned (the last in order)
727    /// and the other equivocating blocks will be returned.
728    pub(crate) fn get_last_cached_block_per_authority(
729        &self,
730        end_round: Round,
731    ) -> Vec<(VerifiedBlock, Vec<BlockRef>)> {
732        // Initialize with the genesis blocks as fallback
733        let mut blocks = self.genesis.values().cloned().collect::<Vec<_>>();
734        let mut equivocating_blocks = vec![vec![]; self.context.committee.size()];
735
736        if end_round == GENESIS_ROUND {
737            panic!(
738                "Attempted to retrieve blocks earlier than the genesis round which is not possible"
739            );
740        }
741
742        if end_round == GENESIS_ROUND + 1 {
743            return blocks.into_iter().map(|b| (b, vec![])).collect();
744        }
745
746        for (authority_index, block_refs) in self.recent_refs_by_authority.iter().enumerate() {
747            let authority_index = self
748                .context
749                .committee
750                .to_authority_index(authority_index)
751                .unwrap();
752
753            let last_evicted_round = self.evicted_rounds[authority_index];
754            if end_round.saturating_sub(1) <= last_evicted_round {
755                panic!(
756                    "Attempted to request for blocks of rounds < {end_round}, when the last evicted round is {last_evicted_round} for authority {authority_index}",
757                );
758            }
759
760            let block_ref_iter = block_refs
761                .range((
762                    Included(BlockRef::new(
763                        last_evicted_round + 1,
764                        authority_index,
765                        BlockDigest::MIN,
766                    )),
767                    Excluded(BlockRef::new(end_round, authority_index, BlockDigest::MIN)),
768                ))
769                .rev();
770
771            let mut last_round = 0;
772            for block_ref in block_ref_iter {
773                if last_round == 0 {
774                    last_round = block_ref.round;
775                    let block_info = self
776                        .recent_blocks
777                        .get(block_ref)
778                        .expect("Block should exist in recent blocks");
779                    blocks[authority_index] = block_info.block.clone();
780                    continue;
781                }
782                if block_ref.round < last_round {
783                    break;
784                }
785                equivocating_blocks[authority_index].push(*block_ref);
786            }
787        }
788
789        blocks
790            .into_iter()
791            .zip_debug_eq(equivocating_blocks)
792            .collect()
793    }
794
795    /// Checks whether a block exists in the slot. The method checks only against the cached data.
796    /// If the user asks for a slot that is not within the cached data then a panic is thrown.
797    pub(crate) fn contains_cached_block_at_slot(&self, slot: Slot) -> bool {
798        // Always return true for genesis slots.
799        if slot.round == GENESIS_ROUND {
800            return true;
801        }
802
803        let eviction_round = self.evicted_rounds[slot.authority];
804        if slot.round <= eviction_round {
805            panic!(
806                "{}",
807                format!(
808                    "Attempted to check for slot {slot} that is <= the last evicted round {eviction_round}"
809                )
810            );
811        }
812
813        let mut result = self.recent_refs_by_authority[slot.authority].range((
814            Included(BlockRef::new(slot.round, slot.authority, BlockDigest::MIN)),
815            Included(BlockRef::new(slot.round, slot.authority, BlockDigest::MAX)),
816        ));
817        result.next().is_some()
818    }
819
820    /// Checks whether the required blocks are in cache, if exist, or otherwise will check in store. The method is not caching
821    /// back the results, so its expensive if keep asking for cache missing blocks.
822    pub(crate) fn contains_blocks(&self, block_refs: Vec<BlockRef>) -> Vec<bool> {
823        let mut exist = vec![false; block_refs.len()];
824        let mut missing = Vec::new();
825
826        for (index, block_ref) in block_refs.into_iter().enumerate() {
827            let recent_refs = &self.recent_refs_by_authority[block_ref.author];
828            if recent_refs.contains(&block_ref) || self.genesis.contains_key(&block_ref) {
829                exist[index] = true;
830            } else if recent_refs.is_empty() || recent_refs.last().unwrap().round < block_ref.round
831            {
832                // Optimization: recent_refs contain the most recent blocks known to this authority.
833                // If a block ref is not found there and has a higher round, it definitely is
834                // missing from this authority and there is no need to check disk.
835                exist[index] = false;
836            } else {
837                missing.push((index, block_ref));
838            }
839        }
840
841        if missing.is_empty() {
842            return exist;
843        }
844
845        let missing_refs = missing
846            .iter()
847            .map(|(_, block_ref)| *block_ref)
848            .collect::<Vec<_>>();
849        let store_results = self
850            .store
851            .contains_blocks(&missing_refs)
852            .unwrap_or_else(|e| panic!("Failed to read from storage: {:?}", e));
853        self.context
854            .metrics
855            .node_metrics
856            .dag_state_store_read_count
857            .with_label_values(&["contains_blocks"])
858            .inc();
859
860        for ((index, _), result) in missing.into_iter().zip_debug_eq(store_results) {
861            exist[index] = result;
862        }
863
864        exist
865    }
866
867    pub(crate) fn contains_block(&self, block_ref: &BlockRef) -> bool {
868        let blocks = self.contains_blocks(vec![*block_ref]);
869        blocks.first().cloned().unwrap()
870    }
871
872    // Sets the block as committed in the cache. If the block is set as committed for first time, then true is returned, otherwise false is returned instead.
873    // Method will panic if the block is not found in the cache.
874    pub(crate) fn set_committed(&mut self, block_ref: &BlockRef) -> bool {
875        if let Some(block_info) = self.recent_blocks.get_mut(block_ref) {
876            if !block_info.committed {
877                block_info.committed = true;
878                return true;
879            }
880            false
881        } else {
882            panic!(
883                "Block {:?} not found in cache to set as committed.",
884                block_ref
885            );
886        }
887    }
888
889    /// Returns true if the block is committed. Only valid for blocks above the GC round.
890    pub(crate) fn is_committed(&self, block_ref: &BlockRef) -> bool {
891        self.recent_blocks
892            .get(block_ref)
893            .unwrap_or_else(|| panic!("Attempted to query for commit status for a block not in cached data {block_ref}"))
894            .committed
895    }
896
897    /// Recursively sets blocks in the causal history of the root block as hard linked, including the root block itself.
898    /// Returns the list of blocks that are newly linked.
899    /// The returned blocks are guaranteed to be above the GC round.
900    /// Transaction votes for the returned blocks are retrieved and carried by the upcoming
901    /// proposed block.
902    pub(crate) fn link_causal_history(&mut self, root_block: BlockRef) -> Vec<BlockRef> {
903        let gc_round = self.gc_round();
904        let mut linked_blocks = vec![];
905        let mut targets = VecDeque::new();
906        targets.push_back(root_block);
907        while let Some(block_ref) = targets.pop_front() {
908            // No need to collect or mark blocks at or below GC round.
909            // These blocks and their causal history will not be included in new commits.
910            // And their transactions do not need votes to finalize or skip.
911            //
912            // CommitFinalizer::gced_transaction_votes_for_pending_block() is the counterpart
913            // to this logic, when deciding if block A in the causal history of block B gets
914            // implicit accept transaction votes from block B.
915            if block_ref.round <= gc_round {
916                continue;
917            }
918            let block_info = self
919                .recent_blocks
920                .get_mut(&block_ref)
921                .unwrap_or_else(|| panic!("Block {:?} is not in DAG state", block_ref));
922            if block_info.included {
923                continue;
924            }
925            linked_blocks.push(block_ref);
926            block_info.included = true;
927            targets.extend(block_info.block.ancestors().iter());
928        }
929        linked_blocks
930    }
931
932    /// Returns true if the block has been included in an owned proposed block.
933    /// NOTE: caller should make sure only blocks above GC round are queried.
934    pub(crate) fn has_been_included(&self, block_ref: &BlockRef) -> bool {
935        self.recent_blocks
936            .get(block_ref)
937            .unwrap_or_else(|| {
938                panic!(
939                    "Attempted to query for inclusion status for a block not in cached data {}",
940                    block_ref
941                )
942            })
943            .included
944    }
945
946    pub(crate) fn threshold_clock_round(&self) -> Round {
947        self.threshold_clock.get_round()
948    }
949
950    // The timestamp of when quorum threshold was last reached in the threshold clock.
951    pub(crate) fn threshold_clock_quorum_ts(&self) -> Instant {
952        self.threshold_clock.get_quorum_ts()
953    }
954
955    pub(crate) fn highest_accepted_round(&self) -> Round {
956        self.highest_accepted_round
957    }
958
959    /// Returns the `RoundInfo` at `round`, if available. `None` if the round has
960    /// been evicted (<= gc_round) or has not been reached yet.
961    pub(crate) fn get_round_info(&self, round: Round) -> Option<&RoundInfo> {
962        let front_round = self.round_info.front()?.round;
963        if round < front_round || round <= self.gc_round() {
964            return None;
965        }
966        let round_info = self.round_info.get((round - front_round) as usize)?;
967        assert_eq!(
968            round_info.round, round,
969            "RoundInfo round {} does not match requested round {}. RoundInfo should be contiguous.",
970            round_info.round, round
971        );
972        Some(round_info)
973    }
974
975    /// Updates the `RoundInfo` for the round of the given block,
976    /// and creates a new `RoundInfo` if the block is in the next round.
977    fn update_round_info(&mut self, block: &VerifiedBlock) {
978        let block_ref = block.reference();
979
980        // RoundInfo is only kept for rounds above GC round.
981        let gc_round = self.gc_round();
982        if block.round() <= gc_round {
983            return;
984        }
985
986        // DAG can only grow one round at a time. Next round is at most 1 round after
987        // the current latest round.
988        let next_round = self
989            .round_info
990            .back()
991            .map(|info| info.round + 1)
992            // round_info starts empty on fresh startup or recovery.
993            .unwrap_or(gc_round + 1);
994        // DAG can only grow one round at a time.
995        assert!(
996            block.round() <= next_round,
997            "Attempted to update round info for block {block_ref} with round higher than next round {next_round}"
998        );
999        if block.round() == next_round {
1000            self.round_info.push_back(RoundInfo::new(block.round()));
1001        }
1002
1003        // Update the RoundInfo of the block round.
1004        let front_round = self
1005            .round_info
1006            .front()
1007            .expect("round_info non-empty after extend")
1008            .round;
1009        let index = (block.round() - front_round) as usize;
1010        let info = &mut self.round_info[index];
1011        info.blocks.insert(block_ref);
1012        info.total_stake
1013            .add_unique(block_ref.author, &self.context.committee);
1014    }
1015
1016    // Buffers a new commit in memory and updates last committed rounds.
1017    // REQUIRED: must not skip over any commit index.
1018    pub(crate) fn add_commit(&mut self, commit: TrustedCommit) {
1019        let time_diff = if let Some(last_commit) = &self.last_commit {
1020            if commit.index() <= last_commit.index() {
1021                error!(
1022                    "New commit index {} <= last commit index {}!",
1023                    commit.index(),
1024                    last_commit.index()
1025                );
1026                return;
1027            }
1028            assert_eq!(commit.index(), last_commit.index() + 1);
1029
1030            if commit.timestamp_ms() < last_commit.timestamp_ms() {
1031                panic!(
1032                    "Commit timestamps do not monotonically increment, prev commit {:?}, new commit {:?}",
1033                    last_commit, commit
1034                );
1035            }
1036            commit
1037                .timestamp_ms()
1038                .saturating_sub(last_commit.timestamp_ms())
1039        } else {
1040            assert_eq!(commit.index(), 1);
1041            0
1042        };
1043
1044        self.context
1045            .metrics
1046            .node_metrics
1047            .last_commit_time_diff
1048            .observe(time_diff as f64);
1049
1050        let commit_round_advanced = if let Some(previous_commit) = &self.last_commit {
1051            previous_commit.round() < commit.round()
1052        } else {
1053            true
1054        };
1055
1056        self.last_commit = Some(commit.clone());
1057
1058        if commit_round_advanced {
1059            let now = std::time::Instant::now();
1060            if let Some(previous_time) = self.last_commit_round_advancement_time {
1061                self.context
1062                    .metrics
1063                    .node_metrics
1064                    .commit_round_advancement_interval
1065                    .observe(now.duration_since(previous_time).as_secs_f64())
1066            }
1067            self.last_commit_round_advancement_time = Some(now);
1068        }
1069
1070        for block_ref in commit.blocks().iter() {
1071            self.last_committed_rounds[block_ref.author] = max(
1072                self.last_committed_rounds[block_ref.author],
1073                block_ref.round,
1074            );
1075        }
1076
1077        for (i, round) in self.last_committed_rounds.iter().enumerate() {
1078            let index = self.context.committee.to_authority_index(i).unwrap();
1079            let hostname = &self.context.committee.authority(index).hostname;
1080            self.context
1081                .metrics
1082                .node_metrics
1083                .last_committed_authority_round
1084                .with_label_values(&[hostname])
1085                .set((*round).into());
1086        }
1087
1088        self.pending_commit_votes.push_back(commit.reference());
1089        self.commits_to_write.push(commit);
1090    }
1091
1092    /// Recovers commits to write from storage, at startup.
1093    pub(crate) fn recover_commits_to_write(&mut self, commits: Vec<TrustedCommit>) {
1094        self.commits_to_write.extend(commits);
1095    }
1096
1097    pub(crate) fn ensure_commits_to_write_is_empty(&self) {
1098        assert!(
1099            self.commits_to_write.is_empty(),
1100            "Commits to write should be empty. {:?}",
1101            self.commits_to_write,
1102        );
1103    }
1104
1105    pub(crate) fn add_commit_info(&mut self, reputation_scores: ReputationScores) {
1106        // We create an empty scoring subdag once reputation scores are calculated.
1107        // Note: It is okay for this to not be gated by protocol config as the
1108        // scoring_subdag should be empty in either case at this point.
1109        assert!(self.scoring_subdag.is_empty());
1110
1111        let commit_info = CommitInfo {
1112            committed_rounds: self.last_committed_rounds.clone(),
1113            reputation_scores,
1114        };
1115        let last_commit = self
1116            .last_commit
1117            .as_ref()
1118            .expect("Last commit should already be set.");
1119        self.commit_info_to_write
1120            .push((last_commit.reference(), commit_info));
1121    }
1122
1123    pub(crate) fn add_finalized_commit(
1124        &mut self,
1125        commit_ref: CommitRef,
1126        rejected_transactions: BTreeMap<BlockRef, Vec<TransactionIndex>>,
1127    ) {
1128        self.finalized_commits_to_write
1129            .push((commit_ref, rejected_transactions));
1130    }
1131
1132    pub(crate) fn take_commit_votes(&mut self, limit: usize) -> Vec<CommitVote> {
1133        let mut votes = Vec::new();
1134        while !self.pending_commit_votes.is_empty() && votes.len() < limit {
1135            votes.push(self.pending_commit_votes.pop_front().unwrap());
1136        }
1137        votes
1138    }
1139
1140    /// Index of the last commit.
1141    pub(crate) fn last_commit_index(&self) -> CommitIndex {
1142        match &self.last_commit {
1143            Some(commit) => commit.index(),
1144            None => 0,
1145        }
1146    }
1147
1148    /// Digest of the last commit.
1149    pub(crate) fn last_commit_digest(&self) -> CommitDigest {
1150        match &self.last_commit {
1151            Some(commit) => commit.digest(),
1152            None => CommitDigest::MIN,
1153        }
1154    }
1155
1156    /// Timestamp of the last commit.
1157    pub(crate) fn last_commit_timestamp_ms(&self) -> BlockTimestampMs {
1158        match &self.last_commit {
1159            Some(commit) => commit.timestamp_ms(),
1160            None => 0,
1161        }
1162    }
1163
1164    /// Leader slot of the last commit.
1165    pub(crate) fn last_commit_leader(&self) -> Slot {
1166        match &self.last_commit {
1167            Some(commit) => commit.leader().into(),
1168            None => self
1169                .genesis
1170                .iter()
1171                .next()
1172                .map(|(genesis_ref, _)| *genesis_ref)
1173                .expect("Genesis blocks should always be available.")
1174                .into(),
1175        }
1176    }
1177
1178    /// Highest round where a block is committed, which is last commit's leader round.
1179    pub(crate) fn last_commit_round(&self) -> Round {
1180        match &self.last_commit {
1181            Some(commit) => commit.leader().round,
1182            None => 0,
1183        }
1184    }
1185
1186    /// Last committed round per authority.
1187    pub(crate) fn last_committed_rounds(&self) -> Vec<Round> {
1188        self.last_committed_rounds.clone()
1189    }
1190
1191    /// The GC round is the highest round that blocks of equal or lower round are considered obsolete and no longer possible to be committed.
1192    /// There is no meaning accepting any blocks with round <= gc_round. The Garbage Collection (GC) round is calculated based on the latest
1193    /// committed leader round. When GC is disabled that will return the genesis round.
1194    pub(crate) fn gc_round(&self) -> Round {
1195        self.calculate_gc_round(self.last_commit_round())
1196    }
1197
1198    /// Calculates the GC round from the input leader round, which can be different
1199    /// from the last committed leader round.
1200    pub(crate) fn calculate_gc_round(&self, commit_round: Round) -> Round {
1201        commit_round.saturating_sub(self.context.protocol_config.gc_depth())
1202    }
1203
1204    /// Flushes unpersisted blocks, commits and commit info to storage.
1205    ///
1206    /// REQUIRED: when buffering a block, all of its ancestors and the latest commit which sets the GC round
1207    /// must also be buffered.
1208    /// REQUIRED: when buffering a commit, all of its included blocks and the previous commits must also be buffered.
1209    /// REQUIRED: when flushing, all of the buffered blocks and commits must be flushed together to ensure consistency.
1210    ///
1211    /// After each flush, DagState becomes persisted in storage and it expected to recover
1212    /// all internal states from storage after restarts.
1213    pub(crate) fn flush(&mut self) {
1214        let _s = self
1215            .context
1216            .metrics
1217            .node_metrics
1218            .scope_processing_time
1219            .with_label_values(&["DagState::flush"])
1220            .start_timer();
1221
1222        // Flush buffered data to storage.
1223        let pending_blocks = std::mem::take(&mut self.blocks_to_write);
1224        let pending_commits = std::mem::take(&mut self.commits_to_write);
1225        let pending_commit_info = std::mem::take(&mut self.commit_info_to_write);
1226        let pending_finalized_commits = std::mem::take(&mut self.finalized_commits_to_write);
1227        if pending_blocks.is_empty()
1228            && pending_commits.is_empty()
1229            && pending_commit_info.is_empty()
1230            && pending_finalized_commits.is_empty()
1231        {
1232            return;
1233        }
1234
1235        debug!(
1236            "Flushing {} blocks ({}), {} commits ({}), {} commit infos ({}), {} finalized commits ({}) to storage.",
1237            pending_blocks.len(),
1238            pending_blocks
1239                .iter()
1240                .map(|b| b.reference().to_string())
1241                .join(","),
1242            pending_commits.len(),
1243            pending_commits
1244                .iter()
1245                .map(|c| c.reference().to_string())
1246                .join(","),
1247            pending_commit_info.len(),
1248            pending_commit_info
1249                .iter()
1250                .map(|(commit_ref, _)| commit_ref.to_string())
1251                .join(","),
1252            pending_finalized_commits.len(),
1253            pending_finalized_commits
1254                .iter()
1255                .map(|(commit_ref, _)| commit_ref.to_string())
1256                .join(","),
1257        );
1258        self.store
1259            .write(WriteBatch::new(
1260                pending_blocks,
1261                pending_commits,
1262                pending_commit_info,
1263                pending_finalized_commits,
1264            ))
1265            .unwrap_or_else(|e| panic!("Failed to write to storage: {:?}", e));
1266        self.context
1267            .metrics
1268            .node_metrics
1269            .dag_state_store_write_count
1270            .inc();
1271
1272        // Clean up old cached data. After flushing, all cached blocks are guaranteed to be persisted.
1273        for (authority_index, _) in self.context.committee.authorities() {
1274            let eviction_round = self.calculate_authority_eviction_round(authority_index);
1275            while let Some(block_ref) = self.recent_refs_by_authority[authority_index].first() {
1276                if block_ref.round <= eviction_round {
1277                    self.recent_blocks.remove(block_ref);
1278                    self.recent_refs_by_authority[authority_index].pop_first();
1279                } else {
1280                    break;
1281                }
1282            }
1283            self.evicted_rounds[authority_index] = eviction_round;
1284        }
1285
1286        // Clean up old RoundInfo below gc_round.
1287        while let Some(info) = self.round_info.front() {
1288            if info.round <= self.gc_round() {
1289                self.round_info.pop_front();
1290            } else {
1291                break;
1292            }
1293        }
1294
1295        let metrics = &self.context.metrics.node_metrics;
1296        metrics
1297            .dag_state_recent_blocks
1298            .set(self.recent_blocks.len() as i64);
1299        metrics.dag_state_recent_refs.set(
1300            self.recent_refs_by_authority
1301                .iter()
1302                .map(BTreeSet::len)
1303                .sum::<usize>() as i64,
1304        );
1305    }
1306
1307    pub(crate) fn recover_last_commit_info(&self) -> Option<(CommitRef, CommitInfo)> {
1308        self.store
1309            .read_last_commit_info()
1310            .unwrap_or_else(|e| panic!("Failed to read from storage: {:?}", e))
1311    }
1312
1313    pub(crate) fn add_scoring_subdags(&mut self, scoring_subdags: Vec<CommittedSubDag>) {
1314        self.scoring_subdag.add_subdags(scoring_subdags);
1315    }
1316
1317    pub(crate) fn clear_scoring_subdag(&mut self) {
1318        self.scoring_subdag.clear();
1319    }
1320
1321    pub(crate) fn scoring_subdags_count(&self) -> usize {
1322        self.scoring_subdag.scored_subdags_count()
1323    }
1324
1325    pub(crate) fn calculate_scoring_subdag_scores(&self) -> ReputationScores {
1326        self.scoring_subdag.calculate_distributed_vote_scores()
1327    }
1328
1329    pub(crate) fn scoring_subdag_commit_range(&self) -> CommitIndex {
1330        self.scoring_subdag
1331            .commit_range
1332            .as_ref()
1333            .expect("commit range should exist for scoring subdag")
1334            .end()
1335    }
1336
1337    /// The last round that should get evicted after a cache clean up operation. After this round we are
1338    /// guaranteed to have all the produced blocks from that authority. For any round that is
1339    /// <= `last_evicted_round` we don't have such guarantees as out of order blocks might exist.
1340    fn calculate_authority_eviction_round(&self, authority_index: AuthorityIndex) -> Round {
1341        let last_round = self.recent_refs_by_authority[authority_index]
1342            .last()
1343            .map(|block_ref| block_ref.round)
1344            .unwrap_or(GENESIS_ROUND);
1345
1346        Self::eviction_round(last_round, self.gc_round(), self.cached_rounds)
1347    }
1348
1349    /// Calculates the eviction round for the given authority. The goal is to keep at least `cached_rounds`
1350    /// of the latest blocks in the cache (if enough data is available), while evicting blocks with rounds <= `gc_round` when possible.
1351    fn eviction_round(last_round: Round, gc_round: Round, cached_rounds: u32) -> Round {
1352        gc_round.min(last_round.saturating_sub(cached_rounds))
1353    }
1354
1355    /// Returns the underlying store.
1356    pub(crate) fn store(&self) -> Arc<dyn Store> {
1357        self.store.clone()
1358    }
1359
1360    /// Detects and returns the blocks of the round that forms the last quorum. The method will return
1361    /// the quorum even if that's genesis.
1362    #[cfg(test)]
1363    pub(crate) fn last_quorum(&self) -> Vec<VerifiedBlock> {
1364        // the quorum should exist either on the highest accepted round or the one before. If we fail to detect
1365        // a quorum then it means that our DAG has advanced with missing causal history.
1366        for round in
1367            (self.highest_accepted_round.saturating_sub(1)..=self.highest_accepted_round).rev()
1368        {
1369            if round == GENESIS_ROUND {
1370                return self.genesis_blocks();
1371            }
1372            use crate::stake_aggregator::{QuorumThreshold, StakeAggregator};
1373            let mut quorum = StakeAggregator::<QuorumThreshold>::new();
1374
1375            // Since the minimum wave length is 3 we expect to find a quorum in the uncommitted rounds.
1376            let blocks = self.get_uncommitted_blocks_at_round(round);
1377            for block in &blocks {
1378                if quorum.add(block.author(), &self.context.committee) {
1379                    return blocks;
1380                }
1381            }
1382        }
1383
1384        panic!("Fatal error, no quorum has been detected in our DAG on the last two rounds.");
1385    }
1386
1387    #[cfg(test)]
1388    pub(crate) fn genesis_blocks(&self) -> Vec<VerifiedBlock> {
1389        self.genesis.values().cloned().collect()
1390    }
1391
1392    #[cfg(test)]
1393    pub(crate) fn set_last_commit(&mut self, commit: TrustedCommit) {
1394        self.last_commit = Some(commit);
1395    }
1396}
1397
1398/// Information on a block accepted into the DAG.
1399#[derive(Clone)]
1400pub(crate) struct BlockInfo {
1401    pub(crate) block: VerifiedBlock,
1402
1403    /// Used in computing commits and leader schedule in Mysticeti v3.
1404    /// Next-round blocks which have this block as an ancestor.
1405    pub(crate) children: BTreeSet<BlockRef>,
1406    /// Total stake from distinct authorities that have authored
1407    /// one of the blocks in `children`.
1408    pub(crate) children_stake: StakeAggregator<QuorumThreshold>,
1409
1410    // Whether the block has been committed
1411    pub(crate) committed: bool,
1412    // Whether the block has been included in the causal history of an owned proposed block.
1413    ///
1414    /// There are two usages of this field:
1415    /// 1. When proposing blocks, determine the set of blocks to carry votes for.
1416    /// 2. When recovering, determine if a block has not been included in a proposed block and
1417    ///    should recover transaction votes by voting.
1418    included: bool,
1419}
1420
1421impl BlockInfo {
1422    fn new(block: VerifiedBlock) -> Self {
1423        Self {
1424            block,
1425            children: BTreeSet::new(),
1426            children_stake: StakeAggregator::new(),
1427            committed: false,
1428            included: false,
1429        }
1430    }
1431}
1432
1433/// Aggregates information about blocks accepted at a single round.
1434/// Used for commit generation and leader schedule calculation in Mysticeti v3.
1435/// RoundInfo is only kept for rounds above GC round.
1436pub(crate) struct RoundInfo {
1437    pub(crate) round: Round,
1438    /// Blocks accepted at `round`.
1439    pub(crate) blocks: BTreeSet<BlockRef>,
1440    /// Total stake from distinct authorities that have authored
1441    /// an accepted block in `round`.
1442    pub(crate) total_stake: StakeAggregator<QuorumThreshold>,
1443}
1444
1445impl RoundInfo {
1446    fn new(round: Round) -> Self {
1447        Self {
1448            round,
1449            blocks: BTreeSet::new(),
1450            total_stake: StakeAggregator::new(),
1451        }
1452    }
1453}
1454
1455#[cfg(test)]
1456mod test {
1457    use std::vec;
1458
1459    use consensus_config::Stake;
1460    use consensus_types::block::{BlockDigest, BlockRef, BlockTimestampMs};
1461    use parking_lot::RwLock;
1462
1463    use super::*;
1464    use crate::{
1465        block::{TestBlock, VerifiedBlock},
1466        storage::{WriteBatch, mem_store::MemStore},
1467        test_dag_builder::DagBuilder,
1468        test_dag_parser::parse_dag,
1469    };
1470
1471    #[tokio::test]
1472    async fn test_get_blocks() {
1473        let (context, _) = Context::new_for_test(4);
1474        let context = Arc::new(context);
1475        let store = Arc::new(MemStore::new());
1476        let mut dag_state = DagState::new(context.clone(), store.clone());
1477        let own_index = AuthorityIndex::new_for_test(0);
1478
1479        // Populate test blocks for round 1 ~ 10, authorities 0 ~ 2.
1480        let num_rounds: u32 = 10;
1481        let non_existent_round: u32 = 100;
1482        let num_authorities: u32 = 3;
1483        let num_blocks_per_slot: usize = 3;
1484        let mut blocks = BTreeMap::new();
1485        for round in 1..=num_rounds {
1486            for author in 0..num_authorities {
1487                // Create 3 blocks per slot, with different timestamps and digests.
1488                let base_ts = round as BlockTimestampMs * 1000;
1489                for timestamp in base_ts..base_ts + num_blocks_per_slot as u64 {
1490                    let block = VerifiedBlock::new_for_test(
1491                        TestBlock::new(round, author)
1492                            .set_timestamp_ms(timestamp)
1493                            .build(),
1494                    );
1495                    dag_state.accept_block(block.clone());
1496                    blocks.insert(block.reference(), block);
1497
1498                    // Only write one block per slot for own index
1499                    if AuthorityIndex::new_for_test(author) == own_index {
1500                        break;
1501                    }
1502                }
1503            }
1504        }
1505
1506        // Check uncommitted blocks that exist.
1507        for (r, block) in &blocks {
1508            assert_eq!(&dag_state.get_block(r).unwrap(), block);
1509        }
1510
1511        // Check uncommitted blocks that do not exist.
1512        let last_ref = blocks.keys().last().unwrap();
1513        assert!(
1514            dag_state
1515                .get_block(&BlockRef::new(
1516                    last_ref.round,
1517                    last_ref.author,
1518                    BlockDigest::MIN
1519                ))
1520                .is_none()
1521        );
1522
1523        // Check slots with uncommitted blocks.
1524        for round in 1..=num_rounds {
1525            for author in 0..num_authorities {
1526                let slot = Slot::new(
1527                    round,
1528                    context
1529                        .committee
1530                        .to_authority_index(author as usize)
1531                        .unwrap(),
1532                );
1533                let blocks = dag_state.get_uncommitted_blocks_at_slot(slot);
1534
1535                // We only write one block per slot for own index
1536                if AuthorityIndex::new_for_test(author) == own_index {
1537                    assert_eq!(blocks.len(), 1);
1538                } else {
1539                    assert_eq!(blocks.len(), num_blocks_per_slot);
1540                }
1541
1542                for b in blocks {
1543                    assert_eq!(b.round(), round);
1544                    assert_eq!(
1545                        b.author(),
1546                        context
1547                            .committee
1548                            .to_authority_index(author as usize)
1549                            .unwrap()
1550                    );
1551                }
1552            }
1553        }
1554
1555        // Check slots without uncommitted blocks.
1556        let slot = Slot::new(non_existent_round, AuthorityIndex::ZERO);
1557        assert!(dag_state.get_uncommitted_blocks_at_slot(slot).is_empty());
1558
1559        // Check rounds with uncommitted blocks.
1560        for round in 1..=num_rounds {
1561            let blocks = dag_state.get_uncommitted_blocks_at_round(round);
1562            // Expect 3 blocks per authority except for own authority which should
1563            // have 1 block.
1564            assert_eq!(
1565                blocks.len(),
1566                (num_authorities - 1) as usize * num_blocks_per_slot + 1
1567            );
1568            for b in blocks {
1569                assert_eq!(b.round(), round);
1570            }
1571        }
1572
1573        // Check rounds without uncommitted blocks.
1574        assert!(
1575            dag_state
1576                .get_uncommitted_blocks_at_round(non_existent_round)
1577                .is_empty()
1578        );
1579    }
1580
1581    #[tokio::test]
1582    async fn test_ancestors_at_uncommitted_round() {
1583        // Initialize DagState.
1584        let (context, _) = Context::new_for_test(4);
1585        let context = Arc::new(context);
1586        let store = Arc::new(MemStore::new());
1587        let mut dag_state = DagState::new(context.clone(), store.clone());
1588
1589        // Populate DagState.
1590
1591        // Round 10 refs will not have their blocks in DagState.
1592        let round_10_refs: Vec<_> = (0..4)
1593            .map(|a| {
1594                VerifiedBlock::new_for_test(TestBlock::new(10, a).set_timestamp_ms(1000).build())
1595                    .reference()
1596            })
1597            .collect();
1598
1599        // Round 11 blocks.
1600        let round_11 = [
1601            // This will connect to round 12.
1602            VerifiedBlock::new_for_test(
1603                TestBlock::new(11, 0)
1604                    .set_timestamp_ms(1100)
1605                    .set_ancestors(round_10_refs.clone())
1606                    .build(),
1607            ),
1608            // Slot(11, 1) has 3 blocks.
1609            // This will connect to round 12.
1610            VerifiedBlock::new_for_test(
1611                TestBlock::new(11, 1)
1612                    .set_timestamp_ms(1110)
1613                    .set_ancestors(round_10_refs.clone())
1614                    .build(),
1615            ),
1616            // This will connect to round 13.
1617            VerifiedBlock::new_for_test(
1618                TestBlock::new(11, 1)
1619                    .set_timestamp_ms(1111)
1620                    .set_ancestors(round_10_refs.clone())
1621                    .build(),
1622            ),
1623            // This will not connect to any block.
1624            VerifiedBlock::new_for_test(
1625                TestBlock::new(11, 1)
1626                    .set_timestamp_ms(1112)
1627                    .set_ancestors(round_10_refs.clone())
1628                    .build(),
1629            ),
1630            // This will not connect to any block.
1631            VerifiedBlock::new_for_test(
1632                TestBlock::new(11, 2)
1633                    .set_timestamp_ms(1120)
1634                    .set_ancestors(round_10_refs.clone())
1635                    .build(),
1636            ),
1637            // This will connect to round 12.
1638            VerifiedBlock::new_for_test(
1639                TestBlock::new(11, 3)
1640                    .set_timestamp_ms(1130)
1641                    .set_ancestors(round_10_refs.clone())
1642                    .build(),
1643            ),
1644        ];
1645
1646        // Round 12 blocks.
1647        let ancestors_for_round_12 = vec![
1648            round_11[0].reference(),
1649            round_11[1].reference(),
1650            round_11[5].reference(),
1651        ];
1652        let round_12 = [
1653            VerifiedBlock::new_for_test(
1654                TestBlock::new(12, 0)
1655                    .set_timestamp_ms(1200)
1656                    .set_ancestors(ancestors_for_round_12.clone())
1657                    .build(),
1658            ),
1659            VerifiedBlock::new_for_test(
1660                TestBlock::new(12, 2)
1661                    .set_timestamp_ms(1220)
1662                    .set_ancestors(ancestors_for_round_12.clone())
1663                    .build(),
1664            ),
1665            VerifiedBlock::new_for_test(
1666                TestBlock::new(12, 3)
1667                    .set_timestamp_ms(1230)
1668                    .set_ancestors(ancestors_for_round_12.clone())
1669                    .build(),
1670            ),
1671        ];
1672
1673        // Round 13 blocks.
1674        let ancestors_for_round_13 = vec![
1675            round_12[0].reference(),
1676            round_12[1].reference(),
1677            round_12[2].reference(),
1678            round_11[2].reference(),
1679        ];
1680        let round_13 = [
1681            VerifiedBlock::new_for_test(
1682                TestBlock::new(12, 1)
1683                    .set_timestamp_ms(1300)
1684                    .set_ancestors(ancestors_for_round_13.clone())
1685                    .build(),
1686            ),
1687            VerifiedBlock::new_for_test(
1688                TestBlock::new(12, 2)
1689                    .set_timestamp_ms(1320)
1690                    .set_ancestors(ancestors_for_round_13.clone())
1691                    .build(),
1692            ),
1693            VerifiedBlock::new_for_test(
1694                TestBlock::new(12, 3)
1695                    .set_timestamp_ms(1330)
1696                    .set_ancestors(ancestors_for_round_13.clone())
1697                    .build(),
1698            ),
1699        ];
1700
1701        // Round 14 anchor block.
1702        let ancestors_for_round_14 = round_13.iter().map(|b| b.reference()).collect();
1703        let anchor = VerifiedBlock::new_for_test(
1704            TestBlock::new(14, 1)
1705                .set_timestamp_ms(1410)
1706                .set_ancestors(ancestors_for_round_14)
1707                .build(),
1708        );
1709
1710        // Add all blocks (at and above round 11) to DagState.
1711        for b in round_11
1712            .iter()
1713            .chain(round_12.iter())
1714            .chain(round_13.iter())
1715            .chain([anchor.clone()].iter())
1716        {
1717            dag_state.accept_block(b.clone());
1718        }
1719
1720        // Check ancestors connected to anchor.
1721        let ancestors = dag_state.ancestors_at_round(&anchor, 11);
1722        let mut ancestors_refs: Vec<BlockRef> = ancestors.iter().map(|b| b.reference()).collect();
1723        ancestors_refs.sort();
1724        let mut expected_refs = vec![
1725            round_11[0].reference(),
1726            round_11[1].reference(),
1727            round_11[2].reference(),
1728            round_11[5].reference(),
1729        ];
1730        expected_refs.sort(); // we need to sort as blocks with same author and round of round 11 (position 1 & 2) might not be in right lexicographical order.
1731        assert_eq!(
1732            ancestors_refs, expected_refs,
1733            "Expected round 11 ancestors: {:?}. Got: {:?}",
1734            expected_refs, ancestors_refs
1735        );
1736    }
1737
1738    #[tokio::test]
1739    async fn test_link_causal_history() {
1740        let (mut context, _) = Context::new_for_test(4);
1741        context.parameters.dag_state_cached_rounds = 10;
1742        context.protocol_config.set_gc_depth_for_testing(3);
1743        let context = Arc::new(context);
1744
1745        let store = Arc::new(MemStore::new());
1746        let mut dag_state = DagState::new(context.clone(), store.clone());
1747
1748        // Create for rounds 1..=6. Skip creating blocks for authority 0 for rounds 4 - 6.
1749        let mut dag_builder = DagBuilder::new(context.clone());
1750        dag_builder.layers(1..=3).build();
1751        dag_builder
1752            .layers(4..=6)
1753            .authorities(vec![AuthorityIndex::new_for_test(0)])
1754            .skip_block()
1755            .build();
1756
1757        // Accept all blocks
1758        let all_blocks = dag_builder.all_blocks();
1759        dag_state.accept_blocks(all_blocks.clone());
1760
1761        // No block is linked yet.
1762        for block in &all_blocks {
1763            assert!(!dag_state.has_been_included(&block.reference()));
1764        }
1765
1766        // Link causal history from a round 1 block.
1767        let round_1_block = &all_blocks[1];
1768        assert_eq!(round_1_block.round(), 1);
1769        let linked_blocks = dag_state.link_causal_history(round_1_block.reference());
1770
1771        // Check that the block is linked.
1772        assert_eq!(linked_blocks.len(), 1);
1773        assert_eq!(linked_blocks[0], round_1_block.reference());
1774        for block_ref in linked_blocks {
1775            assert!(dag_state.has_been_included(&block_ref));
1776        }
1777
1778        // Link causal history from a round 2 block.
1779        let round_2_block = &all_blocks[4];
1780        assert_eq!(round_2_block.round(), 2);
1781        let linked_blocks = dag_state.link_causal_history(round_2_block.reference());
1782
1783        // Check the linked blocks.
1784        assert_eq!(linked_blocks.len(), 4);
1785        for block_ref in linked_blocks {
1786            assert!(block_ref == round_2_block.reference() || block_ref.round == 1);
1787        }
1788
1789        // Check linked status in dag state.
1790        for block in &all_blocks {
1791            if block.round() == 1 || block.reference() == round_2_block.reference() {
1792                assert!(dag_state.has_been_included(&block.reference()));
1793            } else {
1794                assert!(!dag_state.has_been_included(&block.reference()));
1795            }
1796        }
1797
1798        // Select round 6 block.
1799        let round_6_block = all_blocks.last().unwrap();
1800        assert_eq!(round_6_block.round(), 6);
1801
1802        // Get GC round to 3.
1803        let last_commit = TrustedCommit::new_for_test(
1804            6,
1805            CommitDigest::MIN,
1806            context.clock.timestamp_utc_ms(),
1807            round_6_block.reference(),
1808            vec![],
1809        );
1810        dag_state.set_last_commit(last_commit);
1811        assert_eq!(
1812            dag_state.gc_round(),
1813            3,
1814            "GC round should have moved to round 3"
1815        );
1816
1817        // Link causal history from a round 6 block.
1818        let linked_blocks = dag_state.link_causal_history(round_6_block.reference());
1819
1820        // Check the linked blocks. They should not include GC'ed blocks.
1821        assert_eq!(linked_blocks.len(), 7, "Linked blocks: {:?}", linked_blocks);
1822        for block_ref in linked_blocks {
1823            assert!(
1824                block_ref.round == 4
1825                    || block_ref.round == 5
1826                    || block_ref == round_6_block.reference()
1827            );
1828        }
1829
1830        // Check linked status in dag state.
1831        for block in &all_blocks {
1832            let block_ref = block.reference();
1833            if block.round() == 1
1834                || block_ref == round_2_block.reference()
1835                || block_ref.round == 4
1836                || block_ref.round == 5
1837                || block_ref == round_6_block.reference()
1838            {
1839                assert!(dag_state.has_been_included(&block.reference()));
1840            } else {
1841                assert!(!dag_state.has_been_included(&block.reference()));
1842            }
1843        }
1844    }
1845
1846    #[tokio::test]
1847    async fn test_block_children_basics() {
1848        let (mut context, _) = Context::new_for_test(4);
1849        // Small cached_rounds so a later flush actually evicts below-gc
1850        // entries from recent_blocks.
1851        context.parameters.dag_state_cached_rounds = 2;
1852        context.protocol_config.set_gc_depth_for_testing(3);
1853        context.protocol_config.set_enable_v3_for_testing(true);
1854        let context = Arc::new(context);
1855
1856        let store = Arc::new(MemStore::new());
1857        let mut dag_state = DagState::new(context.clone(), store.clone());
1858
1859        // Dense 4-authority DAG for rounds 1..=5 with default full links.
1860        let mut dag_builder = DagBuilder::new(context.clone());
1861        dag_builder.layers(1..=5).build();
1862
1863        let all_blocks = dag_builder.all_blocks();
1864        dag_state.accept_blocks(all_blocks.clone());
1865
1866        // Expected children: for each block, the round+1 blocks that reference it via a
1867        // parent (strong) link.
1868        let mut expected_children: BTreeMap<BlockRef, BTreeSet<BlockRef>> = BTreeMap::new();
1869        for block in &all_blocks {
1870            expected_children
1871                .entry(block.reference())
1872                .or_default()
1873                .extend(
1874                    all_blocks
1875                        .iter()
1876                        .filter(|b| b.round() == block.round() + 1)
1877                        .map(|b| b.reference()),
1878                );
1879        }
1880
1881        // Verify get_block_children() returns the expected set for every block.
1882        for block in &all_blocks {
1883            let block_ref = block.reference();
1884            let actual: BTreeSet<BlockRef> = dag_state
1885                .get_block_children(&block_ref)
1886                .expect("accepted block should be in recent_blocks")
1887                .into_iter()
1888                .collect();
1889            let want = expected_children.get(&block_ref).cloned().unwrap();
1890            assert_eq!(actual, want, "mismatched children for {block_ref:?}");
1891
1892            // Derived fields: distinct authors of the child set, and summed stake.
1893            let expected_authorities: BTreeSet<AuthorityIndex> =
1894                want.iter().map(|r| r.author).collect();
1895            let expected_stake: Stake = expected_authorities
1896                .iter()
1897                .map(|a| context.committee.stake(*a))
1898                .sum();
1899            assert_eq!(
1900                dag_state
1901                    .get_block_children_authorities(&block_ref)
1902                    .expect("accepted block should be in recent_blocks"),
1903                expected_authorities,
1904                "mismatched children_authorities for {block_ref:?}"
1905            );
1906            assert_eq!(
1907                dag_state
1908                    .get_block_total_children_stake(&block_ref)
1909                    .expect("accepted block should be in recent_blocks"),
1910                expected_stake,
1911                "mismatched total_children_stake for {block_ref:?}"
1912            );
1913        }
1914
1915        // Idempotence: re-accepting a block should be a no-op (via the
1916        // contains_block early-return) and must not change any parent's
1917        // children set.
1918        let round_2_block = all_blocks
1919            .iter()
1920            .find(|b| b.round() == 2)
1921            .expect("should have a round-2 block")
1922            .clone();
1923        let before: Vec<(BlockRef, BTreeSet<BlockRef>)> = round_2_block
1924            .ancestors()
1925            .iter()
1926            .map(|a| {
1927                (
1928                    *a,
1929                    dag_state
1930                        .get_block_children(a)
1931                        .unwrap()
1932                        .into_iter()
1933                        .collect(),
1934                )
1935            })
1936            .collect();
1937        dag_state.accept_block(round_2_block);
1938        for (ancestor, before_set) in before {
1939            let after: BTreeSet<BlockRef> = dag_state
1940                .get_block_children(&ancestor)
1941                .unwrap()
1942                .into_iter()
1943                .collect();
1944            assert_eq!(
1945                before_set, after,
1946                "children changed for {ancestor:?} after re-accept"
1947            );
1948        }
1949
1950        // GC boundary: commit a round-5 leader so gc_round advances to 2, then
1951        // flush to evict blocks with round <= eviction_round (= min(gc_round,
1952        // last_round - cached_rounds) = min(2, 5-2) = 2).
1953        let round_5_leader = all_blocks
1954            .last()
1955            .expect("last block should be round 5")
1956            .reference();
1957        let last_commit = TrustedCommit::new_for_test(
1958            5,
1959            CommitDigest::MIN,
1960            context.clock.timestamp_utc_ms(),
1961            round_5_leader,
1962            vec![],
1963        );
1964        dag_state.set_last_commit(last_commit);
1965        assert_eq!(dag_state.gc_round(), 2);
1966
1967        // Child metadata should expose logical retention immediately when the
1968        // GC round advances, even before flush physically removes stale entries.
1969        for block in all_blocks.iter().filter(|block| block.round() <= 2) {
1970            let block_ref = block.reference();
1971            assert!(
1972                dag_state.get_block_children(&block_ref).is_none(),
1973                "below-gc block {block_ref:?} should hide children before flush"
1974            );
1975            assert!(
1976                dag_state
1977                    .get_block_children_authorities(&block_ref)
1978                    .is_none(),
1979                "below-gc block {block_ref:?} should hide child authorities before flush"
1980            );
1981            assert!(
1982                dag_state
1983                    .get_block_total_children_stake(&block_ref)
1984                    .is_none(),
1985                "below-gc block {block_ref:?} should hide child stake before flush"
1986            );
1987        }
1988
1989        dag_state.flush();
1990
1991        // After flush: rounds 1..=2 evicted from recent_blocks → None.
1992        // Rounds 3..=4 retain their children sets; round 5 is above GC but has
1993        // no round-6 children.
1994        for block in &all_blocks {
1995            let block_ref = block.reference();
1996            match block.round() {
1997                1..=2 => assert!(
1998                    dag_state.get_block_children(&block_ref).is_none(),
1999                    "round {} block {block_ref:?} should be evicted after flush",
2000                    block.round()
2001                ),
2002                3..=4 => {
2003                    let actual: BTreeSet<BlockRef> = dag_state
2004                        .get_block_children(&block_ref)
2005                        .expect("above-gc block should remain")
2006                        .into_iter()
2007                        .collect();
2008                    let want = expected_children
2009                        .get(&block_ref)
2010                        .cloned()
2011                        .unwrap_or_default();
2012                    assert_eq!(
2013                        actual, want,
2014                        "children changed for {block_ref:?} after flush"
2015                    );
2016                    // Derived fields survive the flush since we only evict
2017                    // below-GC entries.
2018                    let expected_authorities: BTreeSet<AuthorityIndex> =
2019                        want.iter().map(|r| r.author).collect();
2020                    let expected_stake: Stake = expected_authorities
2021                        .iter()
2022                        .map(|a| context.committee.stake(*a))
2023                        .sum();
2024                    assert_eq!(
2025                        dag_state
2026                            .get_block_children_authorities(&block_ref)
2027                            .expect("above-gc block should remain"),
2028                        expected_authorities,
2029                        "children_authorities changed for {block_ref:?} after flush"
2030                    );
2031                    assert_eq!(
2032                        dag_state
2033                            .get_block_total_children_stake(&block_ref)
2034                            .expect("above-gc block should remain"),
2035                        expected_stake,
2036                        "total_children_stake changed for {block_ref:?} after flush"
2037                    );
2038                }
2039                5 => {
2040                    let actual = dag_state
2041                        .get_block_children(&block_ref)
2042                        .expect("round-5 block should remain");
2043                    assert!(
2044                        actual.is_empty(),
2045                        "round-5 block {block_ref:?} has unexpected children {actual:?}"
2046                    );
2047                    assert!(
2048                        dag_state
2049                            .get_block_children_authorities(&block_ref)
2050                            .expect("round-5 block should remain")
2051                            .is_empty(),
2052                        "round-5 block {block_ref:?} must have no children_authorities"
2053                    );
2054                    assert_eq!(
2055                        dag_state
2056                            .get_block_total_children_stake(&block_ref)
2057                            .expect("round-5 block should remain"),
2058                        0,
2059                        "round-5 block {block_ref:?} must have zero total_children_stake"
2060                    );
2061                }
2062                _ => unreachable!(),
2063            }
2064        }
2065    }
2066
2067    #[tokio::test]
2068    async fn test_get_block_info_at_slot() {
2069        let (mut context, _) = Context::new_for_test(4);
2070        // Children-stake tracking is v3-only; enable it so children_stake
2071        // is non-zero when round-2 blocks reference the slot.
2072        context.protocol_config.set_enable_v3_for_testing(true);
2073        let context = Arc::new(context);
2074
2075        let store = Arc::new(MemStore::new());
2076        let mut dag_state = DagState::new(context.clone(), store.clone());
2077
2078        // Use a non-own author for the slot under test so accept_block
2079        // accepts the equivocating second block (the own-authority check
2080        // rejects equivocations for the local validator only).
2081        let author_1 = AuthorityIndex::new_for_test(1);
2082        let author_2 = AuthorityIndex::new_for_test(2);
2083        let slot_1_1 = Slot::new(1, author_1);
2084
2085        // Empty: no blocks accepted yet.
2086        assert!(dag_state.get_block_info_at_slot(slot_1_1).is_empty());
2087
2088        // Single block at the slot: returns one entry; children_stake
2089        // is still zero because no round-2 block has been accepted.
2090        let block_1_1 = VerifiedBlock::new_for_test(TestBlock::new(1, 1).build());
2091        dag_state.accept_block(block_1_1.clone());
2092        let infos = dag_state.get_block_info_at_slot(slot_1_1);
2093        assert_eq!(infos.len(), 1);
2094        assert_eq!(infos[0].block.reference(), block_1_1.reference());
2095        assert_eq!(infos[0].children_stake.stake(), 0);
2096
2097        // Different slot still returns empty.
2098        assert!(
2099            dag_state
2100                .get_block_info_at_slot(Slot::new(1, author_2))
2101                .is_empty()
2102        );
2103
2104        // Equivocation: a second round-1 block authored by 1 with a different
2105        // timestamp yields a different digest, so both share the same slot.
2106        let block_1_1_equiv =
2107            VerifiedBlock::new_for_test(TestBlock::new(1, 1).set_timestamp_ms(1).build());
2108        assert_ne!(block_1_1_equiv.reference(), block_1_1.reference());
2109        dag_state.accept_block(block_1_1_equiv.clone());
2110
2111        let infos = dag_state.get_block_info_at_slot(slot_1_1);
2112        assert_eq!(infos.len(), 2);
2113        let returned_refs: BTreeSet<BlockRef> = infos.iter().map(|i| i.block.reference()).collect();
2114        assert_eq!(
2115            returned_refs,
2116            BTreeSet::from([block_1_1.reference(), block_1_1_equiv.reference()])
2117        );
2118
2119        // Children-stake propagation: a round-2 block from author 2 referencing
2120        // block_1_1 but not the equivocating block must update only the
2121        // former's children_stake.
2122        let block_2_2 = VerifiedBlock::new_for_test(
2123            TestBlock::new(2, 2)
2124                .set_ancestors(vec![block_1_1.reference()])
2125                .build(),
2126        );
2127        dag_state.accept_block(block_2_2);
2128
2129        let stake_2 = context.committee.stake(author_2);
2130        let by_ref: BTreeMap<BlockRef, BlockInfo> = dag_state
2131            .get_block_info_at_slot(slot_1_1)
2132            .into_iter()
2133            .map(|i| (i.block.reference(), i))
2134            .collect();
2135        assert_eq!(
2136            by_ref[&block_1_1.reference()].children_stake.stake(),
2137            stake_2
2138        );
2139        assert_eq!(
2140            by_ref[&block_1_1_equiv.reference()].children_stake.stake(),
2141            0
2142        );
2143    }
2144
2145    #[tokio::test]
2146    #[should_panic(
2147        expected = "get_block_info_at_slot() should only be called for slots above gc_round"
2148    )]
2149    async fn test_get_block_info_at_slot_panics_at_or_below_gc_round() {
2150        let (context, _) = Context::new_for_test(4);
2151        let context = Arc::new(context);
2152        let store = Arc::new(MemStore::new());
2153        let dag_state = DagState::new(context, store);
2154
2155        // gc_round() is 0 before any commit; querying round 0 fails the
2156        // slot.round > gc_round() guard.
2157        let _ = dag_state.get_block_info_at_slot(Slot::new(0, AuthorityIndex::new_for_test(0)));
2158    }
2159
2160    #[tokio::test]
2161    async fn test_block_children_exclusion() {
2162        let (mut context, _) = Context::new_for_test(4);
2163        context.parameters.dag_state_cached_rounds = 10;
2164        context.protocol_config.set_gc_depth_for_testing(3);
2165        context.protocol_config.set_enable_v3_for_testing(true);
2166        let context = Arc::new(context);
2167
2168        let store = Arc::new(MemStore::new());
2169        let mut dag_state = DagState::new(context.clone(), store.clone());
2170
2171        // Accept rounds 1..=2 with default full links.
2172        let mut dag_builder = DagBuilder::new(context.clone());
2173        dag_builder.layers(1..=2).build();
2174        let base_blocks = dag_builder.all_blocks();
2175        dag_state.accept_blocks(base_blocks.clone());
2176
2177        // Craft a round-3 block with mixed strong + weak ancestors:
2178        //   strong: three round-2 blocks (ancestor.round + 1 == 3, authority other than 0)
2179        //   weak:   one round-1 block (ancestor.round + 1 == 2, not 3, authority 0)
2180        let round_2_refs: Vec<BlockRef> = base_blocks
2181            .iter()
2182            .filter(|b| b.round() == 2 && b.author() != AuthorityIndex::new_for_test(0))
2183            .map(|b| b.reference())
2184            .collect();
2185        let weak_ancestor = base_blocks
2186            .iter()
2187            .find(|b| b.round() == 1 && b.author() == AuthorityIndex::new_for_test(0))
2188            .expect("should have a round-1 authority-0 block")
2189            .reference();
2190
2191        let mut ancestors = round_2_refs.clone();
2192        ancestors.push(weak_ancestor);
2193        let round_3 =
2194            VerifiedBlock::new_for_test(TestBlock::new(3, 1).set_ancestors_raw(ancestors).build());
2195        let round_3_ref = round_3.reference();
2196        dag_state.accept_block(round_3);
2197
2198        // Strong ancestors (round 2): each should now list round_3_ref as a child.
2199        // Derived fields: only authority 1 (round_3's author) is represented, so
2200        // children_authorities == {1} and total_children_stake == stake(1).
2201        let author_1 = AuthorityIndex::new_for_test(1);
2202        let stake_1 = context.committee.stake(author_1);
2203        for r2_ref in &round_2_refs {
2204            let children = dag_state
2205                .get_block_children(r2_ref)
2206                .expect("round-2 block should still be present");
2207            assert!(
2208                children.contains(&round_3_ref),
2209                "round-2 parent {r2_ref:?} should have round-3 block as child",
2210            );
2211            let authorities = dag_state
2212                .get_block_children_authorities(r2_ref)
2213                .expect("round-2 block should still be present");
2214            assert_eq!(
2215                authorities,
2216                BTreeSet::from([author_1]),
2217                "round-2 parent {r2_ref:?} should have children_authorities == {{1}}",
2218            );
2219            assert_eq!(
2220                dag_state
2221                    .get_block_total_children_stake(r2_ref)
2222                    .expect("round-2 block should still be present"),
2223                stake_1,
2224                "round-2 parent {r2_ref:?} total_children_stake mismatch",
2225            );
2226        }
2227
2228        // Weak ancestor (round 1): must NOT have round_3_ref as a child, and
2229        // its children set should only contain the natural round-2 population.
2230        let weak_children = dag_state
2231            .get_block_children(&weak_ancestor)
2232            .expect("weak ancestor should still be present");
2233        assert!(
2234            !weak_children.contains(&round_3_ref),
2235            "weak ancestor {weak_ancestor:?} must NOT have round-3 block as child",
2236        );
2237        for c in &weak_children {
2238            assert_eq!(
2239                c.round, 2,
2240                "weak ancestor's children should all be round 2, got {c:?}"
2241            );
2242        }
2243        // Derived fields for the weak ancestor: children are the 4 natural
2244        // round-2 blocks authored by all 4 authorities, so authorities =
2245        // {0,1,2,3} and stake = total_stake. round_3 does not contribute
2246        // because the weak link was filtered out.
2247        let weak_authorities = dag_state
2248            .get_block_children_authorities(&weak_ancestor)
2249            .expect("weak ancestor should still be present");
2250        let expected_weak_authorities: BTreeSet<AuthorityIndex> =
2251            (0..4).map(AuthorityIndex::new_for_test).collect();
2252        assert_eq!(
2253            weak_authorities, expected_weak_authorities,
2254            "weak ancestor {weak_ancestor:?} children_authorities should cover all 4 round-2 authors",
2255        );
2256        assert_eq!(
2257            dag_state
2258                .get_block_total_children_stake(&weak_ancestor)
2259                .expect("weak ancestor should still be present"),
2260            context.committee.total_stake(),
2261            "weak ancestor {weak_ancestor:?} total_children_stake should equal total committee stake",
2262        );
2263    }
2264
2265    #[tokio::test]
2266    async fn test_round_info() {
2267        let (mut context, _) = Context::new_for_test(4);
2268        // Small cached_rounds so a later flush evicts below-gc round_info entries.
2269        context.parameters.dag_state_cached_rounds = 2;
2270        context.protocol_config.set_gc_depth_for_testing(3);
2271        context.protocol_config.set_enable_v3_for_testing(true);
2272        let context = Arc::new(context);
2273
2274        let store = Arc::new(MemStore::new());
2275        let mut dag_state = DagState::new(context.clone(), store.clone());
2276
2277        // Before any blocks: no round_info entries exist.
2278        assert!(dag_state.get_round_info(1).is_none());
2279
2280        // Accept blocks for rounds 1..=4 with full participation. Each round
2281        // must aggregate every authority and stake equal to total_stake.
2282        let mut dag_builder = DagBuilder::new(context.clone());
2283        dag_builder.layers(1..=4).build();
2284        dag_state.accept_blocks(dag_builder.all_blocks());
2285
2286        let all_authorities: BTreeSet<AuthorityIndex> =
2287            (0..4).map(AuthorityIndex::new_for_test).collect();
2288        for round in 1..=4 {
2289            let info = dag_state
2290                .get_round_info(round)
2291                .unwrap_or_else(|| panic!("round_info missing for round {round}"));
2292            assert_eq!(info.round, round);
2293            assert_eq!(
2294                info.total_stake.authorities(),
2295                &all_authorities,
2296                "round {round} authorities mismatch"
2297            );
2298            assert_eq!(
2299                info.total_stake.stake(),
2300                context.committee.total_stake(),
2301                "round {round} total_stake mismatch"
2302            );
2303        }
2304
2305        // Beyond highest_accepted_round: no entry yet.
2306        assert!(dag_state.get_round_info(5).is_none());
2307
2308        // Re-accepting the same blocks must not double-count stake. The
2309        // contains_block early-return inside accept_block guards this.
2310        dag_state.accept_blocks(dag_builder.all_blocks());
2311        for round in 1..=4 {
2312            let info = dag_state.get_round_info(round).unwrap();
2313            assert_eq!(
2314                info.total_stake.authorities(),
2315                &all_authorities,
2316                "round {round} authorities changed after re-accept"
2317            );
2318            assert_eq!(
2319                info.total_stake.stake(),
2320                context.committee.total_stake(),
2321                "round {round} total_stake changed after re-accept"
2322            );
2323        }
2324
2325        // Partial round: extend the DAG with only authority 0 at round 5.
2326        let round_5_block = VerifiedBlock::new_for_test(
2327            TestBlock::new(5, 0)
2328                .set_ancestors_raw(
2329                    dag_builder
2330                        .all_blocks()
2331                        .iter()
2332                        .filter(|b| b.round() == 4)
2333                        .map(|b| b.reference())
2334                        .collect(),
2335                )
2336                .build(),
2337        );
2338        dag_state.accept_block(round_5_block);
2339        let info_5 = dag_state
2340            .get_round_info(5)
2341            .expect("round 5 entry should exist");
2342        let author_0 = AuthorityIndex::new_for_test(0);
2343        assert_eq!(
2344            info_5.total_stake.authorities(),
2345            &BTreeSet::from([author_0])
2346        );
2347        assert_eq!(
2348            info_5.total_stake.stake(),
2349            context.committee.stake(author_0)
2350        );
2351
2352        // GC boundary: commit a round-5 leader so gc_round advances to 2,
2353        // then flush to evict round_info entries with round <= gc_round.
2354        let round_5_leader_ref = dag_state
2355            .recent_refs_by_authority
2356            .iter()
2357            .flat_map(|set| set.iter())
2358            .find(|r| r.round == 5)
2359            .copied()
2360            .expect("round 5 block should be accepted");
2361        let last_commit = TrustedCommit::new_for_test(
2362            5,
2363            CommitDigest::MIN,
2364            context.clock.timestamp_utc_ms(),
2365            round_5_leader_ref,
2366            vec![],
2367        );
2368        dag_state.set_last_commit(last_commit);
2369        assert_eq!(dag_state.gc_round(), 2);
2370
2371        // RoundInfo should expose logical retention immediately when the GC
2372        // round advances, even before flush physically removes stale entries.
2373        for round in 1..=2 {
2374            assert!(
2375                dag_state.get_round_info(round).is_none(),
2376                "round {round} should be hidden before flush after GC advances"
2377            );
2378        }
2379
2380        dag_state.flush();
2381
2382        // Rounds 1..=2 are evicted; rounds 3..=5 remain with their aggregates.
2383        for round in 1..=2 {
2384            assert!(
2385                dag_state.get_round_info(round).is_none(),
2386                "round {round} should be evicted after flush"
2387            );
2388        }
2389        for round in 3..=4 {
2390            let info = dag_state
2391                .get_round_info(round)
2392                .unwrap_or_else(|| panic!("round_info missing for round {round} after flush"));
2393            assert_eq!(info.total_stake.authorities(), &all_authorities);
2394            assert_eq!(info.total_stake.stake(), context.committee.total_stake());
2395        }
2396        let info_5 = dag_state
2397            .get_round_info(5)
2398            .expect("round 5 should still be present after flush");
2399        assert_eq!(
2400            info_5.total_stake.authorities(),
2401            &BTreeSet::from([author_0])
2402        );
2403        assert_eq!(
2404            info_5.total_stake.stake(),
2405            context.committee.stake(author_0)
2406        );
2407    }
2408
2409    #[tokio::test]
2410    async fn test_contains_blocks_in_cache_or_store() {
2411        /// Only keep elements up to 2 rounds before the last committed round
2412        const CACHED_ROUNDS: Round = 2;
2413
2414        let (mut context, _) = Context::new_for_test(4);
2415        context.parameters.dag_state_cached_rounds = CACHED_ROUNDS;
2416
2417        let context = Arc::new(context);
2418        let store = Arc::new(MemStore::new());
2419        let mut dag_state = DagState::new(context.clone(), store.clone());
2420
2421        // Create test blocks for round 1 ~ 10
2422        let num_rounds: u32 = 10;
2423        let num_authorities: u32 = 4;
2424        let mut blocks = Vec::new();
2425
2426        for round in 1..=num_rounds {
2427            for author in 0..num_authorities {
2428                let block = VerifiedBlock::new_for_test(TestBlock::new(round, author).build());
2429                blocks.push(block);
2430            }
2431        }
2432
2433        // Now write in store the blocks from first 4 rounds and the rest to the dag state
2434        blocks.clone().into_iter().for_each(|block| {
2435            if block.round() <= 4 {
2436                store
2437                    .write(WriteBatch::default().blocks(vec![block]))
2438                    .unwrap();
2439            } else {
2440                dag_state.accept_blocks(vec![block]);
2441            }
2442        });
2443
2444        // Now when trying to query whether we have all the blocks, we should successfully retrieve a positive answer
2445        // where the blocks of first 4 round should be found in DagState and the rest in store.
2446        let mut block_refs = blocks
2447            .iter()
2448            .map(|block| block.reference())
2449            .collect::<Vec<_>>();
2450        let result = dag_state.contains_blocks(block_refs.clone());
2451
2452        // Ensure everything is found
2453        let mut expected = vec![true; (num_rounds * num_authorities) as usize];
2454        assert_eq!(result, expected);
2455
2456        // Now try to ask also for one block ref that is neither in cache nor in store
2457        block_refs.insert(
2458            3,
2459            BlockRef::new(11, AuthorityIndex::new_for_test(3), BlockDigest::default()),
2460        );
2461        let result = dag_state.contains_blocks(block_refs.clone());
2462
2463        // Then all should be found apart from the last one
2464        expected.insert(3, false);
2465        assert_eq!(result, expected.clone());
2466    }
2467
2468    #[tokio::test]
2469    async fn test_contains_cached_block_at_slot() {
2470        /// Only keep elements up to 2 rounds before the last committed round
2471        const CACHED_ROUNDS: Round = 2;
2472
2473        let num_authorities: u32 = 4;
2474        let (mut context, _) = Context::new_for_test(num_authorities as usize);
2475        context.parameters.dag_state_cached_rounds = CACHED_ROUNDS;
2476
2477        let context = Arc::new(context);
2478        let store = Arc::new(MemStore::new());
2479        let mut dag_state = DagState::new(context.clone(), store.clone());
2480
2481        // Create test blocks for round 1 ~ 10
2482        let num_rounds: u32 = 10;
2483        let mut blocks = Vec::new();
2484
2485        for round in 1..=num_rounds {
2486            for author in 0..num_authorities {
2487                let block = VerifiedBlock::new_for_test(TestBlock::new(round, author).build());
2488                blocks.push(block.clone());
2489                dag_state.accept_block(block);
2490            }
2491        }
2492
2493        // Query for genesis round 0, genesis blocks should be returned
2494        for (author, _) in context.committee.authorities() {
2495            assert!(
2496                dag_state.contains_cached_block_at_slot(Slot::new(GENESIS_ROUND, author)),
2497                "Genesis should always be found"
2498            );
2499        }
2500
2501        // Now when trying to query whether we have all the blocks, we should successfully retrieve a positive answer
2502        // where the blocks of first 4 round should be found in DagState and the rest in store.
2503        let mut block_refs = blocks
2504            .iter()
2505            .map(|block| block.reference())
2506            .collect::<Vec<_>>();
2507
2508        for block_ref in block_refs.clone() {
2509            let slot = block_ref.into();
2510            let found = dag_state.contains_cached_block_at_slot(slot);
2511            assert!(found, "A block should be found at slot {}", slot);
2512        }
2513
2514        // Now try to ask also for one block ref that is not in cache
2515        // Then all should be found apart from the last one
2516        block_refs.insert(
2517            3,
2518            BlockRef::new(11, AuthorityIndex::new_for_test(3), BlockDigest::default()),
2519        );
2520        let mut expected = vec![true; (num_rounds * num_authorities) as usize];
2521        expected.insert(3, false);
2522
2523        // Attempt to check the same for via the contains slot method
2524        for block_ref in block_refs {
2525            let slot = block_ref.into();
2526            let found = dag_state.contains_cached_block_at_slot(slot);
2527
2528            assert_eq!(expected.remove(0), found);
2529        }
2530    }
2531
2532    #[tokio::test]
2533    #[ignore]
2534    #[should_panic(
2535        expected = "Attempted to check for slot [1]3 that is <= the last gc evicted round 3"
2536    )]
2537    async fn test_contains_cached_block_at_slot_panics_when_ask_out_of_range() {
2538        /// Keep 2 rounds from the highest committed round. This is considered universal and minimum necessary blocks to hold
2539        /// for the correct node operation.
2540        const GC_DEPTH: u32 = 2;
2541        /// Keep at least 3 rounds in cache for each authority.
2542        const CACHED_ROUNDS: Round = 3;
2543
2544        let (mut context, _) = Context::new_for_test(4);
2545        context.protocol_config.set_gc_depth_for_testing(GC_DEPTH);
2546        context.parameters.dag_state_cached_rounds = CACHED_ROUNDS;
2547
2548        let context = Arc::new(context);
2549        let store = Arc::new(MemStore::new());
2550        let mut dag_state = DagState::new(context.clone(), store.clone());
2551
2552        // Create for rounds 1..=6. Skip creating blocks for authority 0 for rounds 4 - 6.
2553        let mut dag_builder = DagBuilder::new(context.clone());
2554        dag_builder.layers(1..=3).build();
2555        dag_builder
2556            .layers(4..=6)
2557            .authorities(vec![AuthorityIndex::new_for_test(0)])
2558            .skip_block()
2559            .build();
2560
2561        // Accept all blocks
2562        dag_builder
2563            .all_blocks()
2564            .into_iter()
2565            .for_each(|block| dag_state.accept_block(block));
2566
2567        // Now add a commit for leader round 5 to trigger an eviction
2568        dag_state.add_commit(TrustedCommit::new_for_test(
2569            1 as CommitIndex,
2570            CommitDigest::MIN,
2571            0,
2572            dag_builder.leader_block(5).unwrap().reference(),
2573            vec![],
2574        ));
2575        // Flush the DAG state to storage.
2576        dag_state.flush();
2577
2578        // Ensure that gc round has been updated
2579        assert_eq!(dag_state.gc_round(), 3, "GC round should be 3");
2580
2581        // Now what we expect to happen is for:
2582        // * Nodes 1 - 3 should have in cache blocks from gc_round (3) and onwards.
2583        // * Node 0 should have in cache blocks from it's latest round, 3, up to round 1, which is the number of cached_rounds.
2584        for authority_index in 1..=3 {
2585            for round in 4..=6 {
2586                assert!(dag_state.contains_cached_block_at_slot(Slot::new(
2587                    round,
2588                    AuthorityIndex::new_for_test(authority_index)
2589                )));
2590            }
2591        }
2592
2593        for round in 1..=3 {
2594            assert!(
2595                dag_state.contains_cached_block_at_slot(Slot::new(
2596                    round,
2597                    AuthorityIndex::new_for_test(0)
2598                ))
2599            );
2600        }
2601
2602        // When trying to request for authority 1 at block slot 3 it should panic, as anything
2603        // that is <= 3 should be evicted
2604        let _ =
2605            dag_state.contains_cached_block_at_slot(Slot::new(3, AuthorityIndex::new_for_test(1)));
2606    }
2607
2608    #[tokio::test]
2609    async fn test_get_blocks_in_cache_or_store() {
2610        let (context, _) = Context::new_for_test(4);
2611        let context = Arc::new(context);
2612        let store = Arc::new(MemStore::new());
2613        let mut dag_state = DagState::new(context.clone(), store.clone());
2614
2615        // Create test blocks for round 1 ~ 10
2616        let num_rounds: u32 = 10;
2617        let num_authorities: u32 = 4;
2618        let mut blocks = Vec::new();
2619
2620        for round in 1..=num_rounds {
2621            for author in 0..num_authorities {
2622                let block = VerifiedBlock::new_for_test(TestBlock::new(round, author).build());
2623                blocks.push(block);
2624            }
2625        }
2626
2627        // Now write in store the blocks from first 4 rounds and the rest to the dag state
2628        blocks.clone().into_iter().for_each(|block| {
2629            if block.round() <= 4 {
2630                store
2631                    .write(WriteBatch::default().blocks(vec![block]))
2632                    .unwrap();
2633            } else {
2634                dag_state.accept_blocks(vec![block]);
2635            }
2636        });
2637
2638        // Now when trying to query whether we have all the blocks, we should successfully retrieve a positive answer
2639        // where the blocks of first 4 round should be found in DagState and the rest in store.
2640        let mut block_refs = blocks
2641            .iter()
2642            .map(|block| block.reference())
2643            .collect::<Vec<_>>();
2644        let result = dag_state.get_blocks(&block_refs);
2645
2646        let mut expected = blocks
2647            .into_iter()
2648            .map(Some)
2649            .collect::<Vec<Option<VerifiedBlock>>>();
2650
2651        // Ensure everything is found
2652        assert_eq!(result, expected.clone());
2653
2654        // Now try to ask also for one block ref that is neither in cache nor in store
2655        block_refs.insert(
2656            3,
2657            BlockRef::new(11, AuthorityIndex::new_for_test(3), BlockDigest::default()),
2658        );
2659        let result = dag_state.get_blocks(&block_refs);
2660
2661        // Then all should be found apart from the last one
2662        expected.insert(3, None);
2663        assert_eq!(result, expected);
2664    }
2665
2666    #[tokio::test]
2667    async fn test_flush_and_recovery() {
2668        telemetry_subscribers::init_for_testing();
2669
2670        const GC_DEPTH: u32 = 3;
2671        const CACHED_ROUNDS: u32 = 4;
2672
2673        let num_authorities: u32 = 4;
2674        let (mut context, _) = Context::new_for_test(num_authorities as usize);
2675        context.parameters.dag_state_cached_rounds = CACHED_ROUNDS;
2676        context.protocol_config.set_gc_depth_for_testing(GC_DEPTH);
2677
2678        let context = Arc::new(context);
2679
2680        let store = Arc::new(MemStore::new());
2681        let mut dag_state = DagState::new(context.clone(), store.clone());
2682
2683        const NUM_ROUNDS: Round = 20;
2684        let mut dag_builder = DagBuilder::new(context.clone());
2685        dag_builder.layers(1..=5).build();
2686        dag_builder
2687            .layers(6..=8)
2688            .authorities(vec![AuthorityIndex::new_for_test(0)])
2689            .skip_block()
2690            .build();
2691        dag_builder.layers(9..=NUM_ROUNDS).build();
2692
2693        // Get all commits from the DAG builder.
2694        const LAST_COMMIT_ROUND: Round = 16;
2695        const LAST_COMMIT_INDEX: CommitIndex = 15;
2696        let commits = dag_builder
2697            .get_sub_dag_and_commits(1..=NUM_ROUNDS)
2698            .into_iter()
2699            .map(|(_subdag, commit)| commit)
2700            .take(LAST_COMMIT_INDEX as usize)
2701            .collect::<Vec<_>>();
2702        assert_eq!(commits.len(), LAST_COMMIT_INDEX as usize);
2703        assert_eq!(commits.last().unwrap().round(), LAST_COMMIT_ROUND);
2704
2705        // Add the blocks from first 11 rounds and first 8 commits to the dag state
2706        // Note that the commit of round 8 is missing because where authority 0 is the leader but produced no block.
2707        // So commit 8 has leader round 9.
2708        const PERSISTED_BLOCK_ROUNDS: u32 = 12;
2709        const NUM_PERSISTED_COMMITS: usize = 8;
2710        const LAST_PERSISTED_COMMIT_ROUND: Round = 9;
2711        const LAST_PERSISTED_COMMIT_INDEX: CommitIndex = 8;
2712        dag_state.accept_blocks(dag_builder.blocks(1..=PERSISTED_BLOCK_ROUNDS));
2713        let mut finalized_commits = vec![];
2714        for commit in commits.iter().take(NUM_PERSISTED_COMMITS).cloned() {
2715            finalized_commits.push(commit.clone());
2716            dag_state.add_commit(commit);
2717        }
2718        let last_finalized_commit = finalized_commits.last().unwrap();
2719        assert_eq!(last_finalized_commit.round(), LAST_PERSISTED_COMMIT_ROUND);
2720        assert_eq!(last_finalized_commit.index(), LAST_PERSISTED_COMMIT_INDEX);
2721
2722        // Collect finalized blocks.
2723        let finalized_blocks = finalized_commits
2724            .iter()
2725            .flat_map(|commit| commit.blocks())
2726            .collect::<BTreeSet<_>>();
2727
2728        // Flush commits from the dag state
2729        dag_state.flush();
2730
2731        // Verify the store has blocks up to round 12, and commits up to index 8.
2732        let store_blocks = store
2733            .scan_blocks_by_author(AuthorityIndex::new_for_test(1), 1)
2734            .unwrap();
2735        assert_eq!(store_blocks.last().unwrap().round(), PERSISTED_BLOCK_ROUNDS);
2736        let store_commits = store.scan_commits((0..=CommitIndex::MAX).into()).unwrap();
2737        assert_eq!(store_commits.len(), NUM_PERSISTED_COMMITS);
2738        assert_eq!(
2739            store_commits.last().unwrap().index(),
2740            LAST_PERSISTED_COMMIT_INDEX
2741        );
2742        assert_eq!(
2743            store_commits.last().unwrap().round(),
2744            LAST_PERSISTED_COMMIT_ROUND
2745        );
2746
2747        // Add the rest of the blocks and commits to the dag state
2748        dag_state.accept_blocks(dag_builder.blocks(PERSISTED_BLOCK_ROUNDS + 1..=NUM_ROUNDS));
2749        for commit in commits.iter().skip(NUM_PERSISTED_COMMITS).cloned() {
2750            dag_state.add_commit(commit);
2751        }
2752
2753        // All blocks should be found in DagState.
2754        let all_blocks = dag_builder.blocks(1..=NUM_ROUNDS);
2755        let block_refs = all_blocks
2756            .iter()
2757            .map(|block| block.reference())
2758            .collect::<Vec<_>>();
2759        let result = dag_state
2760            .get_blocks(&block_refs)
2761            .into_iter()
2762            .map(|b| b.unwrap())
2763            .collect::<Vec<_>>();
2764        assert_eq!(result, all_blocks);
2765
2766        // Last commit index from DagState should now be 15
2767        assert_eq!(dag_state.last_commit_index(), LAST_COMMIT_INDEX);
2768
2769        // Destroy the dag state without flushing additional data.
2770        drop(dag_state);
2771
2772        // Recover the state from the store
2773        let dag_state = DagState::new(context.clone(), store.clone());
2774
2775        // Persisted blocks rounds should be found in DagState.
2776        let all_blocks = dag_builder.blocks(1..=PERSISTED_BLOCK_ROUNDS);
2777        let block_refs = all_blocks
2778            .iter()
2779            .map(|block| block.reference())
2780            .collect::<Vec<_>>();
2781        let result = dag_state
2782            .get_blocks(&block_refs)
2783            .into_iter()
2784            .map(|b| b.unwrap())
2785            .collect::<Vec<_>>();
2786        assert_eq!(result, all_blocks);
2787
2788        // Unpersisted blocks should not be in DagState, because they are not flushed.
2789        let missing_blocks = dag_builder.blocks(PERSISTED_BLOCK_ROUNDS + 1..=NUM_ROUNDS);
2790        let block_refs = missing_blocks
2791            .iter()
2792            .map(|block| block.reference())
2793            .collect::<Vec<_>>();
2794        let retrieved_blocks = dag_state
2795            .get_blocks(&block_refs)
2796            .into_iter()
2797            .flatten()
2798            .collect::<Vec<_>>();
2799        assert!(retrieved_blocks.is_empty());
2800
2801        // Recovered last commit index and round should be 8 and 9.
2802        assert_eq!(dag_state.last_commit_index(), LAST_PERSISTED_COMMIT_INDEX);
2803        assert_eq!(dag_state.last_commit_round(), LAST_PERSISTED_COMMIT_ROUND);
2804
2805        // The last_commit_rounds of the finalized commits should have been recovered.
2806        let expected_last_committed_rounds = vec![5, 9, 8, 8];
2807        assert_eq!(
2808            dag_state.last_committed_rounds(),
2809            expected_last_committed_rounds
2810        );
2811        // Unscored subdags will be recovered based on the flushed commits and no commit info.
2812        assert_eq!(dag_state.scoring_subdags_count(), NUM_PERSISTED_COMMITS);
2813
2814        // Ensure that cached blocks exist only for specific rounds per authority
2815        for (authority_index, _) in context.committee.authorities() {
2816            let blocks = dag_state.get_cached_blocks(authority_index, 1);
2817
2818            // Ensure that eviction rounds have been properly recovered.
2819            // For every authority, the gc round is 9 - 3 = 6, and cached round is 12-5 = 7.
2820            // So eviction round is the min which is 6.
2821            if authority_index == AuthorityIndex::new_for_test(0) {
2822                assert_eq!(blocks.len(), 4);
2823                assert_eq!(dag_state.evicted_rounds[authority_index.value()], 6);
2824                assert!(
2825                    blocks
2826                        .into_iter()
2827                        .all(|block| block.round() >= 7 && block.round() <= 12)
2828                );
2829            } else {
2830                assert_eq!(blocks.len(), 6);
2831                assert_eq!(dag_state.evicted_rounds[authority_index.value()], 6);
2832                assert!(
2833                    blocks
2834                        .into_iter()
2835                        .all(|block| block.round() >= 7 && block.round() <= 12)
2836                );
2837            }
2838        }
2839
2840        // Ensure that committed blocks from > gc_round have been correctly recovered as committed according to committed sub dags.
2841        let gc_round = dag_state.gc_round();
2842        assert_eq!(gc_round, 6);
2843        dag_state
2844            .recent_blocks
2845            .iter()
2846            .for_each(|(block_ref, block_info)| {
2847                if block_ref.round > gc_round && finalized_blocks.contains(block_ref) {
2848                    assert!(
2849                        block_info.committed,
2850                        "Block {:?} should be set as committed",
2851                        block_ref
2852                    );
2853                }
2854            });
2855
2856        // Ensure the hard linked status of blocks are recovered.
2857        // All blocks below highest accepted round, or authority 0 round 12 block, should be hard linked.
2858        // Other blocks (round 12 but not from authority 0) should not be hard linked.
2859        // This is because authority 0 blocks are considered proposed blocks.
2860        dag_state
2861            .recent_blocks
2862            .iter()
2863            .for_each(|(block_ref, block_info)| {
2864                if block_ref.round < PERSISTED_BLOCK_ROUNDS || block_ref.author.value() == 0 {
2865                    assert!(block_info.included);
2866                } else {
2867                    assert!(!block_info.included);
2868                }
2869            });
2870    }
2871
2872    #[tokio::test]
2873    async fn test_block_info_as_committed() {
2874        let num_authorities: u32 = 4;
2875        let (context, _) = Context::new_for_test(num_authorities as usize);
2876        let context = Arc::new(context);
2877
2878        let store = Arc::new(MemStore::new());
2879        let mut dag_state = DagState::new(context.clone(), store.clone());
2880
2881        // Accept a block
2882        let block = VerifiedBlock::new_for_test(
2883            TestBlock::new(1, 0)
2884                .set_timestamp_ms(1000)
2885                .set_ancestors(vec![])
2886                .build(),
2887        );
2888
2889        dag_state.accept_block(block.clone());
2890
2891        // Query is committed
2892        assert!(!dag_state.is_committed(&block.reference()));
2893
2894        // Set block as committed for first time should return true
2895        assert!(
2896            dag_state.set_committed(&block.reference()),
2897            "Block should be successfully set as committed for first time"
2898        );
2899
2900        // Now it should appear as committed
2901        assert!(dag_state.is_committed(&block.reference()));
2902
2903        // Trying to set the block as committed again, it should return false.
2904        assert!(
2905            !dag_state.set_committed(&block.reference()),
2906            "Block should not be successfully set as committed"
2907        );
2908    }
2909
2910    #[tokio::test]
2911    async fn test_get_cached_blocks() {
2912        let (mut context, _) = Context::new_for_test(4);
2913        context.parameters.dag_state_cached_rounds = 5;
2914
2915        let context = Arc::new(context);
2916        let store = Arc::new(MemStore::new());
2917        let mut dag_state = DagState::new(context.clone(), store.clone());
2918
2919        // Create no blocks for authority 0
2920        // Create one block (round 10) for authority 1
2921        // Create two blocks (rounds 10,11) for authority 2
2922        // Create three blocks (rounds 10,11,12) for authority 3
2923        let mut all_blocks = Vec::new();
2924        for author in 1..=3 {
2925            for round in 10..(10 + author) {
2926                let block = VerifiedBlock::new_for_test(TestBlock::new(round, author).build());
2927                all_blocks.push(block.clone());
2928                dag_state.accept_block(block);
2929            }
2930        }
2931
2932        // Test get_cached_blocks()
2933
2934        let cached_blocks =
2935            dag_state.get_cached_blocks(context.committee.to_authority_index(0).unwrap(), 0);
2936        assert!(cached_blocks.is_empty());
2937
2938        let cached_blocks =
2939            dag_state.get_cached_blocks(context.committee.to_authority_index(1).unwrap(), 10);
2940        assert_eq!(cached_blocks.len(), 1);
2941        assert_eq!(cached_blocks[0].round(), 10);
2942
2943        let cached_blocks =
2944            dag_state.get_cached_blocks(context.committee.to_authority_index(2).unwrap(), 10);
2945        assert_eq!(cached_blocks.len(), 2);
2946        assert_eq!(cached_blocks[0].round(), 10);
2947        assert_eq!(cached_blocks[1].round(), 11);
2948
2949        let cached_blocks =
2950            dag_state.get_cached_blocks(context.committee.to_authority_index(2).unwrap(), 11);
2951        assert_eq!(cached_blocks.len(), 1);
2952        assert_eq!(cached_blocks[0].round(), 11);
2953
2954        let cached_blocks =
2955            dag_state.get_cached_blocks(context.committee.to_authority_index(3).unwrap(), 10);
2956        assert_eq!(cached_blocks.len(), 3);
2957        assert_eq!(cached_blocks[0].round(), 10);
2958        assert_eq!(cached_blocks[1].round(), 11);
2959        assert_eq!(cached_blocks[2].round(), 12);
2960
2961        let cached_blocks =
2962            dag_state.get_cached_blocks(context.committee.to_authority_index(3).unwrap(), 12);
2963        assert_eq!(cached_blocks.len(), 1);
2964        assert_eq!(cached_blocks[0].round(), 12);
2965
2966        // Test get_cached_blocks_in_range()
2967
2968        // Start == end
2969        let cached_blocks = dag_state.get_cached_blocks_in_range(
2970            context.committee.to_authority_index(3).unwrap(),
2971            10,
2972            10,
2973            1,
2974        );
2975        assert!(cached_blocks.is_empty());
2976
2977        // Start > end
2978        let cached_blocks = dag_state.get_cached_blocks_in_range(
2979            context.committee.to_authority_index(3).unwrap(),
2980            11,
2981            10,
2982            1,
2983        );
2984        assert!(cached_blocks.is_empty());
2985
2986        // Empty result.
2987        let cached_blocks = dag_state.get_cached_blocks_in_range(
2988            context.committee.to_authority_index(0).unwrap(),
2989            9,
2990            10,
2991            1,
2992        );
2993        assert!(cached_blocks.is_empty());
2994
2995        // Single block, one round before the end.
2996        let cached_blocks = dag_state.get_cached_blocks_in_range(
2997            context.committee.to_authority_index(1).unwrap(),
2998            9,
2999            11,
3000            1,
3001        );
3002        assert_eq!(cached_blocks.len(), 1);
3003        assert_eq!(cached_blocks[0].round(), 10);
3004
3005        // Respect end round.
3006        let cached_blocks = dag_state.get_cached_blocks_in_range(
3007            context.committee.to_authority_index(2).unwrap(),
3008            9,
3009            12,
3010            5,
3011        );
3012        assert_eq!(cached_blocks.len(), 2);
3013        assert_eq!(cached_blocks[0].round(), 10);
3014        assert_eq!(cached_blocks[1].round(), 11);
3015
3016        // Respect start round.
3017        let cached_blocks = dag_state.get_cached_blocks_in_range(
3018            context.committee.to_authority_index(3).unwrap(),
3019            11,
3020            20,
3021            5,
3022        );
3023        assert_eq!(cached_blocks.len(), 2);
3024        assert_eq!(cached_blocks[0].round(), 11);
3025        assert_eq!(cached_blocks[1].round(), 12);
3026
3027        // Respect limit
3028        let cached_blocks = dag_state.get_cached_blocks_in_range(
3029            context.committee.to_authority_index(3).unwrap(),
3030            10,
3031            20,
3032            1,
3033        );
3034        assert_eq!(cached_blocks.len(), 1);
3035        assert_eq!(cached_blocks[0].round(), 10);
3036    }
3037
3038    #[tokio::test]
3039    async fn test_get_last_cached_block() {
3040        // GIVEN
3041        const CACHED_ROUNDS: Round = 2;
3042        const GC_DEPTH: u32 = 1;
3043        let (mut context, _) = Context::new_for_test(4);
3044        context.parameters.dag_state_cached_rounds = CACHED_ROUNDS;
3045        context.protocol_config.set_gc_depth_for_testing(GC_DEPTH);
3046
3047        let context = Arc::new(context);
3048        let store = Arc::new(MemStore::new());
3049        let mut dag_state = DagState::new(context.clone(), store.clone());
3050
3051        // Create no blocks for authority 0
3052        // Create one block (round 1) for authority 1
3053        // Create two blocks (rounds 1,2) for authority 2
3054        // Create three blocks (rounds 1,2,3) for authority 3
3055        let dag_str = "DAG {
3056            Round 0 : { 4 },
3057            Round 1 : {
3058                B -> [*],
3059                C -> [*],
3060                D -> [*],
3061            },
3062            Round 2 : {
3063                C -> [*],
3064                D -> [*],
3065            },
3066            Round 3 : {
3067                D -> [*],
3068            },
3069        }";
3070
3071        let (_, dag_builder) = parse_dag(dag_str).expect("Invalid dag");
3072
3073        // Add equivocating block for round 2 authority 3
3074        let block = VerifiedBlock::new_for_test(TestBlock::new(2, 2).build());
3075
3076        // Accept all blocks
3077        for block in dag_builder
3078            .all_blocks()
3079            .into_iter()
3080            .chain(std::iter::once(block))
3081        {
3082            dag_state.accept_block(block);
3083        }
3084
3085        dag_state.add_commit(TrustedCommit::new_for_test(
3086            1 as CommitIndex,
3087            CommitDigest::MIN,
3088            context.clock.timestamp_utc_ms(),
3089            dag_builder.leader_block(3).unwrap().reference(),
3090            vec![],
3091        ));
3092
3093        // WHEN search for the latest blocks
3094        let end_round = 4;
3095        let expected_rounds = vec![0, 1, 2, 3];
3096        let expected_excluded_and_equivocating_blocks = vec![0, 0, 1, 0];
3097        // THEN
3098        let last_blocks = dag_state.get_last_cached_block_per_authority(end_round);
3099        assert_eq!(
3100            last_blocks.iter().map(|b| b.0.round()).collect::<Vec<_>>(),
3101            expected_rounds
3102        );
3103        assert_eq!(
3104            last_blocks.iter().map(|b| b.1.len()).collect::<Vec<_>>(),
3105            expected_excluded_and_equivocating_blocks
3106        );
3107
3108        // THEN
3109        for (i, expected_round) in expected_rounds.iter().enumerate() {
3110            let round = dag_state
3111                .get_last_cached_block_in_range(
3112                    context.committee.to_authority_index(i).unwrap(),
3113                    0,
3114                    end_round,
3115                )
3116                .map(|b| b.round())
3117                .unwrap_or_default();
3118            assert_eq!(round, *expected_round, "Authority {i}");
3119        }
3120
3121        // WHEN starting from round 2
3122        let start_round = 2;
3123        let expected_rounds = [0, 0, 2, 3];
3124
3125        // THEN
3126        for (i, expected_round) in expected_rounds.iter().enumerate() {
3127            let round = dag_state
3128                .get_last_cached_block_in_range(
3129                    context.committee.to_authority_index(i).unwrap(),
3130                    start_round,
3131                    end_round,
3132                )
3133                .map(|b| b.round())
3134                .unwrap_or_default();
3135            assert_eq!(round, *expected_round, "Authority {i}");
3136        }
3137
3138        // WHEN we flush the DagState - after adding a commit with all the blocks, we expect this to trigger
3139        // a clean up in the internal cache. That will keep the all the blocks with rounds >= authority_commit_round - CACHED_ROUND.
3140        //
3141        // When GC is enabled then we'll keep all the blocks that are > gc_round (2) and for those who don't have blocks > gc_round, we'll keep
3142        // all their highest round blocks for CACHED_ROUNDS.
3143        dag_state.flush();
3144
3145        // AND we request before round 3
3146        let end_round = 3;
3147        let expected_rounds = vec![0, 1, 2, 2];
3148
3149        // THEN
3150        let last_blocks = dag_state.get_last_cached_block_per_authority(end_round);
3151        assert_eq!(
3152            last_blocks.iter().map(|b| b.0.round()).collect::<Vec<_>>(),
3153            expected_rounds
3154        );
3155
3156        // THEN
3157        for (i, expected_round) in expected_rounds.iter().enumerate() {
3158            let round = dag_state
3159                .get_last_cached_block_in_range(
3160                    context.committee.to_authority_index(i).unwrap(),
3161                    0,
3162                    end_round,
3163                )
3164                .map(|b| b.round())
3165                .unwrap_or_default();
3166            assert_eq!(round, *expected_round, "Authority {i}");
3167        }
3168    }
3169
3170    #[tokio::test]
3171    #[should_panic(
3172        expected = "Attempted to request for blocks of rounds < 2, when the last evicted round is 1 for authority [2]"
3173    )]
3174    async fn test_get_cached_last_block_per_authority_requesting_out_of_round_range() {
3175        // GIVEN
3176        const CACHED_ROUNDS: Round = 1;
3177        const GC_DEPTH: u32 = 1;
3178        let (mut context, _) = Context::new_for_test(4);
3179        context.parameters.dag_state_cached_rounds = CACHED_ROUNDS;
3180        context.protocol_config.set_gc_depth_for_testing(GC_DEPTH);
3181
3182        let context = Arc::new(context);
3183        let store = Arc::new(MemStore::new());
3184        let mut dag_state = DagState::new(context.clone(), store.clone());
3185
3186        // Create no blocks for authority 0
3187        // Create one block (round 1) for authority 1
3188        // Create two blocks (rounds 1,2) for authority 2
3189        // Create three blocks (rounds 1,2,3) for authority 3
3190        let mut dag_builder = DagBuilder::new(context.clone());
3191        dag_builder
3192            .layers(1..=1)
3193            .authorities(vec![AuthorityIndex::new_for_test(0)])
3194            .skip_block()
3195            .build();
3196        dag_builder
3197            .layers(2..=2)
3198            .authorities(vec![
3199                AuthorityIndex::new_for_test(0),
3200                AuthorityIndex::new_for_test(1),
3201            ])
3202            .skip_block()
3203            .build();
3204        dag_builder
3205            .layers(3..=3)
3206            .authorities(vec![
3207                AuthorityIndex::new_for_test(0),
3208                AuthorityIndex::new_for_test(1),
3209                AuthorityIndex::new_for_test(2),
3210            ])
3211            .skip_block()
3212            .build();
3213
3214        // Accept all blocks
3215        for block in dag_builder.all_blocks() {
3216            dag_state.accept_block(block);
3217        }
3218
3219        dag_state.add_commit(TrustedCommit::new_for_test(
3220            1 as CommitIndex,
3221            CommitDigest::MIN,
3222            0,
3223            dag_builder.leader_block(3).unwrap().reference(),
3224            vec![],
3225        ));
3226
3227        // Flush the store so we update the evict rounds
3228        dag_state.flush();
3229
3230        // THEN the method should panic, as some authorities have already evicted rounds <= round 2
3231        dag_state.get_last_cached_block_per_authority(2);
3232    }
3233
3234    #[tokio::test]
3235    async fn test_last_quorum() {
3236        // GIVEN
3237        let (context, _) = Context::new_for_test(4);
3238        let context = Arc::new(context);
3239        let store = Arc::new(MemStore::new());
3240        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store.clone())));
3241
3242        // WHEN no blocks exist then genesis should be returned
3243        {
3244            let genesis = genesis_blocks(context.as_ref());
3245
3246            assert_eq!(dag_state.read().last_quorum(), genesis);
3247        }
3248
3249        // WHEN a fully connected DAG up to round 4 is created, then round 4 blocks should be returned as quorum
3250        {
3251            let mut dag_builder = DagBuilder::new(context.clone());
3252            dag_builder
3253                .layers(1..=4)
3254                .build()
3255                .persist_layers(dag_state.clone());
3256            let round_4_blocks: Vec<_> = dag_builder
3257                .blocks(4..=4)
3258                .into_iter()
3259                .map(|block| block.reference())
3260                .collect();
3261
3262            let last_quorum = dag_state.read().last_quorum();
3263
3264            assert_eq!(
3265                last_quorum
3266                    .into_iter()
3267                    .map(|block| block.reference())
3268                    .collect::<Vec<_>>(),
3269                round_4_blocks
3270            );
3271        }
3272
3273        // WHEN adding one more block at round 5, still round 4 should be returned as quorum
3274        {
3275            let block = VerifiedBlock::new_for_test(TestBlock::new(5, 0).build());
3276            dag_state.write().accept_block(block);
3277
3278            let round_4_blocks = dag_state.read().get_uncommitted_blocks_at_round(4);
3279
3280            let last_quorum = dag_state.read().last_quorum();
3281
3282            assert_eq!(last_quorum, round_4_blocks);
3283        }
3284    }
3285
3286    #[tokio::test]
3287    async fn test_last_block_for_authority() {
3288        // GIVEN
3289        let (context, _) = Context::new_for_test(4);
3290        let context = Arc::new(context);
3291        let store = Arc::new(MemStore::new());
3292        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store.clone())));
3293
3294        // WHEN no blocks exist then genesis should be returned
3295        {
3296            let genesis = genesis_blocks(context.as_ref());
3297            let my_genesis = genesis
3298                .into_iter()
3299                .find(|block| block.author() == context.own_index)
3300                .unwrap();
3301
3302            assert_eq!(dag_state.read().get_last_proposed_block(), Some(my_genesis));
3303        }
3304
3305        // WHEN adding some blocks for authorities, only the last ones should be returned
3306        {
3307            // add blocks up to round 4
3308            let mut dag_builder = DagBuilder::new(context.clone());
3309            dag_builder
3310                .layers(1..=4)
3311                .build()
3312                .persist_layers(dag_state.clone());
3313
3314            // add block 5 for authority 0
3315            let block = VerifiedBlock::new_for_test(TestBlock::new(5, 0).build());
3316            dag_state.write().accept_block(block);
3317
3318            let block = dag_state
3319                .read()
3320                .get_last_block_for_authority(AuthorityIndex::new_for_test(0));
3321            assert_eq!(block.round(), 5);
3322
3323            for (authority_index, _) in context.committee.authorities() {
3324                let block = dag_state
3325                    .read()
3326                    .get_last_block_for_authority(authority_index);
3327
3328                if authority_index.value() == 0 {
3329                    assert_eq!(block.round(), 5);
3330                } else {
3331                    assert_eq!(block.round(), 4);
3332                }
3333            }
3334        }
3335    }
3336
3337    #[tokio::test]
3338    async fn test_accept_block_not_panics_when_timestamp_is_ahead_and_median_timestamp() {
3339        // GIVEN
3340        let (context, _) = Context::new_for_test(4);
3341        let context = Arc::new(context);
3342        let store = Arc::new(MemStore::new());
3343        let mut dag_state = DagState::new(context.clone(), store.clone());
3344
3345        // Set a timestamp for the block that is ahead of the current time
3346        let block_timestamp = context.clock.timestamp_utc_ms() + 5_000;
3347
3348        let block = VerifiedBlock::new_for_test(
3349            TestBlock::new(10, 0)
3350                .set_timestamp_ms(block_timestamp)
3351                .build(),
3352        );
3353
3354        // Try to accept the block - it should not panic
3355        dag_state.accept_block(block);
3356    }
3357
3358    #[tokio::test]
3359    async fn test_last_finalized_commit() {
3360        // GIVEN
3361        let (context, _) = Context::new_for_test(4);
3362        let context = Arc::new(context);
3363        let store = Arc::new(MemStore::new());
3364        let mut dag_state = DagState::new(context.clone(), store.clone());
3365
3366        // WHEN adding a finalized commit
3367        let commit_ref = CommitRef::new(1, CommitDigest::MIN);
3368        let rejected_transactions = BTreeMap::new();
3369        dag_state.add_finalized_commit(commit_ref, rejected_transactions.clone());
3370
3371        // THEN the commit should be added to the buffer
3372        assert_eq!(dag_state.finalized_commits_to_write.len(), 1);
3373        assert_eq!(
3374            dag_state.finalized_commits_to_write[0],
3375            (commit_ref, rejected_transactions.clone())
3376        );
3377
3378        // WHEN flushing the DAG state
3379        dag_state.flush();
3380
3381        // THEN the commit and rejected transactions should be written to storage
3382        let last_finalized_commit = store.read_last_finalized_commit().unwrap();
3383        assert_eq!(last_finalized_commit, Some(commit_ref));
3384        let stored_rejected_transactions = store
3385            .read_rejected_transactions(commit_ref)
3386            .unwrap()
3387            .unwrap();
3388        assert_eq!(stored_rejected_transactions, rejected_transactions);
3389    }
3390}