mithril_client/certificate_client/
verify.rs

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