cometbft_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    /// Verify that more than 2/3 of the validators correctly committed the block.
151    pub fn verify_commit(&self, untrusted: &UntrustedBlockState<'_>) -> Verdict {
152        verdict!(self.predicates.has_sufficient_signers_overlap(
153            untrusted.signed_header,
154            untrusted.validators,
155            &self.voting_power_calculator,
156        ));
157
158        Verdict::Success
159    }
160
161    /// Validate an `UntrustedBlockState` coming from a client update,
162    /// based on the given `TrustedBlockState`, `Options` and current time.
163    pub fn validate_against_trusted(
164        &self,
165        untrusted: &UntrustedBlockState<'_>,
166        trusted: &TrustedBlockState<'_>,
167        options: &Options,
168        now: Time,
169    ) -> Verdict {
170        // Ensure the latest trusted header hasn't expired
171        verdict!(self.predicates.is_within_trust_period(
172            trusted.header_time,
173            options.trusting_period,
174            now,
175        ));
176
177        // Check that the untrusted block is more recent than the trusted state
178        verdict!(self
179            .predicates
180            .is_monotonic_bft_time(untrusted.signed_header.header.time, trusted.header_time));
181
182        // Check that the chain-id of the untrusted block matches that of the trusted state
183        verdict!(self
184            .predicates
185            .is_matching_chain_id(&untrusted.signed_header.header.chain_id, trusted.chain_id));
186
187        let trusted_next_height = trusted.height.increment();
188
189        if untrusted.height() == trusted_next_height {
190            // If the untrusted block is the very next block after the trusted block,
191            // check that their (next) validator sets hashes match.
192            verdict!(self.predicates.valid_next_validator_set(
193                untrusted.signed_header.header.validators_hash,
194                trusted.next_validators_hash,
195            ));
196        } else {
197            // Otherwise, ensure that the untrusted block has a greater height than
198            // the trusted block.
199            verdict!(self
200                .predicates
201                .is_monotonic_height(untrusted.signed_header.header.height, trusted.height));
202        }
203
204        Verdict::Success
205    }
206
207    /// Ensure the header isn't from a future time
208    pub fn check_header_is_from_past(
209        &self,
210        untrusted: &UntrustedBlockState<'_>,
211        options: &Options,
212        now: Time,
213    ) -> Verdict {
214        verdict!(self.predicates.is_header_from_past(
215            untrusted.signed_header.header.time,
216            options.clock_drift,
217            now,
218        ));
219
220        Verdict::Success
221    }
222
223    /// Check there is enough overlap between the validator sets of the trusted and untrusted
224    /// blocks.
225    pub fn verify_commit_against_trusted(
226        &self,
227        untrusted: &UntrustedBlockState<'_>,
228        trusted: &TrustedBlockState<'_>,
229        options: &Options,
230    ) -> Verdict {
231        let trusted_next_height = trusted.height.increment();
232
233        if untrusted.height() != trusted_next_height {
234            // Check there is enough overlap between the validator sets of
235            // the trusted and untrusted blocks.
236            verdict!(self.predicates.has_sufficient_validators_overlap(
237                untrusted.signed_header,
238                trusted.next_validators,
239                &options.trust_threshold,
240                &self.voting_power_calculator,
241            ));
242        }
243
244        Verdict::Success
245    }
246}
247
248impl<P, C, V> Verifier for PredicateVerifier<P, C, V>
249where
250    P: VerificationPredicates,
251    C: VotingPowerCalculator,
252    V: CommitValidator,
253{
254    /// Validate the given light block state by performing the following checks ->
255    ///
256    /// - Validate the untrusted header
257    ///     - Ensure the header validator hashes match the given validators
258    ///     - Ensure the header next validator hashes match the given next validators
259    ///     - Ensure the header matches the commit
260    ///     - Ensure commit is valid
261    /// - Validate the untrusted header against the trusted header
262    ///     - Ensure the latest trusted header hasn't expired
263    ///     - Ensure the header isn't from a future time
264    ///     - Check that the untrusted block is more recent than the trusted state
265    ///     - If the untrusted block is the very next block after the trusted block, check that
266    ///       their (next) validator sets hashes match.
267    ///     - Otherwise, ensure that the untrusted block has a greater height than the trusted
268    ///       block.
269    /// - Check there is enough overlap between the validator sets of the trusted and untrusted
270    ///   blocks.
271    /// - Verify that more than 2/3 of the validators correctly committed the block.
272    ///
273    /// **NOTE**: If the untrusted state's `next_validators` field is `None`,
274    /// this will not (and will not be able to) check whether the untrusted
275    /// state's `next_validators_hash` field is valid.
276    ///
277    /// **NOTE**: It is the caller's responsibility to ensure that
278    /// `trusted.next_validators.hash() == trusted.next_validators_hash`,
279    /// as typically the `trusted.next_validators` validator set comes from the relayer,
280    /// and `trusted.next_validators_hash` is the hash stored on chain.
281    fn verify_update_header(
282        &self,
283        untrusted: UntrustedBlockState<'_>,
284        trusted: TrustedBlockState<'_>,
285        options: &Options,
286        now: Time,
287    ) -> Verdict {
288        ensure_verdict_success!(self.verify_validator_sets(&untrusted));
289        ensure_verdict_success!(self.validate_against_trusted(&untrusted, &trusted, options, now));
290        ensure_verdict_success!(self.check_header_is_from_past(&untrusted, options, now));
291        ensure_verdict_success!(self.verify_commit_against_trusted(&untrusted, &trusted, options));
292        ensure_verdict_success!(self.verify_commit(&untrusted));
293
294        Verdict::Success
295    }
296
297    /// Verify a header received in `MsgSubmitMisbehaviour`.
298    /// The verification for these headers is a bit more relaxed in order to catch FLA attacks.
299    /// In particular the "header in the future" check for the header should be skipped.
300    fn verify_misbehaviour_header(
301        &self,
302        untrusted: UntrustedBlockState<'_>,
303        trusted: TrustedBlockState<'_>,
304        options: &Options,
305        now: Time,
306    ) -> Verdict {
307        ensure_verdict_success!(self.verify_validator_sets(&untrusted));
308        ensure_verdict_success!(self.validate_against_trusted(&untrusted, &trusted, options, now));
309        ensure_verdict_success!(self.verify_commit_against_trusted(&untrusted, &trusted, options));
310        ensure_verdict_success!(self.verify_commit(&untrusted));
311        Verdict::Success
312    }
313}
314
315#[cfg(feature = "rust-crypto")]
316/// The default production implementation of the [`PredicateVerifier`].
317pub type ProdVerifier =
318    PredicateVerifier<ProdPredicates, ProdVotingPowerCalculator, ProdCommitValidator>;
319
320#[cfg(test)]
321mod tests {
322    use alloc::{borrow::ToOwned, string::ToString};
323    use core::{ops::Sub, time::Duration};
324
325    use cometbft::Time;
326    use cometbft_testgen::{light_block::LightBlock as TestgenLightBlock, Generator};
327
328    use crate::{
329        errors::VerificationErrorDetail, options::Options, types::LightBlock, ProdVerifier,
330        Verdict, Verifier,
331    };
332
333    #[cfg(feature = "rust-crypto")]
334    #[derive(Clone, Debug, PartialEq, Eq)]
335    struct ProdVerifierSupportsCommonDerivedTraits {
336        verifier: ProdVerifier,
337    }
338
339    #[test]
340    fn test_verification_failure_on_chain_id_mismatch() {
341        let now = Time::now();
342
343        // Create a default light block with a valid chain-id for height `1` with a timestamp 20
344        // secs before now (to be treated as trusted state)
345        let light_block_1: LightBlock = TestgenLightBlock::new_default_with_time_and_chain_id(
346            "chain-1".to_owned(),
347            now.sub(Duration::from_secs(20)).unwrap(),
348            1u64,
349        )
350        .generate()
351        .unwrap()
352        .into();
353
354        // Create another default block with a different chain-id for height `2` with a timestamp 10
355        // secs before now (to be treated as untrusted state)
356        let light_block_2: LightBlock = TestgenLightBlock::new_default_with_time_and_chain_id(
357            "forged-chain".to_owned(),
358            now.sub(Duration::from_secs(10)).unwrap(),
359            2u64,
360        )
361        .generate()
362        .unwrap()
363        .into();
364
365        let vp = ProdVerifier::default();
366        let opt = Options {
367            trust_threshold: Default::default(),
368            trusting_period: Duration::from_secs(60),
369            clock_drift: Default::default(),
370        };
371
372        let verdict = vp.verify_update_header(
373            light_block_2.as_untrusted_state(),
374            light_block_1.as_trusted_state(),
375            &opt,
376            Time::now(),
377        );
378
379        match verdict {
380            Verdict::Invalid(VerificationErrorDetail::ChainIdMismatch(e)) => {
381                let chain_id_1 = light_block_1.signed_header.header.chain_id;
382                let chain_id_2 = light_block_2.signed_header.header.chain_id;
383                assert_eq!(e.got, chain_id_2.to_string());
384                assert_eq!(e.expected, chain_id_1.to_string());
385            },
386            v => panic!("expected ChainIdMismatch error, got: {:?}", v),
387        }
388    }
389}