mithril_client/certificate_client/
verify.rs1use 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
43pub 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 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 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::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 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 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 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}