mithril_client/certificate_client/
verify.rs

1use anyhow::Context;
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
31        .retriever
32        .get(certificate_hash)
33        .await?
34        .with_context(|| format!("No certificate exist for hash '{certificate_hash}'"))?;
35
36    client.verifier.verify_chain(&certificate).await.with_context(|| {
37        format!("Certificate chain of certificate '{certificate_hash}' is invalid")
38    })?;
39
40    Ok(certificate)
41}
42
43/// Implementation of a [CertificateVerifier] that can send feedbacks using
44/// the [feedback][crate::feedback] mechanism.
45pub struct MithrilCertificateVerifier {
46    retriever: Arc<InternalCertificateRetriever>,
47    internal_verifier: Arc<dyn CommonCertificateVerifier>,
48    genesis_verification_key: ProtocolGenesisVerificationKey,
49    feedback_sender: FeedbackSender,
50    #[cfg(feature = "unstable")]
51    verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
52    logger: Logger,
53}
54
55impl MithrilCertificateVerifier {
56    /// Constructs a new `MithrilCertificateVerifier`.
57    pub fn new(
58        aggregator_requester: Arc<dyn CertificateAggregatorRequest>,
59        genesis_verification_key: &str,
60        feedback_sender: FeedbackSender,
61        #[cfg(feature = "unstable")] verifier_cache: Option<Arc<dyn CertificateVerifierCache>>,
62        logger: Logger,
63    ) -> MithrilResult<MithrilCertificateVerifier> {
64        let logger = logger.new_with_component_name::<Self>();
65        let retriever = Arc::new(InternalCertificateRetriever::new(aggregator_requester));
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            && !certificate.is_genesis()
145        {
146            cache
147                .store_validated_certificate(&certificate.hash, &certificate.previous_hash)
148                .await?;
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::builder::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_requester_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_requester_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::builder::CertificateChainingMethod;
325        use mockall::predicate::eq;
326
327        use crate::certificate_client::verify_cache::MemoryCertificateVerifierCache;
328        use crate::certificate_client::{
329            MockCertificateAggregatorRequest, MockCertificateVerifierCache,
330        };
331        use crate::test_utils::TestLogger;
332
333        use super::*;
334
335        fn build_verifier_with_cache(
336            aggregator_client_mock_config: impl FnOnce(&mut MockCertificateAggregatorRequest),
337            genesis_verification_key: ProtocolGenesisVerificationKey,
338            cache: Arc<dyn CertificateVerifierCache>,
339        ) -> MithrilCertificateVerifier {
340            let mut aggregator_client = MockCertificateAggregatorRequest::new();
341            aggregator_client_mock_config(&mut aggregator_client);
342            let genesis_verification_key: String = genesis_verification_key.try_into().unwrap();
343
344            MithrilCertificateVerifier::new(
345                Arc::new(aggregator_client),
346                &genesis_verification_key,
347                FeedbackSender::new(&[]),
348                Some(cache),
349                TestLogger::stdout(),
350            )
351            .unwrap()
352        }
353
354        #[tokio::test]
355        async fn genesis_certificates_verification_result_is_not_cached() {
356            let chain = CertificateChainBuilder::new()
357                .with_total_certificates(1)
358                .with_certificates_per_epoch(1)
359                .build();
360            let genesis_certificate = chain.last().unwrap();
361            assert!(genesis_certificate.is_genesis());
362
363            let cache = Arc::new(MemoryCertificateVerifierCache::new(TimeDelta::hours(1)));
364            let verifier = build_verifier_with_cache(
365                |_mock| {},
366                chain.genesis_verifier.to_verification_key(),
367                cache.clone(),
368            );
369
370            verifier
371                .verify_with_cache_enabled(
372                    "certificate_chain_validation_id",
373                    CertificateToVerify::Downloaded {
374                        certificate: Box::new(genesis_certificate.clone()),
375                    },
376                )
377                .await
378                .unwrap();
379
380            assert_eq!(
381                cache.get_previous_hash(&genesis_certificate.hash).await.unwrap(),
382                None
383            );
384        }
385
386        #[tokio::test]
387        async fn non_genesis_certificates_verification_result_is_cached() {
388            let chain = CertificateChainBuilder::new()
389                .with_total_certificates(2)
390                .with_certificates_per_epoch(1)
391                .build();
392            let certificate = chain.first().unwrap();
393            let genesis_certificate = chain.last().unwrap();
394            assert!(!certificate.is_genesis());
395
396            let cache = Arc::new(MemoryCertificateVerifierCache::new(TimeDelta::hours(1)));
397            let verifier = build_verifier_with_cache(
398                |mock| mock.expect_certificate_chain(vec![genesis_certificate.clone()]),
399                chain.genesis_verifier.to_verification_key(),
400                cache.clone(),
401            );
402
403            verifier
404                .verify_with_cache_enabled(
405                    "certificate_chain_validation_id",
406                    CertificateToVerify::Downloaded {
407                        certificate: Box::new(certificate.clone()),
408                    },
409                )
410                .await
411                .unwrap();
412
413            assert_eq!(
414                cache.get_previous_hash(&certificate.hash).await.unwrap(),
415                Some(certificate.previous_hash.clone())
416            );
417        }
418
419        #[tokio::test]
420        async fn verification_of_first_certificate_of_a_chain_should_always_fetch_it_from_network()
421        {
422            let chain = CertificateChainBuilder::new()
423                .with_total_certificates(2)
424                .with_certificates_per_epoch(1)
425                .build();
426            let first_certificate = chain.first().unwrap();
427
428            let cache = Arc::new(
429                MemoryCertificateVerifierCache::new(TimeDelta::hours(3))
430                    .with_items_from_chain(&vec![first_certificate.clone()]),
431            );
432            let certificate_client = CertificateClientTestBuilder::default()
433                .config_aggregator_requester_mock(|mock| {
434                    // Expect to fetch the first certificate from the network
435                    mock.expect_certificate_chain(chain.certificates_chained.clone());
436                })
437                .with_genesis_verification_key(chain.genesis_verifier.to_verification_key())
438                .with_verifier_cache(cache.clone())
439                .build();
440
441            certificate_client
442                .verify_chain(&first_certificate.hash)
443                .await
444                .unwrap();
445        }
446
447        #[tokio::test]
448        async fn verification_of_certificates_should_not_use_cache_until_crossing_an_epoch_boundary()
449         {
450            // Scenario:
451            // | Certificate | epoch |         Parent | Can use cache to | Should be fully |
452            // |             |       |                | get parent hash  | Verified        |
453            // |------------:|------:|---------------:|------------------|-----------------|
454            // |         n°6 |     3 |            n°5 | No               | Yes             |
455            // |         n°5 |     3 |            n°4 | No               | Yes             |
456            // |         n°4 |     2 |            n°3 | Yes              | Yes             |
457            // |         n°3 |     2 |            n°2 | Yes              | No              |
458            // |         n°2 |     2 |            n°1 | Yes              | No              |
459            // |         n°1 |     1 | None (genesis) | Yes              | Yes             |
460            let chain = CertificateChainBuilder::new()
461                .with_total_certificates(6)
462                .with_certificates_per_epoch(3)
463                .with_certificate_chaining_method(CertificateChainingMethod::Sequential)
464                .build();
465
466            let first_certificate = chain.first().unwrap();
467            let genesis_certificate = chain.last().unwrap();
468            assert!(genesis_certificate.is_genesis());
469
470            let certificates_that_must_be_fully_verified =
471                [chain[..3].to_vec(), vec![genesis_certificate.clone()]].concat();
472            let certificates_which_parents_can_be_fetched_from_cache = chain[2..5].to_vec();
473
474            let cache = {
475                let mut mock = MockCertificateVerifierCache::new();
476
477                for certificate in certificates_which_parents_can_be_fetched_from_cache {
478                    let previous_hash = certificate.previous_hash.clone();
479                    mock.expect_get_previous_hash()
480                        .with(eq(certificate.hash.clone()))
481                        .return_once(|_| Ok(Some(previous_hash)))
482                        .once();
483                }
484                mock.expect_get_previous_hash()
485                    .with(eq(genesis_certificate.hash.clone()))
486                    .returning(|_| Ok(None));
487                mock.expect_store_validated_certificate().returning(|_, _| Ok(()));
488
489                Arc::new(mock)
490            };
491
492            let certificate_client = CertificateClientTestBuilder::default()
493                .config_aggregator_requester_mock(|mock| {
494                    mock.expect_certificate_chain(certificates_that_must_be_fully_verified);
495                })
496                .with_genesis_verification_key(chain.genesis_verifier.to_verification_key())
497                .with_verifier_cache(cache)
498                .build();
499
500            certificate_client
501                .verify_chain(&first_certificate.hash)
502                .await
503                .unwrap();
504        }
505
506        #[tokio::test]
507        async fn verify_chain_return_certificate_with_cache() {
508            let chain = CertificateChainBuilder::new()
509                .with_total_certificates(5)
510                .with_certificates_per_epoch(1)
511                .build();
512            let last_certificate_hash = chain.first().unwrap().hash.clone();
513
514            // All certificates are cached except the last two (to cross an epoch boundary) and the genesis
515            let cache = MemoryCertificateVerifierCache::new(TimeDelta::hours(3))
516                .with_items_from_chain(&chain[2..4]);
517
518            let certificate_client = CertificateClientTestBuilder::default()
519                .config_aggregator_requester_mock(|mock| {
520                    mock.expect_certificate_chain(
521                        [chain[0..3].to_vec(), vec![chain.last().unwrap().clone()]].concat(),
522                    )
523                })
524                .with_genesis_verification_key(chain.genesis_verifier.to_verification_key())
525                .with_verifier_cache(Arc::new(cache))
526                .build();
527
528            let certificate =
529                certificate_client.verify_chain(&last_certificate_hash).await.unwrap();
530
531            assert_eq!(certificate.hash, last_certificate_hash);
532        }
533    }
534}