1use std::{
6 collections::{BTreeMap, HashMap, hash_map::Entry},
7 hash::Hash,
8 sync::Arc,
9};
10
11use iota_sdk_types::crypto::Intent;
12use iota_types::{
13 base_types::{AuthorityName, ConciseableName},
14 committee::{Committee, CommitteeTrait, StakeUnit},
15 crypto::{AuthorityQuorumSignInfo, AuthoritySignInfo, AuthoritySignInfoTrait},
16 error::{IotaError, IotaResult},
17 message_envelope::{Envelope, Message},
18};
19use serde::Serialize;
20use tracing::warn;
21use typed_store::TypedStoreError;
22
23#[derive(Debug)]
27pub struct StakeAggregator<S, const STRENGTH: bool> {
28 data: HashMap<AuthorityName, S>,
29 total_votes: StakeUnit,
30 committee: Arc<Committee>,
31}
32
33impl<S: Clone + Eq, const STRENGTH: bool> StakeAggregator<S, STRENGTH> {
40 pub fn new(committee: Arc<Committee>) -> Self {
41 Self {
42 data: Default::default(),
43 total_votes: Default::default(),
44 committee,
45 }
46 }
47
48 pub fn from_iter<I: Iterator<Item = Result<(AuthorityName, S), TypedStoreError>>>(
49 committee: Arc<Committee>,
50 data: I,
51 ) -> IotaResult<Self> {
52 let mut this = Self::new(committee);
53 for item in data {
54 let (authority, s) = item?;
55 this.insert_generic(authority, s);
56 }
57 Ok(this)
58 }
59
60 pub fn insert_generic(
67 &mut self,
68 authority: AuthorityName,
69 s: S,
70 ) -> InsertResult<&HashMap<AuthorityName, S>> {
71 match self.data.entry(authority) {
72 Entry::Occupied(oc) => {
73 return InsertResult::Failed {
74 error: IotaError::StakeAggregatorRepeatedSigner {
75 signer: authority,
76 conflicting_sig: oc.get() != &s,
77 },
78 };
79 }
80 Entry::Vacant(va) => {
81 va.insert(s);
82 }
83 }
84 let votes = self.committee.weight(&authority);
85 if votes > 0 {
86 self.total_votes += votes;
87 if self.total_votes >= self.committee.threshold::<STRENGTH>() {
88 InsertResult::QuorumReached(&self.data)
89 } else {
90 InsertResult::NotEnoughVotes {
91 bad_votes: 0,
92 bad_authorities: vec![],
93 }
94 }
95 } else {
96 InsertResult::Failed {
97 error: IotaError::InvalidAuthenticator,
98 }
99 }
100 }
101
102 pub fn contains_key(&self, authority: &AuthorityName) -> bool {
103 self.data.contains_key(authority)
104 }
105
106 pub fn keys(&self) -> impl Iterator<Item = &AuthorityName> {
107 self.data.keys()
108 }
109
110 pub fn committee(&self) -> &Committee {
111 &self.committee
112 }
113
114 pub fn total_votes(&self) -> StakeUnit {
115 self.total_votes
116 }
117
118 pub fn validator_sig_count(&self) -> usize {
119 self.data.len()
120 }
121}
122
123impl<const STRENGTH: bool> StakeAggregator<AuthoritySignInfo, STRENGTH> {
124 pub fn insert<T: Message + Serialize>(
129 &mut self,
130 envelope: Envelope<T, AuthoritySignInfo>,
131 ) -> InsertResult<AuthorityQuorumSignInfo<STRENGTH>> {
132 let (data, sig) = envelope.into_data_and_sig();
133 if self.committee.epoch != sig.epoch {
134 return InsertResult::Failed {
135 error: IotaError::WrongEpoch {
136 expected_epoch: self.committee.epoch,
137 actual_epoch: sig.epoch,
138 },
139 };
140 }
141 match self.insert_generic(sig.authority, sig) {
142 InsertResult::QuorumReached(_) => {
143 match AuthorityQuorumSignInfo::<STRENGTH>::new_from_auth_sign_infos(
144 self.data.values().cloned().collect(),
145 self.committee(),
146 ) {
147 Ok(aggregated) => {
148 match aggregated.verify_secure(
149 &data,
150 Intent::iota_app(T::SCOPE),
151 self.committee(),
152 ) {
153 Ok(_) => InsertResult::QuorumReached(aggregated),
156 Err(_) => {
157 let mut bad_votes = 0;
169 let mut bad_authorities = vec![];
170 for (name, sig) in &self.data.clone() {
171 if let Err(err) = sig.verify_secure(
172 &data,
173 Intent::iota_app(T::SCOPE),
174 self.committee(),
175 ) {
176 warn!(name=?name.concise(), "Bad stake from validator: {:?}", err);
183 self.data.remove(name);
184 let votes = self.committee.weight(name);
185 self.total_votes -= votes;
186 bad_votes += votes;
187 bad_authorities.push(*name);
188 }
189 }
190 if self.total_votes >= self.committee.threshold::<STRENGTH>() {
193 match AuthorityQuorumSignInfo::<STRENGTH>::new_from_auth_sign_infos(
194 self.data.values().cloned().collect(),
195 self.committee(),
196 ) {
197 Ok(aggregated) => InsertResult::QuorumReached(aggregated),
198 Err(error) => InsertResult::Failed { error },
199 }
200 } else {
201 InsertResult::NotEnoughVotes {
202 bad_votes,
203 bad_authorities,
204 }
205 }
206 }
207 }
208 }
209 Err(error) => InsertResult::Failed { error },
210 }
211 }
212 InsertResult::Failed { error } => InsertResult::Failed { error },
214 InsertResult::NotEnoughVotes {
215 bad_votes,
216 bad_authorities,
217 } => InsertResult::NotEnoughVotes {
218 bad_votes,
219 bad_authorities,
220 },
221 }
222 }
223}
224
225pub enum InsertResult<CertT> {
226 QuorumReached(CertT),
227 Failed {
228 error: IotaError,
229 },
230 NotEnoughVotes {
231 bad_votes: u64,
232 bad_authorities: Vec<AuthorityName>,
233 },
234}
235
236impl<CertT> InsertResult<CertT> {
237 pub fn is_quorum_reached(&self) -> bool {
238 matches!(self, Self::QuorumReached(..))
239 }
240}
241
242#[derive(Debug)]
248pub struct MultiStakeAggregator<K, V, const STRENGTH: bool> {
249 committee: Arc<Committee>,
250 stake_maps: HashMap<K, (V, StakeAggregator<AuthoritySignInfo, STRENGTH>)>,
251}
252
253impl<K, V, const STRENGTH: bool> MultiStakeAggregator<K, V, STRENGTH> {
254 pub fn new(committee: Arc<Committee>) -> Self {
255 Self {
256 committee,
257 stake_maps: Default::default(),
258 }
259 }
260
261 pub fn unique_key_count(&self) -> usize {
262 self.stake_maps.len()
263 }
264
265 pub fn total_votes(&self) -> StakeUnit {
266 self.stake_maps
267 .values()
268 .map(|(_, stake_aggregator)| stake_aggregator.total_votes())
269 .sum()
270 }
271}
272
273impl<K, V, const STRENGTH: bool> MultiStakeAggregator<K, V, STRENGTH>
274where
275 K: Hash + Eq,
276 V: Message + Serialize + Clone,
277{
278 pub fn insert(
279 &mut self,
280 k: K,
281 envelope: Envelope<V, AuthoritySignInfo>,
282 ) -> InsertResult<AuthorityQuorumSignInfo<STRENGTH>> {
283 if let Some(entry) = self.stake_maps.get_mut(&k) {
284 entry.1.insert(envelope)
285 } else {
286 let mut new_entry = StakeAggregator::new(self.committee.clone());
287 let result = new_entry.insert(envelope.clone());
288 if !matches!(result, InsertResult::Failed { .. }) {
289 self.stake_maps.insert(k, (envelope.into_data(), new_entry));
292 }
293 result
294 }
295 }
296}
297
298impl<K, V, const STRENGTH: bool> MultiStakeAggregator<K, V, STRENGTH>
299where
300 K: Clone + Ord,
301{
302 pub fn get_all_unique_values(&self) -> BTreeMap<K, (Vec<AuthorityName>, StakeUnit)> {
303 self.stake_maps
304 .iter()
305 .map(|(k, (_, s))| (k.clone(), (s.data.keys().copied().collect(), s.total_votes)))
306 .collect()
307 }
308}
309
310impl<K, V, const STRENGTH: bool> MultiStakeAggregator<K, V, STRENGTH>
311where
312 K: Hash + Eq,
313{
314 #[expect(dead_code)]
315 pub fn authorities_for_key(&self, k: &K) -> Option<impl Iterator<Item = &AuthorityName>> {
316 self.stake_maps.get(k).map(|(_, agg)| agg.keys())
317 }
318
319 pub fn uncommitted_stake(&self) -> StakeUnit {
322 self.committee.total_votes() - self.total_votes()
323 }
324
325 pub fn plurality_stake(&self) -> StakeUnit {
327 self.stake_maps
328 .values()
329 .map(|(_, agg)| agg.total_votes())
330 .max()
331 .unwrap_or_default()
332 }
333
334 pub fn quorum_unreachable(&self) -> bool {
337 self.uncommitted_stake() + self.plurality_stake() < self.committee.threshold::<STRENGTH>()
338 }
339}
340
341#[cfg(test)]
342mod stake_aggregator_insert_tests {
343 use std::{collections::BTreeMap, sync::Arc};
344
345 use fastcrypto::{
346 hash::{HashFunction, Sha3_256},
347 traits::KeyPair,
348 };
349 use iota_sdk_types::crypto::IntentScope;
350 use iota_types::{
351 base_types::AuthorityName,
352 committee::Committee,
353 crypto::{AuthoritySignInfo, random_committee_key_pairs_of_size},
354 message_envelope::{Envelope, Message},
355 };
356 use serde::Serialize;
357
358 use super::*;
359
360 #[derive(Clone, Debug, Serialize, PartialEq, Eq, Hash)]
361 struct TestMessage {
362 value: String,
363 }
364
365 impl Message for TestMessage {
366 type DigestType = [u8; 32];
367 const SCOPE: IntentScope = IntentScope::SenderSignedTransaction;
368
369 fn digest(&self) -> Self::DigestType {
370 let mut hasher = Sha3_256::default();
371 hasher.update(self.value.as_bytes());
372 hasher.finalize().digest
373 }
374 }
375
376 #[test]
380 fn test_quorum_not_lost_after_bad_sig_eviction() {
381 let key_pairs = random_committee_key_pairs_of_size(2);
384 let mut names: Vec<AuthorityName> = key_pairs
385 .iter()
386 .map(|kp| AuthorityName::from(kp.public()))
387 .collect();
388 names.sort();
389
390 let voting_rights: BTreeMap<AuthorityName, u64> = names
391 .iter()
392 .enumerate()
393 .map(|(i, name)| (*name, if i == 0 { 7 } else { 3 }))
394 .collect();
395 let committee = Arc::new(Committee::new_for_testing_with_normalized_voting_power(
396 0,
397 voting_rights,
398 ));
399
400 let find_kp = |name: &AuthorityName| {
401 key_pairs
402 .iter()
403 .find(|kp| &AuthorityName::from(kp.public()) == name)
404 .unwrap()
405 };
406 let (auth0, key0) = (names[0], find_kp(&names[0]));
407 let (auth1, key1) = (names[1], find_kp(&names[1]));
408
409 let mut agg: StakeAggregator<AuthoritySignInfo, true> = StakeAggregator::new(committee);
410
411 let msg = TestMessage {
412 value: "real".to_string(),
413 };
414 let msg_bad = TestMessage {
415 value: "wrong".to_string(),
416 };
417
418 let envelope_bad = Envelope::<TestMessage, AuthoritySignInfo>::new(0, msg_bad, key1, auth1);
420 assert!(matches!(
421 agg.insert(envelope_bad),
422 InsertResult::NotEnoughVotes { .. }
423 ));
424
425 let envelope_good = Envelope::<TestMessage, AuthoritySignInfo>::new(0, msg, key0, auth0);
431 assert!(
432 agg.insert(envelope_good).is_quorum_reached(),
433 "valid sig with weight > quorum threshold must yield QuorumReached after bad sig eviction"
434 );
435 }
436}