tendermint_light_client_verifier/
verifier.rs

1//! Provides an interface and default implementation of the `Verifier` component
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    errors::{ErrorExt, VerificationError, VerificationErrorDetail},
7    operations::{voting_power::VotingPowerTally, CommitValidator, VotingPowerCalculator},
8    options::Options,
9    predicates::VerificationPredicates,
10    types::{Time, TrustedBlockState, UntrustedBlockState},
11};
12
13#[cfg(feature = "rust-crypto")]
14use crate::{
15    operations::{ProdCommitValidator, ProdVotingPowerCalculator},
16    predicates::ProdPredicates,
17};
18
19/// Represents the result of the verification performed by the
20/// verifier component.
21#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
22pub enum Verdict {
23    /// Verification succeeded, the block is valid.
24    Success,
25    /// The minimum voting power threshold is not reached,
26    /// the block cannot be trusted yet.
27    NotEnoughTrust(VotingPowerTally),
28    /// Verification failed, the block is invalid.
29    Invalid(VerificationErrorDetail),
30}
31
32impl From<Result<(), VerificationError>> for Verdict {
33    fn from(result: Result<(), VerificationError>) -> Self {
34        match result {
35            Ok(()) => Self::Success,
36            Err(VerificationError(e, _)) => match e.not_enough_trust() {
37                Some(tally) => Self::NotEnoughTrust(tally),
38                _ => Self::Invalid(e),
39            },
40        }
41    }
42}
43
44/// The verifier checks:
45///
46/// a) whether a given untrusted light block is valid, and
47/// b) whether a given untrusted light block should be trusted
48///    based on a previously verified block.
49///
50/// ## Implements
51/// - [TMBC-VAL-CONTAINS-CORR.1]
52/// - [TMBC-VAL-COMMIT.1]
53pub trait Verifier: Send + Sync {
54    /// Verify a header received in a `MsgUpdateClient`.
55    fn verify_update_header(
56        &self,
57        untrusted: UntrustedBlockState<'_>,
58        trusted: TrustedBlockState<'_>,
59        options: &Options,
60        now: Time,
61    ) -> Verdict;
62
63    /// Verify a header received in `MsgSubmitMisbehaviour`.
64    /// The verification for these headers is a bit more relaxed in order to catch FLA attacks.
65    /// In particular the "header in the future" check for the header should be skipped
66    /// from `validate_against_trusted`.
67    fn verify_misbehaviour_header(
68        &self,
69        untrusted: UntrustedBlockState<'_>,
70        trusted: TrustedBlockState<'_>,
71        options: &Options,
72        now: Time,
73    ) -> Verdict;
74}
75
76macro_rules! verdict {
77    ($e:expr) => {{
78        let result = $e;
79        if result.is_err() {
80            return result.into();
81        }
82    }};
83}
84
85macro_rules! ensure_verdict_success {
86    ($e:expr) => {{
87        let verdict = $e;
88        if !matches!(verdict, Verdict::Success) {
89            return verdict;
90        }
91    }};
92}
93
94/// Predicate verifier encapsulating components necessary to facilitate
95/// verification.
96#[derive(Debug, Clone, Default, PartialEq, Eq)]
97pub struct PredicateVerifier<P, C, V> {
98    predicates: P,
99    voting_power_calculator: C,
100    commit_validator: V,
101}
102
103impl<P, C, V> PredicateVerifier<P, C, V>
104where
105    P: VerificationPredicates,
106    C: VotingPowerCalculator,
107    V: CommitValidator,
108{
109    /// Constructor.
110    pub fn new(predicates: P, voting_power_calculator: C, commit_validator: V) -> Self {
111        Self {
112            predicates,
113            voting_power_calculator,
114            commit_validator,
115        }
116    }
117
118    /// Validates an `UntrustedBlockState`.
119    pub fn verify_validator_sets(&self, untrusted: &UntrustedBlockState<'_>) -> Verdict {
120        // Ensure the header validator hashes match the given validators
121        verdict!(self.predicates.validator_sets_match(
122            untrusted.validators,
123            untrusted.signed_header.header.validators_hash,
124        ));
125
126        // Ensure the header next validator hashes match the given next validators
127        if let Some(untrusted_next_validators) = untrusted.next_validators {
128            verdict!(self.predicates.next_validators_match(
129                untrusted_next_validators,
130                untrusted.signed_header.header.next_validators_hash,
131            ));
132        }
133
134        // Ensure the header matches the commit
135        verdict!(self.predicates.header_matches_commit(
136            &untrusted.signed_header.header,
137            untrusted.signed_header.commit.block_id.hash,
138        ));
139
140        // Additional implementation specific validation
141        verdict!(self.predicates.valid_commit(
142            untrusted.signed_header,
143            untrusted.validators,
144            &self.commit_validator,
145        ));
146
147        Verdict::Success
148    }
149
150    /// Validate an `UntrustedBlockState` coming from a client update,
151    /// based on the given `TrustedBlockState`, `Options` and current time.
152    pub fn validate_against_trusted(
153        &self,
154        untrusted: &UntrustedBlockState<'_>,
155        trusted: &TrustedBlockState<'_>,
156        options: &Options,
157        now: Time,
158    ) -> Verdict {
159        // Ensure the latest trusted header hasn't expired
160        verdict!(self.predicates.is_within_trust_period(
161            trusted.header_time,
162            options.trusting_period,
163            now,
164        ));
165
166        // Check that the untrusted block is more recent than the trusted state
167        verdict!(self
168            .predicates
169            .is_monotonic_bft_time(untrusted.signed_header.header.time, trusted.header_time));
170
171        // Check that the chain-id of the untrusted block matches that of the trusted state
172        verdict!(self
173            .predicates
174            .is_matching_chain_id(&untrusted.signed_header.header.chain_id, trusted.chain_id));
175
176        let trusted_next_height = trusted.height.increment();
177
178        if untrusted.height() == trusted_next_height {
179            // If the untrusted block is the very next block after the trusted block,
180            // check that their (next) validator sets hashes match.
181            verdict!(self.predicates.valid_next_validator_set(
182                untrusted.signed_header.header.validators_hash,
183                trusted.next_validators_hash,
184            ));
185        } else {
186            // Otherwise, ensure that the untrusted block has a greater height than
187            // the trusted block.
188            verdict!(self
189                .predicates
190                .is_monotonic_height(untrusted.signed_header.header.height, trusted.height));
191        }
192
193        Verdict::Success
194    }
195
196    /// Ensure the header isn't from a future time
197    pub fn check_header_is_from_past(
198        &self,
199        untrusted: &UntrustedBlockState<'_>,
200        options: &Options,
201        now: Time,
202    ) -> Verdict {
203        verdict!(self.predicates.is_header_from_past(
204            untrusted.signed_header.header.time,
205            options.clock_drift,
206            now,
207        ));
208
209        Verdict::Success
210    }
211
212    /// Verify that more than 2/3 of the validators correctly committed the block.
213    ///
214    /// Use [`PredicateVerifier::verify_commit_against_trusted()`] to also verify that there is
215    /// enough overlap between validator sets.
216    pub fn verify_commit(&self, untrusted: &UntrustedBlockState<'_>) -> Verdict {
217        verdict!(self.predicates.has_sufficient_signers_overlap(
218            untrusted.signed_header,
219            untrusted.validators,
220            &self.voting_power_calculator,
221        ));
222
223        Verdict::Success
224    }
225
226    /// Verify that a) there is enough overlap between the validator sets of the
227    /// trusted and untrusted blocks and b) more than 2/3 of the validators
228    /// correctly committed the block.
229    pub fn verify_commit_against_trusted(
230        &self,
231        untrusted: &UntrustedBlockState<'_>,
232        trusted: &TrustedBlockState<'_>,
233        options: &Options,
234    ) -> Verdict {
235        // If the trusted validator set has changed we need to check if there’s
236        // overlap between the old trusted set and the new untrested header in
237        // addition to checking if the new set correctly signed the header.
238        let trusted_next_height = trusted.height.increment();
239        let need_both = untrusted.height() != trusted_next_height;
240
241        let result = if need_both {
242            self.predicates
243                .has_sufficient_validators_and_signers_overlap(
244                    untrusted.signed_header,
245                    trusted.next_validators,
246                    &options.trust_threshold,
247                    untrusted.validators,
248                    &self.voting_power_calculator,
249                )
250        } else {
251            self.predicates.has_sufficient_signers_overlap(
252                untrusted.signed_header,
253                untrusted.validators,
254                &self.voting_power_calculator,
255            )
256        };
257        verdict!(result);
258        Verdict::Success
259    }
260}
261
262impl<P, C, V> Verifier for PredicateVerifier<P, C, V>
263where
264    P: VerificationPredicates,
265    C: VotingPowerCalculator,
266    V: CommitValidator,
267{
268    /// Validate the given light block state by performing the following checks ->
269    ///
270    /// - Validate the untrusted header
271    ///     - Ensure the header validator hashes match the given validators
272    ///     - Ensure the header next validator hashes match the given next validators
273    ///     - Ensure the header matches the commit
274    ///     - Ensure commit is valid
275    /// - Validate the untrusted header against the trusted header
276    ///     - Ensure the latest trusted header hasn't expired
277    ///     - Ensure the header isn't from a future time
278    ///     - Check that the untrusted block is more recent than the trusted state
279    ///     - If the untrusted block is the very next block after the trusted block, check that
280    ///       their (next) validator sets hashes match.
281    ///     - Otherwise, ensure that the untrusted block has a greater height than the trusted
282    ///       block.
283    /// - Check there is enough overlap between the validator sets of the trusted and untrusted
284    ///   blocks.
285    /// - Verify that more than 2/3 of the validators correctly committed the block.
286    ///
287    /// **NOTE**: If the untrusted state's `next_validators` field is `None`,
288    /// this will not (and will not be able to) check whether the untrusted
289    /// state's `next_validators_hash` field is valid.
290    ///
291    /// **NOTE**: It is the caller's responsibility to ensure that
292    /// `trusted.next_validators.hash() == trusted.next_validators_hash`,
293    /// as typically the `trusted.next_validators` validator set comes from the relayer,
294    /// and `trusted.next_validators_hash` is the hash stored on chain.
295    fn verify_update_header(
296        &self,
297        untrusted: UntrustedBlockState<'_>,
298        trusted: TrustedBlockState<'_>,
299        options: &Options,
300        now: Time,
301    ) -> Verdict {
302        ensure_verdict_success!(self.verify_validator_sets(&untrusted));
303        ensure_verdict_success!(self.validate_against_trusted(&untrusted, &trusted, options, now));
304        ensure_verdict_success!(self.check_header_is_from_past(&untrusted, options, now));
305        ensure_verdict_success!(self.verify_commit_against_trusted(&untrusted, &trusted, options));
306
307        Verdict::Success
308    }
309
310    /// Verify a header received in `MsgSubmitMisbehaviour`.
311    /// The verification for these headers is a bit more relaxed in order to catch FLA attacks.
312    /// In particular the "header in the future" check for the header should be skipped.
313    fn verify_misbehaviour_header(
314        &self,
315        untrusted: UntrustedBlockState<'_>,
316        trusted: TrustedBlockState<'_>,
317        options: &Options,
318        now: Time,
319    ) -> Verdict {
320        ensure_verdict_success!(self.verify_validator_sets(&untrusted));
321        ensure_verdict_success!(self.validate_against_trusted(&untrusted, &trusted, options, now));
322        ensure_verdict_success!(self.verify_commit_against_trusted(&untrusted, &trusted, options));
323        Verdict::Success
324    }
325}
326
327#[cfg(feature = "rust-crypto")]
328/// The default production implementation of the [`PredicateVerifier`].
329pub type ProdVerifier =
330    PredicateVerifier<ProdPredicates, ProdVotingPowerCalculator, ProdCommitValidator>;
331
332#[cfg(test)]
333mod tests {
334    use alloc::{borrow::ToOwned, string::ToString};
335    use core::{ops::Sub, time::Duration};
336
337    use tendermint::Time;
338    use tendermint_testgen::{light_block::LightBlock as TestgenLightBlock, Generator};
339
340    use crate::{
341        errors::VerificationErrorDetail, options::Options, types::LightBlock, ProdVerifier,
342        Verdict, Verifier,
343    };
344
345    #[allow(dead_code)]
346    #[cfg(feature = "rust-crypto")]
347    #[derive(Clone, Debug, PartialEq, Eq)]
348    struct ProdVerifierSupportsCommonDerivedTraits {
349        verifier: ProdVerifier,
350    }
351
352    #[test]
353    fn test_verification_failure_on_chain_id_mismatch() {
354        let now = Time::now();
355
356        // Create a default light block with a valid chain-id for height `1` with a timestamp 20
357        // secs before now (to be treated as trusted state)
358        let light_block_1: LightBlock = TestgenLightBlock::new_default_with_time_and_chain_id(
359            "chain-1".to_owned(),
360            now.sub(Duration::from_secs(20)).unwrap(),
361            1u64,
362        )
363        .generate()
364        .unwrap()
365        .into();
366
367        // Create another default block with a different chain-id for height `2` with a timestamp 10
368        // secs before now (to be treated as untrusted state)
369        let light_block_2: LightBlock = TestgenLightBlock::new_default_with_time_and_chain_id(
370            "forged-chain".to_owned(),
371            now.sub(Duration::from_secs(10)).unwrap(),
372            2u64,
373        )
374        .generate()
375        .unwrap()
376        .into();
377
378        let vp = ProdVerifier::default();
379        let opt = Options {
380            trust_threshold: Default::default(),
381            trusting_period: Duration::from_secs(60),
382            clock_drift: Default::default(),
383        };
384
385        let verdict = vp.verify_update_header(
386            light_block_2.as_untrusted_state(),
387            light_block_1.as_trusted_state(),
388            &opt,
389            Time::now(),
390        );
391
392        match verdict {
393            Verdict::Invalid(VerificationErrorDetail::ChainIdMismatch(e)) => {
394                let chain_id_1 = light_block_1.signed_header.header.chain_id;
395                let chain_id_2 = light_block_2.signed_header.header.chain_id;
396                assert_eq!(e.got, chain_id_2.to_string());
397                assert_eq!(e.expected, chain_id_1.to_string());
398            },
399            v => panic!("expected ChainIdMismatch error, got: {:?}", v),
400        }
401    }
402
403    #[test]
404    #[cfg(feature = "rust-crypto")]
405    fn test_successful_verify_maliciousupdate_header() {
406        use tendermint::block::CommitSig;
407        use tendermint_testgen::{Header, Validator};
408
409        let now = Time::now();
410
411        // Create options with reasonable values
412        let options = Options {
413            trust_threshold: Default::default(),      // 2/3
414            trusting_period: Duration::from_secs(60), // 60 seconds
415            clock_drift: Duration::from_secs(5),      // 5 seconds
416        };
417
418        // Create verifier
419        let verifier = ProdVerifier::default();
420
421        // Validator Set with one malicious validator
422        let validators = [
423            Validator::new("EVIL").voting_power(51),
424            Validator::new("GOOD").voting_power(50),
425        ];
426
427        let header = Header::new(&validators.clone())
428            .height(1u64)
429            .chain_id("test-chain")
430            .next_validators(&validators)
431            .time(now.sub(Duration::from_secs(20)).unwrap());
432
433        let trusted_block: LightBlock = TestgenLightBlock::new_default_with_header(header)
434            .generate()
435            .unwrap()
436            .into();
437
438        // Generate a untrusted block with the same chain ID and validators
439        // We first generate a valid untrusted block and remove the second validator's signature.
440        // Validating this block will fail as the 2/3 threshold is not reached.
441
442        let header2 = Header::new(&validators)
443            .height(2u64)
444            .chain_id("test-chain")
445            .next_validators(&validators)
446            .time(now.sub(Duration::from_secs(10)).unwrap());
447
448        let mut untrusted_block: LightBlock = TestgenLightBlock::new_default_with_header(header2)
449            .generate()
450            .unwrap()
451            .into();
452        untrusted_block.signed_header.commit.signatures[1] = CommitSig::BlockIdFlagAbsent;
453
454        let verdict = verifier.verify_update_header(
455            untrusted_block.as_untrusted_state(),
456            trusted_block.as_trusted_state(),
457            &options,
458            now,
459        );
460
461        assert_ne!(verdict, Verdict::Success, "Verification should fail");
462
463        // Modify the second validator's address to collide with the malicious one.
464        // This does not change the validator set hash (as the address is not part of it), but will cause the
465        // voting_power_in_impl to double count the single existing commit vote.
466        untrusted_block.validators.validators[1].address =
467            untrusted_block.validators.validators[0].address;
468
469        let verdict = verifier.verify_update_header(
470            untrusted_block.as_untrusted_state(),
471            trusted_block.as_trusted_state(),
472            &options,
473            now,
474        );
475
476        // Test that verification fails
477        match verdict {
478            Verdict::Invalid(VerificationErrorDetail::DuplicateValidator(e)) => {
479                assert_eq!(e.address, untrusted_block.validators.validators[0].address);
480            },
481            v => panic!("expected DuplicateValidator error, got: {:?}", v),
482        }
483
484        // Do a JSON serialization roundtrip.
485        // This isn't needed to perform the attack but verifies that the attack is detected during deserialization.
486        let serialized = serde_json::to_string(&untrusted_block).unwrap();
487        let deserialized: serde_json::Error =
488            serde_json::from_str::<LightBlock>(&serialized).unwrap_err();
489        assert!(deserialized
490            .to_string()
491            .contains("invalid validator address"),);
492    }
493}