pod_types/consensus/
committee.rs1use super::{Attestation, Certificate};
2use crate::cryptography::hash::{Hash, Hashable};
3use alloy_primitives::{Address, Signature};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeSet;
6
7#[derive(Debug, thiserror::Error)]
8pub enum CommitteeError {
9 #[error("verification failed due to insufficient quorum ({got} < {required})")]
10 InsufficientQuorum { got: usize, required: usize },
11 #[error("validator {0} not in committee")]
12 ValidatorNotInCommittee(Address),
13 #[error(transparent)]
14 SignatureError(#[from] alloy_primitives::SignatureError),
15}
16
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct Committee {
19 pub validators: BTreeSet<Address>,
20 pub quorum_size: usize,
21}
22
23impl Committee {
24 pub fn new(validators: impl IntoIterator<Item = Address>, quorum_size: usize) -> Self {
25 let validator_set = validators.into_iter().collect();
26 Committee {
27 validators: validator_set,
28 quorum_size,
29 }
30 }
31
32 pub fn size(&self) -> usize {
33 self.validators.len()
34 }
35
36 pub fn fault_tolerance(&self) -> usize {
37 self.size() - self.quorum_size
38 }
39
40 pub fn f_plus_one(&self) -> usize {
41 self.fault_tolerance() + 1
42 }
43
44 pub fn is_in_committee(&self, address: &Address) -> bool {
45 self.validators.contains(address)
46 }
47
48 pub fn verify_attestation<T: Hashable>(
49 &self,
50 attestation: &Attestation<T>,
51 ) -> Result<bool, CommitteeError> {
52 if !self.is_in_committee(&attestation.public_key) {
53 return Err(CommitteeError::ValidatorNotInCommittee(
54 attestation.public_key,
55 ));
56 }
57
58 let signer = attestation
59 .signature
60 .recover_address_from_prehash(&attestation.attested.hash_custom())?;
61 Ok(signer == attestation.public_key)
62 }
63
64 pub fn verify_aggregate_attestation(
66 &self,
67 digest: Hash,
68 signatures: &Vec<Signature>,
69 ) -> Result<(), CommitteeError> {
70 if signatures.len() < self.quorum_size {
71 return Err(CommitteeError::InsufficientQuorum {
72 got: signatures.len(),
73 required: self.quorum_size,
74 });
75 }
76
77 let mut recovered_signers = BTreeSet::new();
78
79 for sig in signatures {
81 let recovered_address = match sig.recover_address_from_prehash(&digest) {
82 Ok(addr) => addr,
83 Err(e) => {
84 tracing::debug!("failed to recover address from signature: {e}");
85 continue;
86 }
87 };
88
89 if !self.is_in_committee(&recovered_address) {
91 continue;
92 }
93
94 recovered_signers.insert(recovered_address);
95 }
96
97 if recovered_signers.len() < self.quorum_size {
99 return Err(CommitteeError::InsufficientQuorum {
100 got: recovered_signers.len(),
101 required: self.quorum_size,
102 });
103 }
104
105 Ok(())
106 }
107
108 pub fn verify_certificate<C: Hashable>(
109 &self,
110 certificate: &Certificate<C>,
111 ) -> Result<(), CommitteeError> {
112 self.verify_aggregate_attestation(
113 certificate.certified.hash_custom(),
114 &certificate.signatures,
115 )
116 }
117}