mithril_client/certificate_client/
verify.rs1use 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
40pub 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 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 Downloaded { certificate: Box<Certificate> },
166 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 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 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 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 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 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}