mithril_client/
snapshot_client.rs

1//! A client to retrieve snapshots data from an Aggregator.
2//!
3//! In order to do so it defines a [SnapshotClient] which exposes the following features:
4//!  - [get][SnapshotClient::get]: get a single snapshot data from its digest
5//!  - [list][SnapshotClient::list]: get the list of available snapshots
6//!  - [download_unpack_full][SnapshotClient::download_unpack_full]: download and unpack the tarball
7//!    of a snapshot and its ancillary files to a directory, use this function if you want to fast bootstrap
8//!    a Cardano node
9//!  - [download_unpack][SnapshotClient::download_unpack]: download and unpack the tarball of a snapshot
10//!    to a directory (immutable files only)
11//!
12//! **Note:** Ancillary files are the files that are not signed by Mithril but are needed to enable fast
13//! They include the last ledger state snapshot and the last immutable file.
14//!
15//! # Get a single snapshot
16//!
17//! To get a single snapshot using the [ClientBuilder][crate::client::ClientBuilder].
18//!
19//! ```no_run
20//! # async fn run() -> mithril_client::MithrilResult<()> {
21//! use mithril_client::ClientBuilder;
22//!
23//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
24//! let snapshot = client.cardano_database().get("SNAPSHOT_DIGEST").await?.unwrap();
25//!
26//! println!("Snapshot digest={}, size={}", snapshot.digest, snapshot.size);
27//! #    Ok(())
28//! # }
29//! ```
30//!
31//! # List available snapshots
32//!
33//! To list available snapshots using the [ClientBuilder][crate::client::ClientBuilder].
34//!
35//! ```no_run
36//! # async fn run() -> mithril_client::MithrilResult<()> {
37//! use mithril_client::ClientBuilder;
38//!
39//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
40//! let snapshots = client.cardano_database().list().await?;
41//!
42//! for snapshot in snapshots {
43//!     println!("Snapshot digest={}, size={}", snapshot.digest, snapshot.size);
44//! }
45//! #    Ok(())
46//! # }
47//! ```
48//!
49//! # Download a snapshot
50//! **Note:** _Available on crate feature_ **fs** _only._
51//!
52//! To download and simultaneously unpack the tarball of a snapshot using the [ClientBuilder][crate::client::ClientBuilder]
53//! , including its ancillary files, to a directory.
54//!
55//! ```no_run
56//! # #[cfg(feature = "fs")]
57//! # async fn run() -> mithril_client::MithrilResult<()> {
58//! use mithril_client::ClientBuilder;
59//! use std::path::Path;
60//!
61//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY")
62//!     .set_ancillary_verification_key("YOUR_ANCILLARY_VERIFICATION_KEY".to_string())
63//!     .build()?;
64//! let snapshot = client.cardano_database().get("SNAPSHOT_DIGEST").await?.unwrap();
65//!
66//! // Note: the directory must already exist, and the user running the binary must have read/write access to it.
67//! let target_directory = Path::new("/home/user/download/");
68//! client
69//!    .cardano_database()
70//!    .download_unpack_full(&snapshot, target_directory)
71//!    .await?;
72//! #
73//! #    Ok(())
74//! # }
75//! ```
76//!
77//! # Add statistics
78//! **Note:** _Available on crate feature_ **fs** _only._
79//!
80//! Increments the aggregator snapshot download statistics using the [ClientBuilder][crate::client::ClientBuilder].
81//!
82//! ```no_run
83//! # #[cfg(feature = "fs")]
84//! # async fn run() -> mithril_client::MithrilResult<()> {
85//! use mithril_client::ClientBuilder;
86//! use std::path::Path;
87//!
88//! let client = ClientBuilder::aggregator("YOUR_AGGREGATOR_ENDPOINT", "YOUR_GENESIS_VERIFICATION_KEY").build()?;
89//! let snapshot = client.cardano_database().get("SNAPSHOT_DIGEST").await?.unwrap();
90//!
91//! // Note: the directory must already exist, and the user running the binary must have read/write access to it.
92//! let target_directory = Path::new("/home/user/download/");
93//! client
94//!    .cardano_database()
95//!    .download_unpack(&snapshot, target_directory)
96//!    .await?;
97//!
98//! client.snapshot().add_statistics(&snapshot).await.unwrap();
99//! #
100//! #    Ok(())
101//! # }
102//! ```
103
104use anyhow::Context;
105#[cfg(feature = "fs")]
106use slog::Logger;
107#[cfg(feature = "fs")]
108use std::path::{Path, PathBuf};
109use std::sync::Arc;
110use thiserror::Error;
111
112#[cfg(feature = "fs")]
113use mithril_common::entities::CompressionAlgorithm;
114
115use crate::aggregator_client::{AggregatorClient, AggregatorClientError, AggregatorRequest};
116#[cfg(feature = "fs")]
117use crate::feedback::FeedbackSender;
118#[cfg(feature = "fs")]
119use crate::file_downloader::{DownloadEvent, FileDownloader};
120#[cfg(feature = "fs")]
121use crate::utils::create_bootstrap_node_files;
122#[cfg(feature = "fs")]
123use crate::utils::{
124    AncillaryVerifier, UnexpectedDownloadedFileVerifier, ANCILLARIES_NOT_SIGNED_BY_MITHRIL,
125};
126use crate::{MithrilResult, Snapshot, SnapshotListItem};
127
128/// Error for the Snapshot client
129#[derive(Error, Debug)]
130pub enum SnapshotClientError {
131    /// Download location does not work
132    #[error("Could not find a working download location for the snapshot digest '{digest}', tried location: {{'{locations}'}}.")]
133    NoWorkingLocation {
134        /// given digest
135        digest: String,
136
137        /// list of locations tried
138        locations: String,
139    },
140    /// Missing ancillary verifier
141    #[error("Ancillary verifier is not set, please use `set_ancillary_verification_key` when creating the client")]
142    MissingAncillaryVerifier,
143}
144
145/// Aggregator client for the snapshot artifact
146pub struct SnapshotClient {
147    aggregator_client: Arc<dyn AggregatorClient>,
148    #[cfg(feature = "fs")]
149    http_file_downloader: Arc<dyn FileDownloader>,
150    #[cfg(feature = "fs")]
151    ancillary_verifier: Option<Arc<AncillaryVerifier>>,
152    #[cfg(feature = "fs")]
153    _feedback_sender: FeedbackSender,
154    #[cfg(feature = "fs")]
155    logger: Logger,
156}
157
158impl SnapshotClient {
159    /// Constructs a new `SnapshotClient`.
160    pub fn new(
161        aggregator_client: Arc<dyn AggregatorClient>,
162        #[cfg(feature = "fs")] http_file_downloader: Arc<dyn FileDownloader>,
163        #[cfg(feature = "fs")] ancillary_verifier: Option<Arc<AncillaryVerifier>>,
164        #[cfg(feature = "fs")] feedback_sender: FeedbackSender,
165        #[cfg(feature = "fs")] logger: Logger,
166    ) -> Self {
167        Self {
168            aggregator_client,
169            #[cfg(feature = "fs")]
170            http_file_downloader,
171            #[cfg(feature = "fs")]
172            ancillary_verifier,
173            // The underscore prefix prevents breaking the `SnapshotClient` API compatibility.
174            #[cfg(feature = "fs")]
175            _feedback_sender: feedback_sender,
176            #[cfg(feature = "fs")]
177            logger: mithril_common::logging::LoggerExtensions::new_with_component_name::<Self>(
178                &logger,
179            ),
180        }
181    }
182
183    /// Return a list of available snapshots
184    pub async fn list(&self) -> MithrilResult<Vec<SnapshotListItem>> {
185        let response = self
186            .aggregator_client
187            .get_content(AggregatorRequest::ListSnapshots)
188            .await
189            .with_context(|| "Snapshot Client can not get the artifact list")?;
190        let items = serde_json::from_str::<Vec<SnapshotListItem>>(&response)
191            .with_context(|| "Snapshot Client can not deserialize artifact list")?;
192
193        Ok(items)
194    }
195
196    /// Get the given snapshot data. If it cannot be found, a None is returned.
197    pub async fn get(&self, digest: &str) -> MithrilResult<Option<Snapshot>> {
198        match self
199            .aggregator_client
200            .get_content(AggregatorRequest::GetSnapshot {
201                digest: digest.to_string(),
202            })
203            .await
204        {
205            Ok(content) => {
206                let snapshot: Snapshot = serde_json::from_str(&content)
207                    .with_context(|| "Snapshot Client can not deserialize artifact")?;
208
209                Ok(Some(snapshot))
210            }
211            Err(AggregatorClientError::RemoteServerLogical(_)) => Ok(None),
212            Err(e) => Err(e.into()),
213        }
214    }
215
216    cfg_fs! {
217        /// Download and unpack the given snapshot, including its ancillary files, to the given directory
218        ///
219        /// Ancillary files are the files that are not signed by Mithril but are needed to enable fast
220        /// They include the last ledger state snapshot and the last immutable file.
221        ///
222        /// **NOTE**: The target directory should already exist, and the user running the binary
223        /// must have read/write access to it.
224        pub async fn download_unpack_full(
225            &self,
226            snapshot: &Snapshot,
227            target_dir: &Path,
228        ) -> MithrilResult<()> {
229            if self.ancillary_verifier.is_none() {
230                return Err(SnapshotClientError::MissingAncillaryVerifier.into());
231            }
232
233            let include_ancillary = true;
234            let expected_files_after_download = UnexpectedDownloadedFileVerifier::new(
235                target_dir,
236                include_ancillary,
237                snapshot.beacon.immutable_file_number,
238                &self.logger
239            )
240            .compute_expected_state_after_download()
241            .await?;
242
243            // Return the result later so unexpected file removal is always run
244            let result = self.run_download_unpack(snapshot, target_dir, include_ancillary).await;
245
246            expected_files_after_download
247                .remove_unexpected_files()
248                .await?;
249
250            result
251        }
252
253        /// Download and unpack the given immutable files of the snapshot to the given directory
254        ///
255        /// Ancillary files are not included in this operation, if they are needed, use
256        /// [download_unpack_full][Self::download_unpack_full] instead.
257        ///
258        /// **NOTE**: The target directory should already exist, and the user running the binary
259        /// must have read/write access to it.
260        pub async fn download_unpack(
261            &self,
262            snapshot: &Snapshot,
263            target_dir: &Path,
264        ) -> MithrilResult<()> {
265            slog::warn!(
266                self.logger,
267                "The fast bootstrap of the Cardano node is not available with the current parameters used in this command: the ledger state will be recomputed from genesis at startup of the Cardano node. Use the extra function download_unpack_full to allow it."
268            );
269
270            let include_ancillary = false;
271            let expected_files_after_download = UnexpectedDownloadedFileVerifier::new(
272                target_dir,
273                include_ancillary,
274                snapshot.beacon.immutable_file_number,
275                &self.logger
276            )
277            .compute_expected_state_after_download()
278            .await?;
279
280            // Return the result later so unexpected file removal is always run
281            let result = self.run_download_unpack(snapshot, target_dir, include_ancillary).await;
282
283            expected_files_after_download
284                .remove_unexpected_files()
285                .await?;
286
287            result
288        }
289
290        async fn run_download_unpack(
291            &self,
292            snapshot: &Snapshot,
293            target_dir: &Path,
294            include_ancillary: bool,
295        ) -> MithrilResult<()> {
296            use crate::feedback::MithrilEvent;
297
298            let download_id = MithrilEvent::new_snapshot_download_id();
299            self.download_unpack_immutables_files(snapshot, target_dir, &download_id)
300                .await?;
301            if include_ancillary {
302                self.download_unpack_ancillary(snapshot, target_dir, &download_id)
303                    .await?;
304            }
305            create_bootstrap_node_files(
306                &self.logger,
307                target_dir,
308                &snapshot.network,
309            )?;
310            Ok(())
311        }
312
313        async fn download_unpack_immutables_files(
314            &self,
315            snapshot: &Snapshot,
316            target_dir: &Path,
317            download_id: &str,
318        ) -> MithrilResult<()> {
319            self.download_unpack_file(
320                &snapshot.digest,
321                &snapshot.locations,
322                snapshot.size,
323                target_dir,
324                snapshot.compression_algorithm,
325                DownloadEvent::Full {
326                    download_id: download_id.to_string(),
327                    digest: snapshot.digest.clone(),
328                },
329            )
330            .await?;
331
332            Ok(())
333        }
334
335        async fn download_unpack_ancillary(
336            &self,
337            snapshot: &Snapshot,
338            target_dir: &Path,
339            download_id: &str,
340        ) -> MithrilResult<()> {
341            slog::warn!(self.logger, "{}", ANCILLARIES_NOT_SIGNED_BY_MITHRIL);
342
343            match &snapshot.ancillary_locations {
344                None => Ok(()),
345                Some(ancillary_locations) => {
346                    let temp_ancillary_unpack_dir = Self::ancillary_subdir(target_dir, download_id);
347                    tokio::fs::create_dir(&temp_ancillary_unpack_dir)
348                        .await
349                        .with_context(|| {
350                            format!(
351                                "Snapshot Client can not create ancillary unpack directory '{}'",
352                                temp_ancillary_unpack_dir.display()
353                            )
354                        })?;
355
356                    let result = self
357                        .download_unpack_verify_ancillary(
358                            snapshot,
359                            ancillary_locations,
360                            snapshot.ancillary_size.unwrap_or(0),
361                            target_dir,
362                            &temp_ancillary_unpack_dir,
363                            download_id,
364                        )
365                        .await;
366
367                    if let Err(e) = std::fs::remove_dir_all(&temp_ancillary_unpack_dir) {
368                        slog::warn!(
369                            self.logger, "Failed to remove ancillary unpack directory '{}'", temp_ancillary_unpack_dir.display();
370                            "error" => ?e
371                        );
372                    }
373
374                    result
375                }
376            }
377        }
378
379        async fn download_unpack_verify_ancillary(
380            &self,
381            snapshot: &Snapshot,
382            ancillary_locations: &[String],
383            ancillary_size: u64,
384            target_dir: &Path,
385            temp_ancillary_unpack_dir: &Path,
386            download_id: &str,
387        ) -> MithrilResult<()> {
388            self.download_unpack_file(
389                &snapshot.digest,
390                ancillary_locations,
391                ancillary_size,
392                temp_ancillary_unpack_dir,
393                snapshot.compression_algorithm,
394                DownloadEvent::FullAncillary {
395                    download_id: download_id.to_string(),
396                },
397            )
398            .await?;
399
400            let ancillary_verifier = self
401                .ancillary_verifier
402                .as_ref()
403                .ok_or(SnapshotClientError::MissingAncillaryVerifier)?;
404
405            let validated_manifest = ancillary_verifier.verify(temp_ancillary_unpack_dir).await?;
406            validated_manifest
407                .move_to_final_location(target_dir)
408                .await?;
409
410            Ok(())
411        }
412
413        async fn download_unpack_file(
414            &self,
415            digest: &str,
416            locations: &[String],
417            size: u64,
418            target_dir: &Path,
419            compression_algorithm: CompressionAlgorithm,
420            download_event: DownloadEvent,
421        ) -> MithrilResult<()> {
422            for location in locations {
423                let file_downloader_uri = location.to_owned().into();
424
425                if let Err(error) = self
426                    .http_file_downloader
427                    .download_unpack(
428                        &file_downloader_uri,
429                        size,
430                        target_dir,
431                        Some(compression_algorithm),
432                        download_event.clone(),
433                    )
434                    .await
435                {
436                    slog::warn!(self.logger, "Failed downloading snapshot from '{location}'"; "error" => ?error);
437                } else {
438                    return Ok(());
439                }
440            }
441
442            let locations = locations.join(", ");
443
444            Err(SnapshotClientError::NoWorkingLocation {
445                digest: digest.to_string(),
446                locations,
447            }
448            .into())
449        }
450
451        fn ancillary_subdir(target_dir: &Path, download_id: &str) -> PathBuf {
452            target_dir.join(format!("ancillary-{download_id}"))
453        }
454    }
455
456    /// Increments the aggregator snapshot download statistics
457    pub async fn add_statistics(&self, snapshot: &Snapshot) -> MithrilResult<()> {
458        let _response = self
459            .aggregator_client
460            .post_content(AggregatorRequest::IncrementSnapshotStatistic {
461                snapshot: serde_json::to_string(snapshot)?,
462            })
463            .await?;
464
465        Ok(())
466    }
467}
468
469#[cfg(all(test, feature = "fs"))]
470mod tests {
471    use crate::{
472        aggregator_client::MockAggregatorClient,
473        common::CompressionAlgorithm,
474        feedback::MithrilEvent,
475        file_downloader::{MockFileDownloader, MockFileDownloaderBuilder},
476        test_utils::TestLogger,
477    };
478
479    use mithril_common::{
480        assert_dir_eq, crypto_helper::ManifestSigner, digesters::IMMUTABLE_DIR, temp_dir_create,
481        test_utils::fake_keys,
482    };
483
484    use super::*;
485
486    fn dummy_download_event() -> DownloadEvent {
487        DownloadEvent::Full {
488            download_id: MithrilEvent::new_snapshot_download_id(),
489            digest: "test-digest".to_string(),
490        }
491    }
492
493    fn setup_snapshot_client(
494        file_downloader: Arc<dyn FileDownloader>,
495        ancillary_verifier: Option<Arc<AncillaryVerifier>>,
496    ) -> SnapshotClient {
497        let aggregator_client = Arc::new(MockAggregatorClient::new());
498        let logger = TestLogger::stdout();
499
500        SnapshotClient::new(
501            aggregator_client,
502            file_downloader,
503            ancillary_verifier,
504            FeedbackSender::new(&[]),
505            logger.clone(),
506        )
507    }
508
509    mod download_unpack_file {
510        use super::*;
511
512        fn setup_snapshot_client(file_downloader: Arc<dyn FileDownloader>) -> SnapshotClient {
513            super::setup_snapshot_client(file_downloader, None)
514        }
515
516        #[tokio::test]
517        async fn log_warning_if_location_fails() {
518            let (logger, log_inspector) = TestLogger::memory();
519            let mock_downloader = MockFileDownloaderBuilder::default()
520                .with_file_uri("http://whatever.co/snapshot")
521                .with_failure()
522                .build();
523
524            let client = SnapshotClient {
525                logger,
526                ..setup_snapshot_client(Arc::new(mock_downloader))
527            };
528
529            let _result = client
530                .download_unpack_file(
531                    "test-digest",
532                    &["http://whatever.co/snapshot".to_string()],
533                    19,
534                    &PathBuf::from("/whatever"),
535                    CompressionAlgorithm::Gzip,
536                    dummy_download_event(),
537                )
538                .await;
539
540            assert!(
541                log_inspector.contains_log("Failed downloading snapshot"),
542                "Expected log message not found, logs: {log_inspector}"
543            );
544        }
545
546        #[tokio::test]
547        async fn error_contains_list_of_all_tried_locations_if_all_attempts_fails() {
548            let test_locations = vec![
549                "http://example.com/snapshot1".to_string(),
550                "http://example.com/snapshot2".to_string(),
551            ];
552            let mock_downloader = MockFileDownloaderBuilder::default()
553                .with_file_uri("http://example.com/snapshot1")
554                .with_failure()
555                .next_call()
556                .with_file_uri("http://example.com/snapshot2")
557                .with_failure()
558                .build();
559            let client = setup_snapshot_client(Arc::new(mock_downloader));
560
561            let error = client
562                .download_unpack_file(
563                    "test-digest",
564                    &test_locations,
565                    19,
566                    &PathBuf::from("/whatever"),
567                    CompressionAlgorithm::Gzip,
568                    dummy_download_event(),
569                )
570                .await
571                .expect_err("Should fail when all locations fail");
572
573            if let Some(SnapshotClientError::NoWorkingLocation { digest, locations }) =
574                error.downcast_ref::<SnapshotClientError>()
575            {
576                assert_eq!(digest, "test-digest");
577                assert_eq!(
578                    locations,
579                    "http://example.com/snapshot1, http://example.com/snapshot2"
580                );
581            } else {
582                panic!("Expected SnapshotClientError::NoWorkingLocation, but got: {error:?}");
583            }
584        }
585
586        #[tokio::test]
587        async fn fallback_to_another_location() {
588            let mock_downloader = MockFileDownloaderBuilder::default()
589                .with_file_uri("http://example.com/snapshot1")
590                .with_failure()
591                .next_call()
592                .with_file_uri("http://example.com/snapshot2")
593                .with_success()
594                .build();
595            let client = setup_snapshot_client(Arc::new(mock_downloader));
596
597            client
598                .download_unpack_file(
599                    "test-digest",
600                    &[
601                        "http://example.com/snapshot1".to_string(),
602                        "http://example.com/snapshot2".to_string(),
603                    ],
604                    19,
605                    &PathBuf::from("/whatever"),
606                    CompressionAlgorithm::Gzip,
607                    dummy_download_event(),
608                )
609                .await
610                .expect("Should succeed when fallbacking to another location");
611        }
612
613        #[tokio::test]
614        async fn fail_if_location_list_is_empty() {
615            let client = setup_snapshot_client(Arc::new(MockFileDownloader::new()));
616
617            let error = client
618                .download_unpack_file(
619                    "test-digest",
620                    &Vec::new(),
621                    19,
622                    &PathBuf::from("/whatever"),
623                    CompressionAlgorithm::Gzip,
624                    dummy_download_event(),
625                )
626                .await
627                .expect_err("Should fail with empty location list");
628
629            if let Some(SnapshotClientError::NoWorkingLocation { locations, .. }) =
630                error.downcast_ref::<SnapshotClientError>()
631            {
632                assert_eq!(locations, "");
633            } else {
634                panic!("Expected SnapshotClientError::NoWorkingLocation, but got: {error:?}");
635            }
636        }
637    }
638
639    mod download_unpack_full {
640        use super::*;
641
642        #[tokio::test]
643        async fn fail_if_ancillary_verifier_is_not_set() {
644            let snapshot = Snapshot {
645                ancillary_locations: Some(vec!["http://example.com/ancillary".to_string()]),
646                ancillary_size: Some(123),
647                ..Snapshot::dummy()
648            };
649
650            let client = setup_snapshot_client(Arc::new(MockFileDownloader::new()), None);
651
652            let error = client
653                .download_unpack_full(&snapshot, &PathBuf::from("/whatever"))
654                .await
655                .expect_err("Should fail when ancillary verifier is not set");
656
657            assert!(
658                matches!(
659                    error.downcast_ref::<SnapshotClientError>(),
660                    Some(SnapshotClientError::MissingAncillaryVerifier)
661                ),
662                "Expected SnapshotClientError::MissingAncillaryVerifier, but got: {error:#?}"
663            );
664        }
665
666        #[tokio::test]
667        async fn remove_unexpected_downloaded_files_even_if_failing_to_verify_ancillary() {
668            let test_dir = temp_dir_create!();
669            let immutable_dir = test_dir.join(IMMUTABLE_DIR);
670            std::fs::create_dir(&immutable_dir).unwrap();
671            let snapshot = Snapshot::dummy();
672
673            let mut mock_downloader = MockFileDownloader::new();
674            mock_downloader
675                .expect_download_unpack()
676                .returning(move |_, _, _, _, _| {
677                    // Simulate an additional file written mid-download
678                    std::fs::File::create(immutable_dir.join("unexpected.md")).unwrap();
679                    Ok(())
680                });
681
682            let client = setup_snapshot_client(
683                Arc::new(mock_downloader),
684                Some(Arc::new(AncillaryVerifier::new(
685                    fake_keys::manifest_verification_key()[0]
686                        .try_into()
687                        .unwrap(),
688                ))),
689            );
690
691            client
692                .download_unpack_full(&snapshot, &test_dir)
693                .await
694                .unwrap_err();
695            assert_dir_eq!(&test_dir, format!("* {IMMUTABLE_DIR}/"));
696        }
697    }
698
699    mod download_unpack {
700        use super::*;
701
702        #[tokio::test]
703        async fn warn_that_fast_boostrap_is_not_available_without_ancillary_files() {
704            let (logger, log_inspector) = TestLogger::memory();
705            let snapshot = Snapshot::dummy();
706
707            let mut mock_downloader = MockFileDownloader::new();
708            mock_downloader
709                .expect_download_unpack()
710                .returning(|_, _, _, _, _| Ok(()));
711
712            let client = SnapshotClient {
713                logger,
714                ..setup_snapshot_client(Arc::new(mock_downloader), None)
715            };
716
717            let _result = client
718                .download_unpack(&snapshot, &PathBuf::from("/whatever"))
719                .await;
720
721            assert!(
722                log_inspector.contains_log("WARN The fast bootstrap of the Cardano node is not available with the current parameters used in this command: the ledger state will be recomputed from genesis at startup of the Cardano node. Use the extra function download_unpack_full to allow it."),
723                "Expected log message not found, logs: {log_inspector}"
724            );
725        }
726
727        #[tokio::test]
728        async fn remove_unexpected_downloaded_files() {
729            let test_dir = temp_dir_create!();
730            let immutable_dir = test_dir.join(IMMUTABLE_DIR);
731            std::fs::create_dir(&immutable_dir).unwrap();
732            let snapshot = Snapshot::dummy();
733
734            let mut mock_downloader = MockFileDownloader::new();
735            mock_downloader
736                .expect_download_unpack()
737                .returning(move |_, _, _, _, _| {
738                    // Simulate an additional file written mid-download
739                    std::fs::File::create(immutable_dir.join("unexpected.md")).unwrap();
740                    Ok(())
741                });
742
743            let client = setup_snapshot_client(Arc::new(mock_downloader), None);
744
745            client.download_unpack(&snapshot, &test_dir).await.unwrap();
746            assert_dir_eq!(
747                &test_dir,
748                format!("* {IMMUTABLE_DIR}/\n* clean\n * protocolMagicId")
749            );
750        }
751    }
752
753    mod download_unpack_ancillary {
754        use crate::file_downloader::FakeAncillaryFileBuilder;
755
756        use super::*;
757
758        #[tokio::test]
759        async fn log_a_info_message_telling_that_the_feature_does_not_use_mithril_certification() {
760            let (logger, log_inspector) = TestLogger::memory();
761            let verification_key = fake_keys::manifest_verification_key()[0]
762                .try_into()
763                .unwrap();
764            let snapshot = Snapshot {
765                ancillary_locations: None,
766                ancillary_size: None,
767                ..Snapshot::dummy()
768            };
769
770            let client = SnapshotClient {
771                logger,
772                ..setup_snapshot_client(
773                    Arc::new(MockFileDownloader::new()),
774                    Some(Arc::new(AncillaryVerifier::new(verification_key))),
775                )
776            };
777
778            client
779                .download_unpack_ancillary(
780                    &snapshot,
781                    &PathBuf::from("/whatever"),
782                    "test-download-id",
783                )
784                .await
785                .unwrap();
786
787            assert!(
788                log_inspector.contains_log(&format!("WARN {ANCILLARIES_NOT_SIGNED_BY_MITHRIL}")),
789                "Expected log message not found, logs: {log_inspector}"
790            );
791        }
792
793        #[tokio::test]
794        async fn do_nothing_if_no_ancillary_locations_available_in_snapshot() {
795            let verification_key = fake_keys::manifest_verification_key()[0]
796                .try_into()
797                .unwrap();
798            let snapshot = Snapshot {
799                ancillary_locations: None,
800                ancillary_size: None,
801                ..Snapshot::dummy()
802            };
803
804            let client = setup_snapshot_client(
805                Arc::new(MockFileDownloader::new()),
806                Some(Arc::new(AncillaryVerifier::new(verification_key))),
807            );
808
809            client
810                .download_unpack_ancillary(
811                    &snapshot,
812                    &PathBuf::from("/whatever"),
813                    "test-download-id",
814                )
815                .await
816                .expect("Should succeed when no ancillary locations are available");
817        }
818
819        #[tokio::test]
820        async fn delete_temporary_unpack_subfolder_if_download_fail() {
821            let test_dir = temp_dir_create!();
822            let mock_downloader = MockFileDownloaderBuilder::default()
823                .with_file_uri("http://example.com/ancillary")
824                .with_failure()
825                .build();
826            let verification_key = fake_keys::manifest_verification_key()[0]
827                .try_into()
828                .unwrap();
829
830            let client = setup_snapshot_client(
831                Arc::new(mock_downloader),
832                Some(Arc::new(AncillaryVerifier::new(verification_key))),
833            );
834            let snapshot = Snapshot {
835                ancillary_locations: Some(vec!["http://example.com/ancillary".to_string()]),
836                ancillary_size: Some(123),
837                ..Snapshot::dummy()
838            };
839
840            client
841                .download_unpack_ancillary(&snapshot, &test_dir, "test-download-id")
842                .await
843                .unwrap_err();
844
845            assert!(!SnapshotClient::ancillary_subdir(&test_dir, "test-download-id").exists());
846        }
847
848        #[tokio::test]
849        async fn delete_temporary_unpack_subfolder_if_verify_fail() {
850            let test_dir = temp_dir_create!();
851            let mock_downloader = MockFileDownloaderBuilder::default()
852                .with_file_uri("http://example.com/ancillary")
853                .with_success()
854                .build();
855            let verification_key = fake_keys::manifest_verification_key()[0]
856                .try_into()
857                .unwrap();
858
859            let client = setup_snapshot_client(
860                Arc::new(mock_downloader),
861                Some(Arc::new(AncillaryVerifier::new(verification_key))),
862            );
863            let snapshot = Snapshot {
864                ancillary_locations: Some(vec!["http://example.com/ancillary".to_string()]),
865                ancillary_size: Some(123),
866                ..Snapshot::dummy()
867            };
868
869            client
870                .download_unpack_ancillary(&snapshot, &test_dir, "test-download-id")
871                .await
872                .unwrap_err();
873
874            assert!(!SnapshotClient::ancillary_subdir(&test_dir, "test-download-id").exists());
875        }
876
877        #[tokio::test]
878        async fn move_file_in_manifest_then_delete_temporary_unpack_subfolder_if_verify_succeed() {
879            let test_dir = temp_dir_create!();
880            let ancillary_signer = ManifestSigner::create_deterministic_signer();
881            let verification_key = ancillary_signer.verification_key();
882            let mock_downloader = MockFileDownloaderBuilder::default()
883                .with_file_uri("http://example.com/ancillary")
884                .with_success_and_create_fake_ancillary_files(
885                    FakeAncillaryFileBuilder::builder()
886                        .files_in_manifest_to_create(vec!["dummy_ledger".to_string()])
887                        .files_not_in_manifest_to_create(vec!["not_in_ancillary".to_string()])
888                        .sign_manifest(ancillary_signer)
889                        .build(),
890                )
891                .build();
892
893            let client = setup_snapshot_client(
894                Arc::new(mock_downloader),
895                Some(Arc::new(AncillaryVerifier::new(verification_key))),
896            );
897
898            let snapshot = Snapshot {
899                ancillary_locations: Some(vec!["http://example.com/ancillary".to_string()]),
900                ancillary_size: Some(123),
901                ..Snapshot::dummy()
902            };
903            client
904                .download_unpack_ancillary(&snapshot, &test_dir, "test-download-id")
905                .await
906                .expect("Should succeed when ancillary verification is successful");
907
908            assert_dir_eq!(&test_dir, "* dummy_ledger");
909        }
910    }
911}