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