tendermint_light_client_verifier/
predicates.rs

1//! Predicates for light block validation and verification.
2
3use core::time::Duration;
4
5use tendermint::{
6    block::Height, chain::Id as ChainId, crypto::Sha256, hash::Hash, merkle::MerkleHash,
7};
8
9use crate::{
10    errors::VerificationError,
11    operations::{CommitValidator, VotingPowerCalculator},
12    prelude::*,
13    types::{Header, SignedHeader, Time, TrustThreshold, ValidatorSet},
14};
15
16/// Production predicates, using the default implementation
17/// of the `VerificationPredicates` trait.
18#[cfg(feature = "rust-crypto")]
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
20pub struct ProdPredicates;
21
22#[cfg(feature = "rust-crypto")]
23impl VerificationPredicates for ProdPredicates {
24    type Sha256 = tendermint::crypto::default::Sha256;
25}
26
27/// Defines the various predicates used to validate and verify light blocks.
28///
29/// A default, spec abiding implementation is provided for each method.
30///
31/// This enables test implementations to only override a single method rather than
32/// have to re-define every predicate.
33pub trait VerificationPredicates: Send + Sync {
34    /// The implementation of SHA256 digest
35    type Sha256: MerkleHash + Sha256 + Default;
36
37    /// Compare the provided validator_set_hash against the hash produced from hashing the validator
38    /// set.
39    fn validator_sets_match(
40        &self,
41        validators: &ValidatorSet,
42        header_validators_hash: Hash,
43    ) -> Result<(), VerificationError> {
44        let validators_hash = validators.hash_with::<Self::Sha256>();
45        if header_validators_hash == validators_hash {
46            Ok(())
47        } else {
48            Err(VerificationError::invalid_validator_set(
49                header_validators_hash,
50                validators_hash,
51            ))
52        }
53    }
54
55    /// Check that the hash of the next validator set in the header match the actual one.
56    fn next_validators_match(
57        &self,
58        next_validators: &ValidatorSet,
59        header_next_validators_hash: Hash,
60    ) -> Result<(), VerificationError> {
61        let next_validators_hash = next_validators.hash_with::<Self::Sha256>();
62        if header_next_validators_hash == next_validators_hash {
63            Ok(())
64        } else {
65            Err(VerificationError::invalid_next_validator_set(
66                header_next_validators_hash,
67                next_validators_hash,
68            ))
69        }
70    }
71
72    /// Check that the hash of the header in the commit matches the actual one.
73    fn header_matches_commit(
74        &self,
75        header: &Header,
76        commit_hash: Hash,
77    ) -> Result<(), VerificationError> {
78        let header_hash = header.hash_with::<Self::Sha256>();
79        if header_hash == commit_hash {
80            Ok(())
81        } else {
82            Err(VerificationError::invalid_commit_value(
83                header_hash,
84                commit_hash,
85            ))
86        }
87    }
88
89    /// Validate the commit using the given commit validator.
90    fn valid_commit(
91        &self,
92        signed_header: &SignedHeader,
93        validators: &ValidatorSet,
94        commit_validator: &dyn CommitValidator,
95    ) -> Result<(), VerificationError> {
96        commit_validator.validate(signed_header, validators)?;
97        commit_validator.validate_full(signed_header, validators)?;
98
99        Ok(())
100    }
101
102    /// Check that the trusted header is within the trusting period, adjusting for clock drift.
103    fn is_within_trust_period(
104        &self,
105        trusted_header_time: Time,
106        trusting_period: Duration,
107        now: Time,
108    ) -> Result<(), VerificationError> {
109        let expires_at =
110            (trusted_header_time + trusting_period).map_err(VerificationError::tendermint)?;
111
112        if expires_at > now {
113            Ok(())
114        } else {
115            Err(VerificationError::not_within_trust_period(expires_at, now))
116        }
117    }
118
119    /// Check that the untrusted header is from past.
120    fn is_header_from_past(
121        &self,
122        untrusted_header_time: Time,
123        clock_drift: Duration,
124        now: Time,
125    ) -> Result<(), VerificationError> {
126        let drifted = (now + clock_drift).map_err(VerificationError::tendermint)?;
127
128        if untrusted_header_time < drifted {
129            Ok(())
130        } else {
131            Err(VerificationError::header_from_the_future(
132                untrusted_header_time,
133                now,
134                clock_drift,
135            ))
136        }
137    }
138
139    /// Check that time passed monotonically between the trusted header and the untrusted one.
140    fn is_monotonic_bft_time(
141        &self,
142        untrusted_header_time: Time,
143        trusted_header_time: Time,
144    ) -> Result<(), VerificationError> {
145        if untrusted_header_time > trusted_header_time {
146            Ok(())
147        } else {
148            Err(VerificationError::non_monotonic_bft_time(
149                untrusted_header_time,
150                trusted_header_time,
151            ))
152        }
153    }
154
155    /// Check that the height increased between the trusted header and the untrusted one.
156    fn is_monotonic_height(
157        &self,
158        untrusted_height: Height,
159        trusted_height: Height,
160    ) -> Result<(), VerificationError> {
161        if untrusted_height > trusted_height {
162            Ok(())
163        } else {
164            Err(VerificationError::non_increasing_height(
165                untrusted_height,
166                trusted_height.increment(),
167            ))
168        }
169    }
170
171    /// Check that the chain-ids of the trusted header and the untrusted one are the same
172    fn is_matching_chain_id(
173        &self,
174        untrusted_chain_id: &ChainId,
175        trusted_chain_id: &ChainId,
176    ) -> Result<(), VerificationError> {
177        if untrusted_chain_id == trusted_chain_id {
178            Ok(())
179        } else {
180            Err(VerificationError::chain_id_mismatch(
181                untrusted_chain_id.to_string(),
182                trusted_chain_id.to_string(),
183            ))
184        }
185    }
186
187    /// Checks that there is enough overlap between validators and the untrusted
188    /// signed header.
189    ///
190    /// First of all, checks that enough validators from the
191    /// `trusted_validators` set signed the untrusted header to reach given
192    /// `trust_threshold`.
193    ///
194    /// Second of all, checks that enough validators from the
195    /// `untrusted_validators` set signed the untrusted header to reach a trust
196    /// threshold of ⅔.
197    ///
198    /// If both of those conditions aren’t met, it’s unspecified which error is
199    /// returned.
200    ///
201    /// Note also that the method isn’t guaranteed to verify all the signatures
202    /// present in the signed header.  If there are invalid signatures, the
203    /// method may or may not return an error depending on which validators
204    /// those signatures correspond to.
205    fn has_sufficient_validators_and_signers_overlap(
206        &self,
207        untrusted_sh: &SignedHeader,
208        trusted_validators: &ValidatorSet,
209        trust_threshold: &TrustThreshold,
210        untrusted_validators: &ValidatorSet,
211        calculator: &dyn VotingPowerCalculator,
212    ) -> Result<(), VerificationError> {
213        calculator.check_enough_trust_and_signers(
214            untrusted_sh,
215            trusted_validators,
216            *trust_threshold,
217            untrusted_validators,
218        )?;
219        Ok(())
220    }
221
222    /// Check that there is enough signers overlap between the given, untrusted
223    /// validator set and the untrusted signed header.
224    fn has_sufficient_signers_overlap(
225        &self,
226        untrusted_sh: &SignedHeader,
227        untrusted_validators: &ValidatorSet,
228        calculator: &dyn VotingPowerCalculator,
229    ) -> Result<(), VerificationError> {
230        calculator.check_signers_overlap(untrusted_sh, untrusted_validators)?;
231        Ok(())
232    }
233
234    /// Check that the hash of the next validator set in the trusted block matches
235    /// the hash of the validator set in the untrusted one.
236    fn valid_next_validator_set(
237        &self,
238        untrusted_validators_hash: Hash,
239        trusted_next_validators_hash: Hash,
240    ) -> Result<(), VerificationError> {
241        if trusted_next_validators_hash == untrusted_validators_hash {
242            Ok(())
243        } else {
244            Err(VerificationError::invalid_next_validator_set(
245                untrusted_validators_hash,
246                trusted_next_validators_hash,
247            ))
248        }
249    }
250}
251
252#[cfg(all(test, feature = "rust-crypto"))]
253mod tests {
254    use core::time::Duration;
255
256    use tendermint::{block::CommitSig, validator::Set};
257    use tendermint_testgen::{
258        light_block::{LightBlock as TestgenLightBlock, TmLightBlock},
259        Commit, Generator, Header, Validator, ValidatorSet,
260    };
261    use time::OffsetDateTime;
262
263    use crate::{
264        errors::{VerificationError, VerificationErrorDetail},
265        operations::{ProdCommitValidator, ProdVotingPowerCalculator, VotingPowerTally},
266        predicates::{ProdPredicates, VerificationPredicates},
267        prelude::*,
268        types::{LightBlock, TrustThreshold},
269    };
270
271    impl From<TmLightBlock> for LightBlock {
272        fn from(lb: TmLightBlock) -> Self {
273            LightBlock {
274                signed_header: lb.signed_header,
275                validators: lb.validators,
276                next_validators: lb.next_validators,
277                provider: lb.provider,
278            }
279        }
280    }
281
282    #[test]
283    fn test_is_monotonic_bft_time() {
284        let val = vec![Validator::new("val-1")];
285        let header_one = Header::new(&val).generate().unwrap();
286        let header_two = Header::new(&val).generate().unwrap();
287
288        let vp = ProdPredicates;
289
290        // 1. ensure valid header verifies
291        let result_ok = vp.is_monotonic_bft_time(header_two.time, header_one.time);
292        assert!(result_ok.is_ok());
293
294        // 2. ensure header with non-monotonic bft time fails
295        let result_err = vp.is_monotonic_bft_time(header_one.time, header_two.time);
296        match result_err {
297            Err(VerificationError(VerificationErrorDetail::NonMonotonicBftTime(e), _)) => {
298                assert_eq!(e.header_bft_time, header_one.time);
299                assert_eq!(e.trusted_header_bft_time, header_two.time);
300            },
301            _ => panic!("expected NonMonotonicBftTime error"),
302        }
303    }
304
305    #[test]
306    fn test_is_monotonic_height() {
307        let val = vec![Validator::new("val-1")];
308        let header_one = Header::new(&val).generate().unwrap();
309        let header_two = Header::new(&val).height(2).generate().unwrap();
310
311        let vp = ProdPredicates;
312
313        // 1. ensure valid header verifies
314        let result_ok = vp.is_monotonic_height(header_two.height, header_one.height);
315        assert!(result_ok.is_ok());
316
317        // 2. ensure header with non-monotonic height fails
318        let result_err = vp.is_monotonic_height(header_one.height, header_two.height);
319
320        match result_err {
321            Err(VerificationError(VerificationErrorDetail::NonIncreasingHeight(e), _)) => {
322                assert_eq!(e.got, header_one.height);
323                assert_eq!(e.expected, header_two.height.increment());
324            },
325            _ => panic!("expected NonIncreasingHeight error"),
326        }
327    }
328
329    #[test]
330    fn test_is_matching_chain_id() {
331        let val = vec![Validator::new("val-1")];
332        let header_one = Header::new(&val).chain_id("chaina-1").generate().unwrap();
333        let header_two = Header::new(&val).chain_id("chainb-1").generate().unwrap();
334
335        let vp = ProdPredicates;
336
337        // 1. ensure valid header verifies
338        let result_ok = vp.is_matching_chain_id(&header_one.chain_id, &header_one.chain_id);
339        assert!(result_ok.is_ok());
340
341        // 2. ensure header with different chain-id fails
342        let result_err = vp.is_matching_chain_id(&header_one.chain_id, &header_two.chain_id);
343
344        match result_err {
345            Err(VerificationError(VerificationErrorDetail::ChainIdMismatch(e), _)) => {
346                assert_eq!(e.got, header_one.chain_id.to_string());
347                assert_eq!(e.expected, header_two.chain_id.to_string());
348            },
349            _ => panic!("expected ChainIdMismatch error"),
350        }
351    }
352
353    #[test]
354    fn test_is_within_trust_period() {
355        let val = Validator::new("val-1");
356        let header = Header::new(&[val]).generate().unwrap();
357
358        let vp = ProdPredicates;
359
360        // 1. ensure valid header verifies
361        let mut trusting_period = Duration::new(1000, 0);
362        let now = OffsetDateTime::now_utc().try_into().unwrap();
363
364        let result_ok = vp.is_within_trust_period(header.time, trusting_period, now);
365        assert!(result_ok.is_ok());
366
367        // 2. ensure header outside trusting period fails
368        trusting_period = Duration::new(0, 1);
369
370        let result_err = vp.is_within_trust_period(header.time, trusting_period, now);
371
372        let expires_at = (header.time + trusting_period).unwrap();
373        match result_err {
374            Err(VerificationError(VerificationErrorDetail::NotWithinTrustPeriod(e), _)) => {
375                assert_eq!(e.expires_at, expires_at);
376                assert_eq!(e.now, now);
377            },
378            _ => panic!("expected NotWithinTrustPeriod error"),
379        }
380    }
381
382    #[test]
383    fn test_is_header_from_past() {
384        let val = Validator::new("val-1");
385        let header = Header::new(&[val]).generate().unwrap();
386
387        let vp = ProdPredicates;
388        let one_second = Duration::new(1, 0);
389
390        let now = OffsetDateTime::now_utc().try_into().unwrap();
391
392        // 1. ensure valid header verifies
393        let result_ok = vp.is_header_from_past(header.time, one_second, now);
394
395        assert!(result_ok.is_ok());
396
397        // 2. ensure it fails if header is from a future time
398        let now = (now - one_second * 15).unwrap();
399        let result_err = vp.is_header_from_past(header.time, one_second, now);
400
401        match result_err {
402            Err(VerificationError(VerificationErrorDetail::HeaderFromTheFuture(e), _)) => {
403                assert_eq!(e.header_time, header.time);
404                assert_eq!(e.now, now);
405            },
406            _ => panic!("expected HeaderFromTheFuture error"),
407        }
408    }
409
410    #[test]
411    // NOTE: tests both current valset and next valset
412    fn test_validator_sets_match() {
413        let mut light_block: LightBlock =
414            TestgenLightBlock::new_default(1).generate().unwrap().into();
415
416        let bad_validator_set = ValidatorSet::new(vec!["bad-val"]).generate().unwrap();
417
418        let vp = ProdPredicates;
419
420        // Test positive case
421        // 1. For predicate: validator_sets_match
422        let val_sets_match_ok = vp.validator_sets_match(
423            &light_block.validators,
424            light_block.signed_header.header.validators_hash,
425        );
426
427        assert!(val_sets_match_ok.is_ok());
428
429        // 2. For predicate: next_validator_sets_match
430        let next_val_sets_match_ok = vp.next_validators_match(
431            &light_block.next_validators,
432            light_block.signed_header.header.next_validators_hash,
433        );
434
435        assert!(next_val_sets_match_ok.is_ok());
436
437        // Test negative case
438        // 1. For predicate: validator_sets_match
439        light_block.validators = bad_validator_set.clone();
440
441        let val_sets_match_err = vp.validator_sets_match(
442            &light_block.validators,
443            light_block.signed_header.header.validators_hash,
444        );
445
446        match val_sets_match_err {
447            Err(VerificationError(VerificationErrorDetail::InvalidValidatorSet(e), _)) => {
448                assert_eq!(
449                    e.header_validators_hash,
450                    light_block.signed_header.header.validators_hash
451                );
452                assert_eq!(e.validators_hash, light_block.validators.hash());
453            },
454            _ => panic!("expected InvalidValidatorSet error"),
455        }
456
457        // 2. For predicate: next_validator_sets_match
458        light_block.next_validators = bad_validator_set;
459        let next_val_sets_match_err = vp.next_validators_match(
460            &light_block.next_validators,
461            light_block.signed_header.header.next_validators_hash,
462        );
463
464        match next_val_sets_match_err {
465            Err(VerificationError(VerificationErrorDetail::InvalidNextValidatorSet(e), _)) => {
466                assert_eq!(
467                    e.header_next_validators_hash,
468                    light_block.signed_header.header.next_validators_hash
469                );
470                assert_eq!(e.next_validators_hash, light_block.next_validators.hash());
471            },
472            _ => panic!("expected InvalidNextValidatorSet error"),
473        }
474    }
475
476    #[test]
477    fn test_header_matches_commit() {
478        let mut signed_header = TestgenLightBlock::new_default(1)
479            .generate()
480            .unwrap()
481            .signed_header;
482
483        let vp = ProdPredicates;
484
485        // 1. ensure valid signed header verifies
486        let result_ok =
487            vp.header_matches_commit(&signed_header.header, signed_header.commit.block_id.hash);
488
489        assert!(result_ok.is_ok());
490
491        // 2. ensure invalid signed header fails
492        signed_header.commit.block_id.hash =
493            "15F15EF50BDE2018F4B129A827F90C18222C757770C8295EB8EE7BF50E761BC0"
494                .parse()
495                .unwrap();
496        let result_err =
497            vp.header_matches_commit(&signed_header.header, signed_header.commit.block_id.hash);
498
499        // 3. ensure it fails with: VerificationVerificationError::InvalidCommitValue
500        let header_hash = signed_header.header.hash();
501
502        match result_err {
503            Err(VerificationError(VerificationErrorDetail::InvalidCommitValue(e), _)) => {
504                assert_eq!(e.header_hash, header_hash);
505                assert_eq!(e.commit_hash, signed_header.commit.block_id.hash);
506            },
507            _ => panic!("expected InvalidCommitValue error"),
508        }
509    }
510
511    #[test]
512    fn test_valid_commit() {
513        let light_block: LightBlock = TestgenLightBlock::new_default(1).generate().unwrap().into();
514
515        let mut signed_header = light_block.signed_header;
516        let val_set = light_block.validators;
517
518        let vp = ProdPredicates;
519        let commit_validator = ProdCommitValidator;
520
521        // Test scenarios -->
522        // 1. valid commit - must result "Ok"
523        let mut result_ok = vp.valid_commit(&signed_header, &val_set, &commit_validator);
524
525        assert!(result_ok.is_ok());
526
527        // 2. no commit signatures - must return error
528        let signatures = signed_header.commit.signatures.clone();
529        signed_header.commit.signatures = vec![];
530
531        let mut result_err = vp.valid_commit(&signed_header, &val_set, &commit_validator);
532
533        match result_err {
534            Err(VerificationError(VerificationErrorDetail::NoSignatureForCommit(_), _)) => {},
535            _ => panic!("expected ImplementationSpecific error"),
536        }
537
538        // 3. commit.signatures.len() != validator_set.validators().len()
539        // must return error
540        let mut bad_sigs = vec![signatures.clone().swap_remove(1)];
541        signed_header.commit.signatures.clone_from(&bad_sigs);
542
543        result_err = vp.valid_commit(&signed_header, &val_set, &commit_validator);
544
545        match result_err {
546            Err(VerificationError(VerificationErrorDetail::MismatchPreCommitLength(e), _)) => {
547                assert_eq!(e.pre_commit_length, signed_header.commit.signatures.len());
548                assert_eq!(e.validator_length, val_set.validators().len());
549            },
550            _ => panic!("expected MismatchPreCommitLength error"),
551        }
552
553        // 4. commit.BlockIdFlagAbsent - should be "Ok"
554        bad_sigs.push(CommitSig::BlockIdFlagAbsent);
555        signed_header.commit.signatures = bad_sigs;
556        result_ok = vp.valid_commit(&signed_header, &val_set, &commit_validator);
557        assert!(result_ok.is_ok());
558
559        // 5. faulty signer - must return error
560        let mut bad_vals = val_set.validators().clone();
561        bad_vals.pop();
562        bad_vals.push(
563            Validator::new("bad-val")
564                .generate()
565                .expect("Failed to generate validator"),
566        );
567        let val_set_with_faulty_signer = Set::without_proposer(bad_vals);
568
569        // reset signatures
570        signed_header.commit.signatures = signatures;
571
572        result_err = vp.valid_commit(
573            &signed_header,
574            &val_set_with_faulty_signer,
575            &commit_validator,
576        );
577
578        match result_err {
579            Err(VerificationError(VerificationErrorDetail::FaultySigner(e), _)) => {
580                assert_eq!(
581                    e.signer,
582                    signed_header
583                        .commit
584                        .signatures
585                        .iter()
586                        .last()
587                        .unwrap()
588                        .validator_address()
589                        .unwrap()
590                );
591
592                assert_eq!(e.validator_set, val_set_with_faulty_signer);
593            },
594            _ => panic!("expected FaultySigner error"),
595        }
596    }
597
598    #[test]
599    fn test_valid_next_validator_set() {
600        let test_lb1 = TestgenLightBlock::new_default(1);
601        let light_block1: LightBlock = test_lb1.generate().unwrap().into();
602
603        let light_block2: LightBlock = test_lb1.next().generate().unwrap().into();
604
605        let vp = ProdPredicates;
606
607        // Test scenarios -->
608        // 1. next_validator_set hash matches
609        let result_ok = vp.valid_next_validator_set(
610            light_block1.signed_header.header.validators_hash,
611            light_block2.signed_header.header.next_validators_hash,
612        );
613
614        assert!(result_ok.is_ok());
615
616        // 2. next_validator_set hash doesn't match
617        let vals = &[Validator::new("new-1"), Validator::new("new-2")];
618        let header = Header::new(vals);
619        let commit = Commit::new(header.clone(), 1);
620
621        let light_block3: LightBlock = TestgenLightBlock::new(header, commit)
622            .generate()
623            .unwrap()
624            .into();
625
626        let result_err = vp.valid_next_validator_set(
627            light_block3.signed_header.header.validators_hash,
628            light_block2.signed_header.header.next_validators_hash,
629        );
630
631        match result_err {
632            Err(VerificationError(VerificationErrorDetail::InvalidNextValidatorSet(e), _)) => {
633                assert_eq!(
634                    e.header_next_validators_hash,
635                    light_block3.signed_header.header.validators_hash
636                );
637                assert_eq!(
638                    e.next_validators_hash,
639                    light_block2.signed_header.header.next_validators_hash
640                );
641            },
642            _ => panic!("expected InvalidNextValidatorSet error"),
643        }
644    }
645
646    #[test]
647    fn test_has_sufficient_validators_and_signers_overlap() {
648        let light_block: LightBlock = TestgenLightBlock::new_default(1).generate().unwrap().into();
649        let val_set = light_block.validators;
650        let signed_header = light_block.signed_header;
651
652        let vp = ProdPredicates;
653        let voting_power_calculator = ProdVotingPowerCalculator::default();
654
655        // Test scenarios -->
656        // 1. Validators and signers overlap ≥ trust_threshold.
657        vp.has_sufficient_validators_and_signers_overlap(
658            &signed_header,
659            &val_set,
660            &TrustThreshold::TWO_THIRDS,
661            &val_set,
662            &voting_power_calculator,
663        )
664        .unwrap();
665
666        // 2. Validators overlap < threshold; signers overlap ≥ threshold.
667        let mut vals = val_set.validators().clone();
668        vals.push(
669            Validator::new("extra-val")
670                .voting_power(100)
671                .generate()
672                .unwrap(),
673        );
674        let bad_valset = Set::without_proposer(vals);
675
676        let result = vp.has_sufficient_validators_and_signers_overlap(
677            &signed_header,
678            &bad_valset,
679            &TrustThreshold::TWO_THIRDS,
680            &val_set,
681            &voting_power_calculator,
682        );
683
684        let expected_tally = VotingPowerTally {
685            total: 200,
686            tallied: 100,
687            trust_threshold: TrustThreshold::TWO_THIRDS,
688        };
689        match result {
690            Err(VerificationError(VerificationErrorDetail::NotEnoughTrust(e), _)) => {
691                assert_eq!(expected_tally, e.tally)
692            },
693            _ => panic!("expected NotEnoughTrust error, got: {result:?}"),
694        }
695
696        // 3. Validators overlap ≥ threshold; signers overlap < threshold.
697        let result = vp.has_sufficient_validators_and_signers_overlap(
698            &signed_header,
699            &val_set,
700            &TrustThreshold::TWO_THIRDS,
701            &bad_valset,
702            &voting_power_calculator,
703        );
704        match result {
705            Err(VerificationError(VerificationErrorDetail::InsufficientSignersOverlap(e), _)) => {
706                assert_eq!(expected_tally, e.tally)
707            },
708            _ => panic!("expected InsufficientSignersOverlap error, got: {result:?}"),
709        }
710    }
711
712    #[test]
713    fn test_has_sufficient_signers_overlap() {
714        let mut light_block: LightBlock =
715            TestgenLightBlock::new_default(2).generate().unwrap().into();
716
717        let vp = ProdPredicates;
718        let voting_power_calculator = ProdVotingPowerCalculator::default();
719
720        // Test scenarios -->
721        // 1. +2/3 validators sign
722        let result_ok = vp.has_sufficient_signers_overlap(
723            &light_block.signed_header,
724            &light_block.validators,
725            &voting_power_calculator,
726        );
727
728        assert!(result_ok.is_ok());
729
730        // 1. less than 2/3 validators sign
731        light_block.signed_header.commit.signatures.pop();
732
733        let result_err = vp.has_sufficient_signers_overlap(
734            &light_block.signed_header,
735            &light_block.validators,
736            &voting_power_calculator,
737        );
738
739        let trust_threshold = TrustThreshold::TWO_THIRDS;
740
741        match result_err {
742            Err(VerificationError(VerificationErrorDetail::InsufficientSignersOverlap(e), _)) => {
743                assert_eq!(
744                    e.tally,
745                    VotingPowerTally {
746                        total: 100,
747                        tallied: 50,
748                        trust_threshold,
749                    }
750                );
751            },
752            _ => panic!("expected InsufficientSignersOverlap error"),
753        }
754    }
755}