sui_core/
scoring_decision.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3use std::{collections::HashMap, sync::Arc};
4
5use arc_swap::ArcSwap;
6use consensus_config::Committee as ConsensusCommittee;
7use sui_types::{
8    base_types::AuthorityName, committee::Committee, messages_consensus::AuthorityIndex,
9};
10use tracing::debug;
11
12use crate::authority::AuthorityMetrics;
13
14/// Updates list of authorities that are deemed to have low reputation scores by consensus
15/// these may be lagging behind the network, byzantine, or not reliably participating for any reason.
16/// The algorithm is flagging as low scoring authorities all the validators that have the lowest scores
17/// up to the defined protocol_config.consensus_bad_nodes_stake_threshold. This is done to align the
18/// submission side with the consensus leader election schedule. Practically we don't want to submit
19/// transactions for sequencing to validators that have low scores and are not part of the leader
20/// schedule since the chances of getting them sequenced are lower.
21pub(crate) fn update_low_scoring_authorities(
22    low_scoring_authorities: Arc<ArcSwap<HashMap<AuthorityName, u64>>>,
23    sui_committee: &Committee,
24    consensus_committee: &ConsensusCommittee,
25    reputation_score_sorted_desc: Option<Vec<(AuthorityIndex, u64)>>,
26    metrics: &Arc<AuthorityMetrics>,
27    consensus_bad_nodes_stake_threshold: u64,
28) {
29    assert!(
30        (0..=33).contains(&consensus_bad_nodes_stake_threshold),
31        "The bad_nodes_stake_threshold should be in range [0 - 33], out of bounds parameter detected {}",
32        consensus_bad_nodes_stake_threshold
33    );
34
35    let Some(reputation_scores) = reputation_score_sorted_desc else {
36        return;
37    };
38
39    // We order the authorities by score ascending order in the exact same way as the reputation
40    // scores do - so we keep complete alignment between implementations
41    let scores_per_authority_order_asc: Vec<_> = reputation_scores
42        .into_iter()
43        .rev() // we reverse so we get them in asc order
44        .collect();
45
46    let mut final_low_scoring_map = HashMap::new();
47    let mut total_stake = 0;
48    for (index, score) in scores_per_authority_order_asc {
49        let authority_name = sui_committee.authority_by_index(index).unwrap();
50        let authority_index = consensus_committee
51            .to_authority_index(index as usize)
52            .unwrap();
53        let consensus_authority = consensus_committee.authority(authority_index);
54        let hostname = &consensus_authority.hostname;
55        let stake = consensus_authority.stake;
56        total_stake += stake;
57
58        let included = if total_stake
59            <= consensus_bad_nodes_stake_threshold * consensus_committee.total_stake() / 100
60        {
61            final_low_scoring_map.insert(*authority_name, score);
62            true
63        } else {
64            false
65        };
66
67        if !hostname.is_empty() {
68            debug!(
69                "authority {} has score {}, is low scoring: {}",
70                hostname, score, included
71            );
72
73            metrics
74                .consensus_handler_scores
75                .with_label_values(&[hostname])
76                .set(score as i64);
77        }
78    }
79    // Report the actual flagged final low scoring authorities
80    metrics
81        .consensus_handler_num_low_scoring_authorities
82        .set(final_low_scoring_map.len() as i64);
83    low_scoring_authorities.swap(Arc::new(final_low_scoring_map));
84}
85
86#[cfg(test)]
87mod tests {
88    #![allow(clippy::mutable_key_type)]
89    use std::{collections::HashMap, sync::Arc};
90
91    use arc_swap::ArcSwap;
92    use consensus_config::{Committee as ConsensusCommittee, local_committee_and_keys};
93    use prometheus::Registry;
94    use sui_types::{committee::Committee, crypto::AuthorityPublicKeyBytes};
95
96    use crate::{authority::AuthorityMetrics, scoring_decision::update_low_scoring_authorities};
97
98    #[test]
99    #[cfg_attr(msim, ignore)]
100    pub fn test_update_low_scoring_authorities() {
101        // GIVEN
102        // Total stake is 8 for this committee and every authority has equal stake = 1
103        let (sui_committee, consensus_committee) = generate_committees(8);
104
105        let low_scoring = Arc::new(ArcSwap::from_pointee(HashMap::new()));
106        let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));
107
108        // there is a low outlier in the non zero scores, exclude it as well as down nodes
109        let authorities_by_score_desc = vec![
110            (1, 390_u64),
111            (0, 350_u64),
112            (6, 340_u64),
113            (7, 310_u64),
114            (5, 300_u64),
115            (3, 50_u64),
116            (2, 50_u64),
117            (4, 0_u64), // down node
118        ];
119
120        // WHEN
121        let consensus_bad_nodes_stake_threshold = 33; // 33 * 8 / 100 = 2 low scoring validator
122
123        update_low_scoring_authorities(
124            low_scoring.clone(),
125            &sui_committee,
126            &consensus_committee,
127            Some(authorities_by_score_desc.clone()),
128            &metrics,
129            consensus_bad_nodes_stake_threshold,
130        );
131
132        // THEN
133        assert_eq!(low_scoring.load().len(), 2);
134        assert_eq!(
135            *low_scoring
136                .load()
137                // authority 2 is 2nd to the last in authorities_by_score_desc
138                .get(sui_committee.authority_by_index(2).unwrap())
139                .unwrap(),
140            50
141        );
142        assert_eq!(
143            *low_scoring
144                .load()
145                // authority 4 is the last in authorities_by_score_desc
146                .get(sui_committee.authority_by_index(4).unwrap())
147                .unwrap(),
148            0
149        );
150
151        // WHEN setting the threshold to lower
152        let consensus_bad_nodes_stake_threshold = 20; // 20 * 8 / 100 = 1 low scoring validator
153        update_low_scoring_authorities(
154            low_scoring.clone(),
155            &sui_committee,
156            &consensus_committee,
157            Some(authorities_by_score_desc.clone()),
158            &metrics,
159            consensus_bad_nodes_stake_threshold,
160        );
161
162        // THEN
163        assert_eq!(low_scoring.load().len(), 1);
164        assert_eq!(
165            *low_scoring
166                .load()
167                .get(sui_committee.authority_by_index(4).unwrap())
168                .unwrap(),
169            0
170        );
171    }
172
173    /// Generate a pair of Sui and consensus committees for the given size.
174    fn generate_committees(committee_size: usize) -> (Committee, ConsensusCommittee) {
175        let (consensus_committee, _) = local_committee_and_keys(0, vec![1; committee_size]);
176
177        let public_keys = consensus_committee
178            .authorities()
179            .map(|(_i, authority)| authority.authority_key.inner())
180            .collect::<Vec<_>>();
181        let sui_authorities = public_keys
182            .iter()
183            .map(|key| (AuthorityPublicKeyBytes::from(*key), 1))
184            .collect::<Vec<_>>();
185        let sui_committee = Committee::new_for_testing_with_normalized_voting_power(
186            0,
187            sui_authorities.iter().cloned().collect(),
188        );
189
190        (sui_committee, consensus_committee)
191    }
192}