Skip to main content

consensus_core/
core.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::{BTreeMap, BTreeSet},
6    sync::Arc,
7    vec,
8};
9
10use consensus_config::ProtocolKeyPair;
11#[cfg(test)]
12use consensus_config::{AuthorityIndex, Stake, local_committee_and_keys};
13use consensus_types::block::{BlockRef, Round};
14use itertools::Itertools as _;
15#[cfg(test)]
16use mysten_metrics::monitored_mpsc::UnboundedReceiver;
17use mysten_metrics::monitored_scope;
18use parking_lot::RwLock;
19use sui_macros::fail_point;
20use tokio::sync::{broadcast, watch};
21use tracing::{debug, info, trace, warn};
22
23#[cfg(test)]
24use crate::{
25    CommitConsumerArgs, TransactionClient,
26    block_verifier::NoopBlockVerifier,
27    storage::{Store, WriteBatch, mem_store::MemStore},
28    transaction::{TransactionConsumer, TransactionConsumerPool},
29};
30use crate::{
31    ancestor::AncestorStateManager,
32    block::{BlockAPI, ExtendedBlock, GENESIS_ROUND, Slot, VerifiedBlock},
33    block_manager::BlockManager,
34    commit::{
35        CertifiedCommit, CertifiedCommits, CommitAPI, CommittedSubDag, DecidedLeader, Decision,
36        TrustedCommit,
37    },
38    commit_observer::CommitObserver,
39    context::Context,
40    dag_state::DagState,
41    error::{ConsensusError, ConsensusResult},
42    flex_committer::FlexCommitter,
43    leader_schedule::LeaderSchedule,
44    leader_schedule_v3::LeaderScheduleV3,
45    leader_scoring::ReputationScores,
46    proposer::{ProposalLeaderWaiter, Proposer, ValidatorProposer},
47    round_tracker::RoundTracker,
48    transaction::TransactionPool,
49    transaction_vote_tracker::TransactionVoteTracker,
50    universal_committer::{
51        UniversalCommitter, universal_committer_builder::UniversalCommitterBuilder,
52    },
53};
54
55pub(crate) struct Core {
56    context: Arc<Context>,
57    /// The block manager which is responsible for keeping track of the DAG dependencies when processing new blocks
58    /// and accept them or suspend if we are missing their causal history
59    block_manager: BlockManager,
60    /// Used to make commit decisions for leader blocks in the dag.
61    committer: Arc<UniversalCommitter>,
62    /// The last new round for which core has sent out a signal.
63    last_signaled_round: Round,
64    /// The last decided leader returned from the universal committer. Important to note
65    /// that this does not signify that the leader has been persisted yet as it still has
66    /// to go through CommitObserver and persist the commit in store. On recovery/restart
67    /// the last_decided_leader will be set to the last_commit leader in dag state.
68    last_decided_leader: Slot,
69    /// The consensus leader schedule to be used to resolve the leader for a
70    /// given round.
71    leader_schedule: Arc<LeaderSchedule>,
72    /// Scores validators using the DAG and schedules leader for next commit, in a sliding-window.
73    leader_schedule_v3: Option<LeaderScheduleV3>,
74    /// The commit observer is responsible for observing the commits and collecting
75    /// + sending subdags over the consensus output channel.
76    commit_observer: CommitObserver,
77    /// The v3 commit pipeline helper. Set when `enable_v3` is on. Owns both
78    /// the local decision rule via `try_commit` and the certified-commit
79    /// sub-dag construction via `handle_certified_commit`.
80    flex_committer: Option<FlexCommitter>,
81    /// Sender of outgoing signals from Core.
82    signals: CoreSignals,
83    /// Keeping track of state of the DAG, including blocks, commits and last committed rounds.
84    dag_state: Arc<RwLock<DagState>>,
85    /// Block proposal engine for Validator nodes only.
86    /// Validators have a proposer to create blocks, Observers have None (they only receive blocks).
87    proposer: Option<Box<dyn Proposer>>,
88}
89
90impl Core {
91    /// Creates a new Core instance for a validator node that participates in consensus.
92    pub(crate) fn new_validator(
93        context: Arc<Context>,
94        leader_schedule: Arc<LeaderSchedule>,
95        transaction_pool: Arc<dyn TransactionPool>,
96        transaction_vote_tracker: TransactionVoteTracker,
97        block_manager: BlockManager,
98        commit_observer: CommitObserver,
99        signals: CoreSignals,
100        block_signer: ProtocolKeyPair,
101        dag_state: Arc<RwLock<DagState>>,
102        sync_last_known_own_block: bool,
103        round_tracker: Arc<RwLock<RoundTracker>>,
104    ) -> Self {
105        let last_decided_leader = dag_state.read().last_commit_leader();
106        let number_of_leaders = context.protocol_config.num_leaders_per_round().unwrap_or(1);
107
108        let leader_schedule_v3 = if context.protocol_config.enable_v3() {
109            Some(LeaderScheduleV3::from_store(
110                context.clone(),
111                dag_state.clone(),
112            ))
113        } else {
114            None
115        };
116
117        let flex_committer = if context.protocol_config.enable_v3() {
118            Some(FlexCommitter::new(context.clone(), dag_state.clone()))
119        } else {
120            None
121        };
122
123        let committer = Arc::new(
124            UniversalCommitterBuilder::new(
125                context.clone(),
126                leader_schedule.clone(),
127                dag_state.clone(),
128            )
129            .with_number_of_leaders(number_of_leaders)
130            .with_pipeline(true)
131            .build(),
132        );
133
134        let last_proposed_block = dag_state
135            .read()
136            .get_last_proposed_block()
137            .expect("A block should have been returned");
138        let last_signaled_round = last_proposed_block.round();
139
140        // Recover the last included ancestor rounds based on the last proposed block. That will allow
141        // to perform the next block proposal by using ancestor blocks of higher rounds and avoid
142        // re-including blocks that have been already included in the last (or earlier) block proposal.
143        // This is only strongly guaranteed for a quorum of ancestors. It is still possible to re-include
144        // a block from an authority which hadn't been added as part of the last proposal hence its
145        // latest included ancestor is not accurately captured here. This is considered a small deficiency,
146        // and it mostly matters just for this next proposal without any actual penalties in performance
147        // or block proposal.
148        let mut last_included_ancestors = vec![None; context.committee.size()];
149        for ancestor in last_proposed_block.ancestors() {
150            last_included_ancestors[ancestor.author] = Some(*ancestor);
151        }
152
153        let last_known_proposed_round = if sync_last_known_own_block {
154            None
155        } else {
156            // if the sync is disabled then we practically don't want to impose any restriction.
157            Some(0)
158        };
159
160        let ancestor_state_manager = AncestorStateManager::new(context.clone(), dag_state.clone());
161
162        // Create the ValidatorProposer.
163        let leader_waiter = if let Some(schedule) = leader_schedule_v3.as_ref() {
164            let next_commit_leader_schedule = schedule.next_commit_leader_schedule();
165            info!(
166                "Recovered next commit leaders: index={} min_round={} num={} allowed={:?}",
167                next_commit_leader_schedule.next_commit_index,
168                next_commit_leader_schedule.min_next_leader_round,
169                next_commit_leader_schedule.num_leaders(),
170                next_commit_leader_schedule.allowed_leaders,
171            );
172            ProposalLeaderWaiter::V3(next_commit_leader_schedule)
173        } else {
174            ProposalLeaderWaiter::V2(committer.clone())
175        };
176        let proposer = Some(Box::new(ValidatorProposer::new(
177            dag_state.clone(),
178            context.clone(),
179            transaction_pool,
180            transaction_vote_tracker.clone(),
181            block_signer,
182            last_known_proposed_round,
183            ancestor_state_manager,
184            round_tracker.clone(),
185            leader_waiter,
186        )) as Box<dyn Proposer>);
187
188        let mut core = Self {
189            context,
190            last_signaled_round,
191            last_decided_leader,
192            leader_schedule,
193            leader_schedule_v3,
194            block_manager,
195            committer,
196            commit_observer,
197            flex_committer,
198            signals,
199            dag_state,
200            proposer,
201        };
202
203        // Initialize propagation scores for the proposer before recovery.
204        let propagation_scores = core.current_reputation_scores();
205        core.proposer
206            .as_mut()
207            .unwrap()
208            .set_propagation_scores(propagation_scores);
209
210        core.recover_validator()
211    }
212
213    /// Creates a new Core instance for an observer node that only processes blocks.
214    pub(crate) fn new_observer(
215        context: Arc<Context>,
216        leader_schedule: Arc<LeaderSchedule>,
217        block_manager: BlockManager,
218        commit_observer: CommitObserver,
219        signals: CoreSignals,
220        dag_state: Arc<RwLock<DagState>>,
221    ) -> Self {
222        let last_decided_leader = dag_state.read().last_commit_leader();
223        let number_of_leaders = context.protocol_config.num_leaders_per_round().unwrap_or(1);
224
225        let leader_schedule_v3 = if context.protocol_config.enable_v3() {
226            Some(LeaderScheduleV3::from_store(
227                context.clone(),
228                dag_state.clone(),
229            ))
230        } else {
231            None
232        };
233
234        let flex_committer = if context.protocol_config.enable_v3() {
235            Some(FlexCommitter::new(context.clone(), dag_state.clone()))
236        } else {
237            None
238        };
239
240        let committer = Arc::new(
241            UniversalCommitterBuilder::new(
242                context.clone(),
243                leader_schedule.clone(),
244                dag_state.clone(),
245            )
246            .with_number_of_leaders(number_of_leaders)
247            .with_pipeline(true)
248            .build(),
249        );
250
251        // For the Observer nodes let's consider the last signaled round as the latest threshold clock round.
252        let last_signaled_round = dag_state.read().threshold_clock_round();
253
254        Self {
255            context,
256            last_signaled_round,
257            last_decided_leader,
258            leader_schedule,
259            leader_schedule_v3,
260            block_manager,
261            committer,
262            commit_observer,
263            flex_committer,
264            signals,
265            dag_state,
266            proposer: None,
267        }
268        .recover_observer()
269    }
270
271    fn recover_validator(mut self) -> Self {
272        let _s = self
273            .context
274            .metrics
275            .node_metrics
276            .scope_processing_time
277            .with_label_values(&["Core::recover_validator"])
278            .start_timer();
279
280        // Try to commit and propose, since they may not have run after the last storage write.
281        self.try_commit_local().unwrap();
282
283        let last_proposed_block = if let Some(last_proposed_block) = self.try_propose(true).unwrap()
284        {
285            last_proposed_block
286        } else {
287            let proposer = self
288                .proposer
289                .as_ref()
290                .expect("Validator must have proposer");
291            let last_proposed_block = proposer.last_proposed_block();
292
293            if proposer.should_propose() {
294                assert!(
295                    last_proposed_block.round() > GENESIS_ROUND,
296                    "At minimum a block of round higher than genesis should have been produced during recovery"
297                );
298            }
299
300            // if no new block proposed then just re-broadcast the last proposed one to ensure liveness.
301            self.signals
302                .new_block(ExtendedBlock {
303                    block: last_proposed_block.clone(),
304                    excluded_ancestors: vec![],
305                })
306                .unwrap();
307            last_proposed_block
308        };
309
310        // Try to set up leader timeout if needed.
311        // This needs to be called after try_commit() and try_propose(), which may
312        // have advanced the threshold clock round.
313        self.try_signal_new_round();
314
315        info!(
316            "Core recovery for validator completed with last proposed block {:?}",
317            last_proposed_block
318        );
319
320        self
321    }
322
323    fn recover_observer(mut self) -> Self {
324        let _s = self
325            .context
326            .metrics
327            .node_metrics
328            .scope_processing_time
329            .with_label_values(&["Core::recover_observer"])
330            .start_timer();
331
332        // Try to commit, since they may not have run after the last storage write.
333        self.try_commit_local().unwrap();
334
335        self.try_signal_new_round();
336
337        self
338    }
339
340    pub(crate) async fn stop(&mut self) {
341        self.commit_observer.stop().await;
342    }
343
344    /// Calls `BlockManager::try_accept_blocks` and broadcasts each accepted block to any active
345    /// observer subscribers. Returns the accepted blocks alongside any missing block refs.
346    fn accept_blocks(
347        &mut self,
348        blocks: Vec<VerifiedBlock>,
349    ) -> (Vec<VerifiedBlock>, BTreeSet<BlockRef>) {
350        let (accepted_blocks, missing_block_refs) = self.block_manager.try_accept_blocks(blocks);
351        for block in &accepted_blocks {
352            tracing::trace!(
353                "{} Core accepted round {}, author {} at timestamp: {}",
354                self.context.own_index,
355                block.round(),
356                block.author(),
357                block.timestamp_ms()
358            );
359            self.signals.new_accepted_block(block.clone());
360        }
361        (accepted_blocks, missing_block_refs)
362    }
363
364    /// Calls `BlockManager::try_accept_committed_blocks` and broadcasts each accepted block to any
365    /// active observer subscribers. Returns all accepted blocks.
366    fn accept_committed_blocks(&mut self, blocks: Vec<VerifiedBlock>) -> Vec<VerifiedBlock> {
367        let accepted_blocks = self.block_manager.try_accept_committed_blocks(blocks);
368        for block in &accepted_blocks {
369            self.signals.new_accepted_block(block.clone());
370        }
371        #[cfg(test)]
372        sui_macros::handle_fail_point("consensus-after-accept-certified-blocks");
373        accepted_blocks
374    }
375
376    /// Processes the provided blocks and accepts them if possible when their causal history exists.
377    /// The method returns:
378    /// - The references of ancestors missing their block
379    #[tracing::instrument(skip_all)]
380    pub(crate) fn add_blocks(
381        &mut self,
382        blocks: Vec<VerifiedBlock>,
383    ) -> ConsensusResult<BTreeSet<BlockRef>> {
384        let _scope = monitored_scope("Core::add_blocks");
385        let _s = self
386            .context
387            .metrics
388            .node_metrics
389            .scope_processing_time
390            .with_label_values(&["Core::add_blocks"])
391            .start_timer();
392        self.context
393            .metrics
394            .node_metrics
395            .core_add_blocks_batch_size
396            .observe(blocks.len() as f64);
397
398        let (accepted_blocks, missing_block_refs) = self.accept_blocks(blocks);
399
400        if !accepted_blocks.is_empty() {
401            trace!(
402                "Accepted blocks: {}",
403                accepted_blocks
404                    .iter()
405                    .map(|b| b.reference().to_string())
406                    .join(",")
407            );
408
409            // Try to commit the new blocks if possible.
410            self.try_commit_local()?;
411
412            // Try to propose now since there are new blocks accepted.
413            self.try_propose(false)?;
414
415            // Now set up leader timeout if needed.
416            // This needs to be called after try_commit() and try_propose(), which may
417            // have advanced the threshold clock round.
418            self.try_signal_new_round();
419        };
420
421        if !missing_block_refs.is_empty() {
422            trace!(
423                "Missing block refs: {}",
424                missing_block_refs.iter().map(|b| b.to_string()).join(", ")
425            );
426        }
427
428        Ok(missing_block_refs)
429    }
430
431    /// Adds certified commits synced from peers via the commit syncer. Local commit rule is
432    /// skipped and the corresponding leaders and sub dags are committed directly. Blocks of
433    /// the certified commits themselves are accepted inside `try_commit()` or
434    /// `process_certified_commits()`.
435    #[tracing::instrument(skip_all)]
436    pub(crate) fn add_certified_commits(
437        &mut self,
438        certified_commits: CertifiedCommits,
439    ) -> ConsensusResult<BTreeSet<BlockRef>> {
440        let _scope = monitored_scope("Core::add_certified_commits");
441
442        let votes = certified_commits.votes().to_vec();
443        let commits = self
444            .filter_new_commits(certified_commits.commits().to_vec())
445            .expect("Certified commits validation failed");
446
447        // Try to accept the certified commit votes.
448        // Even if they may not be part of a future commit, these blocks are useful for certifying
449        // commits when helping peers sync commits.
450        let (_, missing_block_refs) = self.accept_blocks(votes);
451
452        if self.context.protocol_config.enable_v3() {
453            self.process_certified_commits(commits)?;
454            self.try_commit_v3()?;
455        } else {
456            self.try_commit(commits)?;
457        }
458
459        // Try to propose now since there are new blocks accepted.
460        self.try_propose(false)?;
461
462        // Now set up leader timeout if needed.
463        // This needs to be called after try_commit() and try_propose(), which may
464        // have advanced the threshold clock round.
465        self.try_signal_new_round();
466
467        Ok(missing_block_refs)
468    }
469
470    /// Checks if provided block refs have been accepted. If not, missing block refs are kept for synchronizations.
471    /// Returns the references of missing blocks among the input blocks.
472    pub(crate) fn check_block_refs(
473        &mut self,
474        block_refs: Vec<BlockRef>,
475    ) -> ConsensusResult<BTreeSet<BlockRef>> {
476        let _scope = monitored_scope("Core::check_block_refs");
477        let _s = self
478            .context
479            .metrics
480            .node_metrics
481            .scope_processing_time
482            .with_label_values(&["Core::check_block_refs"])
483            .start_timer();
484        self.context
485            .metrics
486            .node_metrics
487            .core_check_block_refs_batch_size
488            .observe(block_refs.len() as f64);
489
490        // Try to find them via the block manager
491        let missing_block_refs = self.block_manager.try_find_blocks(block_refs);
492
493        if !missing_block_refs.is_empty() {
494            trace!(
495                "Missing block refs: {}",
496                missing_block_refs.iter().map(|b| b.to_string()).join(", ")
497            );
498        }
499        Ok(missing_block_refs)
500    }
501
502    /// If needed, signals a new clock round and sets up leader timeout.
503    fn try_signal_new_round(&mut self) {
504        // Signal only when the threshold clock round is more advanced than the last signaled round.
505        //
506        // NOTE: a signal is still sent even when a block has been proposed at the new round.
507        // We can consider changing this in the future.
508        let new_clock_round = self.dag_state.read().threshold_clock_round();
509        if new_clock_round <= self.last_signaled_round {
510            return;
511        }
512        // Then send a signal to set up leader timeout.
513        self.signals.new_round(new_clock_round);
514        self.last_signaled_round = new_clock_round;
515
516        // Report the threshold clock round
517        self.context
518            .metrics
519            .node_metrics
520            .threshold_clock_round
521            .set(new_clock_round as i64);
522    }
523
524    /// Creating a new block for the dictated round. This is used when a leader timeout occurs, either
525    /// when the min timeout expires or max. When `force = true` , then any checks like previous round
526    /// leader existence will get skipped.
527    pub(crate) fn new_block(
528        &mut self,
529        round: Round,
530        force: bool,
531    ) -> ConsensusResult<Option<VerifiedBlock>> {
532        let _scope = monitored_scope("Core::new_block");
533        if let Some(last_round) = self.last_proposed_round()
534            && last_round < round
535        {
536            self.context
537                .metrics
538                .node_metrics
539                .leader_timeout_total
540                .with_label_values(&[&format!("{force}")])
541                .inc();
542            let result = self.try_propose(force);
543            // The threshold clock round may have advanced, so a signal needs to be sent.
544            self.try_signal_new_round();
545            return result;
546        }
547        Ok(None)
548    }
549
550    /// Keeps only the certified commits that have a commit index > last commit index.
551    /// It also ensures that the first commit in the list is the next one in line, otherwise it panics.
552    fn filter_new_commits(
553        &mut self,
554        commits: Vec<CertifiedCommit>,
555    ) -> ConsensusResult<Vec<CertifiedCommit>> {
556        // Filter out the commits that have been already locally committed and keep only anything that is above the last committed index.
557        let last_commit_index = self.dag_state.read().last_commit_index();
558        let commits = commits
559            .iter()
560            .filter(|commit| {
561                if commit.index() > last_commit_index {
562                    true
563                } else {
564                    self.context
565                        .metrics
566                        .node_metrics
567                        .core_certified_commits_processed
568                        .with_label_values(&["skipped"])
569                        .inc();
570                    tracing::debug!(
571                        "Skip commit for index {} as it is already committed with last commit index {}",
572                        commit.index(),
573                        last_commit_index
574                    );
575                    false
576                }
577            })
578            .cloned()
579            .collect::<Vec<_>>();
580
581        // Make sure that the first commit we find is the next one in line and there is no gap.
582        if let Some(commit) = commits.first()
583            && commit.index() != last_commit_index + 1
584        {
585            return Err(ConsensusError::UnexpectedCertifiedCommitIndex {
586                expected_commit_index: last_commit_index + 1,
587                commit_index: commit.index(),
588            });
589        }
590
591        Ok(commits)
592    }
593
594    // Attempts to create a new block, persist and propose it to all peers.
595    // When force is true, ignore if leader from the last round exists among ancestors and if
596    // the minimum round delay has passed.
597    fn try_propose(&mut self, force: bool) -> ConsensusResult<Option<VerifiedBlock>> {
598        if let Some(proposer) = &mut self.proposer
599            && let Some(extended_block) = proposer.try_new_block(force)
600        {
601            self.signals.new_block(extended_block.clone())?;
602            self.signals
603                .new_accepted_block(extended_block.block.clone());
604
605            fail_point!("consensus-after-propose");
606
607            // The new block may help commit.
608            self.try_commit_local()?;
609            return Ok(Some(extended_block.block));
610        }
611        Ok(None)
612    }
613
614    /// Runs commit rule to attempt to commit additional blocks from the DAG. If any `certified_commits` are provided, then
615    /// it will attempt to commit those first before trying to commit any further leaders.
616    fn try_commit(
617        &mut self,
618        mut certified_commits: Vec<CertifiedCommit>,
619    ) -> ConsensusResult<Vec<CommittedSubDag>> {
620        let _s = self
621            .context
622            .metrics
623            .node_metrics
624            .scope_processing_time
625            .with_label_values(&["Core::try_commit"])
626            .start_timer();
627
628        let mut certified_commits_map = BTreeMap::new();
629        for c in &certified_commits {
630            certified_commits_map.insert(c.index(), c.reference());
631        }
632
633        if !certified_commits.is_empty() {
634            info!(
635                "Processing synced commits: {:?}",
636                certified_commits
637                    .iter()
638                    .map(|c| (c.index(), c.leader()))
639                    .collect::<Vec<_>>()
640            );
641        }
642
643        let mut committed_sub_dags = Vec::new();
644        // TODO: Add optimization to abort early without quorum for a round.
645        loop {
646            // LeaderSchedule has a limit to how many sequenced leaders can be committed
647            // before a change is triggered. Calling into leader schedule will get you
648            // how many commits till next leader change. We will loop back and recalculate
649            // any discarded leaders with the new schedule.
650            let mut commits_until_update = self
651                .leader_schedule
652                .commits_until_leader_schedule_update(self.dag_state.clone());
653
654            if commits_until_update == 0 {
655                let last_commit_index = self.dag_state.read().last_commit_index();
656
657                tracing::info!(
658                    "Leader schedule change triggered at commit index {last_commit_index}"
659                );
660
661                self.leader_schedule
662                    .update_leader_schedule_v2(&self.dag_state);
663
664                let propagation_scores = self.current_reputation_scores();
665                if let Some(proposer) = &mut self.proposer {
666                    proposer.set_propagation_scores(propagation_scores);
667                }
668
669                commits_until_update = self
670                    .leader_schedule
671                    .commits_until_leader_schedule_update(self.dag_state.clone());
672
673                fail_point!("consensus-after-leader-schedule-change");
674            }
675            assert!(commits_until_update > 0);
676
677            // If there are certified commits to process, find out which leaders and commits from them
678            // are decided and use them as the next commits.
679            let (certified_leaders, decided_certified_commits): (
680                Vec<DecidedLeader>,
681                Vec<CertifiedCommit>,
682            ) = self
683                .try_select_certified_leaders(&mut certified_commits, commits_until_update)
684                .into_iter()
685                .unzip();
686
687            // Selected certified leaders are guaranteed to be sequenced below,
688            // so count them as committed here, keeping this metric consistent
689            // with the v3 path (`process_certified_commits`).
690            if !decided_certified_commits.is_empty() {
691                self.context
692                    .metrics
693                    .node_metrics
694                    .core_certified_commits_processed
695                    .with_label_values(&["committed"])
696                    .inc_by(decided_certified_commits.len() as u64);
697            }
698
699            // If there is no certified commit to process, run the decision rule.
700            let (decided_leaders, local) = if certified_leaders.is_empty() {
701                // TODO: limit commits by commits_until_update for efficiency, which may be needed when leader schedule length is reduced.
702                let mut decided_leaders = self.committer.try_decide(self.last_decided_leader);
703                // Truncate the decided leaders to fit the commit schedule limit.
704                if decided_leaders.len() >= commits_until_update {
705                    let _ = decided_leaders.split_off(commits_until_update);
706                }
707                (decided_leaders, true)
708            } else {
709                (certified_leaders, false)
710            };
711
712            // If the decided leaders list is empty then just break the loop.
713            let Some(last_decided) = decided_leaders.last().cloned() else {
714                break;
715            };
716
717            self.last_decided_leader = last_decided.slot();
718            self.context
719                .metrics
720                .node_metrics
721                .last_decided_leader_round
722                .set(self.last_decided_leader.round as i64);
723
724            let sequenced_leaders = decided_leaders
725                .into_iter()
726                .filter_map(|leader| leader.into_committed_block())
727                .collect::<Vec<_>>();
728            // It's possible to reach this point as the decided leaders might all of them be "Skip" decisions. In this case there is no
729            // leader to commit and we should break the loop.
730            if sequenced_leaders.is_empty() {
731                break;
732            }
733            tracing::info!(
734                "Committing {} leaders: {}; {} commits before next leader schedule change",
735                sequenced_leaders.len(),
736                sequenced_leaders
737                    .iter()
738                    .map(|b| b.reference().to_string())
739                    .join(","),
740                commits_until_update,
741            );
742
743            // TODO: refcount subdags
744            let subdags = if local {
745                self.commit_observer
746                    .handle_committed_leaders(sequenced_leaders, true)?
747            } else {
748                let mut subdags = Vec::with_capacity(sequenced_leaders.len());
749                for (leader, certified_commit) in sequenced_leaders
750                    .into_iter()
751                    .zip_eq(decided_certified_commits)
752                {
753                    // A certified subdag can omit ancestors at or below the preceding commit's
754                    // GC round. Insert that commit before accepting the next subdag, so a
755                    // concurrent finalizer flush cannot persist blocks without their GC boundary.
756                    self.accept_committed_blocks(certified_commit.blocks().to_vec());
757                    subdags.extend(
758                        self.commit_observer
759                            .handle_committed_leaders(vec![leader], false)?,
760                    );
761                }
762                subdags
763            };
764
765            // Try to unsuspend blocks if gc_round has advanced.
766            self.block_manager
767                .try_unsuspend_blocks_for_latest_gc_round();
768
769            committed_sub_dags.extend(subdags);
770
771            fail_point!("consensus-after-handle-commit");
772        }
773
774        // Sanity check: for commits that have been linearized using the certified commits, ensure that the same sub dag has been committed.
775        for sub_dag in &committed_sub_dags {
776            if let Some(commit_ref) = certified_commits_map.remove(&sub_dag.commit_ref.index) {
777                assert_eq!(
778                    commit_ref, sub_dag.commit_ref,
779                    "Certified commit has different reference than the committed sub dag"
780                );
781            }
782        }
783
784        // Notify about our own committed blocks
785        let committed_block_refs = committed_sub_dags
786            .iter()
787            .flat_map(|sub_dag| sub_dag.blocks.iter())
788            .filter_map(|block| {
789                (block.author() == self.context.own_index).then_some(block.reference())
790            })
791            .collect::<Vec<_>>();
792        if let Some(proposer) = &self.proposer {
793            proposer.notify_own_blocks_committed(
794                committed_block_refs,
795                self.dag_state.read().gc_round(),
796            );
797        }
798
799        Ok(committed_sub_dags)
800    }
801
802    // Processes certified commits that have been synced from peers. Each commit
803    // is already quorum-certified, so the decision rule is skipped and the
804    // corresponding leaders and sub dags are committed directly. Blocks for the
805    // decided certified commits are accepted inside this function to avoid
806    // flushing blocks that belong to certified commits which end up not being
807    // linearized (see the comment on `accept_committed_blocks` below).
808    fn process_certified_commits(
809        &mut self,
810        certified_commits: Vec<CertifiedCommit>,
811    ) -> ConsensusResult<Vec<CommittedSubDag>> {
812        if certified_commits.is_empty() {
813            return Ok(vec![]);
814        }
815
816        let _s = self
817            .context
818            .metrics
819            .node_metrics
820            .scope_processing_time
821            .with_label_values(&["Core::process_certified_commits"])
822            .start_timer();
823        info!(
824            "Processing certified commits: {:?}",
825            certified_commits
826                .iter()
827                .map(|c| (c.index(), c.leader()))
828                .collect::<Vec<_>>()
829        );
830
831        let num_certified = certified_commits.len();
832        let mut subdags = Vec::with_capacity(num_certified);
833        for cert in certified_commits {
834            // Accept blocks belonging to this commit before building its sub dag — the
835            // sub dag is reconstructed by reading the commit's blocks from DagState.
836            // Accepting per commit (rather than up front) keeps the read-after-write
837            // coupling local and obvious.
838            self.accept_committed_blocks(cert.blocks().to_vec());
839
840            let commit: TrustedCommit = (*cert).clone();
841
842            // A certified commit must extend the local commit chain. A mismatch means
843            // local commits have diverged from the quorum-certified chain, which
844            // should be surfaced immediately instead of corrupting the commit chain.
845            assert_eq!(
846                commit.previous_digest(),
847                self.dag_state.read().last_commit_digest(),
848                "Certified commit {} does not chain onto the last local commit",
849                commit.reference(),
850            );
851
852            // Certified commits are not decided locally — mark blocks
853            // committed and build the sub-dag in one shot.
854            let subdag = self
855                .flex_committer
856                .as_ref()
857                .expect("FlexCommitter must be initialized on the v3 path")
858                .handle_certified_commit(&commit);
859
860            self.post_commit(commit, subdag.clone())?;
861            subdags.push(subdag);
862
863            fail_point!("consensus-after-handle-commit");
864        }
865
866        self.context
867            .metrics
868            .node_metrics
869            .core_certified_commits_processed
870            .with_label_values(&["committed"])
871            .inc_by(num_certified as u64);
872
873        Ok(subdags)
874    }
875
876    /// Runs the local commit decision rule on the path selected by `enable_v3`,
877    /// without processing any certified commits.
878    fn try_commit_local(&mut self) -> ConsensusResult<Vec<CommittedSubDag>> {
879        if self.context.protocol_config.enable_v3() {
880            self.try_commit_v3()
881        } else {
882            self.try_commit(vec![])
883        }
884    }
885
886    // Runs the local commit decision rule on the current DAG and linearizes any
887    // newly-decided sub dags. Does not process certified commits — see
888    // `Self::process_certified_commits` for that path. Used when
889    // `ConsensusProtocolConfig::enable_v3()` is enabled; otherwise the legacy
890    // `try_commit` is used.
891    fn try_commit_v3(&mut self) -> ConsensusResult<Vec<CommittedSubDag>> {
892        // Deliberately reuses the legacy `Core::try_commit` label: only one of the
893        // two paths is active for a given protocol config, and sharing the label
894        // keeps dashboards working across the transition.
895        let _s = self
896            .context
897            .metrics
898            .node_metrics
899            .scope_processing_time
900            .with_label_values(&["Core::try_commit"])
901            .start_timer();
902
903        let mut committed_sub_dags = Vec::new();
904        loop {
905            let next_commit_leaders = self
906                .leader_schedule_v3
907                .as_ref()
908                .expect("LeaderScheduleV3 must be initialized on the v3 path")
909                .next_commit_leader_schedule();
910            let Some((commit, subdag)) = self
911                .flex_committer
912                .as_mut()
913                .expect("FlexCommitter must be initialized on the v3 path")
914                .try_commit(next_commit_leaders)
915            else {
916                break;
917            };
918
919            self.post_commit(commit, subdag.clone())?;
920            committed_sub_dags.push(subdag);
921
922            fail_point!("consensus-after-handle-commit");
923        }
924
925        Ok(committed_sub_dags)
926    }
927
928    // Post-processing for a single committed subdag on the v3 path: persists the
929    // commit, forwards to the finalizer, updates bookkeeping, unsuspends blocks,
930    // notifies the proposer about own blocks, and feeds v3 leader
931    // scoring.
932    fn post_commit(
933        &mut self,
934        commit: TrustedCommit,
935        subdag: CommittedSubDag,
936    ) -> ConsensusResult<()> {
937        self.dag_state.write().add_commit(commit);
938
939        self.commit_observer.report_commit_metrics(&subdag);
940        self.commit_observer.send_to_finalizer(subdag.clone())?;
941
942        self.last_decided_leader = subdag.leader.into();
943        self.context
944            .metrics
945            .node_metrics
946            .last_decided_leader_round
947            .set(self.last_decided_leader.round as i64);
948
949        self.block_manager
950            .try_unsuspend_blocks_for_latest_gc_round();
951
952        let committed_block_refs = subdag
953            .blocks
954            .iter()
955            .filter_map(|block| {
956                (block.author() == self.context.own_index).then_some(block.reference())
957            })
958            .collect::<Vec<_>>();
959        if let Some(proposer) = &self.proposer {
960            proposer.notify_own_blocks_committed(
961                committed_block_refs,
962                self.dag_state.read().gc_round(),
963            );
964        }
965
966        if let Some(schedule) = self.leader_schedule_v3.as_mut() {
967            schedule.add_commit(subdag);
968            let next = schedule.next_commit_leader_schedule();
969            debug!(
970                "Next v3 commit leaders: index={} min_round={} num={} allowed={:?}",
971                next.next_commit_index,
972                next.min_next_leader_round,
973                next.num_leaders(),
974                next.allowed_leaders,
975            );
976            // Push the refreshed schedule into the proposer so its
977            // `allowed_leaders` waiting list tracks current scoring instead
978            // of being frozen at the epoch-start (all-zero-scores) shuffle.
979            if let Some(proposer) = self.proposer.as_mut() {
980                proposer.set_next_commit_leader_schedule(next);
981            }
982        }
983
984        // Refresh the proposer's propagation scores so score-based ancestor
985        // exclusion tracks current v3 leader scoring, instead of staying frozen
986        // at the scores from Core construction. The legacy path refreshes them
987        // on leader schedule updates.
988        let propagation_scores = self.current_reputation_scores();
989        if let Some(proposer) = &mut self.proposer {
990            proposer.set_propagation_scores(propagation_scores);
991        }
992
993        Ok(())
994    }
995
996    pub(crate) fn get_missing_blocks(&self) -> BTreeSet<BlockRef> {
997        let _scope = monitored_scope("Core::get_missing_blocks");
998        self.block_manager.missing_blocks()
999    }
1000
1001    /// Sets the delay by round for propagating blocks to a quorum.
1002    pub(crate) fn set_propagation_delay(&mut self, delay: Round) {
1003        info!("Propagation round delay set to: {delay}");
1004        if let Some(proposer) = &mut self.proposer {
1005            proposer.set_propagation_delay(delay);
1006        }
1007    }
1008
1009    /// Sets the min propose round for the proposer allowing to propose blocks only for round numbers
1010    /// `> last_known_proposed_round`. At the moment is allowed to call the method only once leading to a panic
1011    /// if attempt to do multiple times.
1012    pub(crate) fn set_last_known_proposed_round(&mut self, round: Round) {
1013        if let Some(proposer) = &mut self.proposer {
1014            if proposer.get_last_known_proposed_round().is_some() {
1015                panic!(
1016                    "Should not attempt to set the last known proposed round if that has been already set"
1017                );
1018            }
1019            proposer.set_last_known_proposed_round(round);
1020            info!("Last known proposed round set to {round}");
1021        }
1022    }
1023
1024    /// Returns true if the node should propose blocks.
1025    /// Observers always return false since they don't have a proposer.
1026    pub(crate) fn should_propose(&self) -> bool {
1027        self.proposer
1028            .as_ref()
1029            .map(|p| p.should_propose())
1030            .unwrap_or(false)
1031    }
1032
1033    /// Returns the last proposed round, or None if this is an observer node.
1034    pub(crate) fn last_proposed_round(&self) -> Option<Round> {
1035        self.proposer.as_ref().map(|p| p.last_proposed_round())
1036    }
1037
1038    /// Returns the current `ReputationScores` from the leader schedule.
1039    fn current_reputation_scores(&self) -> ReputationScores {
1040        if let Some(schedule) = self.leader_schedule_v3.as_ref() {
1041            schedule.current_reputation_scores()
1042        } else {
1043            self.leader_schedule
1044                .leader_swap_table
1045                .read()
1046                .reputation_scores
1047                .clone()
1048        }
1049    }
1050
1051    // Tries to select a prefix of certified commits to be committed next respecting the `limit`.
1052    // If provided `limit` is zero, it will panic.
1053    // The function returns a list of certified leaders and certified commits. If empty vector is returned, it means that
1054    // there are no certified commits to be committed, as input `certified_commits` is either empty or all of the certified
1055    // commits have been already committed.
1056    #[tracing::instrument(skip_all)]
1057    fn try_select_certified_leaders(
1058        &mut self,
1059        certified_commits: &mut Vec<CertifiedCommit>,
1060        limit: usize,
1061    ) -> Vec<(DecidedLeader, CertifiedCommit)> {
1062        assert!(limit > 0, "limit should be greater than 0");
1063        if certified_commits.is_empty() {
1064            return vec![];
1065        }
1066
1067        let to_commit = if certified_commits.len() >= limit {
1068            // We keep only the number of leaders as dictated by the `limit`
1069            certified_commits.drain(..limit).collect::<Vec<_>>()
1070        } else {
1071            // Otherwise just take all of them and leave the `synced_commits` empty.
1072            std::mem::take(certified_commits)
1073        };
1074
1075        tracing::debug!(
1076            "Selected {} certified leaders: {}",
1077            to_commit.len(),
1078            to_commit.iter().map(|c| c.leader().to_string()).join(",")
1079        );
1080
1081        to_commit
1082            .into_iter()
1083            .map(|commit| {
1084                let leader = commit.blocks().last().expect("Certified commit should have at least one block");
1085                assert_eq!(leader.reference(), commit.leader(), "Last block of the committed sub dag should have the same digest as the leader of the commit");
1086                // There is no knowledge of direct commit with certified commits, so assuming indirect commit.
1087                let leader = DecidedLeader::Commit(leader.clone(), /* direct */ false);
1088                UniversalCommitter::update_metrics(&self.context, &leader, Decision::Certified);
1089                (leader, commit)
1090            })
1091            .collect::<Vec<_>>()
1092    }
1093
1094    /// Helper method for tests to get the last proposed block from the proposer.
1095    #[cfg(test)]
1096    pub(crate) fn last_proposed_block(&self) -> VerifiedBlock {
1097        self.proposer
1098            .as_ref()
1099            .expect("Proposer should be present")
1100            .last_proposed_block()
1101    }
1102
1103    /// Helper method for tests to get the round tracker from the proposer.
1104    /// Returns None if this is an observer node.
1105    #[cfg(test)]
1106    pub(crate) fn round_tracker_for_tests(&self) -> Arc<RwLock<RoundTracker>> {
1107        self.proposer
1108            .as_ref()
1109            .expect("Proposer should be present")
1110            .round_tracker_for_tests()
1111    }
1112}
1113
1114/// Senders of signals from Core, for outputs and events (ex new block produced).
1115pub(crate) struct CoreSignals {
1116    tx_block_broadcast: broadcast::Sender<ExtendedBlock>,
1117    tx_accepted_block_broadcast: broadcast::Sender<VerifiedBlock>,
1118    new_round_sender: watch::Sender<Round>,
1119    context: Arc<Context>,
1120}
1121
1122impl CoreSignals {
1123    pub fn new(context: Arc<Context>) -> (Self, CoreSignalsReceivers) {
1124        // Blocks buffered in broadcast channel should be roughly equal to thosed cached in dag state,
1125        // since the underlying blocks are ref counted so a lower buffer here will not reduce memory
1126        // usage significantly.
1127        let (tx_block_broadcast, rx_block_broadcast) = broadcast::channel::<ExtendedBlock>(
1128            context.parameters.dag_state_cached_rounds as usize,
1129        );
1130        let (tx_accepted_block_broadcast, rx_accepted_block_broadcast) =
1131            broadcast::channel::<VerifiedBlock>(
1132                2 * context.parameters.dag_state_cached_rounds as usize * context.committee.size(),
1133            );
1134        let (new_round_sender, new_round_receiver) = watch::channel(0);
1135
1136        let me = Self {
1137            tx_block_broadcast,
1138            tx_accepted_block_broadcast,
1139            new_round_sender,
1140            context,
1141        };
1142
1143        let receivers = CoreSignalsReceivers {
1144            rx_block_broadcast,
1145            rx_accepted_block_broadcast,
1146            new_round_receiver,
1147        };
1148
1149        (me, receivers)
1150    }
1151
1152    /// Sends a signal to all the waiters that a new block has been produced. The method will return
1153    /// true if block has reached even one subscriber, false otherwise.
1154    pub(crate) fn new_block(&self, extended_block: ExtendedBlock) -> ConsensusResult<()> {
1155        // When there is only one authority in committee, it is unnecessary to broadcast
1156        // the block which will fail anyway without subscribers to the signal.
1157        if self.context.committee.size() > 1 {
1158            if extended_block.block.round() == GENESIS_ROUND {
1159                debug!("Ignoring broadcasting genesis block to peers");
1160                return Ok(());
1161            }
1162
1163            if let Err(err) = self.tx_block_broadcast.send(extended_block) {
1164                warn!("Couldn't broadcast the block to any receiver: {err}");
1165                return Err(ConsensusError::Shutdown);
1166            }
1167        } else {
1168            debug!(
1169                "Did not broadcast block {extended_block:?} to receivers as committee size is <= 1"
1170            );
1171        }
1172        Ok(())
1173    }
1174
1175    /// Broadcasts a block that has been accepted into the local DAG to any active observer
1176    /// subscribers. Unlike `new_block()`, this covers blocks from all authorities, not just
1177    /// own proposals.
1178    /// Silently drops the send when there are no active observer subscribers.
1179    pub(crate) fn new_accepted_block(&self, block: VerifiedBlock) {
1180        // Ignoring send errors here: it is normal for there to be no observers subscribed.
1181        let _ = self.tx_accepted_block_broadcast.send(block);
1182    }
1183
1184    /// Sends a signal that threshold clock has advanced to new round. The `round_number` is the round at which the
1185    /// threshold clock has advanced to.
1186    pub(crate) fn new_round(&mut self, round_number: Round) {
1187        let _ = self.new_round_sender.send_replace(round_number);
1188    }
1189}
1190
1191/// Receivers of signals from Core.
1192/// Intentionally un-clonable. Comonents should only subscribe to channels they need.
1193pub(crate) struct CoreSignalsReceivers {
1194    rx_block_broadcast: broadcast::Receiver<ExtendedBlock>,
1195    rx_accepted_block_broadcast: broadcast::Receiver<VerifiedBlock>,
1196    new_round_receiver: watch::Receiver<Round>,
1197}
1198
1199impl CoreSignalsReceivers {
1200    pub(crate) fn block_broadcast_receiver(&self) -> broadcast::Receiver<ExtendedBlock> {
1201        self.rx_block_broadcast.resubscribe()
1202    }
1203
1204    pub(crate) fn accepted_block_broadcast_receiver(&self) -> broadcast::Receiver<VerifiedBlock> {
1205        self.rx_accepted_block_broadcast.resubscribe()
1206    }
1207
1208    pub(crate) fn new_round_receiver(&self) -> watch::Receiver<Round> {
1209        self.new_round_receiver.clone()
1210    }
1211}
1212
1213/// Creates cores for the specified number of authorities for their corresponding stakes. The method returns the
1214/// cores and their respective signal receivers are returned in `AuthorityIndex` order asc.
1215#[cfg(test)]
1216pub(crate) async fn create_cores(
1217    context: Context,
1218    authorities: Vec<Stake>,
1219) -> Vec<CoreTestFixture> {
1220    let mut cores = Vec::new();
1221
1222    for index in 0..authorities.len() {
1223        let own_index = AuthorityIndex::new_for_test(index as u32);
1224        let core =
1225            CoreTestFixture::new(context.clone(), authorities.clone(), own_index, false).await;
1226        cores.push(core);
1227    }
1228    cores
1229}
1230
1231#[cfg(test)]
1232pub(crate) struct CoreTestFixture {
1233    pub(crate) core: Core,
1234    pub(crate) commit_consumer_monitor: Arc<crate::CommitConsumerMonitor>,
1235    pub(crate) transaction_vote_tracker: TransactionVoteTracker,
1236    pub(crate) signal_receivers: CoreSignalsReceivers,
1237    pub(crate) block_receiver: broadcast::Receiver<ExtendedBlock>,
1238    pub(crate) _commit_output_receiver: UnboundedReceiver<CommittedSubDag>,
1239    pub(crate) dag_state: Arc<RwLock<DagState>>,
1240    pub(crate) store: Arc<MemStore>,
1241    pub(crate) transaction_client: TransactionClient,
1242}
1243
1244#[cfg(test)]
1245impl CoreTestFixture {
1246    async fn new(
1247        context: Context,
1248        authorities: Vec<Stake>,
1249        own_index: AuthorityIndex,
1250        sync_last_known_own_block: bool,
1251    ) -> Self {
1252        Self::new_with_prepopulated_blocks(
1253            context,
1254            authorities,
1255            own_index,
1256            sync_last_known_own_block,
1257            vec![],
1258        )
1259        .await
1260    }
1261
1262    /// Variant of `new` that writes `prepopulated_blocks` into the store before
1263    /// constructing `DagState` and `Core`. Used by tests that recover from a
1264    /// pre-existing store state.
1265    async fn new_with_prepopulated_blocks(
1266        context: Context,
1267        authorities: Vec<Stake>,
1268        own_index: AuthorityIndex,
1269        sync_last_known_own_block: bool,
1270        prepopulated_blocks: Vec<VerifiedBlock>,
1271    ) -> Self {
1272        let store = Arc::new(MemStore::new());
1273        if !prepopulated_blocks.is_empty() {
1274            store
1275                .write(WriteBatch::default().blocks(prepopulated_blocks))
1276                .expect("Storage error");
1277        }
1278        Self::new_with_store(
1279            context,
1280            authorities,
1281            own_index,
1282            sync_last_known_own_block,
1283            store,
1284        )
1285        .await
1286    }
1287
1288    /// Variant of `new` that builds `DagState` and `Core` against the provided
1289    /// store. Used by tests exercising restart and recovery: constructing a
1290    /// second fixture from the same store simulates a node restart.
1291    async fn new_with_store(
1292        context: Context,
1293        authorities: Vec<Stake>,
1294        own_index: AuthorityIndex,
1295        sync_last_known_own_block: bool,
1296        store: Arc<MemStore>,
1297    ) -> Self {
1298        let (committee, mut signers) = local_committee_and_keys(0, authorities.clone());
1299        let mut context = context.clone();
1300        context = context
1301            .with_committee(committee)
1302            .with_authority_index(own_index);
1303        context
1304            .protocol_config
1305            .set_bad_nodes_stake_threshold_for_testing(33);
1306
1307        let context = Arc::new(context);
1308        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store.clone())));
1309
1310        let block_manager = BlockManager::new(context.clone(), dag_state.clone());
1311        let leader_schedule = Arc::new(
1312            LeaderSchedule::from_store(context.clone(), dag_state.clone())
1313                .with_num_commits_per_schedule(10),
1314        );
1315        let (transaction_client, tx_receiver, priority_tx_receiver) =
1316            TransactionClient::new(context.clone());
1317        let transaction_pool = Arc::new(TransactionConsumerPool::new(TransactionConsumer::new(
1318            tx_receiver,
1319            priority_tx_receiver,
1320            context.clone(),
1321        )));
1322        let transaction_vote_tracker = TransactionVoteTracker::new(
1323            context.clone(),
1324            Arc::new(NoopBlockVerifier {}),
1325            dag_state.clone(),
1326        );
1327        let (signals, signal_receivers) = CoreSignals::new(context.clone());
1328        // Need at least one subscriber to the block broadcast channel.
1329        let block_receiver = signal_receivers.block_broadcast_receiver();
1330
1331        let (commit_consumer, commit_output_receiver) = CommitConsumerArgs::new(0, 0);
1332        let commit_consumer_monitor = commit_consumer.monitor();
1333        let commit_observer = CommitObserver::new(
1334            context.clone(),
1335            commit_consumer,
1336            dag_state.clone(),
1337            transaction_vote_tracker.clone(),
1338        )
1339        .await;
1340
1341        let block_signer = signers.remove(own_index.value()).1;
1342
1343        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1344        let core = Core::new_validator(
1345            context.clone(),
1346            leader_schedule,
1347            transaction_pool,
1348            transaction_vote_tracker.clone(),
1349            block_manager,
1350            commit_observer,
1351            signals,
1352            block_signer,
1353            dag_state.clone(),
1354            sync_last_known_own_block,
1355            round_tracker.clone(),
1356        );
1357
1358        Self {
1359            core,
1360            commit_consumer_monitor,
1361            transaction_vote_tracker,
1362            signal_receivers,
1363            block_receiver,
1364            _commit_output_receiver: commit_output_receiver,
1365            dag_state,
1366            store,
1367            transaction_client,
1368        }
1369    }
1370
1371    pub(crate) fn add_blocks(
1372        &mut self,
1373        blocks: Vec<VerifiedBlock>,
1374    ) -> ConsensusResult<BTreeSet<BlockRef>> {
1375        self.transaction_vote_tracker
1376            .add_voted_blocks(blocks.iter().map(|b| (b.clone(), vec![])).collect());
1377        self.core.add_blocks(blocks)
1378    }
1379}
1380
1381#[cfg(test)]
1382mod test {
1383    use std::{
1384        collections::{BTreeMap, BTreeSet},
1385        iter,
1386        time::Duration,
1387    };
1388
1389    use consensus_config::{AuthorityIndex, Parameters};
1390    use consensus_types::block::BlockTimestampMs;
1391    use futures::{StreamExt, stream::FuturesUnordered};
1392    use tokio::time::sleep;
1393
1394    use super::*;
1395    use crate::{
1396        CommitConsumerArgs, CommitIndex,
1397        block::{TestBlock, genesis_blocks, max_transaction_vote_targets},
1398        block_verifier::NoopBlockVerifier,
1399        commit::CommitAPI,
1400        leader_schedule::LeaderSwapTable,
1401        leader_schedule_v3::NextCommitLeaderSchedule,
1402        leader_scoring::ReputationScores,
1403        storage::{Store, WriteBatch, mem_store::MemStore},
1404        test_dag_builder::DagBuilder,
1405        test_dag_parser::parse_dag,
1406        transaction::{BlockStatus, Priority, TransactionClient},
1407    };
1408
1409    /// Recover Core and continue proposing from the last round which forms a quorum.
1410    #[tokio::test]
1411    async fn test_core_recover_from_store_for_full_round() {
1412        telemetry_subscribers::init_for_testing();
1413        let (context, mut key_pairs) = Context::new_for_test(4);
1414        let context = Arc::new(context);
1415        let store = Arc::new(MemStore::new());
1416        let (_transaction_client, tx_receiver, priority_tx_receiver) =
1417            TransactionClient::new(context.clone());
1418        let transaction_pool = Arc::new(TransactionConsumerPool::new(TransactionConsumer::new(
1419            tx_receiver,
1420            priority_tx_receiver,
1421            context.clone(),
1422        )));
1423        let mut block_status_subscriptions = FuturesUnordered::new();
1424
1425        // Create test blocks for all the authorities for 4 rounds and populate them in store
1426        let mut last_round_blocks = genesis_blocks(&context);
1427        let mut all_blocks: Vec<VerifiedBlock> = last_round_blocks.clone();
1428        for round in 1..=4 {
1429            let mut this_round_blocks = Vec::new();
1430            for (index, _authority) in context.committee.authorities() {
1431                let block = VerifiedBlock::new_for_test(
1432                    TestBlock::new(round, index.value() as u32)
1433                        .set_ancestors(last_round_blocks.iter().map(|b| b.reference()).collect())
1434                        .build(),
1435                );
1436
1437                // If it's round 1, that one will be committed later on, and it's our "own" block, then subscribe to listen for the block status.
1438                if round == 1 && index == context.own_index {
1439                    let subscription =
1440                        transaction_pool.subscribe_for_block_status_testing(block.reference());
1441                    block_status_subscriptions.push(subscription);
1442                }
1443
1444                this_round_blocks.push(block);
1445            }
1446            all_blocks.extend(this_round_blocks.clone());
1447            last_round_blocks = this_round_blocks;
1448        }
1449        // write them in store
1450        store
1451            .write(WriteBatch::default().blocks(all_blocks))
1452            .expect("Storage error");
1453
1454        // create dag state after all blocks have been written to store
1455        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store.clone())));
1456        let block_manager = BlockManager::new(context.clone(), dag_state.clone());
1457        let leader_schedule = Arc::new(LeaderSchedule::from_store(
1458            context.clone(),
1459            dag_state.clone(),
1460        ));
1461        let transaction_vote_tracker = TransactionVoteTracker::new(
1462            context.clone(),
1463            Arc::new(NoopBlockVerifier {}),
1464            dag_state.clone(),
1465        );
1466
1467        let (commit_consumer, _commit_receiver) = CommitConsumerArgs::new(0, 0);
1468        let commit_observer = CommitObserver::new(
1469            context.clone(),
1470            commit_consumer,
1471            dag_state.clone(),
1472            transaction_vote_tracker.clone(),
1473        )
1474        .await;
1475
1476        // Check no commits have been persisted to dag_state or store.
1477        let last_commit = store.read_last_commit().unwrap();
1478        assert!(last_commit.is_none());
1479        assert_eq!(dag_state.read().last_commit_index(), 0);
1480
1481        // Now spin up core
1482        let (signals, signal_receivers) = CoreSignals::new(context.clone());
1483        let transaction_vote_tracker = TransactionVoteTracker::new(
1484            context.clone(),
1485            Arc::new(NoopBlockVerifier {}),
1486            dag_state.clone(),
1487        );
1488        transaction_vote_tracker.recover_blocks_after_round(dag_state.read().gc_round());
1489        // Need at least one subscriber to the block broadcast channel.
1490        let mut block_receiver = signal_receivers.block_broadcast_receiver();
1491        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
1492        let _core = Core::new_validator(
1493            context.clone(),
1494            leader_schedule,
1495            transaction_pool,
1496            transaction_vote_tracker.clone(),
1497            block_manager,
1498            commit_observer,
1499            signals,
1500            key_pairs.remove(context.own_index.value()).1,
1501            dag_state.clone(),
1502            false,
1503            round_tracker,
1504        );
1505
1506        // New round should be 5
1507        let mut new_round = signal_receivers.new_round_receiver();
1508        assert_eq!(*new_round.borrow_and_update(), 5);
1509
1510        // Block for round 5 should have been proposed.
1511        let proposed_block = block_receiver
1512            .recv()
1513            .await
1514            .expect("A block should have been created");
1515        assert_eq!(proposed_block.block.round(), 5);
1516        let ancestors = proposed_block.block.ancestors();
1517
1518        // Only ancestors of round 4 should be included.
1519        assert_eq!(ancestors.len(), 4);
1520        for ancestor in ancestors {
1521            assert_eq!(ancestor.round, 4);
1522        }
1523
1524        // Flush the DAG state to storage.
1525        dag_state.write().flush();
1526
1527        // There were no commits prior to the core starting up but there was completed
1528        // rounds up to and including round 4. So we should commit leaders in round 1 & 2
1529        // as soon as the new block for round 5 is proposed.
1530        let last_commit = store
1531            .read_last_commit()
1532            .unwrap()
1533            .expect("last commit should be set");
1534        assert_eq!(last_commit.index(), 2);
1535        assert_eq!(dag_state.read().last_commit_index(), 2);
1536        let all_stored_commits = store.scan_commits((0..=CommitIndex::MAX).into()).unwrap();
1537        assert_eq!(all_stored_commits.len(), 2);
1538
1539        // And ensure that our "own" block 1 sent to TransactionConsumer as notification alongside with gc_round
1540        while let Some(result) = block_status_subscriptions.next().await {
1541            let status = result.unwrap();
1542            assert!(matches!(status, BlockStatus::Sequenced(_)));
1543        }
1544    }
1545
1546    /// Recover Core and continue proposing when having a partial last round which doesn't form a quorum and we haven't
1547    /// proposed for that round yet.
1548    #[tokio::test]
1549    async fn test_core_recover_from_store_for_partial_round() {
1550        telemetry_subscribers::init_for_testing();
1551
1552        let (context, _) = Context::new_for_test(4);
1553
1554        // Create test blocks for all authorities except our's (index = 0).
1555        let mut last_round_blocks = genesis_blocks(&context);
1556        let mut all_blocks = last_round_blocks.clone();
1557        for round in 1..=4 {
1558            let mut this_round_blocks = Vec::new();
1559
1560            // For round 4 only produce f+1 blocks. Skip our validator 0 and that of position 1 from creating blocks.
1561            let authorities_to_skip = if round == 4 {
1562                context.committee.validity_threshold() as usize
1563            } else {
1564                // otherwise always skip creating a block for our authority
1565                1
1566            };
1567
1568            for (index, _authority) in context.committee.authorities().skip(authorities_to_skip) {
1569                let block = TestBlock::new(round, index.value() as u32)
1570                    .set_ancestors(last_round_blocks.iter().map(|b| b.reference()).collect())
1571                    .build();
1572                this_round_blocks.push(VerifiedBlock::new_for_test(block));
1573            }
1574            all_blocks.extend(this_round_blocks.clone());
1575            last_round_blocks = this_round_blocks;
1576        }
1577
1578        let mut fixture = CoreTestFixture::new_with_prepopulated_blocks(
1579            context,
1580            vec![1, 1, 1, 1],
1581            AuthorityIndex::new_for_test(0),
1582            false,
1583            all_blocks,
1584        )
1585        .await;
1586
1587        // Clock round should have advanced to 5 during recovery because
1588        // a quorum has formed in round 4.
1589        let mut new_round = fixture.signal_receivers.new_round_receiver();
1590        assert_eq!(*new_round.borrow_and_update(), 5);
1591
1592        // During recovery, round 4 block should have been proposed.
1593        let proposed_block = fixture
1594            .block_receiver
1595            .recv()
1596            .await
1597            .expect("A block should have been created");
1598        assert_eq!(proposed_block.block.round(), 4);
1599        let ancestors = proposed_block.block.ancestors();
1600
1601        assert_eq!(ancestors.len(), 4);
1602        let own_index = fixture.core.context.own_index;
1603        for ancestor in ancestors {
1604            if ancestor.author == own_index {
1605                assert_eq!(ancestor.round, 0);
1606            } else {
1607                assert_eq!(ancestor.round, 3);
1608            }
1609        }
1610
1611        // Run commit rule.
1612        fixture.core.try_commit(vec![]).ok();
1613
1614        // Flush the DAG state to storage.
1615        fixture.core.dag_state.write().flush();
1616
1617        // There were no commits prior to the core starting up but there was completed
1618        // rounds up to round 4. So we should commit leaders in round 1 & 2 as soon
1619        // as the new block for round 4 is proposed.
1620        let last_commit = fixture
1621            .store
1622            .read_last_commit()
1623            .unwrap()
1624            .expect("last commit should be set");
1625        assert_eq!(last_commit.index(), 2);
1626        assert_eq!(fixture.dag_state.read().last_commit_index(), 2);
1627        let all_stored_commits = fixture
1628            .store
1629            .scan_commits((0..=CommitIndex::MAX).into())
1630            .unwrap();
1631        assert_eq!(all_stored_commits.len(), 2);
1632    }
1633
1634    #[tokio::test]
1635    async fn test_core_propose_after_genesis() {
1636        telemetry_subscribers::init_for_testing();
1637        let (mut context, _) = Context::new_for_test(4);
1638        context
1639            .protocol_config
1640            .set_max_transaction_size_bytes_for_testing(2_000);
1641        context
1642            .protocol_config
1643            .set_max_transactions_in_block_bytes_for_testing(2_000);
1644
1645        let mut fixture = CoreTestFixture::new(
1646            context,
1647            vec![1, 1, 1, 1],
1648            AuthorityIndex::new_for_test(0),
1649            false,
1650        )
1651        .await;
1652
1653        // Send some transactions
1654        let mut total = 0;
1655        let mut index = 0;
1656        loop {
1657            let transaction =
1658                bcs::to_bytes(&format!("Transaction {index}")).expect("Shouldn't fail");
1659            total += transaction.len();
1660            index += 1;
1661            let _w = fixture
1662                .transaction_client
1663                .submit_no_wait(vec![transaction], Priority::Normal)
1664                .await
1665                .unwrap();
1666
1667            // Create total size of transactions up to 1KB
1668            if total >= 1_000 {
1669                break;
1670            }
1671        }
1672
1673        // a new block should have been created during recovery.
1674        let extended_block = fixture
1675            .block_receiver
1676            .recv()
1677            .await
1678            .expect("A new block should have been created");
1679
1680        // A new block created - assert the details
1681        assert_eq!(extended_block.block.round(), 1);
1682        assert_eq!(extended_block.block.author().value(), 0);
1683        assert_eq!(extended_block.block.ancestors().len(), 4);
1684
1685        let mut total = 0;
1686        for (i, transaction) in extended_block.block.transactions().iter().enumerate() {
1687            total += transaction.data().len() as u64;
1688            let transaction: String = bcs::from_bytes(transaction.data()).unwrap();
1689            assert_eq!(format!("Transaction {i}"), transaction);
1690        }
1691        assert!(
1692            total
1693                <= fixture
1694                    .core
1695                    .context
1696                    .protocol_config
1697                    .max_transactions_in_block_bytes()
1698        );
1699        assert_eq!(
1700            fixture
1701                .core
1702                .context
1703                .metrics
1704                .node_metrics
1705                .proposed_block_transaction_bytes
1706                .get_sample_count(),
1707            1
1708        );
1709        assert_eq!(
1710            fixture
1711                .core
1712                .context
1713                .metrics
1714                .node_metrics
1715                .proposed_block_transaction_bytes
1716                .get_sample_sum(),
1717            total as f64
1718        );
1719
1720        // genesis blocks should be referenced
1721        let all_genesis = genesis_blocks(&fixture.core.context);
1722
1723        for ancestor in extended_block.block.ancestors() {
1724            all_genesis
1725                .iter()
1726                .find(|block| block.reference() == *ancestor)
1727                .expect("Block should be found amongst genesis blocks");
1728        }
1729
1730        // Try to propose again - with or without ignore leaders check, it will not return any block
1731        assert!(fixture.core.try_propose(false).unwrap().is_none());
1732        assert!(fixture.core.try_propose(true).unwrap().is_none());
1733
1734        // Flush the DAG state to storage.
1735        fixture.dag_state.write().flush();
1736
1737        // Check no commits have been persisted to dag_state & store
1738        let last_commit = fixture.store.read_last_commit().unwrap();
1739        assert!(last_commit.is_none());
1740        assert_eq!(fixture.dag_state.read().last_commit_index(), 0);
1741    }
1742
1743    #[tokio::test]
1744    async fn test_core_proposes_v3_block_with_gc_cutoff() {
1745        telemetry_subscribers::init_for_testing();
1746        const GC_DEPTH: u32 = 3;
1747        let (mut context, _) = Context::new_for_test(4);
1748        context.protocol_config.set_enable_v3_for_testing(true);
1749        context.protocol_config.set_gc_depth_for_testing(GC_DEPTH);
1750
1751        let mut fixture = CoreTestFixture::new(
1752            context,
1753            vec![1, 1, 1, 1],
1754            AuthorityIndex::new_for_test(0),
1755            false,
1756        )
1757        .await;
1758        let first_proposal = fixture
1759            .block_receiver
1760            .recv()
1761            .await
1762            .expect("Recovery should propose the first v3 block");
1763        assert_eq!(
1764            first_proposal.block.transaction_votes_cutoff_round(),
1765            GENESIS_ROUND,
1766            "Nothing is committed yet, so the cutoff must be the genesis round"
1767        );
1768
1769        // Commit enough rounds to move the GC round above the genesis round.
1770        // Core proposes the blocks of its own authority, so the builder must skip them.
1771        let mut builder = DagBuilder::new(fixture.core.context.clone());
1772        builder
1773            .layers(1..=10)
1774            .authorities(vec![fixture.core.context.own_index])
1775            .skip_block()
1776            .build();
1777        assert!(
1778            fixture
1779                .add_blocks(builder.blocks.values().cloned().collect())
1780                .unwrap()
1781                .is_empty()
1782        );
1783        let gc_round = fixture.dag_state.read().gc_round();
1784        assert!(gc_round > GENESIS_ROUND, "GC round should have advanced");
1785
1786        fixture.core.try_propose(true).unwrap();
1787        let proposed = fixture.core.last_proposed_block();
1788        assert!(proposed.round() > first_proposal.block.round());
1789        assert_eq!(proposed.transaction_votes_cutoff_round(), gc_round);
1790    }
1791
1792    // Adds 3 rounds of peer blocks. The local authority rejects transaction 0 in each block.
1793    // Then proposes a block.
1794    // Returns the fixture, the proposed block and the peer blocks.
1795    async fn propose_after_peer_blocks_with_reject_votes(
1796        context: Context,
1797    ) -> (CoreTestFixture, VerifiedBlock, Vec<VerifiedBlock>) {
1798        let mut fixture = CoreTestFixture::new(
1799            context,
1800            vec![1, 1, 1, 1],
1801            AuthorityIndex::new_for_test(0),
1802            false,
1803        )
1804        .await;
1805        let mut builder = DagBuilder::new(fixture.core.context.clone());
1806        builder
1807            .layers(1..=3)
1808            .authorities(vec![fixture.core.context.own_index])
1809            .skip_block()
1810            .build();
1811        let blocks = builder.blocks.values().cloned().collect::<Vec<_>>();
1812        fixture
1813            .transaction_vote_tracker
1814            .add_voted_blocks(blocks.iter().map(|b| (b.clone(), vec![0])).collect());
1815        assert!(fixture.core.add_blocks(blocks.clone()).unwrap().is_empty());
1816
1817        fixture.core.try_propose(true).unwrap();
1818        let proposed = fixture.core.last_proposed_block();
1819        (fixture, proposed, blocks)
1820    }
1821
1822    #[tokio::test]
1823    async fn test_core_v2_proposal_keeps_all_transaction_vote_targets() {
1824        telemetry_subscribers::init_for_testing();
1825        let (mut context, _) = Context::new_for_test(4);
1826        context.protocol_config.set_gc_depth_for_testing(1);
1827        let vote_target_limit = max_transaction_vote_targets(&context);
1828
1829        let (_fixture, proposed, blocks) =
1830            propose_after_peer_blocks_with_reject_votes(context).await;
1831
1832        // V2 blocks have no signed cutoff round, so the V3 limit must not remove their targets.
1833        assert!(blocks.len() > vote_target_limit);
1834        assert_eq!(proposed.transaction_votes().len(), blocks.len());
1835    }
1836
1837    #[tokio::test]
1838    async fn test_core_v3_proposal_limits_transaction_vote_targets() {
1839        telemetry_subscribers::init_for_testing();
1840        let (mut context, _) = Context::new_for_test(4);
1841        context.protocol_config.set_enable_v3_for_testing(true);
1842        context.protocol_config.set_gc_depth_for_testing(1);
1843        let vote_target_limit = max_transaction_vote_targets(&context);
1844
1845        let (fixture, proposed, blocks) =
1846            propose_after_peer_blocks_with_reject_votes(context).await;
1847
1848        assert!(blocks.len() > vote_target_limit);
1849        assert!(!proposed.transaction_votes().is_empty());
1850        assert!(proposed.transaction_votes().len() <= vote_target_limit);
1851        // The signed cutoff moves above the GC round and covers every removed target. Only the
1852        // targets above the cutoff stay in the block.
1853        let cutoff_round = proposed.transaction_votes_cutoff_round();
1854        assert!(cutoff_round > fixture.dag_state.read().gc_round());
1855        let expected_targets = blocks
1856            .iter()
1857            .filter(|block| block.round() > cutoff_round)
1858            .count();
1859        assert_eq!(proposed.transaction_votes().len(), expected_targets);
1860    }
1861
1862    #[tokio::test]
1863    async fn test_core_propose_once_receiving_a_quorum() {
1864        telemetry_subscribers::init_for_testing();
1865        let (context, _key_pairs) = Context::new_for_test(4);
1866        let mut core_fixture = CoreTestFixture::new(
1867            context.clone(),
1868            vec![1, 1, 1, 1],
1869            AuthorityIndex::new_for_test(0),
1870            false,
1871        )
1872        .await;
1873        let transaction_vote_tracker = &core_fixture.transaction_vote_tracker;
1874        let store = &core_fixture.store;
1875        let dag_state = &core_fixture.dag_state;
1876        let core = &mut core_fixture.core;
1877
1878        let mut expected_ancestors = BTreeSet::new();
1879
1880        // Adding one block now will trigger the creation of new block for round 1
1881        let block_1 = VerifiedBlock::new_for_test(TestBlock::new(1, 1).build());
1882        expected_ancestors.insert(block_1.reference());
1883        // Wait for min round delay to allow blocks to be proposed.
1884        sleep(context.parameters.min_round_delay).await;
1885        // add blocks to trigger proposal.
1886        transaction_vote_tracker.add_voted_blocks(vec![(block_1.clone(), vec![])]);
1887        _ = core.add_blocks(vec![block_1]);
1888
1889        assert_eq!(core.last_proposed_round(), Some(1));
1890        expected_ancestors.insert(core.last_proposed_block().reference());
1891        // attempt to create a block - none will be produced.
1892        assert!(core.try_propose(false).unwrap().is_none());
1893
1894        // Adding another block now forms a quorum for round 1, so block at round 2 will proposed
1895        let block_2 = VerifiedBlock::new_for_test(TestBlock::new(1, 2).build());
1896        expected_ancestors.insert(block_2.reference());
1897        // Wait for min round delay to allow blocks to be proposed.
1898        sleep(context.parameters.min_round_delay).await;
1899        // add blocks to trigger proposal.
1900        transaction_vote_tracker.add_voted_blocks(vec![(block_2.clone(), vec![1, 4])]);
1901        _ = core.add_blocks(vec![block_2.clone()]);
1902
1903        assert_eq!(core.last_proposed_round(), Some(2));
1904
1905        let proposed_block = core.last_proposed_block();
1906        assert_eq!(proposed_block.round(), 2);
1907        assert_eq!(proposed_block.author(), context.own_index);
1908        assert_eq!(proposed_block.ancestors().len(), 3);
1909        let ancestors = proposed_block.ancestors();
1910        let ancestors = ancestors.iter().cloned().collect::<BTreeSet<_>>();
1911        assert_eq!(ancestors, expected_ancestors);
1912
1913        let transaction_votes = proposed_block.transaction_votes();
1914        assert_eq!(transaction_votes.len(), 1);
1915        let transaction_vote = transaction_votes.first().unwrap();
1916        assert_eq!(transaction_vote.block_ref, block_2.reference());
1917        assert_eq!(transaction_vote.rejects, vec![1, 4]);
1918        assert_eq!(
1919            context
1920                .metrics
1921                .node_metrics
1922                .proposed_block_transaction_vote_blocks
1923                .get_sample_count(),
1924            2
1925        );
1926        assert_eq!(
1927            context
1928                .metrics
1929                .node_metrics
1930                .proposed_block_transaction_vote_blocks
1931                .get_sample_sum(),
1932            1.0
1933        );
1934        assert_eq!(
1935            context
1936                .metrics
1937                .node_metrics
1938                .proposed_block_transaction_vote_entries
1939                .get_sample_count(),
1940            2
1941        );
1942        assert_eq!(
1943            context
1944                .metrics
1945                .node_metrics
1946                .proposed_block_transaction_vote_entries
1947                .get_sample_sum(),
1948            2.0
1949        );
1950
1951        // Flush the DAG state to storage.
1952        dag_state.write().flush();
1953
1954        // Check no commits have been persisted to dag_state & store
1955        let last_commit = store.read_last_commit().unwrap();
1956        assert!(last_commit.is_none());
1957        assert_eq!(dag_state.read().last_commit_index(), 0);
1958    }
1959
1960    #[tokio::test]
1961    async fn test_commit_and_notify_for_block_status() {
1962        telemetry_subscribers::init_for_testing();
1963        let (mut context, mut key_pairs) = Context::new_for_test(4);
1964        const GC_DEPTH: u32 = 2;
1965
1966        context.protocol_config.set_gc_depth_for_testing(GC_DEPTH);
1967
1968        let context = Arc::new(context);
1969
1970        let store = Arc::new(MemStore::new());
1971        let (_transaction_client, tx_receiver, priority_tx_receiver) =
1972            TransactionClient::new(context.clone());
1973        let transaction_pool = Arc::new(TransactionConsumerPool::new(TransactionConsumer::new(
1974            tx_receiver,
1975            priority_tx_receiver,
1976            context.clone(),
1977        )));
1978        let mut block_status_subscriptions = FuturesUnordered::new();
1979
1980        let dag_str = "DAG {
1981            Round 0 : { 4 },
1982            Round 1 : { * },
1983            Round 2 : { * },
1984            Round 3 : {
1985                A -> [*],
1986                B -> [-A2],
1987                C -> [-A2],
1988                D -> [-A2],
1989            },
1990            Round 4 : { 
1991                B -> [-A3],
1992                C -> [-A3],
1993                D -> [-A3],
1994            },
1995            Round 5 : { 
1996                A -> [A3, B4, C4, D4]
1997                B -> [*],
1998                C -> [*],
1999                D -> [*],
2000            },
2001            Round 6 : { * },
2002            Round 7 : { * },
2003            Round 8 : { * },
2004        }";
2005
2006        let (_, dag_builder) = parse_dag(dag_str).expect("Invalid dag");
2007        dag_builder.print();
2008
2009        // Subscribe to all created "own" blocks. We know that for our node (A) we'll be able to commit up to round 5.
2010        for block in dag_builder.blocks(1..=5) {
2011            if block.author() == context.own_index {
2012                let subscription =
2013                    transaction_pool.subscribe_for_block_status_testing(block.reference());
2014                block_status_subscriptions.push(subscription);
2015            }
2016        }
2017
2018        // write them in store
2019        store
2020            .write(WriteBatch::default().blocks(dag_builder.blocks(1..=8)))
2021            .expect("Storage error");
2022
2023        // create dag state after all blocks have been written to store
2024        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store.clone())));
2025        let block_manager = BlockManager::new(context.clone(), dag_state.clone());
2026        let leader_schedule = Arc::new(LeaderSchedule::from_store(
2027            context.clone(),
2028            dag_state.clone(),
2029        ));
2030        let transaction_vote_tracker = TransactionVoteTracker::new(
2031            context.clone(),
2032            Arc::new(NoopBlockVerifier {}),
2033            dag_state.clone(),
2034        );
2035
2036        let (commit_consumer, _commit_receiver) = CommitConsumerArgs::new(0, 0);
2037        let commit_observer = CommitObserver::new(
2038            context.clone(),
2039            commit_consumer,
2040            dag_state.clone(),
2041            transaction_vote_tracker.clone(),
2042        )
2043        .await;
2044
2045        // Flush the DAG state to storage.
2046        dag_state.write().flush();
2047
2048        // Check no commits have been persisted to dag_state or store.
2049        let last_commit = store.read_last_commit().unwrap();
2050        assert!(last_commit.is_none());
2051        assert_eq!(dag_state.read().last_commit_index(), 0);
2052
2053        // Now recover Core and other components.
2054        let (signals, signal_receivers) = CoreSignals::new(context.clone());
2055        let transaction_vote_tracker = TransactionVoteTracker::new(
2056            context.clone(),
2057            Arc::new(NoopBlockVerifier {}),
2058            dag_state.clone(),
2059        );
2060        transaction_vote_tracker.recover_blocks_after_round(dag_state.read().gc_round());
2061        // Need at least one subscriber to the block broadcast channel.
2062        let _block_receiver = signal_receivers.block_broadcast_receiver();
2063        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
2064        let _core = Core::new_validator(
2065            context.clone(),
2066            leader_schedule,
2067            transaction_pool,
2068            transaction_vote_tracker,
2069            block_manager,
2070            commit_observer,
2071            signals,
2072            key_pairs.remove(context.own_index.value()).1,
2073            dag_state.clone(),
2074            false,
2075            round_tracker,
2076        );
2077
2078        // Flush the DAG state to storage.
2079        dag_state.write().flush();
2080
2081        let last_commit = store
2082            .read_last_commit()
2083            .unwrap()
2084            .expect("last commit should be set");
2085
2086        assert_eq!(last_commit.index(), 5);
2087
2088        while let Some(result) = block_status_subscriptions.next().await {
2089            let status = result.unwrap();
2090
2091            match status {
2092                BlockStatus::Sequenced(block_ref) => {
2093                    assert!(block_ref.round == 1 || block_ref.round == 5);
2094                }
2095                BlockStatus::GarbageCollected(block_ref) => {
2096                    assert!(block_ref.round == 2 || block_ref.round == 3);
2097                }
2098            }
2099        }
2100    }
2101
2102    // Tests that the threshold clock advances when blocks get unsuspended due to GC'ed blocks and newly created blocks are always higher
2103    // than the last advanced gc round.
2104    #[tokio::test]
2105    async fn test_multiple_commits_advance_threshold_clock() {
2106        telemetry_subscribers::init_for_testing();
2107        let (mut context, _) = Context::new_for_test(4);
2108        const GC_DEPTH: u32 = 2;
2109
2110        context.protocol_config.set_gc_depth_for_testing(GC_DEPTH);
2111
2112        // On round 1 we do produce the block for authority D but we do not link it until round 6. This is making round 6 unable to get processed
2113        // until leader of round 3 is committed where round 1 gets garbage collected.
2114        // Then we add more rounds so we can trigger a commit for leader of round 9 which will move the gc round to 7.
2115        let dag_str = "DAG {
2116            Round 0 : { 4 },
2117            Round 1 : { * },
2118            Round 2 : {
2119                B -> [-D1],
2120                C -> [-D1],
2121                D -> [-D1],
2122            },
2123            Round 3 : {
2124                B -> [*],
2125                C -> [*]
2126                D -> [*],
2127            },
2128            Round 4 : {
2129                A -> [*],
2130                B -> [*],
2131                C -> [*]
2132                D -> [*],
2133            },
2134            Round 5 : {
2135                A -> [*],
2136                B -> [*],
2137                C -> [*],
2138                D -> [*],
2139            },
2140            Round 6 : {
2141                B -> [A5, B5, C5, D1],
2142                C -> [A5, B5, C5, D1],
2143                D -> [A5, B5, C5, D1],
2144            },
2145            Round 7 : {
2146                B -> [*],
2147                C -> [*],
2148                D -> [*],
2149            },
2150            Round 8 : {
2151                B -> [*],
2152                C -> [*],
2153                D -> [*],
2154            },
2155            Round 9 : {
2156                B -> [*],
2157                C -> [*],
2158                D -> [*],
2159            },
2160            Round 10 : {
2161                B -> [*],
2162                C -> [*],
2163                D -> [*],
2164            },
2165            Round 11 : {
2166                B -> [*],
2167                C -> [*],
2168                D -> [*],
2169            },
2170        }";
2171
2172        let (_, dag_builder) = parse_dag(dag_str).expect("Invalid dag");
2173        dag_builder.print();
2174
2175        let mut fixture = CoreTestFixture::new(
2176            context,
2177            vec![1, 1, 1, 1],
2178            AuthorityIndex::new_for_test(0),
2179            true,
2180        )
2181        .await;
2182
2183        // Check no commits have been persisted to dag_state or store.
2184        let last_commit = fixture.store.read_last_commit().unwrap();
2185        assert!(last_commit.is_none());
2186        assert_eq!(fixture.dag_state.read().last_commit_index(), 0);
2187
2188        // We set the last known round to 4 so we avoid creating new blocks until then - otherwise it will crash as the already created DAG contains blocks for this
2189        // authority.
2190        fixture.core.set_last_known_proposed_round(4);
2191
2192        // We add all the blocks except D1. The only ones we can immediately accept are the ones up to round 5 as they don't have a dependency on D1. Rest of blocks do have causal dependency
2193        // to D1 so they can't be processed until the leader of round 3 can get committed and gc round moves to 1. That will make all the blocks that depend to D1 get accepted.
2194        // However, our threshold clock is now at round 6 as the last quorum that we managed to process was the round 5.
2195        // As commits happen blocks of later rounds get accepted and more leaders get committed. Eventually the leader of round 9 gets committed and gc is moved to 9 - 2 = 7.
2196        // If our node attempts to produce a block for the threshold clock 6, that will make the acceptance checks fail as now gc has moved far past this round.
2197        let mut all_blocks = dag_builder.blocks(1..=11);
2198        all_blocks.sort_by_key(|b| b.round());
2199        // Register votes for every block - including the one withheld from Core -
2200        // so transaction_vote_tracker's state matches the full DAG.
2201        fixture
2202            .transaction_vote_tracker
2203            .add_voted_blocks(all_blocks.iter().map(|b| (b.clone(), vec![])).collect());
2204        let blocks: Vec<VerifiedBlock> = all_blocks
2205            .into_iter()
2206            .filter(|b| !(b.round() == 1 && b.author() == AuthorityIndex::new_for_test(3)))
2207            .collect();
2208        fixture.core.add_blocks(blocks).expect("Should not fail");
2209
2210        assert_eq!(fixture.core.last_proposed_round(), Some(12));
2211    }
2212
2213    #[tokio::test]
2214    async fn test_core_set_min_propose_round() {
2215        telemetry_subscribers::init_for_testing();
2216        let (context, _) = Context::new_for_test(4);
2217        let context = context.with_parameters(Parameters {
2218            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
2219            ..Default::default()
2220        });
2221        let mut fixture = CoreTestFixture::new(
2222            context,
2223            vec![1, 1, 1, 1],
2224            AuthorityIndex::new_for_test(0),
2225            true,
2226        )
2227        .await;
2228
2229        // No new block should have been produced
2230        assert_eq!(
2231            fixture.core.last_proposed_round(),
2232            Some(GENESIS_ROUND),
2233            "No block should have been created other than genesis"
2234        );
2235
2236        // Trying to explicitly propose a block will not produce anything
2237        assert!(fixture.core.try_propose(true).unwrap().is_none());
2238
2239        // Create blocks for the whole network - even "our" node in order to replicate an "amnesia" recovery.
2240        let mut builder = DagBuilder::new(fixture.core.context.clone());
2241        builder.layers(1..=10).build();
2242
2243        let blocks = builder.blocks.values().cloned().collect::<Vec<_>>();
2244
2245        // Process all the blocks
2246        assert!(fixture.add_blocks(blocks).unwrap().is_empty());
2247
2248        fixture
2249            .core
2250            .round_tracker_for_tests()
2251            .write()
2252            .update_from_probe(
2253                vec![
2254                    vec![10, 10, 10, 10],
2255                    vec![10, 10, 10, 10],
2256                    vec![10, 10, 10, 10],
2257                    vec![10, 10, 10, 10],
2258                ],
2259                vec![
2260                    vec![10, 10, 10, 10],
2261                    vec![10, 10, 10, 10],
2262                    vec![10, 10, 10, 10],
2263                    vec![10, 10, 10, 10],
2264                ],
2265            );
2266
2267        // Try to propose - no block should be produced.
2268        assert!(fixture.core.try_propose(true).unwrap().is_none());
2269
2270        // Now set the last known proposed round which is the highest round for which the network informed
2271        // us that we do have proposed a block about.
2272        fixture.core.set_last_known_proposed_round(10);
2273
2274        let block = fixture.core.try_propose(true).expect("No error").unwrap();
2275        assert_eq!(block.round(), 11);
2276        assert_eq!(block.ancestors().len(), 4);
2277
2278        let our_ancestor_included = block.ancestors()[0];
2279        assert_eq!(our_ancestor_included.author, fixture.core.context.own_index);
2280        assert_eq!(our_ancestor_included.round, 10);
2281    }
2282
2283    #[tokio::test(flavor = "current_thread", start_paused = true)]
2284    async fn test_core_try_new_block_leader_timeout() {
2285        telemetry_subscribers::init_for_testing();
2286
2287        // Since we run the test with started_paused = true, any time-dependent operations using Tokio's time
2288        // facilities, such as tokio::time::sleep or tokio::time::Instant, will not advance. So practically each
2289        // Core's clock will have initialised potentially with different values but it never advances.
2290        // To ensure that blocks won't get rejected by cores we'll need to manually wait for the time
2291        // diff before processing them. By calling the `tokio::time::sleep` we implicitly also advance the
2292        // tokio clock.
2293        async fn wait_blocks(blocks: &[VerifiedBlock], context: &Context) {
2294            // Simulate the time wait before processing a block to ensure that block.timestamp <= now
2295            let now = context.clock.timestamp_utc_ms();
2296            let max_timestamp = blocks
2297                .iter()
2298                .max_by_key(|block| block.timestamp_ms() as BlockTimestampMs)
2299                .map(|block| block.timestamp_ms())
2300                .unwrap_or(0);
2301
2302            let wait_time = Duration::from_millis(max_timestamp.saturating_sub(now));
2303            sleep(wait_time).await;
2304        }
2305
2306        let (context, _) = Context::new_for_test(4);
2307        // Create the cores for all authorities
2308        let mut all_cores = create_cores(context, vec![1, 1, 1, 1]).await;
2309
2310        // Create blocks for rounds 1..=3 from all Cores except last Core of authority 3, so we miss the block from it. As
2311        // it will be the leader of round 3 then no-one will be able to progress to round 4 unless we explicitly trigger
2312        // the block creation.
2313        // create the cores and their signals for all the authorities
2314        let (_last_core, cores) = all_cores.split_last_mut().unwrap();
2315
2316        // Now iterate over a few rounds and ensure the corresponding signals are created while network advances
2317        let mut last_round_blocks = Vec::<VerifiedBlock>::new();
2318        for round in 1..=3 {
2319            let mut this_round_blocks = Vec::new();
2320
2321            for core_fixture in cores.iter_mut() {
2322                wait_blocks(&last_round_blocks, &core_fixture.core.context).await;
2323
2324                core_fixture.add_blocks(last_round_blocks.clone()).unwrap();
2325
2326                // Only when round > 1 and using non-genesis parents.
2327                if let Some(r) = last_round_blocks.first().map(|b| b.round()) {
2328                    assert_eq!(round - 1, r);
2329                    if core_fixture.core.last_proposed_round() == Some(r) {
2330                        // Force propose new block regardless of min round delay.
2331                        core_fixture
2332                            .core
2333                            .try_propose(true)
2334                            .unwrap()
2335                            .unwrap_or_else(|| {
2336                                panic!("Block should have been proposed for round {}", round)
2337                            });
2338                    }
2339                }
2340
2341                assert_eq!(core_fixture.core.last_proposed_round(), Some(round));
2342
2343                this_round_blocks.push(core_fixture.core.last_proposed_block().clone());
2344            }
2345
2346            last_round_blocks = this_round_blocks;
2347        }
2348
2349        // Try to create the blocks for round 4 by calling the try_propose() method. No block should be created as the
2350        // leader - authority 3 - hasn't proposed any block.
2351        for core_fixture in cores.iter_mut() {
2352            wait_blocks(&last_round_blocks, &core_fixture.core.context).await;
2353
2354            core_fixture.add_blocks(last_round_blocks.clone()).unwrap();
2355            assert!(core_fixture.core.try_propose(false).unwrap().is_none());
2356        }
2357
2358        // Now try to create the blocks for round 4 via the leader timeout method which should
2359        // ignore any leader checks or min round delay.
2360        for core_fixture in cores.iter_mut() {
2361            assert!(core_fixture.core.new_block(4, true).unwrap().is_some());
2362            assert_eq!(core_fixture.core.last_proposed_round(), Some(4));
2363
2364            // Flush the DAG state to storage.
2365            core_fixture.dag_state.write().flush();
2366
2367            // Check commits have been persisted to store
2368            let last_commit = core_fixture
2369                .store
2370                .read_last_commit()
2371                .unwrap()
2372                .expect("last commit should be set");
2373            // There are 1 leader rounds with rounds completed up to and including
2374            // round 4
2375            assert_eq!(last_commit.index(), 1);
2376            let all_stored_commits = core_fixture
2377                .store
2378                .scan_commits((0..=CommitIndex::MAX).into())
2379                .unwrap();
2380            assert_eq!(all_stored_commits.len(), 1);
2381        }
2382    }
2383
2384    #[tokio::test(flavor = "current_thread", start_paused = true)]
2385    async fn test_core_try_new_block_with_leader_timeout_and_low_scoring_authority() {
2386        telemetry_subscribers::init_for_testing();
2387
2388        // Since we run the test with started_paused = true, any time-dependent operations using Tokio's time
2389        // facilities, such as tokio::time::sleep or tokio::time::Instant, will not advance. So practically each
2390        // Core's clock will have initialised potentially with different values but it never advances.
2391        // To ensure that blocks won't get rejected by cores we'll need to manually wait for the time
2392        // diff before processing them. By calling the `tokio::time::sleep` we implicitly also advance the
2393        // tokio clock.
2394        async fn wait_blocks(blocks: &[VerifiedBlock], context: &Context) {
2395            // Simulate the time wait before processing a block to ensure that block.timestamp <= now
2396            let now = context.clock.timestamp_utc_ms();
2397            let max_timestamp = blocks
2398                .iter()
2399                .max_by_key(|block| block.timestamp_ms() as BlockTimestampMs)
2400                .map(|block| block.timestamp_ms())
2401                .unwrap_or(0);
2402
2403            let wait_time = Duration::from_millis(max_timestamp.saturating_sub(now));
2404            sleep(wait_time).await;
2405        }
2406
2407        let (mut context, _) = Context::new_for_test(5);
2408        context
2409            .protocol_config
2410            .set_bad_nodes_stake_threshold_for_testing(33);
2411
2412        // Create the cores for all authorities
2413        let mut all_cores = create_cores(context, vec![1, 1, 1, 1, 1]).await;
2414        let (_last_core, cores) = all_cores.split_last_mut().unwrap();
2415
2416        // Create blocks for rounds 1..=30 from all Cores except last Core of authority 4.
2417        let mut last_round_blocks = Vec::<VerifiedBlock>::new();
2418        for round in 1..=30 {
2419            let mut this_round_blocks = Vec::new();
2420
2421            for core_fixture in cores.iter_mut() {
2422                wait_blocks(&last_round_blocks, &core_fixture.core.context).await;
2423
2424                core_fixture.add_blocks(last_round_blocks.clone()).unwrap();
2425
2426                core_fixture
2427                    .core
2428                    .round_tracker_for_tests()
2429                    .write()
2430                    .update_from_probe(
2431                        vec![
2432                            vec![round, round, round, round, 0],
2433                            vec![round, round, round, round, 0],
2434                            vec![round, round, round, round, 0],
2435                            vec![round, round, round, round, 0],
2436                            vec![0, 0, 0, 0, 0],
2437                        ],
2438                        vec![
2439                            vec![round, round, round, round, 0],
2440                            vec![round, round, round, round, 0],
2441                            vec![round, round, round, round, 0],
2442                            vec![round, round, round, round, 0],
2443                            vec![0, 0, 0, 0, 0],
2444                        ],
2445                    );
2446
2447                // Only when round > 1 and using non-genesis parents.
2448                if let Some(r) = last_round_blocks.first().map(|b| b.round()) {
2449                    assert_eq!(round - 1, r);
2450                    if core_fixture.core.last_proposed_round() == Some(r) {
2451                        // Force propose new block regardless of min round delay.
2452                        core_fixture
2453                            .core
2454                            .try_propose(true)
2455                            .unwrap()
2456                            .unwrap_or_else(|| {
2457                                panic!("Block should have been proposed for round {}", round)
2458                            });
2459                    }
2460                }
2461
2462                assert_eq!(core_fixture.core.last_proposed_round(), Some(round));
2463
2464                this_round_blocks.push(core_fixture.core.last_proposed_block().clone());
2465            }
2466
2467            last_round_blocks = this_round_blocks;
2468        }
2469
2470        // Now produce blocks for all Cores
2471        for round in 31..=40 {
2472            let mut this_round_blocks = Vec::new();
2473
2474            for core_fixture in all_cores.iter_mut() {
2475                wait_blocks(&last_round_blocks, &core_fixture.core.context).await;
2476
2477                core_fixture.add_blocks(last_round_blocks.clone()).unwrap();
2478
2479                // Don't update probed rounds for authority 3 so it will remain
2480                // excluded
2481                core_fixture
2482                    .core
2483                    .round_tracker_for_tests()
2484                    .write()
2485                    .update_from_probe(
2486                        vec![
2487                            vec![round, round, round, round, 0],
2488                            vec![round, round, round, round, 0],
2489                            vec![round, round, round, round, 0],
2490                            vec![round, round, round, round, 0],
2491                            vec![0, 0, 0, 0, 0],
2492                        ],
2493                        vec![
2494                            vec![round, round, round, round, 0],
2495                            vec![round, round, round, round, 0],
2496                            vec![round, round, round, round, 0],
2497                            vec![round, round, round, round, 0],
2498                            vec![0, 0, 0, 0, 0],
2499                        ],
2500                    );
2501
2502                // Only when round > 1 and using non-genesis parents.
2503                if let Some(r) = last_round_blocks.first().map(|b| b.round()) {
2504                    assert_eq!(round - 1, r);
2505                    if core_fixture.core.last_proposed_round() == Some(r) {
2506                        // Force propose new block regardless of min round delay.
2507                        core_fixture
2508                            .core
2509                            .try_propose(true)
2510                            .unwrap()
2511                            .unwrap_or_else(|| {
2512                                panic!("Block should have been proposed for round {}", round)
2513                            });
2514                    }
2515                }
2516
2517                this_round_blocks.push(core_fixture.core.last_proposed_block().clone());
2518
2519                for block in this_round_blocks.iter() {
2520                    if block.author() != AuthorityIndex::new_for_test(4) {
2521                        // Assert blocks created include only 4 ancestors per block as one
2522                        // should be excluded
2523                        assert_eq!(block.ancestors().len(), 4);
2524                    } else {
2525                        // Authority 3 is the low scoring authority so it will still include
2526                        // its own blocks.
2527                        assert_eq!(block.ancestors().len(), 5);
2528                    }
2529                }
2530            }
2531
2532            last_round_blocks = this_round_blocks;
2533        }
2534    }
2535
2536    #[tokio::test]
2537    async fn test_smart_ancestor_selection() {
2538        telemetry_subscribers::init_for_testing();
2539        let (context, _) = Context::new_for_test(7);
2540        let context = context.with_parameters(Parameters {
2541            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
2542            ..Default::default()
2543        });
2544        let mut fixture =
2545            CoreTestFixture::new(context, vec![1; 7], AuthorityIndex::new_for_test(0), true).await;
2546        let min_round_delay = fixture.core.context.parameters.min_round_delay;
2547
2548        // No new block should have been produced
2549        assert_eq!(
2550            fixture.core.last_proposed_round(),
2551            Some(GENESIS_ROUND),
2552            "No block should have been created other than genesis"
2553        );
2554
2555        // Trying to explicitly propose a block will not produce anything
2556        assert!(fixture.core.try_propose(true).unwrap().is_none());
2557
2558        // Create blocks for the whole network but not for authority 1
2559        let mut builder = DagBuilder::new(fixture.core.context.clone());
2560        builder
2561            .layers(1..=12)
2562            .authorities(vec![AuthorityIndex::new_for_test(1)])
2563            .skip_block()
2564            .build();
2565        let blocks = builder.blocks(1..=12);
2566        // Process all the blocks
2567        assert!(fixture.add_blocks(blocks).unwrap().is_empty());
2568        fixture.core.set_last_known_proposed_round(12);
2569
2570        fixture
2571            .core
2572            .round_tracker_for_tests()
2573            .write()
2574            .update_from_probe(
2575                vec![
2576                    vec![12, 12, 12, 12, 12, 12, 12],
2577                    vec![0, 0, 0, 0, 0, 0, 0],
2578                    vec![12, 12, 12, 12, 12, 12, 12],
2579                    vec![12, 12, 12, 12, 12, 12, 12],
2580                    vec![12, 12, 12, 12, 12, 12, 12],
2581                    vec![12, 12, 12, 12, 12, 12, 12],
2582                    vec![12, 12, 12, 12, 12, 12, 12],
2583                ],
2584                vec![
2585                    vec![12, 12, 12, 12, 12, 12, 12],
2586                    vec![0, 0, 0, 0, 0, 0, 0],
2587                    vec![12, 12, 12, 12, 12, 12, 12],
2588                    vec![12, 12, 12, 12, 12, 12, 12],
2589                    vec![12, 12, 12, 12, 12, 12, 12],
2590                    vec![12, 12, 12, 12, 12, 12, 12],
2591                    vec![12, 12, 12, 12, 12, 12, 12],
2592                ],
2593            );
2594
2595        let block = fixture.core.try_propose(true).expect("No error").unwrap();
2596        assert_eq!(block.round(), 13);
2597        assert_eq!(block.ancestors().len(), 7);
2598
2599        // Build blocks for rest of the network other than own index
2600        builder
2601            .layers(13..=14)
2602            .authorities(vec![AuthorityIndex::new_for_test(0)])
2603            .skip_block()
2604            .build();
2605        let blocks = builder.blocks(13..=14);
2606        assert!(fixture.add_blocks(blocks).unwrap().is_empty());
2607
2608        // We now have triggered a leader schedule change so we should have
2609        // one EXCLUDE authority (1) when we go to select ancestors for the next proposal
2610        let block = fixture.core.try_propose(true).expect("No error").unwrap();
2611        assert_eq!(block.round(), 15);
2612        assert_eq!(block.ancestors().len(), 6);
2613
2614        // Build blocks for a quorum of the network including the EXCLUDE authority (1)
2615        // which will trigger smart select and we will not propose a block
2616        let round_14_ancestors = builder.last_ancestors.clone();
2617        builder
2618            .layer(15)
2619            .authorities(vec![
2620                AuthorityIndex::new_for_test(0),
2621                AuthorityIndex::new_for_test(5),
2622                AuthorityIndex::new_for_test(6),
2623            ])
2624            .skip_block()
2625            .build();
2626        let blocks = builder.blocks(15..=15);
2627        let authority_1_excluded_block_reference = blocks
2628            .iter()
2629            .find(|block| block.author() == AuthorityIndex::new_for_test(1))
2630            .unwrap()
2631            .reference();
2632        // Wait for min round delay to allow blocks to be proposed.
2633        sleep(min_round_delay).await;
2634        // Smart select should be triggered and no block should be proposed.
2635        assert!(fixture.add_blocks(blocks).unwrap().is_empty());
2636        assert_eq!(fixture.core.last_proposed_block().round(), 15);
2637
2638        builder
2639            .layer(15)
2640            .authorities(vec![
2641                AuthorityIndex::new_for_test(0),
2642                AuthorityIndex::new_for_test(1),
2643                AuthorityIndex::new_for_test(2),
2644                AuthorityIndex::new_for_test(3),
2645                AuthorityIndex::new_for_test(4),
2646            ])
2647            .skip_block()
2648            .override_last_ancestors(round_14_ancestors)
2649            .build();
2650        let blocks = builder.blocks(15..=15);
2651        let round_15_ancestors: Vec<BlockRef> = blocks
2652            .iter()
2653            .filter(|block| block.round() == 15)
2654            .map(|block| block.reference())
2655            .collect();
2656        let included_block_references = iter::once(&fixture.core.last_proposed_block())
2657            .chain(blocks.iter())
2658            .filter(|block| block.author() != AuthorityIndex::new_for_test(1))
2659            .map(|block| block.reference())
2660            .collect::<Vec<_>>();
2661
2662        // Have enough ancestor blocks to propose now.
2663        assert!(fixture.add_blocks(blocks).unwrap().is_empty());
2664        assert_eq!(fixture.core.last_proposed_block().round(), 16);
2665
2666        // Check that a new block has been proposed & signaled.
2667        let extended_block = loop {
2668            let extended_block =
2669                tokio::time::timeout(Duration::from_secs(1), fixture.block_receiver.recv())
2670                    .await
2671                    .unwrap()
2672                    .unwrap();
2673            if extended_block.block.round() == 16 {
2674                break extended_block;
2675            }
2676        };
2677        assert_eq!(extended_block.block.round(), 16);
2678        assert_eq!(
2679            extended_block.block.author(),
2680            fixture.core.context.own_index
2681        );
2682        assert_eq!(extended_block.block.ancestors().len(), 6);
2683        assert_eq!(extended_block.block.ancestors(), included_block_references);
2684        assert_eq!(extended_block.excluded_ancestors.len(), 1);
2685        assert_eq!(
2686            extended_block.excluded_ancestors[0],
2687            authority_1_excluded_block_reference
2688        );
2689
2690        // Build blocks for a quorum of the network including the EXCLUDE ancestor
2691        // which will trigger smart select and we will not propose a block.
2692        // This time we will force propose by hitting the leader timeout after which
2693        // should cause us to include this EXCLUDE ancestor.
2694        builder
2695            .layer(16)
2696            .authorities(vec![
2697                AuthorityIndex::new_for_test(0),
2698                AuthorityIndex::new_for_test(5),
2699                AuthorityIndex::new_for_test(6),
2700            ])
2701            .skip_block()
2702            .override_last_ancestors(round_15_ancestors)
2703            .build();
2704        let blocks = builder.blocks(16..=16);
2705        // Wait for leader timeout to force blocks to be proposed.
2706        sleep(min_round_delay).await;
2707        // Smart select should be triggered and no block should be proposed.
2708        assert!(fixture.add_blocks(blocks).unwrap().is_empty());
2709        assert_eq!(fixture.core.last_proposed_block().round(), 16);
2710
2711        // Simulate a leader timeout and a force proposal where we will include
2712        // one EXCLUDE ancestor when we go to select ancestors for the next proposal
2713        let block = fixture.core.try_propose(true).expect("No error").unwrap();
2714        assert_eq!(block.round(), 17);
2715        assert_eq!(block.ancestors().len(), 5);
2716
2717        // Check that a new block has been proposed & signaled.
2718        let extended_block =
2719            tokio::time::timeout(Duration::from_secs(1), fixture.block_receiver.recv())
2720                .await
2721                .unwrap()
2722                .unwrap();
2723        assert_eq!(extended_block.block.round(), 17);
2724        assert_eq!(
2725            extended_block.block.author(),
2726            fixture.core.context.own_index
2727        );
2728        assert_eq!(extended_block.block.ancestors().len(), 5);
2729        assert_eq!(extended_block.excluded_ancestors.len(), 0);
2730
2731        // Excluded authority is locked until round 20, simulate enough rounds to
2732        // unlock
2733        builder
2734            .layers(17..=22)
2735            .authorities(vec![AuthorityIndex::new_for_test(0)])
2736            .skip_block()
2737            .build();
2738        let blocks = builder.blocks(17..=22);
2739
2740        // Simulate updating received and accepted rounds from prober.
2741        // New quorum rounds for authority can then be computed which will unlock
2742        // the Excluded authority (1) and then we should be able to create a new
2743        // layer of blocks which will then all be included as ancestors for the
2744        // next proposal
2745        fixture
2746            .core
2747            .round_tracker_for_tests()
2748            .write()
2749            .update_from_probe(
2750                vec![
2751                    vec![22, 22, 22, 22, 22, 22, 22],
2752                    vec![22, 22, 22, 22, 22, 22, 22],
2753                    vec![22, 22, 22, 22, 22, 22, 22],
2754                    vec![22, 22, 22, 22, 22, 22, 22],
2755                    vec![22, 22, 22, 22, 22, 22, 22],
2756                    vec![22, 22, 22, 22, 22, 22, 22],
2757                    vec![22, 22, 22, 22, 22, 22, 22],
2758                ],
2759                vec![
2760                    vec![22, 22, 22, 22, 22, 22, 22],
2761                    vec![22, 22, 22, 22, 22, 22, 22],
2762                    vec![22, 22, 22, 22, 22, 22, 22],
2763                    vec![22, 22, 22, 22, 22, 22, 22],
2764                    vec![22, 22, 22, 22, 22, 22, 22],
2765                    vec![22, 22, 22, 22, 22, 22, 22],
2766                    vec![22, 22, 22, 22, 22, 22, 22],
2767                ],
2768            );
2769
2770        let own_index = fixture.core.context.own_index;
2771        let included_block_references = iter::once(&fixture.core.last_proposed_block())
2772            .chain(blocks.iter())
2773            .filter(|block| block.round() == 22 || block.author() == own_index)
2774            .map(|block| block.reference())
2775            .collect::<Vec<_>>();
2776
2777        // Have enough ancestor blocks to propose now.
2778        sleep(min_round_delay).await;
2779        assert!(fixture.add_blocks(blocks).unwrap().is_empty());
2780        assert_eq!(fixture.core.last_proposed_block().round(), 23);
2781
2782        // Check that a new block has been proposed & signaled.
2783        let extended_block =
2784            tokio::time::timeout(Duration::from_secs(1), fixture.block_receiver.recv())
2785                .await
2786                .unwrap()
2787                .unwrap();
2788        assert_eq!(extended_block.block.round(), 23);
2789        assert_eq!(
2790            extended_block.block.author(),
2791            fixture.core.context.own_index
2792        );
2793        assert_eq!(extended_block.block.ancestors().len(), 7);
2794        assert_eq!(extended_block.block.ancestors(), included_block_references);
2795        assert_eq!(extended_block.excluded_ancestors.len(), 0);
2796    }
2797
2798    // Builds a core of 7 authorities and adds blocks up to round 14, with no block from
2799    // authority 1. The core proposed its last block at round 13. The first proposal of the
2800    // caller gives authority 1 the EXCLUDE state.
2801    async fn fixture_with_excluded_authority(
2802        enable_v3: bool,
2803    ) -> (CoreTestFixture, DagBuilder, AuthorityIndex) {
2804        let (mut context, _) = Context::new_for_test(7);
2805        context.protocol_config.set_enable_v3_for_testing(enable_v3);
2806        let context = context.with_parameters(Parameters {
2807            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
2808            ..Default::default()
2809        });
2810        let mut fixture =
2811            CoreTestFixture::new(context, vec![1; 7], AuthorityIndex::new_for_test(0), true).await;
2812        let excluded = AuthorityIndex::new_for_test(1);
2813
2814        // The DAG holds no block from this authority, so its propagation score stays zero and
2815        // the proposer moves it to the EXCLUDE state. The state lock keeps that state for the
2816        // next rounds.
2817        let mut builder = DagBuilder::new(fixture.core.context.clone());
2818        builder
2819            .layers(1..=12)
2820            .authorities(vec![excluded])
2821            .skip_block()
2822            .build();
2823        assert!(
2824            fixture
2825                .add_blocks(builder.blocks(1..=12))
2826                .unwrap()
2827                .is_empty()
2828        );
2829        fixture.core.set_last_known_proposed_round(12);
2830        let mut quorum_rounds = vec![vec![12; 7]; 7];
2831        quorum_rounds[excluded] = vec![0; 7];
2832        fixture
2833            .core
2834            .round_tracker_for_tests()
2835            .write()
2836            .update_from_probe(quorum_rounds.clone(), quorum_rounds);
2837        assert_eq!(
2838            fixture
2839                .core
2840                .try_propose(true)
2841                .expect("No error")
2842                .unwrap()
2843                .round(),
2844            13
2845        );
2846
2847        builder
2848            .layers(13..=14)
2849            .authorities(vec![AuthorityIndex::new_for_test(0)])
2850            .skip_block()
2851            .build();
2852        assert!(
2853            fixture
2854                .add_blocks(builder.blocks(13..=14))
2855                .unwrap()
2856                .is_empty()
2857        );
2858        (fixture, builder, excluded)
2859    }
2860
2861    // A v3 proposal must keep a leader ancestor which has the EXCLUDE state.
2862    #[tokio::test]
2863    async fn test_smart_ancestor_selection_keeps_v3_leader() {
2864        telemetry_subscribers::init_for_testing();
2865        let (mut fixture, mut builder, excluded) = fixture_with_excluded_authority(true).await;
2866        // Each commit sets the leader schedule of the proposer, so the schedule below must be set
2867        // after the last add_blocks() and before the proposal which reads it.
2868        let set_leader = |fixture: &mut CoreTestFixture, leader: AuthorityIndex, round: Round| {
2869            fixture
2870                .core
2871                .proposer
2872                .as_mut()
2873                .unwrap()
2874                .set_next_commit_leader_schedule(NextCommitLeaderSchedule {
2875                    next_commit_index: 1,
2876                    min_next_leader_round: round,
2877                    allowed_leaders: vec![leader],
2878                });
2879        };
2880
2881        // Another authority is the leader of round 14, so the proposal excludes authority 1.
2882        set_leader(&mut fixture, AuthorityIndex::new_for_test(2), 14);
2883        let block = fixture.core.try_propose(true).expect("No error").unwrap();
2884        assert_eq!(block.round(), 15);
2885        assert!(
2886            block.ancestors().iter().all(|a| a.author != excluded),
2887            "The EXCLUDE authority is not a leader, so the proposal must not include it"
2888        );
2889
2890        builder
2891            .layer(15)
2892            .authorities(vec![AuthorityIndex::new_for_test(0)])
2893            .skip_block()
2894            .build();
2895        assert!(
2896            fixture
2897                .add_blocks(builder.blocks(15..=15))
2898                .unwrap()
2899                .is_empty()
2900        );
2901        // The minimum round delay stops a proposal inside add_blocks(), so the leader schedule
2902        // below is the schedule of the round 16 proposal.
2903        assert_eq!(fixture.core.last_proposed_block().round(), 15);
2904
2905        // Make the EXCLUDE authority the only allowed leader of round 15.
2906        set_leader(&mut fixture, excluded, 15);
2907        let proposed = fixture.core.try_propose(true).expect("No error").unwrap();
2908        assert_eq!(proposed.round(), 16);
2909        assert!(
2910            proposed.ancestors().iter().any(|a| a.author == excluded),
2911            "The proposal must include the leader block, even with its EXCLUDE state"
2912        );
2913    }
2914
2915    // A v2 proposal must not keep a leader ancestor which has the EXCLUDE state.
2916    #[tokio::test]
2917    async fn test_smart_ancestor_selection_excludes_v2_leader() {
2918        telemetry_subscribers::init_for_testing();
2919        let (mut fixture, mut builder, excluded) = fixture_with_excluded_authority(false).await;
2920
2921        let block = fixture.core.try_propose(true).expect("No error").unwrap();
2922        assert_eq!(block.round(), 15);
2923
2924        builder
2925            .layer(15)
2926            .authorities(vec![AuthorityIndex::new_for_test(0)])
2927            .skip_block()
2928            .build();
2929        assert!(
2930            fixture
2931                .add_blocks(builder.blocks(15..=15))
2932                .unwrap()
2933                .is_empty()
2934        );
2935
2936        // The minimum round delay stops a proposal inside add_blocks(), so the swap table below
2937        // is the table of the round 16 proposal.
2938        assert_eq!(fixture.core.last_proposed_block().round(), 15);
2939
2940        // The leader schedule removes a bad node from the leader slots, so the default swap
2941        // table is necessary to make the EXCLUDE authority the leader of round 15. Test leader
2942        // election is `round % committee size`, which selects authority 1 for round 15. A commit
2943        // can update the swap table, so the table must be set after the last add_blocks().
2944        *fixture.core.leader_schedule.leader_swap_table.write() = LeaderSwapTable::default();
2945        assert_eq!(fixture.core.committer.get_leaders(15), vec![excluded]);
2946
2947        let proposed = fixture.core.try_propose(true).expect("No error").unwrap();
2948        assert_eq!(proposed.round(), 16);
2949        assert!(
2950            proposed.ancestors().iter().all(|a| a.author != excluded),
2951            "V2 must not keep the EXCLUDE ancestor, even when it is the leader"
2952        );
2953    }
2954
2955    #[tokio::test]
2956    async fn test_excluded_ancestor_limit() {
2957        telemetry_subscribers::init_for_testing();
2958        let (context, _) = Context::new_for_test(4);
2959        let context = context.with_parameters(Parameters {
2960            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
2961            ..Default::default()
2962        });
2963        let mut fixture = CoreTestFixture::new(
2964            context,
2965            vec![1, 1, 1, 1],
2966            AuthorityIndex::new_for_test(0),
2967            true,
2968        )
2969        .await;
2970
2971        // No new block should have been produced
2972        assert_eq!(
2973            fixture.core.last_proposed_round(),
2974            Some(GENESIS_ROUND),
2975            "No block should have been created other than genesis"
2976        );
2977
2978        // Create blocks for the whole network
2979        let mut builder = DagBuilder::new(fixture.core.context.clone());
2980        builder.layers(1..=3).build();
2981
2982        // This will equivocate 9 blocks for authority 1 which will be excluded on
2983        // the proposal but because of the limits set will be dropped and not included
2984        // as part of the ExtendedBlock structure sent to the rest of the network
2985        builder
2986            .layer(4)
2987            .authorities(vec![AuthorityIndex::new_for_test(1)])
2988            .equivocate(9)
2989            .build();
2990        let blocks = builder.blocks(1..=4);
2991
2992        // Process all the blocks
2993        assert!(fixture.add_blocks(blocks).unwrap().is_empty());
2994        fixture.core.set_last_known_proposed_round(3);
2995
2996        let block = fixture.core.try_propose(true).expect("No error").unwrap();
2997        assert_eq!(block.round(), 5);
2998        assert_eq!(block.ancestors().len(), 4);
2999
3000        // Check that a new block has been proposed & signaled.
3001        let extended_block =
3002            tokio::time::timeout(Duration::from_secs(1), fixture.block_receiver.recv())
3003                .await
3004                .unwrap()
3005                .unwrap();
3006        assert_eq!(extended_block.block.round(), 5);
3007        assert_eq!(
3008            extended_block.block.author(),
3009            fixture.core.context.own_index
3010        );
3011        assert_eq!(extended_block.block.ancestors().len(), 4);
3012        assert_eq!(extended_block.excluded_ancestors.len(), 8);
3013    }
3014
3015    #[tokio::test]
3016    async fn test_core_set_propagation_delay_per_authority() {
3017        telemetry_subscribers::init_for_testing();
3018        let (context, _) = Context::new_for_test(4);
3019        let mut fixture = CoreTestFixture::new(
3020            context,
3021            vec![1, 1, 1, 1],
3022            AuthorityIndex::new_for_test(0),
3023            false,
3024        )
3025        .await;
3026
3027        // Use a large propagation delay to disable proposing.
3028        // This is done by accepting an own block at round 1000 to dag state and
3029        // then simulating updating round tracker received rounds from probe where
3030        // low quorum round for own index should get calculated to round 0.
3031        let test_block = VerifiedBlock::new_for_test(TestBlock::new(1000, 0).build());
3032        fixture
3033            .transaction_vote_tracker
3034            .add_voted_blocks(vec![(test_block.clone(), vec![])]);
3035        // Force accepting the block to dag state because its causal history is incomplete.
3036        fixture.dag_state.write().accept_block(test_block);
3037
3038        fixture
3039            .core
3040            .round_tracker_for_tests()
3041            .write()
3042            .update_from_probe(
3043                vec![
3044                    vec![0, 0, 0, 0],
3045                    vec![0, 0, 0, 0],
3046                    vec![0, 0, 0, 0],
3047                    vec![0, 0, 0, 0],
3048                ],
3049                vec![
3050                    vec![0, 0, 0, 0],
3051                    vec![0, 0, 0, 0],
3052                    vec![0, 0, 0, 0],
3053                    vec![0, 0, 0, 0],
3054                ],
3055            );
3056
3057        // There is no proposal even with forced proposing.
3058        assert!(fixture.core.try_propose(true).unwrap().is_none());
3059
3060        // Let Core know there is no propagation delay.
3061        // This is done by simulating updating round tracker recieved rounds from probe
3062        // where low quorum round for own index should get calculated to round 1000.
3063        fixture
3064            .core
3065            .round_tracker_for_tests()
3066            .write()
3067            .update_from_probe(
3068                vec![
3069                    vec![1000, 1000, 1000, 1000],
3070                    vec![1000, 1000, 1000, 1000],
3071                    vec![1000, 1000, 1000, 1000],
3072                    vec![1000, 1000, 1000, 1000],
3073                ],
3074                vec![
3075                    vec![1000, 1000, 1000, 1000],
3076                    vec![1000, 1000, 1000, 1000],
3077                    vec![1000, 1000, 1000, 1000],
3078                    vec![1000, 1000, 1000, 1000],
3079                ],
3080            );
3081
3082        // Also add the necessary blocks from round 1000 so core will propose for
3083        // round 1001
3084        for author in 1..4 {
3085            let block = VerifiedBlock::new_for_test(TestBlock::new(1000, author).build());
3086            fixture
3087                .transaction_vote_tracker
3088                .add_voted_blocks(vec![(block.clone(), vec![])]);
3089            // Force accepting the block to dag state because its causal history is incomplete.
3090            fixture.dag_state.write().accept_block(block);
3091        }
3092
3093        // Proposing now would succeed.
3094        assert!(fixture.core.try_propose(true).unwrap().is_some());
3095    }
3096
3097    #[tokio::test(flavor = "current_thread", start_paused = true)]
3098    async fn test_leader_schedule_change() {
3099        telemetry_subscribers::init_for_testing();
3100        let default_params = Parameters::default();
3101
3102        let (context, _) = Context::new_for_test(4);
3103        // create the cores and their signals for all the authorities
3104        let mut cores = create_cores(context, vec![1, 1, 1, 1]).await;
3105
3106        // Now iterate over a few rounds and ensure the corresponding signals are created while network advances
3107        let mut last_round_blocks = Vec::new();
3108        for round in 1..=30 {
3109            let mut this_round_blocks = Vec::new();
3110
3111            // Wait for min round delay to allow blocks to be proposed.
3112            sleep(default_params.min_round_delay).await;
3113
3114            for core_fixture in &mut cores {
3115                // add the blocks from last round
3116                // this will trigger a block creation for the round and a signal should be emitted
3117                core_fixture.add_blocks(last_round_blocks.clone()).unwrap();
3118
3119                core_fixture
3120                    .core
3121                    .round_tracker_for_tests()
3122                    .write()
3123                    .update_from_probe(
3124                        vec![
3125                            vec![round, round, round, round],
3126                            vec![round, round, round, round],
3127                            vec![round, round, round, round],
3128                            vec![round, round, round, round],
3129                        ],
3130                        vec![
3131                            vec![round, round, round, round],
3132                            vec![round, round, round, round],
3133                            vec![round, round, round, round],
3134                            vec![round, round, round, round],
3135                        ],
3136                    );
3137
3138                // A "new round" signal should be received given that all the blocks of previous round have been processed
3139                let new_round = receive(
3140                    Duration::from_secs(1),
3141                    core_fixture.signal_receivers.new_round_receiver(),
3142                )
3143                .await;
3144                assert_eq!(new_round, round);
3145
3146                // Check that a new block has been proposed.
3147                let extended_block = tokio::time::timeout(
3148                    Duration::from_secs(1),
3149                    core_fixture.block_receiver.recv(),
3150                )
3151                .await
3152                .unwrap()
3153                .unwrap();
3154                assert_eq!(extended_block.block.round(), round);
3155                assert_eq!(
3156                    extended_block.block.author(),
3157                    core_fixture.core.context.own_index
3158                );
3159
3160                // append the new block to this round blocks
3161                this_round_blocks.push(core_fixture.core.last_proposed_block().clone());
3162
3163                let block = core_fixture.core.last_proposed_block();
3164
3165                // ensure that produced block is referring to the blocks of last_round
3166                assert_eq!(
3167                    block.ancestors().len(),
3168                    core_fixture.core.context.committee.size()
3169                );
3170                for ancestor in block.ancestors() {
3171                    if block.round() > 1 {
3172                        // don't bother with round 1 block which just contains the genesis blocks.
3173                        assert!(
3174                            last_round_blocks
3175                                .iter()
3176                                .any(|block| block.reference() == *ancestor),
3177                            "Reference from previous round should be added"
3178                        );
3179                    }
3180                }
3181            }
3182
3183            last_round_blocks = this_round_blocks;
3184        }
3185
3186        for core_fixture in cores {
3187            // Flush the DAG state to storage.
3188            core_fixture.dag_state.write().flush();
3189
3190            // Check commits have been persisted to store
3191            let last_commit = core_fixture
3192                .store
3193                .read_last_commit()
3194                .unwrap()
3195                .expect("last commit should be set");
3196            // There are 28 leader rounds with rounds completed up to and including
3197            // round 29. Round 30 blocks will only include their own blocks, so the
3198            // 28th leader will not be committed.
3199            assert_eq!(last_commit.index(), 27);
3200            let all_stored_commits = core_fixture
3201                .store
3202                .scan_commits((0..=CommitIndex::MAX).into())
3203                .unwrap();
3204            assert_eq!(all_stored_commits.len(), 27);
3205            assert_eq!(
3206                core_fixture
3207                    .core
3208                    .leader_schedule
3209                    .leader_swap_table
3210                    .read()
3211                    .bad_nodes
3212                    .len(),
3213                1
3214            );
3215            assert_eq!(
3216                core_fixture
3217                    .core
3218                    .leader_schedule
3219                    .leader_swap_table
3220                    .read()
3221                    .good_nodes
3222                    .len(),
3223                1
3224            );
3225            let expected_reputation_scores =
3226                ReputationScores::new((11..=20).into(), vec![29, 29, 29, 29]);
3227            assert_eq!(
3228                core_fixture
3229                    .core
3230                    .leader_schedule
3231                    .leader_swap_table
3232                    .read()
3233                    .reputation_scores,
3234                expected_reputation_scores
3235            );
3236        }
3237    }
3238
3239    #[tokio::test]
3240    async fn test_filter_new_commits() {
3241        telemetry_subscribers::init_for_testing();
3242
3243        let (context, _key_pairs) = Context::new_for_test(4);
3244        let context = context.with_parameters(Parameters {
3245            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
3246            ..Default::default()
3247        });
3248
3249        let authority_index = AuthorityIndex::new_for_test(0);
3250        let core = CoreTestFixture::new(context, vec![1, 1, 1, 1], authority_index, true).await;
3251        let mut core = core.core;
3252
3253        // No new block should have been produced
3254        assert_eq!(
3255            core.last_proposed_round(),
3256            Some(GENESIS_ROUND),
3257            "No block should have been created other than genesis"
3258        );
3259
3260        // create a DAG of 12 rounds
3261        let mut dag_builder = DagBuilder::new(core.context.clone());
3262        dag_builder.layers(1..=12).build();
3263
3264        // Store all blocks up to round 6 which should be enough to decide up to leader 4
3265        dag_builder.print();
3266        let blocks = dag_builder.blocks(1..=6);
3267        core.dag_state.write().accept_blocks(blocks);
3268
3269        // Get all the committed sub dags up to round 10
3270        let sub_dags_and_commits = dag_builder.get_sub_dag_and_certified_commits(1..=10);
3271
3272        // Now try to commit up to the latest leader (round = 4). Do not provide any certified commits.
3273        let committed_sub_dags = core.try_commit(vec![]).unwrap();
3274
3275        // We should have committed up to round 4
3276        assert_eq!(committed_sub_dags.len(), 4);
3277
3278        // Now validate the certified commits. We'll try 3 different scenarios:
3279        println!("Case 1. Provide certified commits that are all before the last committed round.");
3280
3281        // Highest certified commit should be for leader of round 4.
3282        let certified_commits = sub_dags_and_commits
3283            .iter()
3284            .take(4)
3285            .map(|(_, c)| c)
3286            .cloned()
3287            .collect::<Vec<_>>();
3288        assert!(
3289            certified_commits.last().unwrap().index()
3290                <= committed_sub_dags.last().unwrap().commit_ref.index,
3291            "Highest certified commit should older than the highest committed index."
3292        );
3293
3294        let certified_commits = core.filter_new_commits(certified_commits).unwrap();
3295
3296        // No commits should be processed
3297        assert!(certified_commits.is_empty());
3298
3299        println!("Case 2. Provide certified commits that are all after the last committed round.");
3300
3301        // Highest certified commit should be for leader of round 4.
3302        let certified_commits = sub_dags_and_commits
3303            .iter()
3304            .take(5)
3305            .map(|(_, c)| c.clone())
3306            .collect::<Vec<_>>();
3307
3308        let certified_commits = core.filter_new_commits(certified_commits.clone()).unwrap();
3309
3310        // The certified commit of index 5 should be processed.
3311        assert_eq!(certified_commits.len(), 1);
3312        assert_eq!(certified_commits.first().unwrap().reference().index, 5);
3313
3314        println!(
3315            "Case 3. Provide certified commits where the first certified commit index is not the last_commited_index + 1."
3316        );
3317
3318        // Highest certified commit should be for leader of round 4.
3319        let certified_commits = sub_dags_and_commits
3320            .iter()
3321            .skip(5)
3322            .take(1)
3323            .map(|(_, c)| c.clone())
3324            .collect::<Vec<_>>();
3325
3326        let err = core
3327            .filter_new_commits(certified_commits.clone())
3328            .unwrap_err();
3329        match err {
3330            ConsensusError::UnexpectedCertifiedCommitIndex {
3331                expected_commit_index: 5,
3332                commit_index: 6,
3333            } => (),
3334            _ => panic!("Unexpected error: {:?}", err),
3335        }
3336    }
3337
3338    #[tokio::test]
3339    async fn test_add_certified_commits() {
3340        telemetry_subscribers::init_for_testing();
3341
3342        let (context, _key_pairs) = Context::new_for_test(4);
3343        let context = context.with_parameters(Parameters {
3344            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
3345            ..Default::default()
3346        });
3347
3348        let authority_index = AuthorityIndex::new_for_test(0);
3349        let core = CoreTestFixture::new(context, vec![1, 1, 1, 1], authority_index, true).await;
3350        let store = core.store.clone();
3351        let mut core = core.core;
3352
3353        // No new block should have been produced
3354        assert_eq!(
3355            core.last_proposed_round(),
3356            Some(GENESIS_ROUND),
3357            "No block should have been created other than genesis"
3358        );
3359
3360        // create a DAG of 12 rounds
3361        let mut dag_builder = DagBuilder::new(core.context.clone());
3362        dag_builder.layers(1..=12).build();
3363
3364        // Store all blocks up to round 6 which should be enough to decide up to leader 4
3365        dag_builder.print();
3366        let blocks = dag_builder.blocks(1..=6);
3367        core.dag_state.write().accept_blocks(blocks);
3368
3369        // Get all the committed sub dags up to round 10
3370        let sub_dags_and_commits = dag_builder.get_sub_dag_and_certified_commits(1..=10);
3371
3372        // Now try to commit up to the latest leader (round = 4). Do not provide any certified commits.
3373        let committed_sub_dags = core.try_commit(vec![]).unwrap();
3374
3375        // We should have committed up to round 4
3376        assert_eq!(committed_sub_dags.len(), 4);
3377
3378        // Flush the DAG state to storage.
3379        core.dag_state.write().flush();
3380
3381        {
3382            println!("Case 1. Provide no certified commits. No commit should happen.");
3383            // Now try to commit up to the latest leader (round = 4). Do not provide any certified commits.
3384            let committed_sub_dags = core.try_commit(vec![]).unwrap();
3385            assert!(committed_sub_dags.is_empty());
3386            let last_commit = store
3387                .read_last_commit()
3388                .unwrap()
3389                .expect("Last commit should be set");
3390            assert_eq!(last_commit.reference().index, 4);
3391        }
3392
3393        println!(
3394            "Case 2. Provide certified commits that before and after the last committed round and also there are additional blocks so can run the direct decide rule as well."
3395        );
3396
3397        // The commits of leader rounds 5-8 should be committed via the certified commits.
3398        let certified_commits = sub_dags_and_commits
3399            .iter()
3400            .skip(3)
3401            .take(5)
3402            .map(|(_, c)| c.clone())
3403            .collect::<Vec<_>>();
3404
3405        // Now only add the blocks of rounds 7..=12.
3406        let blocks = dag_builder.blocks(7..=12);
3407        core.dag_state.write().accept_blocks(blocks);
3408
3409        // The corresponding blocks of the certified commits should be accepted and stored before linearizing and committing the DAG.
3410        core.add_certified_commits(CertifiedCommits::new(certified_commits.clone(), vec![]))
3411            .expect("Should not fail");
3412
3413        // Flush the DAG state to storage.
3414        core.dag_state.write().flush();
3415
3416        let commits = store.scan_commits((6..=10).into()).unwrap();
3417
3418        // We expect all the sub dags up to leader round 10 to be committed.
3419        assert_eq!(commits.len(), 5);
3420
3421        for i in 6..=10 {
3422            let commit = &commits[i - 6];
3423            assert_eq!(commit.reference().index, i as u32);
3424        }
3425    }
3426
3427    #[tokio::test(start_paused = true)]
3428    async fn certified_commits_recover_after_flush_before_insertion() {
3429        use crate::linearizer::Linearizer;
3430
3431        // G is catching up while E returns from an outage with its old self-link.
3432        // The other five authorities maintain quorum without committing E1.
3433        let (_, builder) = parse_dag("DAG {
3434            Round 0 : { 7 },
3435            Round 1 : { A -> [*], B -> [*], C -> [*], D -> [*], E -> [*], F -> [*] },
3436            Round 2 : { A -> [-E1], B -> [-E1], C -> [-E1], D -> [-E1], F -> [-E1] },
3437            Round 3 : { A -> [*], B -> [*], C -> [*], D -> [*], F -> [*] },
3438            Round 4 : { A -> [*], B -> [*], C -> [*], D -> [*], F -> [*] },
3439            Round 5 : { A -> [*], B -> [*], C -> [*], D -> [*], E -> [A4, B4, C4, D4, F4, E1], F -> [*] },
3440            Round 6 : { A -> [*], B -> [*], C -> [*], D -> [*], E -> [*], F -> [*] },
3441            Round 7 : { A -> [*], B -> [*], C -> [*], D -> [*], E -> [*], F -> [*] }
3442        }").unwrap();
3443        let mut context = (*builder.context).clone();
3444        context.protocol_config.set_gc_depth_for_testing(4);
3445        context.protocol_config.set_enable_v3_for_testing(false);
3446        let own_index = AuthorityIndex::new_for_test(6);
3447        let block = |round, author| {
3448            builder
3449                .all_blocks()
3450                .into_iter()
3451                .find(|b| b.round() == round && b.author() == AuthorityIndex::new_for_test(author))
3452                .unwrap()
3453        };
3454        let e1 = block(1, 4);
3455        let e5 = block(5, 4);
3456        let leaders = vec![
3457            block(1, 1),
3458            block(2, 2),
3459            block(3, 3),
3460            block(5, 5),
3461            block(7, 0),
3462        ];
3463
3464        let source_store = Arc::new(MemStore::new());
3465        let source_dag = Arc::new(RwLock::new(DagState::new(
3466            Arc::new(context.clone()),
3467            source_store.clone(),
3468        )));
3469        source_dag.write().accept_blocks(builder.all_blocks());
3470        let subdags =
3471            Linearizer::new(Arc::new(context.clone()), source_dag.clone()).handle_commit(leaders);
3472        source_dag.write().flush();
3473        let certified_commits = source_store
3474            .scan_commits((1..=subdags.len() as u32).into())
3475            .unwrap()
3476            .into_iter()
3477            .zip_eq(&subdags)
3478            .map(|(commit, subdag)| CertifiedCommit::new_certified(commit, subdag.blocks.clone()))
3479            .collect::<Vec<_>>();
3480        assert!(subdags.iter().all(|subdag| {
3481            subdag
3482                .blocks
3483                .iter()
3484                .all(|b| b.reference() != e1.reference())
3485        }));
3486        assert!(
3487            subdags
3488                .last()
3489                .unwrap()
3490                .blocks
3491                .iter()
3492                .any(|b| b.reference() == e5.reference())
3493        );
3494
3495        for crash_before_index in 2..=certified_commits.len() {
3496            let mut fixture =
3497                CoreTestFixture::new(context.clone(), vec![1; 7], own_index, true).await;
3498            // Commit sync verifies and votes on fetched blocks before forwarding them to Core.
3499            fixture.transaction_vote_tracker.add_voted_blocks(
3500                certified_commits
3501                    .iter()
3502                    .flat_map(|commit| commit.blocks().iter().cloned())
3503                    .map(|block| (block, vec![]))
3504                    .collect(),
3505            );
3506            let (_, missing) = fixture.core.accept_blocks(
3507                builder
3508                    .all_blocks()
3509                    .into_iter()
3510                    .filter(|block| block.round() <= 3 && block.reference() != e1.reference())
3511                    .collect(),
3512            );
3513            assert!(missing.is_empty());
3514            let prior = fixture.core.try_commit(vec![]).unwrap();
3515            assert_eq!(prior.len(), 1);
3516            assert!(prior[0].decided_with_local_blocks);
3517            assert_eq!(prior[0].commit_ref, certified_commits[0].reference());
3518
3519            // A finalizer handling the prior commit can flush at any block-acceptance
3520            // boundary, before Core inserts the current certified commit.
3521            let dag = fixture.dag_state.clone();
3522            let accepted_leader = certified_commits[crash_before_index - 1].leader();
3523            let finalized_commit_ref = prior[0].commit_ref;
3524            let test_thread = std::thread::current().id();
3525            sui_macros::register_fail_point("consensus-after-accept-certified-blocks", move || {
3526                if std::thread::current().id() != test_thread {
3527                    return;
3528                }
3529                let mut dag = dag.write();
3530                if dag.last_commit_index() != crash_before_index as u32 - 1 {
3531                    return;
3532                }
3533                assert!(dag.contains_block(&accepted_leader));
3534                dag.add_finalized_commit(finalized_commit_ref, BTreeMap::new());
3535                dag.flush();
3536                panic!("crash after certified block acceptance");
3537            });
3538            let crash = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3539                fixture.core.try_commit(certified_commits[1..].to_vec())
3540            }));
3541            sui_macros::clear_fail_point("consensus-after-accept-certified-blocks");
3542            assert_eq!(
3543                crash.unwrap_err().downcast_ref::<&str>(),
3544                Some(&"crash after certified block acceptance")
3545            );
3546            let store = fixture.store.clone();
3547            assert_eq!(
3548                store.read_last_commit().unwrap().unwrap().index(),
3549                crash_before_index as u32 - 1
3550            );
3551            // Drain the fixture's tasks without adding the interrupted commit.
3552            fixture.core.stop().await;
3553            drop(fixture);
3554
3555            // Exercise Core's actual startup decision rule, then resume commit sync.
3556            // Batch acceptance would persist E5 with GC=0 and panic on its absent E1 ancestor.
3557            let mut recovered = CoreTestFixture::new_with_store(
3558                context.clone(),
3559                vec![1; 7],
3560                own_index,
3561                true,
3562                store.clone(),
3563            )
3564            .await;
3565            assert!(!recovered.dag_state.read().contains_block(&e1.reference()));
3566            let remaining = recovered
3567                .core
3568                .filter_new_commits(certified_commits.clone())
3569                .unwrap();
3570            recovered.transaction_vote_tracker.add_voted_blocks(
3571                remaining
3572                    .iter()
3573                    .flat_map(|commit| commit.blocks().iter().cloned())
3574                    .map(|block| (block, vec![]))
3575                    .collect(),
3576            );
3577            let resumed = recovered.core.try_commit(remaining).unwrap();
3578            assert!(
3579                resumed
3580                    .iter()
3581                    .all(|subdag| !subdag.decided_with_local_blocks)
3582            );
3583            recovered.dag_state.write().flush();
3584            let commits = store
3585                .scan_commits((1..=certified_commits.len() as u32).into())
3586                .unwrap();
3587            assert_eq!(commits.len(), certified_commits.len());
3588            for (actual, expected) in commits.iter().zip_eq(&certified_commits) {
3589                assert_eq!(actual.reference(), expected.reference());
3590            }
3591            recovered.core.stop().await;
3592        }
3593    }
3594
3595    #[tokio::test]
3596    async fn try_commit_v3_local_commits() {
3597        telemetry_subscribers::init_for_testing();
3598
3599        let (mut context, _key_pairs) = Context::new_for_test(4);
3600        context.protocol_config.set_enable_v3_for_testing(true);
3601        let context = context.with_parameters(Parameters {
3602            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
3603            ..Default::default()
3604        });
3605
3606        let authority_index = AuthorityIndex::new_for_test(0);
3607        let core = CoreTestFixture::new(context, vec![1, 1, 1, 1], authority_index, true).await;
3608        let monitor = core.commit_consumer_monitor.clone();
3609        let mut core = core.core;
3610
3611        // Build a fully connected DAG and accept its blocks, without any commits yet.
3612        let mut dag_builder = DagBuilder::new(core.context.clone());
3613        dag_builder.layers(1..=12).build();
3614        core.dag_state
3615            .write()
3616            .accept_blocks(dag_builder.blocks(1..=12));
3617
3618        let committed = core.try_commit_v3().unwrap();
3619
3620        // With a fully connected DAG up to round 12, the direct rule can decide leaders
3621        // up to round 11 (quorum of votes at leader round + 1), one commit per leader round.
3622        assert_eq!(committed.len(), 11);
3623        for (i, subdag) in committed.iter().enumerate() {
3624            assert_eq!(subdag.commit_ref.index, (i + 1) as CommitIndex);
3625            assert_eq!(subdag.leader.round, (i + 1) as Round);
3626            assert!(subdag.decided_with_local_blocks);
3627        }
3628        assert_eq!(core.dag_state.read().last_commit_index(), 11);
3629        assert_eq!(monitor.progress().highest_committed_index, 11);
3630        assert_eq!(monitor.progress().highest_committed_round, 11);
3631        assert_eq!(monitor.highest_handled_commit(), 0);
3632
3633        // Re-running the commit rule must be a no-op.
3634        assert!(core.try_commit_v3().unwrap().is_empty());
3635    }
3636
3637    #[tokio::test]
3638    async fn add_certified_commits_v3() {
3639        telemetry_subscribers::init_for_testing();
3640
3641        let (mut context, _key_pairs) = Context::new_for_test(4);
3642        context.protocol_config.set_enable_v3_for_testing(true);
3643        let context = context.with_parameters(Parameters {
3644            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
3645            ..Default::default()
3646        });
3647
3648        let authority_index = AuthorityIndex::new_for_test(0);
3649        let core = CoreTestFixture::new(context, vec![1, 1, 1, 1], authority_index, true).await;
3650        let store = core.store.clone();
3651        let mut core = core.core;
3652
3653        let mut dag_builder = DagBuilder::new(core.context.clone());
3654        dag_builder.layers(1..=12).build();
3655
3656        // Certified commits for leader rounds 1..=5. Their blocks have not been
3657        // accepted locally and are provided by the certified commits themselves.
3658        let certified_commits = dag_builder
3659            .get_sub_dag_and_certified_commits(1..=5)
3660            .into_iter()
3661            .map(|(_, c)| c)
3662            .collect::<Vec<_>>();
3663
3664        core.add_certified_commits(CertifiedCommits::new(certified_commits.clone(), vec![]))
3665            .expect("Should not fail");
3666
3667        assert_eq!(core.dag_state.read().last_commit_index(), 5);
3668        assert_eq!(
3669            core.context
3670                .metrics
3671                .node_metrics
3672                .core_certified_commits_processed
3673                .with_label_values(&["committed"])
3674                .get(),
3675            5
3676        );
3677
3678        // The certified commits should be stored as-is.
3679        core.dag_state.write().flush();
3680        let commits = store.scan_commits((1..=5).into()).unwrap();
3681        assert_eq!(commits.len(), 5);
3682        for (i, commit) in commits.iter().enumerate() {
3683            assert_eq!(commit.reference(), certified_commits[i].reference());
3684        }
3685
3686        // Accept the rest of the DAG. The local commit rule should continue from the
3687        // certified commits and commit the leaders of rounds 6..=11.
3688        core.dag_state
3689            .write()
3690            .accept_blocks(dag_builder.blocks(1..=12));
3691        let committed = core.try_commit_v3().unwrap();
3692        assert!(!committed.is_empty());
3693        assert!(committed.iter().all(|s| s.decided_with_local_blocks));
3694        assert_eq!(committed.last().unwrap().commit_ref.index, 11);
3695        assert_eq!(core.dag_state.read().last_commit_index(), 11);
3696
3697        // Re-sending the same certified commits should skip all of them.
3698        core.add_certified_commits(CertifiedCommits::new(certified_commits, vec![]))
3699            .expect("Should not fail");
3700        assert_eq!(core.dag_state.read().last_commit_index(), 11);
3701        assert_eq!(
3702            core.context
3703                .metrics
3704                .node_metrics
3705                .core_certified_commits_processed
3706                .with_label_values(&["skipped"])
3707                .get(),
3708            5
3709        );
3710    }
3711
3712    #[tokio::test]
3713    async fn add_multi_leader_certified_commits_v3() {
3714        const PREFIX_LAST_ROUND: Round = 5;
3715        const INITIAL_DAG_LAST_ROUND: Round = PREFIX_LAST_ROUND + 1;
3716        const CONTINUATION_DAG_LAST_ROUND: Round = INITIAL_DAG_LAST_ROUND + 2;
3717
3718        telemetry_subscribers::init_for_testing();
3719
3720        let (mut context, _key_pairs) = Context::new_for_test(4);
3721        context.protocol_config.set_enable_v3_for_testing(true);
3722        let context = context.with_parameters(Parameters {
3723            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
3724            ..Default::default()
3725        });
3726        let authority_index = AuthorityIndex::new_for_test(0);
3727
3728        // Use the real v3 commit path as the producer. The legacy DagBuilder certified-commit
3729        // helper creates single-leader commits and cannot exercise this commit shape.
3730        let source_fixture =
3731            CoreTestFixture::new(context.clone(), vec![1, 1, 1, 1], authority_index, true).await;
3732        let source_store = source_fixture.store.clone();
3733        let mut source = source_fixture.core;
3734
3735        let receiver_fixture =
3736            CoreTestFixture::new(context, vec![1, 1, 1, 1], authority_index, true).await;
3737        let receiver_store = receiver_fixture.store.clone();
3738        let mut receiver = receiver_fixture.core;
3739
3740        let next_commit_leaders = source
3741            .leader_schedule_v3
3742            .as_ref()
3743            .expect("LeaderScheduleV3 must be initialized")
3744            .next_commit_leader_schedule();
3745        assert!(next_commit_leaders.num_leaders() > 1);
3746        assert!(
3747            source
3748                .context
3749                .protocol_config
3750                .leader_schedule_update_interval()
3751                > PREFIX_LAST_ROUND
3752        );
3753        let scheduled_leaders = next_commit_leaders
3754            .allowed_leaders
3755            .iter()
3756            .copied()
3757            .collect::<BTreeSet<_>>();
3758
3759        let mut dag_builder = DagBuilder::new(source.context.clone());
3760        dag_builder.layers(1..=INITIAL_DAG_LAST_ROUND).build();
3761        source
3762            .dag_state
3763            .write()
3764            .accept_blocks(dag_builder.blocks(1..=INITIAL_DAG_LAST_ROUND));
3765
3766        let source_subdags = source.try_commit_v3().unwrap();
3767        // Round 6 supplies direct votes for leader rounds through round 5.
3768        assert_eq!(source_subdags.len(), PREFIX_LAST_ROUND as usize);
3769        source.dag_state.write().flush();
3770        let source_commits = source_store
3771            .scan_commits((1..=PREFIX_LAST_ROUND).into())
3772            .unwrap();
3773        assert_eq!(source_commits.len(), source_subdags.len());
3774
3775        let source_scores_at_prefix = source.current_reputation_scores();
3776        assert_ne!(source_scores_at_prefix, ReputationScores::default());
3777
3778        let certified_leader_counts = |core: &Core| {
3779            core.context
3780                .committee
3781                .authorities()
3782                .map(|(authority_index, authority)| {
3783                    (
3784                        authority_index,
3785                        core.context
3786                            .metrics
3787                            .node_metrics
3788                            .committed_leaders_total
3789                            .with_label_values(&[&authority.hostname, "certified-commit"])
3790                            .get(),
3791                    )
3792                })
3793                .collect::<BTreeMap<_, _>>()
3794        };
3795        // The fixtures share a metrics registry. No source commit below uses the
3796        // `certified-commit` label, and the baseline is taken before receiver processing.
3797        let certified_leaders_before = certified_leader_counts(&receiver);
3798
3799        let mut source_subdags_by_commit = source_subdags
3800            .into_iter()
3801            .map(|subdag| (subdag.commit_ref, subdag))
3802            .collect::<BTreeMap<_, _>>();
3803        let mut expected_certified_leaders = BTreeMap::<AuthorityIndex, u64>::new();
3804        let mut expected_receiver_subdags = Vec::new();
3805        let mut has_multi_leader_commit = false;
3806        let certified_commits = source_commits
3807            .iter()
3808            .map(|commit| {
3809                let subdag = source_subdags_by_commit
3810                    .remove(&commit.reference())
3811                    .expect("Every source commit must have a matching subdag");
3812                let block_refs = subdag
3813                    .blocks
3814                    .iter()
3815                    .map(|block| block.reference())
3816                    .collect::<Vec<_>>();
3817                assert_eq!(block_refs, commit.blocks());
3818
3819                // V3 treats each scheduled block in the named leader round as a leader.
3820                let leader_blocks = subdag
3821                    .blocks
3822                    .iter()
3823                    .filter(|block| block.round() == commit.leader().round)
3824                    .map(|block| block.reference())
3825                    .collect::<Vec<_>>();
3826                assert!(leader_blocks.contains(&commit.leader()));
3827                assert!(
3828                    leader_blocks
3829                        .iter()
3830                        .all(|block| scheduled_leaders.contains(&block.author))
3831                );
3832                has_multi_leader_commit |= leader_blocks.len() > 1;
3833                for block in leader_blocks {
3834                    *expected_certified_leaders.entry(block.author).or_default() += 1;
3835                }
3836
3837                let mut expected_receiver_subdag = subdag.clone();
3838                expected_receiver_subdag.decided_with_local_blocks = false;
3839                expected_receiver_subdags.push(expected_receiver_subdag);
3840
3841                // Certification is tested in CommitSyncer. This test starts after verification
3842                // and checks multi-leader reconstruction and continued local commits.
3843                CertifiedCommit::new_certified(commit.clone(), subdag.blocks)
3844            })
3845            .collect::<Vec<_>>();
3846        assert!(source_subdags_by_commit.is_empty());
3847        assert!(has_multi_leader_commit);
3848        assert!(expected_certified_leaders.values().sum::<u64>() > source_commits.len() as u64);
3849
3850        let receiver_subdags = receiver
3851            .process_certified_commits(certified_commits)
3852            .expect("Certified commits should be accepted");
3853        assert_eq!(receiver_subdags, expected_receiver_subdags);
3854        assert!(receiver.try_commit_v3().unwrap().is_empty());
3855        assert_eq!(
3856            receiver.dag_state.read().last_commit_index(),
3857            PREFIX_LAST_ROUND
3858        );
3859        assert_eq!(
3860            receiver.current_reputation_scores(),
3861            source_scores_at_prefix
3862        );
3863
3864        receiver.dag_state.write().flush();
3865        let receiver_commits = receiver_store
3866            .scan_commits((1..=PREFIX_LAST_ROUND).into())
3867            .unwrap();
3868        assert_eq!(receiver_commits, source_commits);
3869        let certified_leaders_after = certified_leader_counts(&receiver);
3870        for (authority_index, expected) in expected_certified_leaders {
3871            assert_eq!(
3872                certified_leaders_after[&authority_index]
3873                    - certified_leaders_before[&authority_index],
3874                expected
3875            );
3876        }
3877
3878        // Give both nodes the same remaining DAG. The receiver must produce the same next
3879        // commits as the source after the certified prefix.
3880        dag_builder
3881            .layers(INITIAL_DAG_LAST_ROUND + 1..=CONTINUATION_DAG_LAST_ROUND)
3882            .build();
3883        source.dag_state.write().accept_blocks(
3884            dag_builder.blocks(INITIAL_DAG_LAST_ROUND + 1..=CONTINUATION_DAG_LAST_ROUND),
3885        );
3886        // Re-accepting the prefix simulates normal block dissemination after commit sync.
3887        // DagState ignores blocks that the certified commits already marked committed.
3888        receiver
3889            .dag_state
3890            .write()
3891            .accept_blocks(dag_builder.blocks(1..=CONTINUATION_DAG_LAST_ROUND));
3892
3893        let source_continuation = source.try_commit_v3().unwrap();
3894        let receiver_continuation = receiver.try_commit_v3().unwrap();
3895        assert_eq!(
3896            source_continuation
3897                .iter()
3898                .map(|subdag| subdag.leader.round)
3899                .collect::<Vec<_>>(),
3900            vec![PREFIX_LAST_ROUND + 1, PREFIX_LAST_ROUND + 2]
3901        );
3902        assert_eq!(receiver_continuation, source_continuation);
3903
3904        source.dag_state.write().flush();
3905        receiver.dag_state.write().flush();
3906        let first_continuation_index = PREFIX_LAST_ROUND + 1;
3907        let last_continuation_index = source_continuation.last().unwrap().commit_ref.index;
3908        let source_continuation_commits = source_store
3909            .scan_commits((first_continuation_index..=last_continuation_index).into())
3910            .unwrap();
3911        let receiver_continuation_commits = receiver_store
3912            .scan_commits((first_continuation_index..=last_continuation_index).into())
3913            .unwrap();
3914        assert_eq!(receiver_continuation_commits, source_continuation_commits);
3915        assert_eq!(
3916            receiver_continuation_commits[0].previous_digest(),
3917            source_commits.last().unwrap().digest()
3918        );
3919    }
3920
3921    #[tokio::test]
3922    async fn add_certified_commits_v3_gced_blocks() {
3923        const GC_DEPTH: u32 = 3;
3924        telemetry_subscribers::init_for_testing();
3925
3926        let (mut context, _key_pairs) = Context::new_for_test(5);
3927        context.protocol_config.set_gc_depth_for_testing(GC_DEPTH);
3928        context.protocol_config.set_enable_v3_for_testing(true);
3929        let context = context.with_parameters(Parameters {
3930            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
3931            ..Default::default()
3932        });
3933
3934        let authority_index = AuthorityIndex::new_for_test(0);
3935        let core = CoreTestFixture::new(context, vec![1, 1, 1, 1, 1], authority_index, true).await;
3936        let store = core.store.clone();
3937        let mut core = core.core;
3938
3939        // Authority E proposes only at rounds 1 and 5: E1 stays outside the DAG
3940        // until E5 links back to it. E5 is first committed by the leader round 6
3941        // commit, whose gc bound (6 - 1 - GC_DEPTH = 2) excludes E1.
3942        let dag_str = "DAG {
3943            Round 0 : { 5 },
3944            Round 1 : { * },
3945            Round 2 : {
3946                A -> [-E1],
3947                B -> [-E1],
3948                C -> [-E1],
3949                D -> [-E1],
3950            },
3951            Round 3 : {
3952                A -> [*],
3953                B -> [*],
3954                C -> [*],
3955                D -> [*],
3956            },
3957            Round 4 : {
3958                A -> [*],
3959                B -> [*],
3960                C -> [*],
3961                D -> [*],
3962            },
3963            Round 5 : {
3964                A -> [*],
3965                B -> [*],
3966                C -> [*],
3967                D -> [*],
3968                E -> [A4, B4, C4, D4, E1]
3969            },
3970            Round 6 : { * },
3971            Round 7 : { * },
3972        }";
3973
3974        let (_, mut dag_builder) = parse_dag(dag_str).expect("Invalid dag");
3975
3976        // parse_dag creates the DagBuilder with a default context. Apply the same
3977        // gc_depth so certified commits are linearized with the same gc bound as
3978        // the receiving Core, like commits certified by peers sharing the
3979        // protocol config.
3980        {
3981            let mut builder_context = (*dag_builder.context).clone();
3982            builder_context
3983                .protocol_config
3984                .set_gc_depth_for_testing(GC_DEPTH);
3985            dag_builder.context = Arc::new(builder_context);
3986        }
3987
3988        // Certified commits for the leader rounds in 1..=6 that have a leader
3989        // block. Rounds where the elected leader is authority E produce no
3990        // commit, so commit indices stay consecutive across leader round gaps.
3991        let certified_commits = dag_builder
3992            .get_sub_dag_and_certified_commits(1..=6)
3993            .into_iter()
3994            .map(|(_, c)| c)
3995            .collect::<Vec<_>>();
3996        let num_certified = certified_commits.len() as CommitIndex;
3997        assert!(num_certified > 0);
3998
3999        core.add_certified_commits(CertifiedCommits::new(certified_commits, vec![]))
4000            .expect("Should not fail");
4001
4002        assert_eq!(core.dag_state.read().last_commit_index(), num_certified);
4003
4004        // E5 must be committed (every round 6 block links to it), while its
4005        // ancestor E1 must be excluded by the gc bound. This also exercises
4006        // accepting a committed block (E5) whose ancestor (E1) is gc'ed and
4007        // never accepted locally.
4008        core.dag_state.write().flush();
4009        let commits = store.scan_commits((1..=num_certified).into()).unwrap();
4010        assert_eq!(commits.len(), num_certified as usize);
4011        let authority_e = AuthorityIndex::new_for_test(4);
4012        let mut e5_committed = false;
4013        for commit in &commits {
4014            for block_ref in commit.blocks() {
4015                assert!(
4016                    !(block_ref.round == 1 && block_ref.author == authority_e),
4017                    "Did not expect to commit block E1"
4018                );
4019                if block_ref.round == 5 && block_ref.author == authority_e {
4020                    e5_committed = true;
4021                }
4022            }
4023        }
4024        assert!(e5_committed, "Expected block E5 to be committed");
4025    }
4026
4027    #[tokio::test]
4028    async fn test_core_recover_v3_keeps_zero_scores_unavailable() {
4029        telemetry_subscribers::init_for_testing();
4030
4031        let (mut context, _key_pairs) = Context::new_for_test(4);
4032        context.protocol_config.set_enable_v3_for_testing(true);
4033        let context = context.with_parameters(Parameters {
4034            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
4035            ..Default::default()
4036        });
4037        let store = Arc::new(MemStore::new());
4038        let authority_index = AuthorityIndex::new_for_test(0);
4039
4040        {
4041            let mut fixture = CoreTestFixture::new_with_store(
4042                context.clone(),
4043                vec![1, 1, 1, 1],
4044                authority_index,
4045                true,
4046                store.clone(),
4047            )
4048            .await;
4049            assert_eq!(
4050                fixture.core.current_reputation_scores(),
4051                ReputationScores::default()
4052            );
4053
4054            let mut dag_builder = DagBuilder::new(fixture.core.context.clone());
4055            dag_builder.layers(1..=4).build();
4056            fixture
4057                .dag_state
4058                .write()
4059                .accept_blocks(dag_builder.blocks(1..=4));
4060            assert_eq!(fixture.core.try_commit_v3().unwrap().len(), 3);
4061            assert_eq!(
4062                fixture.core.current_reputation_scores(),
4063                ReputationScores::default()
4064            );
4065            fixture.dag_state.write().flush();
4066        }
4067
4068        let fixture = CoreTestFixture::new_with_store(
4069            context,
4070            vec![1, 1, 1, 1],
4071            authority_index,
4072            true,
4073            store,
4074        )
4075        .await;
4076        assert_eq!(fixture.dag_state.read().last_commit_index(), 3);
4077        assert_eq!(
4078            fixture.core.current_reputation_scores(),
4079            ReputationScores::default()
4080        );
4081    }
4082
4083    #[tokio::test]
4084    async fn test_core_recover_from_store_v3() {
4085        telemetry_subscribers::init_for_testing();
4086
4087        let (mut context, _key_pairs) = Context::new_for_test(4);
4088        context.protocol_config.set_enable_v3_for_testing(true);
4089        let context = context.with_parameters(Parameters {
4090            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
4091            ..Default::default()
4092        });
4093        let store = Arc::new(MemStore::new());
4094        let authority_index = AuthorityIndex::new_for_test(0);
4095
4096        // Commit with v3 and flush the resulting state to the store.
4097        let (last_commit_index, last_committed_rounds) = {
4098            let mut fixture = CoreTestFixture::new_with_store(
4099                context.clone(),
4100                vec![1, 1, 1, 1],
4101                authority_index,
4102                true,
4103                store.clone(),
4104            )
4105            .await;
4106            let mut dag_builder = DagBuilder::new(fixture.core.context.clone());
4107            dag_builder.layers(1..=12).build();
4108            fixture
4109                .dag_state
4110                .write()
4111                .accept_blocks(dag_builder.blocks(1..=12));
4112            let committed = fixture.core.try_commit_v3().unwrap();
4113            assert!(!committed.is_empty());
4114            fixture.dag_state.write().flush();
4115            (
4116                fixture.dag_state.read().last_commit_index(),
4117                fixture.dag_state.read().last_committed_rounds(),
4118            )
4119        };
4120
4121        // "Restart" by recovering DagState and Core from the store. On the v3 path,
4122        // recovery does not read CommitInfo (never persisted with v3): committed
4123        // rounds are rebuilt from the bounded commit scan, and the scoring subdag
4124        // stays empty since only legacy leader scoring reads it.
4125        let fixture = CoreTestFixture::new_with_store(
4126            context,
4127            vec![1, 1, 1, 1],
4128            authority_index,
4129            true,
4130            store,
4131        )
4132        .await;
4133        assert_eq!(
4134            fixture.dag_state.read().last_commit_index(),
4135            last_commit_index
4136        );
4137        assert_eq!(
4138            fixture.dag_state.read().last_committed_rounds(),
4139            last_committed_rounds
4140        );
4141        assert_eq!(fixture.dag_state.read().scoring_subdags_count(), 0);
4142    }
4143
4144    #[tokio::test]
4145    async fn try_commit_with_certified_commits_gced_blocks() {
4146        const GC_DEPTH: u32 = 3;
4147        telemetry_subscribers::init_for_testing();
4148
4149        let (mut context, mut key_pairs) = Context::new_for_test(5);
4150        context.protocol_config.set_gc_depth_for_testing(GC_DEPTH);
4151        let context = Arc::new(context.with_parameters(Parameters {
4152            sync_last_known_own_block_timeout: Duration::from_millis(2_000),
4153            ..Default::default()
4154        }));
4155
4156        let store = Arc::new(MemStore::new());
4157        let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store.clone())));
4158
4159        let block_manager = BlockManager::new(context.clone(), dag_state.clone());
4160        let leader_schedule = Arc::new(
4161            LeaderSchedule::from_store(context.clone(), dag_state.clone())
4162                .with_num_commits_per_schedule(10),
4163        );
4164
4165        let (_transaction_client, tx_receiver, priority_tx_receiver) =
4166            TransactionClient::new(context.clone());
4167        let transaction_pool = Arc::new(TransactionConsumerPool::new(TransactionConsumer::new(
4168            tx_receiver,
4169            priority_tx_receiver,
4170            context.clone(),
4171        )));
4172        let (signals, signal_receivers) = CoreSignals::new(context.clone());
4173        let transaction_vote_tracker = TransactionVoteTracker::new(
4174            context.clone(),
4175            Arc::new(NoopBlockVerifier {}),
4176            dag_state.clone(),
4177        );
4178        // Need at least one subscriber to the block broadcast channel.
4179        let _block_receiver = signal_receivers.block_broadcast_receiver();
4180
4181        let (commit_consumer, _commit_receiver) = CommitConsumerArgs::new(0, 0);
4182        let commit_observer = CommitObserver::new(
4183            context.clone(),
4184            commit_consumer,
4185            dag_state.clone(),
4186            transaction_vote_tracker.clone(),
4187        )
4188        .await;
4189
4190        let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
4191        let mut core = Core::new_validator(
4192            context.clone(),
4193            leader_schedule,
4194            transaction_pool,
4195            transaction_vote_tracker.clone(),
4196            block_manager,
4197            commit_observer,
4198            signals,
4199            key_pairs.remove(context.own_index.value()).1,
4200            dag_state.clone(),
4201            true,
4202            round_tracker,
4203        );
4204
4205        // No new block should have been produced
4206        assert_eq!(
4207            core.last_proposed_round(),
4208            Some(GENESIS_ROUND),
4209            "No block should have been created other than genesis"
4210        );
4211
4212        let dag_str = "DAG {
4213            Round 0 : { 5 },
4214            Round 1 : { * },
4215            Round 2 : { 
4216                A -> [-E1],
4217                B -> [-E1],
4218                C -> [-E1],
4219                D -> [-E1],
4220            },
4221            Round 3 : {
4222                A -> [*],
4223                B -> [*],
4224                C -> [*],
4225                D -> [*],
4226            },
4227            Round 4 : { 
4228                A -> [*],
4229                B -> [*],
4230                C -> [*],
4231                D -> [*],
4232            },
4233            Round 5 : { 
4234                A -> [*],
4235                B -> [*],
4236                C -> [*],
4237                D -> [*],
4238                E -> [A4, B4, C4, D4, E1]
4239            },
4240            Round 6 : { * },
4241            Round 7 : { * },
4242        }";
4243
4244        let (_, mut dag_builder) = parse_dag(dag_str).expect("Invalid dag");
4245        dag_builder.print();
4246
4247        // Now get all the committed sub dags from the DagBuilder
4248        let (_sub_dags, certified_commits): (Vec<_>, Vec<_>) = dag_builder
4249            .get_sub_dag_and_certified_commits(1..=5)
4250            .into_iter()
4251            .unzip();
4252
4253        // Now try to commit up to the latest leader (round = 5) with the provided certified commits. Not that we have not accepted any
4254        // blocks. That should happen during the commit process.
4255        let committed_sub_dags = core.try_commit(certified_commits).unwrap();
4256
4257        // We should have committed up to round 4
4258        assert_eq!(committed_sub_dags.len(), 4);
4259        for (index, committed_sub_dag) in committed_sub_dags.iter().enumerate() {
4260            assert_eq!(committed_sub_dag.commit_ref.index as usize, index + 1);
4261
4262            // ensure that block from E1 node has not been committed
4263            for block in committed_sub_dag.blocks.iter() {
4264                if block.round() == 1 && block.author() == AuthorityIndex::new_for_test(5) {
4265                    panic!("Did not expect to commit block E1");
4266                }
4267            }
4268        }
4269    }
4270
4271    #[tokio::test(flavor = "current_thread", start_paused = true)]
4272    async fn test_commit_on_leader_schedule_change_boundary_without_multileader() {
4273        parameterized_test_commit_on_leader_schedule_change_boundary(Some(1)).await;
4274    }
4275
4276    #[tokio::test(flavor = "current_thread", start_paused = true)]
4277    async fn test_commit_on_leader_schedule_change_boundary_with_multileader() {
4278        parameterized_test_commit_on_leader_schedule_change_boundary(None).await;
4279    }
4280
4281    async fn parameterized_test_commit_on_leader_schedule_change_boundary(
4282        num_leaders_per_round: Option<usize>,
4283    ) {
4284        telemetry_subscribers::init_for_testing();
4285        let default_params = Parameters::default();
4286
4287        let (mut context, _) = Context::new_for_test(6);
4288        context
4289            .protocol_config
4290            .set_num_leaders_per_round_for_testing(num_leaders_per_round);
4291        // create the cores and their signals for all the authorities
4292        let mut cores = create_cores(context, vec![1, 1, 1, 1, 1, 1]).await;
4293
4294        // Now iterate over a few rounds and ensure the corresponding signals are created while network advances
4295        let mut last_round_blocks: Vec<VerifiedBlock> = Vec::new();
4296        for round in 1..=33 {
4297            let mut this_round_blocks = Vec::new();
4298
4299            // Wait for min round delay to allow blocks to be proposed.
4300            sleep(default_params.min_round_delay).await;
4301
4302            for core_fixture in &mut cores {
4303                // add the blocks from last round
4304                // this will trigger a block creation for the round and a signal should be emitted
4305                core_fixture.add_blocks(last_round_blocks.clone()).unwrap();
4306
4307                core_fixture
4308                    .core
4309                    .round_tracker_for_tests()
4310                    .write()
4311                    .update_from_probe(
4312                        vec![
4313                            vec![round, round, round, round, round, round],
4314                            vec![round, round, round, round, round, round],
4315                            vec![round, round, round, round, round, round],
4316                            vec![round, round, round, round, round, round],
4317                            vec![round, round, round, round, round, round],
4318                            vec![round, round, round, round, round, round],
4319                        ],
4320                        vec![
4321                            vec![round, round, round, round, round, round],
4322                            vec![round, round, round, round, round, round],
4323                            vec![round, round, round, round, round, round],
4324                            vec![round, round, round, round, round, round],
4325                            vec![round, round, round, round, round, round],
4326                            vec![round, round, round, round, round, round],
4327                        ],
4328                    );
4329
4330                // A "new round" signal should be received given that all the blocks of previous round have been processed
4331                let new_round = receive(
4332                    Duration::from_secs(1),
4333                    core_fixture.signal_receivers.new_round_receiver(),
4334                )
4335                .await;
4336                assert_eq!(new_round, round);
4337
4338                // Check that a new block has been proposed.
4339                let extended_block = tokio::time::timeout(
4340                    Duration::from_secs(1),
4341                    core_fixture.block_receiver.recv(),
4342                )
4343                .await
4344                .unwrap()
4345                .unwrap();
4346                assert_eq!(extended_block.block.round(), round);
4347                assert_eq!(
4348                    extended_block.block.author(),
4349                    core_fixture.core.context.own_index
4350                );
4351
4352                // append the new block to this round blocks
4353                this_round_blocks.push(core_fixture.core.last_proposed_block().clone());
4354
4355                let block = core_fixture.core.last_proposed_block();
4356
4357                // ensure that produced block is referring to the blocks of last_round
4358                assert_eq!(
4359                    block.ancestors().len(),
4360                    core_fixture.core.context.committee.size()
4361                );
4362                for ancestor in block.ancestors() {
4363                    if block.round() > 1 {
4364                        // don't bother with round 1 block which just contains the genesis blocks.
4365                        assert!(
4366                            last_round_blocks
4367                                .iter()
4368                                .any(|block| block.reference() == *ancestor),
4369                            "Reference from previous round should be added"
4370                        );
4371                    }
4372                }
4373            }
4374
4375            last_round_blocks = this_round_blocks;
4376        }
4377
4378        for core_fixture in cores {
4379            // There are 31 leader rounds with rounds completed up to and including
4380            // round 33. Round 33 blocks will only include their own blocks, so there
4381            // should only be 30 commits.
4382            // However on a leader schedule change boundary its is possible for a
4383            // new leader to get selected for the same round if the leader elected
4384            // gets swapped allowing for multiple leaders to be committed at a round.
4385            // Meaning with multi leader per round explicitly set to 1 we will have 30,
4386            // otherwise 31.
4387            // NOTE: We used 31 leader rounds to specifically trigger the scenario
4388            // where the leader schedule boundary occurred AND we had a swap to a new
4389            // leader for the same round
4390            let expected_commit_count = match num_leaders_per_round {
4391                Some(1) => 30,
4392                _ => 31,
4393            };
4394
4395            // Flush the DAG state to storage.
4396            core_fixture.dag_state.write().flush();
4397
4398            // Check commits have been persisted to store
4399            let last_commit = core_fixture
4400                .store
4401                .read_last_commit()
4402                .unwrap()
4403                .expect("last commit should be set");
4404            assert_eq!(last_commit.index(), expected_commit_count);
4405            let all_stored_commits = core_fixture
4406                .store
4407                .scan_commits((0..=CommitIndex::MAX).into())
4408                .unwrap();
4409            assert_eq!(all_stored_commits.len(), expected_commit_count as usize);
4410            assert_eq!(
4411                core_fixture
4412                    .core
4413                    .leader_schedule
4414                    .leader_swap_table
4415                    .read()
4416                    .bad_nodes
4417                    .len(),
4418                1
4419            );
4420            assert_eq!(
4421                core_fixture
4422                    .core
4423                    .leader_schedule
4424                    .leader_swap_table
4425                    .read()
4426                    .good_nodes
4427                    .len(),
4428                1
4429            );
4430            let expected_reputation_scores =
4431                ReputationScores::new((21..=30).into(), vec![43, 43, 43, 43, 43, 43]);
4432            assert_eq!(
4433                core_fixture
4434                    .core
4435                    .leader_schedule
4436                    .leader_swap_table
4437                    .read()
4438                    .reputation_scores,
4439                expected_reputation_scores
4440            );
4441        }
4442    }
4443
4444    #[tokio::test]
4445    async fn test_core_signals() {
4446        telemetry_subscribers::init_for_testing();
4447        let default_params = Parameters::default();
4448
4449        let (context, _) = Context::new_for_test(4);
4450        // create the cores and their signals for all the authorities
4451        let mut cores = create_cores(context, vec![1, 1, 1, 1]).await;
4452
4453        // Now iterate over a few rounds and ensure the corresponding signals are created while network advances
4454        let mut last_round_blocks = Vec::new();
4455        for round in 1..=10 {
4456            let mut this_round_blocks = Vec::new();
4457
4458            // Wait for min round delay to allow blocks to be proposed.
4459            sleep(default_params.min_round_delay).await;
4460
4461            for core_fixture in &mut cores {
4462                // add the blocks from last round
4463                // this will trigger a block creation for the round and a signal should be emitted
4464                core_fixture.add_blocks(last_round_blocks.clone()).unwrap();
4465
4466                core_fixture
4467                    .core
4468                    .round_tracker_for_tests()
4469                    .write()
4470                    .update_from_probe(
4471                        vec![
4472                            vec![round, round, round, round],
4473                            vec![round, round, round, round],
4474                            vec![round, round, round, round],
4475                            vec![round, round, round, round],
4476                        ],
4477                        vec![
4478                            vec![round, round, round, round],
4479                            vec![round, round, round, round],
4480                            vec![round, round, round, round],
4481                            vec![round, round, round, round],
4482                        ],
4483                    );
4484
4485                // A "new round" signal should be received given that all the blocks of previous round have been processed
4486                let new_round = receive(
4487                    Duration::from_secs(1),
4488                    core_fixture.signal_receivers.new_round_receiver(),
4489                )
4490                .await;
4491                assert_eq!(new_round, round);
4492
4493                // Check that a new block has been proposed.
4494                let extended_block = tokio::time::timeout(
4495                    Duration::from_secs(1),
4496                    core_fixture.block_receiver.recv(),
4497                )
4498                .await
4499                .unwrap()
4500                .unwrap();
4501                assert_eq!(extended_block.block.round(), round);
4502                assert_eq!(
4503                    extended_block.block.author(),
4504                    core_fixture.core.context.own_index
4505                );
4506
4507                // append the new block to this round blocks
4508                this_round_blocks.push(core_fixture.core.last_proposed_block().clone());
4509
4510                let block = core_fixture.core.last_proposed_block();
4511
4512                // ensure that produced block is referring to the blocks of last_round
4513                assert_eq!(
4514                    block.ancestors().len(),
4515                    core_fixture.core.context.committee.size()
4516                );
4517                for ancestor in block.ancestors() {
4518                    if block.round() > 1 {
4519                        // don't bother with round 1 block which just contains the genesis blocks.
4520                        assert!(
4521                            last_round_blocks
4522                                .iter()
4523                                .any(|block| block.reference() == *ancestor),
4524                            "Reference from previous round should be added"
4525                        );
4526                    }
4527                }
4528            }
4529
4530            last_round_blocks = this_round_blocks;
4531        }
4532
4533        for core_fixture in cores {
4534            // Flush the DAG state to storage.
4535            core_fixture.dag_state.write().flush();
4536            // Check commits have been persisted to store
4537            let last_commit = core_fixture
4538                .store
4539                .read_last_commit()
4540                .unwrap()
4541                .expect("last commit should be set");
4542            // There are 8 leader rounds with rounds completed up to and including
4543            // round 9. Round 10 blocks will only include their own blocks, so the
4544            // 8th leader will not be committed.
4545            assert_eq!(last_commit.index(), 7);
4546            let all_stored_commits = core_fixture
4547                .store
4548                .scan_commits((0..=CommitIndex::MAX).into())
4549                .unwrap();
4550            assert_eq!(all_stored_commits.len(), 7);
4551        }
4552    }
4553
4554    #[tokio::test]
4555    async fn test_core_compress_proposal_references() {
4556        telemetry_subscribers::init_for_testing();
4557        let default_params = Parameters::default();
4558
4559        let (context, _) = Context::new_for_test(4);
4560        // create the cores and their signals for all the authorities
4561        let mut cores = create_cores(context, vec![1, 1, 1, 1]).await;
4562
4563        let mut last_round_blocks = Vec::new();
4564        let mut all_blocks = Vec::new();
4565
4566        let excluded_authority = AuthorityIndex::new_for_test(3);
4567
4568        for round in 1..=10 {
4569            let mut this_round_blocks = Vec::new();
4570
4571            for core_fixture in &mut cores {
4572                // do not produce any block for authority 3
4573                if core_fixture.core.context.own_index == excluded_authority {
4574                    continue;
4575                }
4576
4577                // try to propose to ensure that we are covering the case where we miss the leader authority 3
4578                core_fixture.add_blocks(last_round_blocks.clone()).unwrap();
4579                core_fixture
4580                    .core
4581                    .round_tracker_for_tests()
4582                    .write()
4583                    .update_from_probe(
4584                        vec![
4585                            vec![round, round, round, round],
4586                            vec![round, round, round, round],
4587                            vec![round, round, round, round],
4588                            vec![round, round, round, round],
4589                        ],
4590                        vec![
4591                            vec![round, round, round, round],
4592                            vec![round, round, round, round],
4593                            vec![round, round, round, round],
4594                            vec![round, round, round, round],
4595                        ],
4596                    );
4597                core_fixture.core.new_block(round, true).unwrap();
4598
4599                let block = core_fixture.core.last_proposed_block();
4600                assert_eq!(block.round(), round);
4601
4602                // append the new block to this round blocks
4603                this_round_blocks.push(block.clone());
4604            }
4605
4606            last_round_blocks = this_round_blocks.clone();
4607            all_blocks.extend(this_round_blocks);
4608        }
4609
4610        // Now send all the produced blocks to core of authority 3. It should produce a new block. If no compression would
4611        // be applied the we should expect all the previous blocks to be referenced from round 0..=10. However, since compression
4612        // is applied only the last round's (10) blocks should be referenced + the authority's block of round 0.
4613        let core_fixture = &mut cores[excluded_authority];
4614        // Wait for min round delay to allow blocks to be proposed.
4615        sleep(default_params.min_round_delay).await;
4616        // add blocks to trigger proposal.
4617        core_fixture.add_blocks(all_blocks).unwrap();
4618
4619        // Assert that a block has been created for round 11 and it references to blocks of round 10 for the other peers, and
4620        // to round 1 for its own block (created after recovery).
4621        let block = core_fixture.core.last_proposed_block();
4622        assert_eq!(block.round(), 11);
4623        assert_eq!(block.ancestors().len(), 4);
4624        for block_ref in block.ancestors() {
4625            if block_ref.author == excluded_authority {
4626                assert_eq!(block_ref.round, 1);
4627            } else {
4628                assert_eq!(block_ref.round, 10);
4629            }
4630        }
4631
4632        // Flush the DAG state to storage.
4633        core_fixture.dag_state.write().flush();
4634
4635        // Check commits have been persisted to store
4636        let last_commit = core_fixture
4637            .store
4638            .read_last_commit()
4639            .unwrap()
4640            .expect("last commit should be set");
4641        // There are 8 leader rounds with rounds completed up to and including
4642        // round 10. However because there were no blocks produced for authority 3
4643        // 2 leader rounds will be skipped.
4644        assert_eq!(last_commit.index(), 6);
4645        let all_stored_commits = core_fixture
4646            .store
4647            .scan_commits((0..=CommitIndex::MAX).into())
4648            .unwrap();
4649        assert_eq!(all_stored_commits.len(), 6);
4650    }
4651
4652    #[tokio::test]
4653    async fn try_select_certified_leaders() {
4654        // GIVEN
4655        telemetry_subscribers::init_for_testing();
4656
4657        let (context, _) = Context::new_for_test(4);
4658
4659        let authority_index = AuthorityIndex::new_for_test(0);
4660        let core =
4661            CoreTestFixture::new(context.clone(), vec![1, 1, 1, 1], authority_index, true).await;
4662        let mut core = core.core;
4663
4664        let mut dag_builder = DagBuilder::new(Arc::new(context.clone()));
4665        dag_builder.layers(1..=12).build();
4666
4667        let limit = 2;
4668
4669        let blocks = dag_builder.blocks(1..=12);
4670        core.dag_state.write().accept_blocks(blocks);
4671
4672        // WHEN
4673        let sub_dags_and_commits = dag_builder.get_sub_dag_and_certified_commits(1..=4);
4674        let mut certified_commits = sub_dags_and_commits
4675            .into_iter()
4676            .map(|(_, commit)| commit)
4677            .collect::<Vec<_>>();
4678
4679        let leaders = core.try_select_certified_leaders(&mut certified_commits, limit);
4680
4681        // THEN
4682        assert_eq!(leaders.len(), 2);
4683        assert_eq!(certified_commits.len(), 2);
4684    }
4685
4686    pub(crate) async fn receive<T: Copy>(timeout: Duration, mut receiver: watch::Receiver<T>) -> T {
4687        tokio::time::timeout(timeout, receiver.changed())
4688            .await
4689            .expect("Timeout while waiting to read from receiver")
4690            .expect("Signal receive channel shouldn't be closed");
4691        *receiver.borrow_and_update()
4692    }
4693}