mithril_client/certificate_client/
verify.rs

1use anyhow::{Context, anyhow};
2use async_trait::async_trait;
3use slog::{Logger, trace};
4use std::sync::Arc;
5
6use mithril_common::{
7    certificate_chain::{
8        CertificateRetriever, CertificateVerifier as CommonCertificateVerifier,
9        MithrilCertificateVerifier as CommonMithrilCertificateVerifier,
10    },
11    crypto_helper::ProtocolGenesisVerificationKey,
12    entities::Certificate,
13    logging::LoggerExtensions,
14};
15
16use crate::aggregator_client::AggregatorClient;
17#[cfg(feature = "unstable")]
18use crate::certificate_client::CertificateVerifierCache;
19use crate::certificate_client::fetch::InternalCertificateRetriever;
20use crate::certificate_client::{CertificateClient, CertificateVerifier};
21use crate::feedback::{FeedbackSender, MithrilEvent};
22use crate::{MithrilCertificate, MithrilResult};
23
24#[inline]
25pub(super) async fn verify_chain(
26    client: &CertificateClient,
27    certificate_hash: &str,
28) -> MithrilResult<MithrilCertificate> {
29    let certificate = client.retriever.get(certificate_hash).await?.ok_or(anyhow!(
30        "No certificate exist for hash '{certificate_hash}'"
31    ))?;
32
33    client.verifier.verify_chain(&certificate).await.with_context(|| {
34        format!("Certificate chain of certificate '{certificate_hash}' is invalid")
35    })?;
36
37    Ok(certificate)
38}
39
40/// Implementation of a [CertificateVerifier] that can send feedbacks using
41/// the [feedback][crate::feedback] mechanism.
42pub struct MithrilCertificateVerifier {
43    retriever: Arc<InternalCertificateRetriever>,
44    internal_verifier: Arc<dyn CommonCertificateVerifier>,
45    genesis_verification_key: ProtocolGenesisVerificationKey,
46    feedback_sender: FeedbackSender,
47    #[cfg(feature = "unstable")]
48    verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
49    logger: Logger,
50}
51
52impl MithrilCertificateVerifier {
53    /// Constructs a new `MithrilCertificateVerifier`.
54    pub fn new(
55        aggregator_client: Arc<dyn AggregatorClient>,
56        genesis_verification_key: &str,
57        feedback_sender: FeedbackSender,
58        #[cfg(feature = "unstable")] verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
59        logger: Logger,
60    ) -> MithrilResult<MithrilCertificateVerifier> {
61        let logger = logger.new_with_component_name::<Self>();
62        let retriever = Arc::new(InternalCertificateRetriever::new(
63            aggregator_client,
64            logger.clone(),
65        ));
66        let internal_verifier = Arc::new(CommonMithrilCertificateVerifier::new(
67            logger.clone(),
68            retriever.clone(),
69        ));
70        let genesis_verification_key =
71            ProtocolGenesisVerificationKey::try_from(genesis_verification_key)
72                .with_context(|| "Invalid genesis verification key")?;
73
74        Ok(Self {
75            retriever,
76            internal_verifier,
77            genesis_verification_key,
78            feedback_sender,
79            #[cfg(feature = "unstable")]
80            verifier_cache,
81            logger,
82        })
83    }
84
85    #[cfg(feature = "unstable")]
86    async fn fetch_cached_previous_hash(&self, hash: &str) -> MithrilResult<Option<String>> {
87        if let Some(cache) = self.verifier_cache.as_ref() {
88            Ok(cache.get_previous_hash(hash).await?)
89        } else {
90            Ok(None)
91        }
92    }
93
94    #[cfg(not(feature = "unstable"))]
95    async fn fetch_cached_previous_hash(&self, _hash: &str) -> MithrilResult<Option<String>> {
96        Ok(None)
97    }
98
99    async fn verify_with_cache_enabled(
100        &self,
101        certificate_chain_validation_id: &str,
102        certificate: CertificateToVerify,
103    ) -> MithrilResult<Option<CertificateToVerify>> {
104        trace!(self.logger, "Validating certificate"; "hash" => certificate.hash(), "previous_hash" => certificate.hash());
105        if let Some(previous_hash) = self.fetch_cached_previous_hash(certificate.hash()).await? {
106            trace!(self.logger, "Certificate fetched from cache"; "hash" => certificate.hash(), "previous_hash" => &previous_hash);
107            self.feedback_sender
108                .send_event(MithrilEvent::CertificateFetchedFromCache {
109                    certificate_hash: certificate.hash().to_owned(),
110                    certificate_chain_validation_id: certificate_chain_validation_id.to_string(),
111                })
112                .await;
113
114            Ok(Some(CertificateToVerify::ToDownload {
115                hash: previous_hash,
116            }))
117        } else {
118            let certificate = match certificate {
119                CertificateToVerify::Downloaded { certificate } => *certificate,
120                CertificateToVerify::ToDownload { hash } => {
121                    self.retriever.get_certificate_details(&hash).await?
122                }
123            };
124
125            let previous_certificate = self
126                .verify_without_cache(certificate_chain_validation_id, certificate)
127                .await?;
128            Ok(previous_certificate.map(Into::into))
129        }
130    }
131
132    async fn verify_without_cache(
133        &self,
134        certificate_chain_validation_id: &str,
135        certificate: Certificate,
136    ) -> MithrilResult<Option<Certificate>> {
137        let previous_certificate = self
138            .internal_verifier
139            .verify_certificate(&certificate, &self.genesis_verification_key)
140            .await?;
141
142        #[cfg(feature = "unstable")]
143        if let Some(cache) = self.verifier_cache.as_ref() {
144            if !certificate.is_genesis() {
145                cache
146                    .store_validated_certificate(&certificate.hash, &certificate.previous_hash)
147                    .await?;
148            }
149        }
150
151        trace!(self.logger, "Certificate validated"; "hash" => &certificate.hash, "previous_hash" => &certificate.previous_hash);
152        self.feedback_sender
153            .send_event(MithrilEvent::CertificateValidated {
154                certificate_hash: certificate.hash,
155                certificate_chain_validation_id: certificate_chain_validation_id.to_string(),
156            })
157            .await;
158
159        Ok(previous_certificate)
160    }
161}
162
163enum CertificateToVerify {
164    /// The certificate is already downloaded.
165    Downloaded { certificate: Box<Certificate> },
166    /// The certificate is not downloaded yet (since its parent was cached).
167    ToDownload { hash: String },
168}
169
170impl CertificateToVerify {
171    fn hash(&self) -> &str {
172        match self {
173            CertificateToVerify::Downloaded { certificate } => &certificate.hash,
174            CertificateToVerify::ToDownload { hash } => hash,
175        }
176    }
177}
178
179impl From<Certificate> for CertificateToVerify {
180    fn from(value: Certificate) -> Self {
181        Self::Downloaded {
182            certificate: Box::new(value),
183        }
184    }
185}
186
187#[cfg_attr(target_family = "wasm", async_trait(?Send))]
188#[cfg_attr(not(target_family = "wasm"), async_trait)]
189impl CertificateVerifier for MithrilCertificateVerifier {
190    async fn verify_chain(&self, certificate: &MithrilCertificate) -> MithrilResult<()> {
191        // Todo: move most of this code in the `mithril_common` verifier by defining
192        // a new `verify_chain` method that take a callback called when a certificate is
193        // validated.
194        let certificate_chain_validation_id = MithrilEvent::new_certificate_chain_validation_id();
195        self.feedback_sender
196            .send_event(MithrilEvent::CertificateChainValidationStarted {
197                certificate_chain_validation_id: certificate_chain_validation_id.clone(),
198            })
199            .await;
200
201        // Validate certificates without cache until we cross an epoch boundary
202        // This is necessary to ensure that the AVK chaining is correct
203        let start_epoch = certificate.epoch;
204        let mut current_certificate: Option<Certificate> = Some(certificate.clone().try_into()?);
205        loop {
206            match current_certificate {
207                None => break,
208                Some(next) => {
209                    current_certificate = self
210                        .verify_without_cache(&certificate_chain_validation_id, next)
211                        .await?;
212
213                    let has_crossed_epoch_boundary =
214                        current_certificate.as_ref().is_some_and(|c| c.epoch != start_epoch);
215                    if has_crossed_epoch_boundary {
216                        break;
217                    }
218                }
219            }
220        }
221
222        let mut current_certificate: Option<CertificateToVerify> =
223            current_certificate.map(Into::into);
224        loop {
225            match current_certificate {
226                None => break,
227                Some(next) => {
228                    current_certificate = self
229                        .verify_with_cache_enabled(&certificate_chain_validation_id, next)
230                        .await?
231                }
232            }
233        }
234
235        self.feedback_sender
236            .send_event(MithrilEvent::CertificateChainValidated {
237                certificate_chain_validation_id,
238            })
239            .await;
240
241        Ok(())
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use mithril_common::test_utils::CertificateChainBuilder;
248
249    use crate::certificate_client::tests_utils::CertificateClientTestBuilder;
250    use crate::feedback::StackFeedbackReceiver;
251
252    use super::*;
253
254    #[tokio::test]
255    async fn validating_chain_send_feedbacks() {
256        let chain = CertificateChainBuilder::new()
257            .with_total_certificates(3)
258            .with_certificates_per_epoch(1)
259            .build();
260        let last_certificate_hash = chain.first().unwrap().hash.clone();
261
262        let feedback_receiver = Arc::new(StackFeedbackReceiver::new());
263        let certificate_client = CertificateClientTestBuilder::default()
264            .config_aggregator_client_mock(|mock| {
265                mock.expect_certificate_chain(chain.certificates_chained.clone())
266            })
267            .with_genesis_verification_key(chain.genesis_verifier.to_verification_key())
268            .add_feedback_receiver(feedback_receiver.clone())
269            .build();
270
271        certificate_client
272            .verify_chain(&last_certificate_hash)
273            .await
274            .expect("Chain validation should succeed");
275
276        let actual = feedback_receiver.stacked_events();
277        let id = actual[0].event_id();
278
279        let expected = {
280            let mut vec = vec![MithrilEvent::CertificateChainValidationStarted {
281                certificate_chain_validation_id: id.to_string(),
282            }];
283            vec.extend(chain.certificates_chained.into_iter().map(|c| {
284                MithrilEvent::CertificateValidated {
285                    certificate_chain_validation_id: id.to_string(),
286                    certificate_hash: c.hash,
287                }
288            }));
289            vec.push(MithrilEvent::CertificateChainValidated {
290                certificate_chain_validation_id: id.to_string(),
291            });
292            vec
293        };
294
295        assert_eq!(actual, expected);
296    }
297
298    #[tokio::test]
299    async fn verify_chain_return_certificate_with_given_hash() {
300        let chain = CertificateChainBuilder::new()
301            .with_total_certificates(3)
302            .with_certificates_per_epoch(1)
303            .build();
304        let last_certificate_hash = chain.first().unwrap().hash.clone();
305
306        let certificate_client = CertificateClientTestBuilder::default()
307            .config_aggregator_client_mock(|mock| {
308                mock.expect_certificate_chain(chain.certificates_chained.clone())
309            })
310            .with_genesis_verification_key(chain.genesis_verifier.to_verification_key())
311            .build();
312
313        let certificate = certificate_client
314            .verify_chain(&last_certificate_hash)
315            .await
316            .expect("Chain validation should succeed");
317
318        assert_eq!(certificate.hash, last_certificate_hash);
319    }
320
321    #[cfg(feature = "unstable")]
322    mod cache {
323        use chrono::TimeDelta;
324        use mithril_common::test_utils::CertificateChainingMethod;
325        use mockall::predicate::eq;
326
327        use crate::aggregator_client::MockAggregatorClient;
328        use crate::certificate_client::MockCertificateVerifierCache;
329        use crate::certificate_client::verify_cache::MemoryCertificateVerifierCache;
330        use crate::test_utils::TestLogger;
331
332        use super::*;
333
334        fn build_verifier_with_cache(
335            aggregator_client_mock_config: impl FnOnce(&mut MockAggregatorClient),
336            genesis_verification_key: ProtocolGenesisVerificationKey,
337            cache: Arc<dyn CertificateVerifierCache>,
338        ) -> MithrilCertificateVerifier {
339            let mut aggregator_client = MockAggregatorClient::new();
340            aggregator_client_mock_config(&mut aggregator_client);
341            let genesis_verification_key: String = genesis_verification_key.try_into().unwrap();
342
343            MithrilCertificateVerifier::new(
344                Arc::new(aggregator_client),
345                &genesis_verification_key,
346                FeedbackSender::new(&[]),
347                Some(cache),
348                TestLogger::stdout(),
349            )
350            .unwrap()
351        }
352
353        #[tokio::test]
354        async fn genesis_certificates_verification_result_is_not_cached() {
355            let chain = CertificateChainBuilder::new()
356                .with_total_certificates(1)
357                .with_certificates_per_epoch(1)
358                .build();
359            let genesis_certificate = chain.last().unwrap();
360            assert!(genesis_certificate.is_genesis());
361
362            let cache = Arc::new(MemoryCertificateVerifierCache::new(TimeDelta::hours(1)));
363            let verifier = build_verifier_with_cache(
364                |_mock| {},
365                chain.genesis_verifier.to_verification_key(),
366                cache.clone(),
367            );
368
369            verifier
370                .verify_with_cache_enabled(
371                    "certificate_chain_validation_id",
372                    CertificateToVerify::Downloaded {
373                        certificate: Box::new(genesis_certificate.clone()),
374                    },
375                )
376                .await
377                .unwrap();
378
379            assert_eq!(
380                cache.get_previous_hash(&genesis_certificate.hash).await.unwrap(),
381                None
382            );
383        }
384
385        #[tokio::test]
386        async fn non_genesis_certificates_verification_result_is_cached() {
387            let chain = CertificateChainBuilder::new()
388                .with_total_certificates(2)
389                .with_certificates_per_epoch(1)
390                .build();
391            let certificate = chain.first().unwrap();
392            let genesis_certificate = chain.last().unwrap();
393            assert!(!certificate.is_genesis());
394
395            let cache = Arc::new(MemoryCertificateVerifierCache::new(TimeDelta::hours(1)));
396            let verifier = build_verifier_with_cache(
397                |mock| mock.expect_certificate_chain(vec![genesis_certificate.clone()]),
398                chain.genesis_verifier.to_verification_key(),
399                cache.clone(),
400            );
401
402            verifier
403                .verify_with_cache_enabled(
404                    "certificate_chain_validation_id",
405                    CertificateToVerify::Downloaded {
406                        certificate: Box::new(certificate.clone()),
407                    },
408                )
409                .await
410                .unwrap();
411
412            assert_eq!(
413                cache.get_previous_hash(&certificate.hash).await.unwrap(),
414                Some(certificate.previous_hash.clone())
415            );
416        }
417
418        #[tokio::test]
419        async fn verification_of_first_certificate_of_a_chain_should_always_fetch_it_from_network()
420        {
421            let chain = CertificateChainBuilder::new()
422                .with_total_certificates(2)
423                .with_certificates_per_epoch(1)
424                .build();
425            let first_certificate = chain.first().unwrap();
426
427            let cache = Arc::new(
428                MemoryCertificateVerifierCache::new(TimeDelta::hours(3))
429                    .with_items_from_chain(&vec![first_certificate.clone()]),
430            );
431            let certificate_client = CertificateClientTestBuilder::default()
432                .config_aggregator_client_mock(|mock| {
433                    // Expect to fetch the first certificate from the network
434                    mock.expect_certificate_chain(chain.certificates_chained.clone());
435                })
436                .with_genesis_verification_key(chain.genesis_verifier.to_verification_key())
437                .with_verifier_cache(cache.clone())
438                .build();
439
440            certificate_client
441                .verify_chain(&first_certificate.hash)
442                .await
443                .unwrap();
444        }
445
446        #[tokio::test]
447        async fn verification_of_certificates_should_not_use_cache_until_crossing_an_epoch_boundary()
448         {
449            // Scenario:
450            // | Certificate | epoch |         Parent | Can use cache to | Should be fully |
451            // |             |       |                | get parent hash  | Verified        |
452            // |------------:|------:|---------------:|------------------|-----------------|
453            // |         n°6 |     3 |            n°5 | No               | Yes             |
454            // |         n°5 |     3 |            n°4 | No               | Yes             |
455            // |         n°4 |     2 |            n°3 | Yes              | Yes             |
456            // |         n°3 |     2 |            n°2 | Yes              | No              |
457            // |         n°2 |     2 |            n°1 | Yes              | No              |
458            // |         n°1 |     1 | None (genesis) | Yes              | Yes             |
459            let chain = CertificateChainBuilder::new()
460                .with_total_certificates(6)
461                .with_certificates_per_epoch(3)
462                .with_certificate_chaining_method(CertificateChainingMethod::Sequential)
463                .build();
464
465            let first_certificate = chain.first().unwrap();
466            let genesis_certificate = chain.last().unwrap();
467            assert!(genesis_certificate.is_genesis());
468
469            let certificates_that_must_be_fully_verified =
470                [chain[..3].to_vec(), vec![genesis_certificate.clone()]].concat();
471            let certificates_which_parents_can_be_fetched_from_cache = chain[2..5].to_vec();
472
473            let cache = {
474                let mut mock = MockCertificateVerifierCache::new();
475
476                for certificate in certificates_which_parents_can_be_fetched_from_cache {
477                    let previous_hash = certificate.previous_hash.clone();
478                    mock.expect_get_previous_hash()
479                        .with(eq(certificate.hash.clone()))
480                        .return_once(|_| Ok(Some(previous_hash)))
481                        .once();
482                }
483                mock.expect_get_previous_hash()
484                    .with(eq(genesis_certificate.hash.clone()))
485                    .returning(|_| Ok(None));
486                mock.expect_store_validated_certificate().returning(|_, _| Ok(()));
487
488                Arc::new(mock)
489            };
490
491            let certificate_client = CertificateClientTestBuilder::default()
492                .config_aggregator_client_mock(|mock| {
493                    mock.expect_certificate_chain(certificates_that_must_be_fully_verified);
494                })
495                .with_genesis_verification_key(chain.genesis_verifier.to_verification_key())
496                .with_verifier_cache(cache)
497                .build();
498
499            certificate_client
500                .verify_chain(&first_certificate.hash)
501                .await
502                .unwrap();
503        }
504
505        #[tokio::test]
506        async fn verify_chain_return_certificate_with_cache() {
507            let chain = CertificateChainBuilder::new()
508                .with_total_certificates(5)
509                .with_certificates_per_epoch(1)
510                .build();
511            let last_certificate_hash = chain.first().unwrap().hash.clone();
512
513            // All certificates are cached except the last two (to cross an epoch boundary) and the genesis
514            let cache = MemoryCertificateVerifierCache::new(TimeDelta::hours(3))
515                .with_items_from_chain(&chain[2..4]);
516
517            let certificate_client = CertificateClientTestBuilder::default()
518                .config_aggregator_client_mock(|mock| {
519                    mock.expect_certificate_chain(
520                        [chain[0..3].to_vec(), vec![chain.last().unwrap().clone()]].concat(),
521                    )
522                })
523                .with_genesis_verification_key(chain.genesis_verifier.to_verification_key())
524                .with_verifier_cache(Arc::new(cache))
525                .build();
526
527            let certificate =
528                certificate_client.verify_chain(&last_certificate_hash).await.unwrap();
529
530            assert_eq!(certificate.hash, last_certificate_hash);
531        }
532    }
533}