1use async_trait::async_trait;
2use sha2::{Digest, Sha256};
3use slog::{Logger, debug, info, warn};
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 ImmutableDigester, ImmutableDigesterError, cache::ImmutableFileDigestCacheProvider,
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.into_iter().map(|f| f.filename).collect::<Vec<_>>()
315 );
316 }
317
318 #[tokio::test]
319 async fn fail_if_less_immutable_than_what_required_in_beacon() {
320 let cardano_db = db_builder("fail_if_less_immutable_than_what_required_in_beacon")
321 .with_immutables(&[1, 2, 3, 4, 5])
322 .append_immutable_trio()
323 .build();
324
325 let result = list_immutable_files_to_process(cardano_db.get_immutable_dir(), 10)
326 .expect_err("list_immutable_files_to_process should've failed");
327
328 assert_eq!(
329 format!(
330 "{:?}",
331 ImmutableDigesterError::NotEnoughImmutable {
332 expected_number: 10,
333 found_number: Some(6),
334 db_dir: cardano_db.get_immutable_dir().to_path_buf(),
335 }
336 ),
337 format!("{result:?}")
338 );
339 }
340
341 #[tokio::test]
342 async fn can_compute_hash_of_a_hundred_immutable_file_trio() {
343 let cardano_db = db_builder("can_compute_hash_of_a_hundred_immutable_file_trio")
344 .with_immutables(&(1..=100).collect::<Vec<ImmutableFileNumber>>())
345 .append_immutable_trio()
346 .build();
347 let logger = TestLogger::stdout();
348 let digester = CardanoImmutableDigester::new(
349 "devnet".to_string(),
350 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
351 logger.clone(),
352 );
353 let beacon = CardanoDbBeacon::new(1, 100);
354
355 let result = digester
356 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
357 .await
358 .expect("compute_digest must not fail");
359
360 assert_eq!(
361 "a27fd67e495c2c77e4b6b0af9925b2b0bc39656c56adfad4aaab9f20fae49122".to_string(),
362 result
363 )
364 }
365
366 #[tokio::test]
367 async fn can_compute_merkle_tree_of_a_hundred_immutable_file_trio() {
368 let cardano_db = db_builder("can_compute_merkle_tree_of_a_hundred_immutable_file_trio")
369 .with_immutables(&(1..=100).collect::<Vec<ImmutableFileNumber>>())
370 .append_immutable_trio()
371 .build();
372 let logger = TestLogger::stdout();
373 let digester = CardanoImmutableDigester::new(
374 "devnet".to_string(),
375 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
376 logger.clone(),
377 );
378 let beacon = CardanoDbBeacon::new(1, 100);
379
380 let result = digester
381 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
382 .await
383 .expect("compute_merkle_tree must not fail");
384
385 let expected_merkle_root = result.compute_root().unwrap().to_hex();
386
387 assert_eq!(
388 "8552f75838176c967a33eb6da1fe5f3c9940b706d75a9c2352c0acd8439f3d84".to_string(),
389 expected_merkle_root
390 )
391 }
392
393 #[tokio::test]
394 async fn can_compute_digests_for_range_of_a_hundred_immutable_file_trio() {
395 let immutable_range = 1..=100;
396 let cardano_db =
397 db_builder("can_compute_digests_for_range_of_a_hundred_immutable_file_trio")
398 .with_immutables(&immutable_range.clone().collect::<Vec<ImmutableFileNumber>>())
399 .append_immutable_trio()
400 .build();
401 let logger = TestLogger::stdout();
402 let digester = CardanoImmutableDigester::new(
403 "devnet".to_string(),
404 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
405 logger.clone(),
406 );
407
408 let result = digester
409 .compute_digests_for_range(cardano_db.get_immutable_dir(), &immutable_range)
410 .await
411 .expect("compute_digests_for_range must not fail");
412
413 assert_eq!(cardano_db.get_immutable_files().len(), result.entries.len())
414 }
415
416 #[tokio::test]
417 async fn can_compute_consistent_digests_for_range() {
418 let immutable_range = 1..=1;
419 let cardano_db = db_builder("can_compute_digests_for_range_consistently")
420 .with_immutables(&immutable_range.clone().collect::<Vec<ImmutableFileNumber>>())
421 .append_immutable_trio()
422 .build();
423 let logger = TestLogger::stdout();
424 let digester = CardanoImmutableDigester::new(
425 "devnet".to_string(),
426 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
427 logger.clone(),
428 );
429
430 let result = digester
431 .compute_digests_for_range(cardano_db.get_immutable_dir(), &immutable_range)
432 .await
433 .expect("compute_digests_for_range must not fail");
434
435 assert_eq!(
436 BTreeMap::from([
437 (
438 ImmutableFile {
439 path: cardano_db.get_immutable_dir().join("00001.chunk"),
440 number: 1,
441 filename: "00001.chunk".to_string()
442 },
443 "faebbf47077f68ef57219396ff69edc738978a3eca946ac7df1983dbf11364ec".to_string()
444 ),
445 (
446 ImmutableFile {
447 path: cardano_db.get_immutable_dir().join("00001.primary"),
448 number: 1,
449 filename: "00001.primary".to_string()
450 },
451 "f11bdb991fc7e72970be7d7f666e10333f92c14326d796fed8c2c041675fa826".to_string()
452 ),
453 (
454 ImmutableFile {
455 path: cardano_db.get_immutable_dir().join("00001.secondary"),
456 number: 1,
457 filename: "00001.secondary".to_string()
458 },
459 "b139684b968fa12ce324cce464d000de0e2c2ded0fd3e473a666410821d3fde3".to_string()
460 )
461 ]),
462 result.entries
463 );
464 }
465
466 #[tokio::test]
467 async fn compute_digest_store_digests_into_cache_provider() {
468 let cardano_db = db_builder("compute_digest_store_digests_into_cache_provider")
469 .with_immutables(&[1, 2])
470 .append_immutable_trio()
471 .build();
472 let immutables = cardano_db.get_immutable_files().clone();
473 let cache = Arc::new(MemoryImmutableFileDigestCacheProvider::default());
474 let logger = TestLogger::stdout();
475 let digester = CardanoImmutableDigester::new(
476 "devnet".to_string(),
477 Some(cache.clone()),
478 logger.clone(),
479 );
480 let beacon = CardanoDbBeacon::new(1, 2);
481
482 digester
483 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
484 .await
485 .expect("compute_digest must not fail");
486
487 let cached_entries = cache
488 .get(immutables.clone())
489 .await
490 .expect("Cache read should not fail");
491 let expected: BTreeMap<_, _> = immutables
492 .into_iter()
493 .map(|i| {
494 let digest = hex::encode(i.compute_raw_hash::<Sha256>().unwrap());
495 (i, Some(digest))
496 })
497 .collect();
498
499 assert_eq!(expected, cached_entries);
500 }
501
502 #[tokio::test]
503 async fn compute_merkle_tree_store_digests_into_cache_provider() {
504 let cardano_db = db_builder("compute_merkle_tree_store_digests_into_cache_provider")
505 .with_immutables(&[1, 2])
506 .append_immutable_trio()
507 .build();
508 let immutables = cardano_db.get_immutable_files().clone();
509 let cache = Arc::new(MemoryImmutableFileDigestCacheProvider::default());
510 let logger = TestLogger::stdout();
511 let digester = CardanoImmutableDigester::new(
512 "devnet".to_string(),
513 Some(cache.clone()),
514 logger.clone(),
515 );
516 let beacon = CardanoDbBeacon::new(1, 2);
517
518 digester
519 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
520 .await
521 .expect("compute_digest must not fail");
522
523 let cached_entries = cache
524 .get(immutables.clone())
525 .await
526 .expect("Cache read should not fail");
527 let expected: BTreeMap<_, _> = immutables
528 .into_iter()
529 .map(|i| {
530 let digest = hex::encode(i.compute_raw_hash::<Sha256>().unwrap());
531 (i, Some(digest))
532 })
533 .collect();
534
535 assert_eq!(expected, cached_entries);
536 }
537
538 #[tokio::test]
539 async fn compute_digests_for_range_stores_digests_into_cache_provider() {
540 let cardano_db = db_builder("compute_digests_for_range_stores_digests_into_cache_provider")
541 .with_immutables(&[1, 2])
542 .append_immutable_trio()
543 .build();
544 let immutables = cardano_db.get_immutable_files().clone();
545 let cache = Arc::new(MemoryImmutableFileDigestCacheProvider::default());
546 let logger = TestLogger::stdout();
547 let digester = CardanoImmutableDigester::new(
548 "devnet".to_string(),
549 Some(cache.clone()),
550 logger.clone(),
551 );
552 let immutable_range = 1..=2;
553
554 digester
555 .compute_digests_for_range(cardano_db.get_immutable_dir(), &immutable_range)
556 .await
557 .expect("compute_digests_for_range must not fail");
558
559 let cached_entries = cache
560 .get(immutables.clone())
561 .await
562 .expect("Cache read should not fail");
563 let expected: BTreeMap<_, _> = immutables
564 .into_iter()
565 .filter(|i| immutable_range.contains(&i.number))
566 .map(|i| {
567 let digest = hex::encode(i.compute_raw_hash::<Sha256>().unwrap());
568 (i.to_owned(), Some(digest))
569 })
570 .collect();
571
572 assert_eq!(expected, cached_entries);
573 }
574
575 #[tokio::test]
576 async fn computed_digest_with_cold_or_hot_or_without_any_cache_are_equals() {
577 let cardano_db = DummyCardanoDbBuilder::new(
578 "computed_digest_with_cold_or_hot_or_without_any_cache_are_equals",
579 )
580 .with_immutables(&[1, 2, 3])
581 .append_immutable_trio()
582 .build();
583 let logger = TestLogger::stdout();
584 let no_cache_digester =
585 CardanoImmutableDigester::new("devnet".to_string(), None, logger.clone());
586 let cache_digester = CardanoImmutableDigester::new(
587 "devnet".to_string(),
588 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
589 logger.clone(),
590 );
591 let beacon = CardanoDbBeacon::new(1, 3);
592
593 let without_cache_digest = no_cache_digester
594 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
595 .await
596 .expect("compute_digest must not fail");
597
598 let cold_cache_digest = cache_digester
599 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
600 .await
601 .expect("compute_digest must not fail");
602
603 let full_cache_digest = cache_digester
604 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
605 .await
606 .expect("compute_digest must not fail");
607
608 assert_eq!(
609 without_cache_digest, full_cache_digest,
610 "Digests with or without cache should be the same"
611 );
612
613 assert_eq!(
614 cold_cache_digest, full_cache_digest,
615 "Digests with cold or with hot cache should be the same"
616 );
617 }
618
619 #[tokio::test]
620 async fn computed_merkle_tree_with_cold_or_hot_or_without_any_cache_are_equals() {
621 let cardano_db = DummyCardanoDbBuilder::new(
622 "computed_merkle_tree_with_cold_or_hot_or_without_any_cache_are_equals",
623 )
624 .with_immutables(&[1, 2, 3])
625 .append_immutable_trio()
626 .build();
627 let logger = TestLogger::stdout();
628 let no_cache_digester =
629 CardanoImmutableDigester::new("devnet".to_string(), None, logger.clone());
630 let cache_digester = CardanoImmutableDigester::new(
631 "devnet".to_string(),
632 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
633 logger.clone(),
634 );
635 let beacon = CardanoDbBeacon::new(1, 3);
636
637 let without_cache_digest = no_cache_digester
638 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
639 .await
640 .expect("compute_merkle_tree must not fail");
641
642 let cold_cache_digest = cache_digester
643 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
644 .await
645 .expect("compute_merkle_tree must not fail");
646
647 let full_cache_digest = cache_digester
648 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
649 .await
650 .expect("compute_merkle_tree must not fail");
651
652 let without_cache_merkle_root = without_cache_digest.compute_root().unwrap();
653 let cold_cache_merkle_root = cold_cache_digest.compute_root().unwrap();
654 let full_cache_merkle_root = full_cache_digest.compute_root().unwrap();
655 assert_eq!(
656 without_cache_merkle_root, full_cache_merkle_root,
657 "Merkle roots with or without cache should be the same"
658 );
659
660 assert_eq!(
661 cold_cache_merkle_root, full_cache_merkle_root,
662 "Merkle roots with cold or with hot cache should be the same"
663 );
664 }
665
666 #[tokio::test]
667 async fn computed_digests_for_range_with_cold_or_hot_or_without_any_cache_are_equals() {
668 let cardano_db = DummyCardanoDbBuilder::new(
669 "computed_digests_for_range_with_cold_or_hot_or_without_any_cache_are_equals",
670 )
671 .with_immutables(&[1, 2, 3])
672 .append_immutable_trio()
673 .build();
674 let logger = TestLogger::stdout();
675 let no_cache_digester =
676 CardanoImmutableDigester::new("devnet".to_string(), None, logger.clone());
677 let cache_digester = CardanoImmutableDigester::new(
678 "devnet".to_string(),
679 Some(Arc::new(MemoryImmutableFileDigestCacheProvider::default())),
680 logger.clone(),
681 );
682 let immutable_range = 1..=3;
683
684 let without_cache_digests = no_cache_digester
685 .compute_digests_for_range(cardano_db.get_immutable_dir(), &immutable_range)
686 .await
687 .expect("compute_digests_for_range must not fail");
688
689 let cold_cache_digests = cache_digester
690 .compute_digests_for_range(cardano_db.get_immutable_dir(), &immutable_range)
691 .await
692 .expect("compute_digests_for_range must not fail");
693
694 let full_cache_digests = cache_digester
695 .compute_digests_for_range(cardano_db.get_immutable_dir(), &immutable_range)
696 .await
697 .expect("compute_digests_for_range must not fail");
698
699 let without_cache_entries = without_cache_digests.entries;
700 let cold_cache_entries = cold_cache_digests.entries;
701 let full_cache_entries = full_cache_digests.entries;
702 assert_eq!(
703 without_cache_entries, full_cache_entries,
704 "Digests for range with or without cache should be the same"
705 );
706
707 assert_eq!(
708 cold_cache_entries, full_cache_entries,
709 "Digests for range with cold or with hot cache should be the same"
710 );
711 }
712
713 #[tokio::test]
714 async fn hash_computation_is_quicker_with_a_full_cache() {
715 let cardano_db = db_builder("hash_computation_is_quicker_with_a_full_cache")
716 .with_immutables(&(1..=50).collect::<Vec<ImmutableFileNumber>>())
717 .append_immutable_trio()
718 .set_immutable_trio_file_size(65538)
719 .build();
720 let cache = MemoryImmutableFileDigestCacheProvider::default();
721 let logger = TestLogger::stdout();
722 let digester = CardanoImmutableDigester::new(
723 "devnet".to_string(),
724 Some(Arc::new(cache)),
725 logger.clone(),
726 );
727 let beacon = CardanoDbBeacon::new(1, 50);
728
729 let now = Instant::now();
730 digester
731 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
732 .await
733 .expect("compute_digest must not fail");
734 let elapsed_without_cache = now.elapsed();
735
736 let now = Instant::now();
737 digester
738 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
739 .await
740 .expect("compute_digest must not fail");
741 let elapsed_with_cache = now.elapsed();
742
743 assert!(
748 elapsed_with_cache < (elapsed_without_cache * 9 / 10),
749 "digest computation with full cache should be faster than without cache,\
750 time elapsed: with cache {elapsed_with_cache:?}, without cache {elapsed_without_cache:?}"
751 );
752 }
753
754 #[tokio::test]
755 async fn cache_read_failure_dont_block_computations() {
756 let cardano_db = db_builder("cache_read_failure_dont_block_computation")
757 .with_immutables(&[1, 2, 3])
758 .append_immutable_trio()
759 .build();
760 let mut cache = MockImmutableFileDigestCacheProvider::new();
761 cache.expect_get().returning(|_| Ok(BTreeMap::new()));
762 cache.expect_store().returning(|_| {
763 Err(ImmutableDigesterCacheProviderError::Store(
764 ImmutableDigesterCacheStoreError::Io(io::Error::other("error")),
765 ))
766 });
767 let logger = TestLogger::stdout();
768 let digester = CardanoImmutableDigester::new(
769 "devnet".to_string(),
770 Some(Arc::new(cache)),
771 logger.clone(),
772 );
773 let beacon = CardanoDbBeacon::new(1, 3);
774
775 digester
776 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
777 .await
778 .expect("compute_digest must not fail even with cache write failure");
779
780 digester
781 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
782 .await
783 .expect("compute_merkle_tree must not fail even with cache write failure");
784 }
785
786 #[tokio::test]
787 async fn cache_write_failure_dont_block_computation() {
788 let cardano_db = db_builder("cache_write_failure_dont_block_computation")
789 .with_immutables(&[1, 2, 3])
790 .append_immutable_trio()
791 .build();
792 let mut cache = MockImmutableFileDigestCacheProvider::new();
793 cache.expect_get().returning(|_| {
794 Err(ImmutableDigesterCacheProviderError::Get(
795 ImmutableDigesterCacheGetError::Io(io::Error::other("error")),
796 ))
797 });
798 cache.expect_store().returning(|_| Ok(()));
799 let logger = TestLogger::stdout();
800 let digester = CardanoImmutableDigester::new(
801 "devnet".to_string(),
802 Some(Arc::new(cache)),
803 logger.clone(),
804 );
805 let beacon = CardanoDbBeacon::new(1, 3);
806
807 digester
808 .compute_digest(cardano_db.get_immutable_dir(), &beacon)
809 .await
810 .expect("compute_digest must not fail even with cache read failure");
811
812 digester
813 .compute_merkle_tree(cardano_db.get_immutable_dir(), &beacon)
814 .await
815 .expect("compute_merkle_tree must not fail even with cache read failure");
816 }
817}