cometbft_light_client_verifier/operations/
voting_power.rs

1//! Provides an interface and default implementation for the `VotingPower` operation
2
3use alloc::collections::BTreeSet as HashSet;
4use core::{convert::TryFrom, fmt, marker::PhantomData};
5
6use cometbft::{
7    block::CommitSig,
8    crypto::signature,
9    trust_threshold::TrustThreshold as _,
10    vote::{SignedVote, ValidatorIndex, Vote},
11};
12use serde::{Deserialize, Serialize};
13
14use crate::{
15    errors::VerificationError,
16    prelude::*,
17    types::{Commit, SignedHeader, TrustThreshold, ValidatorSet},
18};
19
20/// Tally for the voting power computed by the `VotingPowerCalculator`
21#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq)]
22pub struct VotingPowerTally {
23    /// Total voting power
24    pub total: u64,
25    /// Tallied voting power
26    pub tallied: u64,
27    /// Trust threshold for voting power
28    pub trust_threshold: TrustThreshold,
29}
30
31impl fmt::Display for VotingPowerTally {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        write!(
34            f,
35            "VotingPower(total={} tallied={} trust_threshold={})",
36            self.total, self.tallied, self.trust_threshold
37        )
38    }
39}
40
41/// Computes the voting power in a commit against a validator set.
42///
43/// This trait provides default implementation of some helper functions.
44pub trait VotingPowerCalculator: Send + Sync {
45    /// Compute the total voting power in a validator set
46    fn total_power_of(&self, validator_set: &ValidatorSet) -> u64 {
47        validator_set
48            .validators()
49            .iter()
50            .fold(0u64, |total, val_info| total + val_info.power.value())
51    }
52
53    /// Check against the given threshold that there is enough trust
54    /// between an untrusted header and a trusted validator set
55    fn check_enough_trust(
56        &self,
57        untrusted_header: &SignedHeader,
58        trusted_validators: &ValidatorSet,
59        trust_threshold: TrustThreshold,
60    ) -> Result<(), VerificationError> {
61        let voting_power =
62            self.voting_power_in(untrusted_header, trusted_validators, trust_threshold)?;
63
64        if trust_threshold.is_enough_power(voting_power.tallied, voting_power.total) {
65            Ok(())
66        } else {
67            Err(VerificationError::not_enough_trust(voting_power))
68        }
69    }
70
71    /// Check if there is 2/3rd overlap between an untrusted header and untrusted validator set
72    fn check_signers_overlap(
73        &self,
74        untrusted_header: &SignedHeader,
75        untrusted_validators: &ValidatorSet,
76    ) -> Result<(), VerificationError> {
77        let trust_threshold = TrustThreshold::TWO_THIRDS;
78        let voting_power =
79            self.voting_power_in(untrusted_header, untrusted_validators, trust_threshold)?;
80
81        if trust_threshold.is_enough_power(voting_power.tallied, voting_power.total) {
82            Ok(())
83        } else {
84            Err(VerificationError::insufficient_signers_overlap(
85                voting_power,
86            ))
87        }
88    }
89
90    /// Compute the voting power in a header and its commit against a validator set.
91    ///
92    /// The `trust_threshold` is currently not used, but might be in the future
93    /// for optimization purposes.
94    fn voting_power_in(
95        &self,
96        signed_header: &SignedHeader,
97        validator_set: &ValidatorSet,
98        trust_threshold: TrustThreshold,
99    ) -> Result<VotingPowerTally, VerificationError>;
100}
101
102/// Default implementation of a `VotingPowerCalculator`, parameterized with
103/// the signature verification trait.
104#[derive(Copy, Clone, Debug, PartialEq, Eq)]
105pub struct ProvidedVotingPowerCalculator<V> {
106    _verifier: PhantomData<V>,
107}
108
109// Safety: the only member is phantom data
110unsafe impl<V> Send for ProvidedVotingPowerCalculator<V> {}
111unsafe impl<V> Sync for ProvidedVotingPowerCalculator<V> {}
112
113impl<V> Default for ProvidedVotingPowerCalculator<V> {
114    fn default() -> Self {
115        Self {
116            _verifier: PhantomData,
117        }
118    }
119}
120
121/// Default implementation of a `VotingPowerCalculator`.
122#[cfg(feature = "rust-crypto")]
123pub type ProdVotingPowerCalculator =
124    ProvidedVotingPowerCalculator<cometbft::crypto::default::signature::Verifier>;
125
126impl<V: signature::Verifier> VotingPowerCalculator for ProvidedVotingPowerCalculator<V> {
127    fn voting_power_in(
128        &self,
129        signed_header: &SignedHeader,
130        validator_set: &ValidatorSet,
131        trust_threshold: TrustThreshold,
132    ) -> Result<VotingPowerTally, VerificationError> {
133        let signatures = &signed_header.commit.signatures;
134
135        let mut tallied_voting_power = 0_u64;
136        let mut seen_validators = HashSet::new();
137
138        // Get non-absent votes from the signatures
139        let non_absent_votes = signatures.iter().enumerate().flat_map(|(idx, signature)| {
140            non_absent_vote(
141                signature,
142                ValidatorIndex::try_from(idx).unwrap(),
143                &signed_header.commit,
144            )
145            .map(|vote| (signature, vote))
146        });
147
148        for (signature, vote) in non_absent_votes {
149            // Ensure we only count a validator's power once
150            if seen_validators.contains(&vote.validator_address) {
151                return Err(VerificationError::duplicate_validator(
152                    vote.validator_address,
153                ));
154            } else {
155                seen_validators.insert(vote.validator_address);
156            }
157
158            let validator = match validator_set.validator(vote.validator_address) {
159                Some(validator) => validator,
160                None => continue, // Cannot find matching validator, so we skip the vote
161            };
162
163            let signed_vote =
164                SignedVote::from_vote(vote.clone(), signed_header.header.chain_id.clone())
165                    .ok_or_else(VerificationError::missing_signature)?;
166
167            // Check vote is valid
168            let sign_bytes = signed_vote.sign_bytes();
169            if validator
170                .verify_signature::<V>(&sign_bytes, signed_vote.signature())
171                .is_err()
172            {
173                return Err(VerificationError::invalid_signature(
174                    signed_vote.signature().as_bytes().to_vec(),
175                    Box::new(validator),
176                    sign_bytes,
177                ));
178            }
179
180            // If the vote is neither absent nor nil, tally its power
181            if signature.is_commit() {
182                tallied_voting_power += validator.power();
183            } else {
184                // It's OK. We include stray signatures (~votes for nil)
185                // to measure validator availability.
186            }
187
188            // TODO: Break out of the loop when we have enough voting power.
189            // See https://github.com/informalsystems/tendermint-rs/issues/235
190        }
191
192        let voting_power = VotingPowerTally {
193            total: self.total_power_of(validator_set),
194            tallied: tallied_voting_power,
195            trust_threshold,
196        };
197
198        Ok(voting_power)
199    }
200}
201
202fn non_absent_vote(
203    commit_sig: &CommitSig,
204    validator_index: ValidatorIndex,
205    commit: &Commit,
206) -> Option<Vote> {
207    let (validator_address, timestamp, signature, block_id) = match commit_sig {
208        CommitSig::BlockIdFlagAbsent { .. } => return None,
209        CommitSig::BlockIdFlagCommit {
210            validator_address,
211            timestamp,
212            signature,
213        } => (
214            *validator_address,
215            *timestamp,
216            signature,
217            Some(commit.block_id),
218        ),
219        CommitSig::BlockIdFlagNil {
220            validator_address,
221            timestamp,
222            signature,
223        } => (*validator_address, *timestamp, signature, None),
224    };
225
226    Some(Vote {
227        vote_type: cometbft::vote::Type::Precommit,
228        height: commit.height,
229        round: commit.round,
230        block_id,
231        timestamp: Some(timestamp),
232        validator_address,
233        validator_index,
234        signature: signature.clone(),
235        extension: Default::default(),
236        extension_signature: None,
237    })
238}
239
240// The below unit tests replaces the static voting power test files
241// see https://github.com/informalsystems/tendermint-rs/pull/383
242// This is essentially to remove the heavy dependency on MBT
243// TODO: We plan to add Lightweight MBT for `voting_power_in` in the near future
244#[cfg(test)]
245mod tests {
246    use cometbft::trust_threshold::TrustThresholdFraction;
247    use cometbft_testgen::{
248        light_block::generate_signed_header, Commit, Generator, Header,
249        LightBlock as TestgenLightBlock, ValidatorSet, Vote as TestgenVote,
250    };
251
252    use super::*;
253    use crate::{errors::VerificationErrorDetail, types::LightBlock};
254
255    const EXPECTED_RESULT: VotingPowerTally = VotingPowerTally {
256        total: 100,
257        tallied: 0,
258        trust_threshold: TrustThresholdFraction::ONE_THIRD,
259    };
260
261    #[test]
262    fn test_empty_signatures() {
263        let vp_calculator = ProdVotingPowerCalculator::default();
264        let trust_threshold = TrustThreshold::default();
265
266        let mut light_block: LightBlock = TestgenLightBlock::new_default(10)
267            .generate()
268            .unwrap()
269            .into();
270        light_block.signed_header.commit.signatures = vec![];
271
272        let result_ok = vp_calculator.voting_power_in(
273            &light_block.signed_header,
274            &light_block.validators,
275            trust_threshold,
276        );
277
278        // ensure the result matches the expected result
279        assert_eq!(result_ok.unwrap(), EXPECTED_RESULT);
280    }
281
282    #[test]
283    fn test_all_signatures_absent() {
284        let vp_calculator = ProdVotingPowerCalculator::default();
285        let trust_threshold = TrustThreshold::default();
286
287        let mut testgen_lb = TestgenLightBlock::new_default(10);
288        let mut commit = testgen_lb.commit.clone().unwrap();
289        // an empty vector of votes translates into all absent signatures
290        commit.votes = Some(vec![]);
291        testgen_lb.commit = Some(commit);
292        let light_block: LightBlock = testgen_lb.generate().unwrap().into();
293
294        let result_ok = vp_calculator.voting_power_in(
295            &light_block.signed_header,
296            &light_block.validators,
297            trust_threshold,
298        );
299
300        // ensure the result matches the expected result
301        assert_eq!(result_ok.unwrap(), EXPECTED_RESULT);
302    }
303
304    #[test]
305    fn test_all_signatures_nil() {
306        let vp_calculator = ProdVotingPowerCalculator::default();
307        let trust_threshold = TrustThreshold::default();
308
309        let validator_set = ValidatorSet::new(vec!["a", "b"]);
310        let vals = validator_set.clone().validators.unwrap();
311        let header = Header::new(&vals);
312        let votes = vec![
313            TestgenVote::new(vals[0].clone(), header.clone()).nil(true),
314            TestgenVote::new(vals[1].clone(), header.clone()).nil(true),
315        ];
316        let commit = Commit::new_with_votes(header.clone(), 1, votes);
317        let signed_header = generate_signed_header(&header, &commit).unwrap();
318        let valset = validator_set.generate().unwrap();
319
320        let result_ok = vp_calculator.voting_power_in(&signed_header, &valset, trust_threshold);
321
322        // ensure the result matches the expected result
323        assert_eq!(result_ok.unwrap(), EXPECTED_RESULT);
324    }
325
326    #[test]
327    fn test_one_invalid_signature() {
328        let vp_calculator = ProdVotingPowerCalculator::default();
329        let trust_threshold = TrustThreshold::default();
330
331        let mut testgen_lb = TestgenLightBlock::new_default(10);
332        let mut commit = testgen_lb.commit.clone().unwrap();
333        let mut votes = commit.votes.unwrap();
334        let vote = votes.pop().unwrap();
335        let header = vote.clone().header.unwrap().chain_id("bad-chain");
336        votes.push(vote.header(header));
337
338        commit.votes = Some(votes);
339        testgen_lb.commit = Some(commit);
340        let light_block: LightBlock = testgen_lb.generate().unwrap().into();
341
342        let result_err = vp_calculator.voting_power_in(
343            &light_block.signed_header,
344            &light_block.validators,
345            trust_threshold,
346        );
347
348        match result_err {
349            Err(VerificationError(VerificationErrorDetail::InvalidSignature(_), _)) => {},
350            _ => panic!("expected InvalidSignature error"),
351        }
352    }
353
354    #[test]
355    fn test_all_signatures_invalid() {
356        let vp_calculator = ProdVotingPowerCalculator::default();
357        let trust_threshold = TrustThreshold::default();
358
359        let mut testgen_lb = TestgenLightBlock::new_default(10);
360        let header = testgen_lb.header.unwrap().chain_id("bad-chain");
361        testgen_lb.header = Some(header);
362        let light_block: LightBlock = testgen_lb.generate().unwrap().into();
363
364        let result_err = vp_calculator.voting_power_in(
365            &light_block.signed_header,
366            &light_block.validators,
367            trust_threshold,
368        );
369
370        match result_err {
371            Err(VerificationError(VerificationErrorDetail::InvalidSignature(_), _)) => {},
372            _ => panic!("expected InvalidSignature error"),
373        }
374    }
375
376    #[test]
377    fn test_signatures_from_diff_valset() {
378        let vp_calculator = ProdVotingPowerCalculator::default();
379        let trust_threshold = TrustThreshold::default();
380
381        let mut light_block: LightBlock = TestgenLightBlock::new_default(10)
382            .generate()
383            .unwrap()
384            .into();
385        light_block.validators = ValidatorSet::new(vec!["bad-val1", "bad-val2"])
386            .generate()
387            .unwrap();
388
389        let result_ok = vp_calculator.voting_power_in(
390            &light_block.signed_header,
391            &light_block.validators,
392            trust_threshold,
393        );
394
395        // ensure the result matches the expected result
396        assert_eq!(result_ok.unwrap(), EXPECTED_RESULT);
397    }
398}