pod_types/consensus/
committee.rs

1use 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    // utility function that does aggregate verification over an arbitrary hash
65    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        // Recover and validate each signature
80        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            // Skip if signer not in committee (treat as invalid signature)
90            if !self.is_in_committee(&recovered_address) {
91                continue;
92            }
93
94            recovered_signers.insert(recovered_address);
95        }
96
97        // Verify we have enough unique valid signatures from committee members
98        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}