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