mithril_cardano_node_internal_database/digesters/
cardano_immutable_digester.rs

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