consensus_core/
leader_scoring.rs1use 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 pub(crate) scores_per_authority: Vec<u64>,
24 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 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
76pub(crate) struct ScoringSubdag {
83 pub(crate) context: Arc<Context>,
84 pub(crate) commit_range: Option<CommitRange>,
85 pub(crate) leaders: HashSet<BlockRef>,
88 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 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 tracing::trace!("Adding new committed leader {} for scoring", subdag.leader);
125 self.leaders.insert(subdag.leader);
126
127 for block in subdag.blocks {
130 for ancestor in block.ancestors() {
131 if ancestor.round != block.round().saturating_sub(1) {
134 continue;
135 }
136
137 if self.leaders.contains(ancestor) {
140 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 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 pub(crate) fn calculate_distributed_vote_scores(&self) -> ReputationScores {
172 let scores_per_authority = self.distributed_votes_scores();
173
174 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 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 let mut dag_builder = DagBuilder::new(context.clone());
294 dag_builder.layers(1..=3).build();
295 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}