mithril_client/cardano_database_client/
proving.rs

1use std::{
2    collections::BTreeMap,
3    fmt, fs,
4    ops::RangeInclusive,
5    path::{Path, PathBuf},
6    sync::Arc,
7};
8
9use anyhow::{Context, anyhow};
10use slog::warn;
11use thiserror::Error;
12
13use mithril_cardano_node_internal_database::{
14    IMMUTABLE_DIR,
15    digesters::{CardanoImmutableDigester, ImmutableDigester, ImmutableDigesterError},
16    entities::ImmutableFile,
17};
18use mithril_common::{
19    crypto_helper::{MKProof, MKTree, MKTreeNode, MKTreeStoreInMemory},
20    entities::{
21        DigestLocation, HexEncodedDigest, ImmutableFileName, ImmutableFileNumber,
22        ProtocolMessagePartKey,
23    },
24    messages::{
25        CardanoDatabaseDigestListItemMessage, CardanoDatabaseSnapshotMessage, CertificateMessage,
26        DigestsMessagePart,
27    },
28};
29
30use crate::{
31    MithrilError, MithrilResult,
32    cardano_database_client::ImmutableFileRange,
33    feedback::MithrilEvent,
34    file_downloader::{DownloadEvent, FileDownloader, FileDownloaderUri},
35    utils::{
36        TempDirectoryProvider, create_directory_if_not_exists, delete_directory,
37        read_files_in_directory,
38    },
39};
40
41/// Represents the verified digests and the Merkle tree built from them.
42pub struct VerifiedDigests {
43    /// A map of immutable file names to their corresponding verified digests.
44    pub digests: BTreeMap<ImmutableFileName, HexEncodedDigest>,
45    /// The Merkle tree built from the digests.
46    pub merkle_tree: MKTree<MKTreeStoreInMemory>,
47}
48
49const MERKLE_PROOF_COMPUTATION_ERROR: &str = "Merkle proof computation failed";
50
51/// Type containing the lists of immutable files that are missing or tampered.
52#[derive(Debug, PartialEq)]
53pub struct ImmutableVerificationResult {
54    /// The immutables files directory.
55    pub immutables_dir: PathBuf,
56    /// List of missing immutable files.
57    pub missing: Vec<ImmutableFileName>,
58    /// List of tampered immutable files.
59    pub tampered: Vec<ImmutableFileName>,
60    /// List of non-verifiable immutable files.
61    pub non_verifiable: Vec<ImmutableFileName>,
62}
63
64/// Cardano database Verification related errors.
65#[derive(Error, Debug)]
66pub enum CardanoDatabaseVerificationError {
67    /// Error related to the verification of immutable files.
68    ImmutableFilesVerification(ImmutableVerificationResult),
69
70    /// Error related to the immutable files digests computation.
71    DigestsComputation(#[from] ImmutableDigesterError),
72
73    /// Error related to the Merkle proof verification.
74    MerkleProofVerification(#[source] MithrilError),
75
76    /// Error related to the immutable files range.
77    ImmutableFilesRangeCreation(#[source] MithrilError),
78}
79
80impl fmt::Display for CardanoDatabaseVerificationError {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        match self {
83            CardanoDatabaseVerificationError::ImmutableFilesVerification(lists) => {
84                fn get_first_10_files_path(
85                    files: &[ImmutableFileName],
86                    immutables_dir: &Path,
87                ) -> String {
88                    files
89                        .iter()
90                        .take(10)
91                        .map(|file| immutables_dir.join(file).to_string_lossy().to_string())
92                        .collect::<Vec<_>>()
93                        .join("\n")
94                }
95
96                if !lists.missing.is_empty() {
97                    let missing_files_subset =
98                        get_first_10_files_path(&lists.missing, &lists.immutables_dir);
99                    writeln!(
100                        f,
101                        "Number of missing immutable files: {}",
102                        lists.missing.len()
103                    )?;
104                    writeln!(f, "First 10 missing immutable files paths:")?;
105                    writeln!(f, "{missing_files_subset}")?;
106                }
107                if !lists.missing.is_empty() && !lists.tampered.is_empty() {
108                    writeln!(f)?;
109                }
110                if !lists.tampered.is_empty() {
111                    let tampered_files_subset =
112                        get_first_10_files_path(&lists.tampered, &lists.immutables_dir);
113                    writeln!(
114                        f,
115                        "Number of tampered immutable files: {}",
116                        lists.tampered.len()
117                    )?;
118                    writeln!(f, "First 10 tampered immutable files paths:")?;
119                    writeln!(f, "{tampered_files_subset}")?;
120                }
121                if (!lists.missing.is_empty() || !lists.tampered.is_empty())
122                    && !lists.non_verifiable.is_empty()
123                {
124                    writeln!(f)?;
125                }
126                if !lists.non_verifiable.is_empty() {
127                    let non_verifiable_files_subset =
128                        get_first_10_files_path(&lists.non_verifiable, &lists.immutables_dir);
129                    writeln!(
130                        f,
131                        "Number of non verifiable immutable files: {}",
132                        lists.non_verifiable.len()
133                    )?;
134                    writeln!(f, "First 10 non verifiable immutable files paths:")?;
135                    writeln!(f, "{non_verifiable_files_subset}")?;
136                }
137                Ok(())
138            }
139            CardanoDatabaseVerificationError::DigestsComputation(e) => {
140                write!(f, "Immutable files digester error: {e:?}")
141            }
142            CardanoDatabaseVerificationError::MerkleProofVerification(e) => {
143                write!(f, "Merkle proof verification error: {e:?}")
144            }
145            CardanoDatabaseVerificationError::ImmutableFilesRangeCreation(e) => {
146                write!(f, "Immutable files range error: {e:?}")
147            }
148        }
149    }
150}
151
152/// Represents the immutable files that were not verified during the digest verification process.
153#[derive(PartialEq, Debug)]
154pub(crate) struct ImmutableFilesNotVerified {
155    /// List of immutable files that were tampered (i.e. their digest does not match the verified digest)
156    pub tampered_files: Vec<ImmutableFileName>,
157    /// List of immutable files that could not be verified (i.e., not present in the digests)
158    pub non_verifiable_files: Vec<ImmutableFileName>,
159}
160
161impl VerifiedDigests {
162    pub(crate) fn list_immutable_files_not_verified(
163        &self,
164        computed_digests: &BTreeMap<ImmutableFile, HexEncodedDigest>,
165    ) -> ImmutableFilesNotVerified {
166        let mut tampered_files = vec![];
167        let mut non_verifiable_files = vec![];
168
169        for (immutable_file, digest) in computed_digests.iter() {
170            let immutable_file_name_to_verify = immutable_file.filename.clone();
171            match self.digests.get(&immutable_file_name_to_verify) {
172                Some(verified_digest) if verified_digest != digest => {
173                    tampered_files.push(immutable_file_name_to_verify);
174                }
175                None => {
176                    non_verifiable_files.push(immutable_file_name_to_verify);
177                }
178                _ => {}
179            }
180        }
181
182        ImmutableFilesNotVerified {
183            tampered_files,
184            non_verifiable_files,
185        }
186    }
187}
188
189pub struct InternalArtifactProver {
190    http_file_downloader: Arc<dyn FileDownloader>,
191    temp_directory_provider: Arc<dyn TempDirectoryProvider>,
192    logger: slog::Logger,
193}
194
195impl InternalArtifactProver {
196    /// Constructs a new `InternalArtifactProver`.
197    pub fn new(
198        http_file_downloader: Arc<dyn FileDownloader>,
199        temp_directory_provider: Arc<dyn TempDirectoryProvider>,
200        logger: slog::Logger,
201    ) -> Self {
202        Self {
203            http_file_downloader,
204            temp_directory_provider,
205            logger,
206        }
207    }
208
209    fn check_merkle_root_is_signed_by_certificate(
210        certificate: &CertificateMessage,
211        merkle_root: &MKTreeNode,
212    ) -> MithrilResult<()> {
213        let mut message = certificate.protocol_message.clone();
214        message.set_message_part(
215            ProtocolMessagePartKey::CardanoDatabaseMerkleRoot,
216            merkle_root.to_hex(),
217        );
218
219        if !certificate.match_message(&message) {
220            return Err(anyhow!(
221                "Certificate message does not match the computed message for certificate {}",
222                certificate.hash
223            ));
224        }
225
226        Ok(())
227    }
228
229    /// Download digests and verify its authenticity against the certificate.
230    pub async fn download_and_verify_digests(
231        &self,
232        certificate: &CertificateMessage,
233        cardano_database_snapshot: &CardanoDatabaseSnapshotMessage,
234    ) -> MithrilResult<VerifiedDigests> {
235        let digest_target_dir = self.digest_target_dir();
236        delete_directory(&digest_target_dir)?;
237        self.download_unpack_digest_file(&cardano_database_snapshot.digests, &digest_target_dir)
238            .await?;
239        let last_immutable_file_number = cardano_database_snapshot.beacon.immutable_file_number;
240
241        let downloaded_digests = self.read_digest_file(&digest_target_dir)?;
242        delete_directory(&digest_target_dir)?;
243
244        let filtered_digests = downloaded_digests
245            .clone()
246            .into_iter()
247            .filter(|(immutable_file_name, _)| {
248                match ImmutableFile::new(Path::new(immutable_file_name).to_path_buf()) {
249                    Ok(immutable_file) => immutable_file.number <= last_immutable_file_number,
250                    Err(_) => false,
251                }
252            })
253            .collect::<BTreeMap<_, _>>();
254
255        let filtered_digests_values = filtered_digests.values().collect::<Vec<_>>();
256        let merkle_tree: MKTree<MKTreeStoreInMemory> = MKTree::new(&filtered_digests_values)?;
257
258        Self::check_merkle_root_is_signed_by_certificate(
259            certificate,
260            &merkle_tree.compute_root()?,
261        )?;
262
263        Ok(VerifiedDigests {
264            digests: filtered_digests,
265            merkle_tree,
266        })
267    }
268
269    fn immutable_dir(db_dir: &Path) -> PathBuf {
270        db_dir.join(IMMUTABLE_DIR)
271    }
272
273    fn list_missing_immutable_files(
274        database_dir: &Path,
275        immutable_file_number_range: &RangeInclusive<ImmutableFileNumber>,
276    ) -> Vec<ImmutableFileName> {
277        let immutable_dir = Self::immutable_dir(database_dir);
278        let mut missing_files = Vec::new();
279
280        for immutable_file_number in immutable_file_number_range.clone() {
281            for immutable_type in ["chunk", "primary", "secondary"] {
282                let file_name = format!("{immutable_file_number:05}.{immutable_type}");
283                if !immutable_dir.join(&file_name).exists() {
284                    missing_files.push(ImmutableFileName::from(file_name));
285                }
286            }
287        }
288
289        missing_files
290    }
291
292    pub async fn verify_cardano_database(
293        &self,
294        certificate: &CertificateMessage,
295        cardano_database_snapshot: &CardanoDatabaseSnapshotMessage,
296        immutable_file_range: &ImmutableFileRange,
297        allow_missing: bool,
298        database_dir: &Path,
299        verified_digests: &VerifiedDigests,
300    ) -> Result<MKProof, CardanoDatabaseVerificationError> {
301        let network = certificate.metadata.network.clone();
302        let immutable_file_number_range = immutable_file_range
303            .to_range_inclusive(cardano_database_snapshot.beacon.immutable_file_number)
304            .map_err(CardanoDatabaseVerificationError::ImmutableFilesRangeCreation)?;
305        let missing_immutable_files = if allow_missing {
306            vec![]
307        } else {
308            Self::list_missing_immutable_files(database_dir, &immutable_file_number_range)
309        };
310        let immutable_digester = CardanoImmutableDigester::new(network, None, self.logger.clone());
311        let computed_digest_entries = immutable_digester
312            .compute_digests_for_range(database_dir, &immutable_file_number_range)
313            .await?
314            .entries;
315        let computed_digests = computed_digest_entries
316            .values()
317            .map(MKTreeNode::from)
318            .collect::<Vec<_>>();
319
320        let proof_result = verified_digests.merkle_tree.compute_proof(&computed_digests);
321        if let Ok(ref merkle_proof) = proof_result
322            && missing_immutable_files.is_empty()
323        {
324            merkle_proof
325                .verify()
326                .map_err(CardanoDatabaseVerificationError::MerkleProofVerification)?;
327
328            return Ok(merkle_proof.clone());
329        }
330
331        let (tampered, non_verifiable) = match proof_result {
332            Err(e) => {
333                warn!(self.logger, "{MERKLE_PROOF_COMPUTATION_ERROR}: {e:}");
334                let verified_digests =
335                    verified_digests.list_immutable_files_not_verified(&computed_digest_entries);
336
337                (
338                    verified_digests.tampered_files,
339                    verified_digests.non_verifiable_files,
340                )
341            }
342            Ok(_) => (vec![], vec![]),
343        };
344        Err(
345            CardanoDatabaseVerificationError::ImmutableFilesVerification(
346                ImmutableVerificationResult {
347                    immutables_dir: Self::immutable_dir(database_dir),
348                    missing: missing_immutable_files,
349                    tampered,
350                    non_verifiable,
351                },
352            ),
353        )
354    }
355
356    async fn download_unpack_digest_file(
357        &self,
358        digests_locations: &DigestsMessagePart,
359        digests_file_target_dir: &Path,
360    ) -> MithrilResult<()> {
361        create_directory_if_not_exists(digests_file_target_dir)?;
362        let mut locations_sorted = digests_locations.sanitized_locations()?;
363        locations_sorted.sort();
364        for location in locations_sorted {
365            let download_id = MithrilEvent::new_cardano_database_download_id();
366            let (file_downloader, compression_algorithm) = match &location {
367                DigestLocation::CloudStorage {
368                    uri: _,
369                    compression_algorithm,
370                } => (self.http_file_downloader.clone(), *compression_algorithm),
371                DigestLocation::Aggregator { .. } => (self.http_file_downloader.clone(), None),
372                // Note: unknown locations should have been filtered out by `sanitized_locations`
373                DigestLocation::Unknown => unreachable!(),
374            };
375            let file_downloader_uri: FileDownloaderUri = location.try_into()?;
376            let downloaded = file_downloader
377                .download_unpack(
378                    &file_downloader_uri,
379                    digests_locations.size_uncompressed,
380                    digests_file_target_dir,
381                    compression_algorithm,
382                    DownloadEvent::Digest {
383                        download_id: download_id.clone(),
384                    },
385                )
386                .await;
387            match downloaded {
388                Ok(_) => {
389                    return Ok(());
390                }
391                Err(e) => {
392                    slog::error!(
393                        self.logger,
394                        "Failed downloading and unpacking digest for location {file_downloader_uri:?}"; "error" => ?e
395                    );
396                }
397            }
398        }
399
400        Err(anyhow!(
401            "Failed downloading and unpacking digests for all locations"
402        ))
403    }
404
405    fn read_digest_file(
406        &self,
407        digest_file_target_dir: &Path,
408    ) -> MithrilResult<BTreeMap<ImmutableFileName, HexEncodedDigest>> {
409        let digest_files = read_files_in_directory(digest_file_target_dir)?;
410        if digest_files.len() > 1 {
411            return Err(anyhow!(
412                "Multiple digest files found in directory: {digest_file_target_dir:?}"
413            ));
414        }
415        if digest_files.is_empty() {
416            return Err(anyhow!(
417                "No digest file found in directory: {digest_file_target_dir:?}"
418            ));
419        }
420
421        let digest_file = &digest_files[0];
422        let content = fs::read_to_string(digest_file)
423            .with_context(|| format!("Failed reading digest file: {digest_file:?}"))?;
424        let digest_messages: Vec<CardanoDatabaseDigestListItemMessage> =
425            serde_json::from_str(&content)
426                .with_context(|| format!("Failed deserializing digest file: {digest_file:?}"))?;
427        let digest_map = digest_messages
428            .into_iter()
429            .map(|message| (message.immutable_file_name, message.digest))
430            .collect::<BTreeMap<_, _>>();
431
432        Ok(digest_map)
433    }
434
435    fn digest_target_dir(&self) -> PathBuf {
436        self.temp_directory_provider.temp_dir()
437    }
438}
439
440#[cfg(test)]
441mod tests {
442    use std::collections::BTreeMap;
443    use std::fs;
444    use std::io::Write;
445    use std::path::Path;
446    use std::sync::Arc;
447
448    use mithril_common::{
449        current_function,
450        entities::{CardanoDbBeacon, Epoch, HexEncodedDigest},
451        messages::CardanoDatabaseDigestListItemMessage,
452        test::{TempDir, double::Dummy},
453    };
454
455    use crate::{
456        cardano_database_client::CardanoDatabaseClientDependencyInjector,
457        file_downloader::MockFileDownloaderBuilder, test_utils::TestLogger,
458        utils::TimestampTempDirectoryProvider,
459    };
460
461    use super::*;
462
463    fn remove_immutable_files<T: AsRef<Path>>(database_dir: &Path, immutable_file_names: &[T]) {
464        for immutable_file_name in immutable_file_names {
465            let immutable_file_path = InternalArtifactProver::immutable_dir(database_dir)
466                .join(immutable_file_name.as_ref());
467            std::fs::remove_file(immutable_file_path).unwrap();
468        }
469    }
470
471    fn tamper_immutable_files<T: AsRef<Path>>(database_dir: &Path, immutable_file_names: &[T]) {
472        for immutable_file_name in immutable_file_names {
473            let immutable_file_path = InternalArtifactProver::immutable_dir(database_dir)
474                .join(immutable_file_name.as_ref());
475            std::fs::write(immutable_file_path, "tampered content").unwrap();
476        }
477    }
478
479    mod list_immutable_files_not_verified {
480
481        use super::*;
482
483        fn fake_immutable(filename: &str) -> ImmutableFile {
484            ImmutableFile {
485                path: PathBuf::from("whatever"),
486                number: 1,
487                filename: filename.to_string(),
488            }
489        }
490
491        #[test]
492        fn should_return_empty_list_when_no_tampered_files() {
493            let digests_to_verify = BTreeMap::from([
494                (fake_immutable("00001.chunk"), "digest-1".to_string()),
495                (fake_immutable("00002.chunk"), "digest-2".to_string()),
496            ]);
497
498            let verified_digests = VerifiedDigests {
499                digests: BTreeMap::from([
500                    ("00001.chunk".to_string(), "digest-1".to_string()),
501                    ("00002.chunk".to_string(), "digest-2".to_string()),
502                ]),
503                merkle_tree: MKTree::new(&["whatever"]).unwrap(),
504            };
505
506            let invalid_files =
507                verified_digests.list_immutable_files_not_verified(&digests_to_verify);
508
509            assert_eq!(
510                invalid_files,
511                ImmutableFilesNotVerified {
512                    tampered_files: vec![],
513                    non_verifiable_files: vec![],
514                }
515            );
516        }
517
518        #[test]
519        fn should_return_list_with_tampered_files() {
520            let digests_to_verify = BTreeMap::from([
521                (fake_immutable("00001.chunk"), "digest-1".to_string()),
522                (fake_immutable("00002.chunk"), "digest-2".to_string()),
523            ]);
524
525            let verified_digests = VerifiedDigests {
526                digests: BTreeMap::from([
527                    ("00001.chunk".to_string(), "digest-1".to_string()),
528                    ("00002.chunk".to_string(), "INVALID".to_string()),
529                ]),
530                merkle_tree: MKTree::new(&["whatever"]).unwrap(),
531            };
532
533            let invalid_files =
534                verified_digests.list_immutable_files_not_verified(&digests_to_verify);
535
536            assert_eq!(
537                invalid_files,
538                ImmutableFilesNotVerified {
539                    tampered_files: vec!["00002.chunk".to_string()],
540                    non_verifiable_files: vec![],
541                }
542            );
543        }
544
545        #[test]
546        fn should_return_list_with_non_verifiable() {
547            let digests_to_verify = BTreeMap::from([
548                (fake_immutable("00001.chunk"), "digest-1".to_string()),
549                (
550                    fake_immutable("00002.not.verifiable"),
551                    "digest-2".to_string(),
552                ),
553            ]);
554
555            let verified_digests = VerifiedDigests {
556                digests: BTreeMap::from([("00001.chunk".to_string(), "digest-1".to_string())]),
557                merkle_tree: MKTree::new(&["whatever"]).unwrap(),
558            };
559
560            let invalid_files =
561                verified_digests.list_immutable_files_not_verified(&digests_to_verify);
562
563            assert_eq!(
564                invalid_files,
565                ImmutableFilesNotVerified {
566                    tampered_files: vec![],
567                    non_verifiable_files: vec!["00002.not.verifiable".to_string()],
568                }
569            );
570        }
571    }
572
573    mod download_and_verify_digests {
574        use mithril_common::{
575            StdResult, current_function,
576            entities::{ProtocolMessage, ProtocolMessagePartKey},
577            messages::DigestsMessagePart,
578        };
579
580        use crate::utils::TimestampTempDirectoryProvider;
581
582        use super::*;
583
584        fn write_digest_file(
585            digest_dir: &Path,
586            digests: &BTreeMap<ImmutableFile, HexEncodedDigest>,
587        ) -> StdResult<()> {
588            let digest_file_path = digest_dir.join("digests.json");
589            if !digest_dir.exists() {
590                fs::create_dir_all(digest_dir).unwrap();
591            }
592
593            let immutable_digest_messages = digests
594                .iter()
595                .map(
596                    |(immutable_file, digest)| CardanoDatabaseDigestListItemMessage {
597                        immutable_file_name: immutable_file.filename.clone(),
598                        digest: digest.to_string(),
599                    },
600                )
601                .collect::<Vec<_>>();
602            serde_json::to_writer(
603                fs::File::create(digest_file_path).unwrap(),
604                &immutable_digest_messages,
605            )?;
606
607            Ok(())
608        }
609
610        fn build_digests_map(size: usize) -> BTreeMap<ImmutableFile, HexEncodedDigest> {
611            let mut digests = BTreeMap::new();
612            for i in 1..=size {
613                for name in ["chunk", "primary", "secondary"] {
614                    let immutable_file_name = format!("{i:05}.{name}");
615                    let immutable_file =
616                        ImmutableFile::new(PathBuf::from(immutable_file_name)).unwrap();
617                    let digest = format!("digest-{i}-{name}");
618                    digests.insert(immutable_file, digest);
619                }
620            }
621
622            digests
623        }
624
625        #[tokio::test]
626        async fn download_and_verify_digest_should_return_digest_map_according_to_beacon() {
627            let beacon = CardanoDbBeacon {
628                epoch: Epoch(123),
629                immutable_file_number: 42,
630            };
631            let hightest_immutable_number_in_digest_file =
632                123 + beacon.immutable_file_number as usize;
633            let digests_in_certificate_map =
634                build_digests_map(beacon.immutable_file_number as usize);
635            let protocol_message_merkle_root = {
636                let digests_in_certificate_values =
637                    digests_in_certificate_map.values().cloned().collect::<Vec<_>>();
638                let certificate_merkle_tree: MKTree<MKTreeStoreInMemory> =
639                    MKTree::new(&digests_in_certificate_values).unwrap();
640
641                certificate_merkle_tree.compute_root().unwrap().to_hex()
642            };
643            let mut protocol_message = ProtocolMessage::new();
644            protocol_message.set_message_part(
645                ProtocolMessagePartKey::CardanoDatabaseMerkleRoot,
646                protocol_message_merkle_root,
647            );
648            let certificate = CertificateMessage {
649                protocol_message: protocol_message.clone(),
650                signed_message: protocol_message.compute_hash(),
651                ..CertificateMessage::dummy()
652            };
653
654            let digests_location = "http://whatever/digests.json";
655            let cardano_database_snapshot = CardanoDatabaseSnapshotMessage {
656                beacon,
657                digests: DigestsMessagePart {
658                    size_uncompressed: 1024,
659                    locations: vec![DigestLocation::CloudStorage {
660                        uri: digests_location.to_string(),
661                        compression_algorithm: None,
662                    }],
663                },
664                ..CardanoDatabaseSnapshotMessage::dummy()
665            };
666            let temp_directory_provider =
667                Arc::new(TimestampTempDirectoryProvider::new(current_function!()));
668            let digest_target_dir = temp_directory_provider.temp_dir();
669            let digest_target_dir_clone = digest_target_dir.clone();
670            let http_file_downloader = Arc::new(
671                MockFileDownloaderBuilder::default()
672                    .with_file_uri(digests_location)
673                    .with_target_dir(digest_target_dir.clone())
674                    .with_compression(None)
675                    .with_returning(Box::new(move |_, _, _, _, _| {
676                        write_digest_file(
677                            &digest_target_dir_clone,
678                            &build_digests_map(hightest_immutable_number_in_digest_file),
679                        )?;
680
681                        Ok(())
682                    }))
683                    .build(),
684            );
685            let client = CardanoDatabaseClientDependencyInjector::new()
686                .with_http_file_downloader(http_file_downloader)
687                .with_temp_directory_provider(temp_directory_provider)
688                .build_cardano_database_client();
689
690            let verified_digests = client
691                .download_and_verify_digests(&certificate, &cardano_database_snapshot)
692                .await
693                .unwrap();
694
695            let expected_digests_in_certificate = digests_in_certificate_map
696                .iter()
697                .map(|(immutable_file, digest)| {
698                    (immutable_file.filename.clone(), digest.to_string())
699                })
700                .collect();
701            assert_eq!(verified_digests.digests, expected_digests_in_certificate);
702
703            assert!(!digest_target_dir.exists());
704        }
705    }
706
707    mod download_unpack_digest_file {
708
709        use mithril_common::entities::CompressionAlgorithm;
710
711        use crate::file_downloader::MockFileDownloader;
712
713        use super::*;
714
715        #[tokio::test]
716        async fn fails_if_no_location_is_retrieved() {
717            let target_dir = Path::new(".");
718            let artifact_prover = InternalArtifactProver::new(
719                Arc::new(
720                    MockFileDownloaderBuilder::default()
721                        .with_compression(None)
722                        .with_failure()
723                        .with_times(2)
724                        .build(),
725                ),
726                Arc::new(TimestampTempDirectoryProvider::new(current_function!())),
727                TestLogger::stdout(),
728            );
729
730            artifact_prover
731                .download_unpack_digest_file(
732                    &DigestsMessagePart {
733                        locations: vec![
734                            DigestLocation::CloudStorage {
735                                uri: "http://whatever-1/digests.json".to_string(),
736                                compression_algorithm: None,
737                            },
738                            DigestLocation::Aggregator {
739                                uri: "http://whatever-2/digest".to_string(),
740                            },
741                        ],
742                        size_uncompressed: 0,
743                    },
744                    target_dir,
745                )
746                .await
747                .expect_err("download_unpack_digest_file should fail");
748        }
749
750        #[tokio::test]
751        async fn fails_if_all_locations_are_unknown() {
752            let target_dir = Path::new(".");
753            let artifact_prover = InternalArtifactProver::new(
754                Arc::new(MockFileDownloader::new()),
755                Arc::new(TimestampTempDirectoryProvider::new(current_function!())),
756                TestLogger::stdout(),
757            );
758
759            artifact_prover
760                .download_unpack_digest_file(
761                    &DigestsMessagePart {
762                        locations: vec![DigestLocation::Unknown],
763                        size_uncompressed: 0,
764                    },
765                    target_dir,
766                )
767                .await
768                .expect_err("download_unpack_digest_file should fail");
769        }
770
771        #[tokio::test]
772        async fn succeeds_if_at_least_one_location_is_retrieved() {
773            let target_dir = Path::new(".");
774            let artifact_prover = InternalArtifactProver::new(
775                Arc::new(
776                    MockFileDownloaderBuilder::default()
777                        .with_compression(None)
778                        .with_failure()
779                        .next_call()
780                        .with_compression(None)
781                        .with_success()
782                        .build(),
783                ),
784                Arc::new(TimestampTempDirectoryProvider::new(current_function!())),
785                TestLogger::stdout(),
786            );
787
788            artifact_prover
789                .download_unpack_digest_file(
790                    &DigestsMessagePart {
791                        locations: vec![
792                            DigestLocation::CloudStorage {
793                                uri: "http://whatever-1/digests.json".to_string(),
794                                compression_algorithm: None,
795                            },
796                            DigestLocation::Aggregator {
797                                uri: "http://whatever-2/digest".to_string(),
798                            },
799                        ],
800                        size_uncompressed: 0,
801                    },
802                    target_dir,
803                )
804                .await
805                .unwrap();
806        }
807
808        #[tokio::test]
809        async fn succeeds_when_first_location_is_retrieved() {
810            let target_dir = Path::new(".");
811            let artifact_prover = InternalArtifactProver::new(
812                Arc::new(
813                    MockFileDownloaderBuilder::default()
814                        .with_compression(None)
815                        .with_times(1)
816                        .with_success()
817                        .build(),
818                ),
819                Arc::new(TimestampTempDirectoryProvider::new(current_function!())),
820                TestLogger::stdout(),
821            );
822
823            artifact_prover
824                .download_unpack_digest_file(
825                    &DigestsMessagePart {
826                        locations: vec![
827                            DigestLocation::CloudStorage {
828                                uri: "http://whatever-1/digests.json".to_string(),
829                                compression_algorithm: None,
830                            },
831                            DigestLocation::Aggregator {
832                                uri: "http://whatever-2/digest".to_string(),
833                            },
834                        ],
835                        size_uncompressed: 0,
836                    },
837                    target_dir,
838                )
839                .await
840                .unwrap();
841        }
842
843        #[tokio::test]
844        async fn should_call_download_with_compression_algorithm() {
845            let target_dir = Path::new(".");
846            let artifact_prover = InternalArtifactProver::new(
847                Arc::new(
848                    MockFileDownloaderBuilder::default()
849                        .with_compression(Some(CompressionAlgorithm::Gzip))
850                        .with_times(1)
851                        .with_success()
852                        .build(),
853                ),
854                Arc::new(TimestampTempDirectoryProvider::new(current_function!())),
855                TestLogger::stdout(),
856            );
857
858            artifact_prover
859                .download_unpack_digest_file(
860                    &DigestsMessagePart {
861                        locations: vec![
862                            DigestLocation::CloudStorage {
863                                uri: "http://whatever-1/digests.tar.gz".to_string(),
864                                compression_algorithm: Some(CompressionAlgorithm::Gzip),
865                            },
866                            DigestLocation::Aggregator {
867                                uri: "http://whatever-2/digest".to_string(),
868                            },
869                        ],
870                        size_uncompressed: 0,
871                    },
872                    target_dir,
873                )
874                .await
875                .unwrap();
876        }
877    }
878
879    mod read_digest_file {
880
881        use super::*;
882
883        fn create_valid_fake_digest_file(
884            file_path: &Path,
885            digest_messages: &[CardanoDatabaseDigestListItemMessage],
886        ) {
887            let mut file = fs::File::create(file_path).unwrap();
888            let digest_json = serde_json::to_string(&digest_messages).unwrap();
889            file.write_all(digest_json.as_bytes()).unwrap();
890        }
891
892        fn create_invalid_fake_digest_file(file_path: &Path) {
893            let mut file = fs::File::create(file_path).unwrap();
894            file.write_all(b"incorrect-digest").unwrap();
895        }
896
897        #[test]
898        fn read_digest_file_fails_when_no_digest_file() {
899            let target_dir = TempDir::new(
900                "cardano_database_client",
901                "read_digest_file_fails_when_no_digest_file",
902            )
903            .build();
904            let artifact_prover = InternalArtifactProver::new(
905                Arc::new(
906                    MockFileDownloaderBuilder::default()
907                        .with_times(0)
908                        .with_success()
909                        .build(),
910                ),
911                Arc::new(TimestampTempDirectoryProvider::new(current_function!())),
912                TestLogger::stdout(),
913            );
914            artifact_prover
915                .read_digest_file(&target_dir)
916                .expect_err("read_digest_file should fail");
917        }
918
919        #[test]
920        fn read_digest_file_fails_when_multiple_digest_files() {
921            let target_dir = TempDir::new(
922                "cardano_database_client",
923                "read_digest_file_fails_when_multiple_digest_files",
924            )
925            .build();
926            create_valid_fake_digest_file(&target_dir.join("digests.json"), &[]);
927            create_valid_fake_digest_file(&target_dir.join("digests-2.json"), &[]);
928            let artifact_prover = InternalArtifactProver::new(
929                Arc::new(
930                    MockFileDownloaderBuilder::default()
931                        .with_times(0)
932                        .with_success()
933                        .build(),
934                ),
935                Arc::new(TimestampTempDirectoryProvider::new(current_function!())),
936                TestLogger::stdout(),
937            );
938            artifact_prover
939                .read_digest_file(&target_dir)
940                .expect_err("read_digest_file should fail");
941        }
942
943        #[test]
944        fn read_digest_file_fails_when_invalid_unique_digest_file() {
945            let target_dir = TempDir::new(
946                "cardano_database_client",
947                "read_digest_file_fails_when_invalid_unique_digest_file",
948            )
949            .build();
950            create_invalid_fake_digest_file(&target_dir.join("digests.json"));
951            let artifact_prover = InternalArtifactProver::new(
952                Arc::new(
953                    MockFileDownloaderBuilder::default()
954                        .with_times(0)
955                        .with_success()
956                        .build(),
957                ),
958                Arc::new(TimestampTempDirectoryProvider::new(current_function!())),
959                TestLogger::stdout(),
960            );
961            artifact_prover
962                .read_digest_file(&target_dir)
963                .expect_err("read_digest_file should fail");
964        }
965
966        #[test]
967        fn read_digest_file_succeeds_when_valid_unique_digest_file() {
968            let target_dir = TempDir::new(
969                "cardano_database_client",
970                "read_digest_file_succeeds_when_valid_unique_digest_file",
971            )
972            .build();
973            let digest_messages = vec![
974                CardanoDatabaseDigestListItemMessage {
975                    immutable_file_name: "00001.chunk".to_string(),
976                    digest: "digest-1".to_string(),
977                },
978                CardanoDatabaseDigestListItemMessage {
979                    immutable_file_name: "00002.chunk".to_string(),
980                    digest: "digest-2".to_string(),
981                },
982            ];
983            create_valid_fake_digest_file(&target_dir.join("digests.json"), &digest_messages);
984            let artifact_prover = InternalArtifactProver::new(
985                Arc::new(
986                    MockFileDownloaderBuilder::default()
987                        .with_times(0)
988                        .with_success()
989                        .build(),
990                ),
991                Arc::new(TimestampTempDirectoryProvider::new(current_function!())),
992                TestLogger::stdout(),
993            );
994
995            let digests = artifact_prover.read_digest_file(&target_dir).unwrap();
996            assert_eq!(
997                BTreeMap::from([
998                    ("00001.chunk".to_string(), "digest-1".to_string()),
999                    ("00002.chunk".to_string(), "digest-2".to_string())
1000                ]),
1001                digests
1002            )
1003        }
1004    }
1005
1006    mod list_missing_immutable_files {
1007        use mithril_cardano_node_internal_database::test::DummyCardanoDbBuilder;
1008        use mithril_common::temp_dir_create;
1009
1010        use super::*;
1011
1012        #[test]
1013        fn should_return_empty_list_if_no_missing_files() {
1014            let immutable_files_in_db = 1..=10;
1015            let range_to_verify = 3..=5;
1016            let cardano_db =
1017                DummyCardanoDbBuilder::new(&format!("{}", temp_dir_create!().display()))
1018                    .with_immutables(&immutable_files_in_db.collect::<Vec<_>>())
1019                    .append_immutable_trio()
1020                    .build();
1021
1022            let missing_files = InternalArtifactProver::list_missing_immutable_files(
1023                cardano_db.get_dir(),
1024                &range_to_verify,
1025            );
1026
1027            assert!(missing_files.is_empty());
1028        }
1029
1030        #[test]
1031        fn should_return_empty_list_if_missing_files_outside_range() {
1032            let immutable_files_in_db = 1..=10;
1033            let range_to_verify = 3..=5;
1034            let cardano_db =
1035                DummyCardanoDbBuilder::new(&format!("{}", temp_dir_create!().display()))
1036                    .with_immutables(&immutable_files_in_db.collect::<Vec<_>>())
1037                    .append_immutable_trio()
1038                    .build();
1039            let files_to_remove = vec!["00002.chunk", "00006.primary"];
1040            remove_immutable_files(cardano_db.get_dir(), &files_to_remove);
1041
1042            let missing_files = InternalArtifactProver::list_missing_immutable_files(
1043                cardano_db.get_dir(),
1044                &range_to_verify,
1045            );
1046
1047            assert!(missing_files.is_empty());
1048        }
1049
1050        #[test]
1051        fn should_return_list_of_missing_files_inside_range() {
1052            let immutable_files_in_db = 1..=10;
1053            let range_to_verify = 3..=5;
1054            let cardano_db =
1055                DummyCardanoDbBuilder::new(&format!("{}", temp_dir_create!().display()))
1056                    .with_immutables(&immutable_files_in_db.collect::<Vec<_>>())
1057                    .append_immutable_trio()
1058                    .build();
1059            let files_to_remove = vec!["00004.chunk", "00005.primary"];
1060            remove_immutable_files(cardano_db.get_dir(), &files_to_remove);
1061
1062            let missing_files = InternalArtifactProver::list_missing_immutable_files(
1063                cardano_db.get_dir(),
1064                &range_to_verify,
1065            );
1066
1067            assert_eq!(missing_files, files_to_remove);
1068        }
1069    }
1070
1071    mod verify_cardano_database {
1072
1073        use std::{collections::BTreeMap, ops::RangeInclusive, path::PathBuf};
1074
1075        use mithril_cardano_node_internal_database::{
1076            digesters::{CardanoImmutableDigester, ImmutableDigester},
1077            test::DummyCardanoDbBuilder,
1078        };
1079        use mithril_common::{
1080            entities::{CardanoDbBeacon, Epoch, ImmutableFileNumber, ProtocolMessage},
1081            messages::CertificateMessage,
1082            test::double::Dummy,
1083        };
1084
1085        use crate::cardano_database_client::ImmutableFileRange;
1086        use crate::{cardano_database_client::VerifiedDigests, test_utils::TestLogger};
1087
1088        use super::*;
1089
1090        async fn prepare_db_and_verified_digests(
1091            dir_name: &str,
1092            beacon: &CardanoDbBeacon,
1093            immutable_file_range: &RangeInclusive<ImmutableFileNumber>,
1094        ) -> (PathBuf, CertificateMessage, VerifiedDigests) {
1095            let cardano_db = DummyCardanoDbBuilder::new(dir_name)
1096                .with_immutables(&immutable_file_range.clone().collect::<Vec<_>>())
1097                .append_immutable_trio()
1098                .build();
1099            let database_dir = cardano_db.get_dir();
1100            let immutable_digester =
1101                CardanoImmutableDigester::new("whatever".to_string(), None, TestLogger::stdout());
1102            let computed_digests = immutable_digester
1103                .compute_digests_for_range(database_dir, immutable_file_range)
1104                .await
1105                .unwrap();
1106
1107            let digests = computed_digests
1108                .entries
1109                .iter()
1110                .map(|(immutable_file, digest)| (immutable_file.filename.clone(), digest.clone()))
1111                .collect::<BTreeMap<_, _>>();
1112
1113            let merkle_tree = immutable_digester
1114                .compute_merkle_tree(database_dir, beacon)
1115                .await
1116                .unwrap();
1117
1118            let verified_digests = VerifiedDigests {
1119                digests,
1120                merkle_tree,
1121            };
1122
1123            let certificate = {
1124                let protocol_message_merkle_root =
1125                    verified_digests.merkle_tree.compute_root().unwrap().to_hex();
1126                let mut protocol_message = ProtocolMessage::new();
1127                protocol_message.set_message_part(
1128                    ProtocolMessagePartKey::CardanoDatabaseMerkleRoot,
1129                    protocol_message_merkle_root,
1130                );
1131
1132                CertificateMessage {
1133                    protocol_message: protocol_message.clone(),
1134                    signed_message: protocol_message.compute_hash(),
1135                    ..CertificateMessage::dummy()
1136                }
1137            };
1138
1139            (database_dir.to_owned(), certificate, verified_digests)
1140        }
1141
1142        fn to_vec_immutable_file_name(list: &[&str]) -> Vec<ImmutableFileName> {
1143            list.iter().map(|s| ImmutableFileName::from(*s)).collect()
1144        }
1145
1146        #[tokio::test]
1147        async fn succeeds() {
1148            let beacon = CardanoDbBeacon {
1149                epoch: Epoch(123),
1150                immutable_file_number: 10,
1151            };
1152            let immutable_file_range = 1..=15;
1153            let immutable_file_range_to_prove = ImmutableFileRange::Range(2, 4);
1154            let (database_dir, certificate, verified_digests) = prepare_db_and_verified_digests(
1155                "verify_cardano_database_succeeds",
1156                &beacon,
1157                &immutable_file_range,
1158            )
1159            .await;
1160
1161            let expected_merkle_root =
1162                verified_digests.merkle_tree.compute_root().unwrap().to_owned();
1163
1164            let client =
1165                CardanoDatabaseClientDependencyInjector::new().build_cardano_database_client();
1166
1167            let merkle_proof = client
1168                .verify_cardano_database(
1169                    &certificate,
1170                    &CardanoDatabaseSnapshotMessage::dummy(),
1171                    &immutable_file_range_to_prove,
1172                    false,
1173                    &database_dir,
1174                    &verified_digests,
1175                )
1176                .await
1177                .unwrap();
1178
1179            merkle_proof.verify().unwrap();
1180            let merkle_proof_root = merkle_proof.root().to_owned();
1181            assert_eq!(expected_merkle_root, merkle_proof_root);
1182        }
1183
1184        #[tokio::test]
1185        async fn should_fail_if_immutable_is_missing_and_allow_missing_not_set() {
1186            let beacon = CardanoDbBeacon {
1187                epoch: Epoch(123),
1188                immutable_file_number: 10,
1189            };
1190            let immutable_file_range = 1..=15;
1191            let immutable_file_range_to_prove = ImmutableFileRange::Range(2, 4);
1192            let (database_dir, certificate, verified_digests) = prepare_db_and_verified_digests(
1193                "verify_cardano_database_should_fail_if_immutable_is_missing_and_allow_missing_not_set",
1194                &beacon,
1195                &immutable_file_range,
1196            )
1197            .await;
1198            let client =
1199                CardanoDatabaseClientDependencyInjector::new().build_cardano_database_client();
1200
1201            let files_to_remove = vec!["00003.chunk", "00004.primary"];
1202            remove_immutable_files(&database_dir, &files_to_remove);
1203
1204            let allow_missing = false;
1205            let error = client
1206                .verify_cardano_database(
1207                    &certificate,
1208                    &CardanoDatabaseSnapshotMessage::dummy(),
1209                    &immutable_file_range_to_prove,
1210                    allow_missing,
1211                    &database_dir,
1212                    &verified_digests,
1213                )
1214                .await
1215                .expect_err("verify_cardano_database should fail if a immutable is missing");
1216
1217            let error_lists = match error {
1218                CardanoDatabaseVerificationError::ImmutableFilesVerification(lists) => lists,
1219                _ => panic!("Expected ImmutableFilesVerification error, got: {error}"),
1220            };
1221
1222            assert_eq!(
1223                error_lists,
1224                ImmutableVerificationResult {
1225                    immutables_dir: InternalArtifactProver::immutable_dir(&database_dir),
1226                    missing: to_vec_immutable_file_name(&files_to_remove),
1227                    tampered: vec![],
1228                    non_verifiable: vec![],
1229                }
1230            );
1231        }
1232
1233        #[tokio::test]
1234        async fn should_success_if_immutable_is_missing_and_allow_missing_is_set() {
1235            let beacon = CardanoDbBeacon {
1236                epoch: Epoch(123),
1237                immutable_file_number: 10,
1238            };
1239            let immutable_file_range = 1..=15;
1240            let immutable_file_range_to_prove = ImmutableFileRange::Range(2, 4);
1241            let (database_dir, certificate, verified_digests) = prepare_db_and_verified_digests(
1242                "verify_cardano_database_should_success_if_immutable_is_missing_and_allow_missing_is_set",
1243                &beacon,
1244                &immutable_file_range,
1245            )
1246            .await;
1247            let client =
1248                CardanoDatabaseClientDependencyInjector::new().build_cardano_database_client();
1249
1250            let files_to_remove = vec!["00003.chunk", "00004.primary"];
1251            remove_immutable_files(&database_dir, &files_to_remove);
1252
1253            let allow_missing = true;
1254            client.verify_cardano_database(
1255                    &certificate,
1256                    &CardanoDatabaseSnapshotMessage::dummy(),
1257                    &immutable_file_range_to_prove,
1258                    allow_missing,
1259                    &database_dir,
1260                    &verified_digests,
1261                )
1262                .await
1263                .expect(
1264                    "verify_cardano_database should succeed if a immutable is missing but 'allow_missing' is set",
1265                );
1266        }
1267
1268        #[tokio::test]
1269        async fn should_fail_if_immutable_is_tampered() {
1270            let beacon = CardanoDbBeacon {
1271                epoch: Epoch(123),
1272                immutable_file_number: 10,
1273            };
1274            let immutable_file_range = 1..=15;
1275            let immutable_file_range_to_prove = ImmutableFileRange::Range(2, 4);
1276            let (database_dir, certificate, verified_digests) = prepare_db_and_verified_digests(
1277                "verify_cardano_database_should_fail_if_immutable_is_tampered",
1278                &beacon,
1279                &immutable_file_range,
1280            )
1281            .await;
1282            let (logger, log_inspector) = TestLogger::memory();
1283            let client = CardanoDatabaseClientDependencyInjector::new()
1284                .with_logger(logger)
1285                .build_cardano_database_client();
1286
1287            let files_to_tamper = vec!["00003.chunk", "00004.primary"];
1288            tamper_immutable_files(&database_dir, &files_to_tamper);
1289
1290            let error = client
1291                .verify_cardano_database(
1292                    &certificate,
1293                    &CardanoDatabaseSnapshotMessage::dummy(),
1294                    &immutable_file_range_to_prove,
1295                    false,
1296                    &database_dir,
1297                    &verified_digests,
1298                )
1299                .await
1300                .expect_err("verify_cardano_database should fail if a immutable is missing");
1301
1302            assert!(log_inspector.contains_log(MERKLE_PROOF_COMPUTATION_ERROR));
1303
1304            let error_lists = match error {
1305                CardanoDatabaseVerificationError::ImmutableFilesVerification(lists) => lists,
1306                _ => panic!("Expected ImmutableFilesVerification error, got: {error}"),
1307            };
1308            assert_eq!(
1309                error_lists,
1310                ImmutableVerificationResult {
1311                    immutables_dir: InternalArtifactProver::immutable_dir(&database_dir),
1312                    missing: vec![],
1313                    tampered: to_vec_immutable_file_name(&files_to_tamper),
1314                    non_verifiable: vec![],
1315                }
1316            )
1317        }
1318
1319        #[tokio::test]
1320        async fn should_fail_if_immutables_are_missing_and_tampered() {
1321            let beacon = CardanoDbBeacon {
1322                epoch: Epoch(123),
1323                immutable_file_number: 10,
1324            };
1325            let immutable_file_range = 1..=15;
1326            let immutable_file_range_to_prove = ImmutableFileRange::Range(2, 4);
1327            let (database_dir, certificate, verified_digests) = prepare_db_and_verified_digests(
1328                "verify_cardano_database_should_fail_if_immutables_are_missing_and_tampered",
1329                &beacon,
1330                &immutable_file_range,
1331            )
1332            .await;
1333
1334            let files_to_remove = vec!["00003.chunk"];
1335            let files_to_tamper = vec!["00004.primary"];
1336            remove_immutable_files(&database_dir, &files_to_remove);
1337            tamper_immutable_files(&database_dir, &files_to_tamper);
1338
1339            let client =
1340                CardanoDatabaseClientDependencyInjector::new().build_cardano_database_client();
1341            let error = client
1342                .verify_cardano_database(
1343                    &certificate,
1344                    &CardanoDatabaseSnapshotMessage::dummy(),
1345                    &immutable_file_range_to_prove,
1346                    false,
1347                    &database_dir,
1348                    &verified_digests,
1349                )
1350                .await
1351                .expect_err("verify_cardano_database should fail if a immutable is missing");
1352
1353            let error_lists = match error {
1354                CardanoDatabaseVerificationError::ImmutableFilesVerification(lists) => lists,
1355                _ => panic!("Expected ImmutableFilesVerification error, got: {error}"),
1356            };
1357            assert_eq!(
1358                error_lists,
1359                ImmutableVerificationResult {
1360                    immutables_dir: InternalArtifactProver::immutable_dir(&database_dir),
1361                    missing: to_vec_immutable_file_name(&files_to_remove),
1362                    tampered: to_vec_immutable_file_name(&files_to_tamper),
1363                    non_verifiable: vec![],
1364                }
1365            )
1366        }
1367
1368        #[tokio::test]
1369        async fn should_fail_if_there_is_more_local_immutable_than_verified_digest() {
1370            let last_verified_digest_number = 10;
1371            let last_local_immutable_file_number = 15;
1372            let range_of_non_verifiable_files =
1373                last_verified_digest_number + 1..=last_local_immutable_file_number;
1374
1375            let expected_non_verifiable_files: Vec<ImmutableFileName> =
1376                (range_of_non_verifiable_files)
1377                    .flat_map(|i| {
1378                        [
1379                            format!("{i:05}.chunk"),
1380                            format!("{i:05}.primary"),
1381                            format!("{i:05}.secondary"),
1382                        ]
1383                    })
1384                    .collect();
1385
1386            let beacon = CardanoDbBeacon {
1387                epoch: Epoch(123),
1388                immutable_file_number: last_verified_digest_number,
1389            };
1390            //create verified digests for immutable files 1 to 10
1391            let (_, certificate, verified_digests) = prepare_db_and_verified_digests(
1392                "database_dir_for_verified_digests",
1393                &beacon,
1394                &(1..=last_verified_digest_number),
1395            )
1396            .await;
1397            //create a local database with immutable files 1 to 15
1398            let (database_dir, _, _) = prepare_db_and_verified_digests(
1399                "database_dir_for_local_immutables",
1400                &beacon,
1401                &(1..=last_local_immutable_file_number),
1402            )
1403            .await;
1404
1405            let client =
1406                CardanoDatabaseClientDependencyInjector::new().build_cardano_database_client();
1407            let error = client
1408                .verify_cardano_database(
1409                    &certificate,
1410                    &CardanoDatabaseSnapshotMessage::dummy(),
1411                    &ImmutableFileRange::Range(1, 15),
1412                    false,
1413                    &database_dir,
1414                    &verified_digests,
1415                )
1416                .await
1417                .expect_err(
1418                    "verify_cardano_database should fail if there is more local immutable than verified digest",
1419                );
1420
1421            let error_lists = match error {
1422                CardanoDatabaseVerificationError::ImmutableFilesVerification(lists) => lists,
1423                _ => panic!("Expected ImmutableFilesVerification error, got: {error}"),
1424            };
1425            assert_eq!(
1426                error_lists,
1427                ImmutableVerificationResult {
1428                    immutables_dir: InternalArtifactProver::immutable_dir(&database_dir),
1429                    missing: vec![],
1430                    tampered: vec![],
1431                    non_verifiable: expected_non_verifiable_files,
1432                }
1433            );
1434        }
1435    }
1436
1437    mod cardano_database_verification_error {
1438        use super::*;
1439
1440        fn generate_immutable_files_verification_error(
1441            missing_range: Option<RangeInclusive<usize>>,
1442            tampered_range: Option<RangeInclusive<usize>>,
1443            non_verifiable_range: Option<RangeInclusive<usize>>,
1444            immutable_path: &str,
1445        ) -> CardanoDatabaseVerificationError {
1446            let missing: Vec<ImmutableFileName> = match missing_range {
1447                Some(range) => range
1448                    .map(|i| ImmutableFileName::from(format!("{i:05}.chunk")))
1449                    .collect(),
1450                None => vec![],
1451            };
1452            let tampered: Vec<ImmutableFileName> = match tampered_range {
1453                Some(range) => range
1454                    .map(|i| ImmutableFileName::from(format!("{i:05}.chunk")))
1455                    .collect(),
1456                None => vec![],
1457            };
1458
1459            let non_verifiable: Vec<ImmutableFileName> = match non_verifiable_range {
1460                Some(range) => range
1461                    .map(|i| ImmutableFileName::from(format!("{i:05}.chunk")))
1462                    .collect(),
1463                None => vec![],
1464            };
1465
1466            CardanoDatabaseVerificationError::ImmutableFilesVerification(
1467                ImmutableVerificationResult {
1468                    immutables_dir: PathBuf::from(immutable_path),
1469                    missing,
1470                    tampered,
1471                    non_verifiable,
1472                },
1473            )
1474        }
1475
1476        fn normalize_path_separators(s: &str) -> String {
1477            s.replace('\\', "/")
1478        }
1479
1480        #[test]
1481        fn display_immutable_files_verification_error_should_displayed_lists_with_10_elements() {
1482            let error = generate_immutable_files_verification_error(
1483                Some(1..=15),
1484                Some(20..=31),
1485                Some(40..=41),
1486                "/path/to/immutables",
1487            );
1488
1489            let display = normalize_path_separators(&format!("{error}"));
1490
1491            assert_eq!(
1492                display,
1493                r###"Number of missing immutable files: 15
1494First 10 missing immutable files paths:
1495/path/to/immutables/00001.chunk
1496/path/to/immutables/00002.chunk
1497/path/to/immutables/00003.chunk
1498/path/to/immutables/00004.chunk
1499/path/to/immutables/00005.chunk
1500/path/to/immutables/00006.chunk
1501/path/to/immutables/00007.chunk
1502/path/to/immutables/00008.chunk
1503/path/to/immutables/00009.chunk
1504/path/to/immutables/00010.chunk
1505
1506Number of tampered immutable files: 12
1507First 10 tampered immutable files paths:
1508/path/to/immutables/00020.chunk
1509/path/to/immutables/00021.chunk
1510/path/to/immutables/00022.chunk
1511/path/to/immutables/00023.chunk
1512/path/to/immutables/00024.chunk
1513/path/to/immutables/00025.chunk
1514/path/to/immutables/00026.chunk
1515/path/to/immutables/00027.chunk
1516/path/to/immutables/00028.chunk
1517/path/to/immutables/00029.chunk
1518
1519Number of non verifiable immutable files: 2
1520First 10 non verifiable immutable files paths:
1521/path/to/immutables/00040.chunk
1522/path/to/immutables/00041.chunk
1523"###
1524            );
1525        }
1526
1527        #[test]
1528        fn display_immutable_files_should_display_tampered_files_only() {
1529            let error = generate_immutable_files_verification_error(
1530                None,
1531                Some(1..=1),
1532                None,
1533                "/path/to/immutables",
1534            );
1535
1536            let display = normalize_path_separators(&format!("{error}"));
1537
1538            assert_eq!(
1539                display,
1540                r###"Number of tampered immutable files: 1
1541First 10 tampered immutable files paths:
1542/path/to/immutables/00001.chunk
1543"###
1544            );
1545        }
1546
1547        #[test]
1548        fn display_immutable_files_should_display_missing_files_only() {
1549            let error = generate_immutable_files_verification_error(
1550                Some(1..=1),
1551                None,
1552                None,
1553                "/path/to/immutables",
1554            );
1555
1556            let display = normalize_path_separators(&format!("{error}"));
1557
1558            assert_eq!(
1559                display,
1560                r###"Number of missing immutable files: 1
1561First 10 missing immutable files paths:
1562/path/to/immutables/00001.chunk
1563"###
1564            );
1565        }
1566
1567        #[test]
1568        fn display_immutable_files_should_display_non_verifiable_files_only() {
1569            let error = generate_immutable_files_verification_error(
1570                None,
1571                None,
1572                Some(1..=5),
1573                "/path/to/immutables",
1574            );
1575
1576            let display = normalize_path_separators(&format!("{error}"));
1577
1578            assert_eq!(
1579                display,
1580                r###"Number of non verifiable immutable files: 5
1581First 10 non verifiable immutable files paths:
1582/path/to/immutables/00001.chunk
1583/path/to/immutables/00002.chunk
1584/path/to/immutables/00003.chunk
1585/path/to/immutables/00004.chunk
1586/path/to/immutables/00005.chunk
1587"###
1588            );
1589        }
1590    }
1591}