Skip to main content

consensus_core/
leader_scoring.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::{BTreeMap, HashSet},
6    sync::Arc,
7};
8
9use consensus_config::AuthorityIndex;
10use consensus_types::block::BlockRef;
11use serde::{Deserialize, Serialize};
12
13use crate::{
14    block::BlockAPI,
15    commit::{CommitRange, CommittedSubDag},
16    context::Context,
17    stake_aggregator::{QuorumThreshold, StakeAggregator},
18};
19
20#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
21pub(crate) struct ReputationScores {
22    /// Score per authority. Vec index is the `AuthorityIndex`.
23    pub(crate) scores_per_authority: Vec<u64>,
24    // The range of commits these scores were calculated from.
25    pub(crate) commit_range: CommitRange,
26}
27
28impl ReputationScores {
29    pub(crate) fn new(commit_range: CommitRange, scores_per_authority: Vec<u64>) -> Self {
30        Self {
31            scores_per_authority,
32            commit_range,
33        }
34    }
35
36    pub(crate) fn highest_score(&self) -> u64 {
37        *self.scores_per_authority.iter().max().unwrap_or(&0)
38    }
39
40    // Returns the authorities index with score tuples.
41    pub(crate) fn authorities_by_score(&self, context: Arc<Context>) -> Vec<(AuthorityIndex, u64)> {
42        self.scores_per_authority
43            .iter()
44            .enumerate()
45            .map(|(index, score)| {
46                (
47                    context
48                        .committee
49                        .to_authority_index(index)
50                        .expect("Should be a valid AuthorityIndex"),
51                    *score,
52                )
53            })
54            .collect()
55    }
56
57    pub(crate) fn update_metrics(&self, context: Arc<Context>) {
58        for (index, score) in self.scores_per_authority.iter().enumerate() {
59            let authority_index = context
60                .committee
61                .to_authority_index(index)
62                .expect("Should be a valid AuthorityIndex");
63            let authority = context.committee.authority(authority_index);
64            if !authority.hostname.is_empty() {
65                context
66                    .metrics
67                    .node_metrics
68                    .reputation_scores
69                    .with_label_values(&[&authority.hostname])
70                    .set(*score as i64);
71            }
72        }
73    }
74}
75
76/// ScoringSubdag represents the scoring votes in a collection of subdags across
77/// multiple commits.
78/// These subdags are "scoring" for the purposes of leader schedule change. As
79/// new subdags are added, the DAG is traversed and votes for leaders are recorded
80/// and scored along with stake. On a leader schedule change, finalized reputation
81/// scores will be calculated based on the votes & stake collected in this struct.
82pub(crate) struct ScoringSubdag {
83    pub(crate) context: Arc<Context>,
84    pub(crate) commit_range: Option<CommitRange>,
85    // Only includes committed leaders for now.
86    // TODO: Include skipped leaders as well
87    pub(crate) leaders: HashSet<BlockRef>,
88    // A map of votes to the stake of strongly linked blocks that include that vote
89    // Note: Including stake aggregator so that we can quickly check if it exceeds
90    // quourum threshold and only include those scores for certain scoring strategies.
91    pub(crate) votes: BTreeMap<BlockRef, StakeAggregator<QuorumThreshold>>,
92}
93
94impl ScoringSubdag {
95    pub(crate) fn new(context: Arc<Context>) -> Self {
96        Self {
97            context,
98            commit_range: None,
99            leaders: HashSet::new(),
100            votes: BTreeMap::new(),
101        }
102    }
103
104    pub(crate) fn add_subdags(&mut self, committed_subdags: Vec<CommittedSubDag>) {
105        let _s = self
106            .context
107            .metrics
108            .node_metrics
109            .scope_processing_time
110            .with_label_values(&["ScoringSubdag::add_unscored_committed_subdags"])
111            .start_timer();
112        for subdag in committed_subdags {
113            // If the commit range is not set, then set it to the range of the first
114            // committed subdag index.
115            if let Some(commit_range) = &mut self.commit_range {
116                commit_range.extend_to(subdag.commit_ref.index);
117            } else {
118                self.commit_range = Some(CommitRange::new(
119                    subdag.commit_ref.index..=subdag.commit_ref.index,
120                ));
121            }
122
123            // Add the committed leader to the list of leaders we will be scoring.
124            tracing::trace!("Adding new committed leader {} for scoring", subdag.leader);
125            self.leaders.insert(subdag.leader);
126
127            // Check each block in subdag. Blocks are in order so we should traverse the
128            // oldest blocks first
129            for block in subdag.blocks {
130                for ancestor in block.ancestors() {
131                    // Weak links may point to blocks with lower round numbers
132                    // than strong links.
133                    if ancestor.round != block.round().saturating_sub(1) {
134                        continue;
135                    }
136
137                    // If a blocks strong linked ancestor is in leaders, then
138                    // it's a vote for leader.
139                    if self.leaders.contains(ancestor) {
140                        // There should never be duplicate references to blocks
141                        // with strong linked ancestors to leader.
142                        tracing::trace!(
143                            "Found a vote {} for leader {ancestor} from authority {}",
144                            block.reference(),
145                            block.author()
146                        );
147                        assert!(
148                            self.votes
149                                .insert(block.reference(), StakeAggregator::new())
150                                .is_none(),
151                            "Vote {block} already exists. Duplicate vote found for leader {ancestor}"
152                        );
153                    }
154
155                    if let Some(stake) = self.votes.get_mut(ancestor) {
156                        // Vote is strongly linked to a future block, so we
157                        // consider this a distributed vote.
158                        tracing::trace!(
159                            "Found a distributed vote {ancestor} from authority {}",
160                            ancestor.author
161                        );
162                        stake.add(block.author(), &self.context.committee);
163                    }
164                }
165            }
166        }
167    }
168
169    // Iterate through votes and calculate scores for each authority based on
170    // distributed vote scoring strategy.
171    pub(crate) fn calculate_distributed_vote_scores(&self) -> ReputationScores {
172        let scores_per_authority = self.distributed_votes_scores();
173
174        // TODO: Normalize scores
175        ReputationScores::new(
176            self.commit_range
177                .clone()
178                .expect("CommitRange should be set if calculate_scores is called."),
179            scores_per_authority,
180        )
181    }
182
183    /// This scoring strategy aims to give scores based on overall vote distribution.
184    /// Instead of only giving one point for each vote that is included in 2f+1
185    /// blocks. We give a score equal to the amount of stake of all blocks that
186    /// included the vote.
187    fn distributed_votes_scores(&self) -> Vec<u64> {
188        let _s = self
189            .context
190            .metrics
191            .node_metrics
192            .scope_processing_time
193            .with_label_values(&["ScoringSubdag::score_distributed_votes"])
194            .start_timer();
195
196        let num_authorities = self.context.committee.size();
197        let mut scores_per_authority = vec![0_u64; num_authorities];
198
199        for (vote, stake_agg) in self.votes.iter() {
200            let authority = vote.author;
201            let stake = stake_agg.stake();
202            tracing::trace!(
203                "[{}] scores +{stake} reputation for {authority}!",
204                self.context.own_index,
205            );
206            scores_per_authority[authority.value()] += stake;
207        }
208        scores_per_authority
209    }
210
211    pub(crate) fn scored_subdags_count(&self) -> usize {
212        if let Some(commit_range) = &self.commit_range {
213            commit_range.size()
214        } else {
215            0
216        }
217    }
218
219    pub(crate) fn is_empty(&self) -> bool {
220        self.leaders.is_empty() && self.votes.is_empty() && self.commit_range.is_none()
221    }
222
223    pub(crate) fn clear(&mut self) {
224        self.leaders.clear();
225        self.votes.clear();
226        self.commit_range = None;
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use crate::test_dag_builder::DagBuilder;
234
235    #[tokio::test]
236    async fn test_reputation_scores_authorities_by_score() {
237        let context = Arc::new(Context::new_for_test(4).0);
238        let scores = ReputationScores::new((1..=300).into(), vec![4, 1, 1, 3]);
239        let authorities = scores.authorities_by_score(context);
240        assert_eq!(
241            authorities,
242            vec![
243                (AuthorityIndex::new_for_test(0), 4),
244                (AuthorityIndex::new_for_test(1), 1),
245                (AuthorityIndex::new_for_test(2), 1),
246                (AuthorityIndex::new_for_test(3), 3),
247            ]
248        );
249    }
250
251    #[tokio::test]
252    async fn test_reputation_scores_update_metrics() {
253        let context = Arc::new(Context::new_for_test(4).0);
254        let scores = ReputationScores::new((1..=300).into(), vec![1, 2, 4, 3]);
255        scores.update_metrics(context.clone());
256        let metrics = context.metrics.node_metrics.reputation_scores.clone();
257        assert_eq!(
258            metrics
259                .get_metric_with_label_values(&["test_host_0"])
260                .unwrap()
261                .get(),
262            1
263        );
264        assert_eq!(
265            metrics
266                .get_metric_with_label_values(&["test_host_1"])
267                .unwrap()
268                .get(),
269            2
270        );
271        assert_eq!(
272            metrics
273                .get_metric_with_label_values(&["test_host_2"])
274                .unwrap()
275                .get(),
276            4
277        );
278        assert_eq!(
279            metrics
280                .get_metric_with_label_values(&["test_host_3"])
281                .unwrap()
282                .get(),
283            3
284        );
285    }
286
287    #[tokio::test]
288    async fn test_scoring_subdag() {
289        telemetry_subscribers::init_for_testing();
290        let context = Arc::new(Context::new_for_test(4).0);
291
292        // Populate fully connected test blocks for round 0 ~ 3, authorities 0 ~ 3.
293        let mut dag_builder = DagBuilder::new(context.clone());
294        dag_builder.layers(1..=3).build();
295        // Build round 4 but with just the leader block
296        dag_builder
297            .layer(4)
298            .authorities(vec![
299                AuthorityIndex::new_for_test(1),
300                AuthorityIndex::new_for_test(2),
301                AuthorityIndex::new_for_test(3),
302            ])
303            .skip_block()
304            .build();
305
306        let mut scoring_subdag = ScoringSubdag::new(context.clone());
307
308        for (sub_dag, _commit) in dag_builder.get_sub_dag_and_commits(1..=4) {
309            scoring_subdag.add_subdags(vec![sub_dag]);
310        }
311
312        let scores = scoring_subdag.calculate_distributed_vote_scores();
313        assert_eq!(scores.scores_per_authority, vec![5, 5, 5, 5]);
314        assert_eq!(scores.commit_range, (1..=4).into());
315    }
316}