sui_core/
scoring_decision.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
use std::{collections::HashMap, sync::Arc};

use arc_swap::ArcSwap;
use consensus_config::Committee as ConsensusCommittee;
use sui_types::{
    base_types::AuthorityName, committee::Committee, messages_consensus::AuthorityIndex,
};
use tracing::debug;

use crate::authority::AuthorityMetrics;

/// Updates list of authorities that are deemed to have low reputation scores by consensus
/// these may be lagging behind the network, byzantine, or not reliably participating for any reason.
/// The algorithm is flagging as low scoring authorities all the validators that have the lowest scores
/// up to the defined protocol_config.consensus_bad_nodes_stake_threshold. This is done to align the
/// submission side with the consensus leader election schedule. Practically we don't want to submit
/// transactions for sequencing to validators that have low scores and are not part of the leader
/// schedule since the chances of getting them sequenced are lower.
pub(crate) fn update_low_scoring_authorities(
    low_scoring_authorities: Arc<ArcSwap<HashMap<AuthorityName, u64>>>,
    sui_committee: &Committee,
    consensus_committee: &ConsensusCommittee,
    reputation_score_sorted_desc: Option<Vec<(AuthorityIndex, u64)>>,
    metrics: &Arc<AuthorityMetrics>,
    consensus_bad_nodes_stake_threshold: u64,
) {
    assert!((0..=33).contains(&consensus_bad_nodes_stake_threshold), "The bad_nodes_stake_threshold should be in range [0 - 33], out of bounds parameter detected {}", consensus_bad_nodes_stake_threshold);

    let Some(reputation_scores) = reputation_score_sorted_desc else {
        return;
    };

    // We order the authorities by score ascending order in the exact same way as the reputation
    // scores do - so we keep complete alignment between implementations
    let scores_per_authority_order_asc: Vec<_> = reputation_scores
        .into_iter()
        .rev() // we reverse so we get them in asc order
        .collect();

    let mut final_low_scoring_map = HashMap::new();
    let mut total_stake = 0;
    for (index, score) in scores_per_authority_order_asc {
        let authority_name = sui_committee.authority_by_index(index).unwrap();
        let authority_index = consensus_committee
            .to_authority_index(index as usize)
            .unwrap();
        let consensus_authority = consensus_committee.authority(authority_index);
        let hostname = &consensus_authority.hostname;
        let stake = consensus_authority.stake;
        total_stake += stake;

        let included = if total_stake
            <= consensus_bad_nodes_stake_threshold * consensus_committee.total_stake() / 100
        {
            final_low_scoring_map.insert(*authority_name, score);
            true
        } else {
            false
        };

        if !hostname.is_empty() {
            debug!(
                "authority {} has score {}, is low scoring: {}",
                hostname, score, included
            );

            metrics
                .consensus_handler_scores
                .with_label_values(&[hostname])
                .set(score as i64);
        }
    }
    // Report the actual flagged final low scoring authorities
    metrics
        .consensus_handler_num_low_scoring_authorities
        .set(final_low_scoring_map.len() as i64);
    low_scoring_authorities.swap(Arc::new(final_low_scoring_map));
}

#[cfg(test)]
mod tests {
    #![allow(clippy::mutable_key_type)]
    use std::{collections::HashMap, sync::Arc};

    use arc_swap::ArcSwap;
    use consensus_config::{local_committee_and_keys, Committee as ConsensusCommittee};
    use prometheus::Registry;
    use sui_types::{committee::Committee, crypto::AuthorityPublicKeyBytes};

    use crate::{authority::AuthorityMetrics, scoring_decision::update_low_scoring_authorities};

    #[test]
    #[cfg_attr(msim, ignore)]
    pub fn test_update_low_scoring_authorities() {
        // GIVEN
        // Total stake is 8 for this committee and every authority has equal stake = 1
        let (sui_committee, consensus_committee) = generate_committees(8);

        let low_scoring = Arc::new(ArcSwap::from_pointee(HashMap::new()));
        let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));

        // there is a low outlier in the non zero scores, exclude it as well as down nodes
        let authorities_by_score_desc = vec![
            (1, 390_u64),
            (0, 350_u64),
            (6, 340_u64),
            (7, 310_u64),
            (5, 300_u64),
            (3, 50_u64),
            (2, 50_u64),
            (4, 0_u64), // down node
        ];

        // WHEN
        let consensus_bad_nodes_stake_threshold = 33; // 33 * 8 / 100 = 2 low scoring validator

        update_low_scoring_authorities(
            low_scoring.clone(),
            &sui_committee,
            &consensus_committee,
            Some(authorities_by_score_desc.clone()),
            &metrics,
            consensus_bad_nodes_stake_threshold,
        );

        // THEN
        assert_eq!(low_scoring.load().len(), 2);
        assert_eq!(
            *low_scoring
                .load()
                // authority 2 is 2nd to the last in authorities_by_score_desc
                .get(sui_committee.authority_by_index(2).unwrap())
                .unwrap(),
            50
        );
        assert_eq!(
            *low_scoring
                .load()
                // authority 4 is the last in authorities_by_score_desc
                .get(sui_committee.authority_by_index(4).unwrap())
                .unwrap(),
            0
        );

        // WHEN setting the threshold to lower
        let consensus_bad_nodes_stake_threshold = 20; // 20 * 8 / 100 = 1 low scoring validator
        update_low_scoring_authorities(
            low_scoring.clone(),
            &sui_committee,
            &consensus_committee,
            Some(authorities_by_score_desc.clone()),
            &metrics,
            consensus_bad_nodes_stake_threshold,
        );

        // THEN
        assert_eq!(low_scoring.load().len(), 1);
        assert_eq!(
            *low_scoring
                .load()
                .get(sui_committee.authority_by_index(4).unwrap())
                .unwrap(),
            0
        );
    }

    /// Generate a pair of Sui and consensus committees for the given size.
    fn generate_committees(committee_size: usize) -> (Committee, ConsensusCommittee) {
        let (consensus_committee, _) = local_committee_and_keys(0, vec![1; committee_size]);

        let public_keys = consensus_committee
            .authorities()
            .map(|(_i, authority)| authority.authority_key.inner())
            .collect::<Vec<_>>();
        let sui_authorities = public_keys
            .iter()
            .map(|key| (AuthorityPublicKeyBytes::from(*key), 1))
            .collect::<Vec<_>>();
        let sui_committee = Committee::new_for_testing_with_normalized_voting_power(
            0,
            sui_authorities.iter().cloned().collect(),
        );

        (sui_committee, consensus_committee)
    }
}