iota_core/
scoring_decision.rs

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