mithril_cardano_node_internal_database/digesters/
cardano_immutable_digester.rs

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
19/// A digester working directly on a Cardano DB immutables files
20pub struct CardanoImmutableDigester {
21    cardano_network: String,
22
23    /// A [ImmutableFileDigestCacheProvider] instance
24    cache_provider: Option<Arc<dyn ImmutableFileDigestCacheProvider>>,
25
26    /// The logger where the logs should be written
27    logger: Logger,
28}
29
30impl CardanoImmutableDigester {
31    /// ImmutableDigester factory
32    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        // The computation of immutable files digests is done in a separate thread because it is blocking the whole task
51        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        // Note real performance doesn't matter here, the purpose is only to check that the computation
744        // time is faster with cache.
745        // We set the limit to 90% to avoid flakiness and ensure that the cache is useful (Note: Real
746        // performance is around ~100 times faster in debug).
747        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}