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
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
41pub 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 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 Downloaded { certificate: Box<Certificate> },
164 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 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 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 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 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 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}