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