tendermint_light_client_verifier/operations/
voting_power.rs

1//! Provides an interface and default implementation for the `VotingPower` operation
2
3use alloc::vec::Vec;
4use core::{fmt, marker::PhantomData};
5
6use serde::{Deserialize, Serialize};
7use tendermint::{
8    account,
9    block::CommitSig,
10    chain,
11    crypto::signature,
12    trust_threshold::TrustThreshold as _,
13    validator,
14    vote::{SignedVote, ValidatorIndex, Vote},
15};
16
17use crate::{
18    errors::VerificationError,
19    prelude::*,
20    types::{Commit, SignedHeader, TrustThreshold, ValidatorSet},
21};
22
23/// Tally for the voting power computed by the `VotingPowerCalculator`
24#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq)]
25pub struct VotingPowerTally {
26    /// Total voting power
27    pub total: u64,
28    /// Tallied voting power
29    pub tallied: u64,
30    /// Trust threshold for voting power
31    pub trust_threshold: TrustThreshold,
32}
33
34impl VotingPowerTally {
35    fn new(total: u64, trust_threshold: TrustThreshold) -> Self {
36        Self {
37            total,
38            tallied: 0,
39            trust_threshold,
40        }
41    }
42
43    /// Adds given amount of power to tallied voting power amount.
44    fn tally(&mut self, power: u64) {
45        self.tallied += power;
46        debug_assert!(self.tallied <= self.total);
47    }
48
49    /// Checks whether tallied amount meets trust threshold.
50    fn check(&self) -> Result<(), Self> {
51        if self
52            .trust_threshold
53            .is_enough_power(self.tallied, self.total)
54        {
55            Ok(())
56        } else {
57            Err(*self)
58        }
59    }
60}
61
62impl fmt::Display for VotingPowerTally {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        write!(
65            f,
66            "VotingPower(total={} tallied={} trust_threshold={})",
67            self.total, self.tallied, self.trust_threshold
68        )
69    }
70}
71
72/// Computes the voting power in a commit against a validator set.
73///
74/// This trait provides default implementation of some helper functions.
75pub trait VotingPowerCalculator: Send + Sync {
76    /// Compute the total voting power in a validator set
77    fn total_power_of(&self, validator_set: &ValidatorSet) -> u64 {
78        validator_set
79            .validators()
80            .iter()
81            .fold(0u64, |total, val_info| total + val_info.power.value())
82    }
83
84    /// Check that there is enough trust between an untrusted header and given
85    /// trusted and untrusted validator sets.
86    ///
87    /// First of all, checks that enough validators from the
88    /// `trusted_validators` set signed the `untrusted_header` to reach given
89    /// `trust_threshold`.
90    ///
91    /// Second of all, checks that enough validators from the
92    /// `untrusted_validators` set signed the `untrusted_header` to reach
93    /// a trust threshold of ⅔.
94    ///
95    /// If both of those conditions aren’t met, it’s unspecified which error is
96    /// returned.
97    fn check_enough_trust_and_signers(
98        &self,
99        untrusted_header: &SignedHeader,
100        trusted_validators: &ValidatorSet,
101        trust_threshold: TrustThreshold,
102        untrusted_validators: &ValidatorSet,
103    ) -> Result<(), VerificationError> {
104        let (trusted_power, untrusted_power) = self.voting_power_in_sets(
105            untrusted_header,
106            (trusted_validators, trust_threshold),
107            (untrusted_validators, TrustThreshold::TWO_THIRDS),
108        )?;
109        trusted_power
110            .check()
111            .map_err(VerificationError::not_enough_trust)?;
112        untrusted_power
113            .check()
114            .map_err(VerificationError::insufficient_signers_overlap)?;
115        Ok(())
116    }
117
118    /// Check if there is 2/3rd overlap between an untrusted header and untrusted validator set
119    fn check_signers_overlap(
120        &self,
121        untrusted_header: &SignedHeader,
122        untrusted_validators: &ValidatorSet,
123    ) -> Result<(), VerificationError> {
124        let trust_threshold = TrustThreshold::TWO_THIRDS;
125        self.voting_power_in(untrusted_header, untrusted_validators, trust_threshold)?
126            .check()
127            .map_err(VerificationError::insufficient_signers_overlap)
128    }
129
130    /// Compute the voting power in a header and its commit against a validator
131    /// set.
132    ///
133    /// Note that the returned tally may be lower than actual tally so long as
134    /// it meets the `trust_threshold`.  Furthermore, the method isn’t
135    /// guaranteed to verify all the signatures present in the signed header.
136    /// If there are invalid signatures, the method may or may not return an
137    /// error depending on which validators those signatures correspond to.
138    ///
139    /// If you have two separate sets of validators and need to check voting
140    /// power for both of them, prefer [`Self::voting_power_in_sets`] method.
141    fn voting_power_in(
142        &self,
143        signed_header: &SignedHeader,
144        validator_set: &ValidatorSet,
145        trust_threshold: TrustThreshold,
146    ) -> Result<VotingPowerTally, VerificationError>;
147
148    /// Compute the voting power in a header and its commit against two separate
149    /// validator sets.
150    ///
151    /// This is equivalent to calling [`Self::voting_power_in`] on each set
152    /// separately but may be more optimised.  Implementators are encouraged to
153    /// write a properly optimised method which avoids checking the same
154    /// signature twice but for a simple unoptimised implementation the
155    /// following works:
156    ///
157    /// ```ignore
158    ///     fn voting_power_in_sets(
159    ///         &self,
160    ///         signed_header: &SignedHeader,
161    ///         first_set: (&ValidatorSet, TrustThreshold),
162    ///         second_set: (&ValidatorSet, TrustThreshold),
163    ///     ) -> Result<(VotingPowerTally, VotingPowerTally), VerificationError> {
164    ///         let first_tally = self.voting_power_in(
165    ///             signed_header,
166    ///             first_set.0,
167    ///             first_set.1,
168    ///         )?;
169    ///         let second_tally = self.voting_power_in(
170    ///             signed_header,
171    ///             first_set.0,
172    ///             first_set.1,
173    ///         )?;
174    ///         Ok((first_tally, second_tally))
175    ///     }
176    ///
177    /// ```
178    fn voting_power_in_sets(
179        &self,
180        signed_header: &SignedHeader,
181        first_set: (&ValidatorSet, TrustThreshold),
182        second_set: (&ValidatorSet, TrustThreshold),
183    ) -> Result<(VotingPowerTally, VotingPowerTally), VerificationError>;
184}
185
186/// Default implementation of a `VotingPowerCalculator`, parameterized with
187/// the signature verification trait.
188#[derive(Copy, Clone, Debug, PartialEq, Eq)]
189pub struct ProvidedVotingPowerCalculator<V> {
190    _verifier: PhantomData<V>,
191}
192
193// Safety: the only member is phantom data
194unsafe impl<V> Send for ProvidedVotingPowerCalculator<V> {}
195unsafe impl<V> Sync for ProvidedVotingPowerCalculator<V> {}
196
197impl<V> Default for ProvidedVotingPowerCalculator<V> {
198    fn default() -> Self {
199        Self {
200            _verifier: PhantomData,
201        }
202    }
203}
204
205/// A signed non-nil vote.
206struct NonAbsentCommitVote {
207    signed_vote: SignedVote,
208    /// Flag indicating whether the signature has already been verified.
209    verified: bool,
210}
211
212impl NonAbsentCommitVote {
213    /// Returns a signed non-nil vote for given commit.
214    ///
215    /// If the CommitSig represents a missing vote or a vote for nil returns
216    /// `None`.  Otherwise, if the vote is missing a signature returns
217    /// `Some(Err)`.  Otherwise, returns a `SignedVote` corresponding to given
218    /// `CommitSig`.
219    pub fn new(
220        commit_sig: &CommitSig,
221        validator_index: ValidatorIndex,
222        commit: &Commit,
223        chain_id: &chain::Id,
224    ) -> Option<Result<Self, VerificationError>> {
225        let (validator_address, timestamp, signature) = match commit_sig {
226            CommitSig::BlockIdFlagAbsent { .. } => return None,
227            CommitSig::BlockIdFlagCommit {
228                validator_address,
229                timestamp,
230                signature,
231            } => (*validator_address, *timestamp, signature),
232            CommitSig::BlockIdFlagNil { .. } => return None,
233        };
234
235        let vote = Vote {
236            vote_type: tendermint::vote::Type::Precommit,
237            height: commit.height,
238            round: commit.round,
239            block_id: Some(commit.block_id),
240            timestamp: Some(timestamp),
241            validator_address,
242            validator_index,
243            signature: signature.clone(),
244            extension: Default::default(),
245            extension_signature: None,
246        };
247        Some(
248            SignedVote::from_vote(vote, chain_id.clone())
249                .ok_or_else(VerificationError::missing_signature)
250                .map(|signed_vote| Self {
251                    signed_vote,
252                    verified: false,
253                }),
254        )
255    }
256
257    /// Returns address of the validator making the vote.
258    pub fn validator_id(&self) -> account::Id {
259        self.signed_vote.validator_id()
260    }
261}
262
263/// Collection of non-absent commit votes.
264struct NonAbsentCommitVotes {
265    /// Votes sorted by validator address.
266    votes: Vec<NonAbsentCommitVote>,
267    /// Internal buffer for storing sign_bytes.
268    ///
269    /// The buffer is reused for each canonical vote so that we allocate it
270    /// once.
271    sign_bytes: Vec<u8>,
272}
273
274impl NonAbsentCommitVotes {
275    /// Initial capacity of the `sign_bytes` buffer.
276    ///
277    /// The buffer will be resized if it happens to be too small so this value
278    /// isn’t critical for correctness.  It’s a matter of performance to avoid
279    /// reallocations.
280    ///
281    /// Note: As of protocol 0.38, maximum length of the sign bytes is `115 + (N > 13) + N`
282    /// where `N` is the length of the chain id.
283    /// Chain id can be at most 50 bytes (see [`tendermint::chain::id::MAX_LEN`])
284    /// thus the largest buffer we’ll ever need is 166 bytes long.
285    const SIGN_BYTES_INITIAL_CAPACITY: usize = 166;
286
287    pub fn new(signed_header: &SignedHeader) -> Result<Self, VerificationError> {
288        let mut votes = signed_header
289            .commit
290            .signatures
291            .iter()
292            .enumerate()
293            .flat_map(|(idx, signature)| {
294                // We never have more than 2³¹ signatures so this always
295                // succeeds.
296                let idx = ValidatorIndex::try_from(idx).unwrap();
297                NonAbsentCommitVote::new(
298                    signature,
299                    idx,
300                    &signed_header.commit,
301                    &signed_header.header.chain_id,
302                )
303            })
304            .collect::<Result<Vec<_>, VerificationError>>()?;
305        votes.sort_unstable_by_key(NonAbsentCommitVote::validator_id);
306
307        // Check if there are duplicate signatures.  If at least one duplicate
308        // is found, report it as an error.
309        let duplicate = votes
310            .windows(2)
311            .find(|pair| pair[0].validator_id() == pair[1].validator_id());
312        if let Some(pair) = duplicate {
313            Err(VerificationError::duplicate_validator(
314                pair[0].validator_id(),
315            ))
316        } else {
317            Ok(Self {
318                votes,
319                sign_bytes: Vec::with_capacity(Self::SIGN_BYTES_INITIAL_CAPACITY),
320            })
321        }
322    }
323
324    /// Looks up a vote cast by given validator.
325    ///
326    /// If the validator didn’t cast a vote or voted for `nil`, returns `Ok(None)`. Otherwise, if
327    /// the vote had valid signature, returns `Ok(Some(idx))` where idx is the validator's index.
328    /// If the vote had invalid signature, returns `Err`.
329    pub fn has_voted<V: signature::Verifier>(
330        &mut self,
331        validator: &validator::Info,
332    ) -> Result<Option<usize>, VerificationError> {
333        if let Ok(idx) = self
334            .votes
335            .binary_search_by_key(&validator.address, NonAbsentCommitVote::validator_id)
336        {
337            let vote = &mut self.votes[idx];
338
339            if !vote.verified {
340                self.sign_bytes.clear();
341                vote.signed_vote
342                    .sign_bytes_into(&mut self.sign_bytes)
343                    .expect("buffer is resized if needed and encoding never fails");
344
345                let sign_bytes = self.sign_bytes.as_slice();
346                validator
347                    .verify_signature::<V>(sign_bytes, vote.signed_vote.signature())
348                    .map_err(|_| {
349                        VerificationError::invalid_signature(
350                            vote.signed_vote.signature().as_bytes().to_vec(),
351                            Box::new(validator.clone()),
352                            sign_bytes.to_vec(),
353                        )
354                    })?;
355
356                vote.verified = true;
357            }
358
359            Ok(Some(idx))
360        } else {
361            Ok(None)
362        }
363    }
364}
365
366/// Default implementation of a `VotingPowerCalculator`.
367#[cfg(feature = "rust-crypto")]
368pub type ProdVotingPowerCalculator =
369    ProvidedVotingPowerCalculator<tendermint::crypto::default::signature::Verifier>;
370
371impl<V: signature::Verifier> VotingPowerCalculator for ProvidedVotingPowerCalculator<V> {
372    fn voting_power_in(
373        &self,
374        signed_header: &SignedHeader,
375        validator_set: &ValidatorSet,
376        trust_threshold: TrustThreshold,
377    ) -> Result<VotingPowerTally, VerificationError> {
378        let mut votes = NonAbsentCommitVotes::new(signed_header)?;
379        voting_power_in_impl::<V>(
380            &mut votes,
381            validator_set,
382            trust_threshold,
383            self.total_power_of(validator_set),
384        )
385    }
386
387    fn voting_power_in_sets(
388        &self,
389        signed_header: &SignedHeader,
390        first_set: (&ValidatorSet, TrustThreshold),
391        second_set: (&ValidatorSet, TrustThreshold),
392    ) -> Result<(VotingPowerTally, VotingPowerTally), VerificationError> {
393        let mut votes = NonAbsentCommitVotes::new(signed_header)?;
394        let first_tally = voting_power_in_impl::<V>(
395            &mut votes,
396            first_set.0,
397            first_set.1,
398            self.total_power_of(first_set.0),
399        )?;
400        let second_tally = voting_power_in_impl::<V>(
401            &mut votes,
402            second_set.0,
403            second_set.1,
404            self.total_power_of(second_set.0),
405        )?;
406        Ok((first_tally, second_tally))
407    }
408}
409
410fn voting_power_in_impl<V: signature::Verifier>(
411    votes: &mut NonAbsentCommitVotes,
412    validator_set: &ValidatorSet,
413    trust_threshold: TrustThreshold,
414    total_voting_power: u64,
415) -> Result<VotingPowerTally, VerificationError> {
416    let mut power = VotingPowerTally::new(total_voting_power, trust_threshold);
417    let mut seen_vals = Vec::new();
418
419    for validator in validator_set.validators() {
420        if let Some(idx) = votes.has_voted::<V>(validator)? {
421            // Check if this validator has already voted.
422            //
423            // O(n) complexity.
424            if seen_vals.contains(&idx) {
425                return Err(VerificationError::duplicate_validator(validator.address));
426            }
427            seen_vals.push(idx);
428
429            power.tally(validator.power());
430
431            // Break early if sufficient voting power is reached.
432            if power.check().is_ok() {
433                break;
434            }
435        }
436    }
437
438    Ok(power)
439}
440
441// The below unit tests replaces the static voting power test files
442// see https://github.com/informalsystems/tendermint-rs/pull/383
443// This is essentially to remove the heavy dependency on MBT
444// TODO: We plan to add Lightweight MBT for `voting_power_in` in the near future
445#[cfg(test)]
446mod tests {
447    use tendermint::trust_threshold::TrustThresholdFraction;
448    use tendermint_testgen::{
449        light_block::generate_signed_header, Commit, Generator, Header,
450        LightBlock as TestgenLightBlock, ValidatorSet, Vote as TestgenVote,
451    };
452
453    use super::*;
454    use crate::{errors::VerificationErrorDetail, types::LightBlock};
455
456    const EXPECTED_RESULT: VotingPowerTally = VotingPowerTally {
457        total: 100,
458        tallied: 0,
459        trust_threshold: TrustThresholdFraction::ONE_THIRD,
460    };
461
462    #[test]
463    fn test_empty_signatures() {
464        let vp_calculator = ProdVotingPowerCalculator::default();
465        let trust_threshold = TrustThreshold::default();
466
467        let mut light_block: LightBlock = TestgenLightBlock::new_default(10)
468            .generate()
469            .unwrap()
470            .into();
471        light_block.signed_header.commit.signatures = vec![];
472
473        let result_ok = vp_calculator.voting_power_in(
474            &light_block.signed_header,
475            &light_block.validators,
476            trust_threshold,
477        );
478
479        // ensure the result matches the expected result
480        assert_eq!(result_ok.unwrap(), EXPECTED_RESULT);
481    }
482
483    #[test]
484    fn test_all_signatures_absent() {
485        let vp_calculator = ProdVotingPowerCalculator::default();
486        let trust_threshold = TrustThreshold::default();
487
488        let mut testgen_lb = TestgenLightBlock::new_default(10);
489        let mut commit = testgen_lb.commit.clone().unwrap();
490        // an empty vector of votes translates into all absent signatures
491        commit.votes = Some(vec![]);
492        testgen_lb.commit = Some(commit);
493        let light_block: LightBlock = testgen_lb.generate().unwrap().into();
494
495        let result_ok = vp_calculator.voting_power_in(
496            &light_block.signed_header,
497            &light_block.validators,
498            trust_threshold,
499        );
500
501        // ensure the result matches the expected result
502        assert_eq!(result_ok.unwrap(), EXPECTED_RESULT);
503    }
504
505    #[test]
506    fn test_all_signatures_nil() {
507        let vp_calculator = ProdVotingPowerCalculator::default();
508        let trust_threshold = TrustThreshold::default();
509
510        let validator_set = ValidatorSet::new(vec!["a", "b"]);
511        let vals = validator_set.clone().validators.unwrap();
512        let header = Header::new(&vals);
513        let votes = vec![
514            TestgenVote::new(vals[0].clone(), header.clone()).nil(true),
515            TestgenVote::new(vals[1].clone(), header.clone()).nil(true),
516        ];
517        let commit = Commit::new_with_votes(header.clone(), 1, votes);
518        let signed_header = generate_signed_header(&header, &commit).unwrap();
519        let valset = validator_set.generate().unwrap();
520
521        let result_ok = vp_calculator.voting_power_in(&signed_header, &valset, trust_threshold);
522
523        // ensure the result matches the expected result
524        assert_eq!(result_ok.unwrap(), EXPECTED_RESULT);
525    }
526
527    #[test]
528    fn test_one_invalid_signature() {
529        let vp_calculator = ProdVotingPowerCalculator::default();
530        let trust_threshold = TrustThreshold::default();
531
532        let mut testgen_lb = TestgenLightBlock::new_default(10);
533        let mut commit = testgen_lb.commit.clone().unwrap();
534        let mut votes = commit.votes.unwrap();
535        let vote = votes.pop().unwrap();
536        let header = vote.clone().header.unwrap().chain_id("bad-chain");
537        votes.push(vote.header(header));
538
539        commit.votes = Some(votes);
540        testgen_lb.commit = Some(commit);
541        let light_block: LightBlock = testgen_lb.generate().unwrap().into();
542
543        let result_err = vp_calculator.voting_power_in(
544            &light_block.signed_header,
545            &light_block.validators,
546            trust_threshold,
547        );
548
549        match result_err {
550            Err(VerificationError(VerificationErrorDetail::InvalidSignature(_), _)) => {},
551            _ => panic!("expected InvalidSignature error"),
552        }
553    }
554
555    #[test]
556    fn test_all_signatures_invalid() {
557        let vp_calculator = ProdVotingPowerCalculator::default();
558        let trust_threshold = TrustThreshold::default();
559
560        let mut testgen_lb = TestgenLightBlock::new_default(10);
561        let header = testgen_lb.header.unwrap().chain_id("bad-chain");
562        testgen_lb.header = Some(header);
563        let light_block: LightBlock = testgen_lb.generate().unwrap().into();
564
565        let result_err = vp_calculator.voting_power_in(
566            &light_block.signed_header,
567            &light_block.validators,
568            trust_threshold,
569        );
570
571        match result_err {
572            Err(VerificationError(VerificationErrorDetail::InvalidSignature(_), _)) => {},
573            _ => panic!("expected InvalidSignature error"),
574        }
575    }
576
577    #[test]
578    fn test_signatures_from_diff_valset() {
579        let vp_calculator = ProdVotingPowerCalculator::default();
580        let trust_threshold = TrustThreshold::default();
581
582        let mut light_block: LightBlock = TestgenLightBlock::new_default(10)
583            .generate()
584            .unwrap()
585            .into();
586        light_block.validators = ValidatorSet::new(vec!["bad-val1", "bad-val2"])
587            .generate()
588            .unwrap();
589
590        let result_ok = vp_calculator.voting_power_in(
591            &light_block.signed_header,
592            &light_block.validators,
593            trust_threshold,
594        );
595
596        // ensure the result matches the expected result
597        assert_eq!(result_ok.unwrap(), EXPECTED_RESULT);
598    }
599}