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.cardano_database().add_statistics(&snapshot).await.unwrap();
99//! #
100//! #    Ok(())
101//! # }
102//! ```
103
104#[cfg(feature = "fs")]
105use anyhow::Context;
106#[cfg(feature = "fs")]
107use slog::Logger;
108#[cfg(feature = "fs")]
109use std::path::{Path, PathBuf};
110use std::sync::Arc;
111use thiserror::Error;
112
113#[cfg(feature = "fs")]
114use mithril_common::entities::CompressionAlgorithm;
115
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
150#[deprecated(since = "0.12.35", note = "superseded by `CardanoDatabaseClient`")]
151pub struct SnapshotClient {
152    aggregator_requester: Arc<dyn SnapshotAggregatorRequest>,
153    #[cfg(feature = "fs")]
154    http_file_downloader: Arc<dyn FileDownloader>,
155    #[cfg(feature = "fs")]
156    ancillary_verifier: Option<Arc<AncillaryVerifier>>,
157    #[cfg(feature = "fs")]
158    _feedback_sender: FeedbackSender,
159    #[cfg(feature = "fs")]
160    logger: Logger,
161}
162
163/// Define the requests against an aggregator related to Cardano database v1 snapshots.
164#[cfg_attr(test, mockall::automock)]
165#[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
166#[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
167pub trait SnapshotAggregatorRequest: Send + Sync {
168    /// Get the list of the latest Cardano database v1 snapshots from the aggregator.
169    async fn list_latest(&self) -> MithrilResult<Vec<SnapshotListItem>>;
170
171    /// Get the details of a Cardano database v1 snapshot for a given hash from the aggregator.
172    async fn get_by_hash(&self, hash: &str) -> MithrilResult<Option<Snapshot>>;
173
174    /// Notify the aggregator that a Cardano database v1 snapshot has been downloaded.
175    async fn increment_snapshot_downloaded_statistic(
176        &self,
177        snapshot: Snapshot,
178    ) -> MithrilResult<()>;
179}
180
181impl SnapshotClient {
182    /// Constructs a new `SnapshotClient`.
183    pub fn new(
184        aggregator_requester: Arc<dyn SnapshotAggregatorRequest>,
185        #[cfg(feature = "fs")] http_file_downloader: Arc<dyn FileDownloader>,
186        #[cfg(feature = "fs")] ancillary_verifier: Option<Arc<AncillaryVerifier>>,
187        #[cfg(feature = "fs")] feedback_sender: FeedbackSender,
188        #[cfg(feature = "fs")] logger: Logger,
189    ) -> Self {
190        Self {
191            aggregator_requester,
192            #[cfg(feature = "fs")]
193            http_file_downloader,
194            #[cfg(feature = "fs")]
195            ancillary_verifier,
196            // The underscore prefix prevents breaking the `SnapshotClient` API compatibility.
197            #[cfg(feature = "fs")]
198            _feedback_sender: feedback_sender,
199            #[cfg(feature = "fs")]
200            logger: mithril_common::logging::LoggerExtensions::new_with_component_name::<Self>(
201                &logger,
202            ),
203        }
204    }
205
206    /// Return a list of available snapshots
207    pub async fn list(&self) -> MithrilResult<Vec<SnapshotListItem>> {
208        self.aggregator_requester.list_latest().await
209    }
210
211    /// Get the given snapshot data. If it cannot be found, None is returned.
212    pub async fn get(&self, digest: &str) -> MithrilResult<Option<Snapshot>> {
213        self.aggregator_requester.get_by_hash(digest).await
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                match 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                { Err(error) => {
436                    slog::warn!(self.logger, "Failed downloading snapshot from '{location}'"; "error" => ?error);
437                } _ => {
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        self.aggregator_requester
459            .increment_snapshot_downloaded_statistic(snapshot.clone())
460            .await
461    }
462}
463
464#[cfg(all(test, feature = "fs"))]
465mod tests {
466    use crate::{
467        common::CompressionAlgorithm,
468        feedback::MithrilEvent,
469        file_downloader::{MockFileDownloader, MockFileDownloaderBuilder},
470        test_utils::TestLogger,
471    };
472
473    use mithril_cardano_node_internal_database::IMMUTABLE_DIR;
474    use mithril_common::test::double::fake_keys;
475    use mithril_common::{assert_dir_eq, crypto_helper::ManifestSigner, temp_dir_create};
476
477    use crate::common::test::Dummy;
478
479    use super::*;
480
481    fn dummy_download_event() -> DownloadEvent {
482        DownloadEvent::Full {
483            download_id: MithrilEvent::new_snapshot_download_id(),
484            digest: "test-digest".to_string(),
485        }
486    }
487
488    fn setup_snapshot_client(
489        file_downloader: Arc<dyn FileDownloader>,
490        ancillary_verifier: Option<Arc<AncillaryVerifier>>,
491    ) -> SnapshotClient {
492        let aggregator_client = Arc::new(MockSnapshotAggregatorRequest::new());
493        let logger = TestLogger::stdout();
494
495        SnapshotClient::new(
496            aggregator_client,
497            file_downloader,
498            ancillary_verifier,
499            FeedbackSender::new(&[]),
500            logger.clone(),
501        )
502    }
503
504    mod fetch {
505        use mockall::predicate::eq;
506
507        use mithril_common::test::mock_extensions::MockBuilder;
508
509        use super::*;
510
511        #[tokio::test]
512        async fn get_snapshots_list() {
513            let requester = MockBuilder::<MockSnapshotAggregatorRequest>::configure(|mock| {
514                let messages = vec![
515                    SnapshotListItem {
516                        digest: "hash-123".to_string(),
517                        ..Dummy::dummy()
518                    },
519                    SnapshotListItem {
520                        digest: "hash-456".to_string(),
521                        ..Dummy::dummy()
522                    },
523                ];
524                mock.expect_list_latest().return_once(move || Ok(messages));
525            });
526            let client = SnapshotClient::new(
527                requester,
528                Arc::new(MockFileDownloader::new()),
529                None,
530                FeedbackSender::new(&[]),
531                TestLogger::stdout(),
532            );
533
534            let items = client.list().await.unwrap();
535
536            assert_eq!(2, items.len());
537            assert_eq!("hash-123".to_string(), items[0].digest);
538            assert_eq!("hash-456".to_string(), items[1].digest);
539        }
540
541        #[tokio::test]
542        async fn get_snapshot() {
543            let requester = MockBuilder::<MockSnapshotAggregatorRequest>::configure(|mock| {
544                let message = Snapshot {
545                    digest: "hash".to_string(),
546                    ancillary_size: Some(123),
547                    ..Dummy::dummy()
548                };
549                mock.expect_get_by_hash()
550                    .with(eq(message.digest.clone()))
551                    .return_once(move |_| Ok(Some(message)));
552            });
553            let client = SnapshotClient::new(
554                requester,
555                Arc::new(MockFileDownloader::new()),
556                None,
557                FeedbackSender::new(&[]),
558                TestLogger::stdout(),
559            );
560
561            let snapshot = client.get("hash").await.unwrap().expect("should return a snapshot");
562
563            assert_eq!("hash", &snapshot.digest);
564            assert_eq!(Some(123), snapshot.ancillary_size);
565        }
566    }
567
568    mod download_unpack_file {
569        use super::*;
570
571        fn setup_snapshot_client(file_downloader: Arc<dyn FileDownloader>) -> SnapshotClient {
572            super::setup_snapshot_client(file_downloader, None)
573        }
574
575        #[tokio::test]
576        async fn log_warning_if_location_fails() {
577            let (logger, log_inspector) = TestLogger::memory();
578            let mock_downloader = MockFileDownloaderBuilder::default()
579                .with_file_uri("http://whatever.co/snapshot")
580                .with_failure()
581                .build();
582
583            let client = SnapshotClient {
584                logger,
585                ..setup_snapshot_client(Arc::new(mock_downloader))
586            };
587
588            let _result = client
589                .download_unpack_file(
590                    "test-digest",
591                    &["http://whatever.co/snapshot".to_string()],
592                    19,
593                    &PathBuf::from("/whatever"),
594                    CompressionAlgorithm::Gzip,
595                    dummy_download_event(),
596                )
597                .await;
598
599            assert!(log_inspector.contains_log("Failed downloading snapshot"));
600        }
601
602        #[tokio::test]
603        async fn error_contains_list_of_all_tried_locations_if_all_attempts_fails() {
604            let test_locations = vec![
605                "http://example.com/snapshot1".to_string(),
606                "http://example.com/snapshot2".to_string(),
607            ];
608            let mock_downloader = MockFileDownloaderBuilder::default()
609                .with_file_uri("http://example.com/snapshot1")
610                .with_failure()
611                .next_call()
612                .with_file_uri("http://example.com/snapshot2")
613                .with_failure()
614                .build();
615            let client = setup_snapshot_client(Arc::new(mock_downloader));
616
617            let error = client
618                .download_unpack_file(
619                    "test-digest",
620                    &test_locations,
621                    19,
622                    &PathBuf::from("/whatever"),
623                    CompressionAlgorithm::Gzip,
624                    dummy_download_event(),
625                )
626                .await
627                .expect_err("Should fail when all locations fail");
628
629            if let Some(SnapshotClientError::NoWorkingLocation { digest, locations }) =
630                error.downcast_ref::<SnapshotClientError>()
631            {
632                assert_eq!(digest, "test-digest");
633                assert_eq!(
634                    locations,
635                    "http://example.com/snapshot1, http://example.com/snapshot2"
636                );
637            } else {
638                panic!("Expected SnapshotClientError::NoWorkingLocation, but got: {error:?}");
639            }
640        }
641
642        #[tokio::test]
643        async fn fallback_to_another_location() {
644            let mock_downloader = MockFileDownloaderBuilder::default()
645                .with_file_uri("http://example.com/snapshot1")
646                .with_failure()
647                .next_call()
648                .with_file_uri("http://example.com/snapshot2")
649                .with_success()
650                .build();
651            let client = setup_snapshot_client(Arc::new(mock_downloader));
652
653            client
654                .download_unpack_file(
655                    "test-digest",
656                    &[
657                        "http://example.com/snapshot1".to_string(),
658                        "http://example.com/snapshot2".to_string(),
659                    ],
660                    19,
661                    &PathBuf::from("/whatever"),
662                    CompressionAlgorithm::Gzip,
663                    dummy_download_event(),
664                )
665                .await
666                .expect("Should succeed when fallbacking to another location");
667        }
668
669        #[tokio::test]
670        async fn fail_if_location_list_is_empty() {
671            let client = setup_snapshot_client(Arc::new(MockFileDownloader::new()));
672
673            let error = client
674                .download_unpack_file(
675                    "test-digest",
676                    &Vec::new(),
677                    19,
678                    &PathBuf::from("/whatever"),
679                    CompressionAlgorithm::Gzip,
680                    dummy_download_event(),
681                )
682                .await
683                .expect_err("Should fail with empty location list");
684
685            if let Some(SnapshotClientError::NoWorkingLocation { locations, .. }) =
686                error.downcast_ref::<SnapshotClientError>()
687            {
688                assert_eq!(locations, "");
689            } else {
690                panic!("Expected SnapshotClientError::NoWorkingLocation, but got: {error:?}");
691            }
692        }
693    }
694
695    mod download_unpack_full {
696        use super::*;
697
698        #[tokio::test]
699        async fn fail_if_ancillary_verifier_is_not_set() {
700            let snapshot = Snapshot {
701                ancillary_locations: Some(vec!["http://example.com/ancillary".to_string()]),
702                ancillary_size: Some(123),
703                ..Snapshot::dummy()
704            };
705
706            let client = setup_snapshot_client(Arc::new(MockFileDownloader::new()), None);
707
708            let error = client
709                .download_unpack_full(&snapshot, &PathBuf::from("/whatever"))
710                .await
711                .expect_err("Should fail when ancillary verifier is not set");
712
713            assert!(
714                matches!(
715                    error.downcast_ref::<SnapshotClientError>(),
716                    Some(SnapshotClientError::MissingAncillaryVerifier)
717                ),
718                "Expected SnapshotClientError::MissingAncillaryVerifier, but got: {error:#?}"
719            );
720        }
721
722        #[tokio::test]
723        async fn remove_unexpected_downloaded_files_even_if_failing_to_verify_ancillary() {
724            let test_dir = temp_dir_create!();
725            let immutable_dir = test_dir.join(IMMUTABLE_DIR);
726            std::fs::create_dir(&immutable_dir).unwrap();
727            let snapshot = Snapshot::dummy();
728
729            let mut mock_downloader = MockFileDownloader::new();
730            mock_downloader
731                .expect_download_unpack()
732                .returning(move |_, _, _, _, _| {
733                    // Simulate an additional file written mid-download
734                    std::fs::File::create(immutable_dir.join("unexpected.md")).unwrap();
735                    Ok(())
736                });
737
738            let client = setup_snapshot_client(
739                Arc::new(mock_downloader),
740                Some(Arc::new(AncillaryVerifier::new(
741                    fake_keys::manifest_verification_key()[0].try_into().unwrap(),
742                ))),
743            );
744
745            client.download_unpack_full(&snapshot, &test_dir).await.unwrap_err();
746            assert_dir_eq!(&test_dir, format!("* {IMMUTABLE_DIR}/"));
747        }
748    }
749
750    mod download_unpack {
751        use super::*;
752
753        #[tokio::test]
754        async fn warn_that_fast_boostrap_is_not_available_without_ancillary_files() {
755            let (logger, log_inspector) = TestLogger::memory();
756            let snapshot = Snapshot::dummy();
757
758            let mut mock_downloader = MockFileDownloader::new();
759            mock_downloader
760                .expect_download_unpack()
761                .returning(|_, _, _, _, _| Ok(()));
762
763            let client = SnapshotClient {
764                logger,
765                ..setup_snapshot_client(Arc::new(mock_downloader), None)
766            };
767
768            let _result = client.download_unpack(&snapshot, &PathBuf::from("/whatever")).await;
769
770            assert!(
771                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."),
772            );
773        }
774
775        #[tokio::test]
776        async fn remove_unexpected_downloaded_files() {
777            let test_dir = temp_dir_create!();
778            let immutable_dir = test_dir.join(IMMUTABLE_DIR);
779            std::fs::create_dir(&immutable_dir).unwrap();
780            let snapshot = Snapshot::dummy();
781
782            let mut mock_downloader = MockFileDownloader::new();
783            mock_downloader
784                .expect_download_unpack()
785                .returning(move |_, _, _, _, _| {
786                    // Simulate an additional file written mid-download
787                    std::fs::File::create(immutable_dir.join("unexpected.md")).unwrap();
788                    Ok(())
789                });
790
791            let client = setup_snapshot_client(Arc::new(mock_downloader), None);
792
793            client.download_unpack(&snapshot, &test_dir).await.unwrap();
794            assert_dir_eq!(
795                &test_dir,
796                format!("* {IMMUTABLE_DIR}/\n* clean\n * protocolMagicId")
797            );
798        }
799    }
800
801    mod download_unpack_ancillary {
802        use crate::file_downloader::FakeAncillaryFileBuilder;
803
804        use super::*;
805
806        #[tokio::test]
807        async fn log_a_info_message_telling_that_the_feature_does_not_use_mithril_certification() {
808            let (logger, log_inspector) = TestLogger::memory();
809            let verification_key = fake_keys::manifest_verification_key()[0].try_into().unwrap();
810            let snapshot = Snapshot {
811                ancillary_locations: None,
812                ancillary_size: None,
813                ..Snapshot::dummy()
814            };
815
816            let client = SnapshotClient {
817                logger,
818                ..setup_snapshot_client(
819                    Arc::new(MockFileDownloader::new()),
820                    Some(Arc::new(AncillaryVerifier::new(verification_key))),
821                )
822            };
823
824            client
825                .download_unpack_ancillary(
826                    &snapshot,
827                    &PathBuf::from("/whatever"),
828                    "test-download-id",
829                )
830                .await
831                .unwrap();
832
833            assert!(
834                log_inspector.contains_log(&format!("WARN {ANCILLARIES_NOT_SIGNED_BY_MITHRIL}")),
835            );
836        }
837
838        #[tokio::test]
839        async fn do_nothing_if_no_ancillary_locations_available_in_snapshot() {
840            let verification_key = fake_keys::manifest_verification_key()[0].try_into().unwrap();
841            let snapshot = Snapshot {
842                ancillary_locations: None,
843                ancillary_size: None,
844                ..Snapshot::dummy()
845            };
846
847            let client = setup_snapshot_client(
848                Arc::new(MockFileDownloader::new()),
849                Some(Arc::new(AncillaryVerifier::new(verification_key))),
850            );
851
852            client
853                .download_unpack_ancillary(
854                    &snapshot,
855                    &PathBuf::from("/whatever"),
856                    "test-download-id",
857                )
858                .await
859                .expect("Should succeed when no ancillary locations are available");
860        }
861
862        #[tokio::test]
863        async fn delete_temporary_unpack_subfolder_if_download_fail() {
864            let test_dir = temp_dir_create!();
865            let mock_downloader = MockFileDownloaderBuilder::default()
866                .with_file_uri("http://example.com/ancillary")
867                .with_failure()
868                .build();
869            let verification_key = fake_keys::manifest_verification_key()[0].try_into().unwrap();
870
871            let client = setup_snapshot_client(
872                Arc::new(mock_downloader),
873                Some(Arc::new(AncillaryVerifier::new(verification_key))),
874            );
875            let snapshot = Snapshot {
876                ancillary_locations: Some(vec!["http://example.com/ancillary".to_string()]),
877                ancillary_size: Some(123),
878                ..Snapshot::dummy()
879            };
880
881            client
882                .download_unpack_ancillary(&snapshot, &test_dir, "test-download-id")
883                .await
884                .unwrap_err();
885
886            assert!(!SnapshotClient::ancillary_subdir(&test_dir, "test-download-id").exists());
887        }
888
889        #[tokio::test]
890        async fn delete_temporary_unpack_subfolder_if_verify_fail() {
891            let test_dir = temp_dir_create!();
892            let mock_downloader = MockFileDownloaderBuilder::default()
893                .with_file_uri("http://example.com/ancillary")
894                .with_success()
895                .build();
896            let verification_key = fake_keys::manifest_verification_key()[0].try_into().unwrap();
897
898            let client = setup_snapshot_client(
899                Arc::new(mock_downloader),
900                Some(Arc::new(AncillaryVerifier::new(verification_key))),
901            );
902            let snapshot = Snapshot {
903                ancillary_locations: Some(vec!["http://example.com/ancillary".to_string()]),
904                ancillary_size: Some(123),
905                ..Snapshot::dummy()
906            };
907
908            client
909                .download_unpack_ancillary(&snapshot, &test_dir, "test-download-id")
910                .await
911                .unwrap_err();
912
913            assert!(!SnapshotClient::ancillary_subdir(&test_dir, "test-download-id").exists());
914        }
915
916        #[tokio::test]
917        async fn move_file_in_manifest_then_delete_temporary_unpack_subfolder_if_verify_succeed() {
918            let test_dir = temp_dir_create!();
919            let ancillary_signer = ManifestSigner::create_deterministic_signer();
920            let verification_key = ancillary_signer.verification_key();
921            let mock_downloader = MockFileDownloaderBuilder::default()
922                .with_file_uri("http://example.com/ancillary")
923                .with_success_and_create_fake_ancillary_files(
924                    FakeAncillaryFileBuilder::builder()
925                        .files_in_manifest_to_create(vec!["dummy_ledger".to_string()])
926                        .files_not_in_manifest_to_create(vec!["not_in_ancillary".to_string()])
927                        .sign_manifest(ancillary_signer)
928                        .build(),
929                )
930                .build();
931
932            let client = setup_snapshot_client(
933                Arc::new(mock_downloader),
934                Some(Arc::new(AncillaryVerifier::new(verification_key))),
935            );
936
937            let snapshot = Snapshot {
938                ancillary_locations: Some(vec!["http://example.com/ancillary".to_string()]),
939                ancillary_size: Some(123),
940                ..Snapshot::dummy()
941            };
942            client
943                .download_unpack_ancillary(&snapshot, &test_dir, "test-download-id")
944                .await
945                .expect("Should succeed when ancillary verification is successful");
946
947            assert_dir_eq!(&test_dir, "* dummy_ledger");
948        }
949    }
950}