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 {}",
34        consensus_bad_nodes_stake_threshold
35    );
36
37    let Some(reputation_scores) = reputation_score_sorted_desc else {
38        return;
39    };
40
41    // We order the authorities by score ascending order in the exact same way as
42    // the reputation scores do - so we keep complete alignment between
43    // implementations
44    let scores_per_authority_order_asc: Vec<_> = reputation_scores
45        .into_iter()
46        .rev() // we reverse so we get them in asc order
47        .collect();
48
49    let mut final_low_scoring_map = HashMap::new();
50    let mut total_stake = 0;
51    for (index, score) in scores_per_authority_order_asc {
52        let authority_name = iota_committee.authority_by_index(index).unwrap();
53        let authority_index = consensus_committee
54            .to_authority_index(index as usize)
55            .unwrap();
56        let consensus_authority = consensus_committee.authority(authority_index);
57        let hostname = &consensus_authority.hostname;
58        let stake = consensus_authority.stake;
59        total_stake += stake;
60
61        let included = if total_stake
62            <= consensus_bad_nodes_stake_threshold * consensus_committee.total_stake() / 100
63        {
64            final_low_scoring_map.insert(*authority_name, score);
65            true
66        } else {
67            false
68        };
69
70        if !hostname.is_empty() {
71            debug!(
72                "authority {} has score {}, is low scoring: {}",
73                hostname, score, included
74            );
75
76            metrics
77                .consensus_handler_scores
78                .with_label_values(&[hostname])
79                .set(score as i64);
80        }
81    }
82    // Report the actual flagged final low scoring authorities
83    metrics
84        .consensus_handler_num_low_scoring_authorities
85        .set(final_low_scoring_map.len() as i64);
86    low_scoring_authorities.swap(Arc::new(final_low_scoring_map));
87}
88
89#[cfg(test)]
90mod tests {
91    #![allow(clippy::mutable_key_type)]
92    use std::{collections::HashMap, sync::Arc};
93
94    use arc_swap::ArcSwap;
95    use consensus_config::{Committee as ConsensusCommittee, local_committee_and_keys};
96    use iota_types::{committee::Committee, crypto::AuthorityPublicKeyBytes};
97    use prometheus::Registry;
98
99    use crate::{authority::AuthorityMetrics, scoring_decision::update_low_scoring_authorities};
100
101    #[test]
102    #[cfg_attr(msim, ignore)]
103    pub fn test_update_low_scoring_authorities() {
104        // GIVEN
105        // Total stake is 8 for this committee and every authority has equal stake = 1
106        let (iota_committee, consensus_committee) = generate_committees(8);
107
108        let low_scoring = Arc::new(ArcSwap::from_pointee(HashMap::new()));
109        let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));
110
111        // there is a low outlier in the non zero scores, exclude it as well as down
112        // nodes
113        let authorities_by_score_desc = vec![
114            (1, 390_u64),
115            (0, 350_u64),
116            (6, 340_u64),
117            (7, 310_u64),
118            (5, 300_u64),
119            (3, 50_u64),
120            (2, 50_u64),
121            (4, 0_u64), // down node
122        ];
123
124        // WHEN
125        let consensus_bad_nodes_stake_threshold = 33; // 33 * 8 / 100 = 2 low scoring validator
126
127        update_low_scoring_authorities(
128            low_scoring.clone(),
129            &iota_committee,
130            &consensus_committee,
131            Some(authorities_by_score_desc.clone()),
132            &metrics,
133            consensus_bad_nodes_stake_threshold,
134        );
135
136        // THEN
137        assert_eq!(low_scoring.load().len(), 2);
138        assert_eq!(
139            *low_scoring
140                .load()
141                // authority 2 is 2nd to the last in authorities_by_score_desc
142                .get(iota_committee.authority_by_index(2).unwrap())
143                .unwrap(),
144            50
145        );
146        assert_eq!(
147            *low_scoring
148                .load()
149                // authority 4 is the last in authorities_by_score_desc
150                .get(iota_committee.authority_by_index(4).unwrap())
151                .unwrap(),
152            0
153        );
154
155        // WHEN setting the threshold to lower
156        let consensus_bad_nodes_stake_threshold = 20; // 20 * 8 / 100 = 1 low scoring validator
157        update_low_scoring_authorities(
158            low_scoring.clone(),
159            &iota_committee,
160            &consensus_committee,
161            Some(authorities_by_score_desc.clone()),
162            &metrics,
163            consensus_bad_nodes_stake_threshold,
164        );
165
166        // THEN
167        assert_eq!(low_scoring.load().len(), 1);
168        assert_eq!(
169            *low_scoring
170                .load()
171                .get(iota_committee.authority_by_index(4).unwrap())
172                .unwrap(),
173            0
174        );
175    }
176
177    /// Generate a pair of IOTA and consensus committees for the given size.
178    fn generate_committees(committee_size: usize) -> (Committee, ConsensusCommittee) {
179        let (consensus_committee, _) = local_committee_and_keys(0, vec![1; committee_size]);
180
181        let public_keys = consensus_committee
182            .authorities()
183            .map(|(_i, authority)| authority.authority_key.inner())
184            .collect::<Vec<_>>();
185        let iota_authorities = public_keys
186            .iter()
187            .map(|key| (AuthorityPublicKeyBytes::from(*key), 1))
188            .collect::<Vec<_>>();
189        let iota_committee = Committee::new_for_testing_with_normalized_voting_power(
190            0,
191            iota_authorities.iter().cloned().collect(),
192        );
193
194        (iota_committee, consensus_committee)
195    }
196}