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