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
41pub struct VerifiedDigests {
43 pub digests: BTreeMap<ImmutableFileName, HexEncodedDigest>,
45 pub merkle_tree: MKTree<MKTreeStoreInMemory>,
47}
48
49const MERKLE_PROOF_COMPUTATION_ERROR: &str = "Merkle proof computation failed";
50
51#[derive(Debug, PartialEq)]
53pub struct ImmutableVerificationResult {
54 pub immutables_dir: PathBuf,
56 pub missing: Vec<ImmutableFileName>,
58 pub tampered: Vec<ImmutableFileName>,
60 pub non_verifiable: Vec<ImmutableFileName>,
62}
63
64#[derive(Error, Debug)]
66pub enum CardanoDatabaseVerificationError {
67 ImmutableFilesVerification(ImmutableVerificationResult),
69
70 DigestsComputation(#[from] ImmutableDigesterError),
72
73 MerkleProofVerification(#[source] MithrilError),
75
76 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#[derive(PartialEq, Debug)]
154pub(crate) struct ImmutableFilesNotVerified {
155 pub tampered_files: Vec<ImmutableFileName>,
157 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 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 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 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 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 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}