1use async_trait::async_trait;
2use sha2::{Digest, Sha256};
3use slog::{debug, info, warn, Logger};
4use std::{collections::BTreeMap, io, ops::RangeInclusive, path::Path, sync::Arc};
5
6use mithril_common::crypto_helper::{MKTree, MKTreeStoreInMemory};
7use mithril_common::entities::{CardanoDbBeacon, HexEncodedDigest, ImmutableFileNumber};
8use mithril_common::logging::LoggerExtensions;
9
10use crate::{
11 digesters::{
12 cache::ImmutableFileDigestCacheProvider, ImmutableDigester, ImmutableDigesterError,
13 },
14 entities::ImmutableFile,
15};
16
17use super::immutable_digester::ComputedImmutablesDigests;
18
19pub struct CardanoImmutableDigester {
21 cardano_network: String,
22
23 cache_provider: Option<Arc<dyn ImmutableFileDigestCacheProvider>>,
25
26 logger: Logger,
28}
29
30impl CardanoImmutableDigester {
31 pub fn new(
33 cardano_network: String,
34 cache_provider: Option<Arc<dyn ImmutableFileDigestCacheProvider>>,
35 logger: Logger,
36 ) -> Self {
37 Self {
38 cardano_network,
39 cache_provider,
40 logger: logger.new_with_component_name::<Self>(),
41 }
42 }
43
44 async fn process_immutables(
45 &self,
46 immutables: Vec<ImmutableFile>,
47 ) -> Result<ComputedImmutablesDigests, ImmutableDigesterError> {
48 let cached_values = self.fetch_immutables_cached(immutables).await;
49
50 let logger = self.logger.clone();
52 let computed_digests =
53 tokio::task::spawn_blocking(move || -> Result<ComputedImmutablesDigests, io::Error> {
54 ComputedImmutablesDigests::compute_immutables_digests(cached_values, logger)
55 })
56 .await
57 .map_err(|e| ImmutableDigesterError::DigestComputationError(e.into()))??;
58
59 Ok(computed_digests)
60 }
61
62 async fn fetch_immutables_cached(
63 &self,
64 immutables: Vec<ImmutableFile>,
65 ) -> BTreeMap<ImmutableFile, Option<String>> {
66 match self.cache_provider.as_ref() {
67 None => BTreeMap::from_iter(immutables.into_iter().map(|i| (i, None))),
68 Some(cache_provider) => match cache_provider.get(immutables.clone()).await {
69 Ok(values) => values,
70 Err(error) => {
71 warn!(
72 self.logger, "Error while getting cached immutable files digests";
73 "error" => ?error
74 );
75 BTreeMap::from_iter(immutables.into_iter().map(|i| (i, None)))
76 }
77 },
78 }
79 }
80
81 async fn update_cache(&self, computed_immutables_digests: &ComputedImmutablesDigests) {
82 if let Some(cache_provider) = self.cache_provider.as_ref() {
83 let new_cached_entries = computed_immutables_digests
84 .entries
85 .iter()
86 .filter(|(file, _hash)| {
87 computed_immutables_digests
88 .new_cached_entries
89 .contains(&file.filename)
90 })
91 .map(|(file, hash)| (file.filename.clone(), hash.clone()))
92 .collect();
93
94 if let Err(error) = cache_provider.store(new_cached_entries).await {
95 warn!(
96 self.logger, "Error while storing new immutable files digests to cache";
97 "error" => ?error
98 );
99 }
100 }
101 }
102}
103
104#[async_trait]
105impl ImmutableDigester for CardanoImmutableDigester {
106 async fn compute_digest(
107 &self,
108 dirpath: &Path,
109 beacon: &CardanoDbBeacon,
110 ) -> Result<String, ImmutableDigesterError> {
111 let immutables_to_process =
112 list_immutable_files_to_process(dirpath, beacon.immutable_file_number)?;
113 info!(self.logger, ">> compute_digest"; "beacon" => #?beacon, "nb_of_immutables" => immutables_to_process.len());
114 let computed_immutables_digests = self.process_immutables(immutables_to_process).await?;
115
116 self.update_cache(&computed_immutables_digests).await;
117
118 let digest = {
119 let mut hasher = Sha256::new();
120 hasher.update(compute_beacon_hash(&self.cardano_network, beacon).as_bytes());
121 for (_, digest) in computed_immutables_digests.entries {
122 hasher.update(digest);
123 }
124 let hash: [u8; 32] = hasher.finalize().into();
125
126 hex::encode(hash)
127 };
128
129 debug!(self.logger, "Computed digest: {digest:?}");
130
131 Ok(digest)
132 }
133
134 async fn compute_digests_for_range(
135 &self,
136 dirpath: &Path,
137 range: &RangeInclusive<ImmutableFileNumber>,
138 ) -> Result<ComputedImmutablesDigests, ImmutableDigesterError> {
139 let immutables_to_process = list_immutable_files_to_process_for_range(dirpath, range)?;
140 info!(self.logger, ">> compute_digests_for_range"; "nb_of_immutables" => immutables_to_process.len());
141 let computed_immutables_digests = self.process_immutables(immutables_to_process).await?;
142
143 self.update_cache(&computed_immutables_digests).await;
144
145 debug!(
146 self.logger,
147 "Successfully computed Digests for Cardano database"; "range" => #?range);
148
149 Ok(computed_immutables_digests)
150 }
151
152 async fn compute_merkle_tree(
153 &self,
154 dirpath: &Path,
155 beacon: &CardanoDbBeacon,
156 ) -> Result<MKTree<MKTreeStoreInMemory>, ImmutableDigesterError> {
157 let immutables_to_process =
158 list_immutable_files_to_process(dirpath, beacon.immutable_file_number)?;
159 info!(self.logger, ">> compute_merkle_tree"; "beacon" => #?beacon, "nb_of_immutables" => immutables_to_process.len());
160 let computed_immutables_digests = self.process_immutables(immutables_to_process).await?;
161
162 self.update_cache(&computed_immutables_digests).await;
163
164 let digests: Vec<HexEncodedDigest> =
165 computed_immutables_digests.entries.into_values().collect();
166 let mktree =
167 MKTree::new(&digests).map_err(ImmutableDigesterError::MerkleTreeComputationError)?;
168
169 debug!(
170 self.logger,
171 "Successfully computed Merkle tree for Cardano database"; "beacon" => #?beacon);
172
173 Ok(mktree)
174 }
175}
176
177fn list_immutable_files_to_process(
178 dirpath: &Path,
179 up_to_file_number: ImmutableFileNumber,
180) -> Result<Vec<ImmutableFile>, ImmutableDigesterError> {
181 let immutables: Vec<ImmutableFile> = ImmutableFile::list_all_in_dir(dirpath)?
182 .into_iter()
183 .filter(|f| f.number <= up_to_file_number)
184 .collect();
185
186 match immutables.last() {
187 None => Err(ImmutableDigesterError::NotEnoughImmutable {
188 expected_number: up_to_file_number,
189 found_number: None,
190 db_dir: dirpath.to_owned(),
191 }),
192 Some(last_immutable_file) if last_immutable_file.number < up_to_file_number => {
193 Err(ImmutableDigesterError::NotEnoughImmutable {
194 expected_number: up_to_file_number,
195 found_number: Some(last_immutable_file.number),
196 db_dir: dirpath.to_owned(),
197 })
198 }
199 Some(_) => Ok(immutables),
200 }
201}
202
203fn list_immutable_files_to_process_for_range(
204 dirpath: &Path,
205 range: &RangeInclusive<ImmutableFileNumber>,
206) -> Result<Vec<ImmutableFile>, ImmutableDigesterError> {
207 let immutables: Vec<ImmutableFile> = ImmutableFile::list_all_in_dir(dirpath)?
208 .into_iter()
209 .filter(|f| range.contains(&f.number))
210 .collect();
211
212 Ok(immutables)
213}
214
215fn compute_beacon_hash(network: &str, cardano_db_beacon: &CardanoDbBeacon) -> String {
216 let mut hasher = Sha256::new();
217 hasher.update(network.as_bytes());
218 hasher.update(cardano_db_beacon.epoch.to_be_bytes());
219 hasher.update(cardano_db_beacon.immutable_file_number.to_be_bytes());
220 hex::encode(hasher.finalize())
221}
222
223#[cfg(test)]
224mod tests {
225 use sha2::Sha256;
226 use std::{collections::BTreeMap, io, sync::Arc};
227 use tokio::time::Instant;
228
229 use crate::digesters::cache::{
230 ImmutableDigesterCacheGetError, ImmutableDigesterCacheProviderError,
231 ImmutableDigesterCacheStoreError, MemoryImmutableFileDigestCacheProvider,
232 MockImmutableFileDigestCacheProvider,
233 };
234 use crate::test::{DummyCardanoDbBuilder, TestLogger};
235
236 use super::*;
237
238 fn db_builder(dir_name: &str) -> DummyCardanoDbBuilder {
239 DummyCardanoDbBuilder::new(&format!("cardano_immutable_digester/{dir_name}"))
240 }
241
242 #[test]
243 fn test_compute_beacon_hash() {
244 let hash_expected = "48cbf709b56204d8315aefd3a416b45398094f6fd51785c5b7dcaf7f35aacbfb";
245 let (network, epoch, immutable_file_number) = ("testnet", 10, 100);
246
247 assert_eq!(
248 hash_expected,
249 compute_beacon_hash(network, &CardanoDbBeacon::new(epoch, immutable_file_number))
250 );
251 assert_ne!(
252 hash_expected,
253 compute_beacon_hash(
254 "mainnet",
255 &CardanoDbBeacon::new(epoch, immutable_file_number)
256 )
257 );
258 assert_ne!(
259 hash_expected,
260 compute_beacon_hash(network, &CardanoDbBeacon::new(20, immutable_file_number))
261 );
262 assert_ne!(
263 hash_expected,
264 compute_beacon_hash(network, &CardanoDbBeacon::new(epoch, 200))
265 );
266 }
267
268 #[tokio::test]
269 async fn fail_if_no_file_in_folder() {
270 let cardano_db = db_builder("fail_if_no_file_in_folder").build();
271
272 let result = list_immutable_files_to_process(cardano_db.get_immutable_dir(), 1)
273 .expect_err("list_immutable_files_to_process should have failed");
274
275 assert_eq!(
276 format!(
277 "{:?}",
278 ImmutableDigesterError::NotEnoughImmutable {
279 expected_number: 1,
280 found_number: None,
281 db_dir: cardano_db.get_immutable_dir().to_path_buf(),
282 }
283 ),
284 format!("{result:?}")
285 );
286 }
287
288 #[tokio::test]
289 async fn fail_if_a_invalid_file_is_in_immutable_folder() {
290 let cardano_db = db_builder("fail_if_no_immutable_exist")
291 .with_non_immutables(&["not_immutable"])
292 .build();
293
294 assert!(list_immutable_files_to_process(cardano_db.get_immutable_dir(), 1).is_err());
295 }
296
297 #[tokio::test]
298 async fn can_list_files_to_process_even_if_theres_only_the_uncompleted_immutable_trio() {
299 let cardano_db = db_builder(
300 "can_list_files_to_process_even_if_theres_only_the_uncompleted_immutable_trio",
301 )
302 .with_immutables(&[1])
303 .build();
304
305 let processable_files =
306 list_immutable_files_to_process(cardano_db.get_immutable_dir(), 1).unwrap();
307
308 assert_eq!(
309 vec![
310 "00001.chunk".to_string(),
311 "00001.primary".to_string(),
312 "00001.secondary".to_string()
313 ],
314 processable_files
315 .into_iter()
316 .map(|f| f.filename)
317 .collect::<Vec<_>>()
318 );
319 }
320
321 #[tokio::test]
322 async fn fail_if_less_immutable_than_what_required_in_beacon() {
323 let cardano_db = db_builder("fail_if_less_immutable_than_what_required_in_beacon")
324 .with_immutables(&[1, 2, 3, 4, 5])
325 .append_immutable_trio()
326 .build();
327
328 let result = list_immutable_files_to_process(cardano_db.get_immutable_dir(), 10)
329 .expect_err("list_immutable_files_to_process should've failed");
330
331 assert_eq!(
332 format!(
333 "{:?}",
334 ImmutableDigesterError::NotEnoughImmutable {
335 expected_number: 10,
336 found_number: Some(6),
337 db_dir: cardano_db.get_immutable_dir().to_path_buf(),
338 }
339 ),
340 format!("{result:?}")
341 );
342 }
343
344 #[tokio::test]
345 async fn can_compute_hash_of_a_hundred_immutable_file_trio() {
346 let cardano_db = db_builder("can_compute_hash_of_a_hundred_immutable_file_trio")
347 .with_immutables(&(1..=100).collect::<Vec<ImmutableFileNumber>>())
348 .append_immutable_trio()
349 .build();
350 let logger = TestLogger::stdout();
351 let digester = CardanoImmutableDigester::new(
352 "devnet".to_string(),
353 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
354 logger.clone(),
355 );
356 let beacon = CardanoDbBeacon::new(1, 100);
357
358 let result = digester
359 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
360 .await
361 .expect("compute_digest must not fail");
362
363 assert_eq!(
364 "a27fd67e495c2c77e4b6b0af9925b2b0bc39656c56adfad4aaab9f20fae49122".to_string(),
365 result
366 )
367 }
368
369 #[tokio::test]
370 async fn can_compute_merkle_tree_of_a_hundred_immutable_file_trio() {
371 let cardano_db = db_builder("can_compute_merkle_tree_of_a_hundred_immutable_file_trio")
372 .with_immutables(&(1..=100).collect::<Vec<ImmutableFileNumber>>())
373 .append_immutable_trio()
374 .build();
375 let logger = TestLogger::stdout();
376 let digester = CardanoImmutableDigester::new(
377 "devnet".to_string(),
378 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
379 logger.clone(),
380 );
381 let beacon = CardanoDbBeacon::new(1, 100);
382
383 let result = digester
384 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
385 .await
386 .expect("compute_merkle_tree must not fail");
387
388 let expected_merkle_root = result.compute_root().unwrap().to_hex();
389
390 assert_eq!(
391 "8552f75838176c967a33eb6da1fe5f3c9940b706d75a9c2352c0acd8439f3d84".to_string(),
392 expected_merkle_root
393 )
394 }
395
396 #[tokio::test]
397 async fn can_compute_digests_for_range_of_a_hundred_immutable_file_trio() {
398 let immutable_range = 1..=100;
399 let cardano_db =
400 db_builder("can_compute_digests_for_range_of_a_hundred_immutable_file_trio")
401 .with_immutables(
402 &immutable_range
403 .clone()
404 .collect::<Vec<ImmutableFileNumber>>(),
405 )
406 .append_immutable_trio()
407 .build();
408 let logger = TestLogger::stdout();
409 let digester = CardanoImmutableDigester::new(
410 "devnet".to_string(),
411 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
412 logger.clone(),
413 );
414
415 let result = digester
416 .compute_digests_for_range(cardano_db.get_immutable_dir(), &immutable_range)
417 .await
418 .expect("compute_digests_for_range must not fail");
419
420 assert_eq!(cardano_db.get_immutable_files().len(), result.entries.len())
421 }
422
423 #[tokio::test]
424 async fn can_compute_consistent_digests_for_range() {
425 let immutable_range = 1..=1;
426 let cardano_db = db_builder("can_compute_digests_for_range_consistently")
427 .with_immutables(
428 &immutable_range
429 .clone()
430 .collect::<Vec<ImmutableFileNumber>>(),
431 )
432 .append_immutable_trio()
433 .build();
434 let logger = TestLogger::stdout();
435 let digester = CardanoImmutableDigester::new(
436 "devnet".to_string(),
437 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
438 logger.clone(),
439 );
440
441 let result = digester
442 .compute_digests_for_range(cardano_db.get_immutable_dir(), &immutable_range)
443 .await
444 .expect("compute_digests_for_range must not fail");
445
446 assert_eq!(
447 BTreeMap::from([
448 (
449 ImmutableFile {
450 path: cardano_db.get_immutable_dir().join("00001.chunk"),
451 number: 1,
452 filename: "00001.chunk".to_string()
453 },
454 "faebbf47077f68ef57219396ff69edc738978a3eca946ac7df1983dbf11364ec".to_string()
455 ),
456 (
457 ImmutableFile {
458 path: cardano_db.get_immutable_dir().join("00001.primary"),
459 number: 1,
460 filename: "00001.primary".to_string()
461 },
462 "f11bdb991fc7e72970be7d7f666e10333f92c14326d796fed8c2c041675fa826".to_string()
463 ),
464 (
465 ImmutableFile {
466 path: cardano_db.get_immutable_dir().join("00001.secondary"),
467 number: 1,
468 filename: "00001.secondary".to_string()
469 },
470 "b139684b968fa12ce324cce464d000de0e2c2ded0fd3e473a666410821d3fde3".to_string()
471 )
472 ]),
473 result.entries
474 );
475 }
476
477 #[tokio::test]
478 async fn compute_digest_store_digests_into_cache_provider() {
479 let cardano_db = db_builder("compute_digest_store_digests_into_cache_provider")
480 .with_immutables(&[1, 2])
481 .append_immutable_trio()
482 .build();
483 let immutables = cardano_db.get_immutable_files().clone();
484 let cache = Arc::new(MemoryImmutableFileDigestCacheProvider::default());
485 let logger = TestLogger::stdout();
486 let digester = CardanoImmutableDigester::new(
487 "devnet".to_string(),
488 Some(cache.clone()),
489 logger.clone(),
490 );
491 let beacon = CardanoDbBeacon::new(1, 2);
492
493 digester
494 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
495 .await
496 .expect("compute_digest must not fail");
497
498 let cached_entries = cache
499 .get(immutables.clone())
500 .await
501 .expect("Cache read should not fail");
502 let expected: BTreeMap<_, _> = immutables
503 .into_iter()
504 .map(|i| {
505 let digest = hex::encode(i.compute_raw_hash::<Sha256>().unwrap());
506 (i, Some(digest))
507 })
508 .collect();
509
510 assert_eq!(expected, cached_entries);
511 }
512
513 #[tokio::test]
514 async fn compute_merkle_tree_store_digests_into_cache_provider() {
515 let cardano_db = db_builder("compute_merkle_tree_store_digests_into_cache_provider")
516 .with_immutables(&[1, 2])
517 .append_immutable_trio()
518 .build();
519 let immutables = cardano_db.get_immutable_files().clone();
520 let cache = Arc::new(MemoryImmutableFileDigestCacheProvider::default());
521 let logger = TestLogger::stdout();
522 let digester = CardanoImmutableDigester::new(
523 "devnet".to_string(),
524 Some(cache.clone()),
525 logger.clone(),
526 );
527 let beacon = CardanoDbBeacon::new(1, 2);
528
529 digester
530 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
531 .await
532 .expect("compute_digest must not fail");
533
534 let cached_entries = cache
535 .get(immutables.clone())
536 .await
537 .expect("Cache read should not fail");
538 let expected: BTreeMap<_, _> = immutables
539 .into_iter()
540 .map(|i| {
541 let digest = hex::encode(i.compute_raw_hash::<Sha256>().unwrap());
542 (i, Some(digest))
543 })
544 .collect();
545
546 assert_eq!(expected, cached_entries);
547 }
548
549 #[tokio::test]
550 async fn compute_digests_for_range_stores_digests_into_cache_provider() {
551 let cardano_db = db_builder("compute_digests_for_range_stores_digests_into_cache_provider")
552 .with_immutables(&[1, 2])
553 .append_immutable_trio()
554 .build();
555 let immutables = cardano_db.get_immutable_files().clone();
556 let cache = Arc::new(MemoryImmutableFileDigestCacheProvider::default());
557 let logger = TestLogger::stdout();
558 let digester = CardanoImmutableDigester::new(
559 "devnet".to_string(),
560 Some(cache.clone()),
561 logger.clone(),
562 );
563 let immutable_range = 1..=2;
564
565 digester
566 .compute_digests_for_range(cardano_db.get_immutable_dir(), &immutable_range)
567 .await
568 .expect("compute_digests_for_range must not fail");
569
570 let cached_entries = cache
571 .get(immutables.clone())
572 .await
573 .expect("Cache read should not fail");
574 let expected: BTreeMap<_, _> = immutables
575 .into_iter()
576 .filter(|i| immutable_range.contains(&i.number))
577 .map(|i| {
578 let digest = hex::encode(i.compute_raw_hash::<Sha256>().unwrap());
579 (i.to_owned(), Some(digest))
580 })
581 .collect();
582
583 assert_eq!(expected, cached_entries);
584 }
585
586 #[tokio::test]
587 async fn computed_digest_with_cold_or_hot_or_without_any_cache_are_equals() {
588 let cardano_db = DummyCardanoDbBuilder::new(
589 "computed_digest_with_cold_or_hot_or_without_any_cache_are_equals",
590 )
591 .with_immutables(&[1, 2, 3])
592 .append_immutable_trio()
593 .build();
594 let logger = TestLogger::stdout();
595 let no_cache_digester =
596 CardanoImmutableDigester::new("devnet".to_string(), None, logger.clone());
597 let cache_digester = CardanoImmutableDigester::new(
598 "devnet".to_string(),
599 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
600 logger.clone(),
601 );
602 let beacon = CardanoDbBeacon::new(1, 3);
603
604 let without_cache_digest = no_cache_digester
605 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
606 .await
607 .expect("compute_digest must not fail");
608
609 let cold_cache_digest = cache_digester
610 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
611 .await
612 .expect("compute_digest must not fail");
613
614 let full_cache_digest = cache_digester
615 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
616 .await
617 .expect("compute_digest must not fail");
618
619 assert_eq!(
620 without_cache_digest, full_cache_digest,
621 "Digests with or without cache should be the same"
622 );
623
624 assert_eq!(
625 cold_cache_digest, full_cache_digest,
626 "Digests with cold or with hot cache should be the same"
627 );
628 }
629
630 #[tokio::test]
631 async fn computed_merkle_tree_with_cold_or_hot_or_without_any_cache_are_equals() {
632 let cardano_db = DummyCardanoDbBuilder::new(
633 "computed_merkle_tree_with_cold_or_hot_or_without_any_cache_are_equals",
634 )
635 .with_immutables(&[1, 2, 3])
636 .append_immutable_trio()
637 .build();
638 let logger = TestLogger::stdout();
639 let no_cache_digester =
640 CardanoImmutableDigester::new("devnet".to_string(), None, logger.clone());
641 let cache_digester = CardanoImmutableDigester::new(
642 "devnet".to_string(),
643 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
644 logger.clone(),
645 );
646 let beacon = CardanoDbBeacon::new(1, 3);
647
648 let without_cache_digest = no_cache_digester
649 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
650 .await
651 .expect("compute_merkle_tree must not fail");
652
653 let cold_cache_digest = cache_digester
654 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
655 .await
656 .expect("compute_merkle_tree must not fail");
657
658 let full_cache_digest = cache_digester
659 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
660 .await
661 .expect("compute_merkle_tree must not fail");
662
663 let without_cache_merkle_root = without_cache_digest.compute_root().unwrap();
664 let cold_cache_merkle_root = cold_cache_digest.compute_root().unwrap();
665 let full_cache_merkle_root = full_cache_digest.compute_root().unwrap();
666 assert_eq!(
667 without_cache_merkle_root, full_cache_merkle_root,
668 "Merkle roots with or without cache should be the same"
669 );
670
671 assert_eq!(
672 cold_cache_merkle_root, full_cache_merkle_root,
673 "Merkle roots with cold or with hot cache should be the same"
674 );
675 }
676
677 #[tokio::test]
678 async fn computed_digests_for_range_with_cold_or_hot_or_without_any_cache_are_equals() {
679 let cardano_db = DummyCardanoDbBuilder::new(
680 "computed_digests_for_range_with_cold_or_hot_or_without_any_cache_are_equals",
681 )
682 .with_immutables(&[1, 2, 3])
683 .append_immutable_trio()
684 .build();
685 let logger = TestLogger::stdout();
686 let no_cache_digester =
687 CardanoImmutableDigester::new("devnet".to_string(), None, logger.clone());
688 let cache_digester = CardanoImmutableDigester::new(
689 "devnet".to_string(),
690 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
691 logger.clone(),
692 );
693 let immutable_range = 1..=3;
694
695 let without_cache_digests = no_cache_digester
696 .compute_digests_for_range(cardano_db.get_immutable_dir(), &immutable_range)
697 .await
698 .expect("compute_digests_for_range must not fail");
699
700 let cold_cache_digests = cache_digester
701 .compute_digests_for_range(cardano_db.get_immutable_dir(), &immutable_range)
702 .await
703 .expect("compute_digests_for_range must not fail");
704
705 let full_cache_digests = cache_digester
706 .compute_digests_for_range(cardano_db.get_immutable_dir(), &immutable_range)
707 .await
708 .expect("compute_digests_for_range must not fail");
709
710 let without_cache_entries = without_cache_digests.entries;
711 let cold_cache_entries = cold_cache_digests.entries;
712 let full_cache_entries = full_cache_digests.entries;
713 assert_eq!(
714 without_cache_entries, full_cache_entries,
715 "Digests for range with or without cache should be the same"
716 );
717
718 assert_eq!(
719 cold_cache_entries, full_cache_entries,
720 "Digests for range with cold or with hot cache should be the same"
721 );
722 }
723
724 #[tokio::test]
725 async fn hash_computation_is_quicker_with_a_full_cache() {
726 let cardano_db = db_builder("hash_computation_is_quicker_with_a_full_cache")
727 .with_immutables(&(1..=50).collect::<Vec<ImmutableFileNumber>>())
728 .append_immutable_trio()
729 .set_immutable_trio_file_size(65538)
730 .build();
731 let cache = MemoryImmutableFileDigestCacheProvider::default();
732 let logger = TestLogger::stdout();
733 let digester = CardanoImmutableDigester::new(
734 "devnet".to_string(),
735 Some(Arc::new(cache)),
736 logger.clone(),
737 );
738 let beacon = CardanoDbBeacon::new(1, 50);
739
740 let now = Instant::now();
741 digester
742 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
743 .await
744 .expect("compute_digest must not fail");
745 let elapsed_without_cache = now.elapsed();
746
747 let now = Instant::now();
748 digester
749 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
750 .await
751 .expect("compute_digest must not fail");
752 let elapsed_with_cache = now.elapsed();
753
754 assert!(
759 elapsed_with_cache < (elapsed_without_cache * 9 / 10),
760 "digest computation with full cache should be faster than without cache,\
761 time elapsed: with cache {elapsed_with_cache:?}, without cache {elapsed_without_cache:?}"
762 );
763 }
764
765 #[tokio::test]
766 async fn cache_read_failure_dont_block_computations() {
767 let cardano_db = db_builder("cache_read_failure_dont_block_computation")
768 .with_immutables(&[1, 2, 3])
769 .append_immutable_trio()
770 .build();
771 let mut cache = MockImmutableFileDigestCacheProvider::new();
772 cache.expect_get().returning(|_| Ok(BTreeMap::new()));
773 cache.expect_store().returning(|_| {
774 Err(ImmutableDigesterCacheProviderError::Store(
775 ImmutableDigesterCacheStoreError::Io(io::Error::other("error")),
776 ))
777 });
778 let logger = TestLogger::stdout();
779 let digester = CardanoImmutableDigester::new(
780 "devnet".to_string(),
781 Some(Arc::new(cache)),
782 logger.clone(),
783 );
784 let beacon = CardanoDbBeacon::new(1, 3);
785
786 digester
787 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
788 .await
789 .expect("compute_digest must not fail even with cache write failure");
790
791 digester
792 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
793 .await
794 .expect("compute_merkle_tree must not fail even with cache write failure");
795 }
796
797 #[tokio::test]
798 async fn cache_write_failure_dont_block_computation() {
799 let cardano_db = db_builder("cache_write_failure_dont_block_computation")
800 .with_immutables(&[1, 2, 3])
801 .append_immutable_trio()
802 .build();
803 let mut cache = MockImmutableFileDigestCacheProvider::new();
804 cache.expect_get().returning(|_| {
805 Err(ImmutableDigesterCacheProviderError::Get(
806 ImmutableDigesterCacheGetError::Io(io::Error::other("error")),
807 ))
808 });
809 cache.expect_store().returning(|_| Ok(()));
810 let logger = TestLogger::stdout();
811 let digester = CardanoImmutableDigester::new(
812 "devnet".to_string(),
813 Some(Arc::new(cache)),
814 logger.clone(),
815 );
816 let beacon = CardanoDbBeacon::new(1, 3);
817
818 digester
819 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
820 .await
821 .expect("compute_digest must not fail even with cache read failure");
822
823 digester
824 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
825 .await
826 .expect("compute_merkle_tree must not fail even with cache read failure");
827 }
828}