cometbft_light_client_verifier/
predicates.rs

1//! Predicates for light block validation and verification.
2
3use core::time::Duration;
4
5use cometbft::{
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 = cometbft::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::cometbft)?;
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::cometbft)?;
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    /// Check that there is enough validators overlap between the trusted validator set
188    /// and the untrusted signed header.
189    fn has_sufficient_validators_overlap(
190        &self,
191        untrusted_sh: &SignedHeader,
192        trusted_validators: &ValidatorSet,
193        trust_threshold: &TrustThreshold,
194        calculator: &dyn VotingPowerCalculator,
195    ) -> Result<(), VerificationError> {
196        calculator.check_enough_trust(untrusted_sh, trusted_validators, *trust_threshold)?;
197        Ok(())
198    }
199
200    /// Check that there is enough signers overlap between the given, untrusted validator set
201    /// and the untrusted signed header.
202    fn has_sufficient_signers_overlap(
203        &self,
204        untrusted_sh: &SignedHeader,
205        untrusted_validators: &ValidatorSet,
206        calculator: &dyn VotingPowerCalculator,
207    ) -> Result<(), VerificationError> {
208        calculator.check_signers_overlap(untrusted_sh, untrusted_validators)?;
209        Ok(())
210    }
211
212    /// Check that the hash of the next validator set in the trusted block matches
213    /// the hash of the validator set in the untrusted one.
214    fn valid_next_validator_set(
215        &self,
216        untrusted_validators_hash: Hash,
217        trusted_next_validators_hash: Hash,
218    ) -> Result<(), VerificationError> {
219        if trusted_next_validators_hash == untrusted_validators_hash {
220            Ok(())
221        } else {
222            Err(VerificationError::invalid_next_validator_set(
223                untrusted_validators_hash,
224                trusted_next_validators_hash,
225            ))
226        }
227    }
228}
229
230#[cfg(all(test, feature = "rust-crypto"))]
231mod tests {
232    use core::{convert::TryInto, time::Duration};
233
234    use cometbft::{block::CommitSig, validator::Set};
235    use cometbft_testgen::{
236        light_block::{LightBlock as TestgenLightBlock, TmLightBlock},
237        Commit, Generator, Header, Validator, ValidatorSet,
238    };
239    use time::OffsetDateTime;
240
241    use crate::{
242        errors::{VerificationError, VerificationErrorDetail},
243        operations::{ProdCommitValidator, ProdVotingPowerCalculator, VotingPowerTally},
244        predicates::{ProdPredicates, VerificationPredicates},
245        prelude::*,
246        types::{LightBlock, TrustThreshold},
247    };
248
249    impl From<TmLightBlock> for LightBlock {
250        fn from(lb: TmLightBlock) -> Self {
251            LightBlock {
252                signed_header: lb.signed_header,
253                validators: lb.validators,
254                next_validators: lb.next_validators,
255                provider: lb.provider,
256            }
257        }
258    }
259
260    #[test]
261    fn test_is_monotonic_bft_time() {
262        let val = vec![Validator::new("val-1")];
263        let header_one = Header::new(&val).generate().unwrap();
264        let header_two = Header::new(&val).generate().unwrap();
265
266        let vp = ProdPredicates;
267
268        // 1. ensure valid header verifies
269        let result_ok = vp.is_monotonic_bft_time(header_two.time, header_one.time);
270        assert!(result_ok.is_ok());
271
272        // 2. ensure header with non-monotonic bft time fails
273        let result_err = vp.is_monotonic_bft_time(header_one.time, header_two.time);
274        match result_err {
275            Err(VerificationError(VerificationErrorDetail::NonMonotonicBftTime(e), _)) => {
276                assert_eq!(e.header_bft_time, header_one.time);
277                assert_eq!(e.trusted_header_bft_time, header_two.time);
278            },
279            _ => panic!("expected NonMonotonicBftTime error"),
280        }
281    }
282
283    #[test]
284    fn test_is_monotonic_height() {
285        let val = vec![Validator::new("val-1")];
286        let header_one = Header::new(&val).generate().unwrap();
287        let header_two = Header::new(&val).height(2).generate().unwrap();
288
289        let vp = ProdPredicates;
290
291        // 1. ensure valid header verifies
292        let result_ok = vp.is_monotonic_height(header_two.height, header_one.height);
293        assert!(result_ok.is_ok());
294
295        // 2. ensure header with non-monotonic height fails
296        let result_err = vp.is_monotonic_height(header_one.height, header_two.height);
297
298        match result_err {
299            Err(VerificationError(VerificationErrorDetail::NonIncreasingHeight(e), _)) => {
300                assert_eq!(e.got, header_one.height);
301                assert_eq!(e.expected, header_two.height.increment());
302            },
303            _ => panic!("expected NonIncreasingHeight error"),
304        }
305    }
306
307    #[test]
308    fn test_is_matching_chain_id() {
309        let val = vec![Validator::new("val-1")];
310        let header_one = Header::new(&val).chain_id("chaina-1").generate().unwrap();
311        let header_two = Header::new(&val).chain_id("chainb-1").generate().unwrap();
312
313        let vp = ProdPredicates;
314
315        // 1. ensure valid header verifies
316        let result_ok = vp.is_matching_chain_id(&header_one.chain_id, &header_one.chain_id);
317        assert!(result_ok.is_ok());
318
319        // 2. ensure header with different chain-id fails
320        let result_err = vp.is_matching_chain_id(&header_one.chain_id, &header_two.chain_id);
321
322        match result_err {
323            Err(VerificationError(VerificationErrorDetail::ChainIdMismatch(e), _)) => {
324                assert_eq!(e.got, header_one.chain_id.to_string());
325                assert_eq!(e.expected, header_two.chain_id.to_string());
326            },
327            _ => panic!("expected ChainIdMismatch error"),
328        }
329    }
330
331    #[test]
332    fn test_is_within_trust_period() {
333        let val = Validator::new("val-1");
334        let header = Header::new(&[val]).generate().unwrap();
335
336        let vp = ProdPredicates;
337
338        // 1. ensure valid header verifies
339        let mut trusting_period = Duration::new(1000, 0);
340        let now = OffsetDateTime::now_utc().try_into().unwrap();
341
342        let result_ok = vp.is_within_trust_period(header.time, trusting_period, now);
343        assert!(result_ok.is_ok());
344
345        // 2. ensure header outside trusting period fails
346        trusting_period = Duration::new(0, 1);
347
348        let result_err = vp.is_within_trust_period(header.time, trusting_period, now);
349
350        let expires_at = (header.time + trusting_period).unwrap();
351        match result_err {
352            Err(VerificationError(VerificationErrorDetail::NotWithinTrustPeriod(e), _)) => {
353                assert_eq!(e.expires_at, expires_at);
354                assert_eq!(e.now, now);
355            },
356            _ => panic!("expected NotWithinTrustPeriod error"),
357        }
358    }
359
360    #[test]
361    fn test_is_header_from_past() {
362        let val = Validator::new("val-1");
363        let header = Header::new(&[val]).generate().unwrap();
364
365        let vp = ProdPredicates;
366        let one_second = Duration::new(1, 0);
367
368        let now = OffsetDateTime::now_utc().try_into().unwrap();
369
370        // 1. ensure valid header verifies
371        let result_ok = vp.is_header_from_past(header.time, one_second, now);
372
373        assert!(result_ok.is_ok());
374
375        // 2. ensure it fails if header is from a future time
376        let now = (now - one_second * 15).unwrap();
377        let result_err = vp.is_header_from_past(header.time, one_second, now);
378
379        match result_err {
380            Err(VerificationError(VerificationErrorDetail::HeaderFromTheFuture(e), _)) => {
381                assert_eq!(e.header_time, header.time);
382                assert_eq!(e.now, now);
383            },
384            _ => panic!("expected HeaderFromTheFuture error"),
385        }
386    }
387
388    #[test]
389    // NOTE: tests both current valset and next valset
390    fn test_validator_sets_match() {
391        let mut light_block: LightBlock =
392            TestgenLightBlock::new_default(1).generate().unwrap().into();
393
394        let bad_validator_set = ValidatorSet::new(vec!["bad-val"]).generate().unwrap();
395
396        let vp = ProdPredicates;
397
398        // Test positive case
399        // 1. For predicate: validator_sets_match
400        let val_sets_match_ok = vp.validator_sets_match(
401            &light_block.validators,
402            light_block.signed_header.header.validators_hash,
403        );
404
405        assert!(val_sets_match_ok.is_ok());
406
407        // 2. For predicate: next_validator_sets_match
408        let next_val_sets_match_ok = vp.next_validators_match(
409            &light_block.next_validators,
410            light_block.signed_header.header.next_validators_hash,
411        );
412
413        assert!(next_val_sets_match_ok.is_ok());
414
415        // Test negative case
416        // 1. For predicate: validator_sets_match
417        light_block.validators = bad_validator_set.clone();
418
419        let val_sets_match_err = vp.validator_sets_match(
420            &light_block.validators,
421            light_block.signed_header.header.validators_hash,
422        );
423
424        match val_sets_match_err {
425            Err(VerificationError(VerificationErrorDetail::InvalidValidatorSet(e), _)) => {
426                assert_eq!(
427                    e.header_validators_hash,
428                    light_block.signed_header.header.validators_hash
429                );
430                assert_eq!(e.validators_hash, light_block.validators.hash());
431            },
432            _ => panic!("expected InvalidValidatorSet error"),
433        }
434
435        // 2. For predicate: next_validator_sets_match
436        light_block.next_validators = bad_validator_set;
437        let next_val_sets_match_err = vp.next_validators_match(
438            &light_block.next_validators,
439            light_block.signed_header.header.next_validators_hash,
440        );
441
442        match next_val_sets_match_err {
443            Err(VerificationError(VerificationErrorDetail::InvalidNextValidatorSet(e), _)) => {
444                assert_eq!(
445                    e.header_next_validators_hash,
446                    light_block.signed_header.header.next_validators_hash
447                );
448                assert_eq!(e.next_validators_hash, light_block.next_validators.hash());
449            },
450            _ => panic!("expected InvalidNextValidatorSet error"),
451        }
452    }
453
454    #[test]
455    fn test_header_matches_commit() {
456        let mut signed_header = TestgenLightBlock::new_default(1)
457            .generate()
458            .unwrap()
459            .signed_header;
460
461        let vp = ProdPredicates;
462
463        // 1. ensure valid signed header verifies
464        let result_ok =
465            vp.header_matches_commit(&signed_header.header, signed_header.commit.block_id.hash);
466
467        assert!(result_ok.is_ok());
468
469        // 2. ensure invalid signed header fails
470        signed_header.commit.block_id.hash =
471            "15F15EF50BDE2018F4B129A827F90C18222C757770C8295EB8EE7BF50E761BC0"
472                .parse()
473                .unwrap();
474        let result_err =
475            vp.header_matches_commit(&signed_header.header, signed_header.commit.block_id.hash);
476
477        // 3. ensure it fails with: VerificationVerificationError::InvalidCommitValue
478        let header_hash = signed_header.header.hash();
479
480        match result_err {
481            Err(VerificationError(VerificationErrorDetail::InvalidCommitValue(e), _)) => {
482                assert_eq!(e.header_hash, header_hash);
483                assert_eq!(e.commit_hash, signed_header.commit.block_id.hash);
484            },
485            _ => panic!("expected InvalidCommitValue error"),
486        }
487    }
488
489    #[test]
490    fn test_valid_commit() {
491        let light_block: LightBlock = TestgenLightBlock::new_default(1).generate().unwrap().into();
492
493        let mut signed_header = light_block.signed_header;
494        let val_set = light_block.validators;
495
496        let vp = ProdPredicates;
497        let commit_validator = ProdCommitValidator;
498
499        // Test scenarios -->
500        // 1. valid commit - must result "Ok"
501        let mut result_ok = vp.valid_commit(&signed_header, &val_set, &commit_validator);
502
503        assert!(result_ok.is_ok());
504
505        // 2. no commit signatures - must return error
506        let signatures = signed_header.commit.signatures.clone();
507        signed_header.commit.signatures = vec![];
508
509        let mut result_err = vp.valid_commit(&signed_header, &val_set, &commit_validator);
510
511        match result_err {
512            Err(VerificationError(VerificationErrorDetail::NoSignatureForCommit(_), _)) => {},
513            _ => panic!("expected ImplementationSpecific error"),
514        }
515
516        // 3. commit.signatures.len() != validator_set.validators().len()
517        // must return error
518        let mut bad_sigs = vec![signatures.clone().swap_remove(1)];
519        signed_header.commit.signatures = bad_sigs.clone();
520
521        result_err = vp.valid_commit(&signed_header, &val_set, &commit_validator);
522
523        match result_err {
524            Err(VerificationError(VerificationErrorDetail::MismatchPreCommitLength(e), _)) => {
525                assert_eq!(e.pre_commit_length, signed_header.commit.signatures.len());
526                assert_eq!(e.validator_length, val_set.validators().len());
527            },
528            _ => panic!("expected MismatchPreCommitLength error"),
529        }
530
531        // 4. commit.BlockIdFlagAbsent - should be "Ok"
532        bad_sigs.push(CommitSig::BlockIdFlagAbsent);
533        signed_header.commit.signatures = bad_sigs;
534        result_ok = vp.valid_commit(&signed_header, &val_set, &commit_validator);
535        assert!(result_ok.is_ok());
536
537        // 5. faulty signer - must return error
538        let mut bad_vals = val_set.validators().clone();
539        bad_vals.pop();
540        bad_vals.push(
541            Validator::new("bad-val")
542                .generate()
543                .expect("Failed to generate validator"),
544        );
545        let val_set_with_faulty_signer = Set::without_proposer(bad_vals);
546
547        // reset signatures
548        signed_header.commit.signatures = signatures;
549
550        result_err = vp.valid_commit(
551            &signed_header,
552            &val_set_with_faulty_signer,
553            &commit_validator,
554        );
555
556        match result_err {
557            Err(VerificationError(VerificationErrorDetail::FaultySigner(e), _)) => {
558                assert_eq!(
559                    e.signer,
560                    signed_header
561                        .commit
562                        .signatures
563                        .iter()
564                        .last()
565                        .unwrap()
566                        .validator_address()
567                        .unwrap()
568                );
569
570                assert_eq!(e.validator_set, val_set_with_faulty_signer);
571            },
572            _ => panic!("expected FaultySigner error"),
573        }
574    }
575
576    #[test]
577    fn test_valid_next_validator_set() {
578        let test_lb1 = TestgenLightBlock::new_default(1);
579        let light_block1: LightBlock = test_lb1.generate().unwrap().into();
580
581        let light_block2: LightBlock = test_lb1.next().generate().unwrap().into();
582
583        let vp = ProdPredicates;
584
585        // Test scenarios -->
586        // 1. next_validator_set hash matches
587        let result_ok = vp.valid_next_validator_set(
588            light_block1.signed_header.header.validators_hash,
589            light_block2.signed_header.header.next_validators_hash,
590        );
591
592        assert!(result_ok.is_ok());
593
594        // 2. next_validator_set hash doesn't match
595        let vals = &[Validator::new("new-1"), Validator::new("new-2")];
596        let header = Header::new(vals);
597        let commit = Commit::new(header.clone(), 1);
598
599        let light_block3: LightBlock = TestgenLightBlock::new(header, commit)
600            .generate()
601            .unwrap()
602            .into();
603
604        let result_err = vp.valid_next_validator_set(
605            light_block3.signed_header.header.validators_hash,
606            light_block2.signed_header.header.next_validators_hash,
607        );
608
609        match result_err {
610            Err(VerificationError(VerificationErrorDetail::InvalidNextValidatorSet(e), _)) => {
611                assert_eq!(
612                    e.header_next_validators_hash,
613                    light_block3.signed_header.header.validators_hash
614                );
615                assert_eq!(
616                    e.next_validators_hash,
617                    light_block2.signed_header.header.next_validators_hash
618                );
619            },
620            _ => panic!("expected InvalidNextValidatorSet error"),
621        }
622    }
623
624    #[test]
625    fn test_has_sufficient_validators_overlap() {
626        let light_block: LightBlock = TestgenLightBlock::new_default(1).generate().unwrap().into();
627        let val_set = light_block.validators;
628        let signed_header = light_block.signed_header;
629
630        let vp = ProdPredicates;
631        let mut trust_threshold = TrustThreshold::new(1, 3).expect("Cannot make trust threshold");
632        let voting_power_calculator = ProdVotingPowerCalculator::default();
633
634        // Test scenarios -->
635        // 1. > trust_threshold validators overlap
636        let result_ok = vp.has_sufficient_validators_overlap(
637            &signed_header,
638            &val_set,
639            &trust_threshold,
640            &voting_power_calculator,
641        );
642
643        assert!(result_ok.is_ok());
644
645        // 2. < trust_threshold validators overlap
646        let mut vals = val_set.validators().clone();
647        vals.push(
648            Validator::new("extra-val")
649                .voting_power(100)
650                .generate()
651                .unwrap(),
652        );
653        let bad_valset = Set::without_proposer(vals);
654
655        trust_threshold = TrustThreshold::new(2, 3).expect("Cannot make trust threshold");
656
657        let result_err = vp.has_sufficient_validators_overlap(
658            &signed_header,
659            &bad_valset,
660            &trust_threshold,
661            &voting_power_calculator,
662        );
663
664        match result_err {
665            Err(VerificationError(VerificationErrorDetail::NotEnoughTrust(e), _)) => {
666                assert_eq!(
667                    e.tally,
668                    VotingPowerTally {
669                        total: 200,
670                        tallied: 100,
671                        trust_threshold,
672                    }
673                );
674            },
675            _ => panic!("expected NotEnoughTrust error"),
676        }
677    }
678
679    #[test]
680    fn test_has_sufficient_signers_overlap() {
681        let mut light_block: LightBlock =
682            TestgenLightBlock::new_default(2).generate().unwrap().into();
683
684        let vp = ProdPredicates;
685        let voting_power_calculator = ProdVotingPowerCalculator::default();
686
687        // Test scenarios -->
688        // 1. +2/3 validators sign
689        let result_ok = vp.has_sufficient_signers_overlap(
690            &light_block.signed_header,
691            &light_block.validators,
692            &voting_power_calculator,
693        );
694
695        assert!(result_ok.is_ok());
696
697        // 1. less than 2/3 validators sign
698        light_block.signed_header.commit.signatures.pop();
699
700        let result_err = vp.has_sufficient_signers_overlap(
701            &light_block.signed_header,
702            &light_block.validators,
703            &voting_power_calculator,
704        );
705
706        let trust_threshold = TrustThreshold::TWO_THIRDS;
707
708        match result_err {
709            Err(VerificationError(VerificationErrorDetail::InsufficientSignersOverlap(e), _)) => {
710                assert_eq!(
711                    e.tally,
712                    VotingPowerTally {
713                        total: 100,
714                        tallied: 50,
715                        trust_threshold,
716                    }
717                );
718            },
719            _ => panic!("expected InsufficientSignersOverlap error"),
720        }
721    }
722}