mithril_common/digesters/
cardano_immutable_digester.rs

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