1use 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#[derive(Error, Debug)]
130pub enum SnapshotClientError {
131 #[error(
133 "Could not find a working download location for the snapshot digest '{digest}', tried location: {{'{locations}'}}."
134 )]
135 NoWorkingLocation {
136 digest: String,
138
139 locations: String,
141 },
142 #[error(
144 "Ancillary verifier is not set, please use `set_ancillary_verification_key` when creating the client"
145 )]
146 MissingAncillaryVerifier,
147}
148
149pub 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 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 #[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 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 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 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 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 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 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 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 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 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}